From 47e9fee7ec506b231c5241c9abe6e1f2bc24fa8b Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 22 May 2026 11:18:12 +1000 Subject: [PATCH] PM-5043: Restore locked challenge fee fallback What was broken Locked challenge rows in the billing account details modal could show a blank Challenge Fee value when challenge markup hydration did not resolve, even though the row still had the billing-account markup needed to calculate the fee. Root cause The prior PM-5043 modal fix moved challenge rows to challenge-specific markup, but canonical challenge rows returned undefined while hydration was missing or failed. Locked rows store member-payment subtotals directly, so the fee calculation had no fallback markup and rendered "-". What was changed The modal now falls back to billing-account markup when challenge-specific markup is unavailable, while preserving hydrated challenge markup values including zero. This keeps locked challenge fees populated without changing consumed-row subtotal handling. Any added/updated tests Added BillingAccountLineItemsModal regression coverage for a locked challenge row whose challenge markup cannot be loaded, asserting the member payments remain unchanged and the Challenge Fee is still calculated. --- .../BillingAccountLineItemsModal.spec.tsx | 27 +++++++++++++++++++ .../BillingAccountLineItemsModal.tsx | 8 +++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/apps/work/src/lib/components/BillingAccountLineItemsModal/BillingAccountLineItemsModal.spec.tsx b/src/apps/work/src/lib/components/BillingAccountLineItemsModal/BillingAccountLineItemsModal.spec.tsx index faa3114c1..8cbd9e45e 100644 --- a/src/apps/work/src/lib/components/BillingAccountLineItemsModal/BillingAccountLineItemsModal.spec.tsx +++ b/src/apps/work/src/lib/components/BillingAccountLineItemsModal/BillingAccountLineItemsModal.spec.tsx @@ -177,6 +177,33 @@ describe('BillingAccountLineItemsModal', () => { .toBeNull() }) + it('uses billing-account markup for locked challenge fees when challenge markup cannot be loaded', () => { + mockedFetchChallenge.mockRejectedValueOnce(new Error('Forbidden')) + + renderModal({ + ...baseBillingAccountDetails, + lockedAmounts: [ + { + amount: '28.6', + date: '2026-05-12T00:00:00.000Z', + externalId: '5fdf48d2-811f-4914-b713-9e5f423c907d', + externalName: 'Copilot and Admin with reviews', + externalType: 'CHALLENGE', + }, + ], + lockedBudget: 28.6, + markup: 0.33, + totalBudgetRemaining: 971.4, + }) + + expect(screen.getAllByText('$28.60')) + .toHaveLength(2) + expect(screen.getByText('$9.44')) + .toBeTruthy() + expect(screen.queryByText('-')) + .toBeNull() + }) + it('removes challenge markup once from consumed challenge charges before showing member payments', async () => { renderModal({ ...baseBillingAccountDetails, diff --git a/src/apps/work/src/lib/components/BillingAccountLineItemsModal/BillingAccountLineItemsModal.tsx b/src/apps/work/src/lib/components/BillingAccountLineItemsModal/BillingAccountLineItemsModal.tsx index 8849d0d81..3aecd2cde 100644 --- a/src/apps/work/src/lib/components/BillingAccountLineItemsModal/BillingAccountLineItemsModal.tsx +++ b/src/apps/work/src/lib/components/BillingAccountLineItemsModal/BillingAccountLineItemsModal.tsx @@ -272,10 +272,11 @@ async function fetchChallengeDetailsById( * @param item Billing-account line item being displayed. * @param billingAccountDetails Billing account detail payload. * @param challengeDetailsById Hydrated challenge details, or `undefined` while loading. - * @returns Challenge-specific markup, billing-account fallback for legacy - * rows, or `undefined` when the challenge row is still being hydrated. + * @returns Challenge-specific markup when available, otherwise the billing-account markup fallback. * @remarks Canonical challenge rows use challenge billing markup so `0` markup - * challenges do not inherit the billing account default fee. + * challenges do not inherit the billing account default fee once hydrated. The + * billing-account fallback keeps locked rows from showing a blank fee when + * challenge hydration is unavailable. */ function getLineItemChallengeMarkup( item: BillingAccountLineItem, @@ -291,6 +292,7 @@ function getLineItemChallengeMarkup( } return challengeDetailsById?.get(challengeId)?.billing?.markup + ?? billingAccountDetails.markup } /**