payment-kit 1.15.20 → 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.
Files changed (52) hide show
  1. package/api/src/crons/base.ts +69 -7
  2. package/api/src/crons/subscription-trial-will-end.ts +20 -5
  3. package/api/src/crons/subscription-will-canceled.ts +22 -6
  4. package/api/src/crons/subscription-will-renew.ts +13 -4
  5. package/api/src/index.ts +4 -1
  6. package/api/src/integrations/arcblock/stake.ts +27 -0
  7. package/api/src/libs/audit.ts +4 -1
  8. package/api/src/libs/context.ts +48 -0
  9. package/api/src/libs/invoice.ts +2 -2
  10. package/api/src/libs/middleware.ts +39 -1
  11. package/api/src/libs/notification/template/subscription-canceled.ts +4 -0
  12. package/api/src/libs/notification/template/subscription-trial-will-end.ts +12 -34
  13. package/api/src/libs/notification/template/subscription-will-canceled.ts +82 -48
  14. package/api/src/libs/notification/template/subscription-will-renew.ts +16 -45
  15. package/api/src/libs/time.ts +13 -0
  16. package/api/src/libs/util.ts +17 -0
  17. package/api/src/locales/en.ts +12 -2
  18. package/api/src/locales/zh.ts +11 -2
  19. package/api/src/queues/checkout-session.ts +15 -0
  20. package/api/src/queues/event.ts +13 -4
  21. package/api/src/queues/invoice.ts +21 -3
  22. package/api/src/queues/payment.ts +3 -0
  23. package/api/src/queues/refund.ts +3 -0
  24. package/api/src/queues/subscription.ts +107 -2
  25. package/api/src/queues/usage-record.ts +4 -0
  26. package/api/src/queues/webhook.ts +9 -0
  27. package/api/src/routes/checkout-sessions.ts +40 -2
  28. package/api/src/routes/connect/recharge.ts +143 -0
  29. package/api/src/routes/connect/shared.ts +25 -0
  30. package/api/src/routes/customers.ts +2 -2
  31. package/api/src/routes/donations.ts +5 -1
  32. package/api/src/routes/events.ts +9 -4
  33. package/api/src/routes/payment-links.ts +40 -20
  34. package/api/src/routes/prices.ts +17 -4
  35. package/api/src/routes/products.ts +21 -2
  36. package/api/src/routes/refunds.ts +20 -3
  37. package/api/src/routes/subscription-items.ts +39 -2
  38. package/api/src/routes/subscriptions.ts +77 -40
  39. package/api/src/routes/usage-records.ts +29 -0
  40. package/api/src/store/models/event.ts +1 -0
  41. package/api/src/store/models/subscription.ts +2 -0
  42. package/api/tests/libs/time.spec.ts +54 -0
  43. package/blocklet.yml +1 -1
  44. package/package.json +19 -19
  45. package/src/app.tsx +10 -0
  46. package/src/components/subscription/actions/cancel.tsx +30 -9
  47. package/src/components/subscription/actions/index.tsx +11 -3
  48. package/src/components/webhook/attempts.tsx +122 -3
  49. package/src/locales/en.tsx +13 -0
  50. package/src/locales/zh.tsx +13 -0
  51. package/src/pages/customer/recharge.tsx +417 -0
  52. package/src/pages/customer/subscription/detail.tsx +38 -20
@@ -16,6 +16,7 @@ import {
16
16
  checkUsageReportEmpty,
17
17
  getSubscriptionCycleAmount,
18
18
  getSubscriptionCycleSetup,
19
+ getSubscriptionRefundSetup,
19
20
  getSubscriptionStakeAddress,
20
21
  getSubscriptionStakeReturnSetup,
21
22
  getSubscriptionStakeSlashSetup,
@@ -29,6 +30,7 @@ import { Price } from '../store/models/price';
29
30
  import { Subscription } from '../store/models/subscription';
30
31
  import { SubscriptionItem } from '../store/models/subscription-item';
31
32
  import { invoiceQueue } from './invoice';
33
+ import { SubscriptionWillCanceledSchedule } from '../crons/subscription-will-canceled';
32
34
 
33
35
  type SubscriptionJob = {
34
36
  subscriptionId: string;
@@ -227,7 +229,10 @@ const handleSubscriptionBeforeCancel = async (subscription: Subscription) => {
227
229
 
228
230
  // persist invoice id
229
231
  await subscription.update({ latest_invoice_id: invoice.id });
230
- logger.info('Subscription updated before cancel', { invoice: invoice.id, subscription: subscription.id });
232
+ logger.info('Subscription updated before cancel', {
233
+ subscription: subscription.id,
234
+ latestInvoiceId: invoice.id,
235
+ });
231
236
  }
232
237
  };
233
238
 
@@ -256,6 +261,9 @@ const handleSubscriptionWhenActive = async (subscription: Subscription) => {
256
261
  const now = dayjs().unix();
257
262
  if (subscription.trial_end && subscription.trial_end <= now) {
258
263
  await subscription.update({ status: 'active' });
264
+ logger.info('Subscription status updated from trialing to active', {
265
+ subscription: subscription.id,
266
+ });
259
267
  }
260
268
  }
261
269
 
@@ -438,6 +446,11 @@ const handleStakeSlashAfterCancel = async (subscription: Subscription) => {
438
446
  },
439
447
  },
440
448
  });
449
+ logger.info('PaymentIntent updated after stake slash', {
450
+ subscription: subscription.id,
451
+ paymentIntent: paymentIntent.id,
452
+ status: 'succeeded',
453
+ });
441
454
  await invoice.update({
442
455
  paid: true,
443
456
  status: 'paid',
@@ -447,6 +460,12 @@ const handleStakeSlashAfterCancel = async (subscription: Subscription) => {
447
460
  attempted: true,
448
461
  status_transitions: { ...invoice.status_transitions, paid_at: dayjs().unix() },
449
462
  });
463
+
464
+ logger.info('Invoice updated after stake slash', {
465
+ subscription: subscription.id,
466
+ invoice: invoice.id,
467
+ status: 'paid',
468
+ });
450
469
  };
451
470
 
452
471
  const ensureReturnStake = async (subscription: Subscription) => {
@@ -626,7 +645,11 @@ const slashStakeOnCancel = async (subscription: Subscription) => {
626
645
  subscription_id: subscription.id,
627
646
  } as unknown as Invoice,
628
647
  });
629
- logger.info('Invoice created for stake slash', { invoice: newInvoice.id, subscription: subscription.id });
648
+ logger.info('New invoice created for stake slash', {
649
+ subscription: subscription.id,
650
+ invoice: newInvoice.id,
651
+ amount: result.return_amount,
652
+ });
630
653
  invoiceQueue.push({
631
654
  id: newInvoice.id,
632
655
  job: { invoiceId: newInvoice.id, retryOnError: true },
@@ -635,6 +658,58 @@ const slashStakeOnCancel = async (subscription: Subscription) => {
635
658
  }
636
659
  };
637
660
 
661
+ const ensureRefundOnCancel = async (subscription: Subscription) => {
662
+ // @ts-ignore
663
+ const { refund } = subscription.cancelation_details;
664
+ if (!['last', 'proration'].includes(refund)) {
665
+ logger.warn('Refund skipped because refund type is not last or proration', {
666
+ subscription: subscription.id,
667
+ refund,
668
+ });
669
+ return;
670
+ }
671
+ const result = await getSubscriptionRefundSetup(subscription, subscription.cancel_at);
672
+ if (result.unused === '0') {
673
+ logger.warn('Refund skipped because unused amount is 0', {
674
+ subscription: subscription.id,
675
+ unused: result.unused,
676
+ });
677
+ return;
678
+ }
679
+ const item = await Refund.create({
680
+ type: 'refund',
681
+ livemode: subscription.livemode,
682
+ amount: refund === 'last' ? result.total : result.unused,
683
+ description: 'refund_transfer_on_subscription_cancel',
684
+ status: 'pending',
685
+ reason: 'requested_by_admin',
686
+ currency_id: subscription.currency_id,
687
+ customer_id: subscription.customer_id,
688
+ payment_method_id: subscription.default_payment_method_id,
689
+ payment_intent_id: result.lastInvoice.payment_intent_id as string,
690
+ invoice_id: result.lastInvoice.id,
691
+ subscription_id: subscription.id,
692
+ attempt_count: 0,
693
+ attempted: false,
694
+ next_attempt: 0,
695
+ last_attempt_error: null,
696
+ starting_balance: '0',
697
+ ending_balance: '0',
698
+ starting_token_balance: {},
699
+ ending_token_balance: {},
700
+ metadata: {
701
+ requested_by: subscription.cancelation_details?.requested_by,
702
+ unused_period_start: refund === 'last' ? subscription.current_period_start : subscription.cancel_at,
703
+ unused_period_end: subscription.current_period_end,
704
+ },
705
+ });
706
+ logger.info('Created refund for canceled subscription', {
707
+ subscription: subscription.id,
708
+ refund: item.toJSON(),
709
+ refundType: refund,
710
+ });
711
+ };
712
+
638
713
  // generate invoice for subscription periodically
639
714
  export const handleSubscription = async (job: SubscriptionJob) => {
640
715
  logger.info('handle subscription', job);
@@ -793,6 +868,25 @@ export const returnStakeQueue = createQueue({
793
868
  },
794
869
  });
795
870
 
871
+ export const subscriptionCancelRefund = createQueue({
872
+ name: 'subscription-cancel-refund',
873
+ onJob: async (job) => {
874
+ const { subscriptionId } = job;
875
+ const subscription = await Subscription.findByPk(subscriptionId);
876
+ if (!subscription) {
877
+ return;
878
+ }
879
+ await ensureRefundOnCancel(subscription);
880
+ },
881
+ options: {
882
+ concurrency: 1,
883
+ maxRetries: 5,
884
+ retryDelay: 1000,
885
+ maxTimeout: 60000,
886
+ enableScheduledJob: true,
887
+ },
888
+ });
889
+
796
890
  export async function addSubscriptionJob(
797
891
  subscription: Subscription,
798
892
  action: 'cycle' | 'cancel' | 'resume',
@@ -831,6 +925,7 @@ subscriptionQueue.on('failed', ({ id, job, error }) => {
831
925
 
832
926
  events.on('customer.subscription.past_due', async (subscription: Subscription) => {
833
927
  await subscriptionQueue.delete(subscription.id);
928
+ await new SubscriptionWillCanceledSchedule().reScheduleSubscriptionTasks([subscription]);
834
929
  await addSubscriptionJob(subscription, 'cancel', true, subscription.cancel_at || subscription.current_period_end);
835
930
  logger.info('subscription cancel job scheduled after past_due', {
836
931
  subscription: subscription.id,
@@ -852,6 +947,15 @@ events.on('customer.subscription.deleted', (subscription: Subscription) => {
852
947
  });
853
948
  // FIXME: ensure invoices that are open or uncollectible are voided
854
949
 
950
+ if (
951
+ subscription.cancelation_details?.refund &&
952
+ ['last', 'proration'].includes(subscription.cancelation_details.refund)
953
+ ) {
954
+ subscriptionCancelRefund.push({
955
+ id: `subscription-cancel-refund-${subscription.id}`,
956
+ job: { subscriptionId: subscription.id },
957
+ });
958
+ }
855
959
  if (subscription.cancelation_details?.slash_stake) {
856
960
  slashStakeQueue.push({
857
961
  id: `slash-stake-${subscription.id}`,
@@ -890,5 +994,6 @@ events.on('customer.stake.revoked', async ({ subscriptionId, tx }: { subscriptio
890
994
  comment: `Revoked by ${tx.tx.from} with tx ${tx.hash}`,
891
995
  },
892
996
  });
997
+ await new SubscriptionWillCanceledSchedule().reScheduleSubscriptionTasks([subscription]);
893
998
  await addSubscriptionJob(subscription, 'cancel', true, subscription.current_period_end);
894
999
  });
@@ -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
 
@@ -0,0 +1,143 @@
1
+ import type { Transaction } from '@ocap/client';
2
+ import { fromAddress } from '@ocap/wallet';
3
+ import { fromTokenToUnit, toBase58 } from '@ocap/util';
4
+ import { encodeTransferItx } from 'api/src/integrations/ethereum/token';
5
+ import { executeEvmTransaction, waitForEvmTxConfirm } from 'api/src/integrations/ethereum/tx';
6
+ import type { CallbackArgs } from '../../libs/auth';
7
+ import { getGasPayerExtra } from '../../libs/payment';
8
+ import { getTxMetadata } from '../../libs/util';
9
+ import { ensureSubscriptionRecharge, getAuthPrincipalClaim } from './shared';
10
+ import logger from '../../libs/logger';
11
+
12
+ export default {
13
+ action: 'recharge',
14
+ authPrincipal: false,
15
+ claims: {
16
+ authPrincipal: async ({ extraParams }: CallbackArgs) => {
17
+ const { paymentMethod } = await ensureSubscriptionRecharge(extraParams.subscriptionId);
18
+ return getAuthPrincipalClaim(paymentMethod, 'recharge');
19
+ },
20
+ },
21
+ onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
22
+ const { subscriptionId } = extraParams;
23
+ let { amount } = extraParams;
24
+ const { paymentMethod, paymentCurrency, receiverAddress } = await ensureSubscriptionRecharge(subscriptionId);
25
+ amount = fromTokenToUnit(amount, paymentCurrency.decimal).toString();
26
+ if (paymentMethod.type === 'arcblock') {
27
+ const tokens = [{ address: paymentCurrency.contract as string, value: amount }];
28
+ // @ts-ignore
29
+ const itx: TransferV3Tx = {
30
+ outputs: [{ owner: receiverAddress, tokens, assets: [] }],
31
+ data: getTxMetadata({
32
+ rechargeCustomerId: receiverAddress,
33
+ subscriptionId,
34
+ currencyId: paymentCurrency.id,
35
+ amount,
36
+ action: 'recharge',
37
+ }),
38
+ };
39
+
40
+ const claims: { [key: string]: object } = {
41
+ prepareTx: {
42
+ type: 'TransferV3Tx',
43
+ description: `Recharge account for ${receiverAddress}`,
44
+ partialTx: { from: userDid, pk: userPk, itx },
45
+ requirement: { tokens },
46
+ chainInfo: {
47
+ host: paymentMethod.settings?.arcblock?.api_host as string,
48
+ id: paymentMethod.settings?.arcblock?.chain_id as string,
49
+ },
50
+ },
51
+ };
52
+
53
+ return claims;
54
+ }
55
+
56
+ if (paymentMethod.type === 'ethereum') {
57
+ return {
58
+ signature: {
59
+ type: 'eth:transaction',
60
+ data: toBase58(
61
+ Buffer.from(
62
+ JSON.stringify({
63
+ network: paymentMethod.settings?.ethereum?.chain_id,
64
+ tx: encodeTransferItx(receiverAddress, amount, paymentCurrency.contract as string),
65
+ }),
66
+ 'utf-8'
67
+ )
68
+ ),
69
+ },
70
+ };
71
+ }
72
+
73
+ throw new Error(`Payment method ${paymentMethod.type} not supported`);
74
+ },
75
+ onAuth: async ({ request, userDid, claims, extraParams }: CallbackArgs) => {
76
+ const { subscriptionId } = extraParams;
77
+ const { paymentMethod, paymentCurrency, receiverAddress } = await ensureSubscriptionRecharge(subscriptionId);
78
+ let { amount } = extraParams;
79
+ amount = fromTokenToUnit(amount, paymentCurrency.decimal).toString();
80
+
81
+ if (paymentMethod.type === 'arcblock') {
82
+ try {
83
+ const client = paymentMethod.getOcapClient();
84
+ const claim = claims.find((x) => x.type === 'prepareTx');
85
+
86
+ const tx: Partial<Transaction> = client.decodeTx(claim.finalTx);
87
+ if (claim.delegator && claim.from) {
88
+ tx.delegator = claim.delegator;
89
+ tx.from = claim.from;
90
+ }
91
+
92
+ // @ts-ignore
93
+ const { buffer } = await client.encodeTransferV3Tx({ tx });
94
+ const txHash = await client.sendTransferV3Tx(
95
+ // @ts-ignore
96
+ { tx, wallet: fromAddress(userDid) },
97
+ getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
98
+ );
99
+
100
+ logger.info('Recharge successful', {
101
+ receiverAddress,
102
+ amount,
103
+ subscriptionId,
104
+ paymentMethod: paymentMethod.type,
105
+ });
106
+
107
+ return { hash: txHash };
108
+ } catch (err) {
109
+ console.error(err);
110
+ logger.error('Failed to finalize recharge on arcblock', { receiverAddress, error: err });
111
+ throw err;
112
+ }
113
+ }
114
+
115
+ if (paymentMethod.type === 'ethereum') {
116
+ try {
117
+ const paymentDetails = await executeEvmTransaction('transfer', userDid, claims, paymentMethod);
118
+ waitForEvmTxConfirm(
119
+ paymentMethod.getEvmClient(),
120
+ Number(paymentDetails.block_height),
121
+ paymentMethod.confirmation.block
122
+ )
123
+ .then(() => {
124
+ logger.info('Recharge successful', {
125
+ receiverAddress,
126
+ amount,
127
+ subscriptionId,
128
+ paymentMethod: paymentMethod.type,
129
+ });
130
+ })
131
+ .catch(console.error);
132
+
133
+ return { hash: paymentDetails.tx_hash };
134
+ } catch (err) {
135
+ console.error(err);
136
+ logger.error('Failed to finalize recharge on ethereum', { receiverAddress, error: err });
137
+ throw err;
138
+ }
139
+ }
140
+
141
+ throw new Error(`Payment method ${paymentMethod.type} not supported`);
142
+ },
143
+ };
@@ -606,6 +606,31 @@ export async function ensureInvoiceForCollect(invoiceId: string) {
606
606
  };
607
607
  }
608
608
 
609
+ export async function ensureSubscriptionRecharge(subscriptionId: string) {
610
+ const subscription = await Subscription.findByPk(subscriptionId);
611
+ if (!subscription) {
612
+ throw new Error(`Subscription ${subscriptionId} not found`);
613
+ }
614
+ const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
615
+ if (!paymentCurrency) {
616
+ throw new Error(`Currency ${subscription.currency_id} not found`);
617
+ }
618
+ const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
619
+ if (!paymentMethod) {
620
+ throw new Error(`Payment method ${paymentCurrency.payment_method_id} not found`);
621
+ }
622
+ // @ts-ignore
623
+ const receiverAddress = subscription?.payment_details?.[paymentMethod.type]?.payer;
624
+ if (!receiverAddress) {
625
+ throw new Error(`Receiver address not found for subscription ${subscriptionId}`);
626
+ }
627
+ return {
628
+ paymentCurrency: paymentCurrency as PaymentCurrency,
629
+ paymentMethod: paymentMethod as PaymentMethod,
630
+ receiverAddress,
631
+ };
632
+ }
633
+
609
634
  export function getAuthPrincipalClaim(method: PaymentMethod, action: string) {
610
635
  let chainInfo = { type: 'none', id: 'none', host: 'none' };
611
636
  if (method.type === 'arcblock') {
@@ -96,8 +96,8 @@ router.get('/me', user(), async (req, res) => {
96
96
  } else {
97
97
  const [summary, stake, token] = await Promise.all([
98
98
  doc.getSummary(),
99
- getStakeSummaryByDid(doc.did, doc.livemode),
100
- getTokenSummaryByDid(doc.did, doc.livemode),
99
+ getStakeSummaryByDid(doc.did, req?.livemode || doc.livemode),
100
+ getTokenSummaryByDid(doc.did, req?.livemode || doc.livemode),
101
101
  ]);
102
102
  res.json({ ...doc.toJSON(), summary: { ...summary, stake, token } });
103
103
  }
@@ -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