fix(backend): serialize mongodb-memory-server startup - #518
Open
samxu01 wants to merge 1 commit into
Open
Conversation
samxu01
pushed a commit
that referenced
this pull request
Aug 6, 2026
Three users want to pay, so this wires the money path onto the entitlement seam that already existed. `entitlements.pro` was already the single gate the product reads; Stripe now writes it. **The webhook is the only source of truth.** Nothing in the checkout success path grants anything — the redirect is attacker-controllable and a session can be abandoned after redirect. Only a signature-verified event moves the flag. **Idempotent by durable marker.** Stripe retries for up to three days and guarantees neither once-only delivery nor ordering. `BillingEvent`'s unique index is the lock, claimed BEFORE the user is touched (test asserts the ordering): a crash between the two would otherwise apply a change with no record, and the retry would apply it twice. **Entitlement is DERIVED from subscription status, never toggled.** Every handler recomputes `pro` from the status Stripe reports, so out-of-order delivery converges instead of latching — pinned by a revoke-then-regrant test. `active`/`trialing` grant; everything else revokes. **Checkout grants only when money settled** (`payment_status === 'paid'`). A completed session with an async or failed payment is not money. **Raw body, mounted before express.json().** `constructEvent` verifies the exact bytes; a re-serialized body fails verification, which is the single most common way this integration breaks. Follows the existing Discord pattern in server.ts, and a test asserts the handler receives a Buffer. **The exit always works.** `POST /billing/portal` gives self-serve cancellation. A user who cannot find the exit disputes the charge instead. And revoking Pro leaves `cloudAgents` untouched — separate entitlements, pinned. **UI** — a v2 Plan panel on /v2/settings reading the same `=== true` check the backend gates on, so it can never claim a tier the API would refuse. It never writes the flag; after checkout the user may briefly still read Free, and the copy says so rather than faking optimistic state. en/zh parity asserted. Ships INERT: all three of STRIPE_SECRET_KEY / STRIPE_PRICE_ID / STRIPE_WEBHOOK_SECRET must be set or every billing route returns 503 rather than half-working. Documented in .env.example; keys belong in Secret Manager. 43 new tests (21 service, 14 route, 8 panel). Backend suite compared against a same-commit baseline: identical failure set apart from one known-flaky mongodb-memory-server ordering case that passes 4/4 in isolation (#518).
lilyshen0722
added a commit
that referenced
this pull request
Aug 6, 2026
* feat(billing): Stripe subscriptions end to end (#768) Three users want to pay, so this wires the money path onto the entitlement seam that already existed. `entitlements.pro` was already the single gate the product reads; Stripe now writes it. **The webhook is the only source of truth.** Nothing in the checkout success path grants anything — the redirect is attacker-controllable and a session can be abandoned after redirect. Only a signature-verified event moves the flag. **Idempotent by durable marker.** Stripe retries for up to three days and guarantees neither once-only delivery nor ordering. `BillingEvent`'s unique index is the lock, claimed BEFORE the user is touched (test asserts the ordering): a crash between the two would otherwise apply a change with no record, and the retry would apply it twice. **Entitlement is DERIVED from subscription status, never toggled.** Every handler recomputes `pro` from the status Stripe reports, so out-of-order delivery converges instead of latching — pinned by a revoke-then-regrant test. `active`/`trialing` grant; everything else revokes. **Checkout grants only when money settled** (`payment_status === 'paid'`). A completed session with an async or failed payment is not money. **Raw body, mounted before express.json().** `constructEvent` verifies the exact bytes; a re-serialized body fails verification, which is the single most common way this integration breaks. Follows the existing Discord pattern in server.ts, and a test asserts the handler receives a Buffer. **The exit always works.** `POST /billing/portal` gives self-serve cancellation. A user who cannot find the exit disputes the charge instead. And revoking Pro leaves `cloudAgents` untouched — separate entitlements, pinned. **UI** — a v2 Plan panel on /v2/settings reading the same `=== true` check the backend gates on, so it can never claim a tier the API would refuse. It never writes the flag; after checkout the user may briefly still read Free, and the copy says so rather than faking optimistic state. en/zh parity asserted. Ships INERT: all three of STRIPE_SECRET_KEY / STRIPE_PRICE_ID / STRIPE_WEBHOOK_SECRET must be set or every billing route returns 503 rather than half-working. Documented in .env.example; keys belong in Secret Manager. 43 new tests (21 service, 14 route, 8 panel). Backend suite compared against a same-commit baseline: identical failure set apart from one known-flaky mongodb-memory-server ordering case that passes 4/4 in isolation (#518). * docs(billing): record why automatic_tax is absent from the Checkout session The Stripe price is now tax_behavior: inclusive, so a customer pays exactly the advertised $12 in every jurisdiction rather than $12 plus whatever their region adds. Without that setting the Australian preview billed $13.20 against a landing page promising $12. automatic_tax stays off: Stripe Tax is not activated on the account and enabling it would fail session creation. Inclusive pricing is the half that cannot be changed later without moving the sticker price, so it is set now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeUH4HVDsDHYPsHJthXjB8 * fix(landing): the Pro card promised "Free in beta" above a $12 price The badge shipped with the pricing rewrite and survived the commit that turns real billing on, so the page currently advertises the paid tier as free in both locales ("Free in beta" / "公测期免费") directly above "$12/human/mo". Charging someone who signed up under that banner is a dispute, not a misunderstanding. Removed rather than reworded: the accent border already marks the featured tier, and every short badge available for a paid plan is either a price promise we would have to keep or a popularity claim we cannot substantiate. The absolutely-positioned rule went with it. Guarded by a test asserting no tier that shows a non-zero price describes itself as free. Scoped to the cost-describing fields only — the feature bullets legitimately say "Everything in Cloud free", which names a tier rather than making a price claim. Verified by reintroducing the badge and watching both locales fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeUH4HVDsDHYPsHJthXjB8 --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resolves TASK-080. Serializes in-memory MongoDB startup in backend test utilities so concurrent suite setup cannot race MongoMemoryServer.create. Adds async-mutex to backend devDependencies. Rollback plan: revert the test utility guard and package dependency if follow-up validation shows no longer needed. Zero-downtime: ✓.