@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 +258 -49
- package/api/cancel-subscription.ts +35 -80
- package/api/create-or-update-subscription.ts +254 -0
- package/api/get-plans.ts +26 -53
- package/api/index.ts +3 -5
- package/api/list-subscriptions.ts +79 -0
- package/api/restore-subscription.ts +79 -0
- package/api/webhook.ts +153 -121
- package/client/hooks.ts +248 -0
- package/client/types.ts +108 -0
- package/index.ts +110 -77
- package/lib/index.ts +10 -4
- package/lib/schemas.ts +20 -44
- package/lib/types.ts +121 -68
- package/package.json +12 -2
- package/api/get-subscription.ts +0 -174
- package/api/pause-subscription.ts +0 -138
- package/api/resume-subscription.ts +0 -150
- package/api/subscribe.ts +0 -188
- package/api/verify-payment.ts +0 -129
|
@@ -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
|
-
)
|
package/api/verify-payment.ts
DELETED
|
@@ -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
|
-
)
|