├── Block
├── Adminhtml
│ └── Form
│ │ └── Field
│ │ └── Discounts.php
├── AllInstallments.php
└── Script.php
├── Helper
└── Data.php
├── LICENSE
├── Model
└── Config
│ ├── ConfigProvider.php
│ ├── Path.php
│ └── Source
│ ├── InterestType.php
│ └── MaximumQuantityInstallments.php
├── README.md
├── composer.json
├── etc
├── acl.xml
├── adminhtml
│ └── system.xml
├── config.xml
└── module.xml
├── i18n
└── pt_BR.csv
├── registration.php
└── view
└── frontend
├── layout
├── catalog_product_view.xml
└── default.xml
├── requirejs-config.js
├── templates
├── all_installments.phtml
└── script.phtml
└── web
├── css
└── source
│ └── _module.less
└── js
├── installment.js
└── price-box-mixin.js
/Block/Adminhtml/Form/Field/Discounts.php:
--------------------------------------------------------------------------------
1 | addColumn(
16 | 'name',
17 | [
18 | 'label' => __('Name'),
19 | 'class' => 'required-entry'
20 | ]
21 | );
22 | $this->addColumn(
23 | 'percentage',
24 | [
25 | 'label' => __('Percentage'),
26 | 'class' => 'required-entry validate-currency-dollar'
27 | ]
28 | );
29 | $this->_addAfter = false;
30 | $this->_addButtonLabel = __('Add');
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Block/AllInstallments.php:
--------------------------------------------------------------------------------
1 | _helperData = $HelperData;
56 | parent::__construct(
57 | $context,
58 | $urlEncoder,
59 | $jsonEncoder,
60 | $stringUtils,
61 | $productHelper,
62 | $productTypeConfig,
63 | $localeFormat,
64 | $customerSession,
65 | $productRepository,
66 | $priceCurrency,
67 | $data
68 | );
69 | }
70 |
71 | /**
72 | * @return bool
73 | */
74 | public function renderAllInstallments(): bool
75 | {
76 | return $this->_helperData->showAllInstallments($this->getProduct());
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Block/Script.php:
--------------------------------------------------------------------------------
1 | _helperData = $HelperData;
29 | parent::__construct($Context, $Data);
30 | }
31 |
32 | /**
33 | * @return string
34 | */
35 | public function getConfig(): string
36 | {
37 | return json_encode($this->_helperData->getConfig());
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Helper/Data.php:
--------------------------------------------------------------------------------
1 | _configProvider = $ConfigProvider;
36 | $this->_priceCurrency = $PriceCurrency;
37 | }
38 |
39 | /**
40 | * @return array
41 | */
42 | public function getConfig(): array
43 | {
44 | $config = [];
45 | $config['enabled'] = $this->_configProvider->isModuleEnable();
46 | $config['interest_type'] = $this->_configProvider->getInterestType();
47 | $config['maximum_quantity_installments'] = $this->_configProvider->getMaximumQuantityInstallments();
48 | $config['minimum_installment_value'] = $this->_configProvider->getMinimumInstallmentValue();
49 | $config['discounts'] = $this->_configProvider->getDiscounts();
50 | $config['best_installment_in_cart'] = $this->_configProvider->bestInstallmentInCart();
51 | $config['interest'] = [];
52 | $config['currency_symbol'] = $this->_priceCurrency->getCurrencySymbol();
53 | $config['show_first_installment'] = $this->_configProvider->showFirstInstallment();
54 | $config['templates'] = [
55 | 'catalog_category_view' => $this->_configProvider->getPriceTemplate('catalog_category_view'),
56 | 'catalogsearch_result_index' => $this->_configProvider->getPriceTemplate('catalogsearch_result_index'),
57 | 'catalog_product_view' => $this->_configProvider->getPriceTemplate('catalog_product_view'),
58 | 'discount_template' => $this->_configProvider->getPriceTemplate('discount_template'),
59 | 'all_installment_template' => $this->_configProvider->getPriceTemplate('all_installment_template'),
60 | 'in_cart_template' => $this->_configProvider->getPriceTemplate('in_cart_template'),
61 | 'text_free_interest' => $this->_configProvider->getPriceTemplate('text_free_interest'),
62 | 'text_with_interest' => $this->_configProvider->getPriceTemplate('text_with_interest')
63 | ];
64 |
65 | for ($i = 1; $i < 13; $i++) {
66 | $config['interest'][$i] = (double)$this->_configProvider->getInterestRate($i);
67 | }
68 |
69 | return $config;
70 | }
71 |
72 | /**
73 | * @param $product
74 | * @return bool
75 | */
76 | public function showAllInstallments($product): bool
77 | {
78 | if (!$this->_configProvider->isModuleEnable() || !$this->_configProvider->showAllInstallments()) {
79 | return false;
80 | }
81 |
82 | if (!$product) {
83 | $this->_logger->alert(__('Missing product parameter on showAllInstallments()'));
84 | return false;
85 | }
86 |
87 | $productPrice = $this->getProductPrice($product);
88 | $minInstallment = $this->_configProvider->getMinimumInstallmentValue();
89 |
90 | if (($productPrice < $minInstallment)) {
91 | return false;
92 | }
93 |
94 | return true;
95 | }
96 |
97 | /**
98 | * @param $product
99 | * @return float
100 | */
101 | private function getProductPrice($product): float
102 | {
103 | return (float)($product->getMinimalPrice()) ? $product->getMinimalPrice() : $product->getPriceInfo()->getPrice('final_price')->getAmount()->getValue();
104 | }
105 |
106 | /**
107 | * @return array
108 | */
109 | public function getStyles(): array
110 | {
111 | return $this->_configProvider->getStyles();
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Backendorf
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Model/Config/ConfigProvider.php:
--------------------------------------------------------------------------------
1 | _configPath = $ConfigPath;
46 | $this->_scopeConfig = $ScopeConfig;
47 | $this->_unserialize = $Unserialize;
48 | $this->_serializerJson = $SerializerJson;
49 | }
50 |
51 | /**
52 | * @return bool
53 | */
54 | public function isModuleEnable(): bool
55 | {
56 | return $this->_scopeConfig->isSetFlag($this->_configPath::MODULE_ENABLE);
57 | }
58 |
59 | /**
60 | * @return int
61 | */
62 | public function getMaximumQuantityInstallments(): int
63 | {
64 | return (int)$this->_scopeConfig->getValue($this->_configPath::MAXIMUM_QUANTITY_INSTALLMENTS);
65 | }
66 |
67 | /**
68 | * @return float
69 | */
70 | public function getMinimumInstallmentValue(): float
71 | {
72 | return (float)$this->_scopeConfig->getValue($this->_configPath::MINIMUM_INSTALLMENT_VALUE);
73 | }
74 |
75 | /**
76 | * @return bool
77 | */
78 | public function showAllInstallments(): bool
79 | {
80 | return $this->_scopeConfig->isSetFlag($this->_configPath::SHOW_ALL_INSTALLMENTS_ON_PRODUCT_PAGE);
81 | }
82 |
83 | /**
84 | * @return string
85 | */
86 | public function getInterestType(): string
87 | {
88 | return (string)$this->_scopeConfig->getValue($this->_configPath::INTEREST_TYPE);
89 | }
90 |
91 | /**
92 | * @param int $x
93 | * @return float|null
94 | */
95 | public function getInterestRate(int $x): ?float
96 | {
97 | return (float)$this->_scopeConfig->getValue($this->_configPath::INSTALLMENT_INTEREST_X . 'interest_' . $x);
98 | }
99 |
100 | /**
101 | * @return array
102 | */
103 | public function getDiscounts(): array
104 | {
105 | $value = $this->_scopeConfig->getValue($this->_configPath::DISCOUNTS);
106 |
107 | if (empty($value)) {
108 | return [];
109 | }
110 |
111 | if ($this->isSerialized($value)) {
112 | $unserializer = $this->_unserialize;
113 | } else {
114 | $unserializer = $this->_serializerJson;
115 | }
116 |
117 | return $unserializer->unserialize($value);
118 | }
119 |
120 | /**
121 | * @param $value
122 | * @return bool
123 | */
124 | private function isSerialized($value): bool
125 | {
126 | return (boolean)preg_match('/^((s|i|d|b|a|O|C):|N;)/', $value);
127 | }
128 |
129 | /**
130 | * @return bool
131 | */
132 | public function showFirstInstallment(): bool
133 | {
134 | return $this->_scopeConfig->isSetFlag($this->_configPath::SHOW_FIRST_INSTALLMENT);
135 | }
136 |
137 | /**
138 | * @return bool
139 | */
140 | public function bestInstallmentInCart(): bool
141 | {
142 | return $this->_scopeConfig->isSetFlag($this->_configPath::BEST_INSTALLMENT_IN_CART);
143 | }
144 |
145 | /**
146 | * @param string $page
147 | * @return string
148 | */
149 | public function getPriceTemplate(string $page): string
150 | {
151 | return (string)$this->_scopeConfig->getValue(str_replace('{{page}}', $page, $this->_configPath::PRICE_TEMPLATE));
152 | }
153 |
154 | /**
155 | * @return array
156 | */
157 | public function getStyles(): array
158 | {
159 | return [
160 | 'primary-color' => $this->_scopeConfig->getValue($this->_configPath::PRIMARY_COLOR),
161 | 'highlight-text-color' => $this->_scopeConfig->getValue($this->_configPath::HIGHLIGHT_TEXT_COLOR),
162 | 'highlight-text-font-weight' => $this->_scopeConfig->getValue($this->_configPath::HIGLIGHT_TEXT_FONT_WEIGHT)
163 | ];
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/Model/Config/Path.php:
--------------------------------------------------------------------------------
1 | 'simple',
18 | 'label' => __('Simple')
19 | ],
20 | [
21 | 'value' => 'compound',
22 | 'label' => __('Compound')
23 | ]
24 | ];
25 | }
26 |
27 | /**
28 | * @return array
29 | */
30 | public function toArray(): array
31 | {
32 | return [
33 | 'simples' => __('Simple'),
34 | 'compound' => __('Compound')
35 | ];
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Model/Config/Source/MaximumQuantityInstallments.php:
--------------------------------------------------------------------------------
1 | '1',
19 | 'label' => __('1')
20 | ],
21 | [
22 | 'value' => '2',
23 | 'label' => __('2')
24 | ],
25 | [
26 | 'value' => '3',
27 | 'label' => __('3')
28 | ],
29 | [
30 | 'value' => '4',
31 | 'label' => __('4')
32 | ],
33 | [
34 | 'value' => '5',
35 | 'label' => __('5')
36 | ],
37 | [
38 | 'value' => '6',
39 | 'label' => __('6')
40 | ],
41 | [
42 | 'value' => '7',
43 | 'label' => __('7')
44 | ],
45 | [
46 | 'value' => '8',
47 | 'label' => __('8')
48 | ],
49 | [
50 | 'value' => '9',
51 | 'label' => __('9')
52 | ],
53 | [
54 | 'value' => '10',
55 | 'label' => __('10')
56 | ],
57 | [
58 | 'value' => '11',
59 | 'label' => __('11')
60 | ],
61 | [
62 | 'value' => '12',
63 | 'label' => __('12')
64 | ]
65 | ];
66 | }
67 |
68 | /**
69 | * @return array
70 | */
71 | public function toArray(): array
72 | {
73 | return [
74 | '1' => __('1'),
75 | '2' => __('2'),
76 | '3' => __('3'),
77 | '4' => __('4'),
78 | '5' => __('5'),
79 | '6' => __('6'),
80 | '7' => __('7'),
81 | '8' => __('8'),
82 | '9' => __('9'),
83 | '10' => __('10'),
84 | '11' => __('11'),
85 | '12' => __('12')
86 | ];
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Description
2 | Basically, this module makes it possible to display the installment values and discounts on the frontend.
3 |
4 | ## Installation
5 | > - `composer require backendorf/module-installment`
6 | > - `bin/magento setup:upgrade && bin/magento setup:di:compile`
7 | > - Navigate to "Shop->Configuration->Backendorf->Installment" and configure the options.
8 | > - Clear the cache.
9 |
10 | ## Prerequisites
11 | > - The module is compatible with Magento 2.x.x
12 |
13 | ## Dependencies
14 | > - Magento_Catalog
15 |
16 | # Features
17 | > - Maximum number of installments;
18 | > - Minimum installment value;
19 | > - Show all installment options on the product page;
20 | > - Show first installment;
21 | > - Interest calculation type (simple or compound);
22 | > - Percentage of interest on each installment (up to 12th);
23 | > - Show the lowest parcel in the cart;
24 | > - Multiple discounts;
25 | > - Configure price templates for the main catalog pages;
26 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backendorf/module-installment",
3 | "description": "Add installment feature to Magento 2",
4 | "type": "magento2-module",
5 | "minimum-stability": "dev",
6 | "require": {},
7 | "autoload": {
8 | "psr-4": {
9 | "Backendorf\\Installment\\": ""
10 | },
11 | "files": [
12 | "registration.php"
13 | ]
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/etc/acl.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/etc/adminhtml/system.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 | Backendorf
7 |
8 |
10 | Installment
11 | backendorf
12 | Backendorf_Installment::config_backendorf_installment
13 |
14 | General
15 |
17 | Enabled
18 |
19 | Magento\Config\Model\Config\Source\Yesno
20 |
21 |
23 | Maximum quantity installments
24 |
25 | Backendorf\Installment\Model\Config\Source\MaximumQuantityInstallments
26 |
27 |
29 | Minimum installment value
30 | required-entry validate-currency-dollar
31 |
32 |
33 |
35 | Show all installments on product page
36 |
37 | Magento\Config\Model\Config\Source\Yesno
38 |
39 |
41 | Show the first installment
42 |
43 | Magento\Config\Model\Config\Source\Yesno
44 |
45 |
47 | Best installment in cart
48 |
49 | Magento\Config\Model\Config\Source\Yesno
50 |
51 |
52 |
53 | Interest
54 |
56 | Interest type
57 |
58 | Backendorf\Installment\Model\Config\Source\InterestType
59 |
60 |
62 | 2x
63 | required-entry validate-currency-dollar
64 |
65 |
66 |
68 | 3x
69 | required-entry validate-currency-dollar
70 |
71 |
72 |
74 | 4x
75 | required-entry validate-currency-dollar
76 |
77 |
78 |
80 | 5x
81 | required-entry validate-currency-dollar
82 |
83 |
84 |
86 | 6x
87 | required-entry validate-currency-dollar
88 |
89 |
90 |
92 | 7x
93 | required-entry validate-currency-dollar
94 |
95 |
96 |
98 | 8x
99 | required-entry validate-currency-dollar
100 |
101 |
102 |
104 | 9x
105 | required-entry validate-currency-dollar
106 |
107 |
108 |
110 | 10x
111 | required-entry validate-currency-dollar
112 |
113 |
114 |
116 | 11x
117 | required-entry validate-currency-dollar
118 |
119 |
120 |
122 | 12x
123 | required-entry validate-currency-dollar
124 |
125 |
126 |
127 |
128 | Discounts
129 |
131 | Discount
132 | Backendorf\Installment\Block\Adminhtml\Form\Field\Discounts
133 | Magento\Config\Model\Config\Backend\Serialized\ArraySerialized
134 |
135 |
136 |
138 | Price Templates
139 |
141 | On Category Page
142 | required-entry
143 |
144 |
145 |
147 | On Search Results
148 | required-entry
149 |
150 |
151 |
153 | On Product View
154 | required-entry
155 |
156 |
157 |
159 | Discount Template
160 | required-entry
161 |
162 |
163 |
165 | In Cart Template
166 | required-entry
167 |
168 |
169 |
171 | All Installment Template
172 | required-entry
173 |
174 |
175 |
177 | Text with interest
178 | required-entry
179 |
180 |
181 |
183 | Text free interest
184 | required-entry
185 |
186 |
187 |
188 |
190 | Styles
191 |
193 | Primary color
194 | required-entry
195 |
196 |
198 | Highlight text color
199 | required-entry
200 |
201 |
202 |
204 | Highlight text font weight
205 | required-entry
206 |
207 |
208 |
209 |
210 |
211 |
--------------------------------------------------------------------------------
/etc/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 | 1
8 | 12
9 | 5
10 | 1
11 | 0
12 | 1
13 |
14 |
15 |
16 | 0
17 | 0
18 | 0
19 | 0
20 | 0
21 | 0
22 | 0
23 | 0
24 | 0
25 | 0
26 | 0
27 |
28 |
29 |
30 |
31 | {{qty}}X de {{value}} {{interest}} {{discounts}}]]>
32 |
33 | {{qty}}X de {{value}} {{interest}} {{discounts}}]]>
34 |
35 | {{qty}}X de {{value}} {{interest}} {{discounts}}]]>
36 |
37 | {{valueWithDiscount}} {{percentage}} no {{name}}]]>
38 |
39 | {{qty}}X de {{value}} {{interest}}]]>
40 |
41 | {{qty}}x de {{value}} {{interest}}]]>
42 |
43 | (taxa de {{rate}} = {{total_interest}}, totalizando {{amount}})]]>
44 | sem juros]]>
45 |
46 |
47 | #1979c3
48 | #1979c3
49 | 700
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/etc/module.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/i18n/pt_BR.csv:
--------------------------------------------------------------------------------
1 | Name,Nome
2 | Percentage,Percentual
3 | Add,Adicionar
4 | "Missing product parameter on showAllInstallments()","Missing product parameter on showAllInstallments()"
5 | Simple,Simples
6 | Compound,Composto
7 | 1,1
8 | 2,2
9 | 3,3
10 | 4,4
11 | 5,5
12 | 6,6
13 | 7,7
14 | 8,8
15 | 9,9
16 | 10,10
17 | 11,11
18 | 12,12
19 | "More installment options","Mais opções de parcelamento"
20 | Calculating...,Calculando...
21 | "The values presented are for consultation only, the real value of the parcel will be displayed at the end of the order.","Os valores apresentados são apenas para consulta, o valor real da parcela será exibido no fechamento do pedido."
22 | Backendorf,Backendorf
23 | Installment,Preços adicionais
24 | General,Geral
25 | Enabled,Habilitado
26 | "Maximum quantity installments","Quantidade máxima de parcelas"
27 | "Minimum installment value","Valor mínimo da parcela"
28 | "Show all installments on product page","Mostrar todas as opções de parcelamento na página de produto"
29 | "Show the first installment","Mostrar a primeira parcela"
30 | "Best installment in cart","Mostrar a parcela mais baixa no carrinho"
31 | Interest,Juros
32 | "Interest type","Tipo de juros"
33 | 2x,2x
34 | 3x,3x
35 | 4x,4x
36 | 5x,5x
37 | 6x,6x
38 | 7x,7x
39 | 8x,8x
40 | 9x,9x
41 | 10x,10x
42 | 11x,11x
43 | 12x,12x
44 | Discounts,Descontos
45 | Discount,Desconto
46 | "Price Templates","Templates de preço"
47 | "On Category Page","Na página de category"
48 | "On Search Results","Nos resultados de busca"
49 | "On Product View","Na página do produto"
50 | "Discount Template","Template de preço para descontos"
51 | "In Cart Template","Template de preço no carrinho"
52 | "All Installment Template","Template de preço para todas as opções de parcelamento"
53 | "Text with interest","Texto com juros"
54 | "Text free interest","Texto sem juros"
55 | Styles,Estilos
56 | "Primary color","Cor primária"
57 | "Highlight text color","Cor dos textos em destaque"
58 | "Highlight text font weight","Peso da fonte dos textos em destaque"
59 |
--------------------------------------------------------------------------------
/registration.php:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/view/frontend/layout/default.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/view/frontend/requirejs-config.js:
--------------------------------------------------------------------------------
1 | var config = {
2 | map: {
3 | '*': {
4 | installment: 'Backendorf_Installment/js/installment'
5 | }
6 | },
7 | config: {
8 | mixins: {
9 | 'Magento_Catalog/js/price-box': {
10 | 'Backendorf_Installment/js/price-box-mixin': true
11 | }
12 | }
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/view/frontend/templates/all_installments.phtml:
--------------------------------------------------------------------------------
1 | renderAllInstallments()) {
7 | ?>
8 |
16 |
17 |
18 |
= __('More installment options'); ?>
19 |
20 |
21 |
27 |
28 |
31 |
--------------------------------------------------------------------------------
/view/frontend/templates/script.phtml:
--------------------------------------------------------------------------------
1 | helper(Data::class)->getStyles();
9 | ?>
10 |
17 |
24 |
--------------------------------------------------------------------------------
/view/frontend/web/css/source/_module.less:
--------------------------------------------------------------------------------
1 | .installments {
2 | .best-installment {
3 | font-size: 14px;
4 | line-height: 16px;
5 | font-weight: 400;
6 | margin: 10px 0;
7 |
8 | strong {
9 | color: var(--bi-highlight-text-color);
10 | font-weight: var(--bi-highlight-text-font-weight);
11 | }
12 | }
13 |
14 | .discounts ul {
15 | list-style: none;
16 | padding: 0;
17 |
18 | li.item {
19 | strong {
20 | color: var(--bi-highlight-text-color);
21 | font-weight: var(--bi-highlight-text-font-weight);
22 | }
23 | }
24 | }
25 | }
26 |
27 | #installments-accordion {
28 | [data-role="collapsible"].active [data-role="trigger"] {
29 | border-radius: 4px 4px 0 0;
30 | }
31 |
32 | [data-role="trigger"] {
33 | display: block;
34 | background-color: var(--bi-primary-color);
35 | border-radius: 4px;
36 | color: @color-white;
37 | cursor: pointer;
38 | text-align: center;
39 | text-transform: uppercase;
40 | padding: 16px 32px;
41 |
42 | h5 {
43 | margin: 0;
44 | }
45 | }
46 |
47 | [data-role="content"] {
48 | border: 1px solid @color-gray-light5;
49 | padding: 16px;
50 | border-radius: 0 0 4px 4px;
51 |
52 | ul.options {
53 | list-style: none;
54 | padding: 0;
55 | font-size: 14px;
56 | line-height: 16px;
57 |
58 | li {
59 | &:not(:last-child) {
60 | margin-bottom: 10px;
61 | }
62 |
63 | strong {
64 | color: var(--bi-highlight-text-color);
65 | font-weight: var(--bi-highlight-text-font-weight);
66 | }
67 | }
68 | }
69 |
70 | .footer-message {
71 | color: @color-gray-light3;
72 | font-size: 10px;
73 | line-height: 12px;
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/view/frontend/web/js/installment.js:
--------------------------------------------------------------------------------
1 | define([
2 | "jquery",
3 | 'Magento_Catalog/js/price-utils',
4 | 'mage/translate',
5 | 'Magento_Customer/js/customer-data'
6 | ], function ($, priceUtils, $t, customerData) {
7 | 'use strict';
8 | $.widget('mage.installment', {
9 | 'options': {
10 | 'discounts': [],
11 | 'interest_type': null,
12 | 'minimum_installment_value': null,
13 | 'maximum_quantity_installments': null,
14 | 'interest': null,
15 | 'best_installment_in_cart': null,
16 | 'currency_symbol': '',
17 | 'show_first_installment': false,
18 | 'templates': {
19 | 'in_cart_template': null,
20 | 'discount_template': null,
21 | 'catalog_category_view': null,
22 | 'catalog_product_view': null,
23 | 'catalogsearch_result_index': null,
24 | 'all_installment_template': null,
25 | 'text_with_interest': null,
26 | 'text_free_interest': null
27 | }
28 | },
29 | _create: function () {
30 | let widget = this;
31 |
32 | if (this.options.enabled) {
33 | try {
34 | $('body').on('afterReloadPrice', function (e, data) {
35 | if (!$(data.element).hasClass('price-tier_price')) {
36 | widget.renderPrices(data.element, {amount: data.price.final});
37 |
38 | if ($(data.element).parents('.product-info-price').length > 0) {
39 | widget.updateAllInstallments(data.element, {amount: data.price.final});
40 | }
41 | }
42 | }).on('products-loaded', function () {
43 | $('.price-box.price-final_price').each(function (i, element) {
44 | widget.renderPrices(element);
45 | });
46 | });
47 |
48 | $('.price-box.price-final_price').each(function (i, element) {
49 | widget.renderPrices(element);
50 | if ($(element).parents('.product-info-price').length > 0) {
51 | widget.updateAllInstallments(element);
52 | }
53 | });
54 |
55 | if (this.options.best_installment_in_cart && $('#cart-totals').length > 0) {
56 | widget.initPricesInCart();
57 | require([
58 | 'Magento_Checkout/js/model/quote'
59 | ], function (quote) {
60 | quote.totals.subscribe(function (data) {
61 | widget.initPricesInCart(data.grand_total);
62 | });
63 | });
64 | }
65 | } catch (e) {
66 | console.info(e);
67 | }
68 | }
69 | },
70 | initPricesInCart: function (total = null) {
71 | $('#cart-totals .installments').remove();
72 | total = (total) ? total : this.getTotal();
73 | if (total) {
74 | let installments = this.getInstallments(total);
75 | if (installments) {
76 | let bestInstallment = this.getBestInstallment(installments);
77 | const template = this.options.templates.in_cart_template;
78 |
79 | let html = template.replace('{{qty}}', bestInstallment.installments_qty)
80 | .replace('{{value}}', bestInstallment.installment_value)
81 | .replace('{{interest}}', (this.renderInterest(bestInstallment)))
82 | $('#cart-totals').append('' + html + '
');
83 | }
84 | }
85 |
86 | },
87 | /**
88 | *
89 | * @param productPrice
90 | * @returns {{}}
91 | */
92 | getInstallments: function (productPrice) {
93 | let widget = this;
94 | const type_interest = this.options.interest_type;
95 | const info_interest = this.options.interest;
96 | const min_installment = this.options.minimum_installment_value;
97 | const max_installment = this.options.maximum_quantity_installments;
98 |
99 | let json_installments = {};
100 | if (this.options.show_first_installment) {
101 | json_installments[1] = {
102 | 'installments_qty': 1,
103 | 'installment_value': widget.formatPrice(productPrice),
104 | 'total_installment': widget.formatPrice(productPrice),
105 | 'total_interest': 0,
106 | 'has_interest': 0
107 | };
108 | } else {
109 | if ((productPrice < min_installment) || ((productPrice / 2) < min_installment)) {
110 | console.info('Installment not available for this product. Very low price:' + this.formatPrice(productPrice));
111 | return null;
112 | }
113 | }
114 |
115 | let max_div = (productPrice / min_installment);
116 |
117 | if (max_div > max_installment) {
118 | max_div = max_installment;
119 | } else {
120 | if (max_div > 12) {
121 | max_div = 12;
122 | }
123 | }
124 |
125 | let limit = max_div;
126 | for (let installment_number in info_interest) {
127 | installment_number = parseInt(installment_number);
128 | if (installment_number <= max_div) {
129 | let rate = info_interest[installment_number] / 100;
130 | let amount = 0;
131 | let installment_value = 0;
132 |
133 | if (rate > 0) {
134 | if (type_interest === "compound") {
135 | amount = productPrice * Math.pow((1 + rate), installment_number);
136 | installment_value = amount / installment_number;
137 | } else {
138 | amount = productPrice + (productPrice * rate * installment_number);
139 | installment_value = (productPrice * (1 + rate * installment_number)) / installment_number;
140 | }
141 |
142 | let total_interest = amount - productPrice;
143 |
144 | if (installment_value > 5 && installment_value > min_installment) {
145 | json_installments[installment_number] = {
146 | 'installments_qty': installment_number,
147 | 'installment_value': widget.formatPrice(installment_value),
148 | 'total_interest': widget.formatPrice(total_interest),
149 | 'amount': widget.formatPrice(amount),
150 | 'has_interest': true,
151 | 'rate': info_interest[installment_number] + "%"
152 | };
153 | }
154 | } else {
155 | if (productPrice > 0 && installment_number > 0) {
156 | json_installments[installment_number] = {
157 | 'installments_qty': installment_number,
158 | 'installment_value': widget.formatPrice((productPrice / installment_number)),
159 | 'total_interest': widget.formatPrice(0),
160 | 'amount': widget.formatPrice(productPrice),
161 | 'has_interest': false,
162 | 'rate': info_interest[installment_number] + "%"
163 | };
164 | }
165 | }
166 | }
167 | }
168 |
169 | _.each(json_installments, function (key) {
170 | if (key > limit) {
171 | delete json_installments[key];
172 | }
173 | });
174 |
175 | return json_installments;
176 | },
177 | /**
178 | *
179 | * @param installment
180 | * @returns {*}
181 | */
182 | getBestInstallment: function (installment) {
183 | let best = null;
184 | for (let i = this.options.maximum_quantity_installments; i >= 1; i--) {
185 | if (installment[i]) {
186 | if (!best) {
187 | best = installment[i];
188 | }
189 |
190 | if (!installment[i]['has_interest']) {
191 | return installment[i];
192 | }
193 | }
194 | }
195 |
196 | return best;
197 | },
198 | /**
199 | *
200 | * @param price
201 | * @returns {[]}
202 | */
203 | getDiscounts: function (price) {
204 | for (let i in this.options.discounts) {
205 | let discount = this.options.discounts[i];
206 | if (discount.hasOwnProperty('percentage')) {
207 | discount.value = this.formatPrice(price - ((price * discount.percentage) / 100));
208 | this.options.discounts[i] = discount;
209 | }
210 | }
211 | return this.options.discounts;
212 |
213 | },
214 | /**
215 | *
216 | * @param discounts
217 | * @returns {string}
218 | */
219 | renderDiscounts: function (discounts) {
220 | let html = '';
221 | for (let i in discounts) {
222 | html += '' + this.options.templates.discount_template
223 | .replace('{{valueWithDiscount}}', discounts[i].value)
224 | .replace('{{percentage}}', discounts[i].percentage + '%')
225 | .replace('{{name}}', '' + discounts[i].name + ' ') + ' ';
226 | }
227 | html += ' ';
228 | return html;
229 | },
230 | /**
231 | *
232 | * @param priceElement
233 | * @param prices
234 | */
235 | updateAllInstallments: function (priceElement, prices = null) {
236 | let price = (prices) ? prices.amount : this.getElmPrice(priceElement);
237 | let installments = this.getInstallments(price);
238 | if (installments) {
239 | let template = this.options.templates.all_installment_template;
240 |
241 | let html = '';
242 | for (let i in installments) {
243 | html += '' + template.replace('{{qty}}', installments[i]['installments_qty'])
244 | .replace('{{value}}', installments[i]['installment_value'])
245 | .replace('{{interest}}', (this.renderInterest(installments[i]))) + ' ';
246 | }
247 | html += ' ';
248 | $('#installments-accordion .all-installments-content').html(html);
249 | }
250 | },
251 | /**
252 | *
253 | * @returns {null}
254 | */
255 | getTemplate() {
256 | if ($('body.catalog-category-view').length > 0) {
257 | return this.options.templates.catalog_category_view;
258 | } else if ($('body.catalog-product-view').length > 0) {
259 | return this.options.templates.catalog_product_view;
260 | } else if ($('body.catalogsearch-result-index').length > 0) {
261 | return this.options.templates.catalogsearch_result_index;
262 | } else {
263 | return this.options.templates.catalog_product_view;
264 | }
265 | },
266 | /**
267 | *
268 | * @param priceElement
269 | * @param prices
270 | */
271 | renderPrices: function (priceElement, prices = null) {
272 | let template = this.getTemplate();
273 | let price = (prices) ? prices.amount : this.getElmPrice(priceElement);
274 |
275 | if (price === 0) {
276 | return;
277 | }
278 |
279 | let installments = this.getInstallments(price);
280 |
281 | let installmentDiv = ($(priceElement).closest('.installments').length > 0) ? $(priceElement).closest('.installments') : null;
282 | if (installments) {
283 | let data = {
284 | 'bestInstallment': this.getBestInstallment(installments),
285 | 'discounts': this.renderDiscounts(this.getDiscounts(price))
286 | };
287 |
288 | template = template
289 | .replace('{{default}}', '
')
290 | .replace('{{qty}}', '' + data.bestInstallment.installments_qty)
291 | .replace('{{value}}', data.bestInstallment.installment_value)
292 | .replace('{{interest}}', (this.renderInterest(data.bestInstallment)) + '
')
293 | .replace('{{discounts}}', '' + data.discounts + '
');
294 | if (installmentDiv) {
295 | $(priceElement).insertBefore($(installmentDiv));
296 | $(installmentDiv).html(template);
297 | } else {
298 | installmentDiv = $('' + template + '
');
299 | installmentDiv.insertBefore(priceElement);
300 | }
301 | $(installmentDiv).find('.default').replaceWith(priceElement);
302 | $(installmentDiv).find(".best-installment, .discounts").show();
303 | $(".wrap-collabsible.installments").show();
304 | } else {
305 | $(installmentDiv).find(".best-installment, .discounts").hide();
306 | $(".wrap-collabsible.installments").hide();
307 | }
308 | },
309 | /**
310 | *
311 | * @param elm
312 | * @returns {number}
313 | */
314 | getElmPrice: function (elm) {
315 | let price = 0;
316 | price = ($(elm).find('.price-wrapper[data-price-type="finalPrice"]').length > 0) ?
317 | parseFloat($(elm).find('.price-wrapper[data-price-type="finalPrice"]').attr('data-price-amount')) : 0;
318 |
319 | if (price === 0) {
320 | price = ($(elm).find('.price-to .price-wrapper').length > 0) ? parseFloat($(elm).find('.price-to .price-wrapper').attr('data-price-amount')) : 0;
321 | }
322 | return price;
323 | },
324 | /**
325 | *
326 | * @returns {number}
327 | */
328 | getTotal: function () {
329 | let grandTotal = 0;
330 | let cartData = customerData.get('cart-data')();
331 |
332 | if (cartData.totals && cartData.totals.base_grand_total) {
333 | grandTotal = parseFloat(cartData.totals.base_grand_total);
334 | }
335 | return grandTotal;
336 | },
337 | /**
338 | * @param price
339 | * @returns {*}
340 | */
341 | formatPrice: function (price) {
342 | let format = {
343 | decimalSymbol: ",",
344 | groupLength: 3,
345 | groupSymbol: ".",
346 | integerRequired: false,
347 | pattern: this.options.currency_symbol + "%s",
348 | precision: 2,
349 | requiredPrecision: 2
350 | };
351 |
352 | format = (window.checkoutConfig && window.checkoutConfig.priceFormat) ? window.checkoutConfig.priceFormat : format;
353 | return priceUtils.formatPrice(price, format);
354 | },
355 | renderInterest: function (installment) {
356 | let template = (installment.has_interest) ? this.options.templates.text_with_interest : this.options.templates.text_free_interest;
357 | template = template.replace('{{amount}}', installment.amount)
358 | .replace('{{total_interest}}', installment.total_interest)
359 | .replace('{{rate}}', installment.rate)
360 | return ' ' + template;
361 | }
362 | });
363 | return $.mage.installment;
364 | });
365 |
--------------------------------------------------------------------------------
/view/frontend/web/js/price-box-mixin.js:
--------------------------------------------------------------------------------
1 | define([
2 | 'jquery',
3 | 'Magento_Catalog/js/price-utils',
4 | 'underscore',
5 | 'mage/template',
6 | 'jquery/ui'
7 | ], function ($, utils, _, mageTemplate) {
8 | 'use strict';
9 |
10 | return function (widget) {
11 | $.widget('mage.priceBox', widget,
12 | {
13 | reloadPrice: function reDrawPrices() {
14 | let priceFormat = (this.options.priceConfig && this.options.priceConfig.priceFormat) || {},
15 | priceTemplate = mageTemplate(this.options.priceTemplate);
16 |
17 | _.each(this.cache.displayPrices, function (price, priceCode) {
18 | price.final = _.reduce(price.adjustments, function (memo, amount) {
19 | return memo + amount;
20 | }, price.amount);
21 |
22 | price.formatted = utils.formatPrice(price.final, priceFormat);
23 |
24 | $('[data-price-type="' + priceCode + '"]', this.element).html(priceTemplate({
25 | data: price
26 | }));
27 |
28 | if (priceCode === 'finalPrice') {
29 | let element = this.element;
30 | $('body').trigger('afterReloadPrice', {price, element});
31 | }
32 | }, this);
33 | }
34 | });
35 | return $.mage.priceBox;
36 | }
37 | });
38 |
--------------------------------------------------------------------------------