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.
- package/api/src/crons/base.ts +69 -7
- package/api/src/crons/subscription-trial-will-end.ts +20 -5
- package/api/src/crons/subscription-will-canceled.ts +22 -6
- package/api/src/crons/subscription-will-renew.ts +13 -4
- package/api/src/index.ts +4 -1
- package/api/src/integrations/arcblock/stake.ts +27 -0
- 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/libs/notification/template/subscription-canceled.ts +4 -0
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +12 -34
- package/api/src/libs/notification/template/subscription-will-canceled.ts +82 -48
- package/api/src/libs/notification/template/subscription-will-renew.ts +16 -45
- package/api/src/libs/time.ts +13 -0
- package/api/src/libs/util.ts +17 -0
- package/api/src/locales/en.ts +12 -2
- package/api/src/locales/zh.ts +11 -2
- 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/subscription.ts +107 -2
- 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/connect/recharge.ts +143 -0
- package/api/src/routes/connect/shared.ts +25 -0
- package/api/src/routes/customers.ts +2 -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 +77 -40
- package/api/src/routes/usage-records.ts +29 -0
- package/api/src/store/models/event.ts +1 -0
- package/api/src/store/models/subscription.ts +2 -0
- package/api/tests/libs/time.spec.ts +54 -0
- package/blocklet.yml +1 -1
- package/package.json +19 -19
- package/src/app.tsx +10 -0
- package/src/components/subscription/actions/cancel.tsx +30 -9
- package/src/components/subscription/actions/index.tsx +11 -3
- package/src/components/webhook/attempts.tsx +122 -3
- package/src/locales/en.tsx +13 -0
- package/src/locales/zh.tsx +13 -0
- package/src/pages/customer/recharge.tsx +417 -0
- 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', {
|
|
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('
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
});
|
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
|
|