payment-kit 1.15.21 → 1.15.23
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/api/src/index.ts +2 -1
- package/api/src/libs/audit.ts +4 -1
- package/api/src/libs/context.ts +48 -0
- package/api/src/libs/invoice.ts +2 -2
- package/api/src/libs/middleware.ts +39 -1
- package/api/src/queues/checkout-session.ts +15 -0
- package/api/src/queues/event.ts +13 -4
- package/api/src/queues/invoice.ts +21 -3
- package/api/src/queues/payment.ts +3 -0
- package/api/src/queues/refund.ts +3 -0
- package/api/src/queues/usage-record.ts +4 -0
- package/api/src/queues/webhook.ts +9 -0
- package/api/src/routes/checkout-sessions.ts +40 -2
- package/api/src/routes/donations.ts +5 -1
- package/api/src/routes/events.ts +9 -4
- package/api/src/routes/invoices.ts +24 -24
- package/api/src/routes/payment-links.ts +40 -20
- package/api/src/routes/prices.ts +17 -4
- package/api/src/routes/products.ts +21 -2
- package/api/src/routes/refunds.ts +20 -3
- package/api/src/routes/subscription-items.ts +39 -2
- package/api/src/routes/subscriptions.ts +36 -4
- package/api/src/routes/usage-records.ts +29 -0
- package/api/src/store/models/event.ts +1 -0
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/components/webhook/attempts.tsx +122 -3
package/api/src/index.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { initResourceHandler } from './integrations/blocklet/resource';
|
|
|
19
19
|
import { ensureWebhookRegistered } from './integrations/stripe/setup';
|
|
20
20
|
import { handlers } from './libs/auth';
|
|
21
21
|
import logger, { accessLogStream } from './libs/logger';
|
|
22
|
-
import { ensureI18n } from './libs/middleware';
|
|
22
|
+
import { contextMiddleware, ensureI18n } from './libs/middleware';
|
|
23
23
|
import { initEventBroadcast } from './libs/ws';
|
|
24
24
|
import { startCheckoutSessionQueue } from './queues/checkout-session';
|
|
25
25
|
import { startEventQueue } from './queues/event';
|
|
@@ -93,6 +93,7 @@ if (isProduction) {
|
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
app.use(contextMiddleware);
|
|
96
97
|
app.use(router);
|
|
97
98
|
|
|
98
99
|
if (isProduction) {
|
package/api/src/libs/audit.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { LiteralUnion } from 'type-fest';
|
|
|
4
4
|
import type { EventType } from '../store/models';
|
|
5
5
|
import { Event } from '../store/models/event';
|
|
6
6
|
import { events } from './event';
|
|
7
|
+
import { context } from './context';
|
|
7
8
|
|
|
8
9
|
const API_VERSION = '2023-09-05';
|
|
9
10
|
|
|
@@ -15,7 +16,6 @@ export async function createEvent(scope: string, type: LiteralUnion<EventType, s
|
|
|
15
16
|
data.previous_attributes = pick(model._previousDataValues, options.fields);
|
|
16
17
|
}
|
|
17
18
|
// console.log('createEvent', scope, type, data, options);
|
|
18
|
-
|
|
19
19
|
const event = await Event.create({
|
|
20
20
|
type,
|
|
21
21
|
api_version: API_VERSION,
|
|
@@ -27,6 +27,7 @@ export async function createEvent(scope: string, type: LiteralUnion<EventType, s
|
|
|
27
27
|
// FIXME:
|
|
28
28
|
id: '',
|
|
29
29
|
idempotency_key: '',
|
|
30
|
+
requested_by: options.requestedBy || context.getRequestedBy() || 'system',
|
|
30
31
|
},
|
|
31
32
|
metadata: {},
|
|
32
33
|
pending_webhooks: 99, // force all events goto the event queue
|
|
@@ -69,6 +70,7 @@ export async function createStatusEvent(
|
|
|
69
70
|
// FIXME:
|
|
70
71
|
id: '',
|
|
71
72
|
idempotency_key: '',
|
|
73
|
+
requested_by: options.requestedBy || context.getRequestedBy() || 'system',
|
|
72
74
|
},
|
|
73
75
|
metadata: {},
|
|
74
76
|
pending_webhooks: 99, // force all events goto the event queue
|
|
@@ -107,6 +109,7 @@ export async function createCustomEvent(
|
|
|
107
109
|
// FIXME:
|
|
108
110
|
id: '',
|
|
109
111
|
idempotency_key: '',
|
|
112
|
+
requested_by: options.requestedBy || context.getRequestedBy() || 'system',
|
|
110
113
|
},
|
|
111
114
|
metadata: {},
|
|
112
115
|
pending_webhooks: 99, // force all events goto the event queue
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AsyncLocalStorage, AsyncResource } from 'async_hooks';
|
|
2
|
+
|
|
3
|
+
interface RequestContext {
|
|
4
|
+
requestedBy?: string;
|
|
5
|
+
requestId?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
class RequestContextManager {
|
|
9
|
+
private storage = new AsyncLocalStorage<RequestContext>();
|
|
10
|
+
private contexts = new Map<string, RequestContext>();
|
|
11
|
+
|
|
12
|
+
getContext(requestId?: string): RequestContext {
|
|
13
|
+
if (requestId && this.contexts.has(requestId)) {
|
|
14
|
+
return this.contexts.get(requestId)!;
|
|
15
|
+
}
|
|
16
|
+
return this.storage.getStore() || {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getRequestedBy(requestId?: string): string | undefined {
|
|
20
|
+
return this.getContext(requestId).requestedBy;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
run<T>(context: RequestContext, fn: () => Promise<T> | T): Promise<T> {
|
|
24
|
+
const requestId = context.requestId || `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
25
|
+
|
|
26
|
+
this.contexts.set(requestId, {
|
|
27
|
+
...context,
|
|
28
|
+
requestId,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
this.storage.run({ ...context, requestId }, async () => {
|
|
33
|
+
const resource = new AsyncResource('RequestContext');
|
|
34
|
+
try {
|
|
35
|
+
const result = await resource.runInAsyncScope(fn);
|
|
36
|
+
resolve(result);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
reject(err);
|
|
39
|
+
} finally {
|
|
40
|
+
// 清理上下文
|
|
41
|
+
this.contexts.delete(requestId);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const context = new RequestContextManager();
|
package/api/src/libs/invoice.ts
CHANGED
|
@@ -71,7 +71,7 @@ export async function getOneTimeProductInfo(invoiceId: string, paymentCurrency:
|
|
|
71
71
|
});
|
|
72
72
|
return oneTimePaymentInfo;
|
|
73
73
|
} catch (err) {
|
|
74
|
-
console.error(err);
|
|
74
|
+
console.error(`Error in getOneTimeProductInfo for invoice ${invoiceId}:`, err);
|
|
75
75
|
return [];
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -140,7 +140,7 @@ export async function getInvoiceShouldPayTotal(invoice: Invoice) {
|
|
|
140
140
|
const amount = getSubscriptionCycleAmount(expandedItems, subscription.currency_id);
|
|
141
141
|
return amount?.total || invoice.total;
|
|
142
142
|
} catch (err) {
|
|
143
|
-
console.error(err);
|
|
143
|
+
console.error(`Error in getInvoiceShouldPayTotal for invoice ${invoice.id}:`, err);
|
|
144
144
|
return invoice.total;
|
|
145
145
|
}
|
|
146
146
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* eslint-disable import/prefer-default-export */
|
|
2
2
|
import type { NextFunction, Request, Response } from 'express';
|
|
3
|
-
|
|
3
|
+
import { verify } from '@blocklet/sdk/lib/util/verify-sign';
|
|
4
4
|
import { translate } from '../locales';
|
|
5
|
+
import { context } from './context';
|
|
5
6
|
|
|
6
7
|
export function ensureI18n() {
|
|
7
8
|
return (req: Request, _: Response, next: NextFunction) => {
|
|
@@ -10,3 +11,40 @@ export function ensureI18n() {
|
|
|
10
11
|
next();
|
|
11
12
|
};
|
|
12
13
|
}
|
|
14
|
+
|
|
15
|
+
export function contextMiddleware(req: Request, _res: Response, next: NextFunction) {
|
|
16
|
+
const requestId =
|
|
17
|
+
(req.headers['x-request-id'] as string) || `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
18
|
+
let requestedBy = 'system';
|
|
19
|
+
|
|
20
|
+
// Check component signature
|
|
21
|
+
const sig = req.get('x-component-sig');
|
|
22
|
+
const componentDid = req.get('x-component-did');
|
|
23
|
+
if (sig && componentDid) {
|
|
24
|
+
const data = typeof req.body === 'undefined' ? {} : req.body;
|
|
25
|
+
const verified = verify(data, sig);
|
|
26
|
+
if (verified) {
|
|
27
|
+
requestedBy = componentDid;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check user DID from headers
|
|
32
|
+
if (req.headers['x-user-did']) {
|
|
33
|
+
requestedBy = req.headers['x-user-did'] as string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check authenticated user
|
|
37
|
+
if (req.user?.did) {
|
|
38
|
+
requestedBy = req.user.did;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return context.run(
|
|
42
|
+
{
|
|
43
|
+
requestId,
|
|
44
|
+
requestedBy,
|
|
45
|
+
},
|
|
46
|
+
async () => {
|
|
47
|
+
await next();
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -39,10 +39,15 @@ export const checkoutSessionQueue = createQueue<CheckoutSessionJob>({
|
|
|
39
39
|
export async function handleCheckoutSessionJob(job: CheckoutSessionJob): Promise<void> {
|
|
40
40
|
const checkoutSession = await CheckoutSession.findByPk(job.id);
|
|
41
41
|
if (!checkoutSession) {
|
|
42
|
+
logger.warn('CheckoutSession not found', { id: job.id });
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
45
|
if (job.action === 'expire') {
|
|
45
46
|
if (checkoutSession.status !== 'open') {
|
|
47
|
+
logger.info('Skip expire CheckoutSession since status is not open', {
|
|
48
|
+
checkoutSession: checkoutSession.id,
|
|
49
|
+
status: checkoutSession.status,
|
|
50
|
+
});
|
|
46
51
|
return;
|
|
47
52
|
}
|
|
48
53
|
if (checkoutSession.payment_status === 'paid') {
|
|
@@ -257,15 +262,25 @@ events.on(
|
|
|
257
262
|
async ({ checkoutSessionId, paymentIntentId }: { checkoutSessionId: string; paymentIntentId: string }) => {
|
|
258
263
|
const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
|
|
259
264
|
if (!checkoutSession) {
|
|
265
|
+
logger.warn('CheckoutSession not found for pending invoice', { checkoutSessionId });
|
|
260
266
|
return;
|
|
261
267
|
}
|
|
262
268
|
if (checkoutSession.invoice_id) {
|
|
269
|
+
logger.info('Invoice already exists for checkout session', {
|
|
270
|
+
checkoutSessionId,
|
|
271
|
+
invoiceId: checkoutSession.invoice_id,
|
|
272
|
+
});
|
|
263
273
|
return;
|
|
264
274
|
}
|
|
265
275
|
if (checkoutSession.mode !== 'payment') {
|
|
276
|
+
logger.info('Skipping invoice creation for non-payment mode', {
|
|
277
|
+
checkoutSessionId,
|
|
278
|
+
mode: checkoutSession.mode,
|
|
279
|
+
});
|
|
266
280
|
return;
|
|
267
281
|
}
|
|
268
282
|
if (!checkoutSession.invoice_creation?.enabled) {
|
|
283
|
+
logger.info('Invoice creation not enabled for checkout session', { checkoutSessionId });
|
|
269
284
|
return;
|
|
270
285
|
}
|
|
271
286
|
|
package/api/src/queues/event.ts
CHANGED
|
@@ -14,16 +14,16 @@ type EventJob = {
|
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
export const handleEvent = async (job: EventJob) => {
|
|
17
|
-
logger.info('handle event', job);
|
|
17
|
+
logger.info('Starting to handle event', job);
|
|
18
18
|
|
|
19
19
|
const event = await Event.findByPk(job.eventId);
|
|
20
20
|
if (!event) {
|
|
21
|
-
logger.warn('
|
|
21
|
+
logger.warn('Event not found', job);
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
if (!event.pending_webhooks) {
|
|
26
|
-
logger.warn('
|
|
26
|
+
logger.warn('Event already processed', job);
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -36,6 +36,8 @@ export const handleEvent = async (job: EventJob) => {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
await event.update({ pending_webhooks: eventWebhooks.length });
|
|
39
|
+
logger.info(`Updated event ${event.id} with ${eventWebhooks.length} pending webhooks`);
|
|
40
|
+
|
|
39
41
|
eventWebhooks.forEach(async (webhook) => {
|
|
40
42
|
const attemptCount = await WebhookAttempt.count({
|
|
41
43
|
where: {
|
|
@@ -53,7 +55,7 @@ export const handleEvent = async (job: EventJob) => {
|
|
|
53
55
|
const jobId = getWebhookJobId(event.id, webhook.id);
|
|
54
56
|
const exist = await webhookQueue.get(jobId);
|
|
55
57
|
if (!exist) {
|
|
56
|
-
logger.info(
|
|
58
|
+
logger.info(`Scheduling attempt for event ${event.id} and webhook ${webhook.id}`, job);
|
|
57
59
|
webhookQueue.push({
|
|
58
60
|
id: jobId,
|
|
59
61
|
job: { eventId: event.id, webhookId: webhook.id },
|
|
@@ -61,6 +63,8 @@ export const handleEvent = async (job: EventJob) => {
|
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
});
|
|
66
|
+
|
|
67
|
+
logger.info(`Finished handling event ${job.eventId}`);
|
|
64
68
|
};
|
|
65
69
|
|
|
66
70
|
export const eventQueue = createQueue<EventJob>({
|
|
@@ -80,12 +84,17 @@ export const startEventQueue = async () => {
|
|
|
80
84
|
attributes: ['id'],
|
|
81
85
|
});
|
|
82
86
|
|
|
87
|
+
logger.info(`Found ${docs.length} events with pending webhooks`);
|
|
88
|
+
|
|
83
89
|
docs.forEach(async (x) => {
|
|
84
90
|
const exist = await eventQueue.get(x.id);
|
|
85
91
|
if (!exist) {
|
|
92
|
+
logger.info(`Pushing event ${x.id} to queue`);
|
|
86
93
|
eventQueue.push({ id: x.id, job: { eventId: x.id } });
|
|
87
94
|
}
|
|
88
95
|
});
|
|
96
|
+
|
|
97
|
+
logger.info('Finished starting event queue');
|
|
89
98
|
};
|
|
90
99
|
|
|
91
100
|
eventQueue.on('failed', ({ id, job, error }) => {
|
|
@@ -57,6 +57,7 @@ export const handleInvoice = async (job: InvoiceJob) => {
|
|
|
57
57
|
attempted: true,
|
|
58
58
|
status_transitions: { ...invoice.status_transitions, paid_at: dayjs().unix() },
|
|
59
59
|
});
|
|
60
|
+
logger.info('Invoice updated to paid status', { invoiceId: invoice.id });
|
|
60
61
|
|
|
61
62
|
if (invoice.subscription_id) {
|
|
62
63
|
const subscription = await Subscription.findByPk(invoice.subscription_id);
|
|
@@ -118,6 +119,11 @@ export const handleInvoice = async (job: InvoiceJob) => {
|
|
|
118
119
|
paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
|
|
119
120
|
if (paymentIntent && paymentIntent.isImmutable() === false) {
|
|
120
121
|
await paymentIntent.update({ status: 'requires_capture', customer_id: invoice.customer_id });
|
|
122
|
+
logger.info('PaymentIntent updated for invoice', {
|
|
123
|
+
invoiceId: invoice.id,
|
|
124
|
+
paymentIntentId: paymentIntent.id,
|
|
125
|
+
newStatus: 'requires_capture',
|
|
126
|
+
});
|
|
121
127
|
}
|
|
122
128
|
} else {
|
|
123
129
|
const descriptionMap: any = {
|
|
@@ -149,7 +155,11 @@ export const handleInvoice = async (job: InvoiceJob) => {
|
|
|
149
155
|
metadata: {},
|
|
150
156
|
});
|
|
151
157
|
await invoice.update({ payment_intent_id: paymentIntent.id });
|
|
152
|
-
logger.info('PaymentIntent created for invoice', {
|
|
158
|
+
logger.info('PaymentIntent created for invoice', {
|
|
159
|
+
invoiceId: invoice.id,
|
|
160
|
+
paymentIntentId: paymentIntent.id,
|
|
161
|
+
amount: paymentIntent.amount,
|
|
162
|
+
});
|
|
153
163
|
|
|
154
164
|
if (invoice.checkout_session_id) {
|
|
155
165
|
const checkoutSession = await CheckoutSession.findByPk(invoice.checkout_session_id);
|
|
@@ -246,15 +256,23 @@ invoiceQueue.on('failed', ({ id, job, error }) => {
|
|
|
246
256
|
events.on('invoice.paid', async ({ id: invoiceId }) => {
|
|
247
257
|
const invoice = await Invoice.findByPk(invoiceId);
|
|
248
258
|
if (!invoice) {
|
|
249
|
-
logger.error('Invoice not found', { invoiceId });
|
|
259
|
+
logger.error('Invoice not found for paid event', { invoiceId });
|
|
250
260
|
return;
|
|
251
261
|
}
|
|
262
|
+
logger.info('Processing paid invoice', { invoiceId, billingReason: invoice.billing_reason });
|
|
263
|
+
|
|
252
264
|
const checkBillingReason = ['subscription_cycle', 'subscription_cancel'];
|
|
253
265
|
if (checkBillingReason.includes(invoice.billing_reason)) {
|
|
254
266
|
const shouldPayTotal = await getInvoiceShouldPayTotal(invoice);
|
|
255
267
|
if (shouldPayTotal !== invoice.total) {
|
|
256
268
|
createEvent('Invoice', 'billing.discrepancy', invoice);
|
|
257
|
-
logger.
|
|
269
|
+
logger.warn('Billing discrepancy detected', {
|
|
270
|
+
invoiceId,
|
|
271
|
+
shouldPayTotal,
|
|
272
|
+
invoiceTotal: invoice.total,
|
|
273
|
+
});
|
|
274
|
+
} else {
|
|
275
|
+
logger.info('Invoice paid successfully with correct amount', { invoiceId, total: invoice.total });
|
|
258
276
|
}
|
|
259
277
|
}
|
|
260
278
|
});
|
|
@@ -577,6 +577,9 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
577
577
|
let result;
|
|
578
578
|
try {
|
|
579
579
|
await paymentIntent.update({ status: 'processing', last_payment_error: null });
|
|
580
|
+
logger.info('PaymentIntent status updated to processing', {
|
|
581
|
+
paymentIntentId: paymentIntent.id,
|
|
582
|
+
});
|
|
580
583
|
if (paymentMethod.type === 'arcblock') {
|
|
581
584
|
if (invoice?.billing_reason === 'slash_stake') {
|
|
582
585
|
await handleStakeSlash(invoice, paymentIntent, paymentMethod, customer, paymentCurrency);
|
package/api/src/queues/refund.ts
CHANGED
|
@@ -175,6 +175,7 @@ const handleRefundJob = async (
|
|
|
175
175
|
},
|
|
176
176
|
},
|
|
177
177
|
});
|
|
178
|
+
logger.info('Refund status updated to succeeded', { id: refund.id, txHash });
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
if (paymentMethod.type === 'ethereum') {
|
|
@@ -376,6 +377,7 @@ const handleStakeReturnJob = async (
|
|
|
376
377
|
},
|
|
377
378
|
},
|
|
378
379
|
});
|
|
380
|
+
logger.info('Stake return refund status updated to succeeded', { id: refund.id, txHash });
|
|
379
381
|
}
|
|
380
382
|
} catch (err: any) {
|
|
381
383
|
logger.error('stake return failed', { error: err, id: refund.id });
|
|
@@ -423,6 +425,7 @@ export const startRefundQueue = async () => {
|
|
|
423
425
|
const exist = await refundQueue.get(x.id);
|
|
424
426
|
if (!exist) {
|
|
425
427
|
refundQueue.push({ id: x.id, job: { refundId: x.id } });
|
|
428
|
+
logger.info('Re-queued pending refund', { id: x.id });
|
|
426
429
|
}
|
|
427
430
|
});
|
|
428
431
|
};
|
|
@@ -27,8 +27,10 @@ type UsageRecordJob = {
|
|
|
27
27
|
export async function handleUsageRecord(job: UsageRecordJob) {
|
|
28
28
|
const lock = getLock(`${job.subscriptionId}-threshold`);
|
|
29
29
|
await lock.acquire();
|
|
30
|
+
logger.info(`Lock acquired for subscription ${job.subscriptionId}`);
|
|
30
31
|
const result = await doHandleUsageRecord(job);
|
|
31
32
|
lock.release();
|
|
33
|
+
logger.info(`Lock released for subscription ${job.subscriptionId}`);
|
|
32
34
|
return result;
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -148,6 +150,8 @@ export const doHandleUsageRecord = async (job: UsageRecordJob) => {
|
|
|
148
150
|
await subscription.update({ latest_invoice_id: invoice.id });
|
|
149
151
|
logger.info(`Subscription updated on threshold: ${subscription.id}`);
|
|
150
152
|
}
|
|
153
|
+
|
|
154
|
+
logger.info(`Usage record handling completed for subscription ${subscription.id}`);
|
|
151
155
|
};
|
|
152
156
|
|
|
153
157
|
export const usageRecordQueue = createQueue<UsageRecordJob>({
|
|
@@ -77,8 +77,11 @@ export const handleWebhook = async (job: WebhookJob) => {
|
|
|
77
77
|
response_body: result.data || {},
|
|
78
78
|
retry_count: retryCount,
|
|
79
79
|
});
|
|
80
|
+
logger.info('WebhookAttempt created successfully', { eventId: event.id, webhookId: webhook.id });
|
|
80
81
|
|
|
81
82
|
await event.decrement('pending_webhooks');
|
|
83
|
+
logger.info('pending_webhooks decremented', { eventId: event.id, newCount: event.pending_webhooks });
|
|
84
|
+
|
|
82
85
|
logger.info('webhook attempt success', { ...job, retryCount });
|
|
83
86
|
} catch (err: any) {
|
|
84
87
|
logger.warn('webhook attempt error', { ...job, retryCount, message: err.message });
|
|
@@ -91,6 +94,7 @@ export const handleWebhook = async (job: WebhookJob) => {
|
|
|
91
94
|
response_body: (err as AxiosError).response?.data || {},
|
|
92
95
|
retry_count: retryCount,
|
|
93
96
|
});
|
|
97
|
+
logger.info('Failed WebhookAttempt created', { eventId: event.id, webhookId: webhook.id });
|
|
94
98
|
|
|
95
99
|
// reschedule next attempt
|
|
96
100
|
if (retryCount < MAX_RETRY_COUNT) {
|
|
@@ -100,9 +104,14 @@ export const handleWebhook = async (job: WebhookJob) => {
|
|
|
100
104
|
job: { eventId: event.id, webhookId: webhook.id },
|
|
101
105
|
runAt: getNextRetry(retryCount),
|
|
102
106
|
});
|
|
107
|
+
logger.info('scheduled webhook job', { ...job, retryCount });
|
|
103
108
|
});
|
|
104
109
|
} else {
|
|
105
110
|
await event.decrement('pending_webhooks');
|
|
111
|
+
logger.info('Max retries reached, pending_webhooks decremented', {
|
|
112
|
+
eventId: event.id,
|
|
113
|
+
newCount: event.pending_webhooks,
|
|
114
|
+
});
|
|
106
115
|
}
|
|
107
116
|
}
|
|
108
117
|
};
|
|
@@ -512,6 +512,10 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
512
512
|
|
|
513
513
|
// start checkout session from payment link
|
|
514
514
|
router.post('/start/:id', user, async (req, res) => {
|
|
515
|
+
logger.info('Starting checkout session from payment link', {
|
|
516
|
+
paymentLinkId: req.params.id,
|
|
517
|
+
userId: req.user?.did,
|
|
518
|
+
});
|
|
515
519
|
await startCheckoutSessionFromPaymentLink(req.params.id as string, req, res);
|
|
516
520
|
});
|
|
517
521
|
|
|
@@ -1010,6 +1014,10 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1010
1014
|
trialInDays,
|
|
1011
1015
|
trialEnd
|
|
1012
1016
|
);
|
|
1017
|
+
logger.info('ensureStripeSubscription', {
|
|
1018
|
+
subscriptionId: subscription.id,
|
|
1019
|
+
stripeSubscriptionId: stripeSubscription?.id,
|
|
1020
|
+
});
|
|
1013
1021
|
if (stripeSubscription && subscription?.payment_details?.stripe?.subscription_id === stripeSubscription.id) {
|
|
1014
1022
|
if (['active', 'trialing'].includes(stripeSubscription.status) && subscription.status === 'incomplete') {
|
|
1015
1023
|
await handleStripeSubscriptionSucceed(subscription, stripeSubscription.status);
|
|
@@ -1028,6 +1036,14 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1028
1036
|
}
|
|
1029
1037
|
}
|
|
1030
1038
|
|
|
1039
|
+
logger.info('Checkout session submitted successfully', {
|
|
1040
|
+
sessionId: req.params.id,
|
|
1041
|
+
paymentIntentId: paymentIntent?.id,
|
|
1042
|
+
setupIntentId: setupIntent?.id,
|
|
1043
|
+
subscriptionId: subscription?.id,
|
|
1044
|
+
customerId: customer?.id,
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1031
1047
|
return res.json({
|
|
1032
1048
|
paymentIntent,
|
|
1033
1049
|
setupIntent,
|
|
@@ -1039,7 +1055,11 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1039
1055
|
balance: checkoutSession.mode === 'payment' && canFastPay ? balance : null,
|
|
1040
1056
|
});
|
|
1041
1057
|
} catch (err) {
|
|
1042
|
-
|
|
1058
|
+
logger.error('Error submitting checkout session', {
|
|
1059
|
+
sessionId: req.params.id,
|
|
1060
|
+
error: err.message,
|
|
1061
|
+
stack: err.stack,
|
|
1062
|
+
});
|
|
1043
1063
|
res.status(500).json({ code: err.code, error: err.message });
|
|
1044
1064
|
}
|
|
1045
1065
|
});
|
|
@@ -1118,9 +1138,18 @@ router.put('/:id/downsell', user, ensureCheckoutSessionOpen, async (req, res) =>
|
|
|
1118
1138
|
}
|
|
1119
1139
|
|
|
1120
1140
|
const items = await Price.expand(checkoutSession.line_items, { upsell: true });
|
|
1141
|
+
logger.info('Checkout session updated after downsell', {
|
|
1142
|
+
sessionId: req.params.id,
|
|
1143
|
+
fromPriceId: from.id,
|
|
1144
|
+
newAmount: checkoutSession.amount_total,
|
|
1145
|
+
});
|
|
1121
1146
|
res.json({ ...checkoutSession.toJSON(), line_items: items });
|
|
1122
1147
|
} catch (err) {
|
|
1123
|
-
|
|
1148
|
+
logger.error('Error processing downsell', {
|
|
1149
|
+
sessionId: req.params.id,
|
|
1150
|
+
error: err.message,
|
|
1151
|
+
stack: err.stack,
|
|
1152
|
+
});
|
|
1124
1153
|
res.status(500).json({ error: err.message });
|
|
1125
1154
|
}
|
|
1126
1155
|
});
|
|
@@ -1140,6 +1169,11 @@ router.put('/:id/expire', auth, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1140
1169
|
}
|
|
1141
1170
|
|
|
1142
1171
|
await doc.update({ status: 'expired', expires_at: dayjs().unix() });
|
|
1172
|
+
logger.info('Checkout session expired', {
|
|
1173
|
+
sessionId: req.params.id,
|
|
1174
|
+
userId: req.user?.did,
|
|
1175
|
+
expiresAt: doc.expires_at,
|
|
1176
|
+
});
|
|
1143
1177
|
|
|
1144
1178
|
res.json(doc);
|
|
1145
1179
|
});
|
|
@@ -1413,6 +1447,10 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
1413
1447
|
}
|
|
1414
1448
|
|
|
1415
1449
|
await doc.update(raw);
|
|
1450
|
+
logger.info('Checkout session updated', {
|
|
1451
|
+
sessionId: doc.id,
|
|
1452
|
+
updatedFields: Object.keys(raw),
|
|
1453
|
+
});
|
|
1416
1454
|
res.json(doc);
|
|
1417
1455
|
});
|
|
1418
1456
|
|
|
@@ -61,12 +61,15 @@ router.post('/', async (req, res) => {
|
|
|
61
61
|
},
|
|
62
62
|
},
|
|
63
63
|
});
|
|
64
|
+
logger.info('Payment link updated successfully', { linkId: link.id });
|
|
64
65
|
res.json(link.toJSON());
|
|
65
66
|
return;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
logger.info('No existing payment link found, creating new one');
|
|
68
70
|
let price = await Price.findByPkOrLookupKey(payload.target);
|
|
69
71
|
if (!price) {
|
|
72
|
+
logger.info('No existing price found, creating new product and price');
|
|
70
73
|
const result = await createProductAndPrices({
|
|
71
74
|
type: 'service',
|
|
72
75
|
livemode: req.livemode,
|
|
@@ -109,9 +112,10 @@ router.post('/', async (req, res) => {
|
|
|
109
112
|
},
|
|
110
113
|
},
|
|
111
114
|
});
|
|
115
|
+
logger.info('New payment link created', { linkId: result.id });
|
|
112
116
|
res.json(result);
|
|
113
117
|
} catch (err) {
|
|
114
|
-
logger.error('prepare payment link for donation', err);
|
|
118
|
+
logger.error('Failed to prepare payment link for donation', err);
|
|
115
119
|
res.status(400).json({ error: err.message });
|
|
116
120
|
}
|
|
117
121
|
});
|
package/api/src/routes/events.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { WhereOptions } from 'sequelize';
|
|
|
5
5
|
import { createListParamSchema } from '../libs/api';
|
|
6
6
|
import { authenticate } from '../libs/security';
|
|
7
7
|
import { Event } from '../store/models/event';
|
|
8
|
+
import { blocklet } from '../libs/auth';
|
|
8
9
|
|
|
9
10
|
const router = Router();
|
|
10
11
|
const auth = authenticate<Event>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -61,13 +62,17 @@ router.get('/:id', auth, async (req, res) => {
|
|
|
61
62
|
});
|
|
62
63
|
|
|
63
64
|
if (doc) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
const requestedBy = doc.request?.requested_by || 'system';
|
|
66
|
+
const { user } = await blocklet.getUser(requestedBy as string);
|
|
67
|
+
if (user) {
|
|
68
|
+
return res.json({ ...doc.toJSON(), requestInfo: user });
|
|
69
|
+
}
|
|
70
|
+
return res.json(doc);
|
|
67
71
|
}
|
|
72
|
+
return res.status(404).json(null);
|
|
68
73
|
} catch (err) {
|
|
69
74
|
console.error(err);
|
|
70
|
-
res.status(
|
|
75
|
+
return res.status(400).json({ error: `Failed to get event: ${err.message}` });
|
|
71
76
|
}
|
|
72
77
|
});
|
|
73
78
|
|
|
@@ -10,7 +10,7 @@ import { createListParamSchema, getWhereFromKvQuery, MetadataSchema } from '../l
|
|
|
10
10
|
import { authenticate } from '../libs/security';
|
|
11
11
|
import { expandLineItems } from '../libs/session';
|
|
12
12
|
import { formatMetadata } from '../libs/util';
|
|
13
|
-
import { Refund } from '../store/models';
|
|
13
|
+
import { Refund, SetupIntent } from '../store/models';
|
|
14
14
|
import { Customer } from '../store/models/customer';
|
|
15
15
|
import { Invoice } from '../store/models/invoice';
|
|
16
16
|
import { InvoiceItem } from '../store/models/invoice-item';
|
|
@@ -131,23 +131,27 @@ router.get('/', authMine, async (req, res) => {
|
|
|
131
131
|
subscription = await Subscription.findByPk(query.subscription_id);
|
|
132
132
|
if (subscription?.payment_details?.arcblock?.staking?.tx_hash) {
|
|
133
133
|
const method = await PaymentMethod.findOne({ where: { type: 'arcblock', livemode: subscription.livemode } });
|
|
134
|
-
|
|
134
|
+
const setup = await SetupIntent.findOne({
|
|
135
|
+
where: {
|
|
136
|
+
customer_id: subscription.customer_id,
|
|
137
|
+
payment_method_id: method?.id,
|
|
138
|
+
metadata: { subscription_id: subscription.id },
|
|
139
|
+
},
|
|
140
|
+
order: [['created_at', 'ASC']],
|
|
141
|
+
});
|
|
142
|
+
const currencyId = setup?.currency_id || subscription.currency_id;
|
|
143
|
+
const currency = await PaymentCurrency.findByPk(currencyId);
|
|
144
|
+
if (method && currency) {
|
|
135
145
|
const { address } = subscription.payment_details.arcblock.staking;
|
|
136
146
|
const firstInvoice = await Invoice.findOne({
|
|
137
|
-
where: { subscription_id: subscription.id },
|
|
147
|
+
where: { subscription_id: subscription.id, currency_id: currencyId },
|
|
138
148
|
order: [['created_at', 'ASC']],
|
|
139
149
|
include: [{ model: PaymentCurrency, as: 'paymentCurrency' }],
|
|
140
150
|
});
|
|
141
|
-
|
|
142
|
-
if (subscription.payment_details.arcblock.staking.tx_hash && firstInvoice) {
|
|
151
|
+
if (firstInvoice) {
|
|
143
152
|
const customer = await Customer.findByPk(firstInvoice.customer_id);
|
|
144
|
-
const currency =
|
|
145
|
-
// @ts-ignore
|
|
146
|
-
firstInvoice?.paymentCurrency ||
|
|
147
|
-
(await PaymentCurrency.findOne({
|
|
148
|
-
where: { payment_method_id: method.id, is_base_currency: true },
|
|
149
|
-
}));
|
|
150
153
|
const stakeAmountResult = await getSubscriptionStakeAmountSetup(subscription, method);
|
|
154
|
+
// @ts-ignore
|
|
151
155
|
const stakeAmount = stakeAmountResult?.[currency?.contract] || '0';
|
|
152
156
|
|
|
153
157
|
list.push({
|
|
@@ -187,7 +191,6 @@ router.get('/', authMine, async (req, res) => {
|
|
|
187
191
|
const stakeRefundRecord = await Refund.findOne({
|
|
188
192
|
where: { subscription_id: subscription.id, status: 'succeeded', type: 'stake_return' },
|
|
189
193
|
});
|
|
190
|
-
|
|
191
194
|
if (stakeRefundRecord) {
|
|
192
195
|
list.unshift({
|
|
193
196
|
id: address as string,
|
|
@@ -198,18 +201,15 @@ router.get('/', authMine, async (req, res) => {
|
|
|
198
201
|
amount_due: '0',
|
|
199
202
|
amount_paid: stakeRefundRecord.amount,
|
|
200
203
|
amount_remaining: '0',
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
'created_at',
|
|
211
|
-
'updated_at',
|
|
212
|
-
]),
|
|
204
|
+
created_at: stakeRefundRecord.created_at,
|
|
205
|
+
updated_at: stakeRefundRecord.updated_at,
|
|
206
|
+
currency_id: stakeRefundRecord.currency_id,
|
|
207
|
+
customer_id: stakeRefundRecord.customer_id,
|
|
208
|
+
subscription_id: subscription.id,
|
|
209
|
+
period_start: subscription.current_period_start,
|
|
210
|
+
period_end: subscription.current_period_end,
|
|
211
|
+
paid: true,
|
|
212
|
+
...pick(firstInvoice, ['number', 'auto_advance']),
|
|
213
213
|
// @ts-ignore
|
|
214
214
|
paymentCurrency: currency,
|
|
215
215
|
paymentMethod: method,
|