Repository navigation
fix(frontend): fan-out modal Payment selects relabel without re-pricing #2070
Description
Summary
The fan-out modal's own Payment selects relabel a recommendation's payment without re-pricing it, exactly the defect #1903 fixes in the single-purchase modal. Changing a bucket's Payment, or a per-recommendation Payment inside a bucket, writes the new payment onto the row while the loaded upfront_cost, monthly_cost and savings stay at the previous payment's values. The submit path posts those unchanged numbers.
This matters more than a display bug because the backend does not re-price. validateAndTotalRecommendations sums the submitted costs into the execution row and the approval email, and recTotalCommitment multiplies the submitted monthly cost by the submitted term for the MaxPurchaseAmount cap. The recorded, emailed and cap-checked figures therefore describe a payment option the user did not buy, while the provider charges for the one they did.
Every multi-bucket or partially incompatible selection reaches this path, so it is not a rare corner.
Location
frontend/src/recommendations.ts:4468(bucket-level Payment select setsb.payment)frontend/src/recommendations.ts:4559(per-recommendation Payment select writesperRecPayments)- consumed unchanged at
frontend/src/app.ts:534
Failure scenario
A bulk selection produces an RDS bucket seeded all-upfront at $1,000 upfront. The user changes that bucket's Payment to partial-upfront. The label updates, the row still carries upfront_cost: 1000 from the all-upfront variant, and the POST submits partial-upfront paired with the all-upfront price. Test T9 in frontend/src/__tests__/purchase-modal-submit.test.ts currently encodes this un-repriced submission as the expected behaviour, and should be inverted when this is fixed.
Suggested fix
Apply the same treatment #1903 used: swap in the loaded sibling variant for the chosen payment, scaled to the bucket's capacity, and build the select's options from priced variants only so an unpriced combination cannot be chosen. The helpers added in that PR (pricedCellVariant, applyVariantChange) are reusable here.
Related
Follow-up from #1903 and #1904, found during adversarial review of their fix. The single-purchase modal is repaired; this is the same defect class in the fan-out path, which that PR deliberately left in scope for a separate change.
A second, cosmetic item belongs with it: recommendations.ts:4453 builds the bucket Payment options from paymentOptionsFor, so an incompatible seed renders as the first option even though the bucket is marked as skipped. Harmless now that the skip label, the totals and the submit filter agree, but it should go away with the re-pricing fix.
Activity
cristim commentedon Sep 11, 2026
Planning update: the backend-risk paragraph predates merged #2073. Current main reprices from stored variants before spend caps, totals, persistence and approval email; missing variants return 409. The remaining defect is the fan-out display/selection/submission contract, not the original client-price cap bypass. The follow-up will reuse #2071 pricing helpers and cover initialization overrides plus bucket and per-row edits, including capacity scaling and override keys when sibling IDs change. Implementation will start after #2071 merges, with failing-before/passing-after tests and intercepted native macOS browser verification. No live cloud purchases are part of verification.
cristim commentedon Sep 11, 2026
Independent fresh-context Astra review of PR #2071 reproduced this fan-out repricing gap identically on base 5405fd1 and candidate 8c00eaf: changing a no-upfront bucket to all-upfront retained $0 upfront / $1,000 monthly. The backend already reprices from stored recommendations, so the remaining issue is the misleading modal estimate and submitted frontend identity/price consistency, not backend trust of submitted amounts. This stays in #2070 for the next focused change after #2071; no additional issue is needed. Independent evidence and limitations: #2071 (comment)
cristim commentedon Sep 14, 2026
Implementation is local and not yet published. The candidate swaps full priced variants for initialization, bucket edits and per-row edits, preserving capacity scaling and explicit overrides. Native macOS verification passed 2,961 tests (one skipped) plus type checking, affected-file lint and production build. Independent final Astra review then found two remaining gaps: a now-unavailable variant can still be included in a bucket, and payment edits during an in-flight submission can change the preview without changing the captured request. An intercepted native Chrome run reproduced the latter: the preview changed from $2,500 to $3,500 while the already captured requests remained $2,500. Both are being corrected with failing-before/passing-after tests before renewed committed-head review and Chrome verification. This is fixture/intercepted evidence, not a real cloud purchase. Backend repricing from #2073 remains unchanged.
cristim commentedon Sep 15, 2026
Completed in #2094, merged as b14df7e. Native macOS tests/build/Chrome, independent exact-head Astra review, substantive full CodeRabbit review and CI all passed before normal merge. All nine post-merge workflows also passed, and all three deployed frontends serve app/recommendation source matching the merged commit. Authenticated live purchases were not exercised; behavioral verification used intercepted local fixtures. Full evidence and limitations are recorded on the PR.