diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c8dd80..b362165 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- `installmentsCriteria` and `installmentOptionsFilter` props to `Installments` component. ## [1.23.0] - 2021-07-13 ### Added diff --git a/docs/README.md b/docs/README.md index ecfc937..8cef2e2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -31,7 +31,7 @@ Now, you can use all the blocks exported by the `product-price` app. Check out t | `product-list-price` | Renders the product list price. If it is equal or lower than the product selling price, this block will not be rendered. | | `product-selling-price` | Renders the product selling price.| | `product-spot-price` | Renders the product spot price (in case it equals the product selling price, the block is not rendered). This block finds the spot price by looking for the cheapest price of all installments options.| -| `product-installments` | Renders the product installments. If more than one option is available, the one with the biggest number of installments will be displayed. | +| `product-installments` | Renders the product installments. If more than one option is available, the one with the biggest number of installments will be displayed by default. | | `product-installments-list` | Renders all the installments of the payment system with the biggest amount of installments options by default. | | `product-installments-list-item` | Renders an installments option of the `product-installments-list-item` | | `product-price-savings` | Renders the product price savings, if there is any. It can show the percentage of the discount or the value of the absolute saving. | @@ -102,6 +102,13 @@ The block `product-installments-list` has two additional props: | `paymentSystemName` | `string` | This prop enables you to filter the listed installments options by a certain payment system. If not passed, the installments of the payment system with the biggest amount of installments options will be rendered. | `undefined` | | `installmentsToShow` | `number[]` | Which installments options you want to show the user, in terms of the number of installments. For example, if `[1, 3]` is passed as a value for this prop, only the installments options with `NumberOfInstallments` equal to 1 and 3 will be rendered. If not passed, all options will be rendered. | `undefined` | +And the block `product-installments` also has two additional props: + +| Prop name | Type | Description | Default value | +| --------------------| ----------|--------------|---------------| +| `installmentsCriteria` | `max-quantity` or `max-quantity-without-interest` | When set to `max-quantity`, the block will render the installments plan with the biggest number of installments. When set to `max-quantity-without-interest`, the block will render the installments plan with the biggest number of installments and **zero interest**. Notice that, if this prop is set to `max-quantity-without-interest`, and no installments plan matches the 'without interest' criteria, the component will fallback the default behavior. | `max-quantity` | +| `installmentOptionsFilter` | `{ paymentSystemName?: string, installmentsQuantity?: number }` | Allows you to define two filtering rules that will narrow down the possible installments plans the component might render. | `undefined` | + If you are using the asynchronous price feature, you can take advantage of the `product-price-suspense` and its props: diff --git a/react/Installments.tsx b/react/Installments.tsx index bd35b5c..3aa3b44 100644 --- a/react/Installments.tsx +++ b/react/Installments.tsx @@ -6,6 +6,10 @@ import { useProduct } from 'vtex.product-context' import InstallmentsRenderer, { CSS_HANDLES, } from './components/InstallmentsRenderer' +import { + pickMaxInstallmentsOption, + pickMaxInstallmentsOptionWithoutInterest, +} from './modules/pickInstallments' import { getDefaultSeller } from './modules/seller' const messages = defineMessages({ @@ -23,6 +27,11 @@ const messages = defineMessages({ interface Props { message?: string markers?: string[] + installmentsCriteria?: 'max-quantity' | 'max-quantity-without-interest' + installmentOptionsFilter?: { + paymentSystemName?: string + installmentsQuantity?: number + } /** Used to override default CSS handles */ classes?: CssHandlesTypes.CustomClasses } @@ -30,6 +39,8 @@ interface Props { function Installments({ message = messages.default.id, markers = [], + installmentsCriteria = 'max-quantity', + installmentOptionsFilter, classes, }: Props) { const productContextValue = useProduct() @@ -45,22 +56,33 @@ function Installments({ return null } - let [maxInstallmentOption] = commercialOffer.Installments + let [installmentsOption] = commercialOffer.Installments + + switch (installmentsCriteria) { + case 'max-quantity-without-interest': { + installmentsOption = pickMaxInstallmentsOptionWithoutInterest( + commercialOffer.Installments, + installmentOptionsFilter + ) + + break + } + + default: { + installmentsOption = pickMaxInstallmentsOption( + commercialOffer.Installments, + installmentOptionsFilter + ) - commercialOffer.Installments.forEach(installmentOption => { - if ( - installmentOption.NumberOfInstallments > - maxInstallmentOption.NumberOfInstallments - ) { - maxInstallmentOption = installmentOption + break } - }) + } return ( diff --git a/react/__mocks__/installments-list-mock.ts b/react/__mocks__/installments-list-mock.ts index 189b79f..27e2856 100644 --- a/react/__mocks__/installments-list-mock.ts +++ b/react/__mocks__/installments-list-mock.ts @@ -3,28 +3,28 @@ import { Installments } from '../components/InstallmentsContext' export const visaInstallments: Installments[] = [ { Value: 44, - InterestRate: 0, + InterestRate: 3, TotalValuePlusInterestRate: 44, NumberOfInstallments: 1, PaymentSystemName: 'Visa', }, { Value: 22, - InterestRate: 0, + InterestRate: 4, TotalValuePlusInterestRate: 44, NumberOfInstallments: 2, PaymentSystemName: 'Visa', }, { Value: 14.7, - InterestRate: 0, + InterestRate: 5, TotalValuePlusInterestRate: 44, NumberOfInstallments: 3, PaymentSystemName: 'Visa', }, { Value: 11, - InterestRate: 0, + InterestRate: 3, TotalValuePlusInterestRate: 44, NumberOfInstallments: 4, PaymentSystemName: 'Visa', @@ -135,7 +135,7 @@ export const installmentsListMastercardMax = [ }, { Value: 44, - InterestRate: 0, + InterestRate: 5, TotalValuePlusInterestRate: 44, NumberOfInstallments: 15, PaymentSystemName: 'Mastercard', diff --git a/react/__tests__/pickInstallments.test.ts b/react/__tests__/pickInstallments.test.ts index dc56891..6b38741 100644 --- a/react/__tests__/pickInstallments.test.ts +++ b/react/__tests__/pickInstallments.test.ts @@ -4,10 +4,13 @@ import { installmentsListMastercardMax, } from 'installments-list-mock' -import pickInstallmentsList from '../modules/pickInstallments' +import pickInstallmentsList, { + pickMaxInstallmentsOption, + pickMaxInstallmentsOptionWithoutInterest, +} from '../modules/pickInstallments' -describe('pickInstallments', () => { - it('should pick installments of the paymentmethod with the biggest amount', () => { +describe('pickInstallmentsList', () => { + it('should pick installments of the payment method with the biggest amount of options', () => { const pickedInstallments = pickInstallmentsList( installmentsList, 'PaymentSystemName' @@ -40,3 +43,78 @@ describe('pickInstallments', () => { expect(pickedInstallments[index].NumberOfInstallments).toBe(15) }) }) + +describe('pickMaxInstallmentsOption', () => { + it('should pick the installments plan with the highest NumberOfInstallments', () => { + const pickedInstallmentsPlan = pickMaxInstallmentsOption( + installmentsListMastercardMax + ) + + expect(pickedInstallmentsPlan.NumberOfInstallments).toBe(15) + expect(pickedInstallmentsPlan.PaymentSystemName).toBe('Mastercard') + }) + + it('should pick the installments plan with the highest NumberOfInstallments, from a certain payment system', () => { + const pickedInstallmentsPlan = pickMaxInstallmentsOption(installmentsList, { + paymentSystemName: 'Customer Credit', + }) + + expect(pickedInstallmentsPlan.NumberOfInstallments).toBe(3) + expect(pickedInstallmentsPlan.PaymentSystemName).toBe('Customer Credit') + }) + + it('should pick an installments plan with a certain number of installments', () => { + const pickedInstallmentsPlan = pickMaxInstallmentsOption( + installmentsListMastercardMax, + { + installmentsQuantity: 14, + } + ) + + expect(pickedInstallmentsPlan.NumberOfInstallments).toBe(14) + }) + + it('should pick the installments plan with the highest NumberOfInstallments, using all filtering options', () => { + const pickedInstallmentsPlan = pickMaxInstallmentsOption(installmentsList, { + paymentSystemName: 'Customer Credit', + installmentsQuantity: 2, + }) + + expect(pickedInstallmentsPlan.NumberOfInstallments).toBe(2) + expect(pickedInstallmentsPlan.PaymentSystemName).toBe('Customer Credit') + }) +}) + +describe('pickMaxInstallmentsOptionWithoutInterest', () => { + it('should pick the installments plan with the highest NumberOfInstallments and no interest', () => { + const pickedInstallmentsPlan = pickMaxInstallmentsOptionWithoutInterest( + installmentsListMastercardMax + ) + + expect(pickedInstallmentsPlan.NumberOfInstallments).toBe(14) + expect(pickedInstallmentsPlan.PaymentSystemName).toBe('Visa') + expect(pickedInstallmentsPlan.InterestRate).toBe(0) + }) + + it('should pick the installments plan with the second highest NumberOfInstallments and no interest, due to filtering rules', () => { + const pickedInstallmentsPlan = pickMaxInstallmentsOptionWithoutInterest( + installmentsList, + { + paymentSystemName: 'Customer Credit', + } + ) + + expect(pickedInstallmentsPlan.NumberOfInstallments).toBe(2) + expect(pickedInstallmentsPlan.PaymentSystemName).toBe('Customer Credit') + expect(pickedInstallmentsPlan.InterestRate).toBe(0) + }) + + it("should pick the installments plan with the highest NumberOfInstallments overall, if there isn't an option with no interest", () => { + const pickedInstallmentsPlan = pickMaxInstallmentsOptionWithoutInterest( + visaInstallments + ) + + expect(pickedInstallmentsPlan.NumberOfInstallments).toBe(4) + expect(pickedInstallmentsPlan.PaymentSystemName).toBe('Visa') + }) +}) diff --git a/react/modules/pickInstallments.ts b/react/modules/pickInstallments.ts index adccbd2..2e5efac 100644 --- a/react/modules/pickInstallments.ts +++ b/react/modules/pickInstallments.ts @@ -3,8 +3,8 @@ import { ProductTypes } from 'vtex.product-context' type ClusterBy = keyof ProductTypes.Installment /** - * Pick which installments should be used, first it cluster all installments - * by the value of clusterBy, then pick the cluster with the biggest amount of + * Pick which installments should be used. First it clusters all installments + * by the value of clusterBy, then picks the cluster with the biggest amount of * installments options and then return this list sorted by the amount of installments * @param installmentsList All installments * @param clusterBy @@ -15,7 +15,10 @@ export default function pickInstallmentsList( ) { const clusteredInstallments = clusterInstallments(installmentsList, clusterBy) - const pickedInstallments = pickMaxOption(clusteredInstallments, clusterBy) + const pickedInstallments = pickMaxOptionCount( + clusteredInstallments, + clusterBy + ) return pickedInstallments.sort( (a, b) => a.NumberOfInstallments - b.NumberOfInstallments @@ -46,12 +49,12 @@ export function clusterInstallments( /** * Pick the cluster with the biggest amount of options, if there are multiple - * clusters witht eh biggest amount it will pick the one that has a installment + * clusters with the biggest amount it will pick the one that has an installment * with the biggest NumberOfInstallments of all options * @param clusteredInstallments * @param clusterBy */ -function pickMaxOption( +function pickMaxOptionCount( clusteredInstallments: Record, clusterBy: ClusterBy ) { @@ -92,3 +95,75 @@ function pickMaxOption( return clusteredInstallments[biggestInstallmentsOptionKey] } + +function applyFiltersToInstallmentsList( + installmentsList: ProductTypes.Installment[], + filteringRules: { + paymentSystemName?: string + installmentsQuantity?: number + } +) { + let filteredInstallmentsList = installmentsList + + if (filteringRules.paymentSystemName) { + filteredInstallmentsList = filteredInstallmentsList.filter( + installmentsOption => + installmentsOption.PaymentSystemName === + filteringRules.paymentSystemName + ) + } + + if (filteringRules.installmentsQuantity) { + filteredInstallmentsList = filteredInstallmentsList.filter( + installmentsOption => + installmentsOption.NumberOfInstallments === + filteringRules.installmentsQuantity + ) + } + + return filteredInstallmentsList +} + +export function pickMaxInstallmentsOption( + installmentsList: ProductTypes.Installment[], + filteringRules?: { + paymentSystemName?: string + installmentsQuantity?: number + } +) { + const filteredInstallmentsList = filteringRules + ? applyFiltersToInstallmentsList(installmentsList, filteringRules) + : installmentsList + + let [maxInstallmentOption] = filteredInstallmentsList + + filteredInstallmentsList.forEach(installmentOption => { + if ( + installmentOption.NumberOfInstallments > + maxInstallmentOption.NumberOfInstallments + ) { + maxInstallmentOption = installmentOption + } + }) + + return maxInstallmentOption +} + +export function pickMaxInstallmentsOptionWithoutInterest( + installmentsList: ProductTypes.Installment[], + filteringRules?: { + paymentSystemName?: string + installmentsQuantity?: number + } +) { + const installmentsWithoutInterest = installmentsList.filter( + installmentsOption => installmentsOption.InterestRate === 0 + ) + + // There aren't any no-interest options + if (installmentsWithoutInterest.length === 0) { + return pickMaxInstallmentsOption(installmentsList, filteringRules) + } + + return pickMaxInstallmentsOption(installmentsWithoutInterest, filteringRules) +}