@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.
@@ -1,150 +0,0 @@
1
- import { createAuthEndpoint, sessionMiddleware } from 'better-auth/api'
2
- import type Razorpay from 'razorpay'
3
- import {
4
- handleRazorpayError,
5
- type RazorpaySubscription,
6
- resumeSubscriptionSchema,
7
- type RazorpaySubscriptionRecord,
8
- } from '../lib'
9
-
10
- /**
11
- * Resumes a paused subscription.
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 resuming
20
- * - Validates that subscription is in paused status
21
- * - Resumes the subscription via Razorpay API
22
- * - Updates subscription and user records with active status
23
- *
24
- * @example
25
- * Request body:
26
- * ```json
27
- * {
28
- * "subscription_id": "sub_1234567890"
29
- * }
30
- * ```
31
- */
32
- export const resumeSubscription = (razorpay: Razorpay) =>
33
- createAuthEndpoint(
34
- '/razorpay/resume-subscription',
35
- { method: 'POST', use: [sessionMiddleware] },
36
- async (_ctx) => {
37
- try {
38
- // Validate input using Zod schema
39
- const validatedInput = resumeSubscriptionSchema.parse(_ctx.body)
40
-
41
- // Get user ID from session
42
- const userId = _ctx.context.session?.user?.id
43
-
44
- if (!userId) {
45
- return {
46
- success: false,
47
- error: {
48
- code: 'UNAUTHORIZED',
49
- description: 'User not authenticated',
50
- },
51
- }
52
- }
53
-
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
59
-
60
- if (!subscriptionRecord) {
61
- return {
62
- success: false,
63
- error: {
64
- code: 'SUBSCRIPTION_NOT_FOUND',
65
- description: 'Subscription not found',
66
- },
67
- }
68
- }
69
-
70
- // Verify that the subscription belongs to the authenticated user
71
- const subscriptionUserId = subscriptionRecord.userId
72
- if (subscriptionUserId !== userId) {
73
- return {
74
- success: false,
75
- error: {
76
- code: 'UNAUTHORIZED',
77
- description: 'Subscription does not belong to authenticated user',
78
- },
79
- }
80
- }
81
-
82
- // Check if subscription is paused
83
- const subscriptionStatus = subscriptionRecord.status
84
- if (subscriptionStatus !== 'paused') {
85
- return {
86
- success: false,
87
- error: {
88
- code: 'INVALID_STATUS',
89
- description: 'Subscription is not paused. Only paused subscriptions can be resumed.',
90
- },
91
- }
92
- }
93
-
94
- // Resume subscription via Razorpay API
95
- const subscription = (await razorpay.subscriptions.resume(
96
- validatedInput.subscription_id
97
- )) as RazorpaySubscription
98
-
99
- // Update subscription status in database
100
- await _ctx.context.adapter.update({
101
- model: 'razorpaySubscription',
102
- where: [{ field: 'subscriptionId', value: validatedInput.subscription_id }],
103
- update: { status: subscription.status },
104
- })
105
-
106
- // Update user table with subscription status
107
- await _ctx.context.adapter.update({
108
- model: 'user',
109
- where: [{ field: 'id', value: userId }],
110
- update: {
111
- data: {
112
- subscriptionStatus: subscription.status,
113
- },
114
- },
115
- })
116
-
117
- return {
118
- success: true,
119
- data: {
120
- id: subscription.id,
121
- entity: subscription.entity,
122
- plan_id: subscription.plan_id,
123
- status: subscription.status,
124
- current_start: subscription.current_start,
125
- current_end: subscription.current_end,
126
- ended_at: subscription.ended_at,
127
- quantity: subscription.quantity,
128
- notes: subscription.notes,
129
- charge_at: subscription.charge_at,
130
- start_at: subscription.start_at,
131
- end_at: subscription.end_at,
132
- auth_attempts: subscription.auth_attempts,
133
- total_count: subscription.total_count,
134
- paid_count: subscription.paid_count,
135
- customer_notify: subscription.customer_notify,
136
- created_at: subscription.created_at,
137
- expire_by: subscription.expire_by,
138
- short_url: subscription.short_url,
139
- has_scheduled_changes: subscription.has_scheduled_changes,
140
- change_scheduled_at: subscription.change_scheduled_at,
141
- source: subscription.source,
142
- offer_id: subscription.offer_id,
143
- remaining_count: subscription.remaining_count,
144
- },
145
- }
146
- } catch (error) {
147
- return handleRazorpayError(error)
148
- }
149
- }
150
- )
package/api/subscribe.ts DELETED
@@ -1,188 +0,0 @@
1
- import { createAuthEndpoint, sessionMiddleware } from 'better-auth/api'
2
- import type Razorpay from 'razorpay'
3
- import {
4
- handleRazorpayError,
5
- subscribeSchema,
6
- type RazorpaySubscription,
7
- type RazorpayUserRecord,
8
- } from '../lib'
9
-
10
- /**
11
- * Creates a new subscription for the authenticated user.
12
- *
13
- * @param razorpay - The Razorpay instance initialized with API credentials
14
- * @param plans - Array of valid plan IDs from Razorpay dashboard configuration
15
- * @returns A Better Auth endpoint handler
16
- *
17
- * @remarks
18
- * This endpoint:
19
- * - Requires user authentication via session
20
- * - Validates the plan ID against configured plans
21
- * - Prevents duplicate active subscriptions
22
- * - Creates subscription via Razorpay API
23
- * - Stores subscription record in database
24
- * - Updates user record with subscription information
25
- *
26
- * @example
27
- * Request body:
28
- * ```json
29
- * {
30
- * "plan_id": "plan_1234567890",
31
- * "total_count": 12,
32
- * "quantity": 1,
33
- * "customer_notify": true
34
- * }
35
- * ```
36
- */
37
- export const subscribe = (razorpay: Razorpay, plans: string[]) =>
38
- createAuthEndpoint(
39
- '/razorpay/subscribe',
40
- { method: 'POST', use: [sessionMiddleware] },
41
- async (_ctx) => {
42
- try {
43
- // Validate input using Zod schema
44
- const validatedInput = subscribeSchema.parse(_ctx.body)
45
-
46
- // Check if plan ID exists in configured plans array
47
- if (!plans.includes(validatedInput.plan_id)) {
48
- return {
49
- success: false,
50
- error: {
51
- code: 'PLAN_NOT_FOUND',
52
- description: 'Plan not found in configured plans',
53
- },
54
- }
55
- }
56
-
57
- // Get user ID from session
58
- const userId = _ctx.context.session?.user?.id
59
-
60
- if (!userId) {
61
- return {
62
- success: false,
63
- error: {
64
- code: 'UNAUTHORIZED',
65
- description: 'User not authenticated',
66
- },
67
- }
68
- }
69
-
70
- // Check if user already has an active subscription
71
- const user = (await _ctx.context.adapter.findOne({
72
- model: 'user',
73
- where: [{ field: 'id', value: userId }],
74
- })) as RazorpayUserRecord | null
75
-
76
- if (!user) {
77
- return {
78
- success: false,
79
- error: {
80
- code: 'USER_NOT_FOUND',
81
- description: 'User not found',
82
- },
83
- }
84
- }
85
-
86
- const existingSubscriptionId = user.subscriptionId
87
- const existingSubscriptionStatus = user.subscriptionStatus
88
-
89
- // Prevent creating new subscription if user already has an active subscription
90
- if (existingSubscriptionId) {
91
- // Check if the existing subscription is still active (not cancelled)
92
- const activeStatuses = ['active', 'authenticated', 'paused', 'created']
93
- if (existingSubscriptionStatus && activeStatuses.includes(existingSubscriptionStatus)) {
94
- return {
95
- success: false,
96
- error: {
97
- code: 'SUBSCRIPTION_ALREADY_EXISTS',
98
- description:
99
- 'You already have an active subscription. Please cancel or pause your current subscription before creating a new one.',
100
- },
101
- }
102
- }
103
- }
104
-
105
- // Create subscription via Razorpay API
106
- const subscriptionData = {
107
- plan_id: validatedInput.plan_id,
108
- total_count: validatedInput.total_count,
109
- quantity: validatedInput.quantity,
110
- customer_notify: validatedInput.customer_notify,
111
- ...(validatedInput.start_at && { start_at: validatedInput.start_at }),
112
- ...(validatedInput.expire_by && { expire_by: validatedInput.expire_by }),
113
- ...(validatedInput.addons &&
114
- validatedInput.addons.length > 0 && { addons: validatedInput.addons }),
115
- ...(validatedInput.offer_id && { offer_id: validatedInput.offer_id }),
116
- ...(validatedInput.notes && { notes: validatedInput.notes }),
117
- }
118
-
119
- const subscription = (await razorpay.subscriptions.create(
120
- subscriptionData
121
- )) as RazorpaySubscription
122
-
123
- // Store subscription in database
124
- await _ctx.context.adapter.create({
125
- model: 'razorpaySubscription',
126
- data: {
127
- userId,
128
- subscriptionId: subscription.id,
129
- planId: validatedInput.plan_id,
130
- status: subscription.status,
131
- },
132
- })
133
-
134
- // Update user table with subscription info
135
- await _ctx.context.adapter.update({
136
- model: 'user',
137
- where: [{ field: 'id', value: userId }],
138
- update: {
139
- data: {
140
- subscriptionStatus: subscription.status,
141
- subscriptionId: subscription.id,
142
- subscriptionPlanId: validatedInput.plan_id,
143
- subscriptionCurrentPeriodEnd: subscription.current_end
144
- ? new Date(subscription.current_end * 1000)
145
- : null,
146
- cancelAtPeriodEnd: false, // Initialize as not cancelling
147
- lastPaymentDate: new Date(), // Set initial payment date
148
- nextBillingDate: subscription.current_end
149
- ? new Date(subscription.current_end * 1000)
150
- : null,
151
- },
152
- },
153
- })
154
-
155
- return {
156
- success: true,
157
- data: {
158
- id: subscription.id,
159
- entity: subscription.entity,
160
- plan_id: subscription.plan_id,
161
- status: subscription.status,
162
- current_start: subscription.current_start,
163
- current_end: subscription.current_end,
164
- ended_at: subscription.ended_at,
165
- quantity: subscription.quantity,
166
- notes: subscription.notes,
167
- charge_at: subscription.charge_at,
168
- start_at: subscription.start_at,
169
- end_at: subscription.end_at,
170
- auth_attempts: subscription.auth_attempts,
171
- total_count: subscription.total_count,
172
- paid_count: subscription.paid_count,
173
- customer_notify: subscription.customer_notify,
174
- created_at: subscription.created_at,
175
- expire_by: subscription.expire_by,
176
- short_url: subscription.short_url,
177
- has_scheduled_changes: subscription.has_scheduled_changes,
178
- change_scheduled_at: subscription.change_scheduled_at,
179
- source: subscription.source,
180
- offer_id: subscription.offer_id,
181
- remaining_count: subscription.remaining_count,
182
- },
183
- }
184
- } catch (error) {
185
- return handleRazorpayError(error)
186
- }
187
- }
188
- )
@@ -1,129 +0,0 @@
1
- import { createHmac } from 'node:crypto'
2
- import { createAuthEndpoint, sessionMiddleware } from 'better-auth/api'
3
- import { handleRazorpayError, type RazorpaySubscriptionRecord, verifyPaymentSchema } from '../lib'
4
-
5
- /**
6
- * Verifies a payment signature after Razorpay checkout completion.
7
- *
8
- * @param keySecret - The Razorpay key secret used for signature verification
9
- * @returns A Better Auth endpoint handler
10
- *
11
- * @remarks
12
- * This endpoint:
13
- * - Requires user authentication via session
14
- * - Verifies payment signature using HMAC SHA256
15
- * - Validates subscription ownership
16
- * - Updates subscription status to 'authenticated'
17
- * - Updates user record with payment date
18
- * - Should be called after successful Razorpay checkout
19
- *
20
- * @example
21
- * Request body:
22
- * ```json
23
- * {
24
- * "razorpay_payment_id": "pay_1234567890",
25
- * "razorpay_subscription_id": "sub_1234567890",
26
- * "razorpay_signature": "abc123..."
27
- * }
28
- * ```
29
- */
30
- export const verifyPayment = (keySecret: string) =>
31
- createAuthEndpoint(
32
- '/razorpay/verify-payment',
33
- { method: 'POST', use: [sessionMiddleware] },
34
- async (_ctx) => {
35
- try {
36
- // Validate input using Zod schema
37
- const validatedInput = verifyPaymentSchema.parse(_ctx.body)
38
-
39
- const { razorpay_payment_id, razorpay_subscription_id, razorpay_signature } = validatedInput
40
-
41
- // Generate expected signature
42
- const generated_signature = createHmac('sha256', keySecret)
43
- .update(`${razorpay_payment_id}|${razorpay_subscription_id}`)
44
- .digest('hex')
45
-
46
- // Verify signature
47
- if (generated_signature !== razorpay_signature) {
48
- return {
49
- success: false,
50
- error: {
51
- code: 'SIGNATURE_VERIFICATION_FAILED',
52
- description: 'Payment signature verification failed',
53
- },
54
- }
55
- }
56
-
57
- // Get user ID from session
58
- const userId = _ctx.context.session?.user?.id
59
-
60
- if (!userId) {
61
- return {
62
- success: false,
63
- error: {
64
- code: 'UNAUTHORIZED',
65
- description: 'User not authenticated',
66
- },
67
- }
68
- }
69
-
70
- // Get subscription record to verify it belongs to the user
71
- const subscriptionRecord = (await _ctx.context.adapter.findOne({
72
- model: 'razorpaySubscription',
73
- where: [{ field: 'subscriptionId', value: razorpay_subscription_id }],
74
- })) as RazorpaySubscriptionRecord | null
75
-
76
- if (!subscriptionRecord) {
77
- return {
78
- success: false,
79
- error: {
80
- code: 'SUBSCRIPTION_NOT_FOUND',
81
- description: 'Subscription not found',
82
- },
83
- }
84
- }
85
-
86
- // Verify that the subscription belongs to the authenticated user
87
- if (subscriptionRecord.userId !== userId) {
88
- return {
89
- success: false,
90
- error: {
91
- code: 'UNAUTHORIZED',
92
- description: 'Subscription does not belong to authenticated user',
93
- },
94
- }
95
- }
96
-
97
- // Update subscription status to authenticated/active
98
- await _ctx.context.adapter.update({
99
- model: 'razorpaySubscription',
100
- where: [{ field: 'subscriptionId', value: razorpay_subscription_id }],
101
- update: { status: 'authenticated' },
102
- })
103
-
104
- // Update user table with subscription status and payment date
105
- await _ctx.context.adapter.update({
106
- model: 'user',
107
- where: [{ field: 'id', value: userId }],
108
- update: {
109
- data: {
110
- subscriptionStatus: 'authenticated',
111
- subscriptionId: razorpay_subscription_id,
112
- lastPaymentDate: new Date(),
113
- },
114
- },
115
- })
116
-
117
- return {
118
- success: true,
119
- data: {
120
- message: 'Payment verified successfully',
121
- payment_id: razorpay_payment_id,
122
- subscription_id: razorpay_subscription_id,
123
- },
124
- }
125
- } catch (error) {
126
- return handleRazorpayError(error)
127
- }
128
- }
129
- )