From ecc5e54b2cfce5696f41d32a15339e54f8e9c393 Mon Sep 17 00:00:00 2001 From: Paulo Henriques Date: Tue, 4 Aug 2026 10:08:00 +0100 Subject: [PATCH] feat: support TaxItem alongside Tax after pricing-client bump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps @epilot/pricing-client to ^3.55.1, which introduced TaxItem: an ad-hoc tax rate with no backing entity, for custom/composite line items with no product or price reference to resolve tax from. Widens every tax parameter/return type from Tax to Tax | TaxItem across the computation chain. TaxItem has no _id (by design — it isn't persisted and can't be deduplicated by identity), which compute-totals.ts's tax-bucket aggregation previously assumed always existed. Added a small getTaxId() helper that only reads _id when present, instead of widening _id itself into an unsafe optional everywhere. Co-Authored-By: Claude Sonnet 5 --- .changeset/tidy-tigers-listen.md | 5 +++++ package.json | 4 ++-- pnpm-lock.yaml | 10 +++++----- src/computations/apply-discounts.ts | 3 ++- .../compute-external-dynamic-tariff-values.ts | 4 ++-- .../compute-external-get-agitem-values.ts | 4 ++-- .../compute-per-unit-price-item-values.ts | 4 ++-- src/computations/compute-price-item.ts | 3 ++- ...mpute-tiered-flat-fee-price-item-values.ts | 4 ++-- ...pute-tiered-graduated-price-item-values.ts | 4 ++-- ...compute-tiered-volume-price-item-values.ts | 4 ++-- src/computations/compute-totals.ts | 19 ++++++++++++++----- src/prices/get-price-tax.ts | 7 ++++++- src/shared/types.ts | 1 + src/taxes/extract-tax-from-price.ts | 4 ++-- src/taxes/get-tax-value.ts | 4 ++-- src/variables/getag/meter-fees-details.ts | 4 ++-- src/variables/getag/network-fees-details.ts | 4 ++-- src/variables/getag/other-fees-details.ts | 4 ++-- src/variables/getag/utils.ts | 4 ++-- 20 files changed, 61 insertions(+), 39 deletions(-) create mode 100644 .changeset/tidy-tigers-listen.md diff --git a/.changeset/tidy-tigers-listen.md b/.changeset/tidy-tigers-listen.md new file mode 100644 index 00000000..8477e969 --- /dev/null +++ b/.changeset/tidy-tigers-listen.md @@ -0,0 +1,5 @@ +--- +'@epilot/pricing': patch +--- + +Support TaxItem (an ad-hoc tax rate with no backing entity) alongside Tax wherever a tax object is accepted or returned, following the @epilot/pricing-client bump that introduced it. Ad-hoc/custom line items with no product or price reference can now be taxed without a full Tax entity. diff --git a/package.json b/package.json index 332cf4d1..b29ed009 100644 --- a/package.json +++ b/package.json @@ -67,13 +67,13 @@ "dinero.js": "^1.9.1" }, "peerDependencies": { - "@epilot/pricing-client": "^3.53.8", + "@epilot/pricing-client": "^3.55.1", "i18next": "^25.1.2" }, "devDependencies": { "@changesets/cli": "^2.30.0", "@epilot/eslint-config": "^3.0.5", - "@epilot/pricing-client": "^3.53.8", + "@epilot/pricing-client": "^3.55.1", "@types/dinero.js": "^1.9.4", "@vitest/coverage-v8": "^1.6.1", "concurrently": "^9.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a64fc2ab..a599abd5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,8 +19,8 @@ importers: specifier: ^3.0.5 version: 3.0.5(eslint@9.28.0)(typescript@5.8.3) '@epilot/pricing-client': - specifier: ^3.53.8 - version: 3.53.8(axios@1.9.0)(js-yaml@4.1.1) + specifier: ^3.55.1 + version: 3.55.1(axios@1.9.0)(js-yaml@4.1.1) '@types/dinero.js': specifier: ^1.9.4 version: 1.9.4 @@ -163,8 +163,8 @@ packages: eslint: '>= 9.0.0' typescript: '>= 5.0.0' - '@epilot/pricing-client@3.53.8': - resolution: {integrity: sha512-jGJ9TEEi+GR82XGCf+KWcOLKJR6mqoTO53UAro8thPpixCfleFBIFT1MVTtbIJvQ+BcOY96Us8dvQhZHIRXE5A==} + '@epilot/pricing-client@3.55.1': + resolution: {integrity: sha512-D/M34UK+D4kWiB3KEKzQfSuET3MvHdo2GcBlFVr4dhBVb+VfVZBUbytv5ch7fvLbEd2vXZ8is/5QepvIRd3bgQ==} peerDependencies: axios: ^1.0.0 || >=0.25.0 <1.0.0 @@ -2743,7 +2743,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - '@epilot/pricing-client@3.53.8(axios@1.9.0)(js-yaml@4.1.1)': + '@epilot/pricing-client@3.55.1(axios@1.9.0)(js-yaml@4.1.1)': dependencies: '@dazn/lambda-powertools-correlation-ids': 1.28.1 axios: 1.9.0 diff --git a/src/computations/apply-discounts.ts b/src/computations/apply-discounts.ts index 9cb4d2a2..dea25e36 100644 --- a/src/computations/apply-discounts.ts +++ b/src/computations/apply-discounts.ts @@ -1,3 +1,4 @@ +import type { TaxItem } from '@epilot/pricing-client'; import { isCashbackCoupon, isFixedValueCoupon, isPercentageCoupon } from '../coupons/guards'; import { toDineroFromInteger, toDinero } from '../money/to-dinero'; import { PricingModel } from '../prices/constants'; @@ -21,7 +22,7 @@ export const applyDiscounts = ( currency: Currency; isTaxInclusive: boolean; unitAmountMultiplier: number; - tax?: Tax; + tax?: Tax | TaxItem; coupon: Coupon; }, ): PriceItemsTotals => { diff --git a/src/computations/compute-external-dynamic-tariff-values.ts b/src/computations/compute-external-dynamic-tariff-values.ts index bbbe356f..9300a137 100644 --- a/src/computations/compute-external-dynamic-tariff-values.ts +++ b/src/computations/compute-external-dynamic-tariff-values.ts @@ -1,4 +1,4 @@ -import type { PriceDynamicTariff, Tax } from '@epilot/pricing-client'; +import type { PriceDynamicTariff, Tax, TaxItem } from '@epilot/pricing-client'; import type { Currency } from 'dinero.js'; import { toDinero } from '../money/to-dinero'; import { ModeDynamicTariff } from '../prices/constants'; @@ -19,7 +19,7 @@ export const computeExternalDynamicTariffValues = ({ isTaxInclusive: boolean; unitAmountMultiplier: number; externalFeeAmountDecimal?: string; - tax?: Tax; + tax?: Tax | TaxItem; }): PriceItemsTotals => { if (externalFeeAmountDecimal === undefined || dynamicTariff === undefined) { return { diff --git a/src/computations/compute-external-get-agitem-values.ts b/src/computations/compute-external-get-agitem-values.ts index 87b059b8..698b3af6 100644 --- a/src/computations/compute-external-get-agitem-values.ts +++ b/src/computations/compute-external-get-agitem-values.ts @@ -1,4 +1,4 @@ -import type { PriceGetAg, Tax } from '@epilot/pricing-client'; +import type { PriceGetAg, Tax, TaxItem } from '@epilot/pricing-client'; import type { Currency } from 'dinero.js'; import { toDinero, toDineroFromInteger } from '../money/to-dinero'; import { MarkupPricingModel, TypeGetAg } from '../prices/constants'; @@ -22,7 +22,7 @@ export const computeExternalGetAGItemValues = ({ unitAmountMultiplier: number; userInput: number; externalFeeAmountDecimal?: string; - tax?: Tax; + tax?: Tax | TaxItem; }): PriceItemsTotals => { if (externalFeeAmountDecimal === undefined || getAg === undefined || userInput === 0) { return { diff --git a/src/computations/compute-per-unit-price-item-values.ts b/src/computations/compute-per-unit-price-item-values.ts index aba57b03..a8745380 100644 --- a/src/computations/compute-per-unit-price-item-values.ts +++ b/src/computations/compute-per-unit-price-item-values.ts @@ -1,4 +1,4 @@ -import type { Tax } from '@epilot/pricing-client'; +import type { Tax, TaxItem } from '@epilot/pricing-client'; import type { Currency, Dinero } from 'dinero.js'; import { toDinero } from '../money/to-dinero'; import type { PriceItemsTotals } from '../prices/types'; @@ -15,7 +15,7 @@ export const computePerUnitPriceItemValues = ({ currency: Currency; isTaxInclusive: boolean; unitAmountMultiplier: number; - tax?: Tax; + tax?: Tax | TaxItem; }): PriceItemsTotals => { const unitAmount = toDinero(unitAmountDecimal, currency); const taxRate = getTaxValue(tax); diff --git a/src/computations/compute-price-item.ts b/src/computations/compute-price-item.ts index 2da3e9dd..e36a74dd 100644 --- a/src/computations/compute-price-item.ts +++ b/src/computations/compute-price-item.ts @@ -6,6 +6,7 @@ import type { RedeemedPromo, PriceItem, Price, + TaxItem, } from '@epilot/pricing-client'; import type { Currency } from 'dinero.js'; import { isValidCoupon } from '../coupons/guards'; @@ -86,7 +87,7 @@ export const computePriceItem = ( externalFeeMapping, redeemedPromos, }: { - tax?: Tax; + tax?: Tax | TaxItem; quantity: number; priceMapping?: PriceInputMapping; externalFeeMapping?: ExternalFeeMapping; diff --git a/src/computations/compute-tiered-flat-fee-price-item-values.ts b/src/computations/compute-tiered-flat-fee-price-item-values.ts index 76dc3912..464c32ea 100644 --- a/src/computations/compute-tiered-flat-fee-price-item-values.ts +++ b/src/computations/compute-tiered-flat-fee-price-item-values.ts @@ -1,4 +1,4 @@ -import type { PriceTier, Tax, Price } from '@epilot/pricing-client'; +import type { PriceTier, Tax, Price, TaxItem } from '@epilot/pricing-client'; import type { Currency } from 'dinero.js'; import { toDineroFromInteger } from '../money/to-dinero'; import type { PriceItemsTotals } from '../prices/types'; @@ -19,7 +19,7 @@ export const computeTieredFlatFeePriceItemValues = ({ currency: Currency; isTaxInclusive: boolean; quantityToSelectTier: number; - tax?: Tax; + tax?: Tax | TaxItem; quantity: number; isUsingPriceMappingToSelectTier: boolean; unchangedPriceDisplayInJourneys: Price['price_display_in_journeys']; diff --git a/src/computations/compute-tiered-graduated-price-item-values.ts b/src/computations/compute-tiered-graduated-price-item-values.ts index baadac53..8378f1f0 100644 --- a/src/computations/compute-tiered-graduated-price-item-values.ts +++ b/src/computations/compute-tiered-graduated-price-item-values.ts @@ -1,4 +1,4 @@ -import type { PriceTier, Tax, Price } from '@epilot/pricing-client'; +import type { PriceTier, Tax, Price, TaxItem } from '@epilot/pricing-client'; import type { Currency } from 'dinero.js'; import { toDineroFromInteger } from '../money/to-dinero'; import type { PriceItemsTotals } from '../prices/types'; @@ -20,7 +20,7 @@ export const computeTieredGraduatedPriceItemValues = ({ currency: Currency; isTaxInclusive: boolean; quantityToSelectTier: number; - tax?: Tax; + tax?: Tax | TaxItem; quantity: number; isUsingPriceMappingToSelectTier: boolean; unchangedPriceDisplayInJourneys: Price['price_display_in_journeys']; diff --git a/src/computations/compute-tiered-volume-price-item-values.ts b/src/computations/compute-tiered-volume-price-item-values.ts index ce870fdf..628e41a3 100644 --- a/src/computations/compute-tiered-volume-price-item-values.ts +++ b/src/computations/compute-tiered-volume-price-item-values.ts @@ -1,4 +1,4 @@ -import type { PriceTier, Tax, Price } from '@epilot/pricing-client'; +import type { PriceTier, Tax, Price, TaxItem } from '@epilot/pricing-client'; import type { Currency } from 'dinero.js'; import { toDineroFromInteger } from '../money/to-dinero'; import type { PriceItemsTotals } from '../prices/types'; @@ -18,7 +18,7 @@ export const computeTieredVolumePriceItemValues = ({ currency: Currency; isTaxInclusive: boolean; quantityToSelectTier: number; - tax: Tax | undefined; + tax: Tax | TaxItem | undefined; unitAmountMultiplier: number; unchangedPriceDisplayInJourneys: Price['price_display_in_journeys']; }): PriceItemsTotals => { diff --git a/src/computations/compute-totals.ts b/src/computations/compute-totals.ts index 4e20b44b..4b53ef37 100644 --- a/src/computations/compute-totals.ts +++ b/src/computations/compute-totals.ts @@ -18,6 +18,7 @@ import type { Price, PriceItem, Tax, + TaxItem, Currency, } from '../shared/types'; import { computeCompositePrice } from './compute-composite-price'; @@ -29,6 +30,13 @@ type ComputeAggregatedAndPriceTotalsOptions = { redeemedPromos?: Array; }; +/** + * TaxItem (an ad-hoc tax with no backing entity) has no _id — unlike Tax, it + * can't be shared/deduplicated across price items by identity, only by rate. + */ +const getTaxId = (tax?: Tax | TaxItem | Partial): string | undefined => + tax && '_id' in tax ? tax._id : undefined; + /** * Computes all the integer amounts for the price items using the string decimal representation defined on prices unit_amount field. * All totals are computed with a decimal precision of DECIMAL_PRECISION. @@ -161,9 +169,10 @@ const recomputeDetailTotals = ( * itemRateValue is only used for outdated prices, not migrated yet */ const itemRateValue = priceItemToAppend.taxes?.[0]?.rateValue; + const itemTaxId = getTaxId(itemTax); const tax = taxes.find( (item) => - (item.tax?._id && itemTax?._id && item.tax?._id === itemTax?._id) || + (item.tax?._id && itemTaxId && item.tax?._id === itemTaxId) || item.tax?.rate === itemTax?.rate || item.tax?.rate === itemRateValue, ); @@ -218,8 +227,8 @@ const recomputeDetailTotals = ( if (tax.tax && itemTax) { // Populates missing data in deprecated taxes - if (!tax.tax._id && itemTax._id) { - tax.tax._id = itemTax._id; + if (!tax.tax._id && itemTaxId) { + tax.tax._id = itemTaxId; } if (!tax.tax.type && itemTax.type) { @@ -227,10 +236,10 @@ const recomputeDetailTotals = ( } } } else if (itemTax) { - const { _id, type, rate } = itemTax; + const { type, rate } = itemTax; taxes.push({ tax: { - ...(_id && { _id }), + ...(itemTaxId && { _id: itemTaxId }), ...(type && { type }), rate, }, diff --git a/src/prices/get-price-tax.ts b/src/prices/get-price-tax.ts index 28c542fe..1a69b25a 100644 --- a/src/prices/get-price-tax.ts +++ b/src/prices/get-price-tax.ts @@ -1,9 +1,14 @@ +import type { TaxItem } from '@epilot/pricing-client'; import type { Tax, Price, TaxAmountDto } from '../shared/types'; /** * Gets a price tax with the proper tax behavior override */ -export const getPriceTax = (applicableTax?: Tax, price?: Price, priceItemTaxes?: TaxAmountDto[]): Tax | undefined => { +export const getPriceTax = ( + applicableTax?: Tax | TaxItem, + price?: Price, + priceItemTaxes?: TaxAmountDto[], +): Tax | TaxItem | undefined => { if (applicableTax) { return applicableTax; } diff --git a/src/shared/types.ts b/src/shared/types.ts index bd227aaf..ff7fb2ab 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -10,6 +10,7 @@ export type { PriceItems, PriceItemsDto, Tax, + TaxItem, TaxAmountDto, TaxAmount, CompositePrice, diff --git a/src/taxes/extract-tax-from-price.ts b/src/taxes/extract-tax-from-price.ts index 43ff9f6f..40f66175 100644 --- a/src/taxes/extract-tax-from-price.ts +++ b/src/taxes/extract-tax-from-price.ts @@ -1,8 +1,8 @@ -import type { CompositePriceItem, PriceItem, Tax } from '@epilot/pricing-client'; +import type { CompositePriceItem, PriceItem, Tax, TaxItem } from '@epilot/pricing-client'; import { isCompositePriceItem } from '../prices/utils'; export const extractTaxFromPriceItem = (item: PriceItem | CompositePriceItem) => { - let tax: Tax | undefined = undefined; + let tax: Tax | TaxItem | undefined = undefined; if ( isCompositePriceItem(item) && diff --git a/src/taxes/get-tax-value.ts b/src/taxes/get-tax-value.ts index 2f52b21b..c89ada3b 100644 --- a/src/taxes/get-tax-value.ts +++ b/src/taxes/get-tax-value.ts @@ -1,7 +1,7 @@ -import type { Tax } from '@epilot/pricing-client'; +import type { Tax, TaxItem } from '@epilot/pricing-client'; import { TaxRates } from './constants'; -export const getTaxValue = (tax?: Tax): number => { +export const getTaxValue = (tax?: Tax | TaxItem): number => { if (!tax) { return TaxRates.nontaxable; } diff --git a/src/variables/getag/meter-fees-details.ts b/src/variables/getag/meter-fees-details.ts index d94fbbf4..865cee92 100644 --- a/src/variables/getag/meter-fees-details.ts +++ b/src/variables/getag/meter-fees-details.ts @@ -1,5 +1,5 @@ import type { Currency } from 'dinero.js'; -import type { Tax } from '../../shared/types'; +import type { Tax, TaxItem } from '../../shared/types'; import type { I18n } from '../../shared/types'; import type { TimeFrequency } from '../../time-frequency/types'; import type { ExternalFeesMetadata, ExternalFeesDetails, ExternalFeesDetailsGroup } from '../types'; @@ -12,7 +12,7 @@ export const processMeterFeesDetails = ( i18n: I18n, billingPeriod: TimeFrequency, unitPricePeriod: TimeFrequency, - tax?: Tax, + tax?: Tax | TaxItem, variableUnit?: string, ) => { if (!result.groups) { diff --git a/src/variables/getag/network-fees-details.ts b/src/variables/getag/network-fees-details.ts index 292b32ff..13e65e8e 100644 --- a/src/variables/getag/network-fees-details.ts +++ b/src/variables/getag/network-fees-details.ts @@ -1,4 +1,4 @@ -import type { Currency, I18n, Tax } from '../../shared/types'; +import type { Currency, I18n, Tax, TaxItem } from '../../shared/types'; import type { TimeFrequency } from '../../time-frequency/types'; import type { ExternalFeesMetadata, ExternalFeesDetails, ExternalFeesDetailsGroup } from '../types'; import { getDetailsFee } from './utils'; @@ -10,7 +10,7 @@ export const processNetworkOperatingFeesDetails = ( i18n: I18n, billingPeriod: TimeFrequency, unitPricePeriod: TimeFrequency, - tax?: Tax, + tax?: Tax | TaxItem, variableUnit?: string, ) => { const type = externalFeesMetadata.inputs.type || 'power'; diff --git a/src/variables/getag/other-fees-details.ts b/src/variables/getag/other-fees-details.ts index 4c146382..1e74abe4 100644 --- a/src/variables/getag/other-fees-details.ts +++ b/src/variables/getag/other-fees-details.ts @@ -1,5 +1,5 @@ import type { Currency } from 'dinero.js'; -import type { Tax } from '../../shared/types'; +import type { Tax, TaxItem } from '../../shared/types'; import type { I18n } from '../../shared/types'; import type { TimeFrequency } from '../../time-frequency/types'; import type { ExternalFeesMetadata, ExternalFeesDetails, ExternalFeesDetailsGroup } from '../types'; @@ -12,7 +12,7 @@ export const processOtherFeesDetails = ( i18n: I18n, billingPeriod: TimeFrequency, unitPricePeriod: TimeFrequency, - tax?: Tax, + tax?: Tax | TaxItem, variableUnit?: string, ) => { const type = externalFeesMetadata.inputs.type || 'power'; diff --git a/src/variables/getag/utils.ts b/src/variables/getag/utils.ts index 4b5f824c..d6fc9378 100644 --- a/src/variables/getag/utils.ts +++ b/src/variables/getag/utils.ts @@ -2,7 +2,7 @@ import type { PriceGetAg, TariffTypeGetAg } from '@epilot/pricing-client'; import { formatFeeAmountFromString } from '../../getag/formatters'; import { DEFAULT_CURRENCY } from '../../money/constants'; import { toDinero } from '../../money/to-dinero'; -import type { Currency, I18n, Tax } from '../../shared/types'; +import type { Currency, I18n, Tax, TaxItem } from '../../shared/types'; import { getAmountWithTax } from '../../taxes/get-amount-with-tax'; import { normalizeValueToFrequencyUnit } from '../../time-frequency/normalizers'; import type { TimeFrequency } from '../../time-frequency/types'; @@ -77,7 +77,7 @@ const getDetailsFee = ({ i18n: I18n; billingPeriod: TimeFrequency; unitPricePeriod: TimeFrequency; - tax?: Tax; + tax?: Tax | TaxItem; variableUnit?: string; }): ExternalFeesDetailsFee | undefined => { if (!fee) {