Skip to content

fix(frontend): fan-out modal Payment selects relabel without re-pricing #2070

Closed

Description

@cristim

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 sets b.payment)
  • frontend/src/recommendations.ts:4559 (per-recommendation Payment select writes perRecPayments)
  • 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

  1. cristim commented on Sep 11, 2026

    @cristim
    MemberAuthor

    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.

  2. cristim commented on Sep 11, 2026

    @cristim
    MemberAuthor

    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)

  3. cristim commented on Sep 14, 2026

    @cristim
    MemberAuthor

    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.

  4. cristim commented on Sep 15, 2026

    @cristim
    MemberAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions