@windrun-huaiin/backend-core 10.0.1

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.
Files changed (198) hide show
  1. package/LICENSE +21 -0
  2. package/dist/app/api/stripe/checkout/route.d.ts +19 -0
  3. package/dist/app/api/stripe/checkout/route.d.ts.map +1 -0
  4. package/dist/app/api/stripe/checkout/route.js +120 -0
  5. package/dist/app/api/stripe/checkout/route.mjs +118 -0
  6. package/dist/app/api/stripe/customer-portal/route.d.ts +11 -0
  7. package/dist/app/api/stripe/customer-portal/route.d.ts.map +1 -0
  8. package/dist/app/api/stripe/customer-portal/route.js +73 -0
  9. package/dist/app/api/stripe/customer-portal/route.mjs +71 -0
  10. package/dist/app/api/user/anonymous/init/route.d.ts +7 -0
  11. package/dist/app/api/user/anonymous/init/route.d.ts.map +1 -0
  12. package/dist/app/api/user/anonymous/init/route.js +210 -0
  13. package/dist/app/api/user/anonymous/init/route.mjs +208 -0
  14. package/dist/app/api/webhook/clerk/user/route.d.ts +7 -0
  15. package/dist/app/api/webhook/clerk/user/route.d.ts.map +1 -0
  16. package/dist/app/api/webhook/clerk/user/route.js +202 -0
  17. package/dist/app/api/webhook/clerk/user/route.mjs +200 -0
  18. package/dist/app/api/webhook/stripe/route.d.ts +8 -0
  19. package/dist/app/api/webhook/stripe/route.d.ts.map +1 -0
  20. package/dist/app/api/webhook/stripe/route.js +70 -0
  21. package/dist/app/api/webhook/stripe/route.mjs +67 -0
  22. package/dist/index.d.ts +7 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +83 -0
  25. package/dist/index.mjs +18 -0
  26. package/dist/lib/auth-utils.d.ts +46 -0
  27. package/dist/lib/auth-utils.d.ts.map +1 -0
  28. package/dist/lib/auth-utils.js +107 -0
  29. package/dist/lib/auth-utils.mjs +102 -0
  30. package/dist/lib/credit-init.d.ts +8 -0
  31. package/dist/lib/credit-init.d.ts.map +1 -0
  32. package/dist/lib/credit-init.js +16 -0
  33. package/dist/lib/credit-init.mjs +10 -0
  34. package/dist/lib/index.d.ts +5 -0
  35. package/dist/lib/index.d.ts.map +1 -0
  36. package/dist/lib/index.js +31 -0
  37. package/dist/lib/index.mjs +4 -0
  38. package/dist/lib/money-price-config.d.ts +51 -0
  39. package/dist/lib/money-price-config.d.ts.map +1 -0
  40. package/dist/lib/money-price-config.js +156 -0
  41. package/dist/lib/money-price-config.mjs +151 -0
  42. package/dist/lib/stripe-config.d.ts +31 -0
  43. package/dist/lib/stripe-config.d.ts.map +1 -0
  44. package/dist/lib/stripe-config.js +278 -0
  45. package/dist/lib/stripe-config.mjs +268 -0
  46. package/dist/node_modules/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.46.2_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.js +48 -0
  47. package/dist/node_modules/.pnpm/@rollup_plugin-typescript@12.1.4_rollup@4.46.2_tslib@2.8.1_typescript@5.9.3/node_modules/tslib/tslib.es6.mjs +45 -0
  48. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/errors.js +54 -0
  49. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/errors.mjs +51 -0
  50. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/iso.js +44 -0
  51. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/iso.mjs +35 -0
  52. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/parse.js +31 -0
  53. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/parse.mjs +18 -0
  54. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/schemas.js +587 -0
  55. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/schemas.mjs +527 -0
  56. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/api.js +447 -0
  57. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/api.mjs +399 -0
  58. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/checks.js +245 -0
  59. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/checks.mjs +232 -0
  60. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/core.js +68 -0
  61. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/core.mjs +62 -0
  62. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/doc.js +39 -0
  63. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/doc.mjs +37 -0
  64. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/errors.js +80 -0
  65. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/errors.mjs +75 -0
  66. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/parse.js +101 -0
  67. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/parse.mjs +86 -0
  68. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/regexes.js +102 -0
  69. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/regexes.mjs +76 -0
  70. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/registries.js +56 -0
  71. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/registries.mjs +52 -0
  72. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/schemas.js +1205 -0
  73. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/schemas.mjs +1157 -0
  74. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/util.js +407 -0
  75. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/util.mjs +374 -0
  76. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/versions.js +9 -0
  77. package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/versions.mjs +7 -0
  78. package/dist/prisma/client.d.ts +2 -0
  79. package/dist/prisma/client.d.ts.map +1 -0
  80. package/dist/prisma/client.js +12 -0
  81. package/dist/prisma/client.mjs +1 -0
  82. package/dist/prisma/index.d.ts +4 -0
  83. package/dist/prisma/index.d.ts.map +1 -0
  84. package/dist/prisma/index.js +10 -0
  85. package/dist/prisma/index.mjs +2 -0
  86. package/dist/prisma/prisma-transaction-util.d.ts +3 -0
  87. package/dist/prisma/prisma-transaction-util.d.ts.map +1 -0
  88. package/dist/prisma/prisma-transaction-util.js +29 -0
  89. package/dist/prisma/prisma-transaction-util.mjs +27 -0
  90. package/dist/prisma/prisma.d.ts +4 -0
  91. package/dist/prisma/prisma.d.ts.map +1 -0
  92. package/dist/prisma/prisma.js +109 -0
  93. package/dist/prisma/prisma.mjs +106 -0
  94. package/dist/services/aggregate/billing.aggregate.service.d.ts +83 -0
  95. package/dist/services/aggregate/billing.aggregate.service.d.ts.map +1 -0
  96. package/dist/services/aggregate/billing.aggregate.service.js +308 -0
  97. package/dist/services/aggregate/billing.aggregate.service.mjs +306 -0
  98. package/dist/services/aggregate/index.d.ts +3 -0
  99. package/dist/services/aggregate/index.d.ts.map +1 -0
  100. package/dist/services/aggregate/index.js +9 -0
  101. package/dist/services/aggregate/index.mjs +2 -0
  102. package/dist/services/aggregate/user.aggregate.service.d.ts +34 -0
  103. package/dist/services/aggregate/user.aggregate.service.d.ts.map +1 -0
  104. package/dist/services/aggregate/user.aggregate.service.js +136 -0
  105. package/dist/services/aggregate/user.aggregate.service.mjs +133 -0
  106. package/dist/services/context/index.d.ts +2 -0
  107. package/dist/services/context/index.d.ts.map +1 -0
  108. package/dist/services/context/index.js +13 -0
  109. package/dist/services/context/index.mjs +1 -0
  110. package/dist/services/context/user-context-service.d.ts +30 -0
  111. package/dist/services/context/user-context-service.d.ts.map +1 -0
  112. package/dist/services/context/user-context-service.js +170 -0
  113. package/dist/services/context/user-context-service.mjs +162 -0
  114. package/dist/services/database/apilog.service.d.ts +39 -0
  115. package/dist/services/database/apilog.service.d.ts.map +1 -0
  116. package/dist/services/database/apilog.service.js +174 -0
  117. package/dist/services/database/apilog.service.mjs +170 -0
  118. package/dist/services/database/constants.d.ts +73 -0
  119. package/dist/services/database/constants.d.ts.map +1 -0
  120. package/dist/services/database/constants.js +135 -0
  121. package/dist/services/database/constants.mjs +117 -0
  122. package/dist/services/database/credit.service.d.ts +107 -0
  123. package/dist/services/database/credit.service.d.ts.map +1 -0
  124. package/dist/services/database/credit.service.js +515 -0
  125. package/dist/services/database/credit.service.mjs +512 -0
  126. package/dist/services/database/creditAuditLog.service.d.ts +73 -0
  127. package/dist/services/database/creditAuditLog.service.d.ts.map +1 -0
  128. package/dist/services/database/creditAuditLog.service.js +305 -0
  129. package/dist/services/database/creditAuditLog.service.mjs +302 -0
  130. package/dist/services/database/index.d.ts +10 -0
  131. package/dist/services/database/index.d.ts.map +1 -0
  132. package/dist/services/database/index.js +38 -0
  133. package/dist/services/database/index.mjs +8 -0
  134. package/dist/services/database/prisma-model-type.d.ts +3 -0
  135. package/dist/services/database/prisma-model-type.d.ts.map +1 -0
  136. package/dist/services/database/subscription.service.d.ts +48 -0
  137. package/dist/services/database/subscription.service.d.ts.map +1 -0
  138. package/dist/services/database/subscription.service.js +267 -0
  139. package/dist/services/database/subscription.service.mjs +264 -0
  140. package/dist/services/database/transaction.service.d.ts +92 -0
  141. package/dist/services/database/transaction.service.d.ts.map +1 -0
  142. package/dist/services/database/transaction.service.js +326 -0
  143. package/dist/services/database/transaction.service.mjs +323 -0
  144. package/dist/services/database/user.service.d.ts +45 -0
  145. package/dist/services/database/user.service.d.ts.map +1 -0
  146. package/dist/services/database/user.service.js +180 -0
  147. package/dist/services/database/user.service.mjs +177 -0
  148. package/dist/services/database/userBackup.service.d.ts +45 -0
  149. package/dist/services/database/userBackup.service.d.ts.map +1 -0
  150. package/dist/services/database/userBackup.service.js +249 -0
  151. package/dist/services/database/userBackup.service.mjs +246 -0
  152. package/dist/services/stripe/index.d.ts +2 -0
  153. package/dist/services/stripe/index.d.ts.map +1 -0
  154. package/dist/services/stripe/index.js +7 -0
  155. package/dist/services/stripe/index.mjs +1 -0
  156. package/dist/services/stripe/webhook-handler.d.ts +6 -0
  157. package/dist/services/stripe/webhook-handler.d.ts.map +1 -0
  158. package/dist/services/stripe/webhook-handler.js +537 -0
  159. package/dist/services/stripe/webhook-handler.mjs +535 -0
  160. package/migrations/create.sql +176 -0
  161. package/migrations/db.init.sql +13 -0
  162. package/migrations/init-schema.sql +19 -0
  163. package/migrations/purge.sql +27 -0
  164. package/migrations/test-check.sql +167 -0
  165. package/package.json +123 -0
  166. package/prisma/schema.prisma +191 -0
  167. package/src/app/api/stripe/checkout/route.ts +145 -0
  168. package/src/app/api/stripe/customer-portal/route.ts +83 -0
  169. package/src/app/api/user/anonymous/init/route.ts +284 -0
  170. package/src/app/api/webhook/clerk/user/route.ts +249 -0
  171. package/src/app/api/webhook/stripe/route.ts +93 -0
  172. package/src/index.ts +6 -0
  173. package/src/lib/auth-utils.ts +101 -0
  174. package/src/lib/credit-init.ts +9 -0
  175. package/src/lib/index.ts +4 -0
  176. package/src/lib/money-price-config.ts +168 -0
  177. package/src/lib/stripe-config.ts +333 -0
  178. package/src/prisma/client.ts +2 -0
  179. package/src/prisma/index.ts +3 -0
  180. package/src/prisma/prisma-transaction-util.ts +24 -0
  181. package/src/prisma/prisma.ts +122 -0
  182. package/src/services/aggregate/billing.aggregate.service.ts +498 -0
  183. package/src/services/aggregate/index.ts +2 -0
  184. package/src/services/aggregate/user.aggregate.service.ts +168 -0
  185. package/src/services/context/index.ts +1 -0
  186. package/src/services/context/user-context-service.ts +200 -0
  187. package/src/services/database/apilog.service.ts +185 -0
  188. package/src/services/database/constants.ts +148 -0
  189. package/src/services/database/credit.service.ts +747 -0
  190. package/src/services/database/creditAuditLog.service.ts +402 -0
  191. package/src/services/database/index.ts +41 -0
  192. package/src/services/database/prisma-model-type.ts +13 -0
  193. package/src/services/database/subscription.service.ts +319 -0
  194. package/src/services/database/transaction.service.ts +447 -0
  195. package/src/services/database/user.service.ts +218 -0
  196. package/src/services/database/userBackup.service.ts +290 -0
  197. package/src/services/stripe/index.ts +1 -0
  198. package/src/services/stripe/webhook-handler.ts +648 -0
@@ -0,0 +1,648 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import { billingAggregateService } from '../aggregate/index';
4
+ import {
5
+ Apilogger,
6
+ BillingReason,
7
+ OrderStatus,
8
+ PaymentStatus,
9
+ subscriptionService,
10
+ transactionService,
11
+ TransactionType
12
+ } from '../database/index';
13
+ import { Transaction } from '../database/prisma-model-type';
14
+ import { oneTimeExpiredDays } from '../../lib/credit-init';
15
+ import { getCreditsFromPriceId } from '../../lib/money-price-config';
16
+ import { fetchPaymentId, stripe } from '../../lib/stripe-config';
17
+ import Stripe from 'stripe';
18
+ import { viewLocalTime } from '@windrun-huaiin/lib/utils';
19
+
20
+ const mapPaymentStatus = (
21
+ status?: Stripe.Checkout.Session.PaymentStatus | null
22
+ ): PaymentStatus => {
23
+ switch (status) {
24
+ case 'paid':
25
+ return PaymentStatus.PAID;
26
+ case 'no_payment_required':
27
+ return PaymentStatus.NO_PAYMENT_REQUIRED;
28
+ case 'unpaid':
29
+ default:
30
+ return PaymentStatus.UN_PAID;
31
+ }
32
+ };
33
+
34
+ const isPaymentSettled = (paymentStatus: PaymentStatus) =>
35
+ paymentStatus === PaymentStatus.PAID || paymentStatus === PaymentStatus.NO_PAYMENT_REQUIRED;
36
+
37
+ /**
38
+ * Main event handler - routes events to specific handlers
39
+ */
40
+ export async function handleStripeEvent(event: Stripe.Event) {
41
+ console.log(`Processing Stripe event: ${event.type}`);
42
+
43
+ try {
44
+ switch (event.type) {
45
+ // ===== Checkout Events =====
46
+ case 'checkout.session.completed':
47
+ return await handleCheckoutCompleted(event.data.object as Stripe.Checkout.Session);
48
+
49
+ case 'checkout.session.async_payment_succeeded':
50
+ return await handleAsyncPaymentSucceeded(event.data.object as Stripe.Checkout.Session);
51
+
52
+ case 'checkout.session.async_payment_failed':
53
+ return await handleAsyncPaymentFailed(event.data.object as Stripe.Checkout.Session);
54
+
55
+ // ===== Invoice Events (Subscription renewals) =====
56
+ case 'invoice.paid':
57
+ return await handleInvoicePaid(event.data.object as Stripe.Invoice);
58
+
59
+ case 'invoice.payment_failed':
60
+ return await handleInvoicePaymentFailed(event.data.object as Stripe.Invoice);
61
+
62
+ // ===== Subscription Events =====
63
+ case 'customer.subscription.created':
64
+ return await handleSubscriptionCreated(event.data.object as Stripe.Subscription);
65
+
66
+ case 'customer.subscription.updated':
67
+ return await handleSubscriptionUpdated(event.data.object as Stripe.Subscription);
68
+
69
+ case 'customer.subscription.deleted':
70
+ return await handleSubscriptionDeleted(event.data.object as Stripe.Subscription);
71
+
72
+ // ===== Payment Intent Events (One-time payments) =====
73
+ case 'payment_intent.succeeded':
74
+ console.log(`Payment Intent succeeded: ${(event.data.object as Stripe.PaymentIntent).id}`);
75
+ // Usually handled by checkout.session.completed
76
+ return;
77
+
78
+ case 'payment_intent.payment_failed':
79
+ console.log(`Payment Intent failed: ${(event.data.object as Stripe.PaymentIntent).id}`);
80
+ return;
81
+
82
+ // ===== Refund Events =====
83
+ case 'charge.refunded':
84
+ return await handleChargeRefunded(event.data.object as Stripe.Charge);
85
+
86
+ default:
87
+ console.log(`Unhandled event type: ${event.type}`);
88
+ }
89
+ } catch (error) {
90
+ console.error(`Error processing event ${event.type}:`, error);
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ async function handleCheckoutCompleted(session: Stripe.Checkout.Session) {
96
+ console.log(`Checkout session completed: ${session.id}`);
97
+
98
+ // 1. Get transaction by session ID or order ID
99
+ const orderId = session.metadata?.order_id;
100
+ if (!orderId) {
101
+ throw new Error('Missing order_id in session metadata');
102
+ }
103
+
104
+ const transaction = await transactionService.findByOrderId(orderId);
105
+ if (!transaction) {
106
+ throw new Error(`Transaction not found: ${orderId}`);
107
+ }
108
+
109
+ if (transaction.orderStatus === OrderStatus.SUCCESS) {
110
+ throw new Error(`Transaction already processed successfully: ${transaction.orderId}, skipping.`);
111
+ }
112
+
113
+ // Stripe docs: checkout.session.completed fires even when payment is pending for async methods
114
+ // https://stripe.com/docs/payments/checkout/one-time#webhooks
115
+ const paymentStatus = mapPaymentStatus(session.payment_status);
116
+
117
+ if (!isPaymentSettled(paymentStatus)) {
118
+ console.log( `Checkout session ${session.id} payment incomplete (status=${session.payment_status}), awaiting async confirmation.`
119
+ );
120
+
121
+ if (
122
+ transaction.orderStatus === OrderStatus.CREATED ||
123
+ transaction.orderStatus === OrderStatus.PENDING_UNPAID
124
+ ) {
125
+ await transactionService.updateStatus(orderId, OrderStatus.PENDING_UNPAID, {
126
+ payUpdatedAt: new Date(),
127
+ paymentStatus,
128
+ });
129
+ }
130
+ return;
131
+ }
132
+
133
+ // 2. Route based on transaction type
134
+ if (transaction.type === TransactionType.SUBSCRIPTION) {
135
+ // For subscriptions, store session info and wait for invoice.paid
136
+ return await handleSubscriptionCheckoutInit(session, transaction);
137
+ }
138
+ return await handleOneTimeCheckout(session, transaction, paymentStatus);
139
+ }
140
+
141
+ async function handleSubscriptionCheckoutInit(
142
+ session: Stripe.Checkout.Session,
143
+ transaction: Transaction,
144
+ ) {
145
+ console.log(`Processing subscription checkout: ${session.id}`);
146
+
147
+ // 1. Get subscription ID from session
148
+ if (!session.subscription) {
149
+ throw new Error('No subscription ID in checkout session');
150
+ }
151
+
152
+ const subscriptionId = session.subscription as string;
153
+
154
+ // ===== STEP 1: FETCH EXTERNAL API DATA (BEFORE TRANSACTION) =====
155
+ // 2. Get COMPLETE Stripe subscription details including billing period
156
+ const stripeSubscription = await stripe.subscriptions.retrieve(subscriptionId);
157
+
158
+ // Extract billing period from subscription items (NOT from top-level subscription object)
159
+ // The current_period_start/end are on SubscriptionItem, not on Subscription
160
+ const subscriptionItem = stripeSubscription.items?.data?.[0];
161
+ if (!subscriptionItem) {
162
+ throw new Error( `No subscription items found for subscription ${subscriptionId}` );
163
+ }
164
+
165
+ const currentPeriodStart = subscriptionItem.current_period_start;
166
+ const currentPeriodEnd = subscriptionItem.current_period_end;
167
+
168
+ if (!currentPeriodStart || !currentPeriodEnd) {
169
+ throw new Error( `Invalid subscription period from Stripe API: start=${currentPeriodStart}, end=${currentPeriodEnd}` );
170
+ }
171
+
172
+ const subPeriodStart = new Date(currentPeriodStart * 1000);
173
+ const subPeriodEnd = new Date(currentPeriodEnd * 1000);
174
+
175
+ // Log the Stripe API response with correct data structure
176
+ const logId = await Apilogger.logStripeOutgoing(
177
+ 'stripe.subscriptions.retrieve',
178
+ { subscriptionId },
179
+ {
180
+ id: stripeSubscription.id,
181
+ status: stripeSubscription.status,
182
+ subPeriodStart: subPeriodStart,
183
+ subPeriodEnd: subPeriodEnd,
184
+ subscriptionItemCount: stripeSubscription.items?.data?.length || 0,
185
+ }
186
+ );
187
+ Apilogger.updateResponse(logId, stripeSubscription);
188
+
189
+ console.log('Subscription checkout completed, just log:', {
190
+ id: subscriptionId,
191
+ orderId: transaction.orderId,
192
+ status: stripeSubscription.status,
193
+ periodStart: viewLocalTime(subPeriodStart),
194
+ periodEnd: viewLocalTime(subPeriodEnd),
195
+ });
196
+ }
197
+
198
+ async function handleOneTimeCheckout(
199
+ session: Stripe.Checkout.Session,
200
+ transaction: Transaction,
201
+ paymentStatus: PaymentStatus
202
+ ) {
203
+ console.log(`Processing one-time payment checkout: ${session.id}`);
204
+ // 1. Calculate one-time credit expiration
205
+ const now = new Date();
206
+ const oneTimePaidStart = now;
207
+ const oneTimePaidEnd = new Date(now);
208
+ oneTimePaidEnd.setDate(oneTimePaidEnd.getDate() + oneTimeExpiredDays);
209
+ oneTimePaidEnd.setHours(23, 59, 59, 999);
210
+
211
+ await billingAggregateService.completeOneTimeCheckout(
212
+ {
213
+ userId: transaction.userId,
214
+ orderId: transaction.orderId,
215
+ creditsGranted: transaction.creditsGranted || 0,
216
+ paymentStatus,
217
+ payTransactionId: session.payment_intent as string,
218
+ paidEmail: session.customer_details?.email,
219
+ paidAt: oneTimePaidStart,
220
+ oneTimePaidStart,
221
+ oneTimePaidEnd,
222
+ }
223
+ );
224
+
225
+ console.log(`One-time payment completed: ${transaction.orderId}`);
226
+ }
227
+
228
+ async function handleInvoicePaid(invoice: Stripe.Invoice) {
229
+ console.log(`Invoice paid: ${invoice.id}`);
230
+
231
+ // ===== STEP 1: EXTRACT AND VALIDATE DATA FROM INVOICE (BEFORE TRANSACTION) =====
232
+ // 1. Get subscription details from invoice parent
233
+ const parentDetails = (invoice as any).parent?.subscription_details;
234
+ if (!parentDetails?.subscription) {
235
+ throw new Error('Invoice not associated with subscription, skipping');
236
+ }
237
+
238
+ // 2. Check billing reason to determine payment type
239
+ const isInitialPayment = invoice.billing_reason === BillingReason.SUBSCRIPTION_CREATE;
240
+ const isRenewal = invoice.billing_reason === BillingReason.SUBSCRIPTION_CYCLE;
241
+
242
+ // Only handle initial payments and renewals
243
+ if (!isInitialPayment && !isRenewal) {
244
+ throw new Error(`Unhandled invoice billing_reason: ${invoice.billing_reason}, skipping`);
245
+ }
246
+
247
+ // 3. Extract subscription period from invoice line items
248
+ const lineItem = invoice.lines?.data?.[0];
249
+ if (!lineItem) {
250
+ throw new Error(`No line items found in invoice ${invoice.id}`);
251
+ }
252
+
253
+ const periodStart = (lineItem as any).period?.start;
254
+ const periodEnd = (lineItem as any).period?.end;
255
+ if (!periodStart || !periodEnd) {
256
+ throw new Error( `Invalid period in invoice line: start=${periodStart}, end=${periodEnd}. Invoice ID: ${invoice.id}`
257
+ );
258
+ }
259
+ const subPeriodStart = new Date(periodStart * 1000);
260
+ const subPeriodEnd = new Date(periodEnd * 1000);
261
+
262
+ const subscriptionMetadata = parentDetails.metadata || {};
263
+ const orderId = subscriptionMetadata.order_id;
264
+ if (!orderId) {
265
+ throw new Error( `No order_id in subscription metadata for initial invoice ${invoice.id}. ` + `Skipping invoice URL update.` );
266
+ }
267
+ const transaction = await transactionService.findByOrderId(orderId);
268
+ if (!transaction) {
269
+ throw new Error(`Transaction not found for order_id: ${orderId}`);
270
+ }
271
+
272
+ const subscriptionId = parentDetails.subscription;
273
+
274
+ const userId = transaction.userId;
275
+ const paymentIntentId = await fetchPaymentId(invoice.id)
276
+
277
+ const invoicePaidAt = invoice.status_transitions?.paid_at;
278
+ const paidAt = invoicePaidAt ? new Date(invoicePaidAt * 1000) : new Date();
279
+ const paidEmail = invoice.customer_email;
280
+
281
+ console.log('Invoice paid event key-info:', {
282
+ invoiceId: invoice.id,
283
+ subscriptionId,
284
+ paymentIntentId,
285
+ billingReason: invoice.billing_reason,
286
+ isInitialPayment,
287
+ paidEmail,
288
+ paidAt: viewLocalTime(paidAt),
289
+ periodStart: viewLocalTime(subPeriodStart),
290
+ periodEnd: viewLocalTime(subPeriodEnd),
291
+ });
292
+
293
+ if (isInitialPayment) {
294
+ // 首次订阅校验
295
+ const nonActiveSubscription = await subscriptionService.getNonActiveSubscription(userId);
296
+ if (!nonActiveSubscription) {
297
+ throw new Error(`Subscription status is ACTIVE for user ${userId}, forbidden to re-active!`);
298
+ }
299
+
300
+ await billingAggregateService.recordSubscriptionInitPayment(
301
+ {
302
+ userId,
303
+ subIdKey: nonActiveSubscription.id,
304
+ orderId,
305
+ paySubscriptionId: subscriptionId,
306
+ creditsGranted: transaction.creditsGranted || 0,
307
+ priceId: transaction.priceId,
308
+ priceName: transaction.priceName,
309
+ periodStart: subPeriodStart,
310
+ periodEnd: subPeriodEnd,
311
+ invoiceId: invoice.id,
312
+ hostedInvoiceUrl: invoice.hosted_invoice_url,
313
+ invoicePdf: invoice.invoice_pdf,
314
+ billingReason: invoice.billing_reason,
315
+ paymentIntentId,
316
+ paidAt,
317
+ paidEmail
318
+ }
319
+ );
320
+
321
+ console.log(`Initial invoice recorded for transaction: ${orderId}`);
322
+ return;
323
+ }
324
+
325
+ if (isRenewal) {
326
+ // 续订时,一定要查到订阅记录
327
+ const subscription = await subscriptionService.findByPaySubscriptionId(subscriptionId);
328
+ if (!subscription) {
329
+ throw new Error(`Subscription not found for renewal: ${subscriptionId}`);
330
+ }
331
+
332
+ const renewalOrderId = `order_renew_${invoice.id}`;
333
+ const existingOrder = await transactionService.findByOrderId(renewalOrderId);
334
+ if (existingOrder) {
335
+ throw new Error(`Renewal invoice ${invoice.id} already processed as ${existingOrder.orderId}, skipping.`);
336
+ }
337
+
338
+ // Get credits from current price configuration (handles plan upgrades/downgrades)
339
+ // 优先从配置中取,取不到就以上个周期的为准作为Fallback,后续有问题再人工补偿,优先保证能用
340
+ // 只要配置正确,这里就不会出错!
341
+ const creditsForRenewal = subscription.priceId
342
+ ? getCreditsFromPriceId(subscription.priceId)
343
+ : subscription.creditsAllocated;
344
+
345
+ const renewalCredits = creditsForRenewal || subscription.creditsAllocated;
346
+
347
+ await billingAggregateService.recordSubscriptionRenewalPayment(
348
+ {
349
+ userId,
350
+ subIdKey: subscription.id,
351
+ orderId: renewalOrderId,
352
+ paySubscriptionId: subscriptionId,
353
+ creditsGranted: renewalCredits,
354
+ priceId: subscription.priceId,
355
+ priceName: subscription.priceName,
356
+ periodStart: subPeriodStart,
357
+ periodEnd: subPeriodEnd,
358
+ invoiceId: invoice.id,
359
+ hostedInvoiceUrl: invoice.hosted_invoice_url,
360
+ invoicePdf: invoice.invoice_pdf,
361
+ billingReason: invoice.billing_reason,
362
+ paymentIntentId,
363
+ paidAt: paidAt,
364
+ paidEmail,
365
+ amountPaidCents: invoice.amount_paid,
366
+ currency: invoice.currency,
367
+ }
368
+ );
369
+
370
+ console.log(`Invoice renewal paid event completed, and invoiceId: ${invoice.id}, subscriptionId: ${subscription.id}, orderId: ${renewalOrderId}`);
371
+ return;
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Handle customer.subscription.deleted
377
+ */
378
+ async function handleSubscriptionDeleted(stripeSubscription: Stripe.Subscription) {
379
+ const subscriptionId = stripeSubscription.id;
380
+ console.log(`Subscription deleted: ${subscriptionId}`);
381
+
382
+ const userCanceledAt = stripeSubscription.canceled_at;
383
+ if (!userCanceledAt) {
384
+ throw new Error( `Invalid period in invoice line: canceldAt=${userCanceledAt}, subscriptionId=${subscriptionId}` );
385
+ }
386
+
387
+ const subscription = await subscriptionService.findByPaySubscriptionId(subscriptionId);
388
+ if (!subscription) {
389
+ throw new Error(`Subscription id invalid: ${subscriptionId}`);
390
+ }
391
+
392
+ const orderId = subscription.orderId;
393
+ if (!orderId) {
394
+ throw new Error(`Subscription's orderId is NULL: ${subscriptionId}`);
395
+ }
396
+
397
+ const transaction = await transactionService.findByOrderId(orderId);
398
+ if (!transaction) {
399
+ throw new Error(`Subscription's orderId is illegal: subscriptionId=${subscriptionId}, orderId=${orderId}`);
400
+ }
401
+
402
+ const canceledAt = new Date(userCanceledAt * 1000);
403
+ const cancellationDetail = stripeSubscription.cancellation_details ? JSON.stringify(stripeSubscription.cancellation_details) : undefined;
404
+ await billingAggregateService.processSubscriptionCancel(
405
+ {
406
+ userId: subscription.userId,
407
+ subIdKey: subscription.id,
408
+ orderId,
409
+ canceledAt,
410
+ cancellationDetail
411
+ }
412
+ );
413
+
414
+ console.log(`Subscription status updated to canceled: ${subscription.id}`);
415
+ }
416
+
417
+ async function handleAsyncPaymentSucceeded(session: Stripe.Checkout.Session) {
418
+ console.log(`Async payment succeeded: ${session.id}`);
419
+
420
+ // Retrieve the latest session state to ensure payment_status is up to date
421
+ const latestSession = await stripe.checkout.sessions.retrieve(session.id);
422
+
423
+ return await handleCheckoutCompleted(latestSession);
424
+ }
425
+
426
+ async function handleAsyncPaymentFailed(session: Stripe.Checkout.Session) {
427
+ console.log(`Async payment failed: ${session.id}`);
428
+
429
+ const orderId = session.metadata?.order_id;
430
+ if (!orderId) {
431
+ throw new Error(`Transaction orderId is NULL for async payment failure`);
432
+ }
433
+
434
+ const transaction = await transactionService.findByOrderId(orderId);
435
+ if (!transaction) {
436
+ throw new Error(`Transaction not found for async payment failure, orderId=${orderId}`);
437
+ }
438
+
439
+ if (transaction.orderStatus === OrderStatus.SUCCESS) {
440
+ throw new Error( `Received async payment failed for already successful order ${orderId}, skipping.` );
441
+ }
442
+
443
+ await transactionService.updateStatus(orderId, OrderStatus.FAILED, {
444
+ payUpdatedAt: new Date(),
445
+ paymentStatus: PaymentStatus.UN_PAID,
446
+ });
447
+ }
448
+
449
+ async function handleInvoicePaymentFailed(invoice: Stripe.Invoice) {
450
+ console.log(`Invoice payment-failed event: ${invoice.id}`);
451
+
452
+ const parentDetails = (invoice as any).parent?.subscription_details;
453
+ if (!parentDetails?.subscription) {
454
+ throw new Error('Invoice not associated with subscription, skipping');
455
+ }
456
+
457
+ const isInitialPayment = invoice.billing_reason === BillingReason.SUBSCRIPTION_CREATE;
458
+ const isRenewal = invoice.billing_reason === BillingReason.SUBSCRIPTION_CYCLE;
459
+
460
+ // Only handle initial payments and renewals
461
+ if (!isInitialPayment && !isRenewal) {
462
+ throw new Error(`Unhandled invoice billing_reason: ${invoice.billing_reason}, skipping`);
463
+ }
464
+
465
+ const subscriptionId = parentDetails.subscription;
466
+ const subscriptionMetadata = parentDetails.metadata || {};
467
+
468
+ // 支付ID
469
+ const paymentIntentId = await fetchPaymentId(invoice.id)
470
+
471
+ console.log('Invoice payment failed event key-info:', {
472
+ invoiceId: invoice.id,
473
+ subscriptionId,
474
+ paymentIntentId,
475
+ billingReason: invoice.billing_reason,
476
+ isInitialPayment
477
+ });
478
+
479
+ const paidEmail = invoice.customer_email;
480
+
481
+ const orderId = subscriptionMetadata.order_id;
482
+ if (!orderId) {
483
+ throw new Error( `No order_id in subscription metadata for failed initial invoice ${invoice.id}. ` + `Skipping payment failure update.` );
484
+ }
485
+
486
+ const transaction = await transactionService.findByOrderId(orderId);
487
+ if (!transaction) {
488
+ throw new Error(`Transaction not found for order_id: ${orderId}`);
489
+ }
490
+
491
+ // ===== CASE 1: Initial subscription payment failed =====
492
+ if (isInitialPayment) {
493
+ await billingAggregateService.recordInitialPaymentFailure(
494
+ {
495
+ orderId: transaction.orderId,
496
+ invoiceId: invoice.id,
497
+ paymentIntentId: paymentIntentId,
498
+ detail: 'Initial subscription payment failed',
499
+ }
500
+ );
501
+ console.log(`Initial subscription payment-failed event updated for order: ${orderId}`);
502
+ return;
503
+ }
504
+
505
+ // ===== CASE 2: Subscription renewal payment failed =====
506
+ if (isRenewal) {
507
+ // For renewals, we need the subscription to get user info
508
+ const subscription = await subscriptionService.findByPaySubscriptionId(subscriptionId);
509
+ if (!subscription) {
510
+ throw new Error(`Subscription not found for renewal payment-failed event, and invoice ${invoice.id}`);
511
+ }
512
+
513
+ const failedOrderId = `order_renew_failed_${invoice.id}`;
514
+ const existingFailureOrder = await transactionService.findByOrderId(failedOrderId);
515
+ if (existingFailureOrder) {
516
+ throw new Error(`Renewal payment-failure event for invoice ${invoice.id} already recorded as ${failedOrderId}, skipping.`);
517
+ }
518
+
519
+ await billingAggregateService.recordRenewalPaymentFailure(
520
+ {
521
+ userId: subscription.userId,
522
+ subIdKey: subscription.id,
523
+ orderId: failedOrderId,
524
+ paySubscriptionId: subscriptionId,
525
+ creditsGranted: 0,
526
+ priceId: subscription.priceId,
527
+ priceName: subscription.priceName,
528
+ periodStart: null,
529
+ periodEnd: null,
530
+ invoiceId: invoice.id,
531
+ billingReason: invoice.billing_reason,
532
+ paymentIntentId,
533
+ amountPaidCents: invoice.amount_due,
534
+ currency: invoice.currency,
535
+ paidAt: null,
536
+ paidEmail
537
+ }
538
+ );
539
+
540
+ console.log(`Invoice renewal payment-failed event completed, and invoiceId: ${invoice.id}, recorded: ${subscription.id}, orderId: ${failedOrderId}`);
541
+ return;
542
+ }
543
+
544
+ }
545
+
546
+ async function handleSubscriptionCreated(stripeSubscription: Stripe.Subscription) {
547
+ console.log(`Subscription created: ${stripeSubscription.id}`);
548
+ // Usually handled by checkout.session.completed
549
+ }
550
+
551
+ /**
552
+ * Handle subscription updated TODO
553
+ */
554
+ async function handleSubscriptionUpdated(stripeSubscription: Stripe.Subscription) {
555
+ console.log(`Subscription updated: ${stripeSubscription.id}`);
556
+ const orderId = stripeSubscription.metadata?.order_id
557
+ if (!orderId) {
558
+ throw new Error('Missing order_id in session metadata');
559
+ }
560
+
561
+ // Extract period timestamps from subscription items (NOT from top-level subscription object)
562
+ const subscriptionItem = stripeSubscription.items?.data?.[0];
563
+
564
+ if (!subscriptionItem) {
565
+ throw new Error(`No subscription items found for ${stripeSubscription.id}, reject!`);
566
+ }
567
+
568
+ const subscription = await subscriptionService.findByPaySubscriptionId(stripeSubscription.id);
569
+ if (!subscription) {
570
+ throw new Error(`Subscription not found in DB: ${stripeSubscription.id}`);
571
+ }
572
+
573
+ const isUserCancel = stripeSubscription.cancellation_details?.reason === 'cancellation_requested'
574
+
575
+ // Use period from subscription item if available
576
+ const currentPeriodStart = subscriptionItem.current_period_start;
577
+ const currentPeriodEnd = subscriptionItem.current_period_end;
578
+
579
+ await billingAggregateService.syncSubscriptionFromStripe(
580
+ {
581
+ subscription,
582
+ status: stripeSubscription.status,
583
+ periodStart: new Date(currentPeriodStart * 1000),
584
+ periodEnd: new Date(currentPeriodEnd * 1000),
585
+ orderId,
586
+ isUserCancel
587
+ }
588
+ );
589
+
590
+ console.log(`Subscription updated in DB: ${subscription.id}`);
591
+ }
592
+
593
+
594
+ async function handleChargeRefunded(charge: Stripe.Charge) {
595
+ console.log(`Charge refunded: ${charge.id}`);
596
+
597
+ // Find transaction by payment intent
598
+ const paymentIntentId = typeof charge.payment_intent === 'string'
599
+ ? charge.payment_intent
600
+ : charge.payment_intent?.id;
601
+
602
+ if (!paymentIntentId) {
603
+ throw new Error("PaymentId is illegal NULL");
604
+ };
605
+
606
+ const transaction = await transactionService.findByPayTransactionId(paymentIntentId);
607
+ if (!transaction) {
608
+ throw new Error(`Transaction not found for paymentId: ${paymentIntentId}`);
609
+ };
610
+
611
+ if (transaction.orderStatus === OrderStatus.REFUNDED) {
612
+ throw new Error(`Transaction already marked refunded: ${transaction.orderId}, skipping.`);
613
+ }
614
+
615
+ if (transaction.type === TransactionType.SUBSCRIPTION) {
616
+ const subscription = transaction.paySubscriptionId
617
+ ? await subscriptionService.findByPaySubscriptionId(transaction.paySubscriptionId)
618
+ : null;
619
+
620
+ await billingAggregateService.processSubscriptionRefund(
621
+ {
622
+ transaction,
623
+ subscription,
624
+ }
625
+ );
626
+
627
+ console.log(`Subscription refund processed for transaction: ${transaction.orderId}`);
628
+ return;
629
+ }
630
+
631
+ if (transaction.type === TransactionType.ONE_TIME) {
632
+ await billingAggregateService.processOneTimeRefund({ transaction });
633
+
634
+ console.log(`One-time refund processed for transaction: ${transaction.orderId}`);
635
+ return;
636
+ }
637
+ // for other type, not available
638
+ await transactionService.update(
639
+ transaction.orderId,
640
+ {
641
+ orderStatus: OrderStatus.REFUNDED,
642
+ paymentStatus: PaymentStatus.UN_PAID,
643
+ payUpdatedAt: new Date(),
644
+ }
645
+ );
646
+
647
+ console.log(`Refund processed for transaction without credit adjustments: ${transaction.orderId}`);
648
+ }