payment-kit 1.15.3 → 1.15.5
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/index.ts +3 -0
- package/api/src/integrations/blocklet/user.ts +30 -0
- package/api/src/libs/invoice.ts +41 -0
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +41 -20
- package/api/src/libs/notification/template/subscription-renew-failed.ts +19 -1
- package/api/src/libs/notification/template/subscription-succeeded.ts +94 -9
- package/api/src/libs/notification/template/subscription-trial-start.ts +92 -18
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +45 -15
- package/api/src/libs/notification/template/subscription-will-renew.ts +28 -11
- package/api/src/libs/util.ts +18 -1
- package/api/src/locales/en.ts +12 -3
- package/api/src/locales/zh.ts +12 -3
- package/api/src/queues/payment.ts +3 -1
- package/api/src/routes/donations.ts +1 -1
- package/api/src/routes/subscriptions.ts +30 -5
- package/api/src/routes/usage-records.ts +13 -4
- package/api/src/store/migrations/20240910-customer-sync.ts +21 -0
- package/api/src/store/models/customer.ts +5 -0
- package/blocklet.yml +1 -1
- package/package.json +9 -9
- package/scripts/sdk.js +25 -2
- package/src/components/payment-link/before-pay.tsx +41 -29
- package/src/components/pricing-table/product-settings.tsx +37 -25
- package/src/pages/admin/index.tsx +0 -1
- package/src/pages/admin/payments/intents/detail.tsx +14 -1
- package/src/pages/admin/payments/payouts/detail.tsx +6 -1
- package/src/pages/admin/products/pricing-tables/create.tsx +3 -0
- package/src/pages/checkout/pricing-table.tsx +26 -7
- package/src/pages/customer/index.tsx +3 -3
- package/src/pages/customer/invoice/past-due.tsx +14 -2
|
@@ -7,7 +7,7 @@ import prettyMsI18n from 'pretty-ms-i18n';
|
|
|
7
7
|
import { getTokenSummaryByDid } from '@api/integrations/arcblock/stake';
|
|
8
8
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
9
9
|
import { translate } from '../../../locales';
|
|
10
|
-
import { Customer, Subscription } from '../../../store/models';
|
|
10
|
+
import { Customer, PaymentMethod, Subscription } from '../../../store/models';
|
|
11
11
|
import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
12
12
|
import { PaymentDetail, getPaymentAmountForCycleSubscription } from '../../payment';
|
|
13
13
|
import { getMainProductName } from '../../product';
|
|
@@ -36,6 +36,7 @@ interface SubscriptionTrialWilEndEmailTemplateContext {
|
|
|
36
36
|
duration: string;
|
|
37
37
|
|
|
38
38
|
viewSubscriptionLink: string;
|
|
39
|
+
paymentMethod: PaymentMethod | null;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export class SubscriptionTrialWilEndEmailTemplate
|
|
@@ -71,6 +72,8 @@ export class SubscriptionTrialWilEndEmailTemplate
|
|
|
71
72
|
},
|
|
72
73
|
})) as PaymentCurrency;
|
|
73
74
|
|
|
75
|
+
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
76
|
+
|
|
74
77
|
const userDid = customer.did;
|
|
75
78
|
const locale = await getUserLocale(userDid);
|
|
76
79
|
const productName = await getMainProductName(subscription.id);
|
|
@@ -116,6 +119,7 @@ export class SubscriptionTrialWilEndEmailTemplate
|
|
|
116
119
|
duration,
|
|
117
120
|
|
|
118
121
|
viewSubscriptionLink,
|
|
122
|
+
paymentMethod,
|
|
119
123
|
};
|
|
120
124
|
}
|
|
121
125
|
|
|
@@ -157,7 +161,7 @@ export class SubscriptionTrialWilEndEmailTemplate
|
|
|
157
161
|
currentPeriodStart,
|
|
158
162
|
currentPeriodEnd,
|
|
159
163
|
duration,
|
|
160
|
-
|
|
164
|
+
paymentMethod,
|
|
161
165
|
viewSubscriptionLink,
|
|
162
166
|
} = await this.getContext();
|
|
163
167
|
|
|
@@ -167,24 +171,27 @@ export class SubscriptionTrialWilEndEmailTemplate
|
|
|
167
171
|
return null;
|
|
168
172
|
}
|
|
169
173
|
|
|
174
|
+
const isStripe = paymentMethod?.type === 'stripe';
|
|
175
|
+
|
|
170
176
|
const template: BaseEmailTemplateType = {
|
|
171
177
|
title: `${translate('notification.subscriptionTrialWillEnd.title', locale, {
|
|
172
178
|
productName,
|
|
173
179
|
willRenewDuration,
|
|
174
180
|
})}`,
|
|
175
|
-
body:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
181
|
+
body:
|
|
182
|
+
canPay || isStripe
|
|
183
|
+
? `${translate('notification.subscriptionTrialWillEnd.body', locale, {
|
|
184
|
+
at,
|
|
185
|
+
productName,
|
|
186
|
+
willRenewDuration,
|
|
187
|
+
})}`
|
|
188
|
+
: `${translate('notification.subscriptionTrialWillEnd.unableToPayBody', locale, {
|
|
189
|
+
at,
|
|
190
|
+
productName,
|
|
191
|
+
willRenewDuration,
|
|
192
|
+
balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
|
|
193
|
+
price: `${paymentDetail.price} ${paymentDetail.symbol}`,
|
|
194
|
+
})}`,
|
|
188
195
|
// @ts-expect-error
|
|
189
196
|
attachments: [
|
|
190
197
|
{
|
|
@@ -235,6 +242,29 @@ export class SubscriptionTrialWilEndEmailTemplate
|
|
|
235
242
|
text: paymentInfo,
|
|
236
243
|
},
|
|
237
244
|
},
|
|
245
|
+
...(!canPay && !isStripe
|
|
246
|
+
? [
|
|
247
|
+
{
|
|
248
|
+
type: 'text',
|
|
249
|
+
data: {
|
|
250
|
+
type: 'plain',
|
|
251
|
+
color: '#9397A1',
|
|
252
|
+
text: translate('notification.common.balanceReminder', locale),
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
type: 'text',
|
|
257
|
+
data: {
|
|
258
|
+
type: 'plain',
|
|
259
|
+
color: '#FF0000',
|
|
260
|
+
text: translate('notification.subscriptionTrialWillEnd.unableToPayReason', locale, {
|
|
261
|
+
balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
|
|
262
|
+
price: `${paymentDetail.price} ${paymentDetail.symbol}`,
|
|
263
|
+
}),
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
]
|
|
267
|
+
: []),
|
|
238
268
|
{
|
|
239
269
|
type: 'text',
|
|
240
270
|
data: {
|
|
@@ -268,14 +268,6 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
268
268
|
at,
|
|
269
269
|
productName,
|
|
270
270
|
willRenewDuration,
|
|
271
|
-
reason: `<span style="color: red;">${translate(
|
|
272
|
-
'notification.subscriptionWillRenew.unableToPayReason',
|
|
273
|
-
locale,
|
|
274
|
-
{
|
|
275
|
-
balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
|
|
276
|
-
price: `${paymentDetail.price} ${paymentDetail.symbol}`,
|
|
277
|
-
}
|
|
278
|
-
)}</span>`,
|
|
279
271
|
})}`,
|
|
280
272
|
// @ts-expect-error
|
|
281
273
|
attachments: [
|
|
@@ -339,12 +331,37 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
339
331
|
type: 'text',
|
|
340
332
|
data: {
|
|
341
333
|
type: 'plain',
|
|
342
|
-
...(!canPay &&
|
|
343
|
-
|
|
344
|
-
|
|
334
|
+
...(!canPay &&
|
|
335
|
+
!isStripe && {
|
|
336
|
+
color: '#FF0000',
|
|
337
|
+
}),
|
|
345
338
|
text: `${paymentDetail.balance} ${paymentDetail.symbol}`,
|
|
346
339
|
},
|
|
347
340
|
},
|
|
341
|
+
...(!canPay && !isStripe
|
|
342
|
+
? [
|
|
343
|
+
{
|
|
344
|
+
type: 'text',
|
|
345
|
+
data: {
|
|
346
|
+
type: 'plain',
|
|
347
|
+
color: '#9397A1',
|
|
348
|
+
text: translate('notification.common.balanceReminder', locale),
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
type: 'text',
|
|
353
|
+
data: {
|
|
354
|
+
type: 'plain',
|
|
355
|
+
color: '#FF0000',
|
|
356
|
+
text: translate('notification.subscriptionWillRenew.unableToPayReason', locale, {
|
|
357
|
+
balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
|
|
358
|
+
price: `${paymentDetail.price} ${paymentDetail.symbol}`,
|
|
359
|
+
}),
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
]
|
|
363
|
+
: []),
|
|
364
|
+
|
|
348
365
|
{
|
|
349
366
|
type: 'text',
|
|
350
367
|
data: {
|
package/api/src/libs/util.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { getWalletDid } from '@blocklet/sdk/lib/did';
|
|
|
6
6
|
import { toStakeAddress } from '@arcblock/did-util';
|
|
7
7
|
import { customAlphabet } from 'nanoid';
|
|
8
8
|
import type { LiteralUnion } from 'type-fest';
|
|
9
|
-
import { withQuery } from 'ufo';
|
|
9
|
+
import { joinURL, withQuery } from 'ufo';
|
|
10
10
|
|
|
11
11
|
import dayjs from './dayjs';
|
|
12
12
|
import { blocklet, wallet } from './auth';
|
|
@@ -272,3 +272,20 @@ export async function getCustomerStakeAddress(customerDid: string, nonce?: strin
|
|
|
272
272
|
|
|
273
273
|
return toStakeAddress(customerDid, wallet.address, nonce);
|
|
274
274
|
}
|
|
275
|
+
|
|
276
|
+
export function getCustomerProfileUrl({
|
|
277
|
+
locale = 'en',
|
|
278
|
+
userDid,
|
|
279
|
+
}: {
|
|
280
|
+
locale: LiteralUnion<'en' | 'zh', string>;
|
|
281
|
+
userDid: string;
|
|
282
|
+
}) {
|
|
283
|
+
const { BLOCKLET_APP_URL } = process.env;
|
|
284
|
+
return joinURL(
|
|
285
|
+
BLOCKLET_APP_URL as string,
|
|
286
|
+
withQuery('.well-known/service/user', {
|
|
287
|
+
locale,
|
|
288
|
+
did: userDid,
|
|
289
|
+
})
|
|
290
|
+
);
|
|
291
|
+
}
|
package/api/src/locales/en.ts
CHANGED
|
@@ -38,6 +38,13 @@ export default flat({
|
|
|
38
38
|
prepaid: 'Prepaid',
|
|
39
39
|
postpaid: 'Postpaid',
|
|
40
40
|
paidType: 'Paid type',
|
|
41
|
+
expandPayment: 'In addition, you have successfully purchased the following products:',
|
|
42
|
+
trialProduct: 'Trial product:',
|
|
43
|
+
subscribeProduct: 'Subscribed product:',
|
|
44
|
+
paymentQuantity: 'Payment quantity',
|
|
45
|
+
qty: '{count} unit',
|
|
46
|
+
failReason: 'Failure reason',
|
|
47
|
+
balanceReminder: 'Balance reminder',
|
|
41
48
|
},
|
|
42
49
|
|
|
43
50
|
sendTo: 'Sent to',
|
|
@@ -55,7 +62,9 @@ export default flat({
|
|
|
55
62
|
title: 'The {productName} trial will end soon',
|
|
56
63
|
body: 'Your trial for {productName} will end in {willRenewDuration}. Please ensure your account balance is sufficient for automatic billing after the trial ends. Thank you for your support and trust!',
|
|
57
64
|
unableToPayBody:
|
|
58
|
-
'Your trial for {productName} will end in {willRenewDuration}.
|
|
65
|
+
'Your trial for {productName} will end in {willRenewDuration}.Your current balance is {balance}, which is less than {price}, Please ensure your balance is sufficient for automatic billing. Thank you for your support and trust!',
|
|
66
|
+
unableToPayReason:
|
|
67
|
+
'The estimated payment amount is {price}, but the current balance is insufficient ({balance}), please ensure that your account has enough balance to avoid payment failure.',
|
|
59
68
|
},
|
|
60
69
|
|
|
61
70
|
subscriptionSucceed: {
|
|
@@ -77,7 +86,7 @@ export default flat({
|
|
|
77
86
|
title: '{productName} automatic payment reminder',
|
|
78
87
|
body: 'Your subscription to {productName} is scheduled for automatic payment on {at}({willRenewDuration} later). If you have any questions or need assistance, please feel free to contact us.',
|
|
79
88
|
unableToPayBody:
|
|
80
|
-
'Your subscription to {productName} is scheduled for automatic payment on {at}({willRenewDuration} later). If you have any questions or need assistance, please feel free to contact us
|
|
89
|
+
'Your subscription to {productName} is scheduled for automatic payment on {at}({willRenewDuration} later). If you have any questions or need assistance, please feel free to contact us.',
|
|
81
90
|
unableToPayReason:
|
|
82
91
|
'The estimated payment amount is {price}, but the current balance is insufficient ({balance}), please ensure that your account has enough balance to avoid payment failure.',
|
|
83
92
|
renewAmount: 'Payment amount',
|
|
@@ -91,7 +100,7 @@ export default flat({
|
|
|
91
100
|
|
|
92
101
|
subscriptionRenewFailed: {
|
|
93
102
|
title: '{productName} automatic payment failed',
|
|
94
|
-
body: 'We are sorry to inform you that your {productName} failed to go through the automatic payment on {at}.
|
|
103
|
+
body: 'We are sorry to inform you that your {productName} failed to go through the automatic payment on {at}. If you have any questions, please contact us in time. Thank you!',
|
|
95
104
|
reason: {
|
|
96
105
|
noDidWallet: 'You have not bound DID Wallet, please bind DID Wallet to ensure sufficient balance',
|
|
97
106
|
noDelegation: 'Your DID Wallet has not been authorized, please update authorization',
|
package/api/src/locales/zh.ts
CHANGED
|
@@ -38,6 +38,13 @@ export default flat({
|
|
|
38
38
|
prepaid: '预付费',
|
|
39
39
|
postpaid: '后付费',
|
|
40
40
|
paidType: '付费类型',
|
|
41
|
+
expandPayment: '此外,您还成功购买了以下产品:',
|
|
42
|
+
trialProduct: '试用产品:',
|
|
43
|
+
subscribeProduct: '订阅产品:',
|
|
44
|
+
paymentQuantity: '购买数量',
|
|
45
|
+
qty: '{count} 件',
|
|
46
|
+
failReason: '失败原因',
|
|
47
|
+
balanceReminder: '余额提醒',
|
|
41
48
|
},
|
|
42
49
|
|
|
43
50
|
sendTo: '发送给',
|
|
@@ -55,7 +62,9 @@ export default flat({
|
|
|
55
62
|
title: '{productName} 试用期即将结束',
|
|
56
63
|
body: '您订阅的 {productName} 试用资格将在 {willRenewDuration} 后结束。请确保您的账户余额充足,以便在试用期结束后自动完成扣费。感谢您的支持与信任!',
|
|
57
64
|
unableToPayBody:
|
|
58
|
-
'您订阅的 {productName} 试用资格将在 {willRenewDuration}
|
|
65
|
+
'您订阅的 {productName} 试用资格将在 {willRenewDuration} 后结束。您的当前余额为 {balance},不足 {price},请确保余额充足以便自动完成扣费。感谢您的支持与信任!',
|
|
66
|
+
unableToPayReason:
|
|
67
|
+
'预计扣款金额为 {price},但当前余额不足(余额为 {balance}),请确保您的账户余额充足,避免扣费失败。',
|
|
59
68
|
},
|
|
60
69
|
|
|
61
70
|
subscriptionSucceed: {
|
|
@@ -77,7 +86,7 @@ export default flat({
|
|
|
77
86
|
title: '{productName} 自动扣费提醒',
|
|
78
87
|
body: '您订阅的 {productName} 将在 {at}({willRenewDuration}后) 发起自动扣费。若有任何疑问或需要帮助,请随时与我们联系。',
|
|
79
88
|
unableToPayBody:
|
|
80
|
-
'您订阅的 {productName} 将在 {at}({willRenewDuration}后)
|
|
89
|
+
'您订阅的 {productName} 将在 {at}({willRenewDuration}后) 发起自动扣费,若有任何疑问或者需要帮助,请随时与我们联系。',
|
|
81
90
|
unableToPayReason:
|
|
82
91
|
'预计扣款金额为 {price},但当前余额不足(余额为 {balance}),请确保您的账户余额充足,避免扣费失败。',
|
|
83
92
|
renewAmount: '扣费金额',
|
|
@@ -91,7 +100,7 @@ export default flat({
|
|
|
91
100
|
|
|
92
101
|
subscriptionRenewFailed: {
|
|
93
102
|
title: '{productName} 扣费失败',
|
|
94
|
-
body: '很抱歉地通知您,您的 {productName} 于 {at}
|
|
103
|
+
body: '很抱歉地通知您,您的 {productName} 于 {at} 扣费失败。如有任何疑问,请及时联系我们。谢谢!',
|
|
95
104
|
reason: {
|
|
96
105
|
noDidWallet: '您尚未绑定 DID Wallet,请绑定 DID Wallet,确保余额充足',
|
|
97
106
|
noDelegation: '您的 DID Wallet 尚未授权,请更新授权',
|
|
@@ -724,7 +724,9 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
724
724
|
subscriptionId: subscription.id,
|
|
725
725
|
invoiceId: invoice.id,
|
|
726
726
|
});
|
|
727
|
-
|
|
727
|
+
if (!subscription.isImmutable()) {
|
|
728
|
+
createEvent('Subscription', 'customer.subscription.renew_failed', subscription);
|
|
729
|
+
}
|
|
728
730
|
}
|
|
729
731
|
}
|
|
730
732
|
|
|
@@ -125,7 +125,7 @@ router.get('/', async (req, res) => {
|
|
|
125
125
|
stripUnknown: true,
|
|
126
126
|
});
|
|
127
127
|
const { rows, count } = await CheckoutSession.findAndCountAll({
|
|
128
|
-
where: { payment_link_id: target, status: 'complete' },
|
|
128
|
+
where: { payment_link_id: target, status: 'complete', livemode: req.livemode },
|
|
129
129
|
attributes: [
|
|
130
130
|
'id',
|
|
131
131
|
'customer_id',
|
|
@@ -6,8 +6,7 @@ import isObject from 'lodash/isObject';
|
|
|
6
6
|
import pick from 'lodash/pick';
|
|
7
7
|
import uniq from 'lodash/uniq';
|
|
8
8
|
|
|
9
|
-
import { literal } from 'sequelize';
|
|
10
|
-
import type { Literal } from 'sequelize/types/utils';
|
|
9
|
+
import { literal, OrderItem } from 'sequelize';
|
|
11
10
|
import { ensureStripeCustomer, ensureStripePrice, ensureStripeSubscription } from '../integrations/stripe/resource';
|
|
12
11
|
import { createListParamSchema, getWhereFromKvQuery, getWhereFromQuery, MetadataSchema } from '../libs/api';
|
|
13
12
|
import dayjs from '../libs/dayjs';
|
|
@@ -76,12 +75,26 @@ const schema = createListParamSchema<{
|
|
|
76
75
|
customer_id?: string;
|
|
77
76
|
customer_did?: string;
|
|
78
77
|
activeFirst?: boolean;
|
|
78
|
+
order?: string | string[] | OrderItem | OrderItem[];
|
|
79
79
|
}>({
|
|
80
80
|
status: Joi.string().empty(''),
|
|
81
81
|
customer_id: Joi.string().empty(''),
|
|
82
82
|
customer_did: Joi.string().empty(''),
|
|
83
83
|
activeFirst: Joi.boolean().optional(),
|
|
84
|
+
order: Joi.alternatives()
|
|
85
|
+
.try(
|
|
86
|
+
Joi.string(),
|
|
87
|
+
Joi.array().items(Joi.string()),
|
|
88
|
+
Joi.array().items(Joi.array().ordered(Joi.string(), Joi.string().valid('ASC', 'DESC').insensitive()))
|
|
89
|
+
)
|
|
90
|
+
.optional(),
|
|
84
91
|
});
|
|
92
|
+
|
|
93
|
+
const parseOrder = (orderStr: string): OrderItem => {
|
|
94
|
+
const [field, direction] = orderStr.split(':');
|
|
95
|
+
return [field ?? '', (direction?.toUpperCase() as 'ASC' | 'DESC') || 'ASC'];
|
|
96
|
+
};
|
|
97
|
+
|
|
85
98
|
router.get('/', authMine, async (req, res) => {
|
|
86
99
|
const { page, pageSize, status, livemode, ...query } = await schema.validateAsync(req.query, {
|
|
87
100
|
stripUnknown: false,
|
|
@@ -118,7 +131,11 @@ router.get('/', authMine, async (req, res) => {
|
|
|
118
131
|
where[key] = query[key];
|
|
119
132
|
});
|
|
120
133
|
|
|
121
|
-
|
|
134
|
+
let order: OrderItem[] = [];
|
|
135
|
+
if (query.order) {
|
|
136
|
+
const orderItems = Array.isArray(query.order) ? query.order : [query.order];
|
|
137
|
+
order = orderItems.map((item) => (typeof item === 'string' ? parseOrder(item) : (item as OrderItem)));
|
|
138
|
+
}
|
|
122
139
|
|
|
123
140
|
if (query.activeFirst) {
|
|
124
141
|
order.unshift([
|
|
@@ -127,6 +144,14 @@ router.get('/', authMine, async (req, res) => {
|
|
|
127
144
|
]);
|
|
128
145
|
}
|
|
129
146
|
|
|
147
|
+
const hasCreatedAtOrUpdatedAtOrder = order.some(
|
|
148
|
+
(item) => Array.isArray(item) && ['created_at', 'updated_at'].includes(item[0] as string)
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (!hasCreatedAtOrUpdatedAtOrder) {
|
|
152
|
+
order.push(['created_at', query.o === 'asc' ? 'ASC' : 'DESC']);
|
|
153
|
+
}
|
|
154
|
+
|
|
130
155
|
try {
|
|
131
156
|
const { rows: list, count } = await Subscription.findAndCountAll({
|
|
132
157
|
where,
|
|
@@ -278,7 +303,7 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
|
278
303
|
feedback: feedback || 'other',
|
|
279
304
|
return_stake: canReturnStake && haveStake,
|
|
280
305
|
slash_stake: slashStake && haveStake,
|
|
281
|
-
slash_reason: slashReason,
|
|
306
|
+
slash_reason: slashStake && haveStake ? slashReason : '',
|
|
282
307
|
},
|
|
283
308
|
};
|
|
284
309
|
const now = dayjs().unix() + 3;
|
|
@@ -292,7 +317,7 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
|
292
317
|
comment,
|
|
293
318
|
return_stake: canReturnStake && haveStake,
|
|
294
319
|
slash_stake: slashStake && haveStake,
|
|
295
|
-
slash_reason: slashReason,
|
|
320
|
+
slash_reason: slashStake && haveStake ? slashReason : '',
|
|
296
321
|
};
|
|
297
322
|
updates.canceled_at = now;
|
|
298
323
|
if (inTrialing) {
|
|
@@ -136,10 +136,21 @@ router.get('/summary', auth, async (req, res) => {
|
|
|
136
136
|
}
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
+
const UsageRecordScheme = Joi.object({
|
|
140
|
+
subscription_item_id: Joi.string().required(),
|
|
141
|
+
start: Joi.number().optional(),
|
|
142
|
+
end: Joi.number().optional(),
|
|
143
|
+
livemode: Joi.boolean().empty('').optional(),
|
|
144
|
+
q: Joi.string().empty('').optional(), // query
|
|
145
|
+
o: Joi.string().empty('').optional(), // order
|
|
146
|
+
}).unknown(true);
|
|
147
|
+
|
|
139
148
|
export function createUsageRecordQueryFn(doc?: Subscription) {
|
|
140
149
|
return async (req: Request, res: Response) => {
|
|
141
|
-
const {
|
|
142
|
-
|
|
150
|
+
const { error, value: query } = await UsageRecordScheme.validate(req.query, { stripUnknown: true });
|
|
151
|
+
if (error) {
|
|
152
|
+
return res.status(400).json({ error: `usage record request query invalid: ${error.message}` });
|
|
153
|
+
}
|
|
143
154
|
try {
|
|
144
155
|
const item = await SubscriptionItem.findByPk(query.subscription_item_id);
|
|
145
156
|
if (!item) {
|
|
@@ -159,8 +170,6 @@ export function createUsageRecordQueryFn(doc?: Subscription) {
|
|
|
159
170
|
},
|
|
160
171
|
},
|
|
161
172
|
order: [['created_at', 'ASC']],
|
|
162
|
-
offset: (page - 1) * pageSize,
|
|
163
|
-
limit: pageSize,
|
|
164
173
|
});
|
|
165
174
|
|
|
166
175
|
res.json({ count, list });
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
|
|
3
|
+
import { Migration, safeApplyColumnChanges } from '../migrate';
|
|
4
|
+
|
|
5
|
+
export const up: Migration = async ({ context }) => {
|
|
6
|
+
await safeApplyColumnChanges(context, {
|
|
7
|
+
customers: [
|
|
8
|
+
{
|
|
9
|
+
name: 'last_sync_at',
|
|
10
|
+
field: {
|
|
11
|
+
type: DataTypes.INTEGER,
|
|
12
|
+
allowNull: true,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const down: Migration = async ({ context }) => {
|
|
20
|
+
await context.removeColumn('customers', 'last_sync_at');
|
|
21
|
+
};
|
|
@@ -63,6 +63,7 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
|
|
|
63
63
|
|
|
64
64
|
declare created_at: CreationOptional<Date>;
|
|
65
65
|
declare updated_at: CreationOptional<Date>;
|
|
66
|
+
declare last_sync_at?: number;
|
|
66
67
|
|
|
67
68
|
public static readonly GENESIS_ATTRIBUTES = {
|
|
68
69
|
id: {
|
|
@@ -228,6 +229,10 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
|
|
|
228
229
|
type: DataTypes.JSON,
|
|
229
230
|
defaultValue: {},
|
|
230
231
|
},
|
|
232
|
+
last_sync_at: {
|
|
233
|
+
type: DataTypes.INTEGER,
|
|
234
|
+
allowNull: true,
|
|
235
|
+
},
|
|
231
236
|
},
|
|
232
237
|
{
|
|
233
238
|
sequelize,
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.5",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -45,18 +45,18 @@
|
|
|
45
45
|
"@abtnode/cron": "1.16.30",
|
|
46
46
|
"@arcblock/did": "^1.18.135",
|
|
47
47
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
48
|
-
"@arcblock/did-connect": "^2.10.
|
|
48
|
+
"@arcblock/did-connect": "^2.10.33",
|
|
49
49
|
"@arcblock/did-util": "^1.18.135",
|
|
50
50
|
"@arcblock/jwt": "^1.18.135",
|
|
51
|
-
"@arcblock/ux": "2.10.
|
|
51
|
+
"@arcblock/ux": "^2.10.33",
|
|
52
52
|
"@arcblock/validator": "^1.18.135",
|
|
53
53
|
"@blocklet/js-sdk": "1.16.30",
|
|
54
54
|
"@blocklet/logger": "1.16.30",
|
|
55
|
-
"@blocklet/payment-react": "1.15.
|
|
55
|
+
"@blocklet/payment-react": "1.15.5",
|
|
56
56
|
"@blocklet/sdk": "1.16.30",
|
|
57
|
-
"@blocklet/ui-react": "^2.10.
|
|
58
|
-
"@blocklet/uploader": "^0.1.
|
|
59
|
-
"@blocklet/xss": "^0.1.
|
|
57
|
+
"@blocklet/ui-react": "^2.10.33",
|
|
58
|
+
"@blocklet/uploader": "^0.1.35",
|
|
59
|
+
"@blocklet/xss": "^0.1.7",
|
|
60
60
|
"@mui/icons-material": "^5.16.6",
|
|
61
61
|
"@mui/lab": "^5.0.0-alpha.173",
|
|
62
62
|
"@mui/material": "^5.16.6",
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"devDependencies": {
|
|
119
119
|
"@abtnode/types": "1.16.30",
|
|
120
120
|
"@arcblock/eslint-config-ts": "^0.3.2",
|
|
121
|
-
"@blocklet/payment-types": "1.15.
|
|
121
|
+
"@blocklet/payment-types": "1.15.5",
|
|
122
122
|
"@types/cookie-parser": "^1.4.7",
|
|
123
123
|
"@types/cors": "^2.8.17",
|
|
124
124
|
"@types/debug": "^4.1.12",
|
|
@@ -160,5 +160,5 @@
|
|
|
160
160
|
"parser": "typescript"
|
|
161
161
|
}
|
|
162
162
|
},
|
|
163
|
-
"gitHead": "
|
|
163
|
+
"gitHead": "013d9fb8161d6021b66da18916f49242dc156580"
|
|
164
164
|
}
|
package/scripts/sdk.js
CHANGED
|
@@ -6,9 +6,19 @@ const payment = require('@blocklet/payment-js').default;
|
|
|
6
6
|
|
|
7
7
|
(async () => {
|
|
8
8
|
payment.environments.setTestMode(true);
|
|
9
|
-
const
|
|
9
|
+
const subcriptions = await payment.subscriptions.list({
|
|
10
|
+
order: 'updated_at:ASC',
|
|
11
|
+
// order: [
|
|
12
|
+
// ['status', 'DESC'],
|
|
13
|
+
// ['updated_at', 'ASC'],
|
|
14
|
+
// ], // also support sequelize order
|
|
15
|
+
activeFirst: true,
|
|
16
|
+
});
|
|
17
|
+
console.log('🚀 ~ subcriptions:', subcriptions);
|
|
10
18
|
|
|
11
|
-
|
|
19
|
+
// const paymentIntent = await payment.paymentIntents.retrieve('pi_ybTOCWweEnb9grWZsTH7MCVi');
|
|
20
|
+
|
|
21
|
+
// console.log('paymentIntent', paymentIntent);
|
|
12
22
|
|
|
13
23
|
// const refundResult = await payment.paymentIntents.refund('pi_ybTOCWweEnb9grWZsTH7MCVi', {
|
|
14
24
|
// amount: '0.001',
|
|
@@ -81,5 +91,18 @@ const payment = require('@blocklet/payment-js').default;
|
|
|
81
91
|
// expires_at: 1721121607,
|
|
82
92
|
// });
|
|
83
93
|
// console.log('checkoutSession', checkoutSession);
|
|
94
|
+
// const product = await payment.products.create({
|
|
95
|
+
// name: 'Test SDK product',
|
|
96
|
+
// description: 'test',
|
|
97
|
+
// });
|
|
98
|
+
// console.log('🚀 ~ product:', product);
|
|
99
|
+
// const paymentPrice = await payment.prices.create({
|
|
100
|
+
// product_id: product.id,
|
|
101
|
+
// type: 'one_time',
|
|
102
|
+
// unit_amount: '1',
|
|
103
|
+
// currency_id: 'pc_9l5sh8bcjbLU',
|
|
104
|
+
// });
|
|
105
|
+
// console.log('🚀 ~ paymentPrice:', paymentPrice);
|
|
106
|
+
|
|
84
107
|
process.exit(0);
|
|
85
108
|
})();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable no-nested-ternary */
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
3
|
import { Checkbox, FormControlLabel, Stack, TextField, Typography } from '@mui/material';
|
|
4
|
-
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
|
5
5
|
import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
|
6
6
|
import { useSearchParams } from 'react-router-dom';
|
|
7
7
|
|
|
@@ -29,6 +29,7 @@ export default function BeforePay({
|
|
|
29
29
|
const items = useFieldArray({ control, name: 'line_items' });
|
|
30
30
|
const includeFreeTrial = useWatch({ control, name: 'include_free_trial' });
|
|
31
31
|
const [state, setState] = useState({ creating: false });
|
|
32
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
32
33
|
|
|
33
34
|
useEffect(() => {
|
|
34
35
|
if (items.fields.length) {
|
|
@@ -56,6 +57,14 @@ export default function BeforePay({
|
|
|
56
57
|
setValue('invoice_creation.enabled', true);
|
|
57
58
|
}
|
|
58
59
|
}
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
if (containerRef.current) {
|
|
62
|
+
containerRef.current.scrollTo({
|
|
63
|
+
top: containerRef.current.scrollHeight,
|
|
64
|
+
behavior: 'smooth',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}, 0);
|
|
59
68
|
}
|
|
60
69
|
};
|
|
61
70
|
|
|
@@ -87,35 +96,38 @@ export default function BeforePay({
|
|
|
87
96
|
{t('admin.paymentLink.products')} ({getValues().line_items.length})
|
|
88
97
|
</Typography>
|
|
89
98
|
<Stack spacing={2} sx={{ width: '100%' }}>
|
|
90
|
-
{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
<Stack direction="column" sx={{ maxHeight: 500, width: '100%', overflowY: 'auto', gap: 2 }} ref={containerRef}>
|
|
100
|
+
{items.fields.map((item, index) => {
|
|
101
|
+
// @ts-ignore
|
|
102
|
+
const product = getProductByPriceId(products, item.price_id);
|
|
103
|
+
if (!product) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<LineItem
|
|
109
|
+
key={item.id}
|
|
110
|
+
// @ts-ignore
|
|
111
|
+
valid={isPriceAligned(items.fields, products, index).aligned}
|
|
112
|
+
prefix={`line_items.${index}`}
|
|
113
|
+
product={product}
|
|
114
|
+
onRemove={() => items.remove(index)}
|
|
115
|
+
onUpdate={refresh}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
})}
|
|
119
|
+
{items.fields.some((_, index) => !isPriceAligned(items.fields as any[], products, index).recurring) && (
|
|
120
|
+
<Typography color="error" fontSize="small">
|
|
121
|
+
{t('admin.paymentLink.recurringNotAligned')}
|
|
122
|
+
</Typography>
|
|
123
|
+
)}
|
|
124
|
+
{items.fields.some((_, index) => !isPriceAligned(items.fields as any[], products, index).currency) && (
|
|
125
|
+
<Typography color="error" fontSize="small">
|
|
126
|
+
{t('admin.paymentLink.currencyNotAligned')}
|
|
127
|
+
</Typography>
|
|
128
|
+
)}
|
|
129
|
+
</Stack>
|
|
96
130
|
|
|
97
|
-
return (
|
|
98
|
-
<LineItem
|
|
99
|
-
key={item.id}
|
|
100
|
-
// @ts-ignore
|
|
101
|
-
valid={isPriceAligned(items.fields, products, index).aligned}
|
|
102
|
-
prefix={`line_items.${index}`}
|
|
103
|
-
product={product}
|
|
104
|
-
onRemove={() => items.remove(index)}
|
|
105
|
-
onUpdate={refresh}
|
|
106
|
-
/>
|
|
107
|
-
);
|
|
108
|
-
})}
|
|
109
|
-
{items.fields.some((_, index) => !isPriceAligned(items.fields as any[], products, index).recurring) && (
|
|
110
|
-
<Typography color="error" fontSize="small">
|
|
111
|
-
{t('admin.paymentLink.recurringNotAligned')}
|
|
112
|
-
</Typography>
|
|
113
|
-
)}
|
|
114
|
-
{items.fields.some((_, index) => !isPriceAligned(items.fields as any[], products, index).currency) && (
|
|
115
|
-
<Typography color="error" fontSize="small">
|
|
116
|
-
{t('admin.paymentLink.currencyNotAligned')}
|
|
117
|
-
</Typography>
|
|
118
|
-
)}
|
|
119
131
|
<ProductSelect
|
|
120
132
|
mode={items.fields.length ? 'waiting' : 'selecting'}
|
|
121
133
|
onSelect={onProductSelected}
|