Skip to content

fix(purchase): scope plan fan-out recommendations to each account - #793

Merged
cristim merged 4 commits into
mainfrom
feat/631-plan-steps-real-recommendations
Oct 9, 2026
Merged

cristim merged 4 commits into
mainfrom
feat/631-plan-steps-real-recommendations

Conversation

@cristim

@cristim cristim commented Oct 9, 2026

Copy link
Copy Markdown
Member

Refs #631 (PR1 of 6; does not close it)

What

Plan fan-out safety prerequisite. executeMultiAccount copied ALL of a step's recommendations to every plan account, so once steps carry real recs each account would buy every account's recs. Now:

  • recs are partitioned by CloudAccountID; each account buys only its own, and its per-account row carries its own TotalUpfrontCost/EstimatedSavings (A3);
  • an empty step fails with ErrPlanStepNoRecommendations before the fan-out, instead of returning nil, being recorded completed and advancing the ramp (A1);
  • a rec with no account, or outside the plan's accounts, fails the step loudly, never dropped (A2).

No user-visible change today: plan steps still carry no recs and Add Purchases still returns 409.

Tests (fixture/mock evidence)

New: own-recs-and-totals, stray rec (nil and foreign account) fails, empty step fails and buys nothing, and TestManager_ExecutePurchase_MultiAccount now asserts 2 PurchaseCommitment calls (not 4). Mutation check: passing the unscoped base exec to executeForAccount fails that test. Existing multi-account fixtures updated to per-account recs. go test ./internal/purchase/, go test -tags integration ./internal/purchase/, go test ./internal/api/ and the api retry/fanout integration tests pass, under the repo test lock.

🤖 Generated with Claude Code

cristim and others added 2 commits October 9, 2026 18:31
Each plan account now buys only its own recommendations and its row carries its own totals. A step with no recommendations, or a rec not attributed to one of the plan's accounts, fails loudly instead of completing or being dropped. Refs #631.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@cristim cristim added urgency/this-sprint Within the current sprint triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm impact/many Affects most users effort/xl Multi-week / refactor type/feat New capability labels Oct 9, 2026
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 7 billable files and costs up to $1.75.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Or wait 21 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 87 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-platform/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 42e42905-b53b-4fe0-ab4a-baecaa1aa76f

📥 Commits

Reviewing files that changed from the base of the PR and between 7c0c9ad and d0247a3.


📒 Files selected for processing (7)
  • internal/api/handler_purchases_retry_fanout_test.go
  • internal/purchase/account_scope.go
  • internal/purchase/account_scope_test.go
  • internal/purchase/execution.go
  • internal/purchase/execution_test.go
  • internal/purchase/money_path_regression_test.go
  • internal/purchase/ramp_step_progress_integration_test.go


  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@cristim

cristim commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

Gate verdict (gate-1, independent adversarial review on claude-opus-5-5): APPROVE at d0247a3, merging.

Head history: I reviewed the production change at cf5320a. Since then: e6ad572 (two test-helper prealloc lines, checked), then d0247a3 (update-branch merge of main 7c0c9ad: frontend CSS and the go pkg bump). The PR-owned internal/ diff against main is byte-identical before and after that merge.

Paths: every plan execution enters through executePurchase. A row with no account and with plan accounts goes executeScopeAware -> executeMultiAccount, the only caller of executeForAccount. That covers approve, direct execute, run-now, the scheduler, root retry successors and claimAndRedrive re-drives. Rows with an account (child retry successors, scope recovered by reattachAccountScope) take the single-account path and carry the child's recs, which are scoped from this PR on. A1, A2 and A3 run before any account executes.

Fixtures: each changed fixture gives one rec per plan account. No assertion was removed or loosened. MultiAccount now also asserts 2 PurchaseCommitment calls and that each saved row carries only its own account's rec. The ramp counts (1/2/3/6) are unchanged. The rec-less guard and its tests are unchanged.

Totals: per account, the sum of UpfrontCost and Savings over that account's recs. This is the same convention as validateAndTotalRecommendations: plain float64 sums, no currency field, no rounding step. History's multi-rec projection reads these exec totals.

Fails-before and mutations (unit, real output):

  • execution.go from main, with the tests and helper kept: MultiAccount, PartialFailure_IsolatesAccounts, RunsAccountsInParallel, EmptyStepFailsAndBuysNothing and api TestRetryOfFailedRootRowStillFansOutToEveryAccount FAIL.
  • M1, bypass scoping: the same tests FAIL.
  • M2, drop A1: EmptyStepFailsAndBuysNothing FAILS ("expected ErrPlanStepNoRecommendations ... got nil").
  • M3, drop A2: StrayRecFailsLoudly FAILS (both subtests).
  • M4, copy the step totals: OwnRecsAndTotals FAILS (account_scope_test.go:43-46).

Local runs, git archive d0247a3f, go1.26.9, macOS: go build ./... && go vet ./... 0; go test ./internal/purchase/... ./internal/api/... 0; go test -race -tags=integration ./internal/purchase/... ./internal/api/... ./internal/server/... 0 (Docker Postgres, under the suite lock); make test-unit 0. Trial merge with #789 (2c15aa9) via git merge-tree: no conflicts; unit tests and purchase race integration tests exit 0. CI is 26/26 and CLEAN at d0247a3.

Non-blocking notes for PR2/PR3:
(1) executeSingleAccount does not check that each rec's CloudAccountID equals the row's CloudAccountID. Today this is latent, because only scoped child rows reach it. Worth a guard before resolved recs exist.
(2) The root row keeps the step's totals while the children carry per-account totals. History can list both. This predates this PR.
(3) The per-account totals are asserted on the helper, not on the saved per-account row.

Evidence is mocks and Postgres containers only; no live cloud calls.

@cristim
cristim merged commit 7d492ec into main Oct 9, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/xl Multi-week / refactor impact/many Affects most users priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/feat New capability urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant