Repository navigation
Conversation
Git-on-my-level
left a comment
There was a problem hiding this comment.
The production fix here is correct and well-tested; one blocking repo-gate item (not code) before merge.
Findings:
- backend/routers/payment.py:789 — adding
payment_behavior='error_if_incomplete'to the cross-planSubscription.modifyis the right call, and actually stricter than thepending_if_incompletesuggested in #23301:pending_if_incompletewould still leave the subscriptionpast_due, which the projector demotes to Free —error_if_incompletemakes Stripe fail the update atomically. - backend/routers/payment.py:799 — the status-not-in-(active, trialing) guard sits correctly before any DB write, content unlock, or fair-use clear, so a degenerated status can no longer wipe the local subscription record.
- backend/routers/payment.py:887-901 — the new
CardError/StripeErrorhandlers reuse_stripe_client_error_detail(payment.py:86), keeping internal Stripe IDs in server logs and out of response bodies; consistent with the existing sanitization pattern. - backend/tests/unit/test_payment_proration_decline_protection.py — I ran the new suite plus the existing payment tests on this head: 5/5 new and 54/54 existing pass. The three behavioral tests (
test_cross_plan_upgrade_passes_payment_behavior_to_stripe,test_cross_plan_upgrade_card_declined_preserves_current_plan,test_cross_plan_upgrade_non_active_result_aborts_without_database_update) exercise the real endpoint through mocked Stripe. Note the first two tests (test_cross_plan_change_declares_error_if_incomplete,test_upgrade_subscription_endpoint_has_card_error_handler) are static source-string tripwires rather than behavioral coverage; per the repo's testing guide, please label them as such in their docstrings.
Blocking (repo gate, not code): the fix: commit requires a Failure-Class declaration in the PR body. scripts/failure-class validate currently fails with missing_declaration, and failure-class-protocol is a blocking checks-manifest gate, so CI will fail once workflows run (none have run on this PR yet). Run scripts/pr-preflight --suggest and add the Failure-Class: line it proposes to the PR description.
Security: no new attack surface; error paths preserve the existing no-internal-IDs-in-responses behavior.
USER IMPACT: a declined card during a plan upgrade no longer drops a paid subscriber to the Free plan — they keep the plan they paid for and see a clear decline message.
Still wanted as a reviewer: (1) the Failure-Class declaration; (2) a maintainer call on error_if_incomplete vs the pending_if_incomplete the issue expected — both prevent the downgrade, but they differ in whether a retried charge can still complete the upgrade. Leaving the billing-semantics sign-off to a human maintainer; thanks for a well-scoped fix with a clear reproduction path.
by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.
|
@Git-on-my-level Thanks for the thorough review and validation! I've addressed both items:
Ready for maintainer sign-off and merge! |
Git-on-my-level
left a comment
There was a problem hiding this comment.
Since my last review: addressed (Failure-Class declaration — failure-class-protocol now passes on this head; the two static source-tripwire tests are now labeled as such in their docstrings) · still open (nothing from the prior review) · newly blocking (one repo-gate item below).
Verdict: the production fix remains correct and well-scoped; one blocking repo-gate item (not code) before merge.
Findings on this head:
- backend/routers/payment.py:789 —
payment_behavior='error_if_incomplete'on the cross-planSubscription.modifyis still the right call: Stripe rejects the update atomically when the prorated charge is declined, instead of landing the subscription inpast_due(which the projector demotes to Free). - backend/routers/payment.py:797-807 — the non-active status guard sits before any DB write, content unlock, or fair-use clear, so a degenerated status can no longer wipe the local subscription record.
- backend/routers/payment.py:887-901 —
CardErroris caught ahead of the broaderStripeErrorhandlers (correct ordering, sinceCardErrorsubclassesStripeError), and theInvalidRequestErrorbranch now also routes through_stripe_client_error_detail, keeping internal Stripe IDs in server logs and out of response bodies. - backend/tests/unit/test_payment_proration_decline_protection.py — good hermetic coverage: the three behavioral tests drive the real endpoint with mocked Stripe and assert
update_user_subscriptionis NOT called on card decline orpast_due; Backend unit suite is green on this head.
Blocking (repo gate, not code): the Hygiene and Formatting checks fail in PR Metadata Preflight on product-file-line-count-ratchet: backend/routers/payment.py grew from 1789 to 1812 lines vs main (#9838 freezes oversized product files). Please add this exact line to the PR body (replace reason with a short justification, e.g. atomic decline-guard for #23301):
Line-Count-Exception: backend/routers/payment.py | 1789 -> 1812 | reason
Splitting the file is not worth it for a 23-line fix; the exception line is the appropriate path here.
USER IMPACT: a declined card during a plan upgrade no longer drops a paid subscriber to the Free plan — they keep the plan they paid for and see a clear decline message.
Still wanted: (1) the Line-Count-Exception line in the PR body; (2) a maintainer call on error_if_incomplete vs the pending_if_incomplete #23301 expected — both prevent the downgrade, but they differ in whether a retried charge can still complete the upgrade.
AI-generated review on behalf of the Omi maintainers. Human maintainer sign-off needed for the billing-semantics choice above before merge.
by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.
Dismissed: the Failure-Class declaration blocking item from this review is resolved — failure-class-protocol now passes on head e1bad4a. A newer review covers the current, separate blocking item (product-file-line-count-ratchet).
|
@Git-on-my-level Thanks! Added the \Line-Count-Exception\ declaration to the PR description for the ratchet check: \Line-Count-Exception: backend/routers/payment.py | 1789 -> 1812 | atomic decline-guard for #23301\ All gates are clear and ready for merge. |
|
Hey @ghazy2001 👋 Thank you so much for taking the time to contribute to Omi! We truly appreciate you putting in the effort to submit this pull request. After careful review, we've decided not to merge this particular PR. Please don't take this personally — we genuinely try to merge as many contributions as possible, but sometimes we have to make tough calls based on:
Before your next PR, please skim:
If this was declined for direction or taste, maintainers should cite an invariant ID or open a proposed one — ask if that citation is missing. Your contribution is still valuable to us, and we'd love to see you contribute again in the future! If you'd like feedback on how to improve this PR or want to discuss alternative approaches, please don't hesitate to reach out. Thank you for being part of the Omi community! |
Fixes #23301
Summary
When an active subscriber attempts an in-app cross-plan upgrade (e.g. from Plus Annual to Unlimited) and the card payment for the prorated invoice is declined (e.g. insufficient funds), Stripe previously modified the subscription items without
payment_behavior='error_if_incomplete'. This caused Stripe to put the subscription intopast_duestatus. Because Omi's subscription projector treatspast_duestatus as Basic/Free, the subscriber's paid account was immediately wiped and downgraded in the database, causing the app UI ("Plan and Usage") to suddenly display "Free Plan" and locking previously-unlocked memories and conversations despite having paid for the existing plan.Fix
payment_behavior='error_if_incomplete': During cross-plan modifications, Stripe now rolls back the subscription update if the prorated charge cannot be settled immediately. The subscription remains unchanged on the user's active paid plan.updated_sub.statusis in('active', 'trialing'). If not, raiseHTTPException(400)to preserve the active database record.stripe.error.CardErrorandstripe.error.StripeErrorexplicitly, using_stripe_client_error_detailto provide clear, actionable feedback to the user while keeping internal Stripe object IDs private.Visual and UI Behavior
Upgrade payment failed: <reason>. Your current plan remains active., and the user's paid plan remains displayed and active on the Plan and Usage screen with all features intact.Verification
backend/tests/unit/test_payment_proration_decline_protection.py:payment_behavior='error_if_incomplete'is declared and passed tostripe.Subscription.modify.stripe.error.CardErrorpreserves database subscription without downgrade.pytest backend/tests/unit/test_payment_proration_decline_protection.py backend/tests/unit/test_payment_stripe_error_sanitization.py backend/tests/unit/test_payment_cancel_subscription_http_exception.py(13/13 green).Failure-Class: none
Line-Count-Exception: backend/routers/payment.py | 1789 -> 1812 | atomic decline-guard for #23301