Skip to content

fix(frontend): never offer Approve without the purchase amount - #497

Merged
cristim merged 1 commit into
mainfrom
fix/approval-modal-no-amount-fallback
Oct 5, 2026
Merged

cristim merged 1 commit into
mainfrom
fix/approval-modal-no-amount-fallback

Conversation

@cristim

@cristim cristim commented Oct 5, 2026

Copy link
Copy Markdown
Member

What

The approval modal no longer offers Approve purchase when it cannot show the amount being committed.

buildApprovalDetailsBody used to catch every getPurchaseDetails failure (403, 404, network error), and also the empty-recommendations case, and return a one-sentence body with no upfront cost, savings or per-recommendation table. Both callers then opened the confirm dialog with a live Approve button:

  • frontend/src/purchases-deeplink.ts: the email approval link
  • frontend/src/history.ts: the inline Approve button in Purchase History and the approval-queue card

Why

Approving executes a cloud commitment. The backend approvePurchase (internal/api/handler_purchases.go) approves the stored execution by ID and does not check an expected amount, so the modal is the only place the user sees the amount before committing.

How

  • buildApprovalDetailsBody now rejects when the details fail to load or contain no recommendations. Failures of the optional listAccounts and getDeploymentInfo calls still degrade gracefully, as before.
  • Deep-link: on rejection it shows a sticky error toast (role="alert") saying the purchase cannot be approved without showing the amount, includes the cause, says to open the link again to retry, moves the page to /purchases, and does not open the dialog. The cancel deep-link does not change.
  • History/queue: on rejection it shows an error toast and returns. runHistoryRowAction re-enables the row's buttons, so clicking Approve again is the retry. Cancel does not change.
  • Removed the now-unused fallback element and its .approval-details-fallback CSS rule.

Verification

Commands, run from frontend/ at the head commit:

  • npm ci && npm run typecheck: clean
  • npx jest --watchman=false: 92 suites passed; 3036 tests passed, 1 skipped (that skip was already there)
  • npx eslint on the touched files: no issues
  • npm run build: compiled with the same single webpack warning as before
  • npx playwright test tests-e2e/history-action-confirmation.spec.ts: 11 passed. The suite is hermetic, with API calls mocked through page.route against the production bundle.

New regression tests and their results without the fix (source files reverted to origin/main, tests kept):

  • purchases-deeplink.test.ts: two tests for the deep-link approve handler, run against the real toast and confirm dialog (details fetch rejects with a 403; details have no recommendations). Without the fix both fail because the dialog opens with Approve and the handler waits on it (Exceeded timeout of 5000 ms).
  • history-approve-button.test.ts: the same two cases for the History button. Without the fix: expect(confirmDialog).not.toHaveBeenCalled(), Received number of calls: 1.
  • history-action-confirmation.spec.ts (Playwright): the first details GET returns 403. The test asserts the alert is visible, no modal opens and the button is enabled again; it then retries, sees $1,200 in the modal and approves. Without the fix: expect(locator).toContainText(expected) failed ... element(s) not found.
  • Totals without the fix: 4 jest tests fail and 1 Playwright test fails. With the fix all pass.
  • Mutation check: disabling the empty-recommendations guard makes both "no recommendations" tests fail again. The mutation was then reverted.

Existing fixtures changed: they mocked recommendations: [] (or left getPurchaseDetails unmocked, in history-approval-queue.test.ts) and expected the dialog to open. They now return one priced recommendation.

Fixture-based evidence: the jest and Playwright runs mock the network. I did not run the change against a live backend.

For the reviewer

  • Behaviour change for email-only approvers. A logged-in user who approves through the email token but lacks view:purchases, or whose account scope excludes the execution, gets a 403 or 404 from GET /api/purchases/{id}. Before this change they could approve without seeing the amount. Now they cannot approve from the link at all. This is what the issue asks for, but it is a real change for those users.
  • An execution whose recommendations JSONB is still empty, a race the old comment attributed to the capacity_percent flow, now shows an error and a retry instead of an amount-free Approve.

Closes #247

buildApprovalDetailsBody caught every getPurchaseDetails failure, and the
empty-recommendations case, and returned a one-sentence body with no
upfront cost or savings. Both callers (the email deep-link and the History
and approval-queue Approve buttons) then opened the confirm dialog with a
live "Approve purchase" button, so a 403, 404 or network error asked the
user to commit money without showing how much.

The builder now rejects in those cases. The deep-link shows a sticky error
toast and leaves the page without opening the dialog; the History button
shows an error toast and re-enables the row so the user can retry. The
backend approves the stored execution without an amount check, so the
modal is the only place the amount is shown before the commitment.

Closes #247
@cristim cristim added 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 triaged Item has been triaged labels Oct 5, 2026
@coderabbitai

coderabbitai Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 9 billable files and costs up to $2.25.

  • 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 10 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available. Your 57 included PR review attempts over the past 7 days set your current allowance at 2 reviews 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: 7ffc55fe-51cf-4a76-a3d5-2472a1395f66
📥 Commits

Reviewing files that changed from the base of the PR and between a442002 and 98964d7.

📒 Files selected for processing (9)
  • frontend/src/__tests__/approval-details.test.ts
  • frontend/src/__tests__/history-approval-queue.test.ts
  • frontend/src/__tests__/history-approve-button.test.ts
  • frontend/src/__tests__/purchases-deeplink.test.ts
  • frontend/src/approval-details.ts
  • frontend/src/history.ts
  • frontend/src/purchases-deeplink.ts
  • frontend/src/styles/components.css
  • frontend/tests-e2e/history-action-confirmation.spec.ts
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

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.

fix(frontend): the approval modal falls back to a no-amount body and still offers Approve

1 participant