payment-kit 1.18.17 → 1.18.19
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/libs/subscription.ts +116 -0
- package/api/src/routes/checkout-sessions.ts +28 -1
- package/api/src/routes/customers.ts +5 -1
- package/api/src/store/migrations/20250318-donate-invoice.ts +45 -0
- package/api/tests/libs/subscription.spec.ts +311 -0
- package/blocklet.yml +1 -1
- package/package.json +9 -9
- package/src/components/currency.tsx +11 -4
- package/src/components/customer/link.tsx +54 -14
- package/src/components/customer/overdraft-protection.tsx +36 -2
- package/src/components/info-card.tsx +55 -7
- package/src/components/info-row-group.tsx +122 -0
- package/src/components/info-row.tsx +14 -1
- package/src/components/payouts/portal/list.tsx +7 -2
- package/src/components/subscription/items/index.tsx +1 -1
- package/src/components/subscription/metrics.tsx +14 -6
- package/src/contexts/info-row.tsx +4 -0
- package/src/locales/en.tsx +1 -0
- package/src/locales/zh.tsx +1 -0
- package/src/pages/admin/billing/invoices/detail.tsx +54 -76
- package/src/pages/admin/billing/subscriptions/detail.tsx +34 -71
- package/src/pages/admin/customers/customers/detail.tsx +41 -64
- package/src/pages/admin/payments/intents/detail.tsx +28 -42
- package/src/pages/admin/payments/payouts/detail.tsx +27 -36
- package/src/pages/admin/payments/refunds/detail.tsx +27 -41
- package/src/pages/admin/products/links/detail.tsx +30 -55
- package/src/pages/admin/products/prices/detail.tsx +43 -50
- package/src/pages/admin/products/pricing-tables/detail.tsx +23 -25
- package/src/pages/admin/products/products/detail.tsx +52 -81
- package/src/pages/customer/index.tsx +183 -108
- package/src/pages/customer/invoice/detail.tsx +49 -50
- package/src/pages/customer/payout/detail.tsx +16 -22
- package/src/pages/customer/recharge/account.tsx +92 -34
- package/src/pages/customer/recharge/subscription.tsx +6 -0
- package/src/pages/customer/subscription/detail.tsx +176 -94
|
@@ -1563,3 +1563,119 @@ export async function getSubscriptionUnpaidInvoicesCount(subscription: Subscript
|
|
|
1563
1563
|
return 0;
|
|
1564
1564
|
}
|
|
1565
1565
|
}
|
|
1566
|
+
|
|
1567
|
+
/**
|
|
1568
|
+
* Calculate recommended recharge amount for subscription cycles
|
|
1569
|
+
* @param subscriptions List of subscriptions
|
|
1570
|
+
* @param currencyId Currency ID
|
|
1571
|
+
* @returns {amount: string, cycle: number, desc: string, interval: string} Recommended amount and cycle info
|
|
1572
|
+
*/
|
|
1573
|
+
export function calculateRecommendedRechargeAmount(subscriptions: any[], currencyId: string) {
|
|
1574
|
+
if (!subscriptions || subscriptions.length === 0) {
|
|
1575
|
+
return { amount: '0', cycle: 0, desc: 'no subscription', interval: '' };
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
type IntervalType = 'hour' | 'day' | 'month' | 'year';
|
|
1579
|
+
|
|
1580
|
+
// Group subscriptions by billing interval
|
|
1581
|
+
const intervalSubscriptions: Record<IntervalType, any[]> = {
|
|
1582
|
+
hour: [],
|
|
1583
|
+
day: [],
|
|
1584
|
+
month: [],
|
|
1585
|
+
year: [],
|
|
1586
|
+
};
|
|
1587
|
+
|
|
1588
|
+
// Classify subscriptions by interval
|
|
1589
|
+
subscriptions.forEach((subscription) => {
|
|
1590
|
+
if (!subscription.items?.length) return;
|
|
1591
|
+
|
|
1592
|
+
subscription.items.forEach((item: any) => {
|
|
1593
|
+
const { price } = item;
|
|
1594
|
+
if (price?.type === 'recurring' && price.recurring?.interval) {
|
|
1595
|
+
const interval = price.recurring.interval as IntervalType;
|
|
1596
|
+
if (Object.keys(intervalSubscriptions).includes(interval)) {
|
|
1597
|
+
intervalSubscriptions[interval].push({ subscription, item, price });
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
const hasHourly = intervalSubscriptions.hour.length > 0;
|
|
1604
|
+
const hasDaily = intervalSubscriptions.day.length > 0;
|
|
1605
|
+
const hasMonthly = intervalSubscriptions.month.length > 0;
|
|
1606
|
+
const hasYearly = intervalSubscriptions.year.length > 0;
|
|
1607
|
+
|
|
1608
|
+
// Determine recommended cycle based on subscription intervals
|
|
1609
|
+
let recommendedInterval = '';
|
|
1610
|
+
let cycleInSeconds = 0;
|
|
1611
|
+
let desc = '';
|
|
1612
|
+
|
|
1613
|
+
if (hasMonthly) {
|
|
1614
|
+
recommendedInterval = 'month';
|
|
1615
|
+
cycleInSeconds = 30 * 24 * 60 * 60;
|
|
1616
|
+
desc = 'monthly';
|
|
1617
|
+
} else if ((hasHourly || hasDaily) && !hasYearly) {
|
|
1618
|
+
recommendedInterval = 'week';
|
|
1619
|
+
cycleInSeconds = 7 * 24 * 60 * 60;
|
|
1620
|
+
desc = 'weekly';
|
|
1621
|
+
} else if ((hasDaily || hasHourly) && hasYearly) {
|
|
1622
|
+
recommendedInterval = 'month';
|
|
1623
|
+
cycleInSeconds = 30 * 24 * 60 * 60;
|
|
1624
|
+
desc = 'monthly';
|
|
1625
|
+
} else if (hasYearly) {
|
|
1626
|
+
recommendedInterval = 'year';
|
|
1627
|
+
cycleInSeconds = 365 * 24 * 60 * 60;
|
|
1628
|
+
desc = 'yearly';
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
let totalAmount = new BN(0);
|
|
1632
|
+
|
|
1633
|
+
// Calculate amount based on recommended cycle
|
|
1634
|
+
subscriptions.forEach((subscription) => {
|
|
1635
|
+
if (!subscription.items?.length) return;
|
|
1636
|
+
|
|
1637
|
+
subscription.items.forEach((item: any) => {
|
|
1638
|
+
const { price } = item;
|
|
1639
|
+
if (!price) return;
|
|
1640
|
+
|
|
1641
|
+
const unitAmount = getPriceUintAmountByCurrency(price, currencyId);
|
|
1642
|
+
const quantity = item.quantity || 1;
|
|
1643
|
+
let itemAmount = new BN(unitAmount).mul(new BN(quantity));
|
|
1644
|
+
|
|
1645
|
+
if (price.type === 'recurring' && price.recurring?.interval) {
|
|
1646
|
+
const { interval } = price.recurring;
|
|
1647
|
+
const intervalCount = price.recurring.interval_count || 1;
|
|
1648
|
+
|
|
1649
|
+
// Apply period conversion calculation
|
|
1650
|
+
if (recommendedInterval === 'month') {
|
|
1651
|
+
if (interval === 'hour') {
|
|
1652
|
+
itemAmount = itemAmount.mul(new BN(720)).div(new BN(intervalCount));
|
|
1653
|
+
} else if (interval === 'day') {
|
|
1654
|
+
itemAmount = itemAmount.mul(new BN(30)).div(new BN(intervalCount));
|
|
1655
|
+
} else if (interval === 'month') {
|
|
1656
|
+
itemAmount = itemAmount.div(new BN(intervalCount));
|
|
1657
|
+
} else if (interval === 'year') {
|
|
1658
|
+
itemAmount = itemAmount.div(new BN(12 * intervalCount));
|
|
1659
|
+
}
|
|
1660
|
+
} else if (recommendedInterval === 'week') {
|
|
1661
|
+
if (interval === 'hour') {
|
|
1662
|
+
itemAmount = itemAmount.mul(new BN(168)).div(new BN(intervalCount));
|
|
1663
|
+
} else if (interval === 'day') {
|
|
1664
|
+
itemAmount = itemAmount.mul(new BN(7)).div(new BN(intervalCount));
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
totalAmount = totalAmount.add(itemAmount);
|
|
1669
|
+
} else if (price.type === 'one_time') {
|
|
1670
|
+
totalAmount = totalAmount.add(itemAmount);
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
});
|
|
1674
|
+
|
|
1675
|
+
return {
|
|
1676
|
+
amount: totalAmount.toString(),
|
|
1677
|
+
cycle: cycleInSeconds,
|
|
1678
|
+
desc,
|
|
1679
|
+
interval: recommendedInterval,
|
|
1680
|
+
};
|
|
1681
|
+
}
|
|
@@ -46,10 +46,12 @@ import {
|
|
|
46
46
|
formatAmountPrecisionLimit,
|
|
47
47
|
formatMetadata,
|
|
48
48
|
getDataObjectFromQuery,
|
|
49
|
+
getUserOrAppInfo,
|
|
49
50
|
isUserInBlocklist,
|
|
50
51
|
} from '../libs/util';
|
|
51
52
|
import {
|
|
52
53
|
Invoice,
|
|
54
|
+
PaymentBeneficiary,
|
|
53
55
|
SetupIntent,
|
|
54
56
|
Subscription,
|
|
55
57
|
SubscriptionItem,
|
|
@@ -501,6 +503,11 @@ export async function ensureCheckoutSessionOpen(req: Request, res: Response, nex
|
|
|
501
503
|
next();
|
|
502
504
|
}
|
|
503
505
|
|
|
506
|
+
const getBeneficiaryName = async (beneficiary: PaymentBeneficiary) => {
|
|
507
|
+
if (!beneficiary) return '';
|
|
508
|
+
return beneficiary.name || (await getUserOrAppInfo(beneficiary.address || ''))?.name || beneficiary.address;
|
|
509
|
+
};
|
|
510
|
+
|
|
504
511
|
export async function getCrossSellItem(checkoutSession: CheckoutSession) {
|
|
505
512
|
// FIXME: perhaps we can support cross sell even if the current session have multiple items
|
|
506
513
|
if (checkoutSession.line_items.length > 1) {
|
|
@@ -624,12 +631,32 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
624
631
|
payment: 'Thanks for your purchase',
|
|
625
632
|
subscription: 'Thanks for your subscribing',
|
|
626
633
|
setup: 'Thanks for your subscribing',
|
|
627
|
-
donate: 'Thanks for
|
|
634
|
+
donate: 'Thanks for your tip',
|
|
628
635
|
};
|
|
629
636
|
const mode = link.submit_type === 'donate' ? 'donate' : raw.mode;
|
|
630
637
|
raw.payment_intent_data = {
|
|
631
638
|
description: descriptions[mode || 'payment'],
|
|
632
639
|
};
|
|
640
|
+
if (mode === 'donate') {
|
|
641
|
+
const beneficiaries = link.donation_settings?.beneficiaries || [];
|
|
642
|
+
|
|
643
|
+
if (beneficiaries.length > 0) {
|
|
644
|
+
// sort beneficiaries by share, the first one is the main beneficiary
|
|
645
|
+
const sortedBeneficiaries = [...beneficiaries].sort((a, b) => {
|
|
646
|
+
// Plain JavaScript comparison without using BN methods
|
|
647
|
+
return Number(b.share) - Number(a.share);
|
|
648
|
+
});
|
|
649
|
+
const mainBeneficiary = sortedBeneficiaries[0];
|
|
650
|
+
const userName = await getBeneficiaryName(mainBeneficiary as PaymentBeneficiary);
|
|
651
|
+
|
|
652
|
+
raw.payment_intent_data.description =
|
|
653
|
+
beneficiaries.length === 1
|
|
654
|
+
? `Tip to ${userName}`
|
|
655
|
+
: `Tip to ${userName} and ${beneficiaries.length - 1} others`;
|
|
656
|
+
} else {
|
|
657
|
+
raw.payment_intent_data.description = 'Thanks for your tip';
|
|
658
|
+
}
|
|
659
|
+
}
|
|
633
660
|
}
|
|
634
661
|
if (link.after_completion?.redirect?.url) {
|
|
635
662
|
raw.success_url = link.after_completion?.redirect?.url;
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
Subscription,
|
|
23
23
|
SubscriptionItem,
|
|
24
24
|
} from '../store/models';
|
|
25
|
-
import { getSubscriptionPaymentAddress } from '../libs/subscription';
|
|
25
|
+
import { getSubscriptionPaymentAddress, calculateRecommendedRechargeAmount } from '../libs/subscription';
|
|
26
26
|
import { expandLineItems } from '../libs/session';
|
|
27
27
|
|
|
28
28
|
const router = Router();
|
|
@@ -272,12 +272,16 @@ router.get('/recharge', sessionMiddleware(), async (req, res) => {
|
|
|
272
272
|
return payerAddress === customer.did;
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
+
// Calculate recommended recharge cycle and amount
|
|
276
|
+
const recommendedRecharge = calculateRecommendedRechargeAmount(relatedSubscriptions, paymentCurrency.id);
|
|
277
|
+
|
|
275
278
|
return res.json({
|
|
276
279
|
currency: {
|
|
277
280
|
...paymentCurrency.toJSON(),
|
|
278
281
|
paymentMethod,
|
|
279
282
|
},
|
|
280
283
|
relatedSubscriptions,
|
|
284
|
+
recommendedRecharge,
|
|
281
285
|
});
|
|
282
286
|
} catch (err) {
|
|
283
287
|
logger.error('Error getting balance recharge info', err);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { QueryTypes } from 'sequelize';
|
|
2
|
+
import { Migration } from '../migrate';
|
|
3
|
+
|
|
4
|
+
export const up: Migration = async ({ context }) => {
|
|
5
|
+
try {
|
|
6
|
+
await context.sequelize.query(
|
|
7
|
+
"UPDATE payment_intents SET description = 'Thanks for your tip' WHERE description = 'Thanks for for your tip'",
|
|
8
|
+
{ type: QueryTypes.UPDATE }
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
await context.sequelize.query(
|
|
12
|
+
`UPDATE payment_intents SET description = 'Thanks for your tip'
|
|
13
|
+
WHERE description = 'Thanks for your support'
|
|
14
|
+
AND id IN (
|
|
15
|
+
SELECT payment_intent_id FROM checkout_sessions
|
|
16
|
+
WHERE submit_type = 'donate' AND payment_intent_id IS NOT NULL
|
|
17
|
+
)`,
|
|
18
|
+
{ type: QueryTypes.UPDATE }
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
await context.sequelize.query(
|
|
22
|
+
`UPDATE invoices SET description = 'Thanks for your tip'
|
|
23
|
+
WHERE (description = 'Thanks for your support' OR description = 'Thanks for for your tip')
|
|
24
|
+
AND payment_intent_id IN (
|
|
25
|
+
SELECT payment_intent_id FROM checkout_sessions
|
|
26
|
+
WHERE submit_type = 'donate' AND payment_intent_id IS NOT NULL
|
|
27
|
+
)`,
|
|
28
|
+
{ type: QueryTypes.UPDATE }
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
await context.sequelize.query(
|
|
32
|
+
`UPDATE payouts SET description = 'Thanks for your tip'
|
|
33
|
+
WHERE (description = 'Thanks for your support' OR description = 'Thanks for for your tip')
|
|
34
|
+
AND payment_intent_id IN (
|
|
35
|
+
SELECT payment_intent_id FROM checkout_sessions
|
|
36
|
+
WHERE submit_type = 'donate' AND payment_intent_id IS NOT NULL
|
|
37
|
+
)`,
|
|
38
|
+
{ type: QueryTypes.UPDATE }
|
|
39
|
+
);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error('Failed to update donation invoice descriptions', error);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const down = async () => {};
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
shouldCancelSubscription,
|
|
12
12
|
getSubscriptionStakeAmountSetup,
|
|
13
13
|
checkUsageReportEmpty,
|
|
14
|
+
calculateRecommendedRechargeAmount,
|
|
14
15
|
} from '../../src/libs/subscription';
|
|
15
16
|
import { PaymentMethod, Subscription, SubscriptionItem, UsageRecord, Price } from '../../src/store/models';
|
|
16
17
|
|
|
@@ -565,3 +566,313 @@ describe('checkUsageReportEmpty', () => {
|
|
|
565
566
|
expect(result).toBe(false);
|
|
566
567
|
});
|
|
567
568
|
});
|
|
569
|
+
|
|
570
|
+
describe('calculateRecommendedRechargeAmount', () => {
|
|
571
|
+
// 测试空订阅列表
|
|
572
|
+
it('should return zero amount for empty subscriptions', () => {
|
|
573
|
+
const result = calculateRecommendedRechargeAmount([], 'usd');
|
|
574
|
+
expect(result).toEqual({ amount: '0', cycle: 0, desc: 'no subscription', interval: '' });
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// 测试只有按小时订阅
|
|
578
|
+
it('should recommend weekly cycle for hourly subscriptions', () => {
|
|
579
|
+
const subscriptions = [
|
|
580
|
+
{
|
|
581
|
+
items: [
|
|
582
|
+
{
|
|
583
|
+
quantity: 2,
|
|
584
|
+
price: {
|
|
585
|
+
type: 'recurring',
|
|
586
|
+
recurring: { interval: 'hour', interval_count: 1 },
|
|
587
|
+
unit_amount: '10',
|
|
588
|
+
currency_id: 'usd',
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
],
|
|
592
|
+
},
|
|
593
|
+
];
|
|
594
|
+
|
|
595
|
+
const result = calculateRecommendedRechargeAmount(subscriptions, 'usd');
|
|
596
|
+
expect(result.cycle).toBe(7 * 24 * 60 * 60); // 一周的秒数
|
|
597
|
+
expect(result.desc).toBe('weekly');
|
|
598
|
+
expect(result.interval).toBe('week');
|
|
599
|
+
// 每小时10,一周168小时,两个单位,总计 10 * 168 * 2 = 3360
|
|
600
|
+
expect(result.amount).toBe('3360');
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// 测试只有按天订阅
|
|
604
|
+
it('should recommend weekly cycle for daily subscriptions', () => {
|
|
605
|
+
const subscriptions = [
|
|
606
|
+
{
|
|
607
|
+
items: [
|
|
608
|
+
{
|
|
609
|
+
quantity: 1,
|
|
610
|
+
price: {
|
|
611
|
+
type: 'recurring',
|
|
612
|
+
recurring: { interval: 'day', interval_count: 1 },
|
|
613
|
+
unit_amount: '100',
|
|
614
|
+
currency_id: 'usd',
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
],
|
|
618
|
+
},
|
|
619
|
+
];
|
|
620
|
+
|
|
621
|
+
const result = calculateRecommendedRechargeAmount(subscriptions, 'usd');
|
|
622
|
+
expect(result.cycle).toBe(7 * 24 * 60 * 60);
|
|
623
|
+
expect(result.desc).toBe('weekly');
|
|
624
|
+
expect(result.interval).toBe('week');
|
|
625
|
+
// 每天100,一周7天,总计 100 * 7 = 700
|
|
626
|
+
expect(result.amount).toBe('700');
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
// 测试只有按月订阅
|
|
630
|
+
it('should recommend monthly cycle for monthly subscriptions', () => {
|
|
631
|
+
const subscriptions = [
|
|
632
|
+
{
|
|
633
|
+
items: [
|
|
634
|
+
{
|
|
635
|
+
quantity: 1,
|
|
636
|
+
price: {
|
|
637
|
+
type: 'recurring',
|
|
638
|
+
recurring: { interval: 'month', interval_count: 1 },
|
|
639
|
+
unit_amount: '500',
|
|
640
|
+
currency_id: 'usd',
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
],
|
|
644
|
+
},
|
|
645
|
+
];
|
|
646
|
+
|
|
647
|
+
const result = calculateRecommendedRechargeAmount(subscriptions, 'usd');
|
|
648
|
+
expect(result.cycle).toBe(30 * 24 * 60 * 60);
|
|
649
|
+
expect(result.desc).toBe('monthly');
|
|
650
|
+
expect(result.interval).toBe('month');
|
|
651
|
+
expect(result.amount).toBe('500');
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// 测试中间档金额计算 (4x)
|
|
655
|
+
it('should calculate the medium preset amount correctly (4x)', () => {
|
|
656
|
+
const subscriptions = [
|
|
657
|
+
{
|
|
658
|
+
items: [
|
|
659
|
+
{
|
|
660
|
+
quantity: 1,
|
|
661
|
+
price: {
|
|
662
|
+
type: 'recurring',
|
|
663
|
+
recurring: { interval: 'month', interval_count: 1 },
|
|
664
|
+
unit_amount: '500',
|
|
665
|
+
currency_id: 'usd',
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
],
|
|
669
|
+
},
|
|
670
|
+
];
|
|
671
|
+
|
|
672
|
+
const result = calculateRecommendedRechargeAmount(subscriptions, 'usd');
|
|
673
|
+
expect(result.amount).toBe('500');
|
|
674
|
+
|
|
675
|
+
// 在UI中,我们应用Math.ceil(500 * 4) = 2000作为中间档预设值
|
|
676
|
+
const baseAmount = result.amount;
|
|
677
|
+
const mediumPresetAmount = Math.ceil(Number(baseAmount) * 4).toString();
|
|
678
|
+
expect(mediumPresetAmount).toBe('2000');
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// 测试高档金额计算 (8x)
|
|
682
|
+
it('should calculate the high preset amount correctly (8x)', () => {
|
|
683
|
+
const subscriptions = [
|
|
684
|
+
{
|
|
685
|
+
items: [
|
|
686
|
+
{
|
|
687
|
+
quantity: 1,
|
|
688
|
+
price: {
|
|
689
|
+
type: 'recurring',
|
|
690
|
+
recurring: { interval: 'month', interval_count: 1 },
|
|
691
|
+
unit_amount: '500',
|
|
692
|
+
currency_id: 'usd',
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
],
|
|
696
|
+
},
|
|
697
|
+
];
|
|
698
|
+
|
|
699
|
+
const result = calculateRecommendedRechargeAmount(subscriptions, 'usd');
|
|
700
|
+
|
|
701
|
+
// 在UI中,我们应用Math.ceil(500 * 8) = 4000作为高档预设值
|
|
702
|
+
const baseAmount = result.amount;
|
|
703
|
+
const highPresetAmount = Math.ceil(Number(baseAmount) * 8).toString();
|
|
704
|
+
expect(highPresetAmount).toBe('4000');
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// 测试前端默认选择中间档
|
|
708
|
+
it('should select medium preset by default in the UI', () => {
|
|
709
|
+
const subscriptions = [
|
|
710
|
+
{
|
|
711
|
+
items: [
|
|
712
|
+
{
|
|
713
|
+
quantity: 1,
|
|
714
|
+
price: {
|
|
715
|
+
type: 'recurring',
|
|
716
|
+
recurring: { interval: 'month', interval_count: 1 },
|
|
717
|
+
unit_amount: '500',
|
|
718
|
+
currency_id: 'usd',
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
],
|
|
722
|
+
},
|
|
723
|
+
];
|
|
724
|
+
|
|
725
|
+
const result = calculateRecommendedRechargeAmount(subscriptions, 'usd');
|
|
726
|
+
const baseAmount = result.amount;
|
|
727
|
+
|
|
728
|
+
// 验证前端默认选择的中间档金额是基础金额的4倍
|
|
729
|
+
const defaultSelectedAmount = Math.ceil(Number(baseAmount) * 4).toString();
|
|
730
|
+
expect(defaultSelectedAmount).toBe('2000');
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
// 测试只有按年订阅
|
|
734
|
+
it('should recommend yearly cycle for yearly subscriptions', () => {
|
|
735
|
+
const subscriptions = [
|
|
736
|
+
{
|
|
737
|
+
items: [
|
|
738
|
+
{
|
|
739
|
+
quantity: 1,
|
|
740
|
+
price: {
|
|
741
|
+
type: 'recurring',
|
|
742
|
+
recurring: { interval: 'year', interval_count: 1 },
|
|
743
|
+
unit_amount: '1200',
|
|
744
|
+
currency_id: 'usd',
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
],
|
|
748
|
+
},
|
|
749
|
+
];
|
|
750
|
+
|
|
751
|
+
const result = calculateRecommendedRechargeAmount(subscriptions, 'usd');
|
|
752
|
+
expect(result.cycle).toBe(365 * 24 * 60 * 60);
|
|
753
|
+
expect(result.desc).toBe('yearly');
|
|
754
|
+
expect(result.amount).toBe('1200');
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// 测试混合按小时和按年的订阅
|
|
758
|
+
it('should recommend monthly cycle for mixed hourly and yearly subscriptions', () => {
|
|
759
|
+
const subscriptions = [
|
|
760
|
+
{
|
|
761
|
+
items: [
|
|
762
|
+
{
|
|
763
|
+
quantity: 1,
|
|
764
|
+
price: {
|
|
765
|
+
type: 'recurring',
|
|
766
|
+
recurring: { interval: 'hour', interval_count: 1 },
|
|
767
|
+
unit_amount: '5',
|
|
768
|
+
currency_id: 'usd',
|
|
769
|
+
},
|
|
770
|
+
},
|
|
771
|
+
],
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
items: [
|
|
775
|
+
{
|
|
776
|
+
quantity: 1,
|
|
777
|
+
price: {
|
|
778
|
+
type: 'recurring',
|
|
779
|
+
recurring: { interval: 'year', interval_count: 1 },
|
|
780
|
+
unit_amount: '1200',
|
|
781
|
+
currency_id: 'usd',
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
],
|
|
785
|
+
},
|
|
786
|
+
];
|
|
787
|
+
|
|
788
|
+
const result = calculateRecommendedRechargeAmount(subscriptions, 'usd');
|
|
789
|
+
expect(result.cycle).toBe(30 * 24 * 60 * 60);
|
|
790
|
+
expect(result.desc).toBe('monthly');
|
|
791
|
+
// 每小时5,一个月约720小时 + 年费1200/12个月
|
|
792
|
+
// 5 * 720 + 1200/12 = 3600 + 100 = 3700
|
|
793
|
+
expect(result.amount).toBe('3700');
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// 测试混合小时、月、年的复杂订阅
|
|
797
|
+
it('should handle complex subscription combinations', () => {
|
|
798
|
+
const subscriptions = [
|
|
799
|
+
{
|
|
800
|
+
items: [
|
|
801
|
+
{
|
|
802
|
+
quantity: 2,
|
|
803
|
+
price: {
|
|
804
|
+
type: 'recurring',
|
|
805
|
+
recurring: { interval: 'hour', interval_count: 1 },
|
|
806
|
+
unit_amount: '5',
|
|
807
|
+
currency_id: 'usd',
|
|
808
|
+
},
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
quantity: 1,
|
|
812
|
+
price: {
|
|
813
|
+
type: 'recurring',
|
|
814
|
+
recurring: { interval: 'month', interval_count: 1 },
|
|
815
|
+
unit_amount: '200',
|
|
816
|
+
currency_id: 'usd',
|
|
817
|
+
},
|
|
818
|
+
},
|
|
819
|
+
],
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
items: [
|
|
823
|
+
{
|
|
824
|
+
quantity: 1,
|
|
825
|
+
price: {
|
|
826
|
+
type: 'recurring',
|
|
827
|
+
recurring: { interval: 'year', interval_count: 1 },
|
|
828
|
+
unit_amount: '1200',
|
|
829
|
+
currency_id: 'usd',
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
quantity: 1,
|
|
834
|
+
price: {
|
|
835
|
+
type: 'one_time',
|
|
836
|
+
unit_amount: '50',
|
|
837
|
+
currency_id: 'usd',
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
],
|
|
841
|
+
},
|
|
842
|
+
];
|
|
843
|
+
|
|
844
|
+
const result = calculateRecommendedRechargeAmount(subscriptions, 'usd');
|
|
845
|
+
expect(result.cycle).toBe(30 * 24 * 60 * 60);
|
|
846
|
+
expect(result.desc).toBe('monthly');
|
|
847
|
+
// 每小时5×2数量=10,一个月约720小时 + 每月200 + 年费1200/12个月 + 一次性费用50
|
|
848
|
+
// 10 * 720 + 200 + 1200/12 + 50 = 7200 + 200 + 100 + 50 = 7550
|
|
849
|
+
expect(result.amount).toBe('7550');
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
// 测试不同货币的处理
|
|
853
|
+
it('should handle currency options', () => {
|
|
854
|
+
const subscriptions = [
|
|
855
|
+
{
|
|
856
|
+
items: [
|
|
857
|
+
{
|
|
858
|
+
quantity: 1,
|
|
859
|
+
price: {
|
|
860
|
+
type: 'recurring',
|
|
861
|
+
recurring: { interval: 'month', interval_count: 1 },
|
|
862
|
+
currency_id: 'eth',
|
|
863
|
+
currency_options: [
|
|
864
|
+
{ currency_id: 'usd', unit_amount: '300' },
|
|
865
|
+
{ currency_id: 'btc', unit_amount: '0.01' },
|
|
866
|
+
],
|
|
867
|
+
},
|
|
868
|
+
},
|
|
869
|
+
],
|
|
870
|
+
},
|
|
871
|
+
];
|
|
872
|
+
|
|
873
|
+
const result = calculateRecommendedRechargeAmount(subscriptions, 'usd');
|
|
874
|
+
expect(result.cycle).toBe(30 * 24 * 60 * 60);
|
|
875
|
+
expect(result.desc).toBe('monthly');
|
|
876
|
+
expect(result.amount).toBe('300');
|
|
877
|
+
});
|
|
878
|
+
});
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.19",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -46,18 +46,18 @@
|
|
|
46
46
|
"@abtnode/cron": "^1.16.40",
|
|
47
47
|
"@arcblock/did": "^1.19.15",
|
|
48
48
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
49
|
-
"@arcblock/did-connect": "^2.12.
|
|
49
|
+
"@arcblock/did-connect": "^2.12.36",
|
|
50
50
|
"@arcblock/did-util": "^1.19.15",
|
|
51
51
|
"@arcblock/jwt": "^1.19.15",
|
|
52
|
-
"@arcblock/ux": "^2.12.
|
|
52
|
+
"@arcblock/ux": "^2.12.36",
|
|
53
53
|
"@arcblock/validator": "^1.19.15",
|
|
54
54
|
"@blocklet/js-sdk": "^1.16.40",
|
|
55
55
|
"@blocklet/logger": "^1.16.40",
|
|
56
|
-
"@blocklet/payment-react": "1.18.
|
|
56
|
+
"@blocklet/payment-react": "1.18.19",
|
|
57
57
|
"@blocklet/sdk": "^1.16.40",
|
|
58
|
-
"@blocklet/ui-react": "^2.12.
|
|
59
|
-
"@blocklet/uploader": "^0.1.
|
|
60
|
-
"@blocklet/xss": "^0.1.
|
|
58
|
+
"@blocklet/ui-react": "^2.12.36",
|
|
59
|
+
"@blocklet/uploader": "^0.1.79",
|
|
60
|
+
"@blocklet/xss": "^0.1.30",
|
|
61
61
|
"@mui/icons-material": "^5.16.6",
|
|
62
62
|
"@mui/lab": "^5.0.0-alpha.173",
|
|
63
63
|
"@mui/material": "^5.16.6",
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
"devDependencies": {
|
|
122
122
|
"@abtnode/types": "^1.16.40",
|
|
123
123
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
124
|
-
"@blocklet/payment-types": "1.18.
|
|
124
|
+
"@blocklet/payment-types": "1.18.19",
|
|
125
125
|
"@types/cookie-parser": "^1.4.7",
|
|
126
126
|
"@types/cors": "^2.8.17",
|
|
127
127
|
"@types/debug": "^4.1.12",
|
|
@@ -167,5 +167,5 @@
|
|
|
167
167
|
"parser": "typescript"
|
|
168
168
|
}
|
|
169
169
|
},
|
|
170
|
-
"gitHead": "
|
|
170
|
+
"gitHead": "eabd58598ad349a5177dca2e6302c82117d9b687"
|
|
171
171
|
}
|
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
import { Avatar, Stack, Typography } from '@mui/material';
|
|
1
|
+
import { Avatar, Stack, Typography, SxProps } from '@mui/material';
|
|
2
2
|
|
|
3
3
|
type Props = {
|
|
4
4
|
logo: string;
|
|
5
5
|
name: string;
|
|
6
|
+
sx?: SxProps;
|
|
6
7
|
};
|
|
7
8
|
|
|
8
|
-
export default function Currency({ logo, name }: Props) {
|
|
9
|
+
export default function Currency({ logo, name, sx }: Props) {
|
|
9
10
|
return (
|
|
10
|
-
<Stack direction="row" alignItems="center" spacing={0.5}>
|
|
11
|
+
<Stack direction="row" alignItems="center" spacing={0.5} sx={sx}>
|
|
11
12
|
<Avatar src={logo} alt={name} sx={{ width: 20, height: 20 }} />
|
|
12
|
-
<Typography color="text.primary"
|
|
13
|
+
<Typography color="text.primary" className="currency-name">
|
|
14
|
+
{name}
|
|
15
|
+
</Typography>
|
|
13
16
|
</Stack>
|
|
14
17
|
);
|
|
15
18
|
}
|
|
19
|
+
|
|
20
|
+
Currency.defaultProps = {
|
|
21
|
+
sx: {},
|
|
22
|
+
};
|