@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,747 @@
1
+ import { Prisma } from './prisma-model-type';
2
+ import type { Credit, CreditAuditLog } from './prisma-model-type';
3
+ import { CreditType, OperationType } from './constants';
4
+ import { freeExpiredDays } from '../../lib/credit-init';
5
+ import { checkAndFallbackWithNonTCClient } from '../../prisma/index';
6
+ import { creditAuditLogService } from './creditAuditLog.service';
7
+
8
+ type CreditAmounts = {
9
+ free?: number;
10
+ paid?: number;
11
+ oneTimePaid?: number;
12
+ };
13
+
14
+ type CreditLimitAdjustments = {
15
+ free?: number;
16
+ paid?: number;
17
+ oneTimePaid?: number;
18
+ };
19
+
20
+ type CreditOperationOptions = {
21
+ context: string;
22
+ operationType: typeof OperationType[keyof typeof OperationType];
23
+ updateMode: 'increment' | 'decrement';
24
+ feature?: string;
25
+ operationReferId: string;
26
+ limitAdjustments?: CreditLimitAdjustments;
27
+ defaultLimitAdjustmentsToAmounts?: boolean;
28
+ ensureSufficientBalance?: boolean;
29
+ ensureSufficientLimits?: boolean;
30
+ };
31
+
32
+ type CreditBalanceField = 'balanceFree' | 'balancePaid' | 'balanceOneTimePaid';
33
+ type CreditLimitField = 'totalFreeLimit' | 'totalPaidLimit' | 'totalOneTimePaidLimit';
34
+ type CreditWindowStartField = 'freeStart' | 'paidStart' | 'oneTimePaidStart';
35
+ type CreditWindowEndField = 'freeEnd' | 'paidEnd' | 'oneTimePaidEnd';
36
+
37
+ type CreditPurgeConfig = {
38
+ amountKey: keyof Required<CreditAmounts>;
39
+ balanceField: CreditBalanceField;
40
+ limitField: CreditLimitField;
41
+ startField: CreditWindowStartField;
42
+ endField: CreditWindowEndField;
43
+ };
44
+
45
+ const CREDIT_PURGE_CONFIG: Record<
46
+ typeof CreditType[keyof typeof CreditType],
47
+ CreditPurgeConfig
48
+ > = {
49
+ [CreditType.FREE]: {
50
+ amountKey: 'free',
51
+ balanceField: 'balanceFree',
52
+ limitField: 'totalFreeLimit',
53
+ startField: 'freeStart',
54
+ endField: 'freeEnd',
55
+ },
56
+ [CreditType.PAID]: {
57
+ amountKey: 'paid',
58
+ balanceField: 'balancePaid',
59
+ limitField: 'totalPaidLimit',
60
+ startField: 'paidStart',
61
+ endField: 'paidEnd',
62
+ },
63
+ [CreditType.ONE_TIME_PAID]: {
64
+ amountKey: 'oneTimePaid',
65
+ balanceField: 'balanceOneTimePaid',
66
+ limitField: 'totalOneTimePaidLimit',
67
+ startField: 'oneTimePaidStart',
68
+ endField: 'oneTimePaidEnd',
69
+ },
70
+ };
71
+
72
+
73
+ export class CreditService {
74
+
75
+ private normalizeAmounts(amounts?: CreditAmounts): Required<CreditAmounts> {
76
+ return {
77
+ free: Math.trunc(amounts?.free ?? 0),
78
+ paid: Math.trunc(amounts?.paid ?? 0),
79
+ oneTimePaid: Math.trunc(amounts?.oneTimePaid ?? 0),
80
+ };
81
+ }
82
+
83
+ private hasAnyChange(amounts: Required<CreditAmounts>): boolean {
84
+ return amounts.free !== 0 || amounts.paid !== 0 || amounts.oneTimePaid !== 0;
85
+ }
86
+
87
+ private ensureNonNegative(amounts: Required<CreditAmounts>, context: string) {
88
+ if (amounts.free < 0 || amounts.paid < 0 || amounts.oneTimePaid < 0) {
89
+ throw new Error(`${context}: negative credit adjustments are not allowed`);
90
+ }
91
+ }
92
+
93
+ private ensureSufficientBalance(current: Credit, deduction: Required<CreditAmounts>) {
94
+ if (deduction.free > current.balanceFree) {
95
+ throw new Error('Insufficient free credits');
96
+ }
97
+ if (deduction.paid > current.balancePaid) {
98
+ throw new Error('Insufficient paid credits');
99
+ }
100
+ if (deduction.oneTimePaid > current.balanceOneTimePaid) {
101
+ throw new Error('Insufficient one-time paid credits');
102
+ }
103
+ }
104
+
105
+ private ensureSufficientLimits(current: Credit, deduction: Required<CreditLimitAdjustments>) {
106
+ if (deduction.free > current.totalFreeLimit) {
107
+ throw new Error('Insufficient free credit limit');
108
+ }
109
+ if (deduction.paid > current.totalPaidLimit) {
110
+ throw new Error('Insufficient paid credit limit');
111
+ }
112
+ if (deduction.oneTimePaid > current.totalOneTimePaidLimit) {
113
+ throw new Error('Insufficient one-time paid credit limit');
114
+ }
115
+ }
116
+
117
+ private buildIncrementData(
118
+ amounts: Required<CreditAmounts>,
119
+ limitAdjustments?: Required<CreditLimitAdjustments>
120
+ ): Prisma.CreditUpdateInput {
121
+ const data: Prisma.CreditUpdateInput = {};
122
+ if (amounts.free !== 0) {
123
+ data.balanceFree = { increment: amounts.free };
124
+ if (limitAdjustments && limitAdjustments.free !== 0) {
125
+ data.totalFreeLimit = { increment: limitAdjustments.free };
126
+ }
127
+ }
128
+ if (amounts.paid !== 0) {
129
+ data.balancePaid = { increment: amounts.paid };
130
+ if (limitAdjustments && limitAdjustments.paid !== 0) {
131
+ data.totalPaidLimit = { increment: limitAdjustments.paid };
132
+ }
133
+ }
134
+ if (amounts.oneTimePaid !== 0) {
135
+ data.balanceOneTimePaid = { increment: amounts.oneTimePaid };
136
+ if (limitAdjustments && limitAdjustments.oneTimePaid !== 0) {
137
+ data.totalOneTimePaidLimit = { increment: limitAdjustments.oneTimePaid };
138
+ }
139
+ }
140
+ return data;
141
+ }
142
+
143
+ private buildDecrementData(
144
+ amounts: Required<CreditAmounts>,
145
+ limitAdjustments?: Required<CreditLimitAdjustments>
146
+ ): Prisma.CreditUpdateInput {
147
+ const data: Prisma.CreditUpdateInput = {};
148
+ if (amounts.free !== 0) {
149
+ data.balanceFree = { decrement: amounts.free };
150
+ if (limitAdjustments && limitAdjustments.free !== 0) {
151
+ data.totalFreeLimit = { decrement: limitAdjustments.free };
152
+ }
153
+ }
154
+ if (amounts.paid !== 0) {
155
+ data.balancePaid = { decrement: amounts.paid };
156
+ if (limitAdjustments && limitAdjustments.paid !== 0) {
157
+ data.totalPaidLimit = { decrement: limitAdjustments.paid };
158
+ }
159
+ }
160
+ if (amounts.oneTimePaid !== 0) {
161
+ data.balanceOneTimePaid = { decrement: amounts.oneTimePaid };
162
+ if (limitAdjustments && limitAdjustments.oneTimePaid !== 0) {
163
+ data.totalOneTimePaidLimit = { decrement: limitAdjustments.oneTimePaid };
164
+ }
165
+ }
166
+ return data;
167
+ }
168
+
169
+ private async executeCreditOperation(
170
+ userId: string,
171
+ amounts: CreditAmounts,
172
+ options: CreditOperationOptions,
173
+ tx?: Prisma.TransactionClient
174
+ ): Promise<{ credit: Credit; usage: CreditAuditLog[] }> {
175
+ const normalized = this.normalizeAmounts(amounts);
176
+ this.ensureNonNegative(normalized, options.context);
177
+
178
+ if (!this.hasAnyChange(normalized)) {
179
+ throw new Error(`${options.context}: no credit change specified`);
180
+ }
181
+
182
+ let normalizedLimitAdjustments: Required<CreditLimitAdjustments> | undefined;
183
+ if (options.limitAdjustments || options.defaultLimitAdjustmentsToAmounts) {
184
+ const raw = options.limitAdjustments ?? amounts;
185
+ normalizedLimitAdjustments = this.normalizeAmounts(raw);
186
+ this.ensureNonNegative(normalizedLimitAdjustments, `${options.context} limitAdjustments`);
187
+ }
188
+
189
+ const client = checkAndFallbackWithNonTCClient(tx);
190
+ const currentCredit = await client.credit.findUnique({
191
+ where: { userId },
192
+ });
193
+
194
+ if (!currentCredit) {
195
+ throw new Error('User credits not found');
196
+ }
197
+
198
+ if (options.ensureSufficientBalance) {
199
+ this.ensureSufficientBalance(currentCredit, normalized);
200
+ }
201
+
202
+ if (options.ensureSufficientLimits && normalizedLimitAdjustments) {
203
+ this.ensureSufficientLimits(currentCredit, normalizedLimitAdjustments);
204
+ }
205
+
206
+ const data =
207
+ options.updateMode === 'increment'
208
+ ? this.buildIncrementData(normalized, normalizedLimitAdjustments)
209
+ : this.buildDecrementData(normalized, normalizedLimitAdjustments);
210
+
211
+ const credit = await client.credit.update({
212
+ where: { userId },
213
+ data,
214
+ });
215
+
216
+ const usage = await this.recordCreditAuditLog(client, userId, options.operationType, normalized, {
217
+ feature: options.feature,
218
+ operationReferId: options.operationReferId,
219
+ });
220
+
221
+ return { credit, usage };
222
+ }
223
+
224
+ private async recordCreditAuditLog(
225
+ client: Prisma.TransactionClient,
226
+ userId: string,
227
+ operationType: string,
228
+ amounts: Required<CreditAmounts>,
229
+ options: {
230
+ feature?: string;
231
+ operationReferId: string;
232
+ }
233
+ ): Promise<CreditAuditLog[]> {
234
+ const auditPayload: Prisma.CreditAuditLogUncheckedCreateInput[] = [];
235
+
236
+ if (amounts.free > 0) {
237
+ auditPayload.push({
238
+ userId,
239
+ feature: options.feature,
240
+ operationReferId: options.operationReferId,
241
+ creditType: CreditType.FREE,
242
+ operationType,
243
+ creditsChange: amounts.free,
244
+ });
245
+ }
246
+
247
+ if (amounts.paid > 0) {
248
+ auditPayload.push({
249
+ userId,
250
+ feature: options.feature,
251
+ operationReferId: options.operationReferId,
252
+ creditType: CreditType.PAID,
253
+ operationType,
254
+ creditsChange: amounts.paid,
255
+ });
256
+ }
257
+
258
+ if (amounts.oneTimePaid > 0) {
259
+ auditPayload.push({
260
+ userId,
261
+ feature: options.feature,
262
+ operationReferId: options.operationReferId,
263
+ creditType: CreditType.ONE_TIME_PAID,
264
+ operationType,
265
+ creditsChange: amounts.oneTimePaid,
266
+ });
267
+ }
268
+
269
+ if (auditPayload.length === 0) {
270
+ return [];
271
+ }
272
+
273
+ const audits: CreditAuditLog[] = [];
274
+ for (const payload of auditPayload) {
275
+ const auditlog = await client.creditAuditLog.create({ data: payload });
276
+ audits.push(auditlog);
277
+ }
278
+
279
+ return audits;
280
+ }
281
+
282
+ // Initialize User Credits, use upsert for easy handle anonymous upgrade to register
283
+ async initializeCreditWithFree(
284
+ init: {
285
+ userId: string,
286
+ feature: string,
287
+ creditType: string,
288
+ operationType: string,
289
+ operationReferId: string,
290
+ creditsChange: number,
291
+ },
292
+ tx?: Prisma.TransactionClient
293
+ ): Promise<Credit> {
294
+ const now = new Date();
295
+ const freeStart = now;
296
+ const freeEnd = new Date(now);
297
+ freeEnd.setDate(freeEnd.getDate() + freeExpiredDays);
298
+ freeEnd.setHours(23, 59, 59, 999);
299
+ const normalized = this.normalizeAmounts({ free: init.creditsChange });
300
+ this.ensureNonNegative(normalized, 'initializeCredit');
301
+ const client = checkAndFallbackWithNonTCClient(tx);
302
+
303
+ // 这里使用upsert语义是为了代码复用,处理匿名初始化和匿名->注册的初始化
304
+ const credit = await client.credit.upsert({
305
+ where: {
306
+ userId: init.userId
307
+ },
308
+ update: {
309
+ balanceFree: normalized.free,
310
+ totalFreeLimit: normalized.free,
311
+ freeStart: freeStart,
312
+ freeEnd: freeEnd,
313
+ },
314
+ create: {
315
+ userId: init.userId,
316
+ balanceFree: normalized.free,
317
+ totalFreeLimit: normalized.free,
318
+ freeStart: freeStart,
319
+ freeEnd: freeEnd,
320
+ },
321
+ });
322
+
323
+ await creditAuditLogService.recordCreditOperation( init, tx );
324
+
325
+ return credit;
326
+ }
327
+
328
+ async payFailedWatcher(
329
+ data: {
330
+ userId: string,
331
+ feature: string,
332
+ creditType: string,
333
+ operationType: string,
334
+ operationReferId: string
335
+ creditsChange: number,
336
+ },
337
+ tx?: Prisma.TransactionClient
338
+ ): Promise<void> {
339
+ await creditAuditLogService.recordAuditLog( data, tx );
340
+ console.warn('payFailedWatcher completed');
341
+ }
342
+
343
+
344
+ // Get User Credits
345
+ async getCredit(userId: string, tx?: Prisma.TransactionClient): Promise<Credit | null> {
346
+ const client = checkAndFallbackWithNonTCClient(tx);
347
+
348
+ const credit = await client.credit.findUnique({
349
+ where: { userId },
350
+ });
351
+
352
+ if (!credit) {
353
+ return null;
354
+ }
355
+
356
+ // Guard query result: if a credit block has no end time or is already expired, treat its balance as 0
357
+ const now = new Date();
358
+ const protectedCredit: Credit = { ...credit };
359
+
360
+ if (!credit.freeEnd || now >= credit.freeEnd) {
361
+ protectedCredit.balanceFree = 0;
362
+ }
363
+
364
+ if (!credit.paidEnd || now >= credit.paidEnd) {
365
+ protectedCredit.balancePaid = 0;
366
+ }
367
+
368
+ if (!credit.oneTimePaidEnd || now >= credit.oneTimePaidEnd) {
369
+ protectedCredit.balanceOneTimePaid = 0;
370
+ }
371
+
372
+ return protectedCredit;
373
+ }
374
+
375
+ // Get Total Credit Balance
376
+ async getTotalBalance(userId: string, tx?: Prisma.TransactionClient): Promise<number> {
377
+ const credits = await this.getCredit(userId, tx);
378
+ if (!credits) return 0;
379
+ return credits.balanceFree + credits.balancePaid + credits.balanceOneTimePaid;
380
+ }
381
+
382
+ // Recharge Credits (Transactional)
383
+ async rechargeCredit(
384
+ userId: string,
385
+ amounts: CreditAmounts,
386
+ options: {
387
+ operationReferId: string;
388
+ feature?: string;
389
+ limitAdjustments?: CreditLimitAdjustments;
390
+ },
391
+ tx?: Prisma.TransactionClient
392
+ ): Promise<{ credit: Credit; usage: CreditAuditLog[] }> {
393
+ return this.executeCreditOperation(
394
+ userId,
395
+ amounts,
396
+ {
397
+ context: 'rechargeCredit',
398
+ operationType: OperationType.RECHARGE,
399
+ updateMode: 'increment',
400
+ feature: options.feature,
401
+ operationReferId: options.operationReferId,
402
+ limitAdjustments: options.limitAdjustments,
403
+ defaultLimitAdjustmentsToAmounts: options.limitAdjustments === undefined,
404
+ },
405
+ tx
406
+ );
407
+ }
408
+
409
+ // Consume Credits (Transactional)
410
+ async consumeCredit(
411
+ userId: string,
412
+ amounts: CreditAmounts,
413
+ options: {
414
+ feature: string;
415
+ operationReferId: string;
416
+ },
417
+ tx?: Prisma.TransactionClient
418
+ ): Promise<{ credit: Credit; usage: CreditAuditLog[] }> {
419
+ return this.executeCreditOperation(
420
+ userId,
421
+ amounts,
422
+ {
423
+ context: 'consumeCredit',
424
+ operationType: OperationType.CONSUME,
425
+ updateMode: 'decrement',
426
+ feature: options.feature,
427
+ operationReferId: options.operationReferId,
428
+ ensureSufficientBalance: true,
429
+ },
430
+ tx
431
+ );
432
+ }
433
+
434
+ // Freeze Credits
435
+ async freezeCredit(
436
+ userId: string,
437
+ amounts: CreditAmounts,
438
+ reason: string,
439
+ operationReferId: string,
440
+ tx?: Prisma.TransactionClient
441
+ ): Promise<{ credit: Credit; usage: CreditAuditLog[] }> {
442
+ return this.executeCreditOperation(
443
+ userId,
444
+ amounts,
445
+ {
446
+ context: 'freezeCredit',
447
+ operationType: OperationType.FREEZE,
448
+ operationReferId,
449
+ updateMode: 'decrement',
450
+ feature: reason,
451
+ ensureSufficientBalance: true,
452
+ },
453
+ tx
454
+ );
455
+ }
456
+
457
+ // Unfreeze Credits
458
+ async unfreezeCredit(
459
+ userId: string,
460
+ amounts: CreditAmounts,
461
+ reason: string,
462
+ operationReferId: string,
463
+ tx?: Prisma.TransactionClient
464
+ ): Promise<{ credit: Credit; usage: CreditAuditLog[] }> {
465
+ return this.executeCreditOperation(
466
+ userId,
467
+ amounts,
468
+ {
469
+ context: 'unfreezeCredit',
470
+ operationType: OperationType.UNFREEZE,
471
+ operationReferId,
472
+ updateMode: 'increment',
473
+ feature: reason,
474
+ },
475
+ tx
476
+ );
477
+ }
478
+
479
+ // Refund Credits
480
+ async refundCredit(
481
+ userId: string,
482
+ amounts: CreditAmounts,
483
+ operationReferId: string,
484
+ options: {
485
+ feature?: string;
486
+ limitAdjustments?: CreditLimitAdjustments;
487
+ } = {},
488
+ tx?: Prisma.TransactionClient
489
+ ): Promise<{ credit: Credit; usage: CreditAuditLog[] }> {
490
+ return this.executeCreditOperation(
491
+ userId,
492
+ amounts,
493
+ {
494
+ context: 'refundCredit',
495
+ operationType: OperationType.CONSUME,
496
+ updateMode: 'decrement',
497
+ feature: options.feature ?? 'Refund',
498
+ operationReferId,
499
+ limitAdjustments: options.limitAdjustments,
500
+ defaultLimitAdjustmentsToAmounts: options.limitAdjustments === undefined,
501
+ ensureSufficientBalance: true,
502
+ ensureSufficientLimits: true,
503
+ },
504
+ tx
505
+ );
506
+ }
507
+
508
+ // Batch Update Credits (Admin Operation)
509
+ async adjustCredit(
510
+ userId: string,
511
+ operationReferId: string,
512
+ adjustments: {
513
+ balanceFree?: number;
514
+ balancePaid?: number;
515
+ balanceOneTimePaid?: number;
516
+ totalFreeLimit?: number;
517
+ totalPaidLimit?: number;
518
+ totalOneTimePaidLimit?: number;
519
+ },
520
+ tx?: Prisma.TransactionClient
521
+ ): Promise<Credit> {
522
+ const client = checkAndFallbackWithNonTCClient(tx);
523
+ const currentCredit = await client.credit.findUnique({
524
+ where: { userId },
525
+ });
526
+ if (!currentCredit) {
527
+ throw new Error('User credits not found');
528
+ }
529
+
530
+ const nextBalanceFree = adjustments.balanceFree ?? currentCredit.balanceFree;
531
+ const nextBalancePaid = adjustments.balancePaid ?? currentCredit.balancePaid;
532
+ const nextBalanceOneTimePaid =
533
+ adjustments.balanceOneTimePaid ?? currentCredit.balanceOneTimePaid;
534
+ const nextTotalFreeLimit = adjustments.totalFreeLimit ?? currentCredit.totalFreeLimit;
535
+ const nextTotalPaidLimit = adjustments.totalPaidLimit ?? currentCredit.totalPaidLimit;
536
+ const nextTotalOneTimePaidLimit =
537
+ adjustments.totalOneTimePaidLimit ?? currentCredit.totalOneTimePaidLimit;
538
+
539
+ if (
540
+ nextBalanceFree < 0 ||
541
+ nextBalancePaid < 0 ||
542
+ nextBalanceOneTimePaid < 0 ||
543
+ nextTotalFreeLimit < 0 ||
544
+ nextTotalPaidLimit < 0 ||
545
+ nextTotalOneTimePaidLimit < 0
546
+ ) {
547
+ throw new Error('adjustCredit: credit values cannot be negative');
548
+ }
549
+
550
+ const increaseDiff = this.normalizeAmounts({
551
+ free: Math.max(nextBalanceFree - currentCredit.balanceFree, 0),
552
+ paid: Math.max(nextBalancePaid - currentCredit.balancePaid, 0),
553
+ oneTimePaid: Math.max(
554
+ nextBalanceOneTimePaid - currentCredit.balanceOneTimePaid,
555
+ 0
556
+ ),
557
+ });
558
+
559
+ const decreaseDiff = this.normalizeAmounts({
560
+ free: Math.max(currentCredit.balanceFree - nextBalanceFree, 0),
561
+ paid: Math.max(currentCredit.balancePaid - nextBalancePaid, 0),
562
+ oneTimePaid: Math.max(
563
+ currentCredit.balanceOneTimePaid - nextBalanceOneTimePaid,
564
+ 0
565
+ ),
566
+ });
567
+
568
+ const credit = await client.credit.update({
569
+ where: { userId },
570
+ data: {
571
+ balanceFree: nextBalanceFree,
572
+ balancePaid: nextBalancePaid,
573
+ balanceOneTimePaid: nextBalanceOneTimePaid,
574
+ totalFreeLimit: nextTotalFreeLimit,
575
+ totalPaidLimit: nextTotalPaidLimit,
576
+ totalOneTimePaidLimit: nextTotalOneTimePaidLimit,
577
+ },
578
+ });
579
+
580
+ if (this.hasAnyChange(increaseDiff)) {
581
+ await this.recordCreditAuditLog(client, userId, OperationType.ADJUST_INCREASE, increaseDiff, {
582
+ feature: 'admin_adjust',
583
+ operationReferId
584
+ });
585
+ }
586
+
587
+ if (this.hasAnyChange(decreaseDiff)) {
588
+ await this.recordCreditAuditLog(client, userId, OperationType.ADJUST_DECREASE, decreaseDiff, {
589
+ feature: 'admin_adjust',
590
+ operationReferId
591
+ });
592
+ }
593
+
594
+ return credit;
595
+ }
596
+
597
+ private async purgeCreditsByTypes(
598
+ userId: string,
599
+ reason: string,
600
+ operationReferId: string,
601
+ types: Array<typeof CreditType[keyof typeof CreditType]>,
602
+ tx?: Prisma.TransactionClient
603
+ ): Promise<{ credit: Credit; usage: CreditAuditLog[] }> {
604
+ const uniqueTypes = Array.from(new Set(types));
605
+ if (uniqueTypes.length === 0) {
606
+ throw new Error('purgeCreditsByTypes: no credit types specified');
607
+ }
608
+
609
+ const client = checkAndFallbackWithNonTCClient(tx);
610
+ const currentCredit = await client.credit.findUnique({ where: { userId }, });
611
+
612
+ if (!currentCredit) {
613
+ throw new Error('User credits not found');
614
+ }
615
+
616
+ const deduction: CreditAmounts = {};
617
+ const updateData: Record<string, unknown> = {};
618
+
619
+ for (const type of uniqueTypes) {
620
+ const config = CREDIT_PURGE_CONFIG[type];
621
+ if (!config) {
622
+ throw new Error(`Unsupported credit type: ${type as string}`);
623
+ }
624
+
625
+ deduction[config.amountKey] = currentCredit[config.balanceField];
626
+
627
+ updateData[config.balanceField] = 0;
628
+ updateData[config.limitField] = 0;
629
+ updateData[config.startField] = null;
630
+ updateData[config.endField] = null;
631
+ }
632
+
633
+ const normalizedDeduction = this.normalizeAmounts(deduction);
634
+ const credit = await client.credit.update({
635
+ where: { userId },
636
+ data: updateData as Prisma.CreditUpdateInput,
637
+ });
638
+
639
+ // 强制留痕,即使是积分变化为0也记录,操作留痕
640
+ const usage = await this.recordCreditAuditLog(client, userId, OperationType.PURGE, normalizedDeduction, { feature: reason, operationReferId })
641
+
642
+ return { credit, usage };
643
+ }
644
+
645
+ async purgePaidCredit(
646
+ userId: string,
647
+ reason: string,
648
+ operationReferId: string,
649
+ tx?: Prisma.TransactionClient
650
+ ): Promise<{ credit: Credit; usage: CreditAuditLog[] }> {
651
+ return this.purgeCreditsByTypes(userId, reason, operationReferId, [CreditType.PAID], tx);
652
+ }
653
+
654
+ async purgeFreeCredit(
655
+ userId: string,
656
+ reason: string,
657
+ operationReferId: string,
658
+ tx?: Prisma.TransactionClient
659
+ ): Promise<{ credit: Credit; usage: CreditAuditLog[] }> {
660
+ return this.purgeCreditsByTypes(userId, reason, operationReferId, [CreditType.FREE], tx);
661
+ }
662
+
663
+ async purgeCredit(
664
+ userId: string,
665
+ reason: string,
666
+ operationReferId:string,
667
+ tx?: Prisma.TransactionClient
668
+ ): Promise<{ credit: Credit; usage: CreditAuditLog[] }> {
669
+ return this.purgeCreditsByTypes(
670
+ userId,
671
+ reason,
672
+ operationReferId,
673
+ [CreditType.FREE, CreditType.PAID, CreditType.ONE_TIME_PAID],
674
+ tx
675
+ );
676
+ }
677
+
678
+ // Get Users with Low Credit Balance
679
+ async getLowBalanceUsers(threshold: number = 10, tx?: Prisma.TransactionClient): Promise<Credit[]> {
680
+ const client = checkAndFallbackWithNonTCClient(tx);
681
+
682
+ const query = Prisma.sql`
683
+ SELECT * FROM credits
684
+ WHERE (balance_free + balance_paid + balance_onetime_paid) < ${threshold}
685
+ ORDER BY (balance_free + balance_paid + balance_onetime_paid) ASC
686
+ `;
687
+
688
+ return await client.$queryRaw<Credit[]>(query);
689
+ }
690
+
691
+ // Get Credit Statistics
692
+ async getCreditStats(tx?: Prisma.TransactionClient): Promise<{
693
+ totalUsers: number;
694
+ totalFreeBalance: number;
695
+ totalPaidBalance: number;
696
+ totalOneTimePaidBalance: number;
697
+ avgFreeBalance: number;
698
+ avgPaidBalance: number;
699
+ avgOneTimePaidBalance: number;
700
+ zeroBalanceUsers: number;
701
+ }> {
702
+ const client = checkAndFallbackWithNonTCClient(tx);
703
+
704
+ const stats = await client.credit.aggregate({
705
+ _count: true,
706
+ _sum: {
707
+ balanceFree: true,
708
+ balancePaid: true,
709
+ balanceOneTimePaid: true,
710
+ },
711
+ _avg: {
712
+ balanceFree: true,
713
+ balancePaid: true,
714
+ balanceOneTimePaid: true,
715
+ },
716
+ });
717
+
718
+ const zeroBalanceUsers = await client.credit.count({
719
+ where: {
720
+ AND: [
721
+ { balanceFree: 0 },
722
+ { balancePaid: 0 },
723
+ { balanceOneTimePaid: 0 },
724
+ ],
725
+ },
726
+ });
727
+
728
+ return {
729
+ totalUsers: stats._count,
730
+ totalFreeBalance: stats._sum.balanceFree || 0,
731
+ totalPaidBalance: stats._sum.balancePaid || 0,
732
+ totalOneTimePaidBalance: stats._sum.balanceOneTimePaid || 0,
733
+ avgFreeBalance: Math.round(stats._avg.balanceFree || 0),
734
+ avgPaidBalance: Math.round(stats._avg.balancePaid || 0),
735
+ avgOneTimePaidBalance: Math.round(stats._avg.balanceOneTimePaid || 0),
736
+ zeroBalanceUsers,
737
+ };
738
+ }
739
+
740
+ // Check if User has Enough Credits
741
+ async hasEnoughCredits(userId: string, amount: number, tx?: Prisma.TransactionClient): Promise<boolean> {
742
+ const totalBalance = await this.getTotalBalance(userId, tx);
743
+ return totalBalance >= amount;
744
+ }
745
+ }
746
+
747
+ export const creditService = new CreditService();