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 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) {
@@ -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();
@@ -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
 
@@ -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('event not found', job);
21
+ logger.warn('Event not found', job);
22
22
  return;
23
23
  }
24
24
 
25
25
  if (!event.pending_webhooks) {
26
- logger.warn('event already processed', job);
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('schedule attempt for event', job);
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', { invoice: invoice.id, paymentIntent: paymentIntent.id });
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.info('create billing discrepancy event', { invoiceId, shouldPayTotal, invoiceTotal: invoice.total });
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);
@@ -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
- console.error(err);
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
- console.error(err);
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
  });
@@ -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
- res.json(doc);
65
- } else {
66
- res.status(404).json(null);
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(500).json({ error: `Failed to get event: ${err.message}` });
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('create payment link error', err);
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
- await doc.update(formatBeforeSave(Object.assign({}, doc.dataValues, req.body)));
339
-
340
- res.json(doc);
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
- // if (doc.locked) {
356
- // return res.status(403).json({ error: 'payment link locked' });
357
- // }
358
-
359
- await doc.update({ active: false });
360
- return res.json(doc);
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
- // if (doc.locked) {
376
- // return res.status(403).json({ error: 'payment link locked' });
377
- // }
378
-
379
- await doc.destroy();
380
- return res.json(doc);
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
- console.error(err);
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
  });
@@ -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
- console.error(err);
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
- await doc.update(Price.formatBeforeSave(updates));
350
-
351
- return res.json(await getExpandedPrice(req.params.id as string));
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('create refund error', err);
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
- console.error(err);
247
- res.json(null);
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('subscription deleted', { subscription: req.params.id });
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
 
@@ -24,6 +24,7 @@ export class Event extends Model<InferAttributes<Event>, InferCreationAttributes
24
24
  declare request: {
25
25
  id: string;
26
26
  idempotency_key: string;
27
+ requested_by?: string;
27
28
  };
28
29
 
29
30
  // Number of webhooks that have yet to be successfully delivered
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.15.21
17
+ version: 1.15.22
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.15.21",
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.21",
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.21",
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": "b27debca52a2ea7f93a2a237053b8e171f8be3a2"
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
- <Typography variant="h6">Event Data</Typography>
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
  )}