fix(organization): render a refused coupon's reason as text, not the raw body - #1769
Conversation
…raw body AddCouponModal passed the central manager's 400/409 body straight into a toast description. A Harper 5 central manager answers with an RFC 9457 object, and React cannot render one, so a rejected coupon threw React error #31 inside the root Toaster (seen in RUM on dev.studio, right after a POST /Coupon 400). Resolve the refusal as text through describeError, which already maps both the problem-details shape and the legacy string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request refactors the coupon submission logic for organizations to handle 400 and 409 error responses by extracting the error message using describeError and returning it, and adds corresponding unit tests. Feedback on the changes points out a potential bug where an empty error string would evaluate to falsy in AddCouponModal, incorrectly triggering a success toast. It is recommended to strictly check for error === undefined and restore the fallback error message.
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||
…o text Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cb1kenobi
left a comment
There was a problem hiding this comment.
Rejected coupons are converted to a string through describeError before the toast, which stops a Harper 5 Problem Details object from crashing the root toaster. The empty-body false-success case is already in the thread and is pinned by the new test plus describeError's non-empty default. No remaining blocking defects on the changed lines.
—
Reviewed e25853d
cb1kenobi
left a comment
There was a problem hiding this comment.
Rejected coupons are converted to a string through describeError before the toast, so a Harper 5 Problem Details object no longer crashes the root toaster. The empty-body false-success case was already raised on AddCouponModal and is pinned by the new test plus describeError's non-empty default. No remaining blocking defects on the changed lines.
—
Reviewed e25853d
Summary
A rejected coupon crashes Studio against a central manager running Harper 5.
onAddCouponToOrganizationSubmitaccepts 400/409 throughvalidateStatusand returns the body unchanged.AddCouponModalthen puts that body straight intotoast.error({ description }).A Harper 5 central manager sends
ClientErrors as RFC 9457 Problem Details, for example{ type, code, title, status, instance }. React cannot render an object as a child, so the root<Toaster>throws React error #31.Found in the daily RUM review (2026-09-30). On dev.studio, one session hit
POST /Coupon→ 400, and 180ms later gotMinified React error #31 … object with keys {type, code, title, status, instance}. The component stack is sonner's toast insideToaster, which is mounted at the app root. The central manager'sCoupon.postthrows 400 for a coupon Stripe rejects and 409 for one already applied (central-manager/src/resources/Coupon.js), so every refusal took this path. Prod is not affected yet, but it will be once its central manager moves to Harper 5.Fix
describeError(...).message. That function already maps Problem Details (title and detail), legacy strings and{ error | message }bodies, and it never returns an empty string. The mutation's return type is nowPromise<string | undefined>.|| 'Failed to add coupon.'fallback could no longer be reached, so it was removed.validateStatusis the only such call site that renders the body. The other one,getSearchByValue's 404, is not rendered.Decisions
string | undefinedrather than throwing, so a rejected coupon stays a form answer and doesn't reach the global toast or RUM.code. The global toast uses that code as its heading; this dialog shows only the title and detail.Test plan
addCouponToOrganization.test.tscovers three cases: a 204 success (and pins the POST path and payload), a Problem Details 409, and a plain-text 400.tsc -b, oxlint and dprint are all clean.toast.errorreceives a string is enforced by the types, not by a render test.Cross-model review
There were two rounds, with codex (gpt-5.6-sol), gemini and harper-domain;
independent=trueand it converged.describeErrorcould yield an empty message and show a fake success. The adjudicator rejected it with line citations:messagedefaults to non-empty anderrorTextnever returns''..tsimport extensions, since the repo convention is extensionless.no-receipton the 1Password SSH agent.Human-Review-Need: 3 @ 79306c2
🤖 Generated with Claude Code