payment-kit 1.15.33 → 1.15.35
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/integrations/stripe/handlers/setup-intent.ts +3 -1
- package/api/src/integrations/stripe/handlers/subscription.ts +2 -8
- package/api/src/integrations/stripe/resource.ts +0 -11
- package/api/src/libs/invoice.ts +202 -1
- package/api/src/libs/notification/template/subscription-canceled.ts +15 -2
- package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -1
- package/api/src/libs/notification/template/subscription-renewed.ts +1 -1
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +9 -5
- package/api/src/libs/notification/template/subscription-will-canceled.ts +9 -5
- package/api/src/libs/notification/template/subscription-will-renew.ts +10 -12
- package/api/src/libs/payment.ts +3 -2
- package/api/src/libs/refund.ts +4 -0
- package/api/src/libs/subscription.ts +58 -14
- package/api/src/queues/invoice.ts +1 -0
- package/api/src/queues/payment.ts +3 -1
- package/api/src/queues/refund.ts +9 -8
- package/api/src/queues/subscription.ts +111 -40
- package/api/src/routes/checkout-sessions.ts +22 -6
- package/api/src/routes/connect/change-payment.ts +51 -34
- package/api/src/routes/connect/change-plan.ts +25 -3
- package/api/src/routes/connect/recharge.ts +28 -3
- package/api/src/routes/connect/setup.ts +27 -6
- package/api/src/routes/connect/shared.ts +223 -1
- package/api/src/routes/connect/subscribe.ts +25 -3
- package/api/src/routes/customers.ts +2 -2
- package/api/src/routes/invoices.ts +27 -105
- package/api/src/routes/payment-links.ts +3 -0
- package/api/src/routes/refunds.ts +22 -1
- package/api/src/routes/subscriptions.ts +112 -21
- package/api/src/routes/webhook-attempts.ts +14 -1
- package/api/src/store/models/invoice.ts +3 -1
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/app.tsx +3 -1
- package/src/components/invoice/list.tsx +83 -31
- package/src/components/invoice/recharge.tsx +244 -0
- package/src/components/payment-intent/actions.tsx +2 -1
- package/src/components/payment-link/actions.tsx +6 -6
- package/src/components/payment-link/item.tsx +53 -18
- package/src/components/pricing-table/actions.tsx +14 -3
- package/src/components/pricing-table/payment-settings.tsx +1 -1
- package/src/components/refund/actions.tsx +43 -1
- package/src/components/refund/list.tsx +1 -1
- package/src/components/subscription/actions/cancel.tsx +10 -7
- package/src/components/subscription/metrics.tsx +1 -1
- package/src/components/subscription/portal/actions.tsx +22 -1
- package/src/components/subscription/portal/list.tsx +1 -0
- package/src/components/webhook/attempts.tsx +19 -121
- package/src/components/webhook/request-info.tsx +139 -0
- package/src/locales/en.tsx +4 -0
- package/src/locales/zh.tsx +8 -0
- package/src/pages/admin/billing/invoices/detail.tsx +15 -0
- package/src/pages/admin/billing/invoices/index.tsx +1 -1
- package/src/pages/admin/billing/subscriptions/detail.tsx +12 -4
- package/src/pages/admin/customers/customers/detail.tsx +1 -0
- package/src/pages/admin/payments/refunds/detail.tsx +2 -2
- package/src/pages/admin/products/links/create.tsx +4 -1
- package/src/pages/customer/index.tsx +1 -1
- package/src/pages/customer/invoice/detail.tsx +34 -14
- package/src/pages/customer/recharge.tsx +45 -35
- package/src/pages/customer/subscription/change-plan.tsx +8 -1
- package/src/pages/customer/subscription/detail.tsx +12 -22
- package/src/pages/customer/subscription/embed.tsx +3 -1
|
@@ -587,11 +587,13 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
587
587
|
}
|
|
588
588
|
const client = paymentMethod.getOcapClient();
|
|
589
589
|
|
|
590
|
+
const payer = paymentSettings?.payment_method_options.arcblock?.payer as string;
|
|
591
|
+
|
|
590
592
|
// check balance before capture with transaction
|
|
591
593
|
result = await isDelegationSufficientForPayment({
|
|
592
594
|
paymentMethod,
|
|
593
595
|
paymentCurrency,
|
|
594
|
-
userDid: customer.did,
|
|
596
|
+
userDid: payer || customer.did,
|
|
595
597
|
amount: paymentIntent.amount,
|
|
596
598
|
});
|
|
597
599
|
if (result.sufficient === false) {
|
package/api/src/queues/refund.ts
CHANGED
|
@@ -126,10 +126,14 @@ const handleRefundJob = async (
|
|
|
126
126
|
// try refund transfer and reschedule on error
|
|
127
127
|
logger.info('Refund transfer attempt', { id: refund.id, attempt: refund.attempt_count });
|
|
128
128
|
let result;
|
|
129
|
+
const paymentIntent = await PaymentIntent.findByPk(refund.payment_intent_id);
|
|
130
|
+
if (!paymentIntent) {
|
|
131
|
+
throw new Error('PaymentIntent not found');
|
|
132
|
+
}
|
|
133
|
+
|
|
129
134
|
try {
|
|
130
135
|
if (paymentMethod.type === 'arcblock') {
|
|
131
136
|
const client = paymentMethod.getOcapClient();
|
|
132
|
-
|
|
133
137
|
// check balance before transfer with transaction
|
|
134
138
|
result = await isBalanceSufficientForRefund({ paymentMethod, paymentCurrency, amount: refund.amount });
|
|
135
139
|
if (result.sufficient === false) {
|
|
@@ -137,11 +141,12 @@ const handleRefundJob = async (
|
|
|
137
141
|
throw new CustomError(result.reason, 'app balance not sufficient for this refund');
|
|
138
142
|
}
|
|
139
143
|
|
|
144
|
+
const payer = paymentIntent?.payment_details?.arcblock?.payer;
|
|
140
145
|
// do the transfer
|
|
141
146
|
const signed = await client.signTransferV2Tx({
|
|
142
147
|
tx: {
|
|
143
148
|
itx: {
|
|
144
|
-
to: customer.did,
|
|
149
|
+
to: payer || customer.did,
|
|
145
150
|
value: '0',
|
|
146
151
|
assets: [],
|
|
147
152
|
tokens: [{ address: paymentCurrency.contract, value: refund.amount }],
|
|
@@ -192,7 +197,6 @@ const handleRefundJob = async (
|
|
|
192
197
|
|
|
193
198
|
// do the capture
|
|
194
199
|
const client = paymentMethod.getEvmClient();
|
|
195
|
-
const paymentIntent = await PaymentIntent.findByPk(refund.payment_intent_id);
|
|
196
200
|
const payer = paymentIntent!.payment_details?.ethereum?.payer as string;
|
|
197
201
|
const receipt = await sendErc20ToUser(client, paymentCurrency.contract, payer, refund.amount);
|
|
198
202
|
logger.info('refund transfer done', { id: refund.id, txHash: receipt.hash });
|
|
@@ -216,10 +220,6 @@ const handleRefundJob = async (
|
|
|
216
220
|
if (!refund.payment_intent_id) {
|
|
217
221
|
throw new Error('payment intent id not found');
|
|
218
222
|
}
|
|
219
|
-
const paymentIntent = await PaymentIntent.findByPk(refund.payment_intent_id);
|
|
220
|
-
if (!paymentIntent) {
|
|
221
|
-
throw new Error('PaymentIntent not found');
|
|
222
|
-
}
|
|
223
223
|
const stripePaymentIntentId = paymentIntent?.payment_details?.stripe?.payment_intent_id;
|
|
224
224
|
if (!stripePaymentIntentId) {
|
|
225
225
|
throw new Error('paymentIntent should have stripe payment intent id');
|
|
@@ -315,7 +315,8 @@ const handleStakeReturnJob = async (
|
|
|
315
315
|
}
|
|
316
316
|
const client = paymentMethod.getOcapClient();
|
|
317
317
|
const subscription = await Subscription.findByPk(refund.subscription_id);
|
|
318
|
-
const address =
|
|
318
|
+
const address =
|
|
319
|
+
arcblockDetail?.staking?.address || (await getSubscriptionStakeAddress(subscription!, customer.did));
|
|
319
320
|
const stakeEnough = await checkRemainingStake(paymentMethod, paymentCurrency, address, refund.amount);
|
|
320
321
|
if (!stakeEnough.enough) {
|
|
321
322
|
logger.warn('Stake return aborted because stake is not enough ', {
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
shouldCancelSubscription,
|
|
24
24
|
} from '../libs/subscription';
|
|
25
25
|
import { ensureInvoiceAndItems } from '../routes/connect/shared';
|
|
26
|
-
import { PaymentCurrency, PaymentIntent, PaymentMethod, Refund, UsageRecord } from '../store/models';
|
|
26
|
+
import { PaymentCurrency, PaymentIntent, PaymentMethod, Refund, SetupIntent, UsageRecord } from '../store/models';
|
|
27
27
|
import { Customer } from '../store/models/customer';
|
|
28
28
|
import { Invoice } from '../store/models/invoice';
|
|
29
29
|
import { Price } from '../store/models/price';
|
|
@@ -325,24 +325,39 @@ const handleSubscriptionAfterRecover = async (subscription: Subscription) => {
|
|
|
325
325
|
};
|
|
326
326
|
|
|
327
327
|
const handleStakeSlashAfterCancel = async (subscription: Subscription) => {
|
|
328
|
-
const
|
|
329
|
-
if (!
|
|
328
|
+
const invoice = await Invoice.findByPk(subscription.latest_invoice_id);
|
|
329
|
+
if (!invoice || invoice.status !== 'uncollectible') {
|
|
330
|
+
logger.warn('Stake slashing aborted because invoice status', {
|
|
331
|
+
subscription: subscription.id,
|
|
332
|
+
invoice: invoice?.id,
|
|
333
|
+
status: invoice?.status,
|
|
334
|
+
});
|
|
330
335
|
return;
|
|
331
336
|
}
|
|
332
|
-
const currency = await PaymentCurrency.findByPk(subscription.currency_id);
|
|
337
|
+
const currency = await PaymentCurrency.findByPk(invoice.currency_id || subscription.currency_id);
|
|
333
338
|
if (!currency) {
|
|
339
|
+
logger.warn('Stake slashing aborted because currency not found', {
|
|
340
|
+
subscription: subscription.id,
|
|
341
|
+
invoice: invoice.id,
|
|
342
|
+
currency: invoice.currency_id || subscription.currency_id,
|
|
343
|
+
});
|
|
334
344
|
return;
|
|
335
345
|
}
|
|
336
|
-
const
|
|
337
|
-
if (!
|
|
346
|
+
const method = await PaymentMethod.findByPk(currency.payment_method_id);
|
|
347
|
+
if (!method || method.type !== 'arcblock') {
|
|
348
|
+
logger.warn('Stake slashing aborted because payment method not arcblock', {
|
|
349
|
+
subscription: subscription.id,
|
|
350
|
+
invoice: invoice.id,
|
|
351
|
+
method: method?.id,
|
|
352
|
+
});
|
|
338
353
|
return;
|
|
339
354
|
}
|
|
340
|
-
const
|
|
341
|
-
if (!
|
|
342
|
-
logger.warn('Stake slashing aborted because
|
|
355
|
+
const customer = await Customer.findByPk(subscription.customer_id);
|
|
356
|
+
if (!customer) {
|
|
357
|
+
logger.warn('Stake slashing aborted because customer not found', {
|
|
343
358
|
subscription: subscription.id,
|
|
344
|
-
invoice: invoice
|
|
345
|
-
|
|
359
|
+
invoice: invoice.id,
|
|
360
|
+
customer: subscription.customer_id,
|
|
346
361
|
});
|
|
347
362
|
return;
|
|
348
363
|
}
|
|
@@ -468,8 +483,18 @@ const handleStakeSlashAfterCancel = async (subscription: Subscription) => {
|
|
|
468
483
|
});
|
|
469
484
|
};
|
|
470
485
|
|
|
471
|
-
const ensureReturnStake = async (subscription: Subscription) => {
|
|
472
|
-
const
|
|
486
|
+
const ensureReturnStake = async (subscription: Subscription, paymentCurrencyId?: string, stakingAddress?: string) => {
|
|
487
|
+
const paymentCurrency = await PaymentCurrency.findByPk(paymentCurrencyId || subscription.currency_id);
|
|
488
|
+
if (!paymentCurrency) {
|
|
489
|
+
logger.warn('Stake return skipped because no payment currency', {
|
|
490
|
+
subscription: subscription.id,
|
|
491
|
+
currency: subscription.currency_id,
|
|
492
|
+
});
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const paymentMethod = await PaymentMethod.findByPk(
|
|
496
|
+
paymentCurrency.payment_method_id || subscription.default_payment_method_id
|
|
497
|
+
);
|
|
473
498
|
if (paymentMethod?.type !== 'arcblock') {
|
|
474
499
|
logger.warn('Stake return skipped because payment method not arcblock', {
|
|
475
500
|
subscription: subscription.id,
|
|
@@ -477,7 +502,7 @@ const ensureReturnStake = async (subscription: Subscription) => {
|
|
|
477
502
|
});
|
|
478
503
|
return;
|
|
479
504
|
}
|
|
480
|
-
const address = subscription?.payment_details?.arcblock?.staking?.address;
|
|
505
|
+
const address = stakingAddress || subscription?.payment_details?.arcblock?.staking?.address;
|
|
481
506
|
if (!address) {
|
|
482
507
|
logger.warn('Stake return skipped because no staking address', {
|
|
483
508
|
subscription: subscription.id,
|
|
@@ -486,21 +511,7 @@ const ensureReturnStake = async (subscription: Subscription) => {
|
|
|
486
511
|
return;
|
|
487
512
|
}
|
|
488
513
|
|
|
489
|
-
const
|
|
490
|
-
if (refunds.length > 0) {
|
|
491
|
-
logger.info(`Stake return skipped because subscription ${subscription.id} already has stake return records.`);
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
|
|
495
|
-
if (!paymentCurrency) {
|
|
496
|
-
logger.warn('Stake return skipped because no payment currency', {
|
|
497
|
-
subscription: subscription.id,
|
|
498
|
-
currency: subscription.currency_id,
|
|
499
|
-
});
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
const result = await getSubscriptionStakeReturnSetup(subscription, address, paymentMethod);
|
|
514
|
+
const result = await getSubscriptionStakeReturnSetup(subscription, address, paymentMethod, paymentCurrencyId);
|
|
504
515
|
|
|
505
516
|
const stakeEnough = await checkRemainingStake(paymentMethod, paymentCurrency, address, result.return_amount);
|
|
506
517
|
if (!stakeEnough.enough) {
|
|
@@ -513,17 +524,27 @@ const ensureReturnStake = async (subscription: Subscription) => {
|
|
|
513
524
|
return;
|
|
514
525
|
}
|
|
515
526
|
if (result.return_amount !== '0') {
|
|
527
|
+
const invoice = await Invoice.findOne({
|
|
528
|
+
where: {
|
|
529
|
+
billing_reason: 'stake',
|
|
530
|
+
subscription_id: subscription.id,
|
|
531
|
+
currency_id: paymentCurrency.id,
|
|
532
|
+
status: 'paid',
|
|
533
|
+
},
|
|
534
|
+
order: [['created_at', 'DESC']],
|
|
535
|
+
});
|
|
516
536
|
// do the stake return
|
|
517
537
|
const item = await Refund.create({
|
|
518
538
|
type: 'stake_return',
|
|
519
539
|
livemode: subscription.livemode,
|
|
520
540
|
amount: result.return_amount,
|
|
521
|
-
description: '
|
|
541
|
+
description: 'stake_return_for_subscription',
|
|
522
542
|
status: 'pending',
|
|
523
543
|
reason: 'requested_by_admin',
|
|
524
|
-
currency_id:
|
|
544
|
+
currency_id: paymentCurrency.id,
|
|
545
|
+
invoice_id: invoice?.id,
|
|
525
546
|
customer_id: subscription.customer_id,
|
|
526
|
-
payment_method_id:
|
|
547
|
+
payment_method_id: paymentMethod.id,
|
|
527
548
|
payment_intent_id: result?.lastInvoice?.payment_intent_id as string,
|
|
528
549
|
subscription_id: subscription.id,
|
|
529
550
|
attempt_count: 0,
|
|
@@ -538,6 +559,10 @@ const ensureReturnStake = async (subscription: Subscription) => {
|
|
|
538
559
|
// @ts-ignore
|
|
539
560
|
arcblock: {
|
|
540
561
|
receiver: result.sender,
|
|
562
|
+
staking: {
|
|
563
|
+
address,
|
|
564
|
+
tx_hash: '',
|
|
565
|
+
},
|
|
541
566
|
},
|
|
542
567
|
},
|
|
543
568
|
});
|
|
@@ -547,7 +572,7 @@ const ensureReturnStake = async (subscription: Subscription) => {
|
|
|
547
572
|
item: item.toJSON(),
|
|
548
573
|
});
|
|
549
574
|
} else {
|
|
550
|
-
logger.info('Skipped stake return for
|
|
575
|
+
logger.info('Skipped stake return for subscription', {
|
|
551
576
|
subscription: subscription.id,
|
|
552
577
|
return_amount: result.return_amount,
|
|
553
578
|
});
|
|
@@ -668,24 +693,33 @@ const ensureRefundOnCancel = async (subscription: Subscription) => {
|
|
|
668
693
|
});
|
|
669
694
|
return;
|
|
670
695
|
}
|
|
671
|
-
const
|
|
672
|
-
if (
|
|
696
|
+
const lastInvoice = await Invoice.findByPk(subscription.latest_invoice_id);
|
|
697
|
+
if (!lastInvoice) {
|
|
698
|
+
logger.warn('Refund skipped because no latest invoice', {
|
|
699
|
+
subscription: subscription.id,
|
|
700
|
+
});
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const result = await getSubscriptionRefundSetup(subscription, subscription.cancel_at, lastInvoice.currency_id);
|
|
705
|
+
if (result.remainingUnused === '0') {
|
|
673
706
|
logger.warn('Refund skipped because unused amount is 0', {
|
|
674
707
|
subscription: subscription.id,
|
|
675
708
|
unused: result.unused,
|
|
676
709
|
});
|
|
677
710
|
return;
|
|
678
711
|
}
|
|
712
|
+
const currency = await PaymentCurrency.findByPk(lastInvoice?.currency_id);
|
|
679
713
|
const item = await Refund.create({
|
|
680
714
|
type: 'refund',
|
|
681
715
|
livemode: subscription.livemode,
|
|
682
|
-
amount: refund === 'last' ? result.
|
|
716
|
+
amount: refund === 'last' ? result.remaining : result.remainingUnused,
|
|
683
717
|
description: 'refund_transfer_on_subscription_cancel',
|
|
684
718
|
status: 'pending',
|
|
685
719
|
reason: 'requested_by_admin',
|
|
686
|
-
currency_id:
|
|
720
|
+
currency_id: result.lastInvoice?.currency_id,
|
|
687
721
|
customer_id: subscription.customer_id,
|
|
688
|
-
payment_method_id: subscription.default_payment_method_id,
|
|
722
|
+
payment_method_id: currency?.payment_method_id || subscription.default_payment_method_id,
|
|
689
723
|
payment_intent_id: result.lastInvoice.payment_intent_id as string,
|
|
690
724
|
invoice_id: result.lastInvoice.id,
|
|
691
725
|
subscription_id: subscription.id,
|
|
@@ -852,12 +886,12 @@ export const slashStakeQueue = createQueue({
|
|
|
852
886
|
export const returnStakeQueue = createQueue({
|
|
853
887
|
name: 'returnStake',
|
|
854
888
|
onJob: async (job) => {
|
|
855
|
-
const { subscriptionId } = job;
|
|
889
|
+
const { subscriptionId, stakingAddress, paymentCurrencyId } = job;
|
|
856
890
|
const subscription = await Subscription.findByPk(subscriptionId);
|
|
857
891
|
if (!subscription) {
|
|
858
892
|
return;
|
|
859
893
|
}
|
|
860
|
-
await ensureReturnStake(subscription);
|
|
894
|
+
await ensureReturnStake(subscription, paymentCurrencyId, stakingAddress);
|
|
861
895
|
},
|
|
862
896
|
options: {
|
|
863
897
|
concurrency: 1,
|
|
@@ -997,3 +1031,40 @@ events.on('customer.stake.revoked', async ({ subscriptionId, tx }: { subscriptio
|
|
|
997
1031
|
await new SubscriptionWillCanceledSchedule().reScheduleSubscriptionTasks([subscription]);
|
|
998
1032
|
await addSubscriptionJob(subscription, 'cancel', true, subscription.current_period_end);
|
|
999
1033
|
});
|
|
1034
|
+
|
|
1035
|
+
events.on('setup_intent.succeeded', async (setupIntent: SetupIntent) => {
|
|
1036
|
+
logger.info('setup intent succeeded', { setupIntent: setupIntent.id });
|
|
1037
|
+
if (setupIntent.metadata?.from_currency && setupIntent?.metadata?.subscription_id) {
|
|
1038
|
+
const subscription = await Subscription.findByPk(setupIntent.metadata.subscription_id);
|
|
1039
|
+
if (!subscription) {
|
|
1040
|
+
logger.info('skip return stake because no subscription found', { setupIntent: setupIntent.id });
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const stakingInvoice = await Invoice.findOne({
|
|
1044
|
+
where: {
|
|
1045
|
+
subscription_id: subscription.id,
|
|
1046
|
+
billing_reason: 'stake',
|
|
1047
|
+
currency_id: setupIntent.metadata?.from_currency,
|
|
1048
|
+
status: 'paid',
|
|
1049
|
+
},
|
|
1050
|
+
order: [['created_at', 'DESC']],
|
|
1051
|
+
});
|
|
1052
|
+
logger.info('staking invoice', { stakingInvoice });
|
|
1053
|
+
if (stakingInvoice) {
|
|
1054
|
+
returnStakeQueue.push({
|
|
1055
|
+
id: `return-stake-${subscription.id}-${stakingInvoice.id}`,
|
|
1056
|
+
job: {
|
|
1057
|
+
subscriptionId: subscription.id,
|
|
1058
|
+
stakingAddress: stakingInvoice?.metadata?.payment_details?.arcblock?.address,
|
|
1059
|
+
paymentCurrencyId: setupIntent.metadata?.from_currency,
|
|
1060
|
+
},
|
|
1061
|
+
});
|
|
1062
|
+
logger.info('subscription return stake job scheduled', {
|
|
1063
|
+
jobId: `return-stake-${subscription.id}-${stakingInvoice.id}`,
|
|
1064
|
+
subscription: subscription.id,
|
|
1065
|
+
stakingAddress: stakingInvoice?.metadata?.payment_details?.arcblock?.address,
|
|
1066
|
+
paymentCurrencyId: setupIntent.metadata?.from_currency,
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable consistent-return */
|
|
2
2
|
import { isValid } from '@arcblock/did';
|
|
3
3
|
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
4
|
-
import
|
|
4
|
+
import sessionMiddleware from '@blocklet/sdk/lib/middlewares/session';
|
|
5
5
|
import { BN, fromUnitToToken } from '@ocap/util';
|
|
6
6
|
import { NextFunction, Request, Response, Router } from 'express';
|
|
7
7
|
import Joi from 'joi';
|
|
@@ -17,7 +17,11 @@ import { MetadataSchema } from '../libs/api';
|
|
|
17
17
|
import { checkPassportForPaymentLink } from '../integrations/blocklet/passport';
|
|
18
18
|
import { handleStripePaymentSucceed } from '../integrations/stripe/handlers/payment-intent';
|
|
19
19
|
import { handleStripeSubscriptionSucceed } from '../integrations/stripe/handlers/subscription';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
ensureStripePaymentCustomer,
|
|
22
|
+
ensureStripePaymentIntent,
|
|
23
|
+
ensureStripeSubscription,
|
|
24
|
+
} from '../integrations/stripe/resource';
|
|
21
25
|
import dayjs from '../libs/dayjs';
|
|
22
26
|
import logger from '../libs/logger';
|
|
23
27
|
import { isCreditSufficientForPayment, isDelegationSufficientForPayment } from '../libs/payment';
|
|
@@ -69,7 +73,7 @@ import { ensureInvoiceForCheckout } from './connect/shared';
|
|
|
69
73
|
|
|
70
74
|
const router = Router();
|
|
71
75
|
|
|
72
|
-
const user =
|
|
76
|
+
const user = sessionMiddleware();
|
|
73
77
|
const auth = authenticate<CheckoutSession>({ component: true, roles: ['owner', 'admin'] });
|
|
74
78
|
|
|
75
79
|
const getPaymentMethods = async (doc: CheckoutSession) => {
|
|
@@ -141,11 +145,11 @@ const SubscriptionDataSchema = Joi.object({
|
|
|
141
145
|
.items(
|
|
142
146
|
Joi.object({
|
|
143
147
|
name: Joi.string().optional(),
|
|
144
|
-
color: Joi.string().
|
|
145
|
-
variant: Joi.string().
|
|
148
|
+
color: Joi.string().valid('primary', 'secondary', 'success', 'error', 'warning').optional(),
|
|
149
|
+
variant: Joi.string().valid('text', 'contained', 'outlined').optional(),
|
|
146
150
|
text: Joi.object().required(),
|
|
147
151
|
link: Joi.string().uri().required(),
|
|
148
|
-
type: Joi.string().
|
|
152
|
+
type: Joi.string().valid('notification', 'custom').optional(),
|
|
149
153
|
triggerEvents: Joi.array().items(Joi.string()).optional(),
|
|
150
154
|
})
|
|
151
155
|
)
|
|
@@ -1027,6 +1031,18 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1027
1031
|
trialInDays,
|
|
1028
1032
|
trialEnd
|
|
1029
1033
|
);
|
|
1034
|
+
const stripeCustomer = await ensureStripePaymentCustomer(subscription, paymentMethod);
|
|
1035
|
+
if (stripeSubscription) {
|
|
1036
|
+
await subscription.update({
|
|
1037
|
+
payment_details: {
|
|
1038
|
+
stripe: {
|
|
1039
|
+
customer_id: stripeCustomer.id,
|
|
1040
|
+
subscription_id: stripeSubscription.id,
|
|
1041
|
+
setup_intent_id: stripeSubscription.pending_setup_intent?.id,
|
|
1042
|
+
},
|
|
1043
|
+
},
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1030
1046
|
logger.info('ensureStripeSubscription', {
|
|
1031
1047
|
subscriptionId: subscription.id,
|
|
1032
1048
|
stripeSubscriptionId: stripeSubscription?.id,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
|
|
2
2
|
import type { CallbackArgs } from '../../libs/auth';
|
|
3
|
-
import { isDelegationSufficientForPayment } from '../../libs/payment';
|
|
4
|
-
import { getFastCheckoutAmount } from '../../libs/session';
|
|
5
3
|
import { getTxMetadata } from '../../libs/util';
|
|
6
|
-
import type
|
|
4
|
+
import { Lock, type TLineItemExpanded } from '../../store/models';
|
|
7
5
|
import {
|
|
8
6
|
ensureChangePaymentContext,
|
|
7
|
+
ensureStakeInvoice,
|
|
9
8
|
executeOcapTransactions,
|
|
10
9
|
getAuthPrincipalClaim,
|
|
11
10
|
getDelegationTxClaim,
|
|
12
11
|
getStakeTxClaim,
|
|
12
|
+
updateStripeSubscriptionAfterChangePayment,
|
|
13
13
|
} from './shared';
|
|
14
14
|
|
|
15
15
|
export default {
|
|
@@ -23,42 +23,31 @@ export default {
|
|
|
23
23
|
},
|
|
24
24
|
onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
|
|
25
25
|
const { subscriptionId } = extraParams;
|
|
26
|
-
const { subscription,
|
|
26
|
+
const { subscription, paymentMethod, paymentCurrency } = await ensureChangePaymentContext(subscriptionId);
|
|
27
27
|
|
|
28
28
|
const claims: { [type: string]: [string, object] } = {};
|
|
29
29
|
|
|
30
30
|
// @ts-ignore
|
|
31
31
|
const items = subscription!.items as TLineItemExpanded[];
|
|
32
|
-
const trialing =
|
|
32
|
+
const trialing = true;
|
|
33
33
|
const billingThreshold = Number(subscription.billing_thresholds?.amount_gte || 0);
|
|
34
|
-
const fastCheckoutAmount = getFastCheckoutAmount(items, 'setup', paymentCurrency.id, trialing);
|
|
35
34
|
|
|
36
35
|
if (paymentMethod.type === 'arcblock') {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
nonce: `change-method-${subscription.id}`,
|
|
53
|
-
data: getTxMetadata({ subscriptionId: subscription.id }),
|
|
54
|
-
paymentCurrency,
|
|
55
|
-
paymentMethod,
|
|
56
|
-
trialing,
|
|
57
|
-
billingThreshold,
|
|
58
|
-
items,
|
|
59
|
-
}),
|
|
60
|
-
];
|
|
61
|
-
}
|
|
36
|
+
claims.delegation = [
|
|
37
|
+
'signature',
|
|
38
|
+
await getDelegationTxClaim({
|
|
39
|
+
mode: 'setup',
|
|
40
|
+
userDid,
|
|
41
|
+
userPk,
|
|
42
|
+
nonce: `change-method-${subscription.id}`,
|
|
43
|
+
data: getTxMetadata({ subscriptionId: subscription.id }),
|
|
44
|
+
paymentCurrency,
|
|
45
|
+
paymentMethod,
|
|
46
|
+
trialing,
|
|
47
|
+
billingThreshold,
|
|
48
|
+
items,
|
|
49
|
+
}),
|
|
50
|
+
];
|
|
62
51
|
|
|
63
52
|
// we always need to stake for the subscription
|
|
64
53
|
claims.staking = [
|
|
@@ -105,7 +94,7 @@ export default {
|
|
|
105
94
|
|
|
106
95
|
onAuth: async ({ request, userDid, userPk, claims, extraParams }: CallbackArgs) => {
|
|
107
96
|
const { subscriptionId } = extraParams;
|
|
108
|
-
const { subscription, setupIntent, paymentCurrency, paymentMethod } =
|
|
97
|
+
const { subscription, setupIntent, paymentCurrency, paymentMethod, customer } =
|
|
109
98
|
await ensureChangePaymentContext(subscriptionId);
|
|
110
99
|
|
|
111
100
|
const prepareTxExecution = async () => {
|
|
@@ -135,18 +124,46 @@ export default {
|
|
|
135
124
|
await subscription?.update({
|
|
136
125
|
currency_id: paymentCurrency.id,
|
|
137
126
|
default_payment_method_id: paymentMethod.id,
|
|
127
|
+
payment_details: {
|
|
128
|
+
...subscription.payment_details,
|
|
129
|
+
[paymentMethod.type]: paymentDetails,
|
|
130
|
+
},
|
|
138
131
|
});
|
|
132
|
+
|
|
133
|
+
await Lock.acquire(`${subscription.id}-change-plan`, subscription.current_period_end);
|
|
134
|
+
// update stripe subscription
|
|
135
|
+
await updateStripeSubscriptionAfterChangePayment(setupIntent, subscription);
|
|
139
136
|
};
|
|
140
137
|
|
|
141
138
|
if (paymentMethod.type === 'arcblock') {
|
|
142
139
|
await prepareTxExecution();
|
|
143
|
-
const paymentDetails = await executeOcapTransactions(
|
|
140
|
+
const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
|
|
144
141
|
userDid,
|
|
145
142
|
userPk,
|
|
146
143
|
claims,
|
|
147
144
|
paymentMethod,
|
|
148
145
|
request,
|
|
149
|
-
subscription?.id
|
|
146
|
+
subscription?.id,
|
|
147
|
+
paymentCurrency.contract
|
|
148
|
+
);
|
|
149
|
+
await ensureStakeInvoice(
|
|
150
|
+
{
|
|
151
|
+
total: stakingAmount,
|
|
152
|
+
description: 'Stake for subscription payment change',
|
|
153
|
+
currency_id: paymentCurrency.id,
|
|
154
|
+
metadata: {
|
|
155
|
+
payment_details: {
|
|
156
|
+
arcblock: {
|
|
157
|
+
tx_hash: paymentDetails?.staking?.tx_hash,
|
|
158
|
+
payer: paymentDetails?.payer,
|
|
159
|
+
address: paymentDetails?.staking?.address,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
subscription!,
|
|
165
|
+
paymentMethod,
|
|
166
|
+
customer!
|
|
150
167
|
);
|
|
151
168
|
await afterTxExecution(paymentDetails);
|
|
152
169
|
return { hash: paymentDetails.tx_hash };
|
|
@@ -8,6 +8,7 @@ import { invoiceQueue } from '../../queues/invoice';
|
|
|
8
8
|
import { addSubscriptionJob, subscriptionQueue } from '../../queues/subscription';
|
|
9
9
|
import type { TLineItemExpanded } from '../../store/models';
|
|
10
10
|
import {
|
|
11
|
+
ensureStakeInvoice,
|
|
11
12
|
ensureSubscription,
|
|
12
13
|
executeOcapTransactions,
|
|
13
14
|
getAuthPrincipalClaim,
|
|
@@ -107,7 +108,8 @@ export default {
|
|
|
107
108
|
|
|
108
109
|
onAuth: async ({ request, userDid, userPk, claims, extraParams }: CallbackArgs) => {
|
|
109
110
|
const { subscriptionId } = extraParams;
|
|
110
|
-
const { invoice, paymentMethod, subscription } =
|
|
111
|
+
const { invoice, paymentMethod, subscription, paymentCurrency, customer } =
|
|
112
|
+
await ensureSubscription(subscriptionId);
|
|
111
113
|
|
|
112
114
|
const prepareTxExecution = async () => {
|
|
113
115
|
await subscription?.update({
|
|
@@ -142,13 +144,33 @@ export default {
|
|
|
142
144
|
if (paymentMethod.type === 'arcblock') {
|
|
143
145
|
await prepareTxExecution();
|
|
144
146
|
|
|
145
|
-
const paymentDetails = await executeOcapTransactions(
|
|
147
|
+
const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
|
|
146
148
|
userDid,
|
|
147
149
|
userPk,
|
|
148
150
|
claims,
|
|
149
151
|
paymentMethod,
|
|
150
152
|
request,
|
|
151
|
-
subscription?.id
|
|
153
|
+
subscription?.id,
|
|
154
|
+
paymentCurrency?.contract
|
|
155
|
+
);
|
|
156
|
+
await ensureStakeInvoice(
|
|
157
|
+
{
|
|
158
|
+
total: stakingAmount,
|
|
159
|
+
description: 'Stake for subscription plan change',
|
|
160
|
+
currency_id: paymentCurrency.id,
|
|
161
|
+
metadata: {
|
|
162
|
+
payment_details: {
|
|
163
|
+
arcblock: {
|
|
164
|
+
tx_hash: paymentDetails?.staking?.tx_hash,
|
|
165
|
+
payer: paymentDetails?.payer,
|
|
166
|
+
address: paymentDetails?.staking?.address,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
subscription!,
|
|
172
|
+
paymentMethod,
|
|
173
|
+
customer!
|
|
152
174
|
);
|
|
153
175
|
await afterTxExecution(paymentDetails);
|
|
154
176
|
|
|
@@ -6,7 +6,7 @@ import { executeEvmTransaction, waitForEvmTxConfirm } from 'api/src/integrations
|
|
|
6
6
|
import type { CallbackArgs } from '../../libs/auth';
|
|
7
7
|
import { getGasPayerExtra } from '../../libs/payment';
|
|
8
8
|
import { getTxMetadata } from '../../libs/util';
|
|
9
|
-
import { ensureSubscriptionRecharge, getAuthPrincipalClaim } from './shared';
|
|
9
|
+
import { ensureRechargeInvoice, ensureSubscriptionRecharge, getAuthPrincipalClaim } from './shared';
|
|
10
10
|
import logger from '../../libs/logger';
|
|
11
11
|
|
|
12
12
|
export default {
|
|
@@ -74,10 +74,29 @@ export default {
|
|
|
74
74
|
},
|
|
75
75
|
onAuth: async ({ request, userDid, claims, extraParams }: CallbackArgs) => {
|
|
76
76
|
const { subscriptionId } = extraParams;
|
|
77
|
-
const { paymentMethod, paymentCurrency, receiverAddress } =
|
|
77
|
+
const { paymentMethod, paymentCurrency, receiverAddress, subscription, customer } =
|
|
78
|
+
await ensureSubscriptionRecharge(subscriptionId);
|
|
78
79
|
let { amount } = extraParams;
|
|
79
80
|
amount = fromTokenToUnit(amount, paymentCurrency.decimal).toString();
|
|
80
81
|
|
|
82
|
+
const afterTxExecution = async (paymentDetails: any) => {
|
|
83
|
+
await ensureRechargeInvoice(
|
|
84
|
+
{
|
|
85
|
+
total: amount,
|
|
86
|
+
description: 'Subscription recharge',
|
|
87
|
+
currency_id: paymentCurrency.id,
|
|
88
|
+
metadata: {
|
|
89
|
+
payment_details: {
|
|
90
|
+
[paymentMethod.type]: paymentDetails,
|
|
91
|
+
receiverAddress,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
subscription!,
|
|
96
|
+
paymentMethod,
|
|
97
|
+
customer!
|
|
98
|
+
);
|
|
99
|
+
};
|
|
81
100
|
if (paymentMethod.type === 'arcblock') {
|
|
82
101
|
try {
|
|
83
102
|
const client = paymentMethod.getOcapClient();
|
|
@@ -104,6 +123,11 @@ export default {
|
|
|
104
123
|
paymentMethod: paymentMethod.type,
|
|
105
124
|
});
|
|
106
125
|
|
|
126
|
+
await afterTxExecution({
|
|
127
|
+
tx_hash: txHash,
|
|
128
|
+
payer: userDid,
|
|
129
|
+
type: 'transfer',
|
|
130
|
+
});
|
|
107
131
|
return { hash: txHash };
|
|
108
132
|
} catch (err) {
|
|
109
133
|
console.error(err);
|
|
@@ -120,13 +144,14 @@ export default {
|
|
|
120
144
|
Number(paymentDetails.block_height),
|
|
121
145
|
paymentMethod.confirmation.block
|
|
122
146
|
)
|
|
123
|
-
.then(() => {
|
|
147
|
+
.then(async () => {
|
|
124
148
|
logger.info('Recharge successful', {
|
|
125
149
|
receiverAddress,
|
|
126
150
|
amount,
|
|
127
151
|
subscriptionId,
|
|
128
152
|
paymentMethod: paymentMethod.type,
|
|
129
153
|
});
|
|
154
|
+
await afterTxExecution(paymentDetails);
|
|
130
155
|
})
|
|
131
156
|
.catch(console.error);
|
|
132
157
|
|