{"record":{"id":"c3ff1b3ea320bae2","repo":"toeverything/AFFiNE","slug":"cant-update-onetime-payment-subscription","errorCode":"cant_update_onetime_payment_subscription","errorMessage":"Onetime payment subscription cannot be canceled.","messagePattern":"Onetime payment subscription cannot be canceled\\.","errorType":"exception","errorClass":"CantUpdateOnetimePaymentSubscription","httpStatus":403,"severity":"error","filePath":"packages/backend/server/src/plugins/payment/service.ts","lineNumber":177,"sourceCode":"    identity: z.infer<typeof SubscriptionIdentity>,\n    idempotencyKey?: string\n  ): Promise<Subscription> {\n    this.assertSubscriptionIdentity(identity);\n\n    const manager = this.select(identity.plan);\n    const subscription = await manager.getActiveSubscription(identity);\n\n    if (!subscription) {\n      throw new SubscriptionNotExists({ plan: identity.plan });\n    }\n\n    // IAP read-only: RevenueCat-managed subscriptions cannot be modified on web\n    if (subscription.provider === 'revenuecat') {\n      throw new ManagedByAppStoreOrPlay();\n    }\n\n    if (!subscription.stripeSubscriptionId) {\n      throw new CantUpdateOnetimePaymentSubscription(\n        'Onetime payment subscription cannot be canceled.'\n      );\n    }\n\n    if (subscription.canceledAt) {\n      throw new SubscriptionHasBeenCanceled();\n    }\n\n    // update the subscription in db optimistically\n    const newSubscription = manager.cancelSubscription(subscription);\n\n    // should release the schedule first\n    if (subscription.stripeScheduleId) {\n      const manager = await this.scheduleManager.fromSchedule(\n        subscription.stripeScheduleId\n      );\n      await manager.cancel(idempotencyKey);\n    } else {","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/payment/service.ts#L159-L195","documentation":"Thrown by cancelSubscription when the active subscription has no stripeSubscriptionId. Such rows are onetime/lifetime purchases (e.g. Lifetime recurring or onetime variant), which have no recurring Stripe subscription to cancel, so cancellation is meaningless and rejected.","triggerScenarios":"Calling cancelSubscription on a Lifetime Pro purchase or any onetime-variant checkout result; refund flows mistakenly routed through cancel.","commonSituations":"Pricing page offers both monthly and lifetime; UI reuses the subscription settings component and shows Cancel for lifetime rows; test data created from onetime payments.","solutions":["Hide the Cancel action when the subscription has no stripeSubscriptionId (or recurring is 'lifetime' / variant onetime).","For refunds on onetime purchases, use the refund/invoice path, not cancelSubscription.","Copy guidance to users: lifetime access does not renew, so there is nothing to cancel."],"exampleFix":"// before\n{sub.status === 'active' && <CancelButton onClick={() => cancel(identity)} />}\n\n// after\n{sub.status === 'active' && sub.stripeSubscriptionId && (\n  <CancelButton onClick={() => cancel(identity)} />\n)}","handlingStrategy":"validation","validationCode":"const cancellable = Boolean(subscription.stripeSubscriptionId);\nif (!cancellable) throw new Error('Onetime/lifetime purchases cannot be canceled');\nawait svc.cancelSubscription(identity, idempotencyKey);","typeGuard":"function isRecurringSub(s: { stripeSubscriptionId: string | null }): s is { stripeSubscriptionId: string } {\n  return typeof s.stripeSubscriptionId === 'string' && s.stripeSubscriptionId.length > 0;\n}","tryCatchPattern":"catch (e) { if (gqlErrorCode(e) === 'cant_update_onetime_payment_subscription') { hideCancelAction(); showOnetimeNotice(); return; } throw e; }","preventionTips":["Hide cancel/resume for lifetime and onetime-variant purchases.","Route refund requests for onetime payments through the refund flow.","Include recurring vs lifetime in the subscription UI model so the branch is explicit."],"tags":["payment","onetime-payment","lifetime","cancel","subscription"],"backgroundTag":"onetime-payment-unsupported-operation","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}