payment-kit 1.13.278 → 1.13.281
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/README.md +10 -0
- package/api/src/libs/logger.ts +0 -1
- package/api/src/libs/notification/template/subscription-renewed.ts +2 -4
- package/api/src/queues/notification.ts +6 -0
- package/api/src/queues/payment.ts +7 -0
- package/api/src/routes/subscription-items.ts +22 -0
- package/blocklet.yml +1 -1
- package/package.json +6 -4
- package/src/components/subscription/items/usage-records.tsx +74 -28
- package/src/locales/en.tsx +8 -0
- package/src/locales/zh.tsx +8 -0
- package/src/pages/admin/products/products/detail.tsx +5 -1
package/README.md
CHANGED
|
@@ -10,6 +10,16 @@ The decentralized stripe for blocklet platform.
|
|
|
10
10
|
2. run `make build`
|
|
11
11
|
3. run `cd blocklets/core && blocklet dev`
|
|
12
12
|
|
|
13
|
+
##### when error
|
|
14
|
+
1. pre-start error component xxx is not running or unreachable
|
|
15
|
+
- create .env.local file in this root
|
|
16
|
+
- add BLOCKLET_DEV_APP_DID="did:abt:your payment kit server did"
|
|
17
|
+
- add BLOCKLET_DEV_MOUNT_POINT="/example"
|
|
18
|
+
- copy .env.local to be under the /core
|
|
19
|
+
- edit BLOCKLET_DEV_MOUNT_POINT="/"
|
|
20
|
+
2. Insufficient fund to pay for tx cost from xxx, expected 1.0020909, got 0
|
|
21
|
+
- copy BLOCKLET_DEV_APP_DID
|
|
22
|
+
- transfer 2 TBA in your DID Wallet to your copied address
|
|
13
23
|
### Debug Stripe
|
|
14
24
|
|
|
15
25
|
1. Install and login with instructions from: https://stripe.com/docs/stripe-cli
|
package/api/src/libs/logger.ts
CHANGED
|
@@ -97,10 +97,8 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
|
|
|
97
97
|
const nftMintItem: NftMintItem | undefined = hasNft
|
|
98
98
|
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as 'arcblock' | 'ethereum']
|
|
99
99
|
: undefined;
|
|
100
|
-
const amountPaid = +fromUnitToToken(invoice.
|
|
101
|
-
const paymentInfo: string = `${fromUnitToToken(invoice.
|
|
102
|
-
paymentCurrency.symbol
|
|
103
|
-
}`;
|
|
100
|
+
const amountPaid = +fromUnitToToken(invoice.total, paymentCurrency.decimal);
|
|
101
|
+
const paymentInfo: string = `${fromUnitToToken(invoice.total, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
104
102
|
const currentPeriodStart: string = formatTime(invoice.period_start * 1000);
|
|
105
103
|
const currentPeriodEnd: string = formatTime(invoice.period_end * 1000);
|
|
106
104
|
const duration: string = prettyMsI18n(
|
|
@@ -211,6 +211,12 @@ export async function startNotificationQueue() {
|
|
|
211
211
|
|
|
212
212
|
events.on('customer.subscription.renew_failed', async (subscription: Subscription) => {
|
|
213
213
|
const invoice = await Invoice.findByPk(subscription.latest_invoice_id);
|
|
214
|
+
|
|
215
|
+
logger.info('events.on', 'customer.subscription.renew_failed', {
|
|
216
|
+
subscriptionId: subscription.id,
|
|
217
|
+
invoiceId: subscription.latest_invoice_id,
|
|
218
|
+
});
|
|
219
|
+
|
|
214
220
|
if (invoice && subscription.metadata.renew_failed_reason) {
|
|
215
221
|
notificationQueue.push({
|
|
216
222
|
id: `customer.subscription.renew_failed.${subscription.id}.${invoice.id}`,
|
|
@@ -558,6 +558,9 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
558
558
|
|
|
559
559
|
// 只有在 第一次重试 或者 重试次数超过阈值 的时候才发送邮件,不然邮件频率太高了
|
|
560
560
|
const minRetryMail = updates.minRetryMail || MIN_RETRY_MAIL;
|
|
561
|
+
|
|
562
|
+
logger.warn('catch:PaymentIntent capture failed', { id: paymentIntent.id, attemptCount, minRetryMail, invoice });
|
|
563
|
+
|
|
561
564
|
if ((attemptCount === 1 || attemptCount >= minRetryMail) && invoice.billing_reason === 'subscription_cycle') {
|
|
562
565
|
const subscription = await Subscription.findByPk(invoice.subscription_id);
|
|
563
566
|
if (subscription) {
|
|
@@ -566,6 +569,10 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
566
569
|
renew_failed_reason: result || { sufficient: false, reason: 'TX_SEND_FAILED' },
|
|
567
570
|
}),
|
|
568
571
|
});
|
|
572
|
+
logger.info('createEvent:customer.subscription.renew_failed', {
|
|
573
|
+
subscriptionId: subscription.id,
|
|
574
|
+
invoiceId: invoice.id,
|
|
575
|
+
});
|
|
569
576
|
createEvent('Subscription', 'customer.subscription.renew_failed', subscription);
|
|
570
577
|
}
|
|
571
578
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
1
2
|
import { Router } from 'express';
|
|
2
3
|
import Joi from 'joi';
|
|
3
4
|
import pick from 'lodash/pick';
|
|
@@ -153,4 +154,25 @@ router.delete('/:id', auth, async (req, res) => {
|
|
|
153
154
|
return res.json(doc);
|
|
154
155
|
});
|
|
155
156
|
|
|
157
|
+
router.post('/:id/add-usage-quantity', auth, async (req, res) => {
|
|
158
|
+
const { livemode } = req;
|
|
159
|
+
if (livemode) {
|
|
160
|
+
return res.status(403).json({ error: 'add usage quantity not allowed in livemode' });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const subscriptionItem = await SubscriptionItem.findByPk(req.params.id);
|
|
164
|
+
if (!subscriptionItem) {
|
|
165
|
+
return res.status(404).json({ error: `SubscriptionItem(${req.params.id}) item not found` });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await UsageRecord.create({
|
|
169
|
+
livemode: Boolean(livemode),
|
|
170
|
+
subscription_item_id: subscriptionItem.id,
|
|
171
|
+
quantity: req.body.quantity,
|
|
172
|
+
timestamp: dayjs().unix(),
|
|
173
|
+
} as UsageRecord);
|
|
174
|
+
|
|
175
|
+
return res.json();
|
|
176
|
+
});
|
|
177
|
+
|
|
156
178
|
export default router;
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.281",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@arcblock/validator": "^1.18.123",
|
|
53
53
|
"@blocklet/js-sdk": "1.16.28",
|
|
54
54
|
"@blocklet/logger": "1.16.28",
|
|
55
|
-
"@blocklet/payment-react": "1.13.
|
|
55
|
+
"@blocklet/payment-react": "1.13.281",
|
|
56
56
|
"@blocklet/sdk": "1.16.28",
|
|
57
57
|
"@blocklet/ui-react": "^2.10.1",
|
|
58
58
|
"@blocklet/uploader": "^0.1.11",
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
"cors": "^2.8.5",
|
|
79
79
|
"date-fns": "^3.6.0",
|
|
80
80
|
"dayjs": "^1.11.11",
|
|
81
|
+
"debug": "^4.3.5",
|
|
81
82
|
"dotenv-flow": "^3.3.0",
|
|
82
83
|
"ethers": "^6.13.0",
|
|
83
84
|
"express": "^4.19.2",
|
|
@@ -117,9 +118,10 @@
|
|
|
117
118
|
"devDependencies": {
|
|
118
119
|
"@abtnode/types": "1.16.28",
|
|
119
120
|
"@arcblock/eslint-config-ts": "^0.3.0",
|
|
120
|
-
"@blocklet/payment-types": "1.13.
|
|
121
|
+
"@blocklet/payment-types": "1.13.281",
|
|
121
122
|
"@types/cookie-parser": "^1.4.7",
|
|
122
123
|
"@types/cors": "^2.8.17",
|
|
124
|
+
"@types/debug": "^4.1.12",
|
|
123
125
|
"@types/dotenv-flow": "^3.3.3",
|
|
124
126
|
"@types/express": "^4.17.21",
|
|
125
127
|
"@types/node": "^18.19.34",
|
|
@@ -156,5 +158,5 @@
|
|
|
156
158
|
"parser": "typescript"
|
|
157
159
|
}
|
|
158
160
|
},
|
|
159
|
-
"gitHead": "
|
|
161
|
+
"gitHead": "dea66d4a8bbd397b5a8f8235332349aaca0ab72c"
|
|
160
162
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/* eslint-disable react/require-default-props */
|
|
2
2
|
import Empty from '@arcblock/ux/lib/Empty';
|
|
3
3
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
|
-
import
|
|
4
|
+
import Toast from '@arcblock/ux/lib/Toast';
|
|
5
|
+
import { ConfirmDialog, api, formatError, usePaymentContext } from '@blocklet/payment-react';
|
|
5
6
|
import type { TUsageRecord } from '@blocklet/payment-types';
|
|
6
|
-
import { Alert, Box, Button, CircularProgress } from '@mui/material';
|
|
7
|
+
import { Alert, Box, Button, CircularProgress, TextField } from '@mui/material';
|
|
7
8
|
import { useRequest } from 'ahooks';
|
|
8
9
|
import { useState } from 'react';
|
|
9
10
|
import { Bar, BarChart, Rectangle, Tooltip, XAxis, YAxis } from 'recharts';
|
|
@@ -35,6 +36,16 @@ const colors = {
|
|
|
35
36
|
active: stringToColor('active'),
|
|
36
37
|
};
|
|
37
38
|
|
|
39
|
+
function addUsageQuantity({
|
|
40
|
+
subscriptionItemId,
|
|
41
|
+
params,
|
|
42
|
+
}: {
|
|
43
|
+
subscriptionItemId: string;
|
|
44
|
+
params: import('stripe').Stripe.UsageRecordCreateParams;
|
|
45
|
+
}) {
|
|
46
|
+
return api.post(`api/subscription-items/${subscriptionItemId}/add-usage-quantity`, params).then((res) => res.data);
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
export function UsageRecordDialog({
|
|
39
50
|
subscriptionId,
|
|
40
51
|
id,
|
|
@@ -52,6 +63,8 @@ export function UsageRecordDialog({
|
|
|
52
63
|
const { loading, error, data } = useRequest(() => fetchData(subscriptionId, id, start, end), {
|
|
53
64
|
refreshDeps: [subscriptionId, id, start, end],
|
|
54
65
|
});
|
|
66
|
+
const settings = usePaymentContext();
|
|
67
|
+
const [usageQuantity, setUsageQuantity] = useState(1);
|
|
55
68
|
|
|
56
69
|
if (error) {
|
|
57
70
|
return (
|
|
@@ -79,36 +92,69 @@ export function UsageRecordDialog({
|
|
|
79
92
|
);
|
|
80
93
|
}
|
|
81
94
|
|
|
95
|
+
const handAddUsageQuantity = async () => {
|
|
96
|
+
try {
|
|
97
|
+
await addUsageQuantity({
|
|
98
|
+
subscriptionItemId: id,
|
|
99
|
+
params: {
|
|
100
|
+
quantity: usageQuantity,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
Toast.success(t('admin.usageRecord.add.success'));
|
|
104
|
+
onConfirm();
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error(err);
|
|
107
|
+
Toast.error(formatError(err));
|
|
108
|
+
}
|
|
109
|
+
};
|
|
82
110
|
return (
|
|
83
111
|
<ConfirmDialog
|
|
84
112
|
title={t('admin.subscription.usage.current')}
|
|
85
113
|
message={
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
<>
|
|
115
|
+
{data.list.length > 0 ? (
|
|
116
|
+
<BarChart
|
|
117
|
+
width={480}
|
|
118
|
+
height={240}
|
|
119
|
+
data={data.list.map((item) => ({
|
|
120
|
+
...item,
|
|
121
|
+
date: new Date(item.timestamp * 1000).toLocaleString(),
|
|
122
|
+
}))}
|
|
123
|
+
margin={{
|
|
124
|
+
top: 5,
|
|
125
|
+
right: 5,
|
|
126
|
+
left: 0,
|
|
127
|
+
bottom: 5,
|
|
128
|
+
}}>
|
|
129
|
+
<Tooltip />
|
|
130
|
+
<Bar
|
|
131
|
+
dataKey="quantity"
|
|
132
|
+
fill={colors.normal}
|
|
133
|
+
activeBar={<Rectangle fill={colors.active} strokeWidth={0} />}
|
|
134
|
+
/>
|
|
135
|
+
<XAxis dataKey="date" />
|
|
136
|
+
<YAxis mirror />
|
|
137
|
+
</BarChart>
|
|
138
|
+
) : (
|
|
139
|
+
<Empty>{t('admin.usageRecord.empty')}</Empty>
|
|
140
|
+
)}
|
|
141
|
+
{!settings.livemode && window.location.pathname.includes('/admin/billing') && (
|
|
142
|
+
<Box sx={{ display: 'flex', justifyContent: 'center' }} pt={1} pb={1}>
|
|
143
|
+
<TextField
|
|
144
|
+
id="add-usage-record"
|
|
145
|
+
label={t('admin.usageRecord.add.quantity')}
|
|
146
|
+
type="number"
|
|
147
|
+
size="small"
|
|
148
|
+
InputLabelProps={{
|
|
149
|
+
shrink: true,
|
|
150
|
+
}}
|
|
151
|
+
value={usageQuantity}
|
|
152
|
+
onChange={(e) => setUsageQuantity(+e.target.value)}
|
|
153
|
+
/>
|
|
154
|
+
<Button onClick={handAddUsageQuantity}>{t('admin.usageRecord.add.label')}</Button>
|
|
155
|
+
</Box>
|
|
156
|
+
)}
|
|
157
|
+
</>
|
|
112
158
|
}
|
|
113
159
|
onConfirm={onConfirm}
|
|
114
160
|
onCancel={onConfirm}
|
package/src/locales/en.tsx
CHANGED
|
@@ -506,6 +506,14 @@ export default flat({
|
|
|
506
506
|
},
|
|
507
507
|
usageRecord: {
|
|
508
508
|
empty: 'No usage records',
|
|
509
|
+
add: {
|
|
510
|
+
label: 'Add usage record',
|
|
511
|
+
success: 'Successfully added usage record',
|
|
512
|
+
quantity: 'Quantity',
|
|
513
|
+
},
|
|
509
514
|
},
|
|
510
515
|
},
|
|
516
|
+
empty: {
|
|
517
|
+
image: 'No Image',
|
|
518
|
+
},
|
|
511
519
|
});
|
package/src/locales/zh.tsx
CHANGED
|
@@ -170,7 +170,11 @@ export default function ProductDetail(props: { id: string }) {
|
|
|
170
170
|
<InfoRow
|
|
171
171
|
label={t('admin.product.image.label')}
|
|
172
172
|
value={
|
|
173
|
-
data.images.length ?
|
|
173
|
+
data.images.length ? (
|
|
174
|
+
<img src={data.images[0]} width={160} height={160} alt={data.name} style={{ objectFit: 'cover' }} />
|
|
175
|
+
) : (
|
|
176
|
+
t('empty.image')
|
|
177
|
+
)
|
|
174
178
|
}
|
|
175
179
|
/>
|
|
176
180
|
</Grid>
|