Part of epic #100.
Functions to ship
Both functions branch on subscriptions.provider to dispatch to the right SDK.
1. cancel-subscription (~1.5h)
- Browser caller:
src/components/payment/SubscriptionManager/SubscriptionManager.tsx:87
- Request body:
{ subscription_id: string } (the internal subscriptions table row id, NOT the provider's id)
- Logic:
- CORS + auth
- Load
subscriptions row, verify owner
- Branch on
provider:
- Stripe:
stripe.subscriptions.cancel(provider_subscription_id) (immediate) or stripe.subscriptions.update(id, { cancel_at_period_end: true }) (graceful)
- PayPal: POST
/v1/billing/subscriptions/{provider_id}/cancel with { reason }
- Update local
subscriptions.status to cancelled or cancel-at-period-end
- Return updated row
2. resume-subscription (~1.5h)
- Browser caller:
src/components/payment/SubscriptionManager/SubscriptionManager.tsx:126
- Request body:
{ subscription_id: string }
- Logic:
- Same auth + load
- Branch on
provider:
- Stripe:
stripe.subscriptions.update(id, { cancel_at_period_end: false }) — only valid if status was cancel-at-period-end
- PayPal: POST
/v1/billing/subscriptions/{provider_id}/activate
- Update local
subscriptions.status to active
- Return updated row
Tests
- Deno unit tests mocking both SDKs
- Un-skip cancel/resume E2E tests under
tests/e2e/payment/
Acceptance
🤖 Created from audit on 2026-05-20
Part of epic #100.
Functions to ship
Both functions branch on
subscriptions.providerto dispatch to the right SDK.1.
cancel-subscription(~1.5h)src/components/payment/SubscriptionManager/SubscriptionManager.tsx:87{ subscription_id: string }(the internal subscriptions table row id, NOT the provider's id)subscriptionsrow, verify ownerprovider:stripe.subscriptions.cancel(provider_subscription_id)(immediate) orstripe.subscriptions.update(id, { cancel_at_period_end: true })(graceful)/v1/billing/subscriptions/{provider_id}/cancelwith{ reason }subscriptions.statustocancelledorcancel-at-period-end2.
resume-subscription(~1.5h)src/components/payment/SubscriptionManager/SubscriptionManager.tsx:126{ subscription_id: string }provider:stripe.subscriptions.update(id, { cancel_at_period_end: false })— only valid if status wascancel-at-period-end/v1/billing/subscriptions/{provider_id}/activatesubscriptions.statustoactiveTests
tests/e2e/payment/Acceptance
SubscriptionManagerCancel + Resume buttons work for both Stripe and PayPal sandbox subscriptions🤖 Created from audit on 2026-05-20