feat(paykit): add explicit subscription cancel API#185
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
📝 WalkthroughWalkthroughThis PR adds a subscription cancellation feature to PayKit. It introduces a ChangesSubscription Cancellation Feature
Sequence DiagramsequenceDiagram
participant Client
participant CancelAPI as cancelSubscription<br/>(API)
participant CancelService as cancelPlanSubscription<br/>(Service)
participant Database
participant Provider as Payment Provider
Client->>CancelAPI: POST /cancel-subscription<br/>(planId, customerId)
CancelAPI->>CancelService: cancelPlanSubscription(ctx, {customerId, planId})
CancelService->>CancelService: resolveRequestedPlan(planId)
CancelService->>Database: Load active subscription<br/>for plan group
alt Active subscription exists & is paid
CancelService->>Provider: Call provider cancel<br/>(subscriptionId, currentPeriodEndAt)
Provider-->>CancelService: Subscription data
CancelService->>Database: transaction start
CancelService->>Database: Clear scheduled<br/>subscriptions
CancelService->>Database: Insert scheduled free<br/>plan subscription
CancelService->>Database: Update active subscription<br/>to mark canceled
CancelService->>Database: Update schedule<br/>to free product
CancelService->>Database: transaction commit
else No active subscription<br/>or already free
CancelService->>CancelService: Short-circuit to<br/>no-payment result
end
CancelService-->>CancelAPI: SubscribeResult<br/>{paymentUrl: null, requiredAction: null}
CancelAPI-->>Client: {paymentUrl: null, requiredAction: null}
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/paykit/src/subscription/__tests__/subscription.service.test.ts (1)
67-225: ⚡ Quick winAdd tests for the documented cancellation edge paths.
This suite currently validates only the happy path. Please add explicit cases for: (1) no active subscription in group →
SUBSCRIPTION_NOT_FOUND, and (2) alreadycancelAtPeriodEnd→ no-op behavior, so the PR’s contract is locked by tests.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/paykit/src/subscription/__tests__/subscription.service.test.ts` around lines 67 - 225, Tests only cover the happy path for cancelPlanSubscription; add two new test cases: one where no active subscription exists in the group (simulate database select/transaction returning null/empty and assert the function throws or returns the SUBSCRIPTION_NOT_FOUND error constant), and one where the found subscription already has cancelAtPeriodEnd=true (mock transaction select to return a subscription with cancelAtPeriodEnd: true and assert cancelPlanSubscription performs no provider cancel/update/inserts and returns a no-op result). Use the same helpers/mocks as the existing test (createSelectChain, tx transaction mock, provider.cancelSubscription spy, and insert/update spies) and reference cancelPlanSubscription, SUBSCRIPTION_NOT_FOUND, and the provider.cancelSubscription mock to locate where to add these tests.packages/paykit/src/subscription/subscription.service.ts (1)
232-249: ⚡ Quick winReuse this helper from the webhook fallback path too.
resolveDefaultFreePlanInGroup()now handles the "plan removed fromplanMap" case by falling back to stored features, butensureScheduledDefaultPlan()still exits early when that normalized plan lookup misses. That leaves explicit cancellation and webhook reconciliation with different default-plan scheduling behavior for the same group. Consider routingensureScheduledDefaultPlan()through this helper so the fallback rules stay aligned.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/paykit/src/subscription/subscription.service.ts` around lines 232 - 249, ensureScheduledDefaultPlan() currently bails when the normalized plan lookup misses, causing inconsistent fallback behavior versus the webhook path; change ensureScheduledDefaultPlan to call resolveDefaultFreePlanInGroup(ctx, group) and use its returned internalId and planFeatures (or null) instead of directly reading ctx.products.planMap so that the stored-plan fallback logic in resolveDefaultFreePlanInGroup is reused and both code paths share the same default-plan resolution rules.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/paykit/src/subscription/__tests__/subscription.service.test.ts`:
- Around line 67-225: Tests only cover the happy path for
cancelPlanSubscription; add two new test cases: one where no active subscription
exists in the group (simulate database select/transaction returning null/empty
and assert the function throws or returns the SUBSCRIPTION_NOT_FOUND error
constant), and one where the found subscription already has
cancelAtPeriodEnd=true (mock transaction select to return a subscription with
cancelAtPeriodEnd: true and assert cancelPlanSubscription performs no provider
cancel/update/inserts and returns a no-op result). Use the same helpers/mocks as
the existing test (createSelectChain, tx transaction mock,
provider.cancelSubscription spy, and insert/update spies) and reference
cancelPlanSubscription, SUBSCRIPTION_NOT_FOUND, and the
provider.cancelSubscription mock to locate where to add these tests.
In `@packages/paykit/src/subscription/subscription.service.ts`:
- Around line 232-249: ensureScheduledDefaultPlan() currently bails when the
normalized plan lookup misses, causing inconsistent fallback behavior versus the
webhook path; change ensureScheduledDefaultPlan to call
resolveDefaultFreePlanInGroup(ctx, group) and use its returned internalId and
planFeatures (or null) instead of directly reading ctx.products.planMap so that
the stored-plan fallback logic in resolveDefaultFreePlanInGroup is reused and
both code paths share the same default-plan resolution rules.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 0724eff4-54c2-4d5a-bdb3-64a0251eef0a
📒 Files selected for processing (6)
packages/paykit/src/api/methods.tspackages/paykit/src/subscription/__tests__/subscription.service.test.tspackages/paykit/src/subscription/subscription.api.tspackages/paykit/src/subscription/subscription.service.tspackages/paykit/src/subscription/subscription.types.tspackages/paykit/src/types/instance.ts
Summary
This adds an explicit
cancelSubscription()API for the current customer instead of forcing cancellation flows to happen indirectly throughsubscribe().The new method cancels the active paid subscription for the selected plan group, updates provider-backed billing state, and schedules the default free plan when that group has one.
Closes #63
How it works
cancelSubscription({ planId })cancelSubscription()hookExample
Client-side:
Behavior notes
SUBSCRIPTION_NOT_FOUNDBreaking change
No required breaking change.
This is an additive API. Existing subscription flows continue to work as before.
Tests
node ./node_modules/vitest/vitest.mjs run --config vitest.unit.config.ts packages/paykit/src/subscription/__tests__/subscription.service.test.tsbun run typecheck --filter=paykitjsSummary by CodeRabbit
New Features
Tests