@deiondz/better-auth-razorpay 1.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 +1201 -0
- package/api/cancel-subscription.ts +135 -0
- package/api/get-plans.ts +63 -0
- package/api/get-subscription.ts +174 -0
- package/api/index.ts +8 -0
- package/api/pause-subscription.ts +138 -0
- package/api/resume-subscription.ts +150 -0
- package/api/subscribe.ts +188 -0
- package/api/verify-payment.ts +129 -0
- package/api/webhook.ts +273 -0
- package/client.ts +8 -0
- package/index.ts +123 -0
- package/lib/error-handler.ts +99 -0
- package/lib/index.ts +22 -0
- package/lib/schemas.ts +58 -0
- package/lib/types.ts +151 -0
- package/package.json +58 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { createAuthEndpoint, sessionMiddleware } from 'better-auth/api'
|
|
2
|
+
import type Razorpay from 'razorpay'
|
|
3
|
+
import {
|
|
4
|
+
cancelSubscriptionSchema,
|
|
5
|
+
handleRazorpayError,
|
|
6
|
+
type RazorpaySubscription,
|
|
7
|
+
type RazorpaySubscriptionRecord,
|
|
8
|
+
} from '../lib'
|
|
9
|
+
|
|
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
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export const cancelSubscription = (razorpay: Razorpay) =>
|
|
33
|
+
createAuthEndpoint(
|
|
34
|
+
'/razorpay/cancel-subscription',
|
|
35
|
+
{ method: 'POST', use: [sessionMiddleware] },
|
|
36
|
+
async (_ctx) => {
|
|
37
|
+
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
|
+
|
|
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
|
+
// Cancel subscription via Razorpay API (cancel at period end)
|
|
83
|
+
const subscription = (await razorpay.subscriptions.cancel(
|
|
84
|
+
validatedInput.subscription_id,
|
|
85
|
+
true
|
|
86
|
+
)) as RazorpaySubscription
|
|
87
|
+
|
|
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 }],
|
|
92
|
+
update: {
|
|
93
|
+
data: {
|
|
94
|
+
cancelAtPeriodEnd: true,
|
|
95
|
+
subscriptionCurrentPeriodEnd: subscription.current_end
|
|
96
|
+
? new Date(subscription.current_end * 1000)
|
|
97
|
+
: null,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
success: true,
|
|
104
|
+
data: {
|
|
105
|
+
id: subscription.id,
|
|
106
|
+
entity: subscription.entity,
|
|
107
|
+
plan_id: subscription.plan_id,
|
|
108
|
+
status: subscription.status,
|
|
109
|
+
current_start: subscription.current_start,
|
|
110
|
+
current_end: subscription.current_end,
|
|
111
|
+
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
|
+
},
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return handleRazorpayError(error)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
)
|
package/api/get-plans.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createAuthEndpoint } from 'better-auth/api'
|
|
2
|
+
import type Razorpay from 'razorpay'
|
|
3
|
+
import { getPlansSchema, handleRazorpayError } from '../lib'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Retrieves plan details from Razorpay for configured plan IDs.
|
|
7
|
+
*
|
|
8
|
+
* @param razorpay - The Razorpay instance initialized with API credentials
|
|
9
|
+
* @param planIds - Array of plan IDs configured in the plugin options
|
|
10
|
+
* @returns A Better Auth endpoint handler
|
|
11
|
+
*
|
|
12
|
+
* @remarks
|
|
13
|
+
* This endpoint:
|
|
14
|
+
* - Fetches plan details from Razorpay API
|
|
15
|
+
* - Silently skips plans that fail to fetch (filters them out)
|
|
16
|
+
* - Returns all successfully fetched plans
|
|
17
|
+
* - Does not require authentication (public endpoint)
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* Response (success):
|
|
21
|
+
* ```json
|
|
22
|
+
* {
|
|
23
|
+
* "success": true,
|
|
24
|
+
* "data": [
|
|
25
|
+
* {
|
|
26
|
+
* "id": "plan_1234567890",
|
|
27
|
+
* "name": "Premium Plan",
|
|
28
|
+
* "amount": 1000,
|
|
29
|
+
* ...
|
|
30
|
+
* }
|
|
31
|
+
* ]
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export const getPlans = (razorpay: Razorpay, planIds: string[]) =>
|
|
36
|
+
createAuthEndpoint('/razorpay/get-plans', { method: 'GET' }, async (_ctx) => {
|
|
37
|
+
try {
|
|
38
|
+
// GET requests don't have a body, so validation is optional
|
|
39
|
+
// Schema allows undefined for GET requests
|
|
40
|
+
getPlansSchema.parse(_ctx.body)
|
|
41
|
+
|
|
42
|
+
// Fetch plan details from Razorpay API using configured plan IDs
|
|
43
|
+
const plans = await Promise.all(
|
|
44
|
+
planIds.map(async (planId) => {
|
|
45
|
+
try {
|
|
46
|
+
const plan = await razorpay.plans.fetch(planId)
|
|
47
|
+
return plan
|
|
48
|
+
} catch {
|
|
49
|
+
// Silently skip failed plan fetches - they will be filtered out
|
|
50
|
+
// Individual plan fetch failures shouldn't break the entire request
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
// Filter out any null values from failed fetches
|
|
57
|
+
const validPlans = plans.filter((plan) => plan !== null)
|
|
58
|
+
|
|
59
|
+
return { success: true, data: validPlans }
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return handleRazorpayError(error)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { createAuthEndpoint, sessionMiddleware } from 'better-auth/api'
|
|
2
|
+
import type Razorpay from 'razorpay'
|
|
3
|
+
import {
|
|
4
|
+
handleRazorpayError,
|
|
5
|
+
type RazorpaySubscription,
|
|
6
|
+
type RazorpayUserRecord,
|
|
7
|
+
} from '../lib'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Retrieves the current subscription details for the authenticated user.
|
|
11
|
+
*
|
|
12
|
+
* @param razorpay - The Razorpay instance initialized with API credentials
|
|
13
|
+
* @returns A Better Auth endpoint handler
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* This endpoint:
|
|
17
|
+
* - Requires user authentication via session
|
|
18
|
+
* - Fetches subscription details from Razorpay API
|
|
19
|
+
* - Includes cancellation status and period end information
|
|
20
|
+
* - Returns null if user has no active subscription
|
|
21
|
+
* - Provides detailed error messages in development mode
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* Response (success):
|
|
25
|
+
* ```json
|
|
26
|
+
* {
|
|
27
|
+
* "success": true,
|
|
28
|
+
* "data": {
|
|
29
|
+
* "id": "sub_1234567890",
|
|
30
|
+
* "status": "active",
|
|
31
|
+
* "plan_id": "plan_1234567890",
|
|
32
|
+
* "cancel_at_period_end": false,
|
|
33
|
+
* ...
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export const getSubscription = (razorpay: Razorpay) =>
|
|
39
|
+
createAuthEndpoint(
|
|
40
|
+
'/razorpay/get-subscription',
|
|
41
|
+
{ method: 'GET', use: [sessionMiddleware] },
|
|
42
|
+
async (_ctx) => {
|
|
43
|
+
try {
|
|
44
|
+
// Get user ID from session
|
|
45
|
+
const userId = _ctx.context.session?.user?.id
|
|
46
|
+
|
|
47
|
+
if (!userId) {
|
|
48
|
+
return {
|
|
49
|
+
success: false,
|
|
50
|
+
error: {
|
|
51
|
+
code: 'UNAUTHORIZED',
|
|
52
|
+
description: 'User not authenticated',
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Get user record to check subscription status
|
|
58
|
+
const user = (await _ctx.context.adapter.findOne({
|
|
59
|
+
model: 'user',
|
|
60
|
+
where: [{ field: 'id', value: userId }],
|
|
61
|
+
})) as RazorpayUserRecord | null
|
|
62
|
+
|
|
63
|
+
if (!user) {
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
error: {
|
|
67
|
+
code: 'USER_NOT_FOUND',
|
|
68
|
+
description: 'User not found',
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check subscription status from user table
|
|
74
|
+
const subscriptionId = user.subscriptionId
|
|
75
|
+
|
|
76
|
+
if (!subscriptionId) {
|
|
77
|
+
return {
|
|
78
|
+
success: true,
|
|
79
|
+
data: null,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fetch full subscription details from Razorpay API
|
|
84
|
+
try {
|
|
85
|
+
const subscription = (await razorpay.subscriptions.fetch(
|
|
86
|
+
subscriptionId
|
|
87
|
+
)) as RazorpaySubscription
|
|
88
|
+
|
|
89
|
+
// Read cancellation status from user table
|
|
90
|
+
const cancelAtPeriodEnd = user.cancelAtPeriodEnd ?? false
|
|
91
|
+
const subscriptionCurrentPeriodEnd = user.subscriptionCurrentPeriodEnd
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
success: true,
|
|
95
|
+
data: {
|
|
96
|
+
id: subscription.id,
|
|
97
|
+
entity: subscription.entity,
|
|
98
|
+
plan_id: subscription.plan_id,
|
|
99
|
+
status: subscription.status,
|
|
100
|
+
current_start: subscription.current_start,
|
|
101
|
+
current_end: subscription.current_end,
|
|
102
|
+
ended_at: subscription.ended_at,
|
|
103
|
+
quantity: subscription.quantity,
|
|
104
|
+
notes: subscription.notes,
|
|
105
|
+
charge_at: subscription.charge_at,
|
|
106
|
+
start_at: subscription.start_at,
|
|
107
|
+
end_at: subscription.end_at,
|
|
108
|
+
auth_attempts: subscription.auth_attempts,
|
|
109
|
+
total_count: subscription.total_count,
|
|
110
|
+
paid_count: subscription.paid_count,
|
|
111
|
+
customer_notify: subscription.customer_notify,
|
|
112
|
+
created_at: subscription.created_at,
|
|
113
|
+
expire_by: subscription.expire_by,
|
|
114
|
+
short_url: subscription.short_url,
|
|
115
|
+
has_scheduled_changes: subscription.has_scheduled_changes,
|
|
116
|
+
change_scheduled_at: subscription.change_scheduled_at,
|
|
117
|
+
source: subscription.source,
|
|
118
|
+
offer_id: subscription.offer_id,
|
|
119
|
+
remaining_count: subscription.remaining_count,
|
|
120
|
+
cancel_at_period_end: cancelAtPeriodEnd,
|
|
121
|
+
subscription_current_period_end: subscriptionCurrentPeriodEnd
|
|
122
|
+
? Math.floor(new Date(subscriptionCurrentPeriodEnd).getTime() / 1000)
|
|
123
|
+
: null,
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
} catch (razorpayError) {
|
|
127
|
+
// Handle Razorpay-specific errors with subscription ID context
|
|
128
|
+
const isDev = process.env.NODE_ENV === 'development'
|
|
129
|
+
|
|
130
|
+
// Extract error message from Razorpay error
|
|
131
|
+
let errorMessage = 'Failed to fetch subscription'
|
|
132
|
+
let errorCode = 'SUBSCRIPTION_FETCH_FAILED'
|
|
133
|
+
|
|
134
|
+
if (razorpayError && typeof razorpayError === 'object') {
|
|
135
|
+
// Razorpay error format: { error: { code: string, description: string } }
|
|
136
|
+
if ('error' in razorpayError) {
|
|
137
|
+
const razorpayErr = razorpayError as {
|
|
138
|
+
error?: { code?: string; description?: string }
|
|
139
|
+
}
|
|
140
|
+
errorCode = razorpayErr.error?.code || errorCode
|
|
141
|
+
errorMessage = razorpayErr.error?.description || errorMessage
|
|
142
|
+
} else if ('message' in razorpayError) {
|
|
143
|
+
errorMessage = (razorpayError as { message: string }).message
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check if error is specifically about subscription not existing
|
|
148
|
+
const isNotFoundError =
|
|
149
|
+
errorMessage.toLowerCase().includes('does not exist') ||
|
|
150
|
+
errorMessage.toLowerCase().includes('not found') ||
|
|
151
|
+
errorCode === 'BAD_REQUEST_ERROR'
|
|
152
|
+
|
|
153
|
+
// Include subscription ID in error for debugging
|
|
154
|
+
const description = isDev
|
|
155
|
+
? `${errorMessage} (Subscription ID: ${subscriptionId})`
|
|
156
|
+
: isNotFoundError
|
|
157
|
+
? 'The subscription could not be found. This may indicate the subscription was deleted or the ID is invalid. Please contact support if this issue persists.'
|
|
158
|
+
: 'Unable to retrieve subscription information. Please try again or contact support if this issue persists.'
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
success: false,
|
|
162
|
+
error: {
|
|
163
|
+
code: errorCode,
|
|
164
|
+
description,
|
|
165
|
+
// Include subscription ID in metadata for development
|
|
166
|
+
...(isDev && { subscriptionId }),
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
return handleRazorpayError(error)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
)
|
package/api/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { cancelSubscription } from './cancel-subscription'
|
|
2
|
+
export { getPlans } from './get-plans'
|
|
3
|
+
export { getSubscription } from './get-subscription'
|
|
4
|
+
export { pauseSubscription } from './pause-subscription'
|
|
5
|
+
export { resumeSubscription } from './resume-subscription'
|
|
6
|
+
export { subscribe } from './subscribe'
|
|
7
|
+
export { verifyPayment } from './verify-payment'
|
|
8
|
+
export { webhook } from './webhook'
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createAuthEndpoint, sessionMiddleware } from 'better-auth/api'
|
|
2
|
+
import type Razorpay from 'razorpay'
|
|
3
|
+
import {
|
|
4
|
+
handleRazorpayError,
|
|
5
|
+
pauseSubscriptionSchema,
|
|
6
|
+
type RazorpaySubscription,
|
|
7
|
+
type RazorpaySubscriptionRecord,
|
|
8
|
+
} from '../lib'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Pauses an active 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 pausing
|
|
20
|
+
* - Pauses the subscription via Razorpay API
|
|
21
|
+
* - Updates subscription and user records with paused status
|
|
22
|
+
* - Paused subscriptions can be resumed later
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* Request body:
|
|
26
|
+
* ```json
|
|
27
|
+
* {
|
|
28
|
+
* "subscription_id": "sub_1234567890"
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export const pauseSubscription = (razorpay: Razorpay) =>
|
|
33
|
+
createAuthEndpoint(
|
|
34
|
+
'/razorpay/pause-subscription',
|
|
35
|
+
{ method: 'POST', use: [sessionMiddleware] },
|
|
36
|
+
async (_ctx) => {
|
|
37
|
+
try {
|
|
38
|
+
// Validate input using Zod schema
|
|
39
|
+
const validatedInput = pauseSubscriptionSchema.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
|
+
// Pause subscription via Razorpay API
|
|
83
|
+
const subscription = (await razorpay.subscriptions.pause(
|
|
84
|
+
validatedInput.subscription_id
|
|
85
|
+
)) as RazorpaySubscription
|
|
86
|
+
|
|
87
|
+
// Update subscription status in database
|
|
88
|
+
await _ctx.context.adapter.update({
|
|
89
|
+
model: 'razorpaySubscription',
|
|
90
|
+
where: [{ field: 'subscriptionId', value: validatedInput.subscription_id }],
|
|
91
|
+
update: { status: subscription.status },
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// Update user table with subscription status
|
|
95
|
+
await _ctx.context.adapter.update({
|
|
96
|
+
model: 'user',
|
|
97
|
+
where: [{ field: 'id', value: userId }],
|
|
98
|
+
update: {
|
|
99
|
+
data: {
|
|
100
|
+
subscriptionStatus: subscription.status,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
success: true,
|
|
107
|
+
data: {
|
|
108
|
+
id: subscription.id,
|
|
109
|
+
entity: subscription.entity,
|
|
110
|
+
plan_id: subscription.plan_id,
|
|
111
|
+
status: subscription.status,
|
|
112
|
+
current_start: subscription.current_start,
|
|
113
|
+
current_end: subscription.current_end,
|
|
114
|
+
ended_at: subscription.ended_at,
|
|
115
|
+
quantity: subscription.quantity,
|
|
116
|
+
notes: subscription.notes,
|
|
117
|
+
charge_at: subscription.charge_at,
|
|
118
|
+
start_at: subscription.start_at,
|
|
119
|
+
end_at: subscription.end_at,
|
|
120
|
+
auth_attempts: subscription.auth_attempts,
|
|
121
|
+
total_count: subscription.total_count,
|
|
122
|
+
paid_count: subscription.paid_count,
|
|
123
|
+
customer_notify: subscription.customer_notify,
|
|
124
|
+
created_at: subscription.created_at,
|
|
125
|
+
expire_by: subscription.expire_by,
|
|
126
|
+
short_url: subscription.short_url,
|
|
127
|
+
has_scheduled_changes: subscription.has_scheduled_changes,
|
|
128
|
+
change_scheduled_at: subscription.change_scheduled_at,
|
|
129
|
+
source: subscription.source,
|
|
130
|
+
offer_id: subscription.offer_id,
|
|
131
|
+
remaining_count: subscription.remaining_count,
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return handleRazorpayError(error)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
)
|
|
@@ -0,0 +1,150 @@
|
|
|
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
|
+
)
|