diff --git a/libs/pricing/src/components/cart-summary/cart-summary.component.spec.ts b/libs/pricing/src/components/cart-summary/cart-summary.component.spec.ts index 4f18e9cdf48..3e59e2f9cb7 100644 --- a/libs/pricing/src/components/cart-summary/cart-summary.component.spec.ts +++ b/libs/pricing/src/components/cart-summary/cart-summary.component.spec.ts @@ -537,6 +537,29 @@ describe("CartSummaryComponent", () => { ).toContain("-$10.00"); }); + it("should compute total consistent with displayed rounded line items when chained discounts produce fractional cents", () => { + // Reproduces: 1x $47.88 seat with 20% off → $10 flat → 5% off, tax $2.15 + // Raw math: 47.88 - 9.576 - 10 - 1.4152 + 2.15 = 29.0388 → $29.04 (wrong) + // Rounded: 47.88 - 9.58 - 10 - 1.42 + 2.15 = 29.03 (correct) + const cart: Cart = { + passwordManager: { + seats: { quantity: 1, translationKey: "members", cost: 47.88 }, + }, + cadence: "annually", + estimatedTax: 2.15, + discounts: [ + { type: DiscountTypes.PercentOff, value: 20 }, + { type: DiscountTypes.AmountOff, value: 10 }, + { type: DiscountTypes.PercentOff, value: 5 }, + ], + }; + fixture.componentRef.setInput("cart", cart); + fixture.detectChanges(); + + const bottomTotal = fixture.debugElement.query(By.css("[data-testid='final-total']")); + expect(bottomTotal.nativeElement.textContent).toContain("$29.03"); + }); + it("should apply cascading subtotal when multiple percent-off discounts are stacked", () => { // Arrange const cartWithStackedPercents: Cart = { diff --git a/libs/pricing/src/types/discount.spec.ts b/libs/pricing/src/types/discount.spec.ts index bf30d12a019..12a8552afe9 100644 --- a/libs/pricing/src/types/discount.spec.ts +++ b/libs/pricing/src/types/discount.spec.ts @@ -34,6 +34,30 @@ describe("getAmount", () => { const discount: Discount = { type: DiscountTypes.PercentOff, value: 100 }; expect(getAmount(discount, 200)).toBe(200); }); + + it("should round result to 2 decimal places when percent produces fractional cents", () => { + const discount: Discount = { type: DiscountTypes.PercentOff, value: 20 }; + // 20% of $47.88 = $9.576 → rounds to $9.58 + expect(getAmount(discount, 47.88)).toBe(9.58); + }); + + it("should round result when applied to a running subtotal with fractional cents", () => { + const discount: Discount = { type: DiscountTypes.PercentOff, value: 5 }; + // 5% of $28.304 = $1.4152 → rounds to $1.42 + expect(getAmount(discount, 28.304)).toBe(1.42); + }); + + it("should round half-cent up", () => { + const discount: Discount = { type: DiscountTypes.PercentOff, value: 50 }; + // 50% of $0.01 = $0.005 → rounds to $0.01 + expect(getAmount(discount, 0.01)).toBe(0.01); + }); + + it("should round down when fractional cent is less than half", () => { + const discount: Discount = { type: DiscountTypes.PercentOff, value: 20 }; + // 20% of $47.82 = $9.564 → rounds down to $9.56 + expect(getAmount(discount, 47.82)).toBe(9.56); + }); }); describe("AmountOff", () => { diff --git a/libs/pricing/src/types/discount.ts b/libs/pricing/src/types/discount.ts index f8d5b3a8099..271414577ef 100644 --- a/libs/pricing/src/types/discount.ts +++ b/libs/pricing/src/types/discount.ts @@ -23,7 +23,7 @@ export const getAmount = (discount: Discount, baseAmount: number): number => { switch (discount.type) { case DiscountTypes.PercentOff: { const percentage = discount.value < 1 ? discount.value : discount.value / 100; - return baseAmount * percentage; + return Math.round(baseAmount * percentage * 100) / 100; } case DiscountTypes.AmountOff: return discount.value;