Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MWPW-165302: Calculate promo prices with duration #3492

Merged
merged 6 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions libs/deps/mas/commerce.js
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] reported by reviewdog 🐶
File ignored because of a matching ignore pattern. Use "--no-ignore" to override.

Large diffs are not rendered by default.

118 changes: 59 additions & 59 deletions libs/deps/mas/mas.js
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] reported by reviewdog 🐶
File ignored because of a matching ignore pattern. Use "--no-ignore" to override.

Large diffs are not rendered by default.

118 changes: 59 additions & 59 deletions libs/features/mas/dist/mas.js
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] reported by reviewdog 🐶
File ignored because of a matching ignore pattern. Use "--no-ignore" to override.

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions libs/features/mas/src/price/index.js
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] reported by reviewdog 🐶
File ignored because of a matching ignore pattern. Use "--no-ignore" to override.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
createPromoPriceWithAnnualTemplate,
} from './template.js';

import { isPromotionActive } from './utilities.js';

const price = createPriceTemplate();
const pricePromo = createPromoPriceTemplate();
const priceOptical = createPriceTemplate({
Expand All @@ -27,4 +29,5 @@ export {
priceAnnual,
priceWithAnnual,
pricePromoWithAnnual,
isPromotionActive,
};
37 changes: 31 additions & 6 deletions libs/features/mas/src/price/template.js
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] reported by reviewdog 🐶
File ignored because of a matching ignore pattern. Use "--no-ignore" to override.

Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
formatRegularPrice,
formatAnnualPrice,
makeSpacesAroundNonBreaking,
} from './utilities';
} from './utilities.js';

// JSON imports require new syntax to run in Milo/wtr tests,
// but the new syntax is not yet supported by ESLint:
Expand Down Expand Up @@ -153,6 +153,7 @@
displayOptical = false,
displayStrikethrough = false,
displayAnnual = false,
instant = undefined,
} = {}) =>
(
{
Expand All @@ -163,6 +164,7 @@
displayTax = false,
language,
literals: priceLiterals = {},
quantity = 1,
} = {},
{
commitment,
Expand All @@ -174,6 +176,7 @@
taxTerm,
term,
usePrecision,
promotion,
} = {},
attributes = {},
) => {
Expand All @@ -184,8 +187,10 @@
price,
}).forEach(([key, value]) => {
if (value == null) {
/* c8 ignore next 2 */
throw new Error(`Argument "${key}" is missing for osi ${offerSelectorIds?.toString()}, country ${country}, language ${language}`);
/* c8 ignore next 2 */
throw new Error(
`Argument "${key}" is missing for osi ${offerSelectorIds?.toString()}, country ${country}, language ${language}`,
);

Check warning on line 193 in libs/features/mas/src/price/template.js

View check run for this annotation

Codecov / codecov/patch

libs/features/mas/src/price/template.js#L193

Added line #L193 was not covered by tests
}
});

Expand All @@ -208,7 +213,7 @@
locale,
).format(parameters);
} catch {
/* c8 ignore next 2 */
/* c8 ignore next 2 */
log.error('Failed to format literal:', literal);
return '';
}
Expand All @@ -226,10 +231,15 @@
const { accessiblePrice, recurrenceTerm, ...formattedPrice } = method({
commitment,
formatString,
instant,
isIndianPrice: country === 'IN',
originalPrice: price,
priceWithoutDiscount,
price: (displayOptical ? price : displayPrice),
promotion,
quantity,
term,
price: displayOptical ? price : displayPrice,
usePrecision,
isIndianPrice: country === 'IN',
});

let accessibleLabel = accessiblePrice;
Expand Down Expand Up @@ -365,6 +375,20 @@

const createPromoPriceWithAnnualTemplate =
() => (context, value, attributes) => {
let { instant } = context;
try {
if (!instant) {
instant = new URLSearchParams(document.location.search).get(
'instant',
);
}
if (instant) {
instant = new Date(instant);
}
} catch (e) {
instant = undefined;
/* ignore the error */
}

Check warning on line 391 in libs/features/mas/src/price/template.js

View check run for this annotation

Codecov / codecov/patch

libs/features/mas/src/price/template.js#L389-L391

Added lines #L389 - L391 were not covered by tests
const ctxStAnnual = {
...context,
displayTax: false,
Expand All @@ -386,6 +410,7 @@
}${createPriceTemplate()(context, value, attributes)}${renderSpan(cssClassNames.containerAnnualPrefix, ' (')}${createPriceTemplate(
{
displayAnnual: true,
instant,
},
)(
ctxStAnnual,
Expand Down
125 changes: 108 additions & 17 deletions libs/features/mas/src/price/utilities.js
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] reported by reviewdog 🐶
File ignored because of a matching ignore pattern. Use "--no-ignore" to override.

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,31 @@
const SPACE_END_PATTERN = /\s+$/;
const NBSP = ' ';

const getAnnualPrice = (price) => price * 12;

/**
* Checks if a promotion is active based on its start and end dates
* @param {{ start: string, end: string }} promotion The promotion with start and end dates
* @param {string} [instant] Optional instant date string to check against
* @returns {boolean} Whether the promotion is active
*/
const isPromotionActive = (promotion, instant) => {
const { amount, duration, minProductQuantity, outcomeType } = promotion;
if (!(amount && duration && outcomeType && minProductQuantity)) {
return false;
}
const now = instant ? new Date(instant) : new Date();
const { start, end } = promotion;
if (!start || !end) {
return false;
}

Check warning on line 32 in libs/features/mas/src/price/utilities.js

View check run for this annotation

Codecov / codecov/patch

libs/features/mas/src/price/utilities.js#L31-L32

Added lines #L31 - L32 were not covered by tests

const startDate = new Date(start);
const endDate = new Date(end);

return now >= startDate && now <= endDate;
};

// TODO: @pandora/react-price does not have "module" field in package.json and is bundled entirely by Webpack
const RecurrenceTerm = {
MONTH: 'MONTH',
Expand Down Expand Up @@ -46,18 +71,18 @@
opticalPriceRoundingRule(
// optical price for the term is a multiple of the initial price
({ divisor, price }) => price % divisor == 0,
({ divisor, price }) => price / divisor
({ divisor, price }) => price / divisor,
),
opticalPriceRoundingRule(
// round optical price up to 2 decimals
({ usePrecision }) => usePrecision,
({ divisor, price }) => Math.round((price / divisor) * 100.0) / 100.0
({ divisor, price }) => Math.round((price / divisor) * 100.0) / 100.0,
),
opticalPriceRoundingRule(
// round optical price up to integer
() => true,
({ divisor, price }) =>
Math.ceil(Math.floor((price * 100) / divisor) / 100)
Math.ceil(Math.floor((price * 100) / divisor) / 100),
),
];

Expand Down Expand Up @@ -95,7 +120,7 @@
// As the formatString could be container non-symbol like `A #,##0.00 B` so using regex here.
numberMask = numberMask.replace(
/\s?(#.*0)(?!\s)?/,
'$&' + getPossibleDecimalsDelimiter(formatString)
'$&' + getPossibleDecimalsDelimiter(formatString),
);
} else if (!usePrecision) {
// Trim the 0s after the decimalsDelimiter. `#,##0.00` will become `#,##0.`
Expand Down Expand Up @@ -169,15 +194,31 @@
// Utilities, specific to tacocat needs.

/**
* @param { import('./types').PriceData } data
* @param { RecurrenceTerm } recurrenceTerm
* @param { (price: number, format: { currencySymbol: string }) => number } transformPrice
* Formats a price according to the specified format string and currency rules.
*
* @param {object} options - The formatting options
* @param {string} options.formatString - The currency format string (e.g., "'US$ '#,##0.00")
* @param {number} options.price - The price value to format
* @param {boolean} options.usePrecision - Whether to include decimal precision in the formatted price
* @param {boolean} [options.isIndianPrice=false] - Whether to use Indian locale-specific formatting
* @param {string} recurrenceTerm - The recurrence term (MONTH or YEAR) for the price
* @param {function} [transformPrice=(price) => price] - Optional function to transform the price before formatting
* @returns {{
* accessiblePrice: string,
* currencySymbol: string,
* decimals: string,
* decimalsDelimiter: string,
* hasCurrencySpace: boolean,
* integer: string,
* isCurrencyFirst: boolean,
* recurrenceTerm: string
* }} Formatted price object containing the accessible price string and formatting details
*
*/
// TODO: Move this function to pandora library
function formatPrice(
{ formatString, price, usePrecision, isIndianPrice = false },
recurrenceTerm,
transformPrice = (formattedPrice) => formattedPrice
transformPrice = (formattedPrice) => formattedPrice,
) {
const { currencySymbol, isCurrencyFirst, hasCurrencySpace } =
getCurrencySymbolDetails(formatString);
Expand Down Expand Up @@ -233,14 +274,14 @@
usePrecision,
};
const { round } = opticalPriceRoundingRules.find(({ accept }) =>
accept(priceData)
accept(priceData),
);
if (!round)
throw new Error(
`Missing rounding rule for: ${JSON.stringify(priceData)}`
`Missing rounding rule for: ${JSON.stringify(priceData)}`,

Check warning on line 281 in libs/features/mas/src/price/utilities.js

View check run for this annotation

Codecov / codecov/patch

libs/features/mas/src/price/utilities.js#L281

Added line #L281 was not covered by tests
);
return round(priceData);
}
},
);
};

Expand All @@ -252,15 +293,65 @@
formatPrice(data, recurrenceTerms[commitment]?.[term]);

/**
* Formats annual price.
* @param { import('./types').PriceData } data
* Creates a function that calculates the annual price with promotion applied.
*
* @param {object} data - The data object containing price and priceWithoutDiscount
* @returns {function(number): number} A function that takes a monthly price and returns the calculated annual price with promotion
*
*/
const formatAnnualPrice = (data) => {
const { commitment, term } = data;
const {
commitment,
instant,
price,
originalPrice,
priceWithoutDiscount,
promotion,
quantity = 1,
term,
} = data;
if (commitment === Commitment.YEAR && term === Term.MONTHLY) {
return formatPrice(data, RecurrenceTerm.YEAR, (price) => price * 12);
if (!promotion) {
return formatPrice(data, RecurrenceTerm.YEAR, getAnnualPrice);
}
const { outcomeType, duration, minProductQuantity } = promotion;
switch (outcomeType) {
case 'PERCENTAGE_DISCOUNT': {
if (
quantity >= minProductQuantity &&
isPromotionActive(promotion, instant)
bozojovicic marked this conversation as resolved.
Show resolved Hide resolved
) {
const durationInMonths = parseInt(
duration.replace('P', '').replace('M', ''),
);
if (isNaN(durationInMonths)) return getAnnualPrice(price);
const discountPrice =
quantity * originalPrice * durationInMonths;
const regularPrice =
quantity *
priceWithoutDiscount *
(12 - durationInMonths);
const totalPrice =
Math.floor((discountPrice + regularPrice) * 100) / 100;
return formatPrice(
{ ...data, price: totalPrice },
recurrenceTerms[commitment]?.[term],
);
}
}
default:
return formatPrice(data, RecurrenceTerm.YEAR, () =>
getAnnualPrice(priceWithoutDiscount ?? price),
);
}
}
return formatPrice(data, recurrenceTerms[commitment]?.[term]);
};

export { formatOpticalPrice, formatRegularPrice, formatAnnualPrice, makeSpacesAroundNonBreaking };
export {
formatOpticalPrice,
formatRegularPrice,
formatAnnualPrice,
makeSpacesAroundNonBreaking,
isPromotionActive,
};
Loading
Loading