payment-kit 1.15.21 → 1.15.22
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/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
|
|
|
@@ -194,6 +194,7 @@ router.post('/', auth, async (req, res) => {
|
|
|
194
194
|
try {
|
|
195
195
|
const { error } = PaymentLinkCreateSchema.validate(req.body);
|
|
196
196
|
if (error) {
|
|
197
|
+
logger.warn('Payment link create request invalid', { error: error.message, body: req.body });
|
|
197
198
|
res.status(400).json({ error: `Payment link create request invalid: ${error.message}` });
|
|
198
199
|
return;
|
|
199
200
|
}
|
|
@@ -204,9 +205,10 @@ router.post('/', auth, async (req, res) => {
|
|
|
204
205
|
currency_id: req.body.currency_id || req.currency.id,
|
|
205
206
|
metadata: formatMetadata(req.body.metadata),
|
|
206
207
|
});
|
|
208
|
+
logger.info('Payment link created successfully', { id: result.id, user: req.user?.did });
|
|
207
209
|
res.json(result);
|
|
208
210
|
} catch (err) {
|
|
209
|
-
logger.error('
|
|
211
|
+
logger.error('Create payment link error', { error: err.message, stack: err.stack, body: req.body });
|
|
210
212
|
res.status(400).json({ error: err.message });
|
|
211
213
|
}
|
|
212
214
|
});
|
|
@@ -321,23 +323,33 @@ const PaymentLinkUpdateSchema = Joi.object({
|
|
|
321
323
|
router.put('/:id', auth, async (req, res) => {
|
|
322
324
|
const { error } = PaymentLinkUpdateSchema.validate(req.body);
|
|
323
325
|
if (error) {
|
|
326
|
+
logger.warn('Payment link update request invalid', { error: error.message, id: req.params.id, body: req.body });
|
|
324
327
|
return res.status(400).json({ error: `Payment link update request invalid: ${error.message}` });
|
|
325
328
|
}
|
|
326
329
|
const doc = await PaymentLink.findByPk(req.params.id);
|
|
327
330
|
|
|
328
331
|
if (!doc) {
|
|
332
|
+
logger.warn('Payment link not found for update', { id: req.params.id });
|
|
329
333
|
return res.status(404).json({ error: 'payment link not found' });
|
|
330
334
|
}
|
|
331
335
|
if (doc.active === false) {
|
|
336
|
+
logger.warn('Attempt to update archived payment link', { id: req.params.id });
|
|
332
337
|
return res.status(403).json({ error: 'payment link archived' });
|
|
333
338
|
}
|
|
334
|
-
// if (doc.locked) {
|
|
335
|
-
// return res.status(403).json({ error: 'payment link locked' });
|
|
336
|
-
// }
|
|
337
339
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
340
|
+
try {
|
|
341
|
+
await doc.update(formatBeforeSave(Object.assign({}, doc.dataValues, req.body)));
|
|
342
|
+
logger.info('Payment link updated successfully', { id: req.params.id, user: req.user?.did });
|
|
343
|
+
res.json(doc);
|
|
344
|
+
} catch (err) {
|
|
345
|
+
logger.error('Update payment link error', {
|
|
346
|
+
error: err.message,
|
|
347
|
+
stack: err.stack,
|
|
348
|
+
id: req.params.id,
|
|
349
|
+
body: req.body,
|
|
350
|
+
});
|
|
351
|
+
res.status(500).json({ error: 'Failed to update payment link' });
|
|
352
|
+
}
|
|
341
353
|
});
|
|
342
354
|
|
|
343
355
|
// archive
|
|
@@ -345,19 +357,23 @@ router.put('/:id/archive', auth, async (req, res) => {
|
|
|
345
357
|
const doc = await PaymentLink.findByPk(req.params.id);
|
|
346
358
|
|
|
347
359
|
if (!doc) {
|
|
360
|
+
logger.warn('Payment link not found for archiving', { id: req.params.id });
|
|
348
361
|
return res.status(404).json({ error: 'payment link not found' });
|
|
349
362
|
}
|
|
350
363
|
|
|
351
364
|
if (doc.active === false) {
|
|
365
|
+
logger.warn('Attempt to archive already archived payment link', { id: req.params.id });
|
|
352
366
|
return res.status(403).json({ error: 'payment link already archived' });
|
|
353
367
|
}
|
|
354
368
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
369
|
+
try {
|
|
370
|
+
await doc.update({ active: false });
|
|
371
|
+
logger.info('Payment link archived successfully', { id: req.params.id, user: req.user?.did });
|
|
372
|
+
return res.json(doc);
|
|
373
|
+
} catch (err) {
|
|
374
|
+
logger.error('Archive payment link error', { error: err.message, stack: err.stack, id: req.params.id });
|
|
375
|
+
return res.status(500).json({ error: 'Failed to archive payment link' });
|
|
376
|
+
}
|
|
361
377
|
});
|
|
362
378
|
|
|
363
379
|
// delete
|
|
@@ -372,12 +388,14 @@ router.delete('/:id', auth, async (req, res) => {
|
|
|
372
388
|
return res.status(403).json({ error: 'payment link archived' });
|
|
373
389
|
}
|
|
374
390
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
391
|
+
try {
|
|
392
|
+
await doc.destroy();
|
|
393
|
+
logger.info('Payment link deleted successfully', { id: req.params.id, user: req.user?.did });
|
|
394
|
+
return res.json(doc);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
logger.error('Delete payment link error', { error: err.message, stack: err.stack, id: req.params.id });
|
|
397
|
+
return res.status(500).json({ error: 'Failed to delete payment link' });
|
|
398
|
+
}
|
|
381
399
|
});
|
|
382
400
|
|
|
383
401
|
router.post('/stash', auth, async (req, res) => {
|
|
@@ -393,12 +411,14 @@ router.post('/stash', auth, async (req, res) => {
|
|
|
393
411
|
let doc = await PaymentLink.findByPk(raw.id);
|
|
394
412
|
if (doc) {
|
|
395
413
|
await doc.update({ ...formatBeforeSave(req.body), livemode: raw.livemode });
|
|
414
|
+
logger.info('Stashed payment link updated', { id: raw.id, user: req.user?.did });
|
|
396
415
|
} else {
|
|
397
416
|
doc = await PaymentLink.create(raw as PaymentLink);
|
|
417
|
+
logger.info('New stashed payment link created', { id: raw.id, user: req.user?.did });
|
|
398
418
|
}
|
|
399
419
|
res.json(doc);
|
|
400
420
|
} catch (err) {
|
|
401
|
-
|
|
421
|
+
logger.error('Stash payment link error', { error: err.message, stack: err.stack, body: req.body });
|
|
402
422
|
res.status(500).json({ error: err.message });
|
|
403
423
|
}
|
|
404
424
|
});
|
package/api/src/routes/prices.ts
CHANGED
|
@@ -217,9 +217,10 @@ router.post('/', auth, async (req, res) => {
|
|
|
217
217
|
quantity_sold: 0,
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
+
logger.info(`Price created: ${result?.id}`, { priceId: result?.id, requestedBy: req.user?.did });
|
|
220
221
|
res.json(result);
|
|
221
222
|
} catch (err) {
|
|
222
|
-
|
|
223
|
+
logger.error('Error creating price', { error: err.message, request: req.body });
|
|
223
224
|
res.status(400).json({ error: err.message });
|
|
224
225
|
}
|
|
225
226
|
});
|
|
@@ -346,9 +347,14 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
346
347
|
}
|
|
347
348
|
}
|
|
348
349
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
350
|
+
try {
|
|
351
|
+
await doc.update(Price.formatBeforeSave(updates));
|
|
352
|
+
logger.info(`Price updated: ${req.params.id}`, { priceId: req.params.id, updates, requestedBy: req.user?.did });
|
|
353
|
+
return res.json(await getExpandedPrice(req.params.id as string));
|
|
354
|
+
} catch (err) {
|
|
355
|
+
logger.error('Error updating price', { error: err.message, request: req.body });
|
|
356
|
+
return res.status(400).json({ error: err.message });
|
|
357
|
+
}
|
|
352
358
|
});
|
|
353
359
|
|
|
354
360
|
// archive
|
|
@@ -369,6 +375,7 @@ router.put('/:id/archive', auth, async (req, res) => {
|
|
|
369
375
|
|
|
370
376
|
await price.update({ active: false });
|
|
371
377
|
|
|
378
|
+
logger.info(`Price archived: ${req.params.id}`, { priceId: req.params.id, requestedBy: req.user?.did });
|
|
372
379
|
return res.json(await getExpandedPrice(req.params.id as string));
|
|
373
380
|
});
|
|
374
381
|
|
|
@@ -431,6 +438,12 @@ router.put('/:id/inventory', auth, async (req, res) => {
|
|
|
431
438
|
}
|
|
432
439
|
await price.increment('quantity_sold', { by: req.body.quantity });
|
|
433
440
|
}
|
|
441
|
+
logger.info(`Price inventory updated: ${req.params.id}`, {
|
|
442
|
+
priceId: req.params.id,
|
|
443
|
+
action: req.body.action,
|
|
444
|
+
quantity: req.body.quantity,
|
|
445
|
+
requestedBy: req.user?.did,
|
|
446
|
+
});
|
|
434
447
|
return res.json(await getExpandedPrice(req.params.id as string));
|
|
435
448
|
} catch (err) {
|
|
436
449
|
logger.error('update price inventory error', err);
|
|
@@ -161,6 +161,12 @@ router.post('/', auth, async (req, res) => {
|
|
|
161
161
|
currency_id: req.currency.id,
|
|
162
162
|
metadata: formatMetadata(req.body.metadata),
|
|
163
163
|
});
|
|
164
|
+
logger.info('Product and prices created', {
|
|
165
|
+
productId: result.id,
|
|
166
|
+
name: result.name,
|
|
167
|
+
priceCount: result.prices.length,
|
|
168
|
+
requestedBy: req.user?.did,
|
|
169
|
+
});
|
|
164
170
|
res.json(result);
|
|
165
171
|
} catch (err) {
|
|
166
172
|
logger.error('create product error', err);
|
|
@@ -300,7 +306,11 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
300
306
|
updates.metadata = formatMetadata(updates.metadata);
|
|
301
307
|
}
|
|
302
308
|
await product.update(updates);
|
|
303
|
-
|
|
309
|
+
logger.info('Product updated', {
|
|
310
|
+
productId: product.id,
|
|
311
|
+
updatedFields: Object.keys(updates),
|
|
312
|
+
requestedBy: req.user?.did,
|
|
313
|
+
});
|
|
304
314
|
return res.json(await Product.expand(req.params.id as string));
|
|
305
315
|
});
|
|
306
316
|
|
|
@@ -346,7 +356,10 @@ router.delete('/:id', auth, async (req, res) => {
|
|
|
346
356
|
|
|
347
357
|
await product.destroy();
|
|
348
358
|
await Price.destroy({ where: { product_id: product.id } });
|
|
349
|
-
|
|
359
|
+
logger.info('Product and associated prices deleted', {
|
|
360
|
+
productId: product.id,
|
|
361
|
+
requestedBy: req.user?.did,
|
|
362
|
+
});
|
|
350
363
|
return res.json(product);
|
|
351
364
|
} catch (err) {
|
|
352
365
|
logger.error('delete product error', err);
|
|
@@ -415,6 +428,12 @@ router.post('/batch-price-update', auth, async (req, res) => {
|
|
|
415
428
|
})
|
|
416
429
|
);
|
|
417
430
|
|
|
431
|
+
logger.info('Batch price update completed', {
|
|
432
|
+
updatedCount: updated.length,
|
|
433
|
+
dryRun,
|
|
434
|
+
factor,
|
|
435
|
+
requestedBy: req.user?.did,
|
|
436
|
+
});
|
|
418
437
|
return res.json(updated);
|
|
419
438
|
} catch (err) {
|
|
420
439
|
logger.error('batch price update error', err);
|
|
@@ -169,10 +169,16 @@ router.post('/', authAdmin, async (req, res) => {
|
|
|
169
169
|
...req.params,
|
|
170
170
|
...req.body,
|
|
171
171
|
result: item.toJSON(),
|
|
172
|
+
requestedBy: req.user?.did,
|
|
172
173
|
});
|
|
173
174
|
res.json(item);
|
|
174
175
|
} catch (err) {
|
|
175
|
-
logger.error('
|
|
176
|
+
logger.error('Create refund failed', {
|
|
177
|
+
error: err.message,
|
|
178
|
+
stack: err.stack,
|
|
179
|
+
requestBody: req.body,
|
|
180
|
+
requestedBy: req.user?.did,
|
|
181
|
+
});
|
|
176
182
|
res.status(400).json({ error: err.message });
|
|
177
183
|
}
|
|
178
184
|
});
|
|
@@ -241,10 +247,21 @@ router.put('/:id', authAdmin, async (req, res) => {
|
|
|
241
247
|
}
|
|
242
248
|
|
|
243
249
|
await doc.update(raw);
|
|
250
|
+
logger.info('Refund updated', {
|
|
251
|
+
refundId: doc.id,
|
|
252
|
+
updatedFields: Object.keys(raw),
|
|
253
|
+
requestedBy: req.user?.did,
|
|
254
|
+
});
|
|
244
255
|
res.json(doc);
|
|
245
256
|
} catch (err) {
|
|
246
|
-
|
|
247
|
-
|
|
257
|
+
logger.error('Update refund failed', {
|
|
258
|
+
refundId: req.params.id,
|
|
259
|
+
error: err.message,
|
|
260
|
+
stack: err.stack,
|
|
261
|
+
requestBody: req.body,
|
|
262
|
+
requestedBy: req.user?.did,
|
|
263
|
+
});
|
|
264
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
248
265
|
}
|
|
249
266
|
});
|
|
250
267
|
|
|
@@ -11,6 +11,7 @@ import { formatMetadata } from '../libs/util';
|
|
|
11
11
|
import { Price, Product, Subscription, SubscriptionItem, UsageRecord } from '../store/models';
|
|
12
12
|
import { forwardUsageRecordToStripe } from '../integrations/stripe/resource';
|
|
13
13
|
import { usageRecordQueue } from '../queues/usage-record';
|
|
14
|
+
import logger from '../libs/logger';
|
|
14
15
|
|
|
15
16
|
const router = Router();
|
|
16
17
|
const auth = authenticate<SubscriptionItem>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -42,6 +43,13 @@ router.post('/', auth, async (req, res) => {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
const doc = await SubscriptionItem.create(raw as SubscriptionItem);
|
|
46
|
+
logger.info('SubscriptionItem created', {
|
|
47
|
+
id: doc.id,
|
|
48
|
+
subscriptionId: doc.subscription_id,
|
|
49
|
+
priceId: doc.price_id,
|
|
50
|
+
quantity: doc.quantity,
|
|
51
|
+
requestedBy: req.user?.did,
|
|
52
|
+
});
|
|
45
53
|
return res.json(doc);
|
|
46
54
|
});
|
|
47
55
|
|
|
@@ -135,7 +143,12 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
135
143
|
}
|
|
136
144
|
|
|
137
145
|
await doc.update(updates);
|
|
138
|
-
|
|
146
|
+
logger.info('SubscriptionItem updated', {
|
|
147
|
+
id: doc.id,
|
|
148
|
+
subscriptionId: doc.subscription_id,
|
|
149
|
+
updatedFields: Object.keys(updates),
|
|
150
|
+
requestedBy: req.user?.did,
|
|
151
|
+
});
|
|
139
152
|
return res.json(doc);
|
|
140
153
|
});
|
|
141
154
|
|
|
@@ -156,7 +169,12 @@ router.delete('/:id', auth, async (req, res) => {
|
|
|
156
169
|
}
|
|
157
170
|
|
|
158
171
|
await doc.destroy();
|
|
159
|
-
|
|
172
|
+
logger.info('SubscriptionItem deleted', {
|
|
173
|
+
id: doc.id,
|
|
174
|
+
subscriptionId: doc.subscription_id,
|
|
175
|
+
clearUsage: req.body.clear_usage,
|
|
176
|
+
requestedBy: req.user?.did,
|
|
177
|
+
});
|
|
160
178
|
return res.json(doc);
|
|
161
179
|
});
|
|
162
180
|
|
|
@@ -198,11 +216,23 @@ router.post('/:id/add-usage-quantity', auth, async (req, res) => {
|
|
|
198
216
|
timestamp: now,
|
|
199
217
|
} as UsageRecord);
|
|
200
218
|
|
|
219
|
+
logger.info('Usage quantity added', {
|
|
220
|
+
subscriptionItemId: subscriptionItem.id,
|
|
221
|
+
subscriptionId: subscription.id,
|
|
222
|
+
quantity,
|
|
223
|
+
timestamp: now,
|
|
224
|
+
requestedBy: req.user?.did,
|
|
225
|
+
});
|
|
226
|
+
|
|
201
227
|
if (subscription.billing_thresholds?.amount_gte) {
|
|
202
228
|
usageRecordQueue.push({
|
|
203
229
|
id: `usage-${subscription.id}`,
|
|
204
230
|
job: { subscriptionId: subscription.id, subscriptionItemId: subscriptionItem.id },
|
|
205
231
|
});
|
|
232
|
+
logger.info('Usage record pushed to queue', {
|
|
233
|
+
subscriptionId: subscription.id,
|
|
234
|
+
subscriptionItemId: subscriptionItem.id,
|
|
235
|
+
});
|
|
206
236
|
}
|
|
207
237
|
|
|
208
238
|
await forwardUsageRecordToStripe(subscriptionItem, {
|
|
@@ -210,6 +240,13 @@ router.post('/:id/add-usage-quantity', auth, async (req, res) => {
|
|
|
210
240
|
timestamp: now,
|
|
211
241
|
action: 'increment',
|
|
212
242
|
});
|
|
243
|
+
logger.info('Usage record forwarded to Stripe', {
|
|
244
|
+
subscriptionItemId: subscriptionItem.id,
|
|
245
|
+
quantity,
|
|
246
|
+
timestamp: now,
|
|
247
|
+
action: 'increment',
|
|
248
|
+
});
|
|
249
|
+
|
|
213
250
|
return res.json(usageRecord);
|
|
214
251
|
} catch (err) {
|
|
215
252
|
console.error(err);
|
|
@@ -407,7 +407,14 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
|
407
407
|
}
|
|
408
408
|
await subscription.update(updates);
|
|
409
409
|
await new SubscriptionWillCanceledSchedule().reScheduleSubscriptionTasks([subscription]);
|
|
410
|
-
|
|
410
|
+
logger.info('Update subscription for cancel request successful', {
|
|
411
|
+
subscriptionId: subscription.id,
|
|
412
|
+
customerId: subscription.customer_id,
|
|
413
|
+
reason: req.body.reason,
|
|
414
|
+
cancelAt: subscription.cancel_at,
|
|
415
|
+
requestedBy: req.user?.did,
|
|
416
|
+
updates,
|
|
417
|
+
});
|
|
411
418
|
return res.json(subscription);
|
|
412
419
|
});
|
|
413
420
|
|
|
@@ -1105,7 +1112,11 @@ router.put('/:id', authPortal, async (req, res) => {
|
|
|
1105
1112
|
}
|
|
1106
1113
|
}
|
|
1107
1114
|
}
|
|
1108
|
-
|
|
1115
|
+
logger.info('Subscription updated successfully', {
|
|
1116
|
+
subscriptionId: subscription.id,
|
|
1117
|
+
updatedFields: Object.keys(updates),
|
|
1118
|
+
newStatus: subscription.status,
|
|
1119
|
+
});
|
|
1109
1120
|
return res.json({ ...subscription.toJSON(), connectAction });
|
|
1110
1121
|
} catch (err) {
|
|
1111
1122
|
console.error(err);
|
|
@@ -1624,8 +1635,16 @@ router.delete('/:id', auth, async (req, res) => {
|
|
|
1624
1635
|
await UsageRecord.destroy({ where: { subscription_item_id: items.map((x) => x.id) } });
|
|
1625
1636
|
await SubscriptionItem.destroy({ where: { subscription_id: doc.id } });
|
|
1626
1637
|
await doc.destroy();
|
|
1627
|
-
logger.info('
|
|
1628
|
-
|
|
1638
|
+
logger.info('Subscription deleted successfully', {
|
|
1639
|
+
subscriptionId: req.params.id,
|
|
1640
|
+
deletedRelatedRecords: {
|
|
1641
|
+
invoiceItems: await InvoiceItem.count({ where: { subscription_id: doc.id } }),
|
|
1642
|
+
invoices: await Invoice.count({ where: { subscription_id: doc.id } }),
|
|
1643
|
+
usageRecords: await UsageRecord.count({ where: { subscription_item_id: items.map((x) => x.id) } }),
|
|
1644
|
+
subscriptionItems: items.length,
|
|
1645
|
+
},
|
|
1646
|
+
requestedBy: req.user?.did,
|
|
1647
|
+
});
|
|
1629
1648
|
return res.json(doc);
|
|
1630
1649
|
});
|
|
1631
1650
|
|
|
@@ -1691,6 +1710,12 @@ router.put('/:id/slash-stake', auth, async (req, res) => {
|
|
|
1691
1710
|
return res.status(400).json({ error: 'Staking not found on subscription payment detail' });
|
|
1692
1711
|
}
|
|
1693
1712
|
try {
|
|
1713
|
+
logger.warn('Stake slash initiated', {
|
|
1714
|
+
subscriptionId: subscription.id,
|
|
1715
|
+
slashReason: req.body.slashReason,
|
|
1716
|
+
requestedBy: req.user?.did,
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1694
1719
|
await subscription.update({
|
|
1695
1720
|
// @ts-ignore
|
|
1696
1721
|
cancelation_details: {
|
|
@@ -1703,6 +1728,13 @@ router.put('/:id/slash-stake', auth, async (req, res) => {
|
|
|
1703
1728
|
id: `slash-stake-${subscription.id}`,
|
|
1704
1729
|
job: { subscriptionId: subscription.id },
|
|
1705
1730
|
});
|
|
1731
|
+
logger.info('Stake slash scheduled successfully', {
|
|
1732
|
+
subscriptionId: subscription.id,
|
|
1733
|
+
result,
|
|
1734
|
+
slashReason: req.body.slashReason,
|
|
1735
|
+
requestedBy: req.user?.did,
|
|
1736
|
+
stakingAddress: address,
|
|
1737
|
+
});
|
|
1706
1738
|
return res.json(result);
|
|
1707
1739
|
} catch (err) {
|
|
1708
1740
|
logger.error('subscription slash stake failed', { subscription: subscription.id, error: err });
|
|
@@ -45,17 +45,36 @@ router.post('/', auth, async (req, res) => {
|
|
|
45
45
|
});
|
|
46
46
|
if (doc) {
|
|
47
47
|
if (doc.billed) {
|
|
48
|
+
logger.info('UsageRecord updated', {
|
|
49
|
+
subscriptionItemId: raw.subscription_item_id,
|
|
50
|
+
timestamp: raw.timestamp,
|
|
51
|
+
newQuantity: raw.quantity,
|
|
52
|
+
});
|
|
48
53
|
return res.status(400).json({ error: 'UsageRecord is immutable because already billed' });
|
|
49
54
|
}
|
|
50
55
|
if (req.body.action === 'increment') {
|
|
51
56
|
await doc.increment('quantity', { by: raw.quantity });
|
|
57
|
+
logger.info('UsageRecord incremented', {
|
|
58
|
+
subscriptionItemId: raw.subscription_item_id,
|
|
59
|
+
timestamp: raw.timestamp,
|
|
60
|
+
incrementBy: raw.quantity,
|
|
61
|
+
});
|
|
52
62
|
} else {
|
|
53
63
|
if (subscription.billing_thresholds?.amount_gte) {
|
|
64
|
+
logger.warn('Invalid action for subscription with billing_thresholds', {
|
|
65
|
+
subscriptionId: subscription.id,
|
|
66
|
+
action: req.body.action,
|
|
67
|
+
});
|
|
54
68
|
return res
|
|
55
69
|
.status(400)
|
|
56
70
|
.json({ error: 'UsageRecord action must be `increment` for subscriptions with billing_thresholds' });
|
|
57
71
|
}
|
|
58
72
|
await doc.update({ quantity: raw.quantity });
|
|
73
|
+
logger.info('UsageRecord updated', {
|
|
74
|
+
subscriptionItemId: raw.subscription_item_id,
|
|
75
|
+
timestamp: raw.timestamp,
|
|
76
|
+
newQuantity: raw.quantity,
|
|
77
|
+
});
|
|
59
78
|
}
|
|
60
79
|
} else {
|
|
61
80
|
raw.livemode = req.livemode;
|
|
@@ -67,6 +86,10 @@ router.post('/', auth, async (req, res) => {
|
|
|
67
86
|
id: `usage-${subscription.id}`,
|
|
68
87
|
job: { subscriptionId: subscription.id, subscriptionItemId: item.id },
|
|
69
88
|
});
|
|
89
|
+
logger.info('UsageRecord pushed to queue', {
|
|
90
|
+
subscriptionId: subscription.id,
|
|
91
|
+
subscriptionItemId: item.id,
|
|
92
|
+
});
|
|
70
93
|
}
|
|
71
94
|
|
|
72
95
|
await forwardUsageRecordToStripe(item, {
|
|
@@ -75,6 +98,12 @@ router.post('/', auth, async (req, res) => {
|
|
|
75
98
|
action: req.body.action,
|
|
76
99
|
});
|
|
77
100
|
|
|
101
|
+
logger.info('UsageRecord forwarded to Stripe', {
|
|
102
|
+
subscriptionItemId: item.id,
|
|
103
|
+
quantity: Number(raw.quantity),
|
|
104
|
+
timestamp: raw.timestamp,
|
|
105
|
+
action: req.body.action,
|
|
106
|
+
});
|
|
78
107
|
return res.json(doc);
|
|
79
108
|
});
|
|
80
109
|
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.22",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@arcblock/validator": "^1.18.136",
|
|
53
53
|
"@blocklet/js-sdk": "^1.16.32",
|
|
54
54
|
"@blocklet/logger": "^1.16.32",
|
|
55
|
-
"@blocklet/payment-react": "1.15.
|
|
55
|
+
"@blocklet/payment-react": "1.15.22",
|
|
56
56
|
"@blocklet/sdk": "^1.16.32",
|
|
57
57
|
"@blocklet/ui-react": "^2.10.51",
|
|
58
58
|
"@blocklet/uploader": "^0.1.46",
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"devDependencies": {
|
|
119
119
|
"@abtnode/types": "^1.16.32",
|
|
120
120
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
121
|
-
"@blocklet/payment-types": "1.15.
|
|
121
|
+
"@blocklet/payment-types": "1.15.22",
|
|
122
122
|
"@types/cookie-parser": "^1.4.7",
|
|
123
123
|
"@types/cors": "^2.8.17",
|
|
124
124
|
"@types/debug": "^4.1.12",
|
|
@@ -160,5 +160,5 @@
|
|
|
160
160
|
"parser": "typescript"
|
|
161
161
|
}
|
|
162
162
|
},
|
|
163
|
-
"gitHead": "
|
|
163
|
+
"gitHead": "12a3cbcf04d0c8bdbbf33c04d07cecfb24bd3e9b"
|
|
164
164
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import CodeBlock from '@arcblock/ux/lib/CodeBlock';
|
|
3
3
|
import { api, formatTime } from '@blocklet/payment-react';
|
|
4
4
|
import type { Paginated, TEvent, TWebhookAttemptExpanded } from '@blocklet/payment-types';
|
|
5
|
-
import { CheckCircleOutlined, ErrorOutlined } from '@mui/icons-material';
|
|
5
|
+
import { CheckCircleOutlined, ErrorOutlined, InfoOutlined } from '@mui/icons-material';
|
|
6
6
|
import {
|
|
7
7
|
Box,
|
|
8
8
|
Button,
|
|
@@ -15,12 +15,15 @@ import {
|
|
|
15
15
|
ListSubheader,
|
|
16
16
|
Stack,
|
|
17
17
|
Typography,
|
|
18
|
+
Popper,
|
|
19
|
+
Paper,
|
|
18
20
|
} from '@mui/material';
|
|
19
21
|
import { useInfiniteScroll } from 'ahooks';
|
|
20
22
|
import React, { useEffect, useState } from 'react';
|
|
21
23
|
|
|
22
24
|
import { isEmpty } from 'lodash';
|
|
23
25
|
import { isSuccessAttempt } from '../../libs/util';
|
|
26
|
+
import InfoCard from '../info-card';
|
|
24
27
|
|
|
25
28
|
const fetchData = (params: Record<string, any> = {}): Promise<Paginated<TWebhookAttemptExpanded>> => {
|
|
26
29
|
const search = new URLSearchParams();
|
|
@@ -45,7 +48,7 @@ const groupAttemptsByDate = (attempts: TWebhookAttemptExpanded[]) => {
|
|
|
45
48
|
type Props = {
|
|
46
49
|
event_id?: string;
|
|
47
50
|
webhook_endpoint_id?: string;
|
|
48
|
-
event?: TEvent;
|
|
51
|
+
event?: TEvent & { requestInfo?: { avatar: string; email: string; did: string } };
|
|
49
52
|
};
|
|
50
53
|
|
|
51
54
|
WebhookAttempts.defaultProps = {
|
|
@@ -82,6 +85,35 @@ export default function WebhookAttempts({ event_id, webhook_endpoint_id, event }
|
|
|
82
85
|
setSelected(attempt);
|
|
83
86
|
};
|
|
84
87
|
|
|
88
|
+
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
|
89
|
+
|
|
90
|
+
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
|
|
91
|
+
setAnchorEl(anchorEl ? null : e.currentTarget);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
96
|
+
if (anchorEl && !anchorEl.contains(e.target as Node) && !(e.target as Element).closest('.popper-content')) {
|
|
97
|
+
setAnchorEl(null);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const handleScroll = (e: Event) => {
|
|
102
|
+
// @ts-ignore
|
|
103
|
+
if (anchorEl && !e.target?.closest('.popper-content')) {
|
|
104
|
+
setAnchorEl(null);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
document.addEventListener('click', handleClickOutside);
|
|
109
|
+
window.addEventListener('scroll', handleScroll, true);
|
|
110
|
+
|
|
111
|
+
return () => {
|
|
112
|
+
document.removeEventListener('click', handleClickOutside);
|
|
113
|
+
window.removeEventListener('scroll', handleScroll, true);
|
|
114
|
+
};
|
|
115
|
+
}, [anchorEl]);
|
|
116
|
+
|
|
85
117
|
if (loading) {
|
|
86
118
|
return <CircularProgress />;
|
|
87
119
|
}
|
|
@@ -146,7 +178,94 @@ export default function WebhookAttempts({ event_id, webhook_endpoint_id, event }
|
|
|
146
178
|
)}
|
|
147
179
|
{data?.list.length === 0 && event && (
|
|
148
180
|
<Box>
|
|
149
|
-
<
|
|
181
|
+
<Stack direction="row" alignItems="center" spacing={1}>
|
|
182
|
+
<Typography variant="h6">Event Data</Typography>
|
|
183
|
+
<>
|
|
184
|
+
{/* @ts-ignore */}
|
|
185
|
+
<InfoOutlined
|
|
186
|
+
fontSize="small"
|
|
187
|
+
onClick={handleClick}
|
|
188
|
+
sx={{
|
|
189
|
+
color: 'text.secondary',
|
|
190
|
+
opacity: 0.6,
|
|
191
|
+
cursor: 'pointer',
|
|
192
|
+
}}
|
|
193
|
+
/>
|
|
194
|
+
<Popper
|
|
195
|
+
open={Boolean(anchorEl)}
|
|
196
|
+
anchorEl={anchorEl}
|
|
197
|
+
placement="right"
|
|
198
|
+
sx={{
|
|
199
|
+
zIndex: 1000,
|
|
200
|
+
'@media (max-width: 600px)': {
|
|
201
|
+
'& .MuiPaper-root': {
|
|
202
|
+
width: 'calc(100vw - 32px)',
|
|
203
|
+
maxWidth: 'none',
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
}}
|
|
207
|
+
modifiers={[
|
|
208
|
+
{
|
|
209
|
+
name: 'preventOverflow',
|
|
210
|
+
options: {
|
|
211
|
+
boundary: window,
|
|
212
|
+
altAxis: true,
|
|
213
|
+
padding: 16,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'flip',
|
|
218
|
+
options: {
|
|
219
|
+
fallbackPlacements: ['bottom'],
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'matchWidth',
|
|
224
|
+
enabled: true,
|
|
225
|
+
fn: ({ state }) => {
|
|
226
|
+
if (window.innerWidth <= 600) {
|
|
227
|
+
state.styles.popper = {
|
|
228
|
+
...state.styles.popper,
|
|
229
|
+
width: 'calc(100vw - 32px)',
|
|
230
|
+
maxWidth: 'none',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return state;
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
]}>
|
|
237
|
+
<Paper
|
|
238
|
+
className="popper-content"
|
|
239
|
+
elevation={3}
|
|
240
|
+
sx={{
|
|
241
|
+
p: 2,
|
|
242
|
+
border: '1px solid',
|
|
243
|
+
borderColor: 'divider',
|
|
244
|
+
maxWidth: 300,
|
|
245
|
+
'@media (max-width: 600px)': {
|
|
246
|
+
maxWidth: 'none',
|
|
247
|
+
margin: '0 auto',
|
|
248
|
+
},
|
|
249
|
+
}}>
|
|
250
|
+
{event.requestInfo ? (
|
|
251
|
+
<>
|
|
252
|
+
<Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
|
|
253
|
+
Requested by:
|
|
254
|
+
</Typography>
|
|
255
|
+
<InfoCard
|
|
256
|
+
logo={event.requestInfo.avatar}
|
|
257
|
+
name={event.requestInfo.email}
|
|
258
|
+
description={event.requestInfo.did || event.request.requested_by}
|
|
259
|
+
size={40}
|
|
260
|
+
/>
|
|
261
|
+
</>
|
|
262
|
+
) : (
|
|
263
|
+
<Typography>Requested by: {event.request?.requested_by || 'system'}</Typography>
|
|
264
|
+
)}
|
|
265
|
+
</Paper>
|
|
266
|
+
</Popper>
|
|
267
|
+
</>
|
|
268
|
+
</Stack>
|
|
150
269
|
<CodeBlock language="json">{JSON.stringify(event.data, null, 2)}</CodeBlock>
|
|
151
270
|
</Box>
|
|
152
271
|
)}
|