payment-kit 1.16.16 → 1.16.18
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/index.ts +1 -1
- package/api/src/hooks/pre-start.ts +2 -0
- package/api/src/index.ts +2 -0
- package/api/src/integrations/arcblock/stake.ts +7 -1
- package/api/src/integrations/stripe/resource.ts +1 -1
- package/api/src/libs/env.ts +12 -0
- package/api/src/libs/event.ts +8 -0
- package/api/src/libs/invoice.ts +585 -3
- package/api/src/libs/notification/template/subscription-succeeded.ts +1 -2
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +2 -2
- package/api/src/libs/notification/template/subscription-will-renew.ts +6 -2
- package/api/src/libs/notification/template/subscription.overdraft-protection.exhausted.ts +139 -0
- package/api/src/libs/overdraft-protection.ts +85 -0
- package/api/src/libs/payment.ts +1 -65
- package/api/src/libs/queue/index.ts +0 -1
- package/api/src/libs/subscription.ts +532 -2
- package/api/src/libs/util.ts +4 -0
- package/api/src/locales/en.ts +5 -0
- package/api/src/locales/zh.ts +5 -0
- package/api/src/queues/event.ts +3 -2
- package/api/src/queues/invoice.ts +28 -3
- package/api/src/queues/notification.ts +25 -3
- package/api/src/queues/payment.ts +154 -3
- package/api/src/queues/refund.ts +2 -2
- package/api/src/queues/subscription.ts +215 -4
- package/api/src/queues/webhook.ts +1 -0
- package/api/src/routes/connect/change-payment.ts +1 -1
- package/api/src/routes/connect/change-plan.ts +1 -1
- package/api/src/routes/connect/overdraft-protection.ts +120 -0
- package/api/src/routes/connect/recharge.ts +2 -1
- package/api/src/routes/connect/setup.ts +1 -1
- package/api/src/routes/connect/shared.ts +117 -350
- package/api/src/routes/connect/subscribe.ts +1 -1
- package/api/src/routes/customers.ts +2 -2
- package/api/src/routes/invoices.ts +9 -4
- package/api/src/routes/subscriptions.ts +172 -2
- package/api/src/store/migrate.ts +9 -10
- package/api/src/store/migrations/20240905-index.ts +95 -60
- package/api/src/store/migrations/20241203-overdraft-protection.ts +25 -0
- package/api/src/store/migrations/20241216-update-overdraft-protection.ts +30 -0
- package/api/src/store/models/customer.ts +2 -2
- package/api/src/store/models/invoice.ts +7 -0
- package/api/src/store/models/lock.ts +7 -0
- package/api/src/store/models/subscription.ts +15 -0
- package/api/src/store/sequelize.ts +6 -1
- package/blocklet.yml +1 -1
- package/package.json +23 -23
- package/src/components/customer/overdraft-protection.tsx +367 -0
- package/src/components/event/list.tsx +3 -4
- package/src/components/subscription/actions/cancel.tsx +3 -0
- package/src/components/subscription/portal/actions.tsx +324 -77
- package/src/components/uploader.tsx +31 -26
- package/src/env.d.ts +1 -0
- package/src/hooks/subscription.ts +30 -0
- package/src/libs/env.ts +4 -0
- package/src/locales/en.tsx +41 -0
- package/src/locales/zh.tsx +37 -0
- package/src/pages/admin/billing/invoices/detail.tsx +16 -15
- package/src/pages/customer/index.tsx +7 -2
- package/src/pages/customer/invoice/detail.tsx +29 -5
- package/src/pages/customer/invoice/past-due.tsx +18 -4
- package/src/pages/customer/recharge.tsx +2 -4
- package/src/pages/customer/subscription/change-payment.tsx +7 -1
- package/src/pages/customer/subscription/detail.tsx +69 -51
- package/tsconfig.json +0 -5
- package/api/tests/libs/payment.spec.ts +0 -168
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { CallbackArgs } from '../../libs/auth';
|
|
2
|
+
import { isSubscriptionOverdraftProtectionEnabled } from '../../libs/subscription';
|
|
3
|
+
import { Lock } from '../../store/models';
|
|
4
|
+
import {
|
|
5
|
+
ensureSubscriptionForOverdraftProtection,
|
|
6
|
+
executeOcapTransactions,
|
|
7
|
+
getAuthPrincipalClaim,
|
|
8
|
+
getOverdraftProtectionStakeTxClaim,
|
|
9
|
+
} from './shared';
|
|
10
|
+
import { ensureStakeInvoice } from '../../libs/invoice';
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
action: 'overdraft-protection',
|
|
14
|
+
authPrincipal: false,
|
|
15
|
+
claims: {
|
|
16
|
+
authPrincipal: async ({ extraParams }: CallbackArgs) => {
|
|
17
|
+
const { paymentMethod } = await ensureSubscriptionForOverdraftProtection(
|
|
18
|
+
extraParams.subscriptionId,
|
|
19
|
+
extraParams.amount
|
|
20
|
+
);
|
|
21
|
+
return getAuthPrincipalClaim(paymentMethod, 'continue');
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
|
|
25
|
+
const { subscriptionId } = extraParams;
|
|
26
|
+
const { subscription, paymentMethod, paymentCurrency, stakeAmount } =
|
|
27
|
+
await ensureSubscriptionForOverdraftProtection(subscriptionId, extraParams.amount);
|
|
28
|
+
const { remaining } = await isSubscriptionOverdraftProtectionEnabled(subscription);
|
|
29
|
+
|
|
30
|
+
const payerAddress = subscription.overdraft_protection?.payment_details?.arcblock?.payer;
|
|
31
|
+
if (userDid !== payerAddress && remaining !== '0') {
|
|
32
|
+
// if previous account has remaining overdraft protection, we don't allow to use new account to pay for it
|
|
33
|
+
throw new Error(
|
|
34
|
+
`You are not the payer for this subscription. Expected payer: ${payerAddress}, but found: ${userDid}.`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const claims: { [type: string]: [string, object] } = {};
|
|
39
|
+
|
|
40
|
+
if (paymentMethod.type === 'arcblock') {
|
|
41
|
+
// we always need to stake for the subscription
|
|
42
|
+
claims.staking = [
|
|
43
|
+
'prepareTx',
|
|
44
|
+
await getOverdraftProtectionStakeTxClaim({
|
|
45
|
+
userDid,
|
|
46
|
+
userPk,
|
|
47
|
+
paymentCurrency,
|
|
48
|
+
paymentMethod,
|
|
49
|
+
stakeAmount,
|
|
50
|
+
subscription,
|
|
51
|
+
}),
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
return claims;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
throw new Error(`OverdraftProtection: Payment method ${paymentMethod.type} not supported`);
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
onAuth: async ({ request, userDid, userPk, claims, extraParams }: CallbackArgs) => {
|
|
61
|
+
const { subscriptionId, amount } = extraParams;
|
|
62
|
+
const { subscription, paymentCurrency, paymentMethod, customer } = await ensureSubscriptionForOverdraftProtection(
|
|
63
|
+
subscriptionId,
|
|
64
|
+
amount
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const afterTxExecution = async (paymentDetails: any) => {
|
|
68
|
+
await subscription?.update({
|
|
69
|
+
overdraft_protection: {
|
|
70
|
+
payment_details: {
|
|
71
|
+
...(subscription.overdraft_protection?.payment_details || {}),
|
|
72
|
+
[paymentMethod.type]: paymentDetails,
|
|
73
|
+
},
|
|
74
|
+
enabled: subscription.overdraft_protection?.enabled || false,
|
|
75
|
+
payment_method_id: paymentMethod.id,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// release the exhausted lock, so that the notification can be sent again if overdraft protection exhausted
|
|
80
|
+
await Lock.release(`${subscription.id}-${paymentCurrency.id}-overdraft-protection-exhausted`);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (paymentMethod.type === 'arcblock') {
|
|
84
|
+
const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
|
|
85
|
+
userDid,
|
|
86
|
+
userPk,
|
|
87
|
+
claims,
|
|
88
|
+
paymentMethod,
|
|
89
|
+
request,
|
|
90
|
+
subscription?.id,
|
|
91
|
+
paymentCurrency.contract,
|
|
92
|
+
`overdraft-protection-${subscription.id}`
|
|
93
|
+
);
|
|
94
|
+
await ensureStakeInvoice(
|
|
95
|
+
{
|
|
96
|
+
total: stakingAmount,
|
|
97
|
+
description: 'Stake for subscription overdraft protection',
|
|
98
|
+
currency_id: paymentCurrency.id,
|
|
99
|
+
billing_reason: 'stake_overdraft_protection',
|
|
100
|
+
metadata: {
|
|
101
|
+
payment_details: {
|
|
102
|
+
arcblock: {
|
|
103
|
+
tx_hash: paymentDetails?.staking?.tx_hash,
|
|
104
|
+
payer: paymentDetails?.payer,
|
|
105
|
+
address: paymentDetails?.staking?.address,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
subscription!,
|
|
111
|
+
paymentMethod,
|
|
112
|
+
customer!
|
|
113
|
+
);
|
|
114
|
+
await afterTxExecution(paymentDetails);
|
|
115
|
+
return { hash: paymentDetails.tx_hash };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
throw new Error(`OverdraftProtection: Payment method ${paymentMethod.type} not supported`);
|
|
119
|
+
},
|
|
120
|
+
};
|
|
@@ -6,8 +6,9 @@ import { executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/e
|
|
|
6
6
|
import type { CallbackArgs } from '../../libs/auth';
|
|
7
7
|
import { getGasPayerExtra } from '../../libs/payment';
|
|
8
8
|
import { getTxMetadata } from '../../libs/util';
|
|
9
|
-
import {
|
|
9
|
+
import { ensureSubscriptionRecharge, getAuthPrincipalClaim } from './shared';
|
|
10
10
|
import logger from '../../libs/logger';
|
|
11
|
+
import { ensureRechargeInvoice } from '../../libs/invoice';
|
|
11
12
|
|
|
12
13
|
export default {
|
|
13
14
|
action: 'recharge',
|
|
@@ -11,12 +11,12 @@ import { addSubscriptionJob } from '../../queues/subscription';
|
|
|
11
11
|
import type { TLineItemExpanded } from '../../store/models';
|
|
12
12
|
import {
|
|
13
13
|
ensureSetupIntent,
|
|
14
|
-
ensureStakeInvoice,
|
|
15
14
|
executeOcapTransactions,
|
|
16
15
|
getAuthPrincipalClaim,
|
|
17
16
|
getDelegationTxClaim,
|
|
18
17
|
getStakeTxClaim,
|
|
19
18
|
} from './shared';
|
|
19
|
+
import { ensureStakeInvoice } from '../../libs/invoice';
|
|
20
20
|
|
|
21
21
|
export default {
|
|
22
22
|
action: 'setup',
|
|
@@ -11,18 +11,17 @@ import isEmpty from 'lodash/isEmpty';
|
|
|
11
11
|
import { estimateMaxGasForTx, hasStakedForGas } from '../../integrations/arcblock/stake';
|
|
12
12
|
import { encodeApproveItx } from '../../integrations/ethereum/token';
|
|
13
13
|
import { blocklet, ethWallet, wallet } from '../../libs/auth';
|
|
14
|
-
import dayjs from '../../libs/dayjs';
|
|
15
14
|
import logger from '../../libs/logger';
|
|
16
15
|
import { getGasPayerExtra, getTokenLimitsForDelegation } from '../../libs/payment';
|
|
17
|
-
import { getFastCheckoutAmount,
|
|
16
|
+
import { getFastCheckoutAmount, getStatementDescriptor } from '../../libs/session';
|
|
18
17
|
import {
|
|
19
18
|
expandSubscriptionItems,
|
|
20
19
|
getSubscriptionCreateSetup,
|
|
21
|
-
getSubscriptionItemPrice,
|
|
22
20
|
getSubscriptionPaymentAddress,
|
|
23
21
|
getSubscriptionStakeSetup,
|
|
24
22
|
} from '../../libs/subscription';
|
|
25
23
|
import { getCustomerStakeAddress, OCAP_PAYMENT_TX_TYPE } from '../../libs/util';
|
|
24
|
+
|
|
26
25
|
import { invoiceQueue } from '../../queues/invoice';
|
|
27
26
|
import type { TLineItemExpanded } from '../../store/models';
|
|
28
27
|
import { CheckoutSession } from '../../store/models/checkout-session';
|
|
@@ -35,7 +34,7 @@ import { PaymentMethod } from '../../store/models/payment-method';
|
|
|
35
34
|
import { Price } from '../../store/models/price';
|
|
36
35
|
import { SetupIntent } from '../../store/models/setup-intent';
|
|
37
36
|
import { Subscription } from '../../store/models/subscription';
|
|
38
|
-
import {
|
|
37
|
+
import { ensureInvoiceAndItems } from '../../libs/invoice';
|
|
39
38
|
|
|
40
39
|
type Result = {
|
|
41
40
|
checkoutSession: CheckoutSession;
|
|
@@ -401,184 +400,6 @@ export async function ensureInvoiceForCheckout({
|
|
|
401
400
|
return { invoice, items };
|
|
402
401
|
}
|
|
403
402
|
|
|
404
|
-
export async function ensureInvoiceAndItems({
|
|
405
|
-
customer,
|
|
406
|
-
currency,
|
|
407
|
-
subscription,
|
|
408
|
-
props,
|
|
409
|
-
lineItems,
|
|
410
|
-
trialing,
|
|
411
|
-
metered,
|
|
412
|
-
applyCredit = true,
|
|
413
|
-
}: {
|
|
414
|
-
customer: Customer;
|
|
415
|
-
currency: PaymentCurrency;
|
|
416
|
-
subscription?: Subscription;
|
|
417
|
-
props: TInvoice;
|
|
418
|
-
lineItems: TLineItemExpanded[];
|
|
419
|
-
trialing: boolean; // do we have trialing
|
|
420
|
-
metered: boolean; // is the quantity metered
|
|
421
|
-
applyCredit?: boolean; // should we apply customer credit?
|
|
422
|
-
}): Promise<{ invoice: Invoice; items: InvoiceItem[] }> {
|
|
423
|
-
// apply possible balance to invoice
|
|
424
|
-
let remaining = props.total;
|
|
425
|
-
let result = { starting: {}, ending: {} };
|
|
426
|
-
if (applyCredit && props.total > '0') {
|
|
427
|
-
const balance = customer.getBalanceToApply(currency.id, props.total);
|
|
428
|
-
result = await customer.decreaseTokenBalance(currency.id, balance);
|
|
429
|
-
remaining = new BN(props.total).sub(new BN(balance)).toString();
|
|
430
|
-
logger.info('Invoice will use customer credit', { result, remaining, total: props.total });
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const invoice = await Invoice.create({
|
|
434
|
-
livemode: props.livemode,
|
|
435
|
-
number: await customer.getInvoiceNumber(),
|
|
436
|
-
description: props.description,
|
|
437
|
-
statement_descriptor: props.statement_descriptor,
|
|
438
|
-
period_start: props.period_start,
|
|
439
|
-
period_end: props.period_end,
|
|
440
|
-
|
|
441
|
-
auto_advance: props.auto_advance,
|
|
442
|
-
paid: false,
|
|
443
|
-
paid_out_of_band: false,
|
|
444
|
-
|
|
445
|
-
status: props.status || 'open',
|
|
446
|
-
collection_method: 'charge_automatically',
|
|
447
|
-
billing_reason: props.billing_reason,
|
|
448
|
-
|
|
449
|
-
currency_id: props.currency_id,
|
|
450
|
-
customer_id: customer.id,
|
|
451
|
-
payment_intent_id: props.payment_intent_id || '',
|
|
452
|
-
subscription_id: subscription?.id,
|
|
453
|
-
checkout_session_id: props.checkout_session_id || '',
|
|
454
|
-
|
|
455
|
-
total: props.total || '0',
|
|
456
|
-
subtotal: props.total || '0',
|
|
457
|
-
tax: '0',
|
|
458
|
-
subtotal_excluding_tax: props.total || '0',
|
|
459
|
-
|
|
460
|
-
amount_due: props.amount_due || remaining,
|
|
461
|
-
amount_paid: props.amount_paid || '0',
|
|
462
|
-
amount_remaining: props.amount_remaining || remaining,
|
|
463
|
-
amount_shipping: '0',
|
|
464
|
-
|
|
465
|
-
starting_balance: '0',
|
|
466
|
-
ending_balance: '0',
|
|
467
|
-
starting_token_balance: result.starting,
|
|
468
|
-
ending_token_balance: result.ending,
|
|
469
|
-
|
|
470
|
-
attempt_count: 0,
|
|
471
|
-
attempted: false,
|
|
472
|
-
// next_payment_attempt: undefined,
|
|
473
|
-
|
|
474
|
-
custom_fields: props.custom_fields || [],
|
|
475
|
-
customer_address: customer.address,
|
|
476
|
-
customer_email: customer.email,
|
|
477
|
-
customer_name: customer.name,
|
|
478
|
-
customer_phone: customer.phone,
|
|
479
|
-
|
|
480
|
-
discounts: [],
|
|
481
|
-
total_discount_amounts: [],
|
|
482
|
-
|
|
483
|
-
due_date: undefined, // The date on which payment for this invoice is due
|
|
484
|
-
effective_at: dayjs().unix(), // The date when this invoice is in effect
|
|
485
|
-
status_transitions: {
|
|
486
|
-
finalized_at: dayjs().unix(),
|
|
487
|
-
},
|
|
488
|
-
|
|
489
|
-
payment_settings: subscription?.payment_settings,
|
|
490
|
-
default_payment_method_id: props.default_payment_method_id,
|
|
491
|
-
|
|
492
|
-
account_country: '',
|
|
493
|
-
account_name: '',
|
|
494
|
-
footer: props.footer || '',
|
|
495
|
-
metadata: props.metadata || {},
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
// create invoice items: for those require payment this time
|
|
499
|
-
const subscriptionItems = subscription
|
|
500
|
-
? await SubscriptionItem.findAll({ where: { subscription_id: subscription?.id } })
|
|
501
|
-
: [];
|
|
502
|
-
|
|
503
|
-
const getLineSetup = (x: TLineItemExpanded) => {
|
|
504
|
-
const price = getSubscriptionItemPrice(x);
|
|
505
|
-
if (price.type === 'recurring' && trialing) {
|
|
506
|
-
return {
|
|
507
|
-
price,
|
|
508
|
-
amount: '0',
|
|
509
|
-
// @ts-ignore
|
|
510
|
-
description: trialing ? `${price.product.name} (trialing)` : price.product.name,
|
|
511
|
-
period: {
|
|
512
|
-
start: props.period_start,
|
|
513
|
-
end: props.period_end,
|
|
514
|
-
},
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
return {
|
|
519
|
-
price,
|
|
520
|
-
amount:
|
|
521
|
-
x.custom_amount ||
|
|
522
|
-
new BN(getPriceUintAmountByCurrency(price, props.currency_id)).mul(new BN(x.quantity)).toString(),
|
|
523
|
-
// @ts-ignore
|
|
524
|
-
description: price.product.name,
|
|
525
|
-
period: undefined,
|
|
526
|
-
};
|
|
527
|
-
};
|
|
528
|
-
|
|
529
|
-
const items = await Promise.all(
|
|
530
|
-
lineItems.map((x: TLineItemExpanded) => {
|
|
531
|
-
const setup = getLineSetup(x);
|
|
532
|
-
const { price } = setup;
|
|
533
|
-
let { quantity } = x;
|
|
534
|
-
if (price.type === 'recurring') {
|
|
535
|
-
if (price.recurring?.usage_type === 'metered' && !metered) {
|
|
536
|
-
quantity = 0;
|
|
537
|
-
}
|
|
538
|
-
if (trialing) {
|
|
539
|
-
quantity = 0;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
return InvoiceItem.create({
|
|
544
|
-
livemode: !!props.livemode,
|
|
545
|
-
amount: quantity > 0 ? setup.amount : '0',
|
|
546
|
-
quantity,
|
|
547
|
-
description: setup.description,
|
|
548
|
-
period: setup.period,
|
|
549
|
-
currency_id: props.currency_id,
|
|
550
|
-
customer_id: customer.id,
|
|
551
|
-
price_id: price.id,
|
|
552
|
-
invoice_id: invoice.id,
|
|
553
|
-
subscription_id: subscription?.id,
|
|
554
|
-
subscription_item_id: subscriptionItems.find((si) => si.price_id === price.id)?.id,
|
|
555
|
-
discountable: false,
|
|
556
|
-
discounts: [],
|
|
557
|
-
discount_amounts: [],
|
|
558
|
-
proration: false,
|
|
559
|
-
proration_details: {},
|
|
560
|
-
metadata: x.metadata || {},
|
|
561
|
-
});
|
|
562
|
-
})
|
|
563
|
-
);
|
|
564
|
-
|
|
565
|
-
return { invoice, items };
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
export async function cleanupInvoiceAndItems(invoiceId: string) {
|
|
569
|
-
const invoice = await Invoice.findByPk(invoiceId);
|
|
570
|
-
if (!invoice) {
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
if (invoice.isImmutable()) {
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
const removedItem = await InvoiceItem.destroy({ where: { invoice_id: invoiceId } });
|
|
578
|
-
const removedInvoice = await Invoice.destroy({ where: { id: invoiceId } });
|
|
579
|
-
logger.info('cleanup invoice and items', { invoiceId, removedItem, removedInvoice });
|
|
580
|
-
}
|
|
581
|
-
|
|
582
403
|
export async function ensureInvoiceForCollect(invoiceId: string) {
|
|
583
404
|
const invoice = await Invoice.findByPk(invoiceId);
|
|
584
405
|
if (!invoice) {
|
|
@@ -717,6 +538,7 @@ export async function getDelegationTxClaim({
|
|
|
717
538
|
paymentMethod,
|
|
718
539
|
paymentCurrency,
|
|
719
540
|
});
|
|
541
|
+
|
|
720
542
|
if (mode === 'delegation') {
|
|
721
543
|
tokenRequirements = [];
|
|
722
544
|
}
|
|
@@ -863,6 +685,82 @@ export async function getStakeTxClaim({
|
|
|
863
685
|
throw new Error(`getStakeTxClaim: Payment method ${paymentMethod.type} not supported`);
|
|
864
686
|
}
|
|
865
687
|
|
|
688
|
+
export async function getOverdraftProtectionStakeTxClaim({
|
|
689
|
+
userDid,
|
|
690
|
+
userPk,
|
|
691
|
+
subscription,
|
|
692
|
+
paymentCurrency,
|
|
693
|
+
paymentMethod,
|
|
694
|
+
stakeAmount,
|
|
695
|
+
}: {
|
|
696
|
+
userDid: string;
|
|
697
|
+
userPk: string;
|
|
698
|
+
subscription: Subscription;
|
|
699
|
+
paymentCurrency: PaymentCurrency;
|
|
700
|
+
paymentMethod: PaymentMethod;
|
|
701
|
+
stakeAmount: string;
|
|
702
|
+
}) {
|
|
703
|
+
// create staking amount
|
|
704
|
+
logger.info('getStakeTxClaim', {
|
|
705
|
+
subscriptionId: subscription.id,
|
|
706
|
+
paymentCurrencyId: paymentCurrency.id,
|
|
707
|
+
paymentMethodId: paymentMethod.id,
|
|
708
|
+
stakeAmount,
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
if (paymentMethod.type === 'arcblock') {
|
|
712
|
+
// create staking data
|
|
713
|
+
const client = paymentMethod.getOcapClient();
|
|
714
|
+
const nonce = `overdraft-protection-${subscription.id}`;
|
|
715
|
+
const address = await getCustomerStakeAddress(userDid, nonce);
|
|
716
|
+
const { state } = await client.getStakeState({ address });
|
|
717
|
+
const data = {
|
|
718
|
+
type: 'json',
|
|
719
|
+
value: Object.assign(
|
|
720
|
+
{
|
|
721
|
+
appId: wallet.address,
|
|
722
|
+
subscriptionId: subscription.id,
|
|
723
|
+
},
|
|
724
|
+
JSON.parse(state?.data?.value || '{}')
|
|
725
|
+
),
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
return {
|
|
729
|
+
type: 'StakeTx',
|
|
730
|
+
description: `Stake to complete subscription ${subscription.id} overdraft protection`,
|
|
731
|
+
partialTx: {
|
|
732
|
+
from: userDid,
|
|
733
|
+
pk: userPk,
|
|
734
|
+
itx: {
|
|
735
|
+
address,
|
|
736
|
+
receiver: wallet.address,
|
|
737
|
+
slashers: [wallet.address],
|
|
738
|
+
revokeWaitingPeriod: 0,
|
|
739
|
+
message: `Stake for subscription ${subscription.id} overdraft protection`,
|
|
740
|
+
nonce,
|
|
741
|
+
inputs: [],
|
|
742
|
+
data,
|
|
743
|
+
},
|
|
744
|
+
signatures: [],
|
|
745
|
+
},
|
|
746
|
+
requirement: {
|
|
747
|
+
tokens: [{ address: paymentCurrency.contract as string, value: stakeAmount }],
|
|
748
|
+
},
|
|
749
|
+
nonce: `stake-${subscription.id}`,
|
|
750
|
+
meta: {
|
|
751
|
+
purpose: 'staking',
|
|
752
|
+
address,
|
|
753
|
+
},
|
|
754
|
+
chainInfo: {
|
|
755
|
+
host: paymentMethod.settings?.arcblock?.api_host as string,
|
|
756
|
+
id: paymentMethod.settings?.arcblock?.chain_id as string,
|
|
757
|
+
},
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
throw new Error(`getStakeTxClaim: Payment method ${paymentMethod.type} not supported`);
|
|
762
|
+
}
|
|
763
|
+
|
|
866
764
|
export type TokenRequirementArgs = {
|
|
867
765
|
items: TLineItemExpanded[];
|
|
868
766
|
mode: string;
|
|
@@ -1045,6 +943,40 @@ export async function ensureSubscriptionForCollectBatch(subscriptionId: string,
|
|
|
1045
943
|
};
|
|
1046
944
|
}
|
|
1047
945
|
|
|
946
|
+
export async function ensureSubscriptionForOverdraftProtection(subscriptionId: string, amount: string) {
|
|
947
|
+
const subscription = await Subscription.findByPk(subscriptionId);
|
|
948
|
+
if (!subscription) {
|
|
949
|
+
throw new Error(`Subscription ${subscriptionId} not found when prepare overdraft protection`);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
|
|
953
|
+
if (!paymentCurrency) {
|
|
954
|
+
throw new Error(`PaymentCurrency ${subscription.currency_id} not found when prepare overdraft protection`);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
958
|
+
|
|
959
|
+
if (!paymentMethod) {
|
|
960
|
+
throw new Error(`Payment method not found for subscription ${subscriptionId}`);
|
|
961
|
+
}
|
|
962
|
+
if (paymentMethod.type !== 'arcblock') {
|
|
963
|
+
throw new Error(`Payment method ${paymentMethod.type} not supported for overdraft protection`);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const customer = await Customer.findByPk(subscription.customer_id);
|
|
967
|
+
if (!customer) {
|
|
968
|
+
throw new Error(`Customer not found for subscription ${subscriptionId}`);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
return {
|
|
972
|
+
subscription,
|
|
973
|
+
paymentCurrency: paymentCurrency as PaymentCurrency,
|
|
974
|
+
paymentMethod: paymentMethod as PaymentMethod,
|
|
975
|
+
customer,
|
|
976
|
+
stakeAmount: fromTokenToUnit(amount, paymentCurrency.decimal).toString(),
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
|
|
1048
980
|
export async function executeOcapTransactions(
|
|
1049
981
|
userDid: string,
|
|
1050
982
|
userPk: string,
|
|
@@ -1052,7 +984,8 @@ export async function executeOcapTransactions(
|
|
|
1052
984
|
paymentMethod: PaymentMethod,
|
|
1053
985
|
request: Request,
|
|
1054
986
|
subscriptionId?: string,
|
|
1055
|
-
paymentCurrencyContract?: string
|
|
987
|
+
paymentCurrencyContract?: string,
|
|
988
|
+
nonce?: string
|
|
1056
989
|
) {
|
|
1057
990
|
const client = paymentMethod.getOcapClient();
|
|
1058
991
|
const delegation = claims.find((x) => x.type === 'signature' && x.meta?.purpose === 'delegation');
|
|
@@ -1089,104 +1022,18 @@ export async function executeOcapTransactions(
|
|
|
1089
1022
|
})
|
|
1090
1023
|
);
|
|
1091
1024
|
|
|
1092
|
-
const nonce = subscriptionId || '';
|
|
1093
|
-
|
|
1094
1025
|
return {
|
|
1095
1026
|
tx_hash: delegationTxHash,
|
|
1096
1027
|
payer: userDid,
|
|
1097
1028
|
type: 'delegate',
|
|
1098
1029
|
staking: {
|
|
1099
1030
|
tx_hash: stakingTxHash,
|
|
1100
|
-
address: await getCustomerStakeAddress(userDid, nonce),
|
|
1031
|
+
address: await getCustomerStakeAddress(userDid, nonce || subscriptionId || ''),
|
|
1101
1032
|
},
|
|
1102
1033
|
stakingAmount,
|
|
1103
1034
|
};
|
|
1104
1035
|
}
|
|
1105
1036
|
|
|
1106
|
-
export async function ensureStakeInvoice(
|
|
1107
|
-
invoiceProps: { total: string; description?: string; checkout_session_id?: string; currency_id: string; metadata?: any; payment_settings?: any },
|
|
1108
|
-
subscription: Subscription,
|
|
1109
|
-
paymentMethod: PaymentMethod,
|
|
1110
|
-
customer: Customer
|
|
1111
|
-
) {
|
|
1112
|
-
if (paymentMethod.type !== 'arcblock') {
|
|
1113
|
-
return;
|
|
1114
|
-
}
|
|
1115
|
-
try {
|
|
1116
|
-
const stakingInvoice = await Invoice.create({
|
|
1117
|
-
livemode: subscription.livemode,
|
|
1118
|
-
number: await customer.getInvoiceNumber(),
|
|
1119
|
-
description: invoiceProps?.description || 'Stake for subscription',
|
|
1120
|
-
statement_descriptor: '',
|
|
1121
|
-
period_start: dayjs().unix(),
|
|
1122
|
-
period_end: subscription.current_period_end,
|
|
1123
|
-
|
|
1124
|
-
auto_advance: false,
|
|
1125
|
-
paid: true,
|
|
1126
|
-
paid_out_of_band: false,
|
|
1127
|
-
|
|
1128
|
-
status: 'paid',
|
|
1129
|
-
collection_method: 'charge_automatically',
|
|
1130
|
-
billing_reason: 'stake',
|
|
1131
|
-
|
|
1132
|
-
currency_id: invoiceProps.currency_id,
|
|
1133
|
-
customer_id: customer.id,
|
|
1134
|
-
payment_intent_id: '',
|
|
1135
|
-
subscription_id: subscription?.id,
|
|
1136
|
-
checkout_session_id: invoiceProps?.checkout_session_id || '',
|
|
1137
|
-
|
|
1138
|
-
total: invoiceProps.total || '0',
|
|
1139
|
-
subtotal: invoiceProps.total || '0',
|
|
1140
|
-
tax: '0',
|
|
1141
|
-
subtotal_excluding_tax: invoiceProps.total || '0',
|
|
1142
|
-
|
|
1143
|
-
amount_due: '0',
|
|
1144
|
-
amount_paid: invoiceProps.total || '0',
|
|
1145
|
-
amount_remaining: '0',
|
|
1146
|
-
amount_shipping: '0',
|
|
1147
|
-
|
|
1148
|
-
starting_balance: '0',
|
|
1149
|
-
ending_balance: '0',
|
|
1150
|
-
starting_token_balance: {},
|
|
1151
|
-
ending_token_balance: {},
|
|
1152
|
-
|
|
1153
|
-
attempt_count: 0,
|
|
1154
|
-
attempted: false,
|
|
1155
|
-
// next_payment_attempt: undefined,
|
|
1156
|
-
|
|
1157
|
-
custom_fields: [],
|
|
1158
|
-
customer_address: customer.address,
|
|
1159
|
-
customer_email: customer.email,
|
|
1160
|
-
customer_name: customer.name,
|
|
1161
|
-
customer_phone: customer.phone,
|
|
1162
|
-
|
|
1163
|
-
discounts: [],
|
|
1164
|
-
total_discount_amounts: [],
|
|
1165
|
-
|
|
1166
|
-
due_date: undefined,
|
|
1167
|
-
effective_at: dayjs().unix(),
|
|
1168
|
-
status_transitions: {
|
|
1169
|
-
finalized_at: dayjs().unix(),
|
|
1170
|
-
},
|
|
1171
|
-
|
|
1172
|
-
payment_settings: invoiceProps?.payment_settings || subscription?.payment_settings,
|
|
1173
|
-
default_payment_method_id: paymentMethod.id,
|
|
1174
|
-
|
|
1175
|
-
account_country: '',
|
|
1176
|
-
account_name: '',
|
|
1177
|
-
metadata: invoiceProps.metadata || {},
|
|
1178
|
-
});
|
|
1179
|
-
logger.info('create staking invoice success', {
|
|
1180
|
-
stakingInvoice,
|
|
1181
|
-
subscriptionId: subscription?.id,
|
|
1182
|
-
paymentMethod: paymentMethod.id,
|
|
1183
|
-
customerId: customer.id,
|
|
1184
|
-
});
|
|
1185
|
-
} catch (error) {
|
|
1186
|
-
logger.error('ensureStake: create invoice failed', { error, subscriptionId: subscription?.id, paymentMethod: paymentMethod.id, customerId: customer.id });
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
1037
|
|
|
1191
1038
|
export async function updateStripeSubscriptionAfterChangePayment(setupIntent: SetupIntent, subscription: Subscription) {
|
|
1192
1039
|
const { from_method: fromMethodId, to_method: toMethodId } = setupIntent.metadata || {};
|
|
@@ -1232,83 +1079,3 @@ export async function updateStripeSubscriptionAfterChangePayment(setupIntent: Se
|
|
|
1232
1079
|
}
|
|
1233
1080
|
}
|
|
1234
1081
|
|
|
1235
|
-
export async function ensureRechargeInvoice(
|
|
1236
|
-
invoiceProps: { total: string; description?: string; checkout_session_id?: string; currency_id: string; metadata?: any; payment_settings?: any },
|
|
1237
|
-
subscription: Subscription,
|
|
1238
|
-
paymentMethod: PaymentMethod,
|
|
1239
|
-
customer: Customer
|
|
1240
|
-
) {
|
|
1241
|
-
try {
|
|
1242
|
-
const rechargeInvoice = await Invoice.create({
|
|
1243
|
-
livemode: subscription.livemode,
|
|
1244
|
-
number: await customer.getInvoiceNumber(),
|
|
1245
|
-
description: invoiceProps?.description || 'Add funds for subscription',
|
|
1246
|
-
statement_descriptor: '',
|
|
1247
|
-
period_start: dayjs().unix(),
|
|
1248
|
-
period_end: dayjs().unix(),
|
|
1249
|
-
|
|
1250
|
-
auto_advance: false,
|
|
1251
|
-
paid: true,
|
|
1252
|
-
paid_out_of_band: false,
|
|
1253
|
-
|
|
1254
|
-
status: 'paid',
|
|
1255
|
-
collection_method: 'charge_automatically',
|
|
1256
|
-
billing_reason: 'recharge',
|
|
1257
|
-
|
|
1258
|
-
currency_id: invoiceProps.currency_id,
|
|
1259
|
-
customer_id: customer.id,
|
|
1260
|
-
payment_intent_id: '',
|
|
1261
|
-
subscription_id: subscription?.id,
|
|
1262
|
-
checkout_session_id: invoiceProps?.checkout_session_id || '',
|
|
1263
|
-
|
|
1264
|
-
total: invoiceProps.total || '0',
|
|
1265
|
-
subtotal: invoiceProps.total || '0',
|
|
1266
|
-
tax: '0',
|
|
1267
|
-
subtotal_excluding_tax: invoiceProps.total || '0',
|
|
1268
|
-
|
|
1269
|
-
amount_due: '0',
|
|
1270
|
-
amount_paid: invoiceProps.total || '0',
|
|
1271
|
-
amount_remaining: '0',
|
|
1272
|
-
amount_shipping: '0',
|
|
1273
|
-
|
|
1274
|
-
starting_balance: '0',
|
|
1275
|
-
ending_balance: '0',
|
|
1276
|
-
starting_token_balance: {},
|
|
1277
|
-
ending_token_balance: {},
|
|
1278
|
-
|
|
1279
|
-
attempt_count: 0,
|
|
1280
|
-
attempted: false,
|
|
1281
|
-
// next_payment_attempt: undefined,
|
|
1282
|
-
|
|
1283
|
-
custom_fields: [],
|
|
1284
|
-
customer_address: customer.address,
|
|
1285
|
-
customer_email: customer.email,
|
|
1286
|
-
customer_name: customer.name,
|
|
1287
|
-
customer_phone: customer.phone,
|
|
1288
|
-
|
|
1289
|
-
discounts: [],
|
|
1290
|
-
total_discount_amounts: [],
|
|
1291
|
-
|
|
1292
|
-
due_date: undefined,
|
|
1293
|
-
effective_at: dayjs().unix(),
|
|
1294
|
-
status_transitions: {
|
|
1295
|
-
finalized_at: dayjs().unix(),
|
|
1296
|
-
},
|
|
1297
|
-
|
|
1298
|
-
payment_settings: invoiceProps?.payment_settings || subscription?.payment_settings,
|
|
1299
|
-
default_payment_method_id: paymentMethod.id,
|
|
1300
|
-
|
|
1301
|
-
account_country: '',
|
|
1302
|
-
account_name: '',
|
|
1303
|
-
metadata: invoiceProps.metadata || {},
|
|
1304
|
-
});
|
|
1305
|
-
logger.info('create recharge invoice success', {
|
|
1306
|
-
rechargeInvoice,
|
|
1307
|
-
subscriptionId: subscription?.id,
|
|
1308
|
-
paymentMethod: paymentMethod.id,
|
|
1309
|
-
customerId: customer.id,
|
|
1310
|
-
});
|
|
1311
|
-
} catch (error) {
|
|
1312
|
-
logger.error('ensureRechargeInvoice: create invoice failed', { error, subscriptionId: subscription?.id, paymentMethod: paymentMethod.id, customerId: customer.id });
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
@@ -12,12 +12,12 @@ import type { Invoice, TLineItemExpanded } from '../../store/models';
|
|
|
12
12
|
import {
|
|
13
13
|
ensureInvoiceForCheckout,
|
|
14
14
|
ensurePaymentIntent,
|
|
15
|
-
ensureStakeInvoice,
|
|
16
15
|
executeOcapTransactions,
|
|
17
16
|
getAuthPrincipalClaim,
|
|
18
17
|
getDelegationTxClaim,
|
|
19
18
|
getStakeTxClaim,
|
|
20
19
|
} from './shared';
|
|
20
|
+
import { ensureStakeInvoice } from '../../libs/invoice';
|
|
21
21
|
|
|
22
22
|
export default {
|
|
23
23
|
action: 'subscription',
|