diff --git a/.gitignore b/.gitignore index e109f43..8d97424 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ coverage .idea .yalc yalc.lock +.epilot-docs diff --git a/src/__tests__/fixtures/price-getag.samples.ts b/src/__tests__/fixtures/price-getag.samples.ts index 27638e7..c3f5f04 100644 --- a/src/__tests__/fixtures/price-getag.samples.ts +++ b/src/__tests__/fixtures/price-getag.samples.ts @@ -214,6 +214,7 @@ export const compositePriceGetAG: PriceItemDto = { tax: [tax19percent], get_ag: { category: 'power', + type: 'work_price', markup_amount: 10, markup_amount_decimal: '0.10', }, @@ -390,6 +391,7 @@ export const compositePriceTieredFlatFeeGetAG: PriceItemDto = { tax: [tax19percent], get_ag: { category: 'power', + type: 'work_price', markup_amount: 10, markup_amount_decimal: '0.10', }, @@ -510,6 +512,7 @@ export const compositePriceGetAGWithZeroInputMapping: PriceItemDto = { tax: [tax19percent], get_ag: { category: 'power', + type: 'work_price', markup_amount: 10, markup_amount_decimal: '0.10', }, diff --git a/src/__tests__/fixtures/price.samples.ts b/src/__tests__/fixtures/price.samples.ts index c02159d..b884593 100644 --- a/src/__tests__/fixtures/price.samples.ts +++ b/src/__tests__/fixtures/price.samples.ts @@ -2416,6 +2416,7 @@ export const compositePriceWithTaxExclusiveComponent: CompositePriceItemDto = { _updated_at: '2023-02-08T11:01:50.179Z', variable_price: true, unit: 'kwh', + pricing_model: 'per_unit', tax: [ [ { @@ -3205,6 +3206,7 @@ export const compositePriceWithNumberInputEqualsToZero: CompositePriceItemDto = _updated_at: '2023-02-08T11:01:50.179Z', variable_price: true, unit: 'kwh', + pricing_model: 'per_unit', tax: [ [ { diff --git a/src/computations/compute-price-item.ts b/src/computations/compute-price-item.ts index 8221794..2da3e9d 100644 --- a/src/computations/compute-price-item.ts +++ b/src/computations/compute-price-item.ts @@ -14,6 +14,7 @@ import { DEFAULT_CURRENCY } from '../money/constants'; import { PricingModel } from '../prices/constants'; import { convertPriceItemWithCouponAppliedToPriceItemDto } from '../prices/convert-precision'; import { getPriceTax } from '../prices/get-price-tax'; +import { isVariablePrice } from '../prices/is-variable-price'; import { mapToProductSnapshot, mapToPriceSnapshot } from '../prices/map-to-snapshots'; import { normalizePriceMappingInput } from '../prices/mapping'; import type { PriceItemsTotals } from '../prices/types'; @@ -51,7 +52,7 @@ const computeExternalFee = ( export const computeQuantities = (price: Price | undefined, quantity: number, priceMapping?: PriceInputMapping) => { const safeQuantity = getSafeQuantity(quantity); - if (!price?.variable_price) { + if (!price || !isVariablePrice(price)) { return { safeQuantity, unitAmountMultiplier: safeQuantity, diff --git a/src/exports.test.ts b/src/exports.test.ts index b8375da..6122d51 100644 --- a/src/exports.test.ts +++ b/src/exports.test.ts @@ -35,6 +35,8 @@ const expectedNamedExports = [ 'extractGetAgConfig', 'getAmountWithTax', 'getTaxValue', + 'isVariablePrice', + 'isVariablePriceItem', ]; /** diff --git a/src/index.ts b/src/index.ts index 6b50c51..ae4a669 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ export { computeQuantities } from './computations/compute-price-item'; export { extractPricingEntitiesBySlug, extractCouponsFromItem } from './prices/extract-pricing-entities-by-slug'; export { computeAggregatedAndPriceTotals } from './computations/compute-totals'; export { PricingModel } from './prices/constants'; +export { isVariablePrice, isVariablePriceItem } from './prices/is-variable-price'; export { getDisplayTierByQuantity, getDisplayTiersByQuantity, getTierDescription } from './tiers/utils'; export { computeCumulativeValue } from './tiers/compute-cumulative-value'; export type { diff --git a/src/prices/__tests__/is-variable-price.test.ts b/src/prices/__tests__/is-variable-price.test.ts new file mode 100644 index 0000000..5e122c1 --- /dev/null +++ b/src/prices/__tests__/is-variable-price.test.ts @@ -0,0 +1,91 @@ +import type { CompositePrice, CompositePriceItem, Price, PriceItem } from '../../shared/types'; +import { isVariablePrice, isVariablePriceItem } from '../is-variable-price'; + +const makePrice = (overrides: Partial = {}): Price => + ({ + pricing_model: 'per_unit', + ...overrides, + }) as Price; + +describe('isVariablePrice', () => { + it('returns true for per_unit with variable_price true', () => { + expect(isVariablePrice(makePrice({ pricing_model: 'per_unit', variable_price: true }))).toBe(true); + }); + + it('returns false for per_unit with variable_price false', () => { + expect(isVariablePrice(makePrice({ pricing_model: 'per_unit', variable_price: false }))).toBe(false); + }); + + it('returns false for per_unit without variable_price', () => { + expect(isVariablePrice(makePrice({ pricing_model: 'per_unit' }))).toBe(false); + }); + + it.each(['tiered_volume', 'tiered_graduated', 'tiered_flatfee'] as const)( + 'returns true for tiered model: %s', + (pricing_model) => { + expect(isVariablePrice(makePrice({ pricing_model }))).toBe(true); + }, + ); + + it('returns true for dynamic_tariff', () => { + expect(isVariablePrice(makePrice({ pricing_model: 'dynamic_tariff' }))).toBe(true); + }); + + it('returns true for external_getag with work_price', () => { + expect(isVariablePrice(makePrice({ pricing_model: 'external_getag', get_ag: { type: 'work_price' } }))).toBe(true); + }); + + it('returns true for external_getag with base_price and tiered_flatfee markup', () => { + expect( + isVariablePrice( + makePrice({ + pricing_model: 'external_getag', + get_ag: { type: 'base_price', markup_pricing_model: 'tiered_flatfee' }, + }), + ), + ).toBe(true); + }); + + it('returns false for external_getag with base_price and non-tiered markup', () => { + expect(isVariablePrice(makePrice({ pricing_model: 'external_getag', get_ag: { type: 'base_price' } }))).toBe(false); + }); + + it('returns false for external_getag without get_ag', () => { + expect(isVariablePrice(makePrice({ pricing_model: 'external_getag' }))).toBe(false); + }); + + it('returns false for CompositePrice', () => { + const composite = { price_components: [makePrice()] } as unknown as CompositePrice; + + expect(isVariablePrice(composite)).toBe(false); + }); +}); + +describe('isVariablePriceItem', () => { + it('returns true when _price is variable', () => { + const item = { _price: makePrice({ pricing_model: 'tiered_volume' }) } as PriceItem; + + expect(isVariablePriceItem(item)).toBe(true); + }); + + it('returns false when _price is not variable', () => { + const item = { _price: makePrice({ pricing_model: 'per_unit', variable_price: false }) } as PriceItem; + + expect(isVariablePriceItem(item)).toBe(false); + }); + + it('returns false for CompositePriceItem', () => { + const item = { + is_composite_price: true, + _price: { is_composite_price: true, price_components: [] }, + } as unknown as CompositePriceItem; + + expect(isVariablePriceItem(item)).toBe(false); + }); + + it('returns false when _price is missing', () => { + const item = {} as PriceItem; + + expect(isVariablePriceItem(item)).toBe(false); + }); +}); diff --git a/src/prices/is-variable-price.ts b/src/prices/is-variable-price.ts new file mode 100644 index 0000000..a28ec33 --- /dev/null +++ b/src/prices/is-variable-price.ts @@ -0,0 +1,36 @@ +import type { CompositePrice, CompositePriceItem, Price, PriceItem } from '../shared/types'; +import { MarkupPricingModel, PricingModel, TypeGetAg } from './constants'; + +const isTieredPrice = (price: Price): boolean => { + return ( + price.pricing_model === PricingModel.tieredVolume || + price.pricing_model === PricingModel.tieredGraduated || + price.pricing_model === PricingModel.tieredFlatFee + ); +}; + +export const isVariablePrice = (price: Price | CompositePrice): boolean => { + if (price.is_composite_price) { + return false; + } + + const p = price as Price; + + if (isTieredPrice(p)) return true; + if (p.pricing_model === PricingModel.dynamicTariff) return true; + if (p.pricing_model === PricingModel.externalGetAG) { + if (p.get_ag?.type === TypeGetAg.workPrice) return true; + if (p.get_ag?.type === TypeGetAg.basePrice && p.get_ag?.markup_pricing_model === MarkupPricingModel.tieredFlatFee) { + return true; + } + } + if (p.pricing_model === PricingModel.perUnit && p.variable_price) return true; + + return false; +}; + +export const isVariablePriceItem = (priceItem: PriceItem | CompositePriceItem): boolean => { + if (!priceItem._price) return false; + + return isVariablePrice(priceItem._price); +}; diff --git a/src/variables/index.test.ts b/src/variables/index.test.ts index d6569ac..691e1b2 100644 --- a/src/variables/index.test.ts +++ b/src/variables/index.test.ts @@ -140,6 +140,7 @@ describe('getQuantity', () => { const baseVariableItem = { price_id: 'price_id', _price: { + pricing_model: 'per_unit', variable_price: true, }, }; @@ -147,6 +148,7 @@ describe('getQuantity', () => { const baseNotVariableItem = { price_id: 'price_id', _price: { + pricing_model: 'per_unit', variable_price: false, }, }; diff --git a/src/variables/process-order-table-data.ts b/src/variables/process-order-table-data.ts index e845c4d..f442937 100644 --- a/src/variables/process-order-table-data.ts +++ b/src/variables/process-order-table-data.ts @@ -2,6 +2,7 @@ import type { Currency } from 'dinero.js'; import { formatPriceUnit } from '../money/formatters'; import { PricingModel } from '../prices/constants'; import { getRecurrencesWithEstimatedPrices } from '../prices/get-recurrences-with-estimated-prices'; +import { isVariablePrice, isVariablePriceItem } from '../prices/is-variable-price'; import { isCompositePrice } from '../prices/utils'; import { isTruthy } from '../shared/is-truthy'; import type { @@ -414,7 +415,7 @@ export const processOrderTableData = (data: any, i18n: I18n) => { /** * Process Quantity data */ - if (item._price?.variable_price && !isCoupon) { + if (isVariablePriceItem(item) && !isCoupon) { const itemPriceMapping = (item.parent_item ?? item).price_mappings?.find( (mapping: any) => mapping.price_id === item.price_id, ); @@ -428,7 +429,8 @@ export const processOrderTableData = (data: any, i18n: I18n) => { if (item.external_fees_metadata) { const unit = isCompositePrice(item) && Array.isArray(item._price?.price_components) - ? item._price?.price_components.find((component: Price) => component.variable_price && component.unit)?.unit + ? item._price?.price_components.find((component: Price) => isVariablePrice(component) && component.unit) + ?.unit : item._price?.unit; item.external_fees_details = processExternalFeesDetails( diff --git a/src/variables/utils.ts b/src/variables/utils.ts index 8a9a771..ef43a50 100644 --- a/src/variables/utils.ts +++ b/src/variables/utils.ts @@ -1,5 +1,6 @@ import { formatAmount, formatAmountFromString, formatPriceUnit } from '../money/formatters'; import { PricingModel } from '../prices/constants'; +import { isVariablePriceItem } from '../prices/is-variable-price'; import { isCompositePrice } from '../prices/utils'; import { isTruthy } from '../shared/is-truthy'; import type { @@ -469,7 +470,7 @@ export const getFormattedTieredDetails = ( }; export const getQuantity = (item: PriceItem, parentItem?: PriceItem) => { - if (!parentItem && item._price?.variable_price) { + if (!parentItem && isVariablePriceItem(item)) { const itemPriceMapping = item.price_mappings?.find((mapping) => mapping.price_id === item.price_id); const quantity = typeof itemPriceMapping?.value === 'number' @@ -478,10 +479,10 @@ export const getQuantity = (item: PriceItem, parentItem?: PriceItem) => { return item.quantity == 1 ? quantity : `${item.quantity} x ${quantity}`; } - if (parentItem && !item._price?.variable_price) { + if (parentItem && !isVariablePriceItem(item)) { return parentItem.quantity == 1 ? `${item.quantity}` : `${parentItem.quantity} x ${item.quantity}`; } - if (parentItem && item._price?.variable_price) { + if (parentItem && isVariablePriceItem(item)) { const itemPriceMapping = parentItem.price_mappings?.find((mapping) => mapping.price_id === item.price_id); const quantity =