@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,498 @@
1
+ import {
2
+ creditService,
3
+ CreditType,
4
+ OperationType,
5
+ OrderStatus,
6
+ PaymentStatus,
7
+ PaySupplier,
8
+ subscriptionService,
9
+ SubscriptionStatus,
10
+ transactionService,
11
+ TransactionType,
12
+ } from '../database/index';
13
+ import type { Subscription, Transaction } from '../database/prisma-model-type';
14
+ import { runInTransaction } from '../../prisma/prisma-transaction-util';
15
+
16
+ type NullableString = string | null | undefined;
17
+
18
+ type BasicOrderContext = {
19
+ userId: string;
20
+ subIdKey: bigint;
21
+ orderId: string;
22
+ paySubscriptionId: string,
23
+ creditsGranted: number;
24
+ priceId?: NullableString;
25
+ priceName?: NullableString;
26
+ periodStart: Date | null;
27
+ periodEnd: Date | null;
28
+ invoiceId: string;
29
+ hostedInvoiceUrl?: NullableString;
30
+ invoicePdf?: NullableString;
31
+ billingReason?: NullableString;
32
+ paymentIntentId?: string;
33
+ paidAt: Date | null;
34
+ paidEmail: string | null;
35
+ };
36
+
37
+ type RenewalOrderContext = BasicOrderContext & {
38
+ // 续订时以Stripe的发票价格为准
39
+ amountPaidCents: number;
40
+ currency: string;
41
+ }
42
+
43
+ type SubscriptionCancelContext = {
44
+ userId: string;
45
+ subIdKey: bigint;
46
+ orderId: string;
47
+ canceledAt: Date;
48
+ cancellationDetail?: string
49
+ };
50
+
51
+ type SubscriptionRefundContext = {
52
+ transaction: Transaction;
53
+ subscription?: Subscription | null;
54
+ };
55
+
56
+ type OneTimeRefundContext = {
57
+ transaction: Transaction;
58
+ };
59
+
60
+ class BillingAggregateService {
61
+
62
+ async recordSubscriptionInitPayment(
63
+ context: BasicOrderContext
64
+ ): Promise<void> {
65
+ await runInTransaction(async (tx) => {
66
+ const now = new Date();
67
+ // 订阅截止时间统一为到期日最后1s
68
+ const originSubPeriodEnd = context.periodEnd;
69
+ const specialEnd = originSubPeriodEnd ? new Date(originSubPeriodEnd.setHours(23, 59, 59, 999)) : originSubPeriodEnd;
70
+ const updatedSubscription = await subscriptionService.updateSubscription(
71
+ context.subIdKey,
72
+ {
73
+ status: SubscriptionStatus.ACTIVE,
74
+ orderId: context.orderId ?? undefined,
75
+ paySubscriptionId: context.paySubscriptionId,
76
+ priceId: context.priceId ?? undefined,
77
+ priceName: context.priceName ?? undefined,
78
+ creditsAllocated: context.creditsGranted || 0,
79
+ subPeriodStart: context.periodStart,
80
+ subPeriodEnd: specialEnd,
81
+ updatedAt: now,
82
+ },
83
+ tx
84
+ );
85
+
86
+
87
+ await transactionService.update(
88
+ context.orderId,
89
+ {
90
+ orderStatus: OrderStatus.SUCCESS,
91
+ paymentStatus: PaymentStatus.PAID,
92
+ paySubscriptionId: context.paySubscriptionId,
93
+ subPeriodStart: context.periodStart,
94
+ subPeriodEnd: context.periodEnd,
95
+ payInvoiceId: context.invoiceId,
96
+ hostedInvoiceUrl: context.hostedInvoiceUrl ?? undefined,
97
+ invoicePdf: context.invoicePdf ?? undefined,
98
+ billingReason: context.billingReason ?? undefined,
99
+ payTransactionId: context.paymentIntentId ?? undefined,
100
+ paidEmail: context.paidEmail ?? undefined,
101
+ paidAt: context.paidAt,
102
+ payUpdatedAt: now,
103
+ },
104
+ tx
105
+ );
106
+
107
+ if (context.creditsGranted > 0) {
108
+ await creditService.rechargeCredit(
109
+ context.userId,
110
+ { paid: context.creditsGranted },
111
+ {
112
+ feature: TransactionType.SUBSCRIPTION,
113
+ operationReferId: context.orderId,
114
+ },
115
+ tx
116
+ );
117
+ }
118
+
119
+ await tx.credit.update({
120
+ where: { userId: context.userId },
121
+ data: {
122
+ paidStart: context.periodStart,
123
+ paidEnd: context.periodEnd,
124
+ },
125
+ });
126
+
127
+ return updatedSubscription;
128
+ })
129
+ }
130
+
131
+
132
+ async completeOneTimeCheckout(
133
+ params: {
134
+ userId: string;
135
+ orderId: string;
136
+ creditsGranted: number;
137
+ paymentStatus: PaymentStatus;
138
+ payTransactionId: string;
139
+ paidAt: Date;
140
+ paidEmail?: NullableString;
141
+ oneTimePaidStart: Date;
142
+ oneTimePaidEnd: Date;
143
+ }
144
+ ): Promise<void> {
145
+ await runInTransaction(async (tx) => {
146
+ const now = new Date();
147
+
148
+ await transactionService.update(
149
+ params.orderId,
150
+ {
151
+ orderStatus: OrderStatus.SUCCESS,
152
+ paymentStatus: params.paymentStatus,
153
+ payTransactionId: params.payTransactionId,
154
+ paidAt: now,
155
+ paidEmail: params.paidEmail ?? undefined,
156
+ payUpdatedAt: now,
157
+ },
158
+ tx
159
+ );
160
+
161
+ if (params.creditsGranted > 0) {
162
+ await creditService.rechargeCredit(
163
+ params.userId,
164
+ { oneTimePaid: params.creditsGranted },
165
+ {
166
+ feature: TransactionType.ONE_TIME,
167
+ operationReferId: params.orderId,
168
+ },
169
+ tx
170
+ );
171
+ }
172
+
173
+ await tx.credit.update({
174
+ where: { userId: params.userId },
175
+ data: {
176
+ oneTimePaidStart: params.oneTimePaidStart,
177
+ oneTimePaidEnd: params.oneTimePaidEnd,
178
+ },
179
+ });
180
+ });
181
+ }
182
+
183
+ async recordInitialInvoiceDetails(
184
+ params: {
185
+ orderId: string;
186
+ invoiceId: string;
187
+ paymentIntentId: string,
188
+ hostedInvoiceUrl?: NullableString;
189
+ invoicePdf?: NullableString;
190
+ billingReason?: NullableString;
191
+ }
192
+ ): Promise<void> {
193
+ await runInTransaction(async (tx) => {
194
+ await transactionService.update(
195
+ params.orderId,
196
+ {
197
+ payInvoiceId: params.invoiceId,
198
+ payTransactionId: params.paymentIntentId,
199
+ hostedInvoiceUrl: params.hostedInvoiceUrl ?? undefined,
200
+ invoicePdf: params.invoicePdf ?? undefined,
201
+ billingReason: params.billingReason ?? undefined,
202
+ payUpdatedAt: new Date(),
203
+ },
204
+ tx
205
+ );
206
+ });
207
+ }
208
+
209
+ async recordSubscriptionRenewalPayment(
210
+ context: RenewalOrderContext
211
+ ): Promise<void> {
212
+ await runInTransaction(async (tx) => {
213
+ await transactionService.createTransaction(
214
+ {
215
+ userId: context.userId,
216
+ orderId: context.orderId,
217
+ orderStatus: OrderStatus.SUCCESS,
218
+ paymentStatus: PaymentStatus.PAID,
219
+ paySupplier: PaySupplier.STRIPE,
220
+ paySubscriptionId: context.paySubscriptionId ?? undefined,
221
+ subPeriodStart: context.periodStart ?? undefined,
222
+ subPeriodEnd: context.periodEnd ?? undefined,
223
+ payInvoiceId: context.invoiceId,
224
+ hostedInvoiceUrl: context.hostedInvoiceUrl ?? undefined,
225
+ invoicePdf: context.invoicePdf ?? undefined,
226
+ billingReason: context.billingReason ?? undefined,
227
+ payTransactionId: context.paymentIntentId ?? undefined,
228
+ priceId: context.priceId ?? undefined,
229
+ priceName: context.priceName ?? undefined,
230
+ type: TransactionType.SUBSCRIPTION,
231
+ amount: context.amountPaidCents / 100,
232
+ currency: context.currency.toUpperCase(),
233
+ creditsGranted: context.creditsGranted,
234
+ paidAt: context.paidAt ?? undefined,
235
+ paidEmail: context.paidEmail,
236
+ payUpdatedAt: new Date(),
237
+ },
238
+ tx
239
+ );
240
+ // 订阅截止时间统一为到期日最后1s
241
+ const originSubPeriodEnd = context.periodEnd;
242
+ const specialEnd = originSubPeriodEnd ? new Date(originSubPeriodEnd.setHours(23, 59, 59, 999)) : originSubPeriodEnd;
243
+
244
+ await subscriptionService.updateSubscription(
245
+ context.subIdKey,
246
+ {
247
+ status: SubscriptionStatus.ACTIVE,
248
+ orderId: context.orderId,
249
+ subPeriodStart: context.periodStart,
250
+ subPeriodEnd: specialEnd,
251
+ updatedAt: new Date(),
252
+ },
253
+ tx
254
+ );
255
+
256
+ if (context.creditsGranted > 0) {
257
+ await creditService.rechargeCredit(
258
+ context.userId,
259
+ { paid: context.creditsGranted },
260
+ {
261
+ feature: `${TransactionType.SUBSCRIPTION}_renewal`,
262
+ operationReferId: context.orderId,
263
+ },
264
+ tx
265
+ );
266
+ }
267
+
268
+ await tx.credit.update({
269
+ where: { userId: context.userId },
270
+ data: {
271
+ paidStart: context.periodStart,
272
+ paidEnd: context.periodEnd,
273
+ },
274
+ });
275
+ });
276
+ }
277
+
278
+ async recordInitialPaymentFailure(
279
+ params: {
280
+ orderId: string;
281
+ invoiceId: string;
282
+ paymentIntentId: string;
283
+ detail?: string;
284
+ }
285
+ ): Promise<void> {
286
+ await runInTransaction(async (tx) => {
287
+ await transactionService.updateStatus(
288
+ params.orderId,
289
+ OrderStatus.FAILED,
290
+ {
291
+ paymentStatus: PaymentStatus.UN_PAID,
292
+ payInvoiceId: params.invoiceId,
293
+ payTransactionId: params.paymentIntentId,
294
+ payUpdatedAt: new Date(),
295
+ paidDetail: params.detail ?? undefined,
296
+ orderDetail: params.detail ?? undefined,
297
+ },
298
+ tx
299
+ );
300
+ });
301
+ }
302
+
303
+ async recordRenewalPaymentFailure(
304
+ context: RenewalOrderContext
305
+ ): Promise<void> {
306
+ await runInTransaction(async (tx) => {
307
+ await transactionService.createTransaction(
308
+ {
309
+ userId: context.userId,
310
+ orderId: context.orderId,
311
+ orderStatus: OrderStatus.FAILED,
312
+ paymentStatus: PaymentStatus.UN_PAID,
313
+ paySupplier: PaySupplier.STRIPE,
314
+ paySubscriptionId: context.paySubscriptionId ?? undefined,
315
+ payInvoiceId: context.invoiceId,
316
+ billingReason: context.billingReason ?? undefined,
317
+ payTransactionId: context.paymentIntentId ?? undefined,
318
+ priceId: context.priceId ?? undefined,
319
+ priceName: context.priceName ?? undefined,
320
+ type: TransactionType.SUBSCRIPTION,
321
+ amount: context.amountPaidCents / 100,
322
+ currency: context.currency.toUpperCase(),
323
+ creditsGranted: 0,
324
+ paidAt: context.paidAt ?? undefined,
325
+ paidEmail: context.paidEmail,
326
+ payUpdatedAt: new Date(),
327
+ orderDetail: 'Subscription renewal payment failed',
328
+ },
329
+ tx
330
+ );
331
+
332
+ await creditService.payFailedWatcher(
333
+ {
334
+ userId: context.userId,
335
+ feature: `${TransactionType.SUBSCRIPTION}_renewal_failed`,
336
+ operationReferId: context.orderId,
337
+ creditType: CreditType.PAID,
338
+ operationType: OperationType.RECHARGE,
339
+ creditsChange: 0,
340
+ },
341
+ tx
342
+ );
343
+
344
+ await subscriptionService.updateSubscription(
345
+ context.subIdKey,
346
+ {
347
+ status: SubscriptionStatus.PAST_DUE,
348
+ orderId: context.orderId,
349
+ updatedAt: new Date(),
350
+ },
351
+ tx
352
+ );
353
+ });
354
+ }
355
+
356
+ async syncSubscriptionFromStripe(
357
+ params: {
358
+ subscription: Subscription;
359
+ status: string;
360
+ periodStart: Date;
361
+ periodEnd: Date;
362
+ orderId: string,
363
+ isUserCancel: boolean
364
+ }
365
+ ): Promise<void> {
366
+ await runInTransaction(async (tx) => {
367
+ if (params.isUserCancel) {
368
+ // 记录用户取消订阅的时间信息
369
+ await transactionService.update(
370
+ params.orderId,
371
+ {
372
+ subLastTryCancelAt: new Date(),
373
+ },
374
+ tx
375
+ );
376
+ }
377
+ await subscriptionService.updateSubscription(
378
+ params.subscription.id,
379
+ {
380
+ status: params.status,
381
+ subPeriodStart: params.periodStart,
382
+ subPeriodEnd: params.periodEnd,
383
+ updatedAt: new Date(),
384
+ },
385
+ tx
386
+ );
387
+ });
388
+ }
389
+
390
+ async processSubscriptionCancel(
391
+ context: SubscriptionCancelContext
392
+ ): Promise<void> {
393
+ await runInTransaction(async (tx) => {
394
+ // 更新订单, 记录取消信息
395
+ await transactionService.update(
396
+ context.orderId,
397
+ {
398
+ subPeriodCanceledAt: context.canceledAt,
399
+ subCancellationDetail: context.cancellationDetail ?? undefined
400
+ },
401
+ tx
402
+ )
403
+ // 更新订阅信息
404
+ await subscriptionService.updateStatus(context.subIdKey, SubscriptionStatus.CANCELED, tx);
405
+
406
+ // 清理积分并留痕
407
+ await creditService.purgePaidCredit(context.userId, 'cancel_subscription_purge', context.orderId, tx);
408
+ })
409
+ }
410
+
411
+
412
+ async processSubscriptionRefund(
413
+ context: SubscriptionRefundContext
414
+ ): Promise<void> {
415
+ await runInTransaction(async (tx) => {
416
+ const now = new Date();
417
+
418
+ await transactionService.update(
419
+ context.transaction.orderId,
420
+ {
421
+ orderStatus: OrderStatus.REFUNDED,
422
+ paymentStatus: PaymentStatus.UN_PAID,
423
+ payUpdatedAt: now,
424
+ },
425
+ tx
426
+ );
427
+
428
+ const subscription =
429
+ context.subscription ??
430
+ (context.transaction.paySubscriptionId
431
+ ? await subscriptionService.findByPaySubscriptionId(context.transaction.paySubscriptionId, tx)
432
+ : null);
433
+
434
+ if (subscription) {
435
+ await subscriptionService.updateSubscription(
436
+ subscription.id,
437
+ {
438
+ status: SubscriptionStatus.CANCELED,
439
+ updatedAt: now,
440
+ },
441
+ tx
442
+ );
443
+ }
444
+
445
+ const credit = await creditService.getCredit(context.transaction.userId, tx);
446
+ const paidBalance = Math.max(credit?.balancePaid ?? 0, 0);
447
+
448
+ if (paidBalance > 0) {
449
+ await creditService.consumeCredit(
450
+ context.transaction.userId,
451
+ { paid: paidBalance },
452
+ {
453
+ feature: OrderStatus.REFUNDED,
454
+ operationReferId: context.transaction.orderId,
455
+ },
456
+ tx
457
+ );
458
+ }
459
+ });
460
+ }
461
+
462
+ async processOneTimeRefund(
463
+ context: OneTimeRefundContext
464
+ ): Promise<void> {
465
+ await runInTransaction(async (tx) => {
466
+ const now = new Date();
467
+
468
+ await transactionService.update(
469
+ context.transaction.orderId,
470
+ {
471
+ orderStatus: OrderStatus.REFUNDED,
472
+ paymentStatus: PaymentStatus.UN_PAID,
473
+ payUpdatedAt: now,
474
+ },
475
+ tx
476
+ );
477
+
478
+ const credit = await creditService.getCredit(context.transaction.userId, tx);
479
+ const currentBalance = Math.max(credit?.balanceOneTimePaid ?? 0, 0);
480
+ const granted = Math.max(context.transaction.creditsGranted ?? 0, 0);
481
+ const amountToConsume = Math.min(currentBalance, granted);
482
+
483
+ if (amountToConsume > 0) {
484
+ await creditService.consumeCredit(
485
+ context.transaction.userId,
486
+ { oneTimePaid: amountToConsume },
487
+ {
488
+ feature: OrderStatus.REFUNDED,
489
+ operationReferId: context.transaction.orderId,
490
+ },
491
+ tx
492
+ );
493
+ }
494
+ });
495
+ }
496
+ }
497
+
498
+ export const billingAggregateService = new BillingAggregateService();
@@ -0,0 +1,2 @@
1
+ export { userAggregateService } from './user.aggregate.service';
2
+ export { billingAggregateService } from './billing.aggregate.service';
@@ -0,0 +1,168 @@
1
+ import { CreditType, OperationType, UserStatus } from '../database/constants';
2
+ import { creditService, subscriptionService, userService } from '../database/index';
3
+ import type { Credit, Prisma, User } from '../database/prisma-model-type';
4
+ import { freeAmount, freeRegisterAmount } from '../../lib/credit-init';
5
+ import { runInTransaction } from '../../prisma/prisma-transaction-util';
6
+
7
+
8
+ export class UserAggregateService {
9
+
10
+ async initAnonymousUser(
11
+ fingerprintId: string,
12
+ options?: { sourceRef?: Prisma.InputJsonValue; }
13
+ ): Promise<{ newUser: User; credit: Credit; }> {
14
+ return runInTransaction(async (tx) => {
15
+ const newUser = await userService.createUser(
16
+ {
17
+ fingerprintId,
18
+ sourceRef: options?.sourceRef,
19
+ status: UserStatus.ANONYMOUS,
20
+ },
21
+ tx
22
+ );
23
+
24
+ const credit = await creditService.initializeCreditWithFree(
25
+ {
26
+ userId: newUser.userId,
27
+ feature: 'anonymous_user_init',
28
+ creditType: CreditType.FREE,
29
+ operationType: OperationType.SYS_GIFT,
30
+ operationReferId: newUser.userId,
31
+ creditsChange: freeAmount,
32
+ },
33
+ tx
34
+ );
35
+
36
+ await subscriptionService.initializeSubscription(newUser.userId, tx);
37
+
38
+ return { newUser, credit };
39
+ });
40
+ }
41
+
42
+ /**
43
+ * 创建新的注册用户
44
+ *
45
+ * 初始化步骤(与 credit 平行):
46
+ * 1. 创建 User 记录
47
+ * 2. 初始化 Credit 记录(免费积分)
48
+ * 3. 初始化 Subscription 记录(占位符,status=INCOMPLETE)
49
+ * 4. 记录 CreditUsage(审计)
50
+ *
51
+ * 后续当用户通过 Stripe 订阅时:
52
+ * - session.completed 或 invoice.paid 会 UPDATE subscription 记录
53
+ * - 不需要 CREATE,只需 UPDATE 确保逻辑一致
54
+ */
55
+ async createNewRegisteredUser(
56
+ clerkUserId: string,
57
+ email?: string,
58
+ fingerprintId?: string,
59
+ userName?: string,
60
+ sourceRef?: Prisma.InputJsonValue,
61
+ ): Promise<{ newUser: User; credit: Credit; }> {
62
+ return runInTransaction(async (tx) => {
63
+ const newUser = await userService.createUser(
64
+ {
65
+ clerkUserId,
66
+ email,
67
+ fingerprintId,
68
+ userName,
69
+ sourceRef,
70
+ status: UserStatus.REGISTERED,
71
+ },
72
+ tx
73
+ );
74
+
75
+ const credit = await creditService.initializeCreditWithFree(
76
+ {
77
+ userId: newUser.userId,
78
+ feature: 'user_registration_init',
79
+ creditType: CreditType.FREE,
80
+ operationType: OperationType.SYS_GIFT,
81
+ operationReferId: newUser.userId,
82
+ creditsChange: freeRegisterAmount,
83
+ },
84
+ );
85
+
86
+ await subscriptionService.initializeSubscription(newUser.userId, tx);
87
+ return { newUser, credit };
88
+ });
89
+ }
90
+
91
+ // 注意积分审查日志的处理
92
+ async upgradeToRegistered(
93
+ userId: string,
94
+ email: string,
95
+ clerkUserId: string,
96
+ userName?: string,
97
+ ): Promise<{ updateUser: User; credit: Credit; }> {
98
+ return runInTransaction(async (tx) => {
99
+ const updateUser = await userService.upgradeToRegistered(
100
+ userId,
101
+ {
102
+ email,
103
+ clerkUserId,
104
+ userName
105
+ },
106
+ tx
107
+ );
108
+
109
+ // 先清空匿名积分并审计日志留痕
110
+ await creditService.purgeFreeCredit(userId, 'user_registered_purge', userId, tx);
111
+ // 再初始化完成注册获得免费积分
112
+ const credit = await creditService.initializeCreditWithFree(
113
+ {
114
+ userId: updateUser.userId,
115
+ feature: 'user_registration_init',
116
+ creditType: CreditType.FREE,
117
+ operationType: OperationType.SYS_GIFT,
118
+ operationReferId: userId,
119
+ creditsChange: freeRegisterAmount,
120
+ },
121
+ tx
122
+ );
123
+
124
+ return { updateUser: updateUser, credit: credit };
125
+ });
126
+ }
127
+
128
+ async handleUserUnregister(clerkUserId: string): Promise<string | null> {
129
+ return runInTransaction(async (tx) => {
130
+ // 根据clerkUserId查找用户
131
+ const user = await userService.findByClerkUserId(clerkUserId, tx);
132
+ if (!user) {
133
+ console.log(`User with clerkUserId ${clerkUserId} not found`);
134
+ return null;
135
+ }
136
+ const userId = user.userId;
137
+ // 更改用户状态,保留user信息尤其是FingerprintId,防止反复注册薅羊毛
138
+ await userService.unregister(user.userId);
139
+ // 清空积分
140
+ await creditService.purgeCredit(userId, 'soft_delete_user', userId, tx);
141
+
142
+ const subscription = await subscriptionService.getActiveSubscription(userId, tx);
143
+ if (subscription) {
144
+ // 如果有订阅信息,则要更新
145
+ await subscriptionService.cancelSubscription(subscription.id, true, tx);
146
+ }
147
+
148
+ return user.userId;
149
+ });
150
+ }
151
+
152
+ private async findUserWithRelations(
153
+ userId: string,
154
+ tx: Prisma.TransactionClient
155
+ ) {
156
+ return tx.user.findUnique({
157
+ where: { userId },
158
+ include: {
159
+ credit: true,
160
+ subscription: true,
161
+ transactions: true,
162
+ creditAuditLogs: true,
163
+ },
164
+ });
165
+ }
166
+ }
167
+
168
+ export const userAggregateService = new UserAggregateService();
@@ -0,0 +1 @@
1
+ export * from './user-context-service';