@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.
- package/LICENSE +21 -0
- package/dist/app/api/stripe/checkout/route.d.ts +19 -0
- package/dist/app/api/stripe/checkout/route.d.ts.map +1 -0
- package/dist/app/api/stripe/checkout/route.js +120 -0
- package/dist/app/api/stripe/checkout/route.mjs +118 -0
- package/dist/app/api/stripe/customer-portal/route.d.ts +11 -0
- package/dist/app/api/stripe/customer-portal/route.d.ts.map +1 -0
- package/dist/app/api/stripe/customer-portal/route.js +73 -0
- package/dist/app/api/stripe/customer-portal/route.mjs +71 -0
- package/dist/app/api/user/anonymous/init/route.d.ts +7 -0
- package/dist/app/api/user/anonymous/init/route.d.ts.map +1 -0
- package/dist/app/api/user/anonymous/init/route.js +210 -0
- package/dist/app/api/user/anonymous/init/route.mjs +208 -0
- package/dist/app/api/webhook/clerk/user/route.d.ts +7 -0
- package/dist/app/api/webhook/clerk/user/route.d.ts.map +1 -0
- package/dist/app/api/webhook/clerk/user/route.js +202 -0
- package/dist/app/api/webhook/clerk/user/route.mjs +200 -0
- package/dist/app/api/webhook/stripe/route.d.ts +8 -0
- package/dist/app/api/webhook/stripe/route.d.ts.map +1 -0
- package/dist/app/api/webhook/stripe/route.js +70 -0
- package/dist/app/api/webhook/stripe/route.mjs +67 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +83 -0
- package/dist/index.mjs +18 -0
- package/dist/lib/auth-utils.d.ts +46 -0
- package/dist/lib/auth-utils.d.ts.map +1 -0
- package/dist/lib/auth-utils.js +107 -0
- package/dist/lib/auth-utils.mjs +102 -0
- package/dist/lib/credit-init.d.ts +8 -0
- package/dist/lib/credit-init.d.ts.map +1 -0
- package/dist/lib/credit-init.js +16 -0
- package/dist/lib/credit-init.mjs +10 -0
- package/dist/lib/index.d.ts +5 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +31 -0
- package/dist/lib/index.mjs +4 -0
- package/dist/lib/money-price-config.d.ts +51 -0
- package/dist/lib/money-price-config.d.ts.map +1 -0
- package/dist/lib/money-price-config.js +156 -0
- package/dist/lib/money-price-config.mjs +151 -0
- package/dist/lib/stripe-config.d.ts +31 -0
- package/dist/lib/stripe-config.d.ts.map +1 -0
- package/dist/lib/stripe-config.js +278 -0
- package/dist/lib/stripe-config.mjs +268 -0
- 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
- 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
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/errors.js +54 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/errors.mjs +51 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/iso.js +44 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/iso.mjs +35 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/parse.js +31 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/parse.mjs +18 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/schemas.js +587 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/classic/schemas.mjs +527 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/api.js +447 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/api.mjs +399 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/checks.js +245 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/checks.mjs +232 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/core.js +68 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/core.mjs +62 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/doc.js +39 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/doc.mjs +37 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/errors.js +80 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/errors.mjs +75 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/parse.js +101 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/parse.mjs +86 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/regexes.js +102 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/regexes.mjs +76 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/registries.js +56 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/registries.mjs +52 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/schemas.js +1205 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/schemas.mjs +1157 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/util.js +407 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/util.mjs +374 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/versions.js +9 -0
- package/dist/node_modules/.pnpm/zod@4.1.12/node_modules/zod/v4/core/versions.mjs +7 -0
- package/dist/prisma/client.d.ts +2 -0
- package/dist/prisma/client.d.ts.map +1 -0
- package/dist/prisma/client.js +12 -0
- package/dist/prisma/client.mjs +1 -0
- package/dist/prisma/index.d.ts +4 -0
- package/dist/prisma/index.d.ts.map +1 -0
- package/dist/prisma/index.js +10 -0
- package/dist/prisma/index.mjs +2 -0
- package/dist/prisma/prisma-transaction-util.d.ts +3 -0
- package/dist/prisma/prisma-transaction-util.d.ts.map +1 -0
- package/dist/prisma/prisma-transaction-util.js +29 -0
- package/dist/prisma/prisma-transaction-util.mjs +27 -0
- package/dist/prisma/prisma.d.ts +4 -0
- package/dist/prisma/prisma.d.ts.map +1 -0
- package/dist/prisma/prisma.js +109 -0
- package/dist/prisma/prisma.mjs +106 -0
- package/dist/services/aggregate/billing.aggregate.service.d.ts +83 -0
- package/dist/services/aggregate/billing.aggregate.service.d.ts.map +1 -0
- package/dist/services/aggregate/billing.aggregate.service.js +308 -0
- package/dist/services/aggregate/billing.aggregate.service.mjs +306 -0
- package/dist/services/aggregate/index.d.ts +3 -0
- package/dist/services/aggregate/index.d.ts.map +1 -0
- package/dist/services/aggregate/index.js +9 -0
- package/dist/services/aggregate/index.mjs +2 -0
- package/dist/services/aggregate/user.aggregate.service.d.ts +34 -0
- package/dist/services/aggregate/user.aggregate.service.d.ts.map +1 -0
- package/dist/services/aggregate/user.aggregate.service.js +136 -0
- package/dist/services/aggregate/user.aggregate.service.mjs +133 -0
- package/dist/services/context/index.d.ts +2 -0
- package/dist/services/context/index.d.ts.map +1 -0
- package/dist/services/context/index.js +13 -0
- package/dist/services/context/index.mjs +1 -0
- package/dist/services/context/user-context-service.d.ts +30 -0
- package/dist/services/context/user-context-service.d.ts.map +1 -0
- package/dist/services/context/user-context-service.js +170 -0
- package/dist/services/context/user-context-service.mjs +162 -0
- package/dist/services/database/apilog.service.d.ts +39 -0
- package/dist/services/database/apilog.service.d.ts.map +1 -0
- package/dist/services/database/apilog.service.js +174 -0
- package/dist/services/database/apilog.service.mjs +170 -0
- package/dist/services/database/constants.d.ts +73 -0
- package/dist/services/database/constants.d.ts.map +1 -0
- package/dist/services/database/constants.js +135 -0
- package/dist/services/database/constants.mjs +117 -0
- package/dist/services/database/credit.service.d.ts +107 -0
- package/dist/services/database/credit.service.d.ts.map +1 -0
- package/dist/services/database/credit.service.js +515 -0
- package/dist/services/database/credit.service.mjs +512 -0
- package/dist/services/database/creditAuditLog.service.d.ts +73 -0
- package/dist/services/database/creditAuditLog.service.d.ts.map +1 -0
- package/dist/services/database/creditAuditLog.service.js +305 -0
- package/dist/services/database/creditAuditLog.service.mjs +302 -0
- package/dist/services/database/index.d.ts +10 -0
- package/dist/services/database/index.d.ts.map +1 -0
- package/dist/services/database/index.js +38 -0
- package/dist/services/database/index.mjs +8 -0
- package/dist/services/database/prisma-model-type.d.ts +3 -0
- package/dist/services/database/prisma-model-type.d.ts.map +1 -0
- package/dist/services/database/subscription.service.d.ts +48 -0
- package/dist/services/database/subscription.service.d.ts.map +1 -0
- package/dist/services/database/subscription.service.js +267 -0
- package/dist/services/database/subscription.service.mjs +264 -0
- package/dist/services/database/transaction.service.d.ts +92 -0
- package/dist/services/database/transaction.service.d.ts.map +1 -0
- package/dist/services/database/transaction.service.js +326 -0
- package/dist/services/database/transaction.service.mjs +323 -0
- package/dist/services/database/user.service.d.ts +45 -0
- package/dist/services/database/user.service.d.ts.map +1 -0
- package/dist/services/database/user.service.js +180 -0
- package/dist/services/database/user.service.mjs +177 -0
- package/dist/services/database/userBackup.service.d.ts +45 -0
- package/dist/services/database/userBackup.service.d.ts.map +1 -0
- package/dist/services/database/userBackup.service.js +249 -0
- package/dist/services/database/userBackup.service.mjs +246 -0
- package/dist/services/stripe/index.d.ts +2 -0
- package/dist/services/stripe/index.d.ts.map +1 -0
- package/dist/services/stripe/index.js +7 -0
- package/dist/services/stripe/index.mjs +1 -0
- package/dist/services/stripe/webhook-handler.d.ts +6 -0
- package/dist/services/stripe/webhook-handler.d.ts.map +1 -0
- package/dist/services/stripe/webhook-handler.js +537 -0
- package/dist/services/stripe/webhook-handler.mjs +535 -0
- package/migrations/create.sql +176 -0
- package/migrations/db.init.sql +13 -0
- package/migrations/init-schema.sql +19 -0
- package/migrations/purge.sql +27 -0
- package/migrations/test-check.sql +167 -0
- package/package.json +123 -0
- package/prisma/schema.prisma +191 -0
- package/src/app/api/stripe/checkout/route.ts +145 -0
- package/src/app/api/stripe/customer-portal/route.ts +83 -0
- package/src/app/api/user/anonymous/init/route.ts +284 -0
- package/src/app/api/webhook/clerk/user/route.ts +249 -0
- package/src/app/api/webhook/stripe/route.ts +93 -0
- package/src/index.ts +6 -0
- package/src/lib/auth-utils.ts +101 -0
- package/src/lib/credit-init.ts +9 -0
- package/src/lib/index.ts +4 -0
- package/src/lib/money-price-config.ts +168 -0
- package/src/lib/stripe-config.ts +333 -0
- package/src/prisma/client.ts +2 -0
- package/src/prisma/index.ts +3 -0
- package/src/prisma/prisma-transaction-util.ts +24 -0
- package/src/prisma/prisma.ts +122 -0
- package/src/services/aggregate/billing.aggregate.service.ts +498 -0
- package/src/services/aggregate/index.ts +2 -0
- package/src/services/aggregate/user.aggregate.service.ts +168 -0
- package/src/services/context/index.ts +1 -0
- package/src/services/context/user-context-service.ts +200 -0
- package/src/services/database/apilog.service.ts +185 -0
- package/src/services/database/constants.ts +148 -0
- package/src/services/database/credit.service.ts +747 -0
- package/src/services/database/creditAuditLog.service.ts +402 -0
- package/src/services/database/index.ts +41 -0
- package/src/services/database/prisma-model-type.ts +13 -0
- package/src/services/database/subscription.service.ts +319 -0
- package/src/services/database/transaction.service.ts +447 -0
- package/src/services/database/user.service.ts +218 -0
- package/src/services/database/userBackup.service.ts +290 -0
- package/src/services/stripe/index.ts +1 -0
- 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();
|