{"record":{"id":"d0c55c34c358ae60","repo":"toeverything/AFFiNE","slug":"subscription-expired","errorCode":"subscription_expired","errorMessage":"Your subscription has expired.","messagePattern":"Your subscription has expired\\.","errorType":"exception","errorClass":"SubscriptionExpired","httpStatus":403,"severity":"error","filePath":"packages/backend/server/src/plugins/payment/service.ts","lineNumber":238,"sourceCode":"    }\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.canceledAt) {\n      throw new SubscriptionHasNotBeenCanceled();\n    }\n\n    if (!subscription.stripeSubscriptionId || !subscription.end) {\n      throw new CantUpdateOnetimePaymentSubscription(\n        'Onetime payment subscription cannot be resumed.'\n      );\n    }\n\n    if (subscription.end < new Date()) {\n      throw new SubscriptionExpired();\n    }\n\n    // update the subscription in db optimistically\n    const newSubscription = await manager.resumeSubscription(subscription);\n\n    if (subscription.stripeScheduleId) {\n      const manager = await this.scheduleManager.fromSchedule(\n        subscription.stripeScheduleId\n      );\n      await manager.resume(idempotencyKey);\n    } else {\n      await this.stripe.subscriptions.update(\n        subscription.stripeSubscriptionId,\n        { cancel_at_period_end: false },\n        { idempotencyKey }\n      );\n    }\n","sourceCodeStart":220,"sourceCodeEnd":256,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/payment/service.ts#L220-L256","documentation":"Thrown by resumeSubscription when subscription.end is in the past. A canceled subscription can only be resumed before its scheduled end date; once end has passed, the subscription is terminated and the user must start a new checkout instead.","triggerScenarios":"User returns after the billing period ended and clicks Resume on a stale canceled row; clock/timezone bug in the client showing Resume past the deadline; delayed webhook left the row visible after expiration.","commonSituations":"Billing page left open across the period boundary; 'cancel at period end' with the user coming back weeks later; server clock skew in self-hosted deployments.","solutions":["Compare subscription.end with the current time client-side and swap Resume for a checkout/re-subscribe CTA once passed.","Hide expired canceled rows from the resume list; surface them as expired entitlements only.","On catching this code, route the user directly to the checkout flow for the same plan."],"exampleFix":"// before\nif (sub.canceledAt) await svc.resumeSubscription(identity);\n\n// after\nif (sub.canceledAt && sub.end && sub.end > new Date()) {\n  await svc.resumeSubscription(identity);\n} else {\n  router.push(`/checkout?plan=${identity.plan}`);\n}","handlingStrategy":"validation","validationCode":"const now = Date.now();\nconst withinWindow = subscription.end ? new Date(subscription.end).getTime() > now : false;\nif (!withinWindow) throw new Error('Resume window closed; start a new checkout');\nawait svc.resumeSubscription(identity, idempotencyKey);","typeGuard":"function isInResumeWindow(s: { end: Date | string | null }): boolean {\n  return s.end !== null && new Date(s.end).getTime() > Date.now();\n}","tryCatchPattern":"catch (e) { if (gqlErrorCode(e) === 'subscription_expired') { routeToCheckout(identity.plan); notify('Offer expired, please re-subscribe.'); return; } throw e; }","preventionTips":["Compare subscription.end with server time or a synced clock client-side before enabling Resume.","Hide expired canceled rows; show a re-subscribe CTA instead.","On catching subscription_expired, never retry resume; route to checkout."],"tags":["payment","subscription","resume","expiry","state-machine"],"backgroundTag":"expired-subscription","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"}