@windrun-huaiin/backend-core 30.0.0 → 31.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/README.md +95 -0
- package/dist/app/api/user/anonymous/init/fingerprint-only-route.d.ts +8 -0
- package/dist/app/api/user/anonymous/init/fingerprint-only-route.d.ts.map +1 -0
- package/dist/app/api/user/anonymous/init/fingerprint-only-route.js +20 -0
- package/dist/app/api/user/anonymous/init/fingerprint-only-route.mjs +18 -0
- package/dist/app/api/user/anonymous/init/route-shared.d.ts +10 -0
- package/dist/app/api/user/anonymous/init/route-shared.d.ts.map +1 -0
- package/dist/app/api/user/anonymous/init/route-shared.js +557 -0
- package/dist/app/api/user/anonymous/init/route-shared.mjs +555 -0
- package/dist/app/api/user/anonymous/init/route.d.ts +3 -3
- package/dist/app/api/user/anonymous/init/route.d.ts.map +1 -1
- package/dist/app/api/user/anonymous/init/route.js +6 -554
- package/dist/app/api/user/anonymous/init/route.mjs +7 -555
- package/dist/app/api/webhook/clerk/user/route.js +16 -16
- package/dist/app/api/webhook/clerk/user/route.mjs +16 -16
- package/dist/auth/auth-utils.d.ts +8 -23
- package/dist/auth/auth-utils.d.ts.map +1 -1
- package/dist/auth/auth-utils.js +8 -20
- package/dist/auth/auth-utils.mjs +8 -20
- package/dist/lib/money-price-config.d.ts +28 -28
- package/dist/lib/money-price-config.js +31 -31
- package/dist/lib/money-price-config.mjs +31 -31
- package/dist/lib/stripe-config.js +3 -3
- package/dist/lib/stripe-config.mjs +3 -3
- package/dist/prisma/prisma-transaction-util.js +1 -1
- package/dist/prisma/prisma-transaction-util.mjs +1 -1
- package/dist/prisma/prisma.d.ts.map +1 -1
- package/dist/prisma/prisma.js +18 -19
- package/dist/prisma/prisma.mjs +18 -19
- package/dist/services/aggregate/billing.aggregate.service.js +6 -6
- package/dist/services/aggregate/billing.aggregate.service.mjs +6 -6
- package/dist/services/aggregate/user.aggregate.service.d.ts +9 -9
- package/dist/services/aggregate/user.aggregate.service.js +16 -16
- package/dist/services/aggregate/user.aggregate.service.mjs +16 -16
- package/dist/services/database/constants.js +34 -34
- package/dist/services/database/constants.mjs +34 -34
- package/dist/services/database/credit.service.js +2 -2
- package/dist/services/database/credit.service.mjs +2 -2
- package/dist/services/database/transaction.service.js +1 -1
- package/dist/services/database/transaction.service.mjs +1 -1
- package/dist/services/database/user.service.js +2 -2
- package/dist/services/database/user.service.mjs +2 -2
- package/dist/services/stripe/webhook-handler.js +5 -5
- package/dist/services/stripe/webhook-handler.mjs +5 -5
- package/package.json +18 -6
- package/src/app/api/user/anonymous/init/fingerprint-only-route.ts +14 -0
- package/src/app/api/user/anonymous/init/route-shared.ts +710 -0
- package/src/app/api/user/anonymous/init/route.ts +7 -712
- package/src/app/api/webhook/clerk/user/route.ts +17 -17
- package/src/auth/auth-utils.ts +8 -23
- package/src/lib/money-price-config.ts +31 -32
- package/src/lib/stripe-config.ts +3 -3
- package/src/prisma/prisma-transaction-util.ts +1 -1
- package/src/prisma/prisma.ts +18 -19
- package/src/services/aggregate/billing.aggregate.service.ts +7 -7
- package/src/services/aggregate/user.aggregate.service.ts +16 -16
- package/src/services/database/constants.ts +34 -34
- package/src/services/database/credit.service.ts +2 -2
- package/src/services/database/transaction.service.ts +1 -1
- package/src/services/database/user.service.ts +2 -2
- package/src/services/stripe/webhook-handler.ts +5 -5
|
@@ -11,7 +11,7 @@ import { headers } from 'next/headers';
|
|
|
11
11
|
import { NextRequest, NextResponse } from 'next/server';
|
|
12
12
|
import { Webhook } from 'svix';
|
|
13
13
|
|
|
14
|
-
//
|
|
14
|
+
// Clerk webhook event type definition
|
|
15
15
|
interface ClerkWebhookEvent {
|
|
16
16
|
data: {
|
|
17
17
|
id: string;
|
|
@@ -41,12 +41,12 @@ interface ClerkWebhookEvent {
|
|
|
41
41
|
}
|
|
42
42
|
export async function POST(request: NextRequest) {
|
|
43
43
|
try {
|
|
44
|
-
//
|
|
44
|
+
// Read the raw request body.
|
|
45
45
|
const rawBody = await request.text();
|
|
46
46
|
|
|
47
47
|
let event: ClerkWebhookEvent;
|
|
48
48
|
|
|
49
|
-
//
|
|
49
|
+
// Skip signature verification in development.
|
|
50
50
|
if (process.env.NODE_ENV === 'development') {
|
|
51
51
|
console.log('Development mode: skipping webhook signature verification');
|
|
52
52
|
try {
|
|
@@ -59,13 +59,13 @@ export async function POST(request: NextRequest) {
|
|
|
59
59
|
);
|
|
60
60
|
}
|
|
61
61
|
} else {
|
|
62
|
-
//
|
|
62
|
+
// Verify the signature in production.
|
|
63
63
|
const headerPayload = await headers();
|
|
64
64
|
const svix_id = headerPayload.get('svix-id');
|
|
65
65
|
const svix_timestamp = headerPayload.get('svix-timestamp');
|
|
66
66
|
const svix_signature = headerPayload.get('svix-signature');
|
|
67
67
|
|
|
68
|
-
//
|
|
68
|
+
// Reject requests missing required headers.
|
|
69
69
|
if (!svix_id || !svix_timestamp || !svix_signature) {
|
|
70
70
|
return NextResponse.json(
|
|
71
71
|
{ error: 'Missing webhook headers' },
|
|
@@ -73,7 +73,7 @@ export async function POST(request: NextRequest) {
|
|
|
73
73
|
);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
//
|
|
76
|
+
// Load the webhook signing secret.
|
|
77
77
|
const webhookSecret = process.env.CLERK_WEBHOOK_SECRET;
|
|
78
78
|
if (!webhookSecret) {
|
|
79
79
|
console.error('CLERK_WEBHOOK_SECRET is not configured');
|
|
@@ -83,7 +83,7 @@ export async function POST(request: NextRequest) {
|
|
|
83
83
|
);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
//
|
|
86
|
+
// Verify the webhook signature.
|
|
87
87
|
try {
|
|
88
88
|
const wh = new Webhook(webhookSecret);
|
|
89
89
|
event = wh.verify(rawBody, {
|
|
@@ -110,7 +110,7 @@ export async function POST(request: NextRequest) {
|
|
|
110
110
|
let processingResult = { success: true, message: 'Event processed successfully' };
|
|
111
111
|
|
|
112
112
|
try {
|
|
113
|
-
//
|
|
113
|
+
// Dispatch by event type.
|
|
114
114
|
const { type } = event;
|
|
115
115
|
|
|
116
116
|
switch (type) {
|
|
@@ -155,7 +155,7 @@ export async function POST(request: NextRequest) {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/**
|
|
158
|
-
*
|
|
158
|
+
* Handle the user.created event.
|
|
159
159
|
*/
|
|
160
160
|
async function handleUserCreated(event: ClerkWebhookEvent) {
|
|
161
161
|
const { data } = event;
|
|
@@ -174,7 +174,7 @@ async function handleUserCreated(event: ClerkWebhookEvent) {
|
|
|
174
174
|
userName
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
//
|
|
177
|
+
// Validate required fields.
|
|
178
178
|
if (!fingerprintId) {
|
|
179
179
|
console.error('Missing fingerprintId in webhook data, process flow error');
|
|
180
180
|
return;
|
|
@@ -186,17 +186,17 @@ async function handleUserCreated(event: ClerkWebhookEvent) {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
try {
|
|
189
|
-
//
|
|
189
|
+
// Find all non-deleted users for this device fingerprint.
|
|
190
190
|
const existingUsers = await userService.findListByFingerprintId(fingerprintId);
|
|
191
191
|
if (!existingUsers || existingUsers.length === 0) {
|
|
192
192
|
console.error('Invalid fingerprintId in webhook data, process flow error');
|
|
193
193
|
return;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
//
|
|
196
|
+
// Find an existing user with the same email.
|
|
197
197
|
const sameEmailUser = existingUsers.find(user => user.email === email);
|
|
198
198
|
if (sameEmailUser) {
|
|
199
|
-
//
|
|
199
|
+
// Same account; update clerkUserId if needed.
|
|
200
200
|
if (sameEmailUser.clerkUserId !== clerkUserId) {
|
|
201
201
|
await userService.updateUser(sameEmailUser.userId, { clerkUserId, userName: userName, status: UserStatus.REGISTERED });
|
|
202
202
|
console.log(`Updated clerkUserId for user ${sameEmailUser.userId}`);
|
|
@@ -206,16 +206,16 @@ async function handleUserCreated(event: ClerkWebhookEvent) {
|
|
|
206
206
|
return;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
//
|
|
209
|
+
// Find an anonymous user with no email or clerkUserId.
|
|
210
210
|
const anonymousUser = existingUsers.find(user => !user.email && !user.clerkUserId && user.status === UserStatus.ANONYMOUS );
|
|
211
211
|
if (anonymousUser) {
|
|
212
|
-
//
|
|
212
|
+
// Upgrade the anonymous user.
|
|
213
213
|
await userAggregateService.upgradeToRegistered(anonymousUser.userId, email, clerkUserId, userName);
|
|
214
214
|
console.log(`Successfully upgraded anonymous user ${anonymousUser.userId} to registered user`);
|
|
215
215
|
return;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
//
|
|
218
|
+
// New account on the same device; create a new user.
|
|
219
219
|
await userAggregateService.createNewRegisteredUser(clerkUserId, email, fingerprintId, userName);
|
|
220
220
|
console.log(`Created new user for device ${fingerprintId} with email ${email}`);
|
|
221
221
|
|
|
@@ -226,7 +226,7 @@ async function handleUserCreated(event: ClerkWebhookEvent) {
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
/**
|
|
229
|
-
*
|
|
229
|
+
* Handle the user.deleted event.
|
|
230
230
|
*/
|
|
231
231
|
async function handleUserDeleted(event: ClerkWebhookEvent) {
|
|
232
232
|
const { data } = event;
|
package/src/auth/auth-utils.ts
CHANGED
|
@@ -4,9 +4,6 @@ import { userService } from '../services/database/index';
|
|
|
4
4
|
import { User } from '../services/database/prisma-model-type';
|
|
5
5
|
import { AUTH_ERRORS, AUTH_HEADERS, type AuthProvider, type ProviderIdentity } from './auth-shared';
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* 认证结果类型
|
|
9
|
-
*/
|
|
10
7
|
export interface AuthResult {
|
|
11
8
|
userId: string;
|
|
12
9
|
user: User;
|
|
@@ -15,7 +12,7 @@ export interface AuthResult {
|
|
|
15
12
|
}
|
|
16
13
|
|
|
17
14
|
/**
|
|
18
|
-
*
|
|
15
|
+
* Fetch User's info from header field by Middleware
|
|
19
16
|
*/
|
|
20
17
|
export async function getAuthenticatedUser(req: NextRequest): Promise<AuthResult> {
|
|
21
18
|
try {
|
|
@@ -43,7 +40,7 @@ export async function getAuthenticatedUser(req: NextRequest): Promise<AuthResult
|
|
|
43
40
|
}
|
|
44
41
|
|
|
45
42
|
/**
|
|
46
|
-
*
|
|
43
|
+
* Require Auth, success back user's id
|
|
47
44
|
*/
|
|
48
45
|
export async function requireAuth(req: NextRequest): Promise<string> {
|
|
49
46
|
const auth = await getAuthenticatedUser(req);
|
|
@@ -51,15 +48,15 @@ export async function requireAuth(req: NextRequest): Promise<string> {
|
|
|
51
48
|
}
|
|
52
49
|
|
|
53
50
|
/**
|
|
54
|
-
*
|
|
51
|
+
* Require Auth, success back user's info
|
|
55
52
|
*/
|
|
56
53
|
export async function requireAuthWithUser(req: NextRequest): Promise<AuthResult> {
|
|
57
54
|
return await getAuthenticatedUser(req);
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
58
|
+
* Only use in server side
|
|
59
|
+
* Server Component / Server Action, just need user's login status
|
|
63
60
|
*/
|
|
64
61
|
export async function getOptionalServerAuthIdentity(): Promise<ProviderIdentity | null> {
|
|
65
62
|
try {
|
|
@@ -79,8 +76,8 @@ export async function getOptionalServerAuthIdentity(): Promise<ProviderIdentity
|
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
79
|
+
* Only use in server side
|
|
80
|
+
* Server Component / Server Action, need user's login status and user's data, will check db
|
|
84
81
|
*/
|
|
85
82
|
export async function getOptionalServerAuthUser(): Promise<AuthResult | null> {
|
|
86
83
|
try {
|
|
@@ -107,7 +104,7 @@ export async function getOptionalServerAuthUser(): Promise<AuthResult | null> {
|
|
|
107
104
|
}
|
|
108
105
|
|
|
109
106
|
/**
|
|
110
|
-
* API Route
|
|
107
|
+
* API Route Auth Util
|
|
111
108
|
*/
|
|
112
109
|
export class ApiAuthUtils {
|
|
113
110
|
private req: NextRequest;
|
|
@@ -116,23 +113,14 @@ export class ApiAuthUtils {
|
|
|
116
113
|
this.req = req;
|
|
117
114
|
}
|
|
118
115
|
|
|
119
|
-
/**
|
|
120
|
-
* 要求用户必须已认证,返回用户ID
|
|
121
|
-
*/
|
|
122
116
|
async requireAuth(): Promise<string> {
|
|
123
117
|
return await requireAuth(this.req);
|
|
124
118
|
}
|
|
125
119
|
|
|
126
|
-
/**
|
|
127
|
-
* 要求用户必须已认证,返回完整用户信息
|
|
128
|
-
*/
|
|
129
120
|
async requireAuthWithUser(): Promise<AuthResult> {
|
|
130
121
|
return await requireAuthWithUser(this.req);
|
|
131
122
|
}
|
|
132
123
|
|
|
133
|
-
/**
|
|
134
|
-
* 获取用户ID(如果已认证)
|
|
135
|
-
*/
|
|
136
124
|
async getUserId(): Promise<string | null> {
|
|
137
125
|
try {
|
|
138
126
|
const auth = await getAuthenticatedUser(this.req);
|
|
@@ -142,9 +130,6 @@ export class ApiAuthUtils {
|
|
|
142
130
|
}
|
|
143
131
|
}
|
|
144
132
|
|
|
145
|
-
/**
|
|
146
|
-
* 获取完整用户信息(如果已认证)
|
|
147
|
-
*/
|
|
148
133
|
async getUser(): Promise<AuthResult | null> {
|
|
149
134
|
try {
|
|
150
135
|
return await getAuthenticatedUser(this.req);
|
|
@@ -12,7 +12,7 @@ export const moneyPriceConfig: MoneyPriceConfig = {
|
|
|
12
12
|
stripe: {
|
|
13
13
|
provider: 'stripe',
|
|
14
14
|
enabled: true,
|
|
15
|
-
//
|
|
15
|
+
// Subscription products
|
|
16
16
|
subscriptionProducts: {
|
|
17
17
|
F1: {
|
|
18
18
|
key: 'F1',
|
|
@@ -70,7 +70,7 @@ export const moneyPriceConfig: MoneyPriceConfig = {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
|
-
//
|
|
73
|
+
// Credit pack products
|
|
74
74
|
creditPackProducts: {
|
|
75
75
|
F1: {
|
|
76
76
|
key: 'F1',
|
|
@@ -106,56 +106,56 @@ export const moneyPriceConfig: MoneyPriceConfig = {
|
|
|
106
106
|
}
|
|
107
107
|
};
|
|
108
108
|
|
|
109
|
-
// ============
|
|
109
|
+
// ============ Application-level wrappers that hide moneyPriceConfig details ============
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
*
|
|
112
|
+
* Get the currently active payment provider configuration.
|
|
113
113
|
*
|
|
114
|
-
*
|
|
115
|
-
* -
|
|
116
|
-
* -
|
|
117
|
-
* -
|
|
114
|
+
* Security design:
|
|
115
|
+
* - Wrapper functions keep moneyPriceConfig private.
|
|
116
|
+
* - Utility functions extract the active provider configuration from the config.
|
|
117
|
+
* - External callers can access only this wrapper, not the full config object.
|
|
118
118
|
*
|
|
119
|
-
* @returns
|
|
119
|
+
* @returns The currently active payment provider configuration.
|
|
120
120
|
*/
|
|
121
121
|
export function getActiveProviderConfig(): PaymentProviderConfig {
|
|
122
122
|
return getActiveProviderConfigUtil(moneyPriceConfig);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
*
|
|
126
|
+
* Get the credit amount for a price ID.
|
|
127
127
|
*
|
|
128
|
-
*
|
|
129
|
-
* -
|
|
130
|
-
* -
|
|
131
|
-
* -
|
|
128
|
+
* Security design:
|
|
129
|
+
* - Wrapper functions keep moneyPriceConfig private.
|
|
130
|
+
* - Utility functions parse the config and extract the result.
|
|
131
|
+
* - External callers can access only this wrapper, not the full config object.
|
|
132
132
|
*
|
|
133
|
-
* @param priceId -
|
|
134
|
-
* @param _provider -
|
|
135
|
-
* @returns
|
|
133
|
+
* @param priceId - Price ID to query.
|
|
134
|
+
* @param _provider - Reserved for backward compatibility; currently unused.
|
|
135
|
+
* @returns The matching credit amount, or null.
|
|
136
136
|
*/
|
|
137
137
|
export function getCreditsFromPriceId(priceId?: string, _provider?: string): number | null {
|
|
138
138
|
return getCreditsFromPriceIdUtil(priceId, moneyPriceConfig);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/**
|
|
142
|
-
*
|
|
142
|
+
* Get price configuration by query parameters.
|
|
143
143
|
*
|
|
144
|
-
*
|
|
145
|
-
* 1.
|
|
146
|
-
* 2.
|
|
147
|
-
* 3.
|
|
144
|
+
* Supported query modes:
|
|
145
|
+
* 1. By priceId: getPriceConfig(priceId='price_xxx')
|
|
146
|
+
* 2. By plan and billingType: getPriceConfig(undefined, 'P2', 'monthly')
|
|
147
|
+
* 3. By plan: getPriceConfig(undefined, 'P2')
|
|
148
148
|
*
|
|
149
|
-
*
|
|
150
|
-
* -
|
|
151
|
-
* -
|
|
152
|
-
* -
|
|
149
|
+
* Security design:
|
|
150
|
+
* - Wrapper functions keep moneyPriceConfig private.
|
|
151
|
+
* - Utility functions parse the config and extract the matching result.
|
|
152
|
+
* - External callers can access only this wrapper, not the full config object.
|
|
153
153
|
*
|
|
154
|
-
* @param priceId -
|
|
155
|
-
* @param plan -
|
|
156
|
-
* @param billingType -
|
|
157
|
-
* @param _provider -
|
|
158
|
-
* @returns
|
|
154
|
+
* @param priceId - Optional price ID to query.
|
|
155
|
+
* @param plan - Optional plan name, such as 'P2' or 'U3'.
|
|
156
|
+
* @param billingType - Optional billing type, such as 'monthly' or 'yearly'.
|
|
157
|
+
* @param _provider - Reserved for backward compatibility; currently unused.
|
|
158
|
+
* @returns The matching price config with derived metadata: priceName, description, and interval.
|
|
159
159
|
*/
|
|
160
160
|
export function getPriceConfig(
|
|
161
161
|
priceId?: string,
|
|
@@ -165,4 +165,3 @@ export function getPriceConfig(
|
|
|
165
165
|
): (EnhancePricePlan & { priceName: string; description: string; interval?: string }) | null {
|
|
166
166
|
return getPriceConfigUtil(priceId, plan, billingType, moneyPriceConfig);
|
|
167
167
|
}
|
|
168
|
-
|
package/src/lib/stripe-config.ts
CHANGED
|
@@ -97,7 +97,7 @@ export const createCheckoutSession = async (
|
|
|
97
97
|
|
|
98
98
|
// One-time payment specific configuration
|
|
99
99
|
if (isSubscriptionMode) {
|
|
100
|
-
//
|
|
100
|
+
// Attach order metadata for later event matching. Stripe only allows this in subscription mode.
|
|
101
101
|
sessionParams.subscription_data = subscriptionData;
|
|
102
102
|
} else {
|
|
103
103
|
// One-time payments don't create invoices
|
|
@@ -129,7 +129,7 @@ export const createCheckoutSession = async (
|
|
|
129
129
|
}
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
-
//
|
|
132
|
+
// Resolve the payment ID from an invoice ID.
|
|
133
133
|
export const fetchPaymentId = async (invoiceId: string ): Promise<string> => {
|
|
134
134
|
const fullInvoice = await getStripe().invoices.retrieve(invoiceId, {
|
|
135
135
|
expand: ['payments']
|
|
@@ -193,7 +193,7 @@ export const createOrGetCustomer = async (params: {
|
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
//
|
|
196
|
+
// Create a new customer.
|
|
197
197
|
const customerParams: Stripe.CustomerCreateParams = {
|
|
198
198
|
metadata: {
|
|
199
199
|
user_id: userId,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getBackendCorePrisma } from './prisma';
|
|
2
2
|
import type { Prisma } from '@core/db/prisma-model-type';
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// Transaction helper that logs rollback details on failure.
|
|
5
5
|
export async function runInTransaction<T>(
|
|
6
6
|
fn: (tx: Prisma.TransactionClient) => Promise<T>,
|
|
7
7
|
operationName?: string
|
package/src/prisma/prisma.ts
CHANGED
|
@@ -41,7 +41,7 @@ const globalForPrisma = globalThis as unknown as {
|
|
|
41
41
|
__prisma_ssl_warning_logged?: boolean;
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
// ====================
|
|
44
|
+
// ==================== Logging Configuration ====================
|
|
45
45
|
const getLogConfig = () => {
|
|
46
46
|
if (process.env.PRISMA_DEBUG === 'true') {
|
|
47
47
|
return [
|
|
@@ -125,42 +125,41 @@ function registerDevelopmentQueryLogger(prismaClient: BackendCoreHostPrismaClien
|
|
|
125
125
|
globalForPrisma[ID_KEY] = listenerId;
|
|
126
126
|
console.log(`Prisma Query Logger Registered | Listener ID: ${listenerId} | Instance ID: ${instanceId}`);
|
|
127
127
|
|
|
128
|
-
// ---
|
|
128
|
+
// --- Custom SQL interpolation ---
|
|
129
129
|
const interpolate = (query: string, params: string) => {
|
|
130
|
-
// 1.
|
|
130
|
+
// 1. Validate and parse parameters safely.
|
|
131
131
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
132
132
|
let parameters: any[] = [];
|
|
133
133
|
try {
|
|
134
|
-
//
|
|
135
|
-
// 如果 params 是空字符串 "",或者不是有效的 JSON,这里会捕获错误
|
|
134
|
+
// Parse the params string. Empty strings or invalid JSON are handled by the catch block.
|
|
136
135
|
parameters = params && params.length > 0 ? JSON.parse(params) : [];
|
|
137
136
|
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
138
137
|
} catch (e) {
|
|
139
|
-
//
|
|
138
|
+
// If parsing fails, return the original query without interpolation.
|
|
140
139
|
return query;
|
|
141
140
|
}
|
|
142
141
|
|
|
143
|
-
//
|
|
142
|
+
// Ensure parameters is an array.
|
|
144
143
|
if (!Array.isArray(parameters)) {
|
|
145
|
-
console.warn('Prisma params
|
|
144
|
+
console.warn('Prisma params did not parse to an array; skipping parameter interpolation. Result:', parameters);
|
|
146
145
|
return query;
|
|
147
146
|
}
|
|
148
147
|
|
|
149
|
-
//
|
|
148
|
+
// If there are no parameters, return the query as-is.
|
|
150
149
|
if (parameters.length === 0) {
|
|
151
150
|
return query;
|
|
152
151
|
}
|
|
153
152
|
|
|
154
|
-
// 2.
|
|
153
|
+
// 2. Safely stringify parameter values.
|
|
155
154
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
156
155
|
const safeValues = parameters.map((p: any) => {
|
|
157
156
|
if (p === null) return 'NULL';
|
|
158
|
-
//
|
|
157
|
+
// Quote and escape string values for readable SQL logging.
|
|
159
158
|
if (typeof p === 'string') return `'${p.replace(/'/g, "''")}'`;
|
|
160
|
-
return p; //
|
|
159
|
+
return p; // Numbers, booleans, and similar values can be returned directly.
|
|
161
160
|
});
|
|
162
161
|
|
|
163
|
-
// 3.
|
|
162
|
+
// 3. Replace $1, $2, ... placeholders.
|
|
164
163
|
let sql = query;
|
|
165
164
|
for (let i = 0; i < safeValues.length; i++) {
|
|
166
165
|
const placeholder = new RegExp('\\$' + (i + 1) + '(?!\\d)', 'g');
|
|
@@ -176,23 +175,23 @@ function registerDevelopmentQueryLogger(prismaClient: BackendCoreHostPrismaClien
|
|
|
176
175
|
const interpolatedSql = interpolate(event.query, event.params);
|
|
177
176
|
|
|
178
177
|
const clean = interpolatedSql
|
|
179
|
-
.replace(/"[^"]+"\./g, '') //
|
|
180
|
-
.replace(/= '([^']+)'/g, `= '$1'`) //
|
|
181
|
-
.replace(/"/g, ''); //
|
|
178
|
+
.replace(/"[^"]+"\./g, '') // Remove "table". prefixes.
|
|
179
|
+
.replace(/= '([^']+)'/g, `= '$1'`) // Keep normalized quoted values.
|
|
180
|
+
.replace(/"/g, ''); // Remove remaining double quotes.
|
|
182
181
|
|
|
183
182
|
console.log('─'.repeat(60));
|
|
184
183
|
console.log(`Prisma Instance ID: ${instanceId} | Listener ID: ${listenerId}`);
|
|
185
184
|
console.log(`${clean};`);
|
|
186
|
-
console.log(
|
|
185
|
+
console.log(`Duration: ${ms}ms, ${slow}`);
|
|
187
186
|
};
|
|
188
|
-
//
|
|
187
|
+
// Register the wrapped handler.
|
|
189
188
|
prismaClient.$on?.('query' as never, wrappedHandler);
|
|
190
189
|
|
|
191
190
|
globalForPrisma[REGISTERED_KEY] = true;
|
|
192
191
|
}
|
|
193
192
|
}
|
|
194
193
|
|
|
195
|
-
// ====================
|
|
194
|
+
// ==================== Client Helper: fall back to the global non-transaction client when no transaction client is provided ====================
|
|
196
195
|
export function checkAndFallbackWithNonTCClient(tx?: Prisma.TransactionClient): Prisma.TransactionClient | BackendCorePrismaClient {
|
|
197
196
|
return tx ?? getBackendCorePrisma();
|
|
198
197
|
}
|
|
@@ -35,7 +35,7 @@ type BasicOrderContext = {
|
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
type RenewalOrderContext = BasicOrderContext & {
|
|
38
|
-
//
|
|
38
|
+
// Use Stripe invoice price as the source of truth for renewal
|
|
39
39
|
amountPaidCents: number;
|
|
40
40
|
currency: string;
|
|
41
41
|
}
|
|
@@ -64,7 +64,7 @@ class BillingAggregateService {
|
|
|
64
64
|
): Promise<void> {
|
|
65
65
|
await runInTransaction(async (tx) => {
|
|
66
66
|
const now = new Date();
|
|
67
|
-
|
|
67
|
+
// Set subscription expiry to the last second of the expiration date
|
|
68
68
|
const originSubPeriodEnd = context.periodEnd;
|
|
69
69
|
const specialEnd = originSubPeriodEnd ? new Date(originSubPeriodEnd.setHours(23, 59, 59, 999)) : originSubPeriodEnd;
|
|
70
70
|
const updatedSubscription = await subscriptionService.updateSubscription(
|
|
@@ -237,7 +237,7 @@ class BillingAggregateService {
|
|
|
237
237
|
},
|
|
238
238
|
tx
|
|
239
239
|
);
|
|
240
|
-
//
|
|
240
|
+
// Set subscription expiry to the last second of the expiration date
|
|
241
241
|
const originSubPeriodEnd = context.periodEnd;
|
|
242
242
|
const specialEnd = originSubPeriodEnd ? new Date(originSubPeriodEnd.setHours(23, 59, 59, 999)) : originSubPeriodEnd;
|
|
243
243
|
|
|
@@ -365,7 +365,7 @@ class BillingAggregateService {
|
|
|
365
365
|
): Promise<void> {
|
|
366
366
|
await runInTransaction(async (tx) => {
|
|
367
367
|
if (params.isUserCancel) {
|
|
368
|
-
//
|
|
368
|
+
// Record the time when the user unsubscribes
|
|
369
369
|
await transactionService.update(
|
|
370
370
|
params.orderId,
|
|
371
371
|
{
|
|
@@ -391,7 +391,7 @@ class BillingAggregateService {
|
|
|
391
391
|
context: SubscriptionCancelContext
|
|
392
392
|
): Promise<void> {
|
|
393
393
|
await runInTransaction(async (tx) => {
|
|
394
|
-
//
|
|
394
|
+
// Update the order and log cancellation details
|
|
395
395
|
await transactionService.update(
|
|
396
396
|
context.orderId,
|
|
397
397
|
{
|
|
@@ -400,10 +400,10 @@ class BillingAggregateService {
|
|
|
400
400
|
},
|
|
401
401
|
tx
|
|
402
402
|
)
|
|
403
|
-
//
|
|
403
|
+
// Update subscription information
|
|
404
404
|
await subscriptionService.updateStatus(context.subIdKey, SubscriptionStatus.CANCELED, tx);
|
|
405
405
|
|
|
406
|
-
//
|
|
406
|
+
// Clear credits and keep audit trail
|
|
407
407
|
await creditService.purgePaidCredit(context.userId, 'cancel_subscription_purge', context.orderId, tx);
|
|
408
408
|
})
|
|
409
409
|
}
|
|
@@ -40,17 +40,17 @@ export class UserAggregateService {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
43
|
+
* Create a new registered user
|
|
44
44
|
*
|
|
45
|
-
*
|
|
46
|
-
* 1.
|
|
47
|
-
* 2.
|
|
48
|
-
* 3.
|
|
49
|
-
* 4.
|
|
45
|
+
* Initialization steps (parallel to credit):
|
|
46
|
+
* 1. Create User record
|
|
47
|
+
* 2. Initialize Credit record (free credits)
|
|
48
|
+
* 3. Initialize Subscription record (placeholder, status=INCOMPLETE)
|
|
49
|
+
* 4. Record CreditUsage (audit)
|
|
50
50
|
*
|
|
51
|
-
*
|
|
52
|
-
* - session.completed
|
|
53
|
-
* -
|
|
51
|
+
* When the user subscribes via Stripe later:
|
|
52
|
+
* - session.completed or invoice.paid will UPDATE the subscription record
|
|
53
|
+
* - No CREATE needed, only UPDATE to ensure logical consistency
|
|
54
54
|
*/
|
|
55
55
|
async createNewRegisteredUser(
|
|
56
56
|
clerkUserId: string,
|
|
@@ -89,7 +89,7 @@ export class UserAggregateService {
|
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
//
|
|
92
|
+
// Note: Handle credit review logs
|
|
93
93
|
async upgradeToRegistered(
|
|
94
94
|
userId: string,
|
|
95
95
|
email: string,
|
|
@@ -107,9 +107,9 @@ export class UserAggregateService {
|
|
|
107
107
|
tx
|
|
108
108
|
);
|
|
109
109
|
|
|
110
|
-
//
|
|
110
|
+
// Clear anonymous credits first and audit for traceability
|
|
111
111
|
await creditService.purgeFreeCredit(userId, 'user_registered_purge', userId, tx);
|
|
112
|
-
//
|
|
112
|
+
// Then initialize free credits upon successful registration
|
|
113
113
|
const credit = await creditService.initializeCreditWithFree(
|
|
114
114
|
{
|
|
115
115
|
userId: updateUser.userId,
|
|
@@ -128,21 +128,21 @@ export class UserAggregateService {
|
|
|
128
128
|
|
|
129
129
|
async handleUserUnregister(clerkUserId: string): Promise<string | null> {
|
|
130
130
|
return runInTransaction(async (tx) => {
|
|
131
|
-
//
|
|
131
|
+
// query DB user
|
|
132
132
|
const user = await userService.findByClerkUserId(clerkUserId, tx);
|
|
133
133
|
if (!user) {
|
|
134
134
|
console.log(`User with clerkUserId ${clerkUserId} not found`);
|
|
135
135
|
return null;
|
|
136
136
|
}
|
|
137
137
|
const userId = user.userId;
|
|
138
|
-
//
|
|
138
|
+
// Update user status and retain user info (especially FingerprintId) to prevent repeated registration abuse
|
|
139
139
|
await userService.unregister(user.userId, tx);
|
|
140
|
-
//
|
|
140
|
+
// Clear credits
|
|
141
141
|
await creditService.purgeCredit(userId, 'soft_delete_user', userId, tx);
|
|
142
142
|
|
|
143
143
|
const subscription = await subscriptionService.getActiveSubscription(userId, tx);
|
|
144
144
|
if (subscription) {
|
|
145
|
-
//
|
|
145
|
+
// Update subscription info if it exists
|
|
146
146
|
await subscriptionService.cancelSubscription(subscription.id, true, tx);
|
|
147
147
|
}
|
|
148
148
|
|