Skip to content

fix(ui/purchases): preserve per-rec payment in Configure Purchase fan-out#710

Merged
cristim merged 3 commits into
feat/multicloud-web-frontendfrom
fix/699-multi-bucket-payment-seed
May 25, 2026
Merged

fix(ui/purchases): preserve per-rec payment in Configure Purchase fan-out#710
cristim merged 3 commits into
feat/multicloud-web-frontendfrom
fix/699-multi-bucket-payment-seed

Conversation

@cristim

@cristim cristim commented May 25, 2026

Copy link
Copy Markdown
Member

Closes #699. Multi-bucket Configure Purchase modal was seeding all selected recs with the toolbar default payment (all-upfront) regardless of each rec's actual payment option, silently mis-billing on bulk purchases.

Changes

  1. Bucket key (frontend/src/recommendations.ts:3155) — added |${r.payment ?? ''} so recs with different payments separate into payment-uniform buckets, mirroring the existing term-based fan-out.
  2. resolveBucketPaymentSeed — after the override lookup (account override still takes priority), falls through to recs[0].payment when it is supported for the (provider, service, term) cell rather than unconditionally seeding from toolbar.payment. Multi-account buckets, missing-payment, or unsupported-payment values still fall back to toolbar.

Tests

  • Added test case (e) in Issue #111: per-bucket Payment seed...: two AWS EC2 1yr recs with partial-upfront + no-upfront now produce 2 buckets with payments ['no-upfront', 'partial-upfront'] instead of one collapsed bucket seeded to all-upfront. Test failed pre-fix, passes post-fix.
  • 1932/1932 frontend tests pass. TypeScript clean.

Summary by CodeRabbit

  • Bug Fixes

    • Bulk-purchase bucketing now distinguishes recommendations by their individual payment option and seeds payment correctly instead of always using the toolbar default.
  • Tests

    • Added regression test verifying separate buckets and correct payment seeding when recommendations differ only by payment; test suite now resets fan-out state before each test.

Review Change Stack

…igure Purchase preserves per-rec payment

Before this fix, recs with identical (provider, service, term) but different
per-rec payment values were bucketed together under the same key and seeded
from toolbar.payment ('all-upfront' by default), silently overriding each
rec's actual payment option. The Configure Purchase modal then submitted all
commitments as 'all-upfront' regardless of what the recommendations specified.

Two-part fix:
1. Add `payment` to the bucket key at handleBulkPurchaseClick so recs with
   different payments fan into separate, payment-uniform buckets. Mirrors
   the existing 'bundle B' term-key approach.
2. Update resolveBucketPaymentSeed to seed from recs[0].payment (uniform
   within the bucket after fix 1) before falling back to toolbar.payment,
   matching the resolvePerRecPaymentSeed precedence used by the single-bucket
   path.

Closes #699
@cristim cristim added triaged Item has been triaged priority/p1 Next up; this sprint severity/medium Moderate harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/s Hours type/bug Defect labels May 25, 2026
@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@cristim, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 15 minutes and 42 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9bb73888-331e-463b-a33e-8222eed42f8c

📥 Commits

Reviewing files that changed from the base of the PR and between 8e21a1b and 40f2826.

📒 Files selected for processing (2)
  • frontend/src/__tests__/recommendations.test.ts
  • frontend/src/recommendations.ts
📝 Walkthrough

Walkthrough

Includes normalized per-rec payment in bulk-purchase bucket keys and rewrites resolveBucketPaymentSeed so each bucket is seeded from the bucket's rec payment (or a supported account override); tests clear fan-out state and add a regression validating multi-payment bucketing.

Changes

Fix multi-bucket payment seeding for Issue #699

Layer / File(s) Summary
Tests: clear fan-out state & add regression
frontend/src/__tests__/recommendations.test.ts
Make suite beforeEach async and call clearFanOutBuckets(); add Issue #699 regression asserting differing rec.payment values produce separate fan-out buckets seeded from the recs with paymentSource: toolbar when no override exists.
Normalize payment and include in bucket key
frontend/src/recommendations.ts
Add normalizeBulkPayment() and include normalized per-rec payment in the constructed bulk-purchase bucket key so selections differing only by payment form distinct buckets.
ResolveBucketPaymentSeed precedence & fallback
frontend/src/recommendations.ts
Rewrite resolveBucketPaymentSeed to guard empty recs, only consult account overrides when all recs share a single non-empty cloud_account_id, accept overrides only for supported (provider,service,term,payment) combos, otherwise seed from normalized recs[0].payment when supported, falling back to toolbar payment last.

Sequence Diagram

sequenceDiagram
  participant User
  participant handleBulkPurchase as handleBulkPurchaseClick
  participant Buckets as Fan-out Buckets
  participant resolveSeed as resolveBucketPaymentSeed
  participant Modal as Configure Purchase Modal
  User->>handleBulkPurchase: Select recs (same provider/service/term, different payments)
  handleBulkPurchase->>Buckets: Create bucket key (provider,service,term,normalized payment)
  Buckets->>Buckets: Split into separate buckets per payment
  Buckets->>resolveSeed: For each bucket, request payment seed
  resolveSeed->>resolveSeed: If uniform non-empty cloud_account_id -> lookup override
  resolveSeed->>resolveSeed: If override supports (provider,service,term,payment) -> use override
  resolveSeed->>resolveSeed: Else if recs[0].payment normalized & supported -> use recs[0].payment
  resolveSeed->>Modal: Return payment + paymentSource for bucket
  Modal->>User: Show Configure Purchase modal seeded per bucket
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

  • LeanerCloud/CUDly#193: Related changes to per-bucket payment seeding and paymentSource handling in frontend bucketing logic.
  • LeanerCloud/CUDly#682: Backend changes emitting per-payment recommendation variants that align with this frontend bucketization update.
  • LeanerCloud/CUDly#325: Changes that interact with fan-out/bulk bucketization and per-row purchase workflows affected by this PR.

Poem

🐰 I hop through rows with payments bright,

Fan out the buckets, split them right.
Each bucket seeds from its own song,
No more one-size-all going wrong.
Hoppity cheers for purchases made light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main fix: preserving per-recommendation payment values in the Configure Purchase fan-out, which is the core bug fix addressed by including payment in the bucket key.
Linked Issues check ✅ Passed The pull request fully implements the primary objective from issue #699: including normalized payment in the bucket key so recommendations with different payments fan into separate buckets, enabling correct per-rec payment seeding instead of defaulting to toolbar payment.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the multi-bucket Configure Purchase payment seeding bug: bucket key normalization, payment canonicalization via normalizeBulkPayment(), and resolveBucketPaymentSeed logic updates align with issue #699 requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/699-multi-bucket-payment-seed

Comment @coderabbitai help to get the list of available commands and usage tips.

@cristim

cristim commented May 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/recommendations.ts`:
- Around line 3157-3158: Normalize incoming payment synonyms before building
bucket keys and seeding: ensure the payment value (r.payment / rec.payment) is
mapped so that 'upfront' becomes 'all-upfront' (the form used by
BulkPurchasePayment/paymentOptionsFor) prior to computing bucketService/key and
any seeding logic; update the code around bucketService/key (and the analogous
block at lines ~3319-3323) to use the normalized payment variable when
constructing the key and when storing/returning bucket seeds, keeping references
to isSavingsPlanService and SAVINGS_PLANS_BUCKET_KEY unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: da979f9e-402e-44a8-bb27-1858f7615623

📥 Commits

Reviewing files that changed from the base of the PR and between f3240f4 and 46356d8.

📒 Files selected for processing (2)
  • frontend/src/__tests__/recommendations.test.ts
  • frontend/src/recommendations.ts

Comment thread frontend/src/recommendations.ts Outdated
Use normalizeBulkPayment() to map upstream payment synonyms ('upfront'
-> 'all-upfront') before building the fan-out bucket key and before
seeding resolveBucketPaymentSeed. Without this, two recs that share the
same effective payment but differ only in the synonym form ('upfront' vs
'all-upfront') were keyed into separate buckets, defeating the #699 fix.

Closes: CR Major finding on PR #710 (coderabbitai comment 3299769088).
@cristim

cristim commented May 25, 2026

Copy link
Copy Markdown
Member Author

CR Major finding resolved (commit 8e21a1b)

Finding: rec.payment used raw as bucket discriminator; upstream 'upfront' (Azure synonym) and 'all-upfront' (canonical) created separate buckets for the same effective payment.

Fix: Added normalizeBulkPayment(payment: string | undefined): BulkPurchasePayment | null in recommendations.ts (maps 'upfront' -> 'all-upfront', all other known BulkPurchasePayment values pass through, unknown/undefined returns null). Applied at two sites:

  1. Bucket key construction: normalizeBulkPayment(r.payment) ?? '' replaces r.payment ?? ''
  2. resolveBucketPaymentSeed: normalizeBulkPayment(r0.payment) before the isPaymentSupported check

Test added: test (f) in the Issue #111 suite: two recs with payment: 'upfront' and payment: 'all-upfront' for the same (aws, ec2, term=1) cell now collapse into one bucket (single-bucket happy path). Issue #699 regression test (e) still passes.

Note: 'monthly' is preserved as 'monthly' (not mapped to 'no-upfront') because BulkPurchasePayment includes 'monthly' as a distinct GCP value, and isPaymentSupported already handles it.

Full suite: 1933 passed, 1 skipped, 0 failed. TypeScript: no errors.

@coderabbitai review

@cristim

cristim commented May 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

@cristim: The fix looks well-structured — introducing a dedicated normalizeBulkPayment normalizer at both callsites (key construction and seed resolution) is a clean boundary to enforce payment canonicalization. The preservation of 'monthly' as a distinct GCP value rather than collapsing it to 'no-upfront' is the correct call given BulkPurchasePayment treats it separately.

Triggering a review of the updated commits now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/recommendations.ts (1)

3324-3335: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize override payments before seeding the bucket.

match.payment still skips normalizeBulkPayment(). If an account override carries Azure/GCP's 'upfront' synonym, this branch seeds FanOutBucket.payment with a value the fan-out dropdown never renders, so the modal can display all-upfront while the backing bucket state/post body still holds upfront.

Suggested fix
-      if (
-        match
-        && match.payment
-        // Defensive: only honour the override when the (provider, service,
-        // term, payment) combo is actually supported. A stale or hand-saved
-        // override pointing at an unsupported payment for this term shouldn't
-        // poison the dropdown — fall through to rec.payment seed.
-        && isPaymentSupported(provider, r0.service, term, match.payment as CompatPayment)
-      ) {
+      const overridePayment = normalizeBulkPayment(match?.payment);
+      if (
+        overridePayment
+        // Defensive: only honour the override when the (provider, service,
+        // term, payment) combo is actually supported. A stale or hand-saved
+        // override pointing at an unsupported payment for this term shouldn't
+        // poison the dropdown — fall through to rec.payment seed.
+        && isPaymentSupported(provider, r0.service, term, overridePayment)
+      ) {
         return {
-          payment: match.payment as BulkPurchaseToolbarState['payment'],
+          payment: overridePayment,
           source: 'override',
         };
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/recommendations.ts` around lines 3324 - 3335, The override
branch currently returns match.payment directly without running it through
normalizeBulkPayment, allowing synonyms like 'upfront' to persist; update the
branch so you first compute const normalized =
normalizeBulkPayment(match.payment) (or similar), use normalized when calling
isPaymentSupported(provider, r0.service, term, normalized) and return payment:
normalized (as BulkPurchaseToolbarState['payment']) and source: 'override' so
the FanOutBucket.payment is seeded with the normalized value the UI expects.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@frontend/src/recommendations.ts`:
- Around line 3324-3335: The override branch currently returns match.payment
directly without running it through normalizeBulkPayment, allowing synonyms like
'upfront' to persist; update the branch so you first compute const normalized =
normalizeBulkPayment(match.payment) (or similar), use normalized when calling
isPaymentSupported(provider, r0.service, term, normalized) and return payment:
normalized (as BulkPurchaseToolbarState['payment']) and source: 'override' so
the FanOutBucket.payment is seeded with the normalized value the UI expects.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 710f2ace-b70b-4bd7-8f1b-d4810149cd42

📥 Commits

Reviewing files that changed from the base of the PR and between 46356d8 and 8e21a1b.

📒 Files selected for processing (2)
  • frontend/src/__tests__/recommendations.test.ts
  • frontend/src/recommendations.ts

In resolveBucketPaymentSeed, match.payment was passed raw to
isPaymentSupported and returned directly. An account override carrying
Azure's 'upfront' synonym would seed FanOutBucket.payment = 'upfront',
a value the fan-out dropdown never renders (it only lists canonical
BulkPurchasePayment values), while the backing state still held 'upfront'.

Fix: compute overridePayment = normalizeBulkPayment(match?.payment) first,
then use overridePayment in both the isPaymentSupported guard and the
return value. Synonyms like 'upfront' now normalize to 'all-upfront'
before reaching the bucket, keeping the displayed value and submitted
value in sync.

Add test (f): azure/vm override carrying 'upfront' produces two fan-out
buckets each seeded with payment='all-upfront' and source='override'.
Rename previous test (f) to (g) to preserve ordering.

Resolves CR outside-diff finding on PR #710.
@cristim

cristim commented May 25, 2026

Copy link
Copy Markdown
Member Author

CR outside-diff finding resolved (commit 40f2826)

Finding: resolveBucketPaymentSeed used match.payment raw, skipping normalizeBulkPayment(). An account override carrying Azure's 'upfront' synonym would seed FanOutBucket.payment = 'upfront' -- a value the fan-out dropdown never renders -- while the backing bucket state and POST body still held 'upfront'. Inconsistent UX + data drift.

Fix (frontend/src/recommendations.ts:3324-3337): introduced const overridePayment = normalizeBulkPayment(match?.payment) and use it in both the isPaymentSupported guard and the return value. Removed the now-unnecessary as CompatPayment and as BulkPurchaseToolbarState['payment'] casts -- BulkPurchasePayment is already a subtype of CompatPayment.

Test added (test (f) in the per-bucket Payment seed describe block): two azure/vm recs, same single account, different terms to force fan-out; account override carries payment: 'upfront'. Verifies both buckets emerge with payment === 'all-upfront' and paymentSource === 'override'. Previous test (f) (rec-payment synonym collapse) renumbered to (g).

Counts: 1935 tests pass (1 pre-existing skip), tsc --noEmit clean.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

@cristim: This completes the normalization coverage across all three callsites — bucket key construction, rec-payment seed, and now the account override path. The const overridePayment = normalizeBulkPayment(match?.payment) introduction is the right approach: using the normalized value in both the isPaymentSupported guard and the return value ensures no synonym ever escapes into a FanOutBucket.payment that the dropdown can't render. Removing the as CompatPayment / as BulkPurchaseToolbarState['payment'] casts is a good cleanup since those were silently permitting the raw synonym pass-through.

The new test (f) covering the azure/override → 'upfront''all-upfront' path is exactly the right regression guard for this scenario.

Triggering a review of the updated commits now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim cristim merged commit 464146a into feat/multicloud-web-frontend May 25, 2026
5 checks passed
@cristim cristim deleted the fix/699-multi-bucket-payment-seed branch May 25, 2026 22:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/many Affects most users priority/p1 Next up; this sprint severity/medium Moderate harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant