@deiondz/better-auth-razorpay 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,10 @@ A comprehensive subscription management plugin for Better Auth that integrates w
4
4
 
5
5
  > **📚 Always consult [better-auth.com/docs](https://better-auth.com/docs) for the latest Better Auth API and best practices.**
6
6
 
7
+ ## Credits
8
+
9
+ This plugin is inspired by and aligned with the design of the [better-auth-razorpay](https://github.com/iamjasonkendrick/better-auth-razorpay) community plugin. Credit and thanks go to the original author **[Jason Kendrick](https://github.com/iamjasonkendrick)** ([@iamjasonkendrick](https://github.com/iamjasonkendrick)) for the subscription flow, callback API, and feature set that this implementation follows.
10
+
7
11
  ## Table of Contents
8
12
 
9
13
  - [Overview](#overview)
@@ -12,24 +16,26 @@ A comprehensive subscription management plugin for Better Auth that integrates w
12
16
  - [Database Setup](#database-setup)
13
17
  - [API Endpoints](#api-endpoints)
14
18
  - [Client Usage](#client-usage)
19
+ - [TanStack Query Hooks](#tanstack-query-hooks)
15
20
  - [Webhook Setup](#webhook-setup)
16
21
  - [TypeScript Types](#typescript-types)
17
22
  - [Error Handling](#error-handling)
18
23
  - [Usage Examples](#usage-examples)
19
24
  - [Best Practices](#best-practices)
20
25
  - [Troubleshooting](#troubleshooting)
26
+ - [Credits](#credits)
21
27
 
22
28
  ## Overview
23
29
 
24
- The Razorpay plugin provides a complete subscription management solution with the following features:
30
+ The Razorpay plugin provides a subscription management solution aligned with the [community plugin design](https://github.com/iamjasonkendrick/better-auth-razorpay):
25
31
 
26
- - ✅ **Subscription Management**: Create, pause, resume, and cancel subscriptions
27
- - ✅ **Payment Verification**: Secure payment signature verification
28
- - ✅ **Webhook Handling**: Automatic processing of Razorpay webhook events
29
- - ✅ **Plan Management**: Retrieve and manage subscription plans
30
- - ✅ **Type Safety**: Full TypeScript support with comprehensive type definitions
31
- - ✅ **Error Handling**: Robust error handling with detailed error codes
32
- - ✅ **Security**: Production-safe error messages and signature verification
32
+ - ✅ **Subscription flow**: Create-or-update (returns checkout URL), cancel (at period end or immediately), restore, list active/trialing subscriptions
33
+ - ✅ **Named plans**: Plans with `name`, `monthlyPlanId`, optional `annualPlanId`, `limits`, and `freeTrial`
34
+ - ✅ **Customer on sign-up**: Optional Razorpay customer creation when a user signs up, with `onCustomerCreate` and `getCustomerCreateParams`
35
+ - ✅ **Webhook handling**: Subscription events (activated, cancelled, expired, etc.) with optional `onSubscriptionActivated`, `onSubscriptionCancel`, `onSubscriptionUpdate`, and global `onEvent`
36
+ - ✅ **Authorization**: `authorizeReference` for list/create actions; `requireEmailVerification` for subscriptions
37
+ - ✅ **Type safety**: Full TypeScript support with `SubscriptionRecord`, `RazorpayPlan`, and plugin options
38
+ - ✅ **TanStack Query**: Works with TanStack Query; use our optional [pre-built hooks](#tanstack-query-hooks) or build your own hooks around `authClient.api` (GET/POST to the Razorpay endpoints).
33
39
 
34
40
  ## Installation
35
41
 
@@ -59,35 +65,45 @@ The package includes `razorpay` and `zod` as dependencies.
59
65
 
60
66
  ```typescript
61
67
  // src/lib/auth.ts (or your auth configuration file)
68
+ import Razorpay from 'razorpay'
62
69
  import { betterAuth } from 'better-auth'
63
70
  import { razorpayPlugin } from 'better-auth-razorpay'
64
71
 
72
+ const razorpayClient = new Razorpay({
73
+ key_id: process.env.RAZORPAY_KEY_ID!,
74
+ key_secret: process.env.RAZORPAY_KEY_SECRET!,
75
+ })
76
+
65
77
  export const auth = betterAuth({
66
78
  // ... your Better Auth configuration
67
79
  database: mongodbAdapter(await connect()), // or your adapter
68
80
  secret: process.env.BETTER_AUTH_SECRET,
69
- baseURL: process.env.BETTER_AUTH_URL, // Optional if env var is set
81
+ baseURL: process.env.BETTER_AUTH_URL,
70
82
 
71
83
  plugins: [
72
84
  razorpayPlugin({
73
- keyId: process.env.RAZORPAY_KEY_ID!,
74
- keySecret: process.env.RAZORPAY_KEY_SECRET!,
75
- webhookSecret: process.env.RAZORPAY_WEBHOOK_SECRET,
76
- plans: [
77
- 'plan_1234567890', // Basic Plan
78
- 'plan_0987654321', // Premium Plan
79
- ],
85
+ razorpayClient,
86
+ razorpayWebhookSecret: process.env.RAZORPAY_WEBHOOK_SECRET,
87
+ createCustomerOnSignUp: true, // optional
88
+ subscription: {
89
+ enabled: true,
90
+ plans: [
91
+ {
92
+ name: 'Starter',
93
+ monthlyPlanId: 'plan_xxxxxxxxxxxx',
94
+ annualPlanId: 'plan_yyyyyyyyyyyy', // optional
95
+ limits: { features: 5 },
96
+ freeTrial: { days: 7 }, // optional
97
+ },
98
+ ],
99
+ onSubscriptionActivated: async ({ subscription, plan }) => {
100
+ console.log(`Subscription ${subscription.id} activated for plan ${plan.name}`)
101
+ },
102
+ },
80
103
  onWebhookEvent: async (payload, context) => {
81
- // Optional: Custom webhook event handling
82
104
  const { event, subscription, payment } = payload
83
- const { userId, user } = context
84
-
85
- // Send emails, update external systems, analytics, etc.
86
- if (event === 'subscription.charged') {
87
- await sendPaymentConfirmationEmail(user.email, {
88
- amount: payment?.amount,
89
- subscriptionId: subscription.id,
90
- })
105
+ if (event === 'subscription.charged' && payment) {
106
+ // Send confirmation email, etc.
91
107
  }
92
108
  },
93
109
  }),
@@ -145,21 +161,93 @@ npx @better-auth/cli@latest generate
145
161
 
146
162
  ```typescript
147
163
  interface RazorpayPluginOptions {
148
- keyId: string // Required: Razorpay Key ID
149
- keySecret: string // Required: Razorpay Key Secret
150
- webhookSecret?: string // Optional: Webhook secret for signature verification
151
- plans: string[] // Required: Array of plan IDs from Razorpay dashboard
152
- onWebhookEvent?: OnWebhookEventCallback // Optional: Custom webhook callback
164
+ razorpayClient: Razorpay // Required: Initialized Razorpay instance (key_id, key_secret)
165
+ razorpayWebhookSecret?: string // Optional: Webhook secret for signature verification
166
+ createCustomerOnSignUp?: boolean // Optional: Create Razorpay customer on user sign-up (default: false)
167
+ onCustomerCreate?: (args) => Promise<void>
168
+ getCustomerCreateParams?: (args) => Promise<{ params?: Record<string, unknown> }>
169
+ subscription?: SubscriptionOptions // Optional: { enabled, plans, callbacks, authorizeReference, ... }
170
+ onEvent?: (event) => Promise<void>
171
+ onWebhookEvent?: (payload, context) => Promise<void> // Optional: Custom webhook callback
153
172
  }
154
173
  ```
155
174
 
175
+ ### Callback functions
176
+
177
+ The plugin supports the same callback hooks as the [community plugin](https://github.com/iamjasonkendrick/better-auth-razorpay). You can use them for emails, analytics, external systems, or custom logic.
178
+
179
+ | Callback | When it runs |
180
+ |----------|----------------|
181
+ | **`onCustomerCreate`** | After a Razorpay customer is created (when `createCustomerOnSignUp` is true and the user signs up). Receives `{ user, razorpayCustomer }`. |
182
+ | **`getCustomerCreateParams`** | Before creating a Razorpay customer on sign-up. Return `{ params }` (e.g. `notes`) to add custom data to the customer. |
183
+ | **`getSubscriptionCreateParams`** | Before creating a Razorpay subscription (create-or-update). Return `{ params }` (e.g. `notes`) to add custom data to the subscription. Receives `{ user, session, plan, subscription }`. |
184
+ | **`authorizeReference`** | Before create-or-update and before listing subscriptions for a `referenceId` other than the current user. Return `true` to allow. Receives `{ user, referenceId, action }`. |
185
+ | **`onSubscriptionCreated`** | After a new subscription is created (create-or-update). Receives `{ razorpaySubscription, subscription, plan }`. |
186
+ | **`onSubscriptionActivated`** | When the webhook receives `subscription.activated`. Receives `{ event, razorpaySubscription, subscription, plan }`. |
187
+ | **`onSubscriptionUpdate`** | When the webhook receives any other subscription event (e.g. `charged`, `paused`, `resumed`, `pending`, `halted`). Receives `{ event, subscription }`. |
188
+ | **`onSubscriptionCancel`** | When the webhook receives `subscription.cancelled` or `subscription.expired`. Receives `{ event, razorpaySubscription, subscription }`. |
189
+ | **`onEvent`** | After every processed webhook event. Receives the full event payload `{ event, ...payload }`. |
190
+ | **`onWebhookEvent`** | Legacy: after webhook processing, with payload and user context. Receives `(payload, context)` where `context` has `userId` and `user`. |
191
+ | **`freeTrial.onTrialStart`** | Optional, on a plan’s `freeTrial`. Call when you consider a subscription’s trial to have started (e.g. from your own logic or webhook handling). Receives `(subscription)`. |
192
+ | **`freeTrial.onTrialEnd`** | Optional, on a plan’s `freeTrial`. Call when you consider a subscription’s trial to have ended. Receives `{ subscription }`. |
193
+
194
+ Example: using callbacks in your config:
195
+
196
+ ```typescript
197
+ razorpayPlugin({
198
+ razorpayClient,
199
+ razorpayWebhookSecret: process.env.RAZORPAY_WEBHOOK_SECRET,
200
+ createCustomerOnSignUp: true,
201
+ onCustomerCreate: async ({ user, razorpayCustomer }) => {
202
+ console.log(`Razorpay customer created for user ${user.id}: ${razorpayCustomer.id}`)
203
+ },
204
+ getCustomerCreateParams: async ({ user, session }) => ({
205
+ params: { notes: { internalUserId: user.id } },
206
+ }),
207
+ subscription: {
208
+ enabled: true,
209
+ plans: [
210
+ {
211
+ name: 'Starter',
212
+ monthlyPlanId: 'plan_xxx',
213
+ freeTrial: {
214
+ days: 7,
215
+ onTrialStart: async (subscription) => console.log('Trial started', subscription.id),
216
+ onTrialEnd: async ({ subscription }) => console.log('Trial ended', subscription.id),
217
+ },
218
+ },
219
+ ],
220
+ getSubscriptionCreateParams: async ({ user, plan, subscription }) => ({
221
+ params: { notes: { planName: plan.name } },
222
+ }),
223
+ onSubscriptionCreated: async ({ razorpaySubscription, subscription, plan }) => {
224
+ console.log(`Subscription ${subscription.id} created for plan ${plan.name}`)
225
+ },
226
+ onSubscriptionActivated: async ({ event, subscription, plan }) => {
227
+ console.log(`Subscription ${subscription.id} activated`)
228
+ },
229
+ onSubscriptionUpdate: async ({ event, subscription }) => {
230
+ console.log(`Subscription ${subscription.id} updated: ${event}`)
231
+ },
232
+ onSubscriptionCancel: async ({ event, subscription }) => {
233
+ console.log(`Subscription ${subscription.id} cancelled/expired: ${event}`)
234
+ },
235
+ authorizeReference: async ({ user, referenceId, action }) => user.id === referenceId,
236
+ },
237
+ onEvent: async (event) => console.log('Razorpay event:', event.event),
238
+ onWebhookEvent: async (payload, context) => {
239
+ // Custom logic: emails, analytics, etc.
240
+ },
241
+ })
242
+ ```
243
+
156
244
  ### User Fields (Plug-and-Play)
157
245
 
158
- The plugin extends the Better Auth user schema with subscription-related fields. When you add the plugin and run `npx @better-auth/cli@latest migrate` (or `generate`), these columns are added to your user table automatically:
246
+ The plugin extends the Better Auth user schema with:
159
247
 
160
- - `subscriptionId`, `subscriptionPlanId`, `subscriptionStatus`, `subscriptionCurrentPeriodEnd`, `cancelAtPeriodEnd`, `lastPaymentDate`, `nextBillingDate`
248
+ - **user**: `razorpayCustomerId` (optional) set when `createCustomerOnSignUp` is true or when a customer is created for subscriptions.
161
249
 
162
- You do **not** need to add them manually to `user.additionalFields` unless you prefer to define them yourself.
250
+ You do **not** need to add it manually to `user.additionalFields` unless you prefer to define it yourself.
163
251
 
164
252
  ## Database Setup
165
253
 
@@ -167,15 +255,12 @@ You do **not** need to add them manually to `user.additionalFields` unless you p
167
255
 
168
256
  The plugin automatically creates the following database models via Better Auth's schema system:
169
257
 
170
- **`razorpaySubscription`**
171
- - `userId` (string) - User ID
172
- - `subscriptionId` (string) - Razorpay subscription ID
173
- - `planId` (string) - Plan ID
174
- - `status` (string) - Subscription status
258
+ **`user`** (extended)
259
+ - `razorpayCustomerId` (string, optional) Razorpay customer ID when customer creation is enabled.
175
260
 
176
- **`razorpayCustomer`** (for future use)
177
- - `userId` (string, unique) - User ID
178
- - `razorpayCustomerId` (string, unique) - Razorpay customer ID
261
+ **`subscription`**
262
+ - `id`, `plan`, `referenceId`, `razorpayCustomerId`, `razorpaySubscriptionId`, `status`, `trialStart`, `trialEnd`, `periodStart`, `periodEnd`, `cancelAtPeriodEnd`, `seats`, `groupId`, `createdAt`, `updatedAt`
263
+ - `status` values: `created`, `active`, `pending`, `halted`, `cancelled`, `completed`, `expired`, `trialing`
179
264
 
180
265
  ### Database Adapters
181
266
 
@@ -190,11 +275,22 @@ The plugin works with all Better Auth database adapters:
190
275
 
191
276
  ## API Endpoints
192
277
 
193
- All endpoints are prefixed with `/api/auth/razorpay/` (or your configured `basePath`). Endpoints use Better Auth's `createAuthEndpoint` and automatically handle authentication via `sessionMiddleware` where required.
278
+ All endpoints are prefixed with `/api/auth/razorpay/` (or your configured `basePath`).
279
+
280
+ ### Subscription flow
281
+
282
+ | Action | Method | Endpoint | Description |
283
+ |--------|--------|----------|-------------|
284
+ | Create or update | `POST` | `subscription/create-or-update` | Start a subscription or update; returns `checkoutUrl` for Razorpay payment page. Body: `plan`, `annual?`, `seats?`, `subscriptionId?`, `successUrl?`, `disableRedirect?`. |
285
+ | Cancel | `POST` | `subscription/cancel` | Cancel by local subscription ID. Body: `subscriptionId`, `immediately?`. |
286
+ | Restore | `POST` | `subscription/restore` | Restore a subscription scheduled to cancel. Body: `subscriptionId`. |
287
+ | List | `GET` | `subscription/list` | List active/trialing subscriptions. Query: `referenceId?` (default: current user). |
288
+ | Get plans | `GET` | `get-plans` | Return configured plans (name, monthlyPlanId, annualPlanId, limits, freeTrial). |
289
+ | Webhook | `POST` | `webhook` | Razorpay webhook URL; configure in Razorpay Dashboard. |
194
290
 
195
291
  ### 1. Get Plans
196
292
 
197
- Retrieve all configured subscription plans.
293
+ Retrieve all configured subscription plans (from plugin config; no Razorpay API call).
198
294
 
199
295
  **Endpoint:** `GET /api/auth/razorpay/get-plans`
200
296
 
@@ -205,7 +301,7 @@ Retrieve all configured subscription plans.
205
301
  ```typescript
206
302
  {
207
303
  success: true,
208
- data: RazorpayPlan[]
304
+ data: Array<{ name: string; monthlyPlanId: string; annualPlanId?: string; limits?: Record<string, number>; freeTrial?: { days: number } }>
209
305
  }
210
306
  ```
211
307
 
@@ -577,12 +673,11 @@ import { authClient } from '@/lib/auth-client'
577
673
  // GET request
578
674
  const plans = await authClient.api.get('/razorpay/get-plans')
579
675
 
580
- // POST request
581
- const subscription = await authClient.api.post('/razorpay/subscribe', {
582
- body: {
583
- plan_id: 'plan_1234567890',
584
- },
676
+ // POST request (create or update subscription)
677
+ const result = await authClient.api.post('/razorpay/subscription/create-or-update', {
678
+ body: { plan: 'Starter', annual: false, seats: 1 },
585
679
  })
680
+ // result.data.checkoutUrl -> redirect user to Razorpay payment page
586
681
  ```
587
682
 
588
683
  ### Type Safety
@@ -602,6 +697,120 @@ type Session = typeof authClient.$Infer.Session
602
697
  type User = typeof authClient.$Infer.Session.user
603
698
  ```
604
699
 
700
+ ## TanStack Query Hooks
701
+
702
+ The plugin works with **TanStack Query**: you can use it with `useQuery`/`useMutation` and `authClient.api` in any way you like. We provide optional pre-built hooks below; if you prefer a different setup, build your own hooks around the same endpoints (e.g. `authClient.api.get('/razorpay/get-plans')`, `authClient.api.post('/razorpay/subscription/create-or-update', { body })`, etc.).
703
+
704
+ To use our pre-built hooks, install peer dependencies:
705
+
706
+ ```bash
707
+ npm install @tanstack/react-query react
708
+ # or yarn / pnpm / bun
709
+ ```
710
+
711
+ Import from `better-auth-razorpay/hooks` and pass your Better Auth client as the first argument:
712
+
713
+ ```tsx
714
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
715
+ import { createAuthClient } from 'better-auth/react'
716
+ import { razorpayClientPlugin } from 'better-auth-razorpay/client'
717
+ import {
718
+ usePlans,
719
+ useSubscriptions,
720
+ useCreateOrUpdateSubscription,
721
+ useCancelSubscription,
722
+ useRestoreSubscription,
723
+ razorpayQueryKeys,
724
+ } from 'better-auth-razorpay/hooks'
725
+ import type { CreateOrUpdateSubscriptionInput } from 'better-auth-razorpay/hooks'
726
+
727
+ const queryClient = new QueryClient()
728
+ // auth from your server config (e.g. import type { auth } from './auth')
729
+ const authClient = createAuthClient<typeof auth>({
730
+ plugins: [razorpayClientPlugin()],
731
+ })
732
+
733
+ function App() {
734
+ return (
735
+ <QueryClientProvider client={queryClient}>
736
+ <SubscriptionUI />
737
+ </QueryClientProvider>
738
+ )
739
+ }
740
+
741
+ function SubscriptionUI() {
742
+ // Plans (no auth required)
743
+ const { data: plans, isLoading: plansLoading } = usePlans(authClient)
744
+
745
+ // Current user's subscriptions (requires session)
746
+ const { data: subscriptions, isLoading: subsLoading } = useSubscriptions(authClient)
747
+
748
+ const createOrUpdate = useCreateOrUpdateSubscription(authClient)
749
+ const cancel = useCancelSubscription(authClient)
750
+ const restore = useRestoreSubscription(authClient)
751
+
752
+ const handleSubscribe = () => {
753
+ createOrUpdate.mutate(
754
+ { plan: 'Starter', annual: false },
755
+ {
756
+ onSuccess: (data) => {
757
+ window.location.href = data.checkoutUrl // Redirect to Razorpay
758
+ },
759
+ }
760
+ )
761
+ }
762
+
763
+ const handleCancel = (subscriptionId: string) => {
764
+ cancel.mutate({ subscriptionId, immediately: false })
765
+ }
766
+
767
+ const handleRestore = (subscriptionId: string) => {
768
+ restore.mutate({ subscriptionId })
769
+ }
770
+
771
+ if (plansLoading) return <div>Loading plans...</div>
772
+ return (
773
+ <div>
774
+ {plans?.map((p) => (
775
+ <button key={p.name} onClick={handleSubscribe} disabled={createOrUpdate.isPending}>
776
+ Subscribe to {p.name}
777
+ </button>
778
+ ))}
779
+ {subscriptions?.map((s) => (
780
+ <div key={s.id}>
781
+ {s.plan} – {s.status}
782
+ {s.cancelAtPeriodEnd ? (
783
+ <button onClick={() => handleRestore(s.id)}>Restore</button>
784
+ ) : (
785
+ <button onClick={() => handleCancel(s.id)}>Cancel</button>
786
+ )}
787
+ </div>
788
+ ))}
789
+ </div>
790
+ )
791
+ }
792
+ ```
793
+
794
+ ### Hooks reference
795
+
796
+ | Hook | Type | Description |
797
+ |------|------|-------------|
798
+ | `usePlans(client, options?)` | `useQuery` | Fetches configured plans (GET `/razorpay/get-plans`). |
799
+ | `useSubscriptions(client, input?, options?)` | `useQuery` | Lists active/trialing subscriptions (GET `/razorpay/subscription/list`). Optional `referenceId` in input or options. |
800
+ | `useCreateOrUpdateSubscription(client, options?)` | `useMutation` | Creates or updates subscription; returns `checkoutUrl`. Invalidates subscriptions list on success. |
801
+ | `useCancelSubscription(client, options?)` | `useMutation` | Cancels by local subscription ID; optional `immediately`. Invalidates subscriptions list on success. |
802
+ | `useRestoreSubscription(client, options?)` | `useMutation` | Restores a subscription scheduled to cancel. Invalidates subscriptions list on success. |
803
+
804
+ **Query keys** (for manual invalidation or prefetching):
805
+
806
+ ```ts
807
+ import { razorpayQueryKeys } from 'better-auth-razorpay/hooks'
808
+
809
+ razorpayQueryKeys.plans() // ['razorpay', 'plans']
810
+ razorpayQueryKeys.subscriptions() // ['razorpay', 'subscriptions', 'me']
811
+ razorpayQueryKeys.subscriptions('user-id') // ['razorpay', 'subscriptions', 'user-id']
812
+ ```
813
+
605
814
  ## Webhook Setup
606
815
 
607
816
  ### 1. Configure Webhook in Razorpay Dashboard
@@ -4,97 +4,71 @@ import {
4
4
  cancelSubscriptionSchema,
5
5
  handleRazorpayError,
6
6
  type RazorpaySubscription,
7
- type RazorpaySubscriptionRecord,
7
+ type SubscriptionRecord,
8
8
  } from '../lib'
9
9
 
10
10
  /**
11
- * Cancels a subscription at the end of the current billing period.
12
- *
13
- * @param razorpay - The Razorpay instance initialized with API credentials
14
- * @returns A Better Auth endpoint handler
15
- *
16
- * @remarks
17
- * This endpoint:
18
- * - Requires user authentication via session
19
- * - Verifies subscription ownership before cancellation
20
- * - Cancels subscription at period end (not immediately)
21
- * - Updates user record with cancellation flag and period end date
22
- * - Subscription remains active until the current period ends
23
- *
24
- * @example
25
- * Request body:
26
- * ```json
27
- * {
28
- * "subscription_id": "sub_1234567890"
29
- * }
30
- * ```
11
+ * POST /api/auth/razorpay/subscription/cancel
12
+ * Cancels subscription by local subscription ID. Optionally cancel immediately.
31
13
  */
32
14
  export const cancelSubscription = (razorpay: Razorpay) =>
33
15
  createAuthEndpoint(
34
- '/razorpay/cancel-subscription',
16
+ '/razorpay/subscription/cancel',
35
17
  { method: 'POST', use: [sessionMiddleware] },
36
- async (_ctx) => {
18
+ async (ctx) => {
37
19
  try {
38
- // Validate input using Zod schema
39
- const validatedInput = cancelSubscriptionSchema.parse(_ctx.body)
40
-
41
- // Get user ID from session
42
- const userId = _ctx.context.session?.user?.id
43
-
20
+ const body = cancelSubscriptionSchema.parse(ctx.body)
21
+ const userId = ctx.context.session?.user?.id
44
22
  if (!userId) {
45
23
  return {
46
24
  success: false,
47
- error: {
48
- code: 'UNAUTHORIZED',
49
- description: 'User not authenticated',
50
- },
25
+ error: { code: 'UNAUTHORIZED', description: 'User not authenticated' },
51
26
  }
52
27
  }
53
28
 
54
- // Get subscription record to verify it belongs to the user
55
- const subscriptionRecord = (await _ctx.context.adapter.findOne({
56
- model: 'razorpaySubscription',
57
- where: [{ field: 'subscriptionId', value: validatedInput.subscription_id }],
58
- })) as RazorpaySubscriptionRecord | null
29
+ const record = (await ctx.context.adapter.findOne({
30
+ model: 'subscription',
31
+ where: [{ field: 'id', value: body.subscriptionId }],
32
+ })) as SubscriptionRecord | null
59
33
 
60
- if (!subscriptionRecord) {
34
+ if (!record) {
61
35
  return {
62
36
  success: false,
63
- error: {
64
- code: 'SUBSCRIPTION_NOT_FOUND',
65
- description: 'Subscription not found',
66
- },
37
+ error: { code: 'SUBSCRIPTION_NOT_FOUND', description: 'Subscription not found' },
38
+ }
39
+ }
40
+ if (record.referenceId !== userId) {
41
+ return {
42
+ success: false,
43
+ error: { code: 'FORBIDDEN', description: 'Subscription does not belong to you' },
67
44
  }
68
45
  }
69
46
 
70
- // Verify that the subscription belongs to the authenticated user
71
- const subscriptionUserId = subscriptionRecord.userId
72
- if (subscriptionUserId !== userId) {
47
+ const rpId = record.razorpaySubscriptionId
48
+ if (!rpId) {
73
49
  return {
74
50
  success: false,
75
- error: {
76
- code: 'UNAUTHORIZED',
77
- description: 'Subscription does not belong to authenticated user',
78
- },
51
+ error: { code: 'INVALID_STATE', description: 'No Razorpay subscription linked' },
79
52
  }
80
53
  }
81
54
 
82
- // Cancel subscription via Razorpay API (cancel at period end)
55
+ // cancel_at_cycle_end: true = cancel at period end, false = cancel immediately
83
56
  const subscription = (await razorpay.subscriptions.cancel(
84
- validatedInput.subscription_id,
85
- true
57
+ rpId,
58
+ !body.immediately
86
59
  )) as RazorpaySubscription
87
60
 
88
- // Update user table with cancellation info (keep active status)
89
- await _ctx.context.adapter.update({
90
- model: 'user',
91
- where: [{ field: 'id', value: userId }],
61
+ await ctx.context.adapter.update({
62
+ model: 'subscription',
63
+ where: [{ field: 'id', value: body.subscriptionId }],
92
64
  update: {
93
65
  data: {
94
- cancelAtPeriodEnd: true,
95
- subscriptionCurrentPeriodEnd: subscription.current_end
66
+ status: 'cancelled',
67
+ cancelAtPeriodEnd: !body.immediately,
68
+ periodEnd: subscription.current_end
96
69
  ? new Date(subscription.current_end * 1000)
97
- : null,
70
+ : record.periodEnd,
71
+ updatedAt: new Date(),
98
72
  },
99
73
  },
100
74
  })
@@ -103,29 +77,10 @@ export const cancelSubscription = (razorpay: Razorpay) =>
103
77
  success: true,
104
78
  data: {
105
79
  id: subscription.id,
106
- entity: subscription.entity,
107
- plan_id: subscription.plan_id,
108
80
  status: subscription.status,
109
- current_start: subscription.current_start,
81
+ plan_id: subscription.plan_id,
110
82
  current_end: subscription.current_end,
111
83
  ended_at: subscription.ended_at,
112
- quantity: subscription.quantity,
113
- notes: subscription.notes,
114
- charge_at: subscription.charge_at,
115
- start_at: subscription.start_at,
116
- end_at: subscription.end_at,
117
- auth_attempts: subscription.auth_attempts,
118
- total_count: subscription.total_count,
119
- paid_count: subscription.paid_count,
120
- customer_notify: subscription.customer_notify,
121
- created_at: subscription.created_at,
122
- expire_by: subscription.expire_by,
123
- short_url: subscription.short_url,
124
- has_scheduled_changes: subscription.has_scheduled_changes,
125
- change_scheduled_at: subscription.change_scheduled_at,
126
- source: subscription.source,
127
- offer_id: subscription.offer_id,
128
- remaining_count: subscription.remaining_count,
129
84
  },
130
85
  }
131
86
  } catch (error) {