From de3bd6f811d984c030d1cbbad8173c2d9113a5a8 Mon Sep 17 00:00:00 2001 From: Julian Maurer Date: Tue, 10 Mar 2026 17:02:13 +0100 Subject: [PATCH 1/2] Add helper to determin variable prices --- .gitignore | 1 + src/index.ts | 1 + .../__tests__/is-variable-price.test.ts | 91 +++++++++++++++++++ src/prices/is-variable-price.ts | 36 ++++++++ 4 files changed, 129 insertions(+) create mode 100644 src/prices/__tests__/is-variable-price.test.ts create mode 100644 src/prices/is-variable-price.ts diff --git a/.gitignore b/.gitignore index e109f433..8d974249 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ coverage .idea .yalc yalc.lock +.epilot-docs diff --git a/src/index.ts b/src/index.ts index 6b50c51e..ae4a6697 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 00000000..5e122c14 --- /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 00000000..a28ec33c --- /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); +}; From 3f1e3590e9e1cf08cda259d79cad45f389a64b01 Mon Sep 17 00:00:00 2001 From: Julian Maurer Date: Tue, 10 Mar 2026 17:24:50 +0100 Subject: [PATCH 2/2] Ensure prices are variable prices before applying input mapping --- src/__tests__/fixtures/price-getag.samples.ts | 3 +++ src/__tests__/fixtures/price.samples.ts | 2 ++ src/computations/compute-price-item.ts | 3 ++- src/exports.test.ts | 2 ++ src/variables/index.test.ts | 2 ++ src/variables/process-order-table-data.ts | 6 ++++-- src/variables/utils.ts | 7 ++++--- 7 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/__tests__/fixtures/price-getag.samples.ts b/src/__tests__/fixtures/price-getag.samples.ts index 27638e7f..c3f5f04f 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 c02159d1..b8845937 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 82217944..2da3e9dd 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 b8375da2..6122d511 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/variables/index.test.ts b/src/variables/index.test.ts index d6569acd..691e1b27 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 e845c4dd..f442937a 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 8a9a7712..ef43a50e 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 =