@deiondz/better-auth-razorpay 2.0.5 → 2.0.8

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,254 +0,0 @@
1
- import { createAuthEndpoint, sessionMiddleware } from 'better-auth/api'
2
- import type Razorpay from 'razorpay'
3
- import {
4
- createOrUpdateSubscriptionSchema,
5
- handleRazorpayError,
6
- type RazorpayPlan,
7
- type RazorpayPluginOptions,
8
- type RazorpaySubscription,
9
- type RazorpayUserRecord,
10
- type SubscriptionRecord,
11
- } from '../lib'
12
-
13
- async function resolvePlans(
14
- plans: RazorpayPlan[] | (() => Promise<RazorpayPlan[]>)
15
- ): Promise<RazorpayPlan[]> {
16
- return typeof plans === 'function' ? plans() : plans
17
- }
18
-
19
- function toLocalStatus(razorpayStatus: string): SubscriptionRecord['status'] {
20
- const map: Record<string, SubscriptionRecord['status']> = {
21
- created: 'created',
22
- authenticated: 'pending',
23
- active: 'active',
24
- pending: 'pending',
25
- halted: 'halted',
26
- cancelled: 'cancelled',
27
- completed: 'completed',
28
- expired: 'expired',
29
- }
30
- return map[razorpayStatus] ?? 'pending'
31
- }
32
-
33
- /**
34
- * POST /api/auth/razorpay/subscription/create-or-update
35
- * Creates a new subscription or updates an existing one (plan/quantity).
36
- * Returns checkoutUrl for Razorpay payment page, or updated subscription for updates.
37
- */
38
- export const createOrUpdateSubscription = (
39
- razorpay: Razorpay,
40
- options: Pick<
41
- RazorpayPluginOptions,
42
- 'subscription' | 'createCustomerOnSignUp'
43
- >
44
- ) =>
45
- createAuthEndpoint(
46
- '/razorpay/subscription/create-or-update',
47
- { method: 'POST', use: [sessionMiddleware] },
48
- async (ctx) => {
49
- try {
50
- const body = createOrUpdateSubscriptionSchema.parse(ctx.body)
51
- const subOpts = options.subscription
52
- if (!subOpts?.enabled) {
53
- return {
54
- success: false,
55
- error: { code: 'SUBSCRIPTION_DISABLED', description: 'Subscription feature is disabled' },
56
- }
57
- }
58
-
59
- const plans = await resolvePlans(subOpts.plans)
60
- const plan = plans.find((p) => p.name === body.plan)
61
- if (!plan) {
62
- return {
63
- success: false,
64
- error: { code: 'PLAN_NOT_FOUND', description: `Plan "${body.plan}" not found` },
65
- }
66
- }
67
-
68
- const planId = body.annual && plan.annualPlanId ? plan.annualPlanId : plan.monthlyPlanId
69
- const userId = ctx.context.session?.user?.id
70
- if (!userId) {
71
- return {
72
- success: false,
73
- error: { code: 'UNAUTHORIZED', description: 'User not authenticated' },
74
- }
75
- }
76
-
77
- const user = (await ctx.context.adapter.findOne({
78
- model: 'user',
79
- where: [{ field: 'id', value: userId }],
80
- })) as RazorpayUserRecord | null
81
- if (!user) {
82
- return {
83
- success: false,
84
- error: { code: 'USER_NOT_FOUND', description: 'User not found' },
85
- }
86
- }
87
-
88
- if (subOpts.requireEmailVerification && user.email) {
89
- // If your Better Auth setup has emailVerified, check it here
90
- // const verified = (user as { emailVerified?: boolean }).emailVerified
91
- // if (!verified) return { success: false, error: { code: 'EMAIL_NOT_VERIFIED', ... } }
92
- }
93
-
94
- if (subOpts.authorizeReference) {
95
- const allowed = await subOpts.authorizeReference({
96
- user: user as { id: string; email?: string; name?: string; [key: string]: unknown },
97
- referenceId: userId,
98
- action: 'create-or-update',
99
- })
100
- if (!allowed) {
101
- return {
102
- success: false,
103
- error: { code: 'FORBIDDEN', description: 'Not authorized to manage this subscription' },
104
- }
105
- }
106
- }
107
-
108
- const now = new Date()
109
- const generateId = ctx.context.generateId as
110
- | ((options: { model: string; size?: number }) => string | false)
111
- | undefined
112
- const generated =
113
- typeof generateId === 'function'
114
- ? generateId({ model: 'subscription' })
115
- : undefined
116
- const localId =
117
- (typeof generated === 'string' ? generated : undefined) ??
118
- `sub_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`
119
-
120
- // Update existing subscription (by local id)
121
- if (body.subscriptionId) {
122
- const existing = (await ctx.context.adapter.findOne({
123
- model: 'subscription',
124
- where: [{ field: 'id', value: body.subscriptionId }],
125
- })) as SubscriptionRecord | null
126
- if (!existing) {
127
- return {
128
- success: false,
129
- error: { code: 'SUBSCRIPTION_NOT_FOUND', description: 'Subscription not found' },
130
- }
131
- }
132
- if (existing.referenceId !== userId) {
133
- return {
134
- success: false,
135
- error: { code: 'FORBIDDEN', description: 'Subscription does not belong to you' },
136
- }
137
- }
138
- // For "update" we could call Razorpay subscription update API if needed.
139
- // Here we treat update as "create new" is not required by GitHub README; they return "updated subscription".
140
- const rpSub = existing.razorpaySubscriptionId
141
- ? ((await razorpay.subscriptions.fetch(existing.razorpaySubscriptionId)) as RazorpaySubscription)
142
- : null
143
- if (rpSub) {
144
- return {
145
- success: true,
146
- data: {
147
- checkoutUrl: rpSub.short_url,
148
- subscription: {
149
- id: existing.id,
150
- plan: existing.plan,
151
- status: existing.status,
152
- razorpaySubscriptionId: existing.razorpaySubscriptionId,
153
- cancelAtPeriodEnd: existing.cancelAtPeriodEnd,
154
- periodEnd: existing.periodEnd,
155
- seats: existing.seats,
156
- },
157
- },
158
- }
159
- }
160
- }
161
-
162
- // Create new subscription
163
- const totalCount = body.annual ? 1 : 12
164
- const subscriptionPayload: Parameters<Razorpay['subscriptions']['create']>[0] = {
165
- plan_id: planId,
166
- total_count: totalCount,
167
- quantity: body.seats,
168
- customer_notify: true,
169
- notes: { referenceId: userId, planName: plan.name },
170
- }
171
-
172
- if (subOpts.getSubscriptionCreateParams) {
173
- const tempSub: SubscriptionRecord = {
174
- id: '',
175
- plan: plan.name,
176
- referenceId: userId,
177
- status: 'created',
178
- cancelAtPeriodEnd: false,
179
- seats: body.seats,
180
- createdAt: now,
181
- updatedAt: now,
182
- }
183
- const extra = await subOpts.getSubscriptionCreateParams({
184
- user: user as { id: string; email?: string; name?: string; [key: string]: unknown },
185
- session: ctx.context.session,
186
- plan,
187
- subscription: tempSub,
188
- })
189
- if (extra?.params?.notes && typeof extra.params.notes === 'object') {
190
- subscriptionPayload.notes = { ...subscriptionPayload.notes, ...extra.params.notes }
191
- }
192
- if (extra?.params && typeof extra.params === 'object') {
193
- Object.assign(subscriptionPayload, extra.params)
194
- }
195
- }
196
-
197
- const rpSubscription = (await razorpay.subscriptions.create(
198
- subscriptionPayload
199
- )) as RazorpaySubscription
200
-
201
- const subscriptionRecord: Omit<SubscriptionRecord, 'id'> & { id: string } = {
202
- id: localId,
203
- plan: plan.name,
204
- referenceId: userId,
205
- razorpayCustomerId: user.razorpayCustomerId ?? null,
206
- razorpaySubscriptionId: rpSubscription.id,
207
- status: toLocalStatus(rpSubscription.status),
208
- trialStart: null,
209
- trialEnd: null,
210
- periodStart: rpSubscription.current_start
211
- ? new Date(rpSubscription.current_start * 1000)
212
- : null,
213
- periodEnd: rpSubscription.current_end
214
- ? new Date(rpSubscription.current_end * 1000)
215
- : null,
216
- cancelAtPeriodEnd: false,
217
- seats: body.seats,
218
- groupId: null,
219
- createdAt: now,
220
- updatedAt: now,
221
- }
222
-
223
- await ctx.context.adapter.create({
224
- model: 'subscription',
225
- data: subscriptionRecord,
226
- })
227
-
228
- if (subOpts.onSubscriptionCreated) {
229
- await subOpts.onSubscriptionCreated({
230
- razorpaySubscription: rpSubscription,
231
- subscription: subscriptionRecord as SubscriptionRecord,
232
- plan,
233
- })
234
- }
235
-
236
- const checkoutUrl = body.disableRedirect
237
- ? rpSubscription.short_url
238
- : body.successUrl
239
- ? `${rpSubscription.short_url}?redirect=${encodeURIComponent(body.successUrl)}`
240
- : rpSubscription.short_url
241
-
242
- return {
243
- success: true,
244
- data: {
245
- checkoutUrl,
246
- subscriptionId: localId,
247
- razorpaySubscriptionId: rpSubscription.id,
248
- },
249
- }
250
- } catch (error) {
251
- return handleRazorpayError(error)
252
- }
253
- }
254
- )
package/api/get-plans.ts DELETED
@@ -1,36 +0,0 @@
1
- import { createAuthEndpoint } from 'better-auth/api'
2
- import { handleRazorpayError, type RazorpayPlan, type RazorpayPluginOptions } from '../lib'
3
-
4
- async function resolvePlans(
5
- plans: RazorpayPlan[] | (() => Promise<RazorpayPlan[]>)
6
- ): Promise<RazorpayPlan[]> {
7
- return typeof plans === 'function' ? plans() : plans
8
- }
9
-
10
- /**
11
- * GET /api/auth/razorpay/get-plans
12
- * Returns the configured subscription plans (name, monthlyPlanId, annualPlanId, limits, freeTrial).
13
- * Does not call Razorpay API.
14
- */
15
- export const getPlans = (options: Pick<RazorpayPluginOptions, 'subscription'>) =>
16
- createAuthEndpoint('/razorpay/get-plans', { method: 'GET' }, async (_ctx) => {
17
- try {
18
- const subOpts = options.subscription
19
- if (!subOpts?.enabled) {
20
- return { success: true, data: [] }
21
- }
22
- const plans = await resolvePlans(subOpts.plans)
23
- return {
24
- success: true,
25
- data: plans.map((p) => ({
26
- name: p.name,
27
- monthlyPlanId: p.monthlyPlanId,
28
- annualPlanId: p.annualPlanId,
29
- limits: p.limits,
30
- freeTrial: p.freeTrial ? { days: p.freeTrial.days } : undefined,
31
- })),
32
- }
33
- } catch (error) {
34
- return handleRazorpayError(error)
35
- }
36
- })
package/api/index.ts DELETED
@@ -1,7 +0,0 @@
1
- export { cancelSubscription } from './cancel-subscription'
2
- export { createOrUpdateSubscription } from './create-or-update-subscription'
3
- export { getPlans } from './get-plans'
4
- export { listSubscriptions } from './list-subscriptions'
5
- export { restoreSubscription } from './restore-subscription'
6
- export { verifyPayment } from './verify-payment'
7
- export { webhook } from './webhook'
@@ -1,79 +0,0 @@
1
- import { createAuthEndpoint, sessionMiddleware } from 'better-auth/api'
2
- import {
3
- handleRazorpayError,
4
- listSubscriptionsSchema,
5
- type RazorpayPluginOptions,
6
- type SubscriptionRecord,
7
- } from '../lib'
8
-
9
- const ACTIVE_STATUSES: SubscriptionRecord['status'][] = [
10
- 'active',
11
- 'trialing',
12
- 'pending',
13
- 'created',
14
- ]
15
-
16
- /**
17
- * GET /api/auth/razorpay/subscription/list
18
- * Lists active and trialing subscriptions for the current user (or referenceId if authorized).
19
- */
20
- export const listSubscriptions = (
21
- options: Pick<RazorpayPluginOptions, 'subscription'>
22
- ) =>
23
- createAuthEndpoint(
24
- '/razorpay/subscription/list',
25
- { method: 'GET', use: [sessionMiddleware] },
26
- async (ctx) => {
27
- try {
28
- const query = listSubscriptionsSchema.parse(ctx.query ?? {})
29
- const userId = ctx.context.session?.user?.id
30
- if (!userId) {
31
- return {
32
- success: false,
33
- error: { code: 'UNAUTHORIZED', description: 'User not authenticated' },
34
- }
35
- }
36
-
37
- const referenceId = query.referenceId ?? userId
38
- if (referenceId !== userId && options.subscription?.authorizeReference) {
39
- const user = (await ctx.context.adapter.findOne({
40
- model: 'user',
41
- where: [{ field: 'id', value: userId }],
42
- })) as { id: string; email?: string; name?: string } | null
43
- if (!user) {
44
- return {
45
- success: false,
46
- error: { code: 'USER_NOT_FOUND', description: 'User not found' },
47
- }
48
- }
49
- const allowed = await options.subscription.authorizeReference({
50
- user: user as { id: string; email?: string; name?: string; [key: string]: unknown },
51
- referenceId,
52
- action: 'list',
53
- })
54
- if (!allowed) {
55
- return {
56
- success: false,
57
- error: { code: 'FORBIDDEN', description: 'Not authorized to list this user\'s subscriptions' },
58
- }
59
- }
60
- }
61
-
62
- const list = (await ctx.context.adapter.findMany({
63
- model: 'subscription',
64
- where: [{ field: 'referenceId', value: referenceId }],
65
- })) as SubscriptionRecord[] | null
66
-
67
- const subscriptions = (list ?? []).filter((s) =>
68
- ACTIVE_STATUSES.includes(s.status)
69
- )
70
-
71
- return {
72
- success: true,
73
- data: subscriptions,
74
- }
75
- } catch (error) {
76
- return handleRazorpayError(error)
77
- }
78
- }
79
- )
@@ -1,79 +0,0 @@
1
- import { createAuthEndpoint, sessionMiddleware } from 'better-auth/api'
2
- import type Razorpay from 'razorpay'
3
- import {
4
- handleRazorpayError,
5
- restoreSubscriptionSchema,
6
- type SubscriptionRecord,
7
- } from '../lib'
8
-
9
- /**
10
- * POST /api/auth/razorpay/subscription/restore
11
- * Restores a subscription that was scheduled to cancel at period end.
12
- */
13
- export const restoreSubscription = (razorpay: Razorpay) =>
14
- createAuthEndpoint(
15
- '/razorpay/subscription/restore',
16
- { method: 'POST', use: [sessionMiddleware] },
17
- async (ctx) => {
18
- try {
19
- const body = restoreSubscriptionSchema.parse(ctx.body)
20
- const userId = ctx.context.session?.user?.id
21
- if (!userId) {
22
- return {
23
- success: false,
24
- error: { code: 'UNAUTHORIZED', description: 'User not authenticated' },
25
- }
26
- }
27
-
28
- const record = (await ctx.context.adapter.findOne({
29
- model: 'subscription',
30
- where: [{ field: 'id', value: body.subscriptionId }],
31
- })) as SubscriptionRecord | null
32
-
33
- if (!record) {
34
- return {
35
- success: false,
36
- error: { code: 'SUBSCRIPTION_NOT_FOUND', description: 'Subscription not found' },
37
- }
38
- }
39
- if (record.referenceId !== userId) {
40
- return {
41
- success: false,
42
- error: { code: 'FORBIDDEN', description: 'Subscription does not belong to you' },
43
- }
44
- }
45
-
46
- const rpId = record.razorpaySubscriptionId
47
- if (!rpId) {
48
- return {
49
- success: false,
50
- error: { code: 'INVALID_STATE', description: 'No Razorpay subscription linked' },
51
- }
52
- }
53
-
54
- // Razorpay: resume a paused subscription (or cancel scheduled cancellation)
55
- const subscription = await razorpay.subscriptions.resume(rpId)
56
-
57
- await ctx.context.adapter.update({
58
- model: 'subscription',
59
- where: [{ field: 'id', value: body.subscriptionId }],
60
- update: {
61
- data: {
62
- cancelAtPeriodEnd: false,
63
- updatedAt: new Date(),
64
- },
65
- },
66
- })
67
-
68
- return {
69
- success: true,
70
- data: {
71
- id: (subscription as { id: string }).id,
72
- status: (subscription as { status: string }).status,
73
- },
74
- }
75
- } catch (error) {
76
- return handleRazorpayError(error)
77
- }
78
- }
79
- )
@@ -1,87 +0,0 @@
1
- import { createHmac } from 'node:crypto'
2
- import { createAuthEndpoint, sessionMiddleware } from 'better-auth/api'
3
- import {
4
- handleRazorpayError,
5
- verifyPaymentSchema,
6
- type SubscriptionRecord,
7
- } from '../lib'
8
-
9
- /**
10
- * POST /api/auth/razorpay/verify-payment
11
- * Verifies payment signature after Razorpay subscription checkout completion.
12
- * Requires razorpayKeySecret to be set in plugin options.
13
- */
14
- export const verifyPayment = (keySecret: string) =>
15
- createAuthEndpoint(
16
- '/razorpay/verify-payment',
17
- { method: 'POST', use: [sessionMiddleware] },
18
- async (ctx) => {
19
- try {
20
- const body = verifyPaymentSchema.parse(ctx.body)
21
- const { razorpay_payment_id, razorpay_subscription_id, razorpay_signature } = body
22
-
23
- const generatedSignature = createHmac('sha256', keySecret)
24
- .update(`${razorpay_payment_id}|${razorpay_subscription_id}`)
25
- .digest('hex')
26
-
27
- if (generatedSignature !== razorpay_signature) {
28
- return {
29
- success: false,
30
- error: {
31
- code: 'SIGNATURE_VERIFICATION_FAILED',
32
- description: 'Payment signature verification failed',
33
- },
34
- }
35
- }
36
-
37
- const userId = ctx.context.session?.user?.id
38
- if (!userId) {
39
- return {
40
- success: false,
41
- error: { code: 'UNAUTHORIZED', description: 'User not authenticated' },
42
- }
43
- }
44
-
45
- const record = (await ctx.context.adapter.findOne({
46
- model: 'subscription',
47
- where: [{ field: 'razorpaySubscriptionId', value: razorpay_subscription_id }],
48
- })) as SubscriptionRecord | null
49
-
50
- if (!record) {
51
- return {
52
- success: false,
53
- error: { code: 'SUBSCRIPTION_NOT_FOUND', description: 'Subscription not found' },
54
- }
55
- }
56
-
57
- if (record.referenceId !== userId) {
58
- return {
59
- success: false,
60
- error: { code: 'FORBIDDEN', description: 'Subscription does not belong to you' },
61
- }
62
- }
63
-
64
- await ctx.context.adapter.update({
65
- model: 'subscription',
66
- where: [{ field: 'razorpaySubscriptionId', value: razorpay_subscription_id }],
67
- update: {
68
- data: {
69
- status: 'pending',
70
- updatedAt: new Date(),
71
- },
72
- },
73
- })
74
-
75
- return {
76
- success: true,
77
- data: {
78
- message: 'Payment verified successfully',
79
- payment_id: razorpay_payment_id,
80
- subscription_id: razorpay_subscription_id,
81
- },
82
- }
83
- } catch (error) {
84
- return handleRazorpayError(error)
85
- }
86
- }
87
- )