payment-kit 1.15.4 → 1.15.6
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 +31 -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-renewed.ts +1 -1
- package/api/src/libs/notification/template/subscription-succeeded.ts +96 -11
- package/api/src/libs/notification/template/subscription-trial-start.ts +92 -18
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +46 -17
- package/api/src/libs/notification/template/subscription-will-renew.ts +38 -14
- package/api/src/libs/payment.ts +12 -0
- 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/checkout-sessions.ts +1 -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/filter-toolbar.tsx +1 -1
- 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
|
@@ -4,11 +4,10 @@ import { fromUnitToToken } from '@ocap/util';
|
|
|
4
4
|
import type { ManipulateType } from 'dayjs';
|
|
5
5
|
import prettyMsI18n from 'pretty-ms-i18n';
|
|
6
6
|
|
|
7
|
-
import { getTokenSummaryByDid } from '
|
|
7
|
+
import { getTokenSummaryByDid } from '../../../integrations/arcblock/stake';
|
|
8
8
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
9
9
|
import { translate } from '../../../locales';
|
|
10
|
-
import { Customer, Subscription } from '../../../store/models';
|
|
11
|
-
import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
10
|
+
import { Customer, PaymentMethod, Subscription, PaymentCurrency } from '../../../store/models';
|
|
12
11
|
import { PaymentDetail, getPaymentAmountForCycleSubscription } from '../../payment';
|
|
13
12
|
import { getMainProductName } from '../../product';
|
|
14
13
|
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
@@ -36,6 +35,7 @@ interface SubscriptionTrialWilEndEmailTemplateContext {
|
|
|
36
35
|
duration: string;
|
|
37
36
|
|
|
38
37
|
viewSubscriptionLink: string;
|
|
38
|
+
paymentMethod: PaymentMethod | null;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export class SubscriptionTrialWilEndEmailTemplate
|
|
@@ -71,6 +71,8 @@ export class SubscriptionTrialWilEndEmailTemplate
|
|
|
71
71
|
},
|
|
72
72
|
})) as PaymentCurrency;
|
|
73
73
|
|
|
74
|
+
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
75
|
+
|
|
74
76
|
const userDid = customer.did;
|
|
75
77
|
const locale = await getUserLocale(userDid);
|
|
76
78
|
const productName = await getMainProductName(subscription.id);
|
|
@@ -116,6 +118,7 @@ export class SubscriptionTrialWilEndEmailTemplate
|
|
|
116
118
|
duration,
|
|
117
119
|
|
|
118
120
|
viewSubscriptionLink,
|
|
121
|
+
paymentMethod,
|
|
119
122
|
};
|
|
120
123
|
}
|
|
121
124
|
|
|
@@ -157,7 +160,7 @@ export class SubscriptionTrialWilEndEmailTemplate
|
|
|
157
160
|
currentPeriodStart,
|
|
158
161
|
currentPeriodEnd,
|
|
159
162
|
duration,
|
|
160
|
-
|
|
163
|
+
paymentMethod,
|
|
161
164
|
viewSubscriptionLink,
|
|
162
165
|
} = await this.getContext();
|
|
163
166
|
|
|
@@ -167,24 +170,27 @@ export class SubscriptionTrialWilEndEmailTemplate
|
|
|
167
170
|
return null;
|
|
168
171
|
}
|
|
169
172
|
|
|
173
|
+
const isStripe = paymentMethod?.type === 'stripe';
|
|
174
|
+
|
|
170
175
|
const template: BaseEmailTemplateType = {
|
|
171
176
|
title: `${translate('notification.subscriptionTrialWillEnd.title', locale, {
|
|
172
177
|
productName,
|
|
173
178
|
willRenewDuration,
|
|
174
179
|
})}`,
|
|
175
|
-
body:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
180
|
+
body:
|
|
181
|
+
canPay || isStripe
|
|
182
|
+
? `${translate('notification.subscriptionTrialWillEnd.body', locale, {
|
|
183
|
+
at,
|
|
184
|
+
productName,
|
|
185
|
+
willRenewDuration,
|
|
186
|
+
})}`
|
|
187
|
+
: `${translate('notification.subscriptionTrialWillEnd.unableToPayBody', locale, {
|
|
188
|
+
at,
|
|
189
|
+
productName,
|
|
190
|
+
willRenewDuration,
|
|
191
|
+
balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
|
|
192
|
+
price: `${paymentDetail.price} ${paymentDetail.symbol}`,
|
|
193
|
+
})}`,
|
|
188
194
|
// @ts-expect-error
|
|
189
195
|
attachments: [
|
|
190
196
|
{
|
|
@@ -235,6 +241,29 @@ export class SubscriptionTrialWilEndEmailTemplate
|
|
|
235
241
|
text: paymentInfo,
|
|
236
242
|
},
|
|
237
243
|
},
|
|
244
|
+
...(!canPay && !isStripe
|
|
245
|
+
? [
|
|
246
|
+
{
|
|
247
|
+
type: 'text',
|
|
248
|
+
data: {
|
|
249
|
+
type: 'plain',
|
|
250
|
+
color: '#9397A1',
|
|
251
|
+
text: translate('notification.common.balanceReminder', locale),
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
type: 'text',
|
|
256
|
+
data: {
|
|
257
|
+
type: 'plain',
|
|
258
|
+
color: '#FF0000',
|
|
259
|
+
text: translate('notification.subscriptionTrialWillEnd.unableToPayReason', locale, {
|
|
260
|
+
balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
|
|
261
|
+
price: `${paymentDetail.price} ${paymentDetail.symbol}`,
|
|
262
|
+
}),
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
]
|
|
266
|
+
: []),
|
|
238
267
|
{
|
|
239
268
|
type: 'text',
|
|
240
269
|
data: {
|
|
@@ -5,12 +5,19 @@ import dayjs from 'dayjs';
|
|
|
5
5
|
import prettyMsI18n from 'pretty-ms-i18n';
|
|
6
6
|
import type { LiteralUnion } from 'type-fest';
|
|
7
7
|
|
|
8
|
-
import { getTokenSummaryByDid } from '@api/integrations/arcblock/stake';
|
|
9
8
|
import { fromUnitToToken } from '@ocap/util';
|
|
9
|
+
import { getTokenSummaryByDid } from '../../../integrations/arcblock/stake';
|
|
10
10
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
11
11
|
import { translate } from '../../../locales';
|
|
12
|
-
import {
|
|
13
|
-
|
|
12
|
+
import {
|
|
13
|
+
Customer,
|
|
14
|
+
Invoice,
|
|
15
|
+
PaymentMethod,
|
|
16
|
+
Price,
|
|
17
|
+
Subscription,
|
|
18
|
+
SubscriptionItem,
|
|
19
|
+
PaymentCurrency,
|
|
20
|
+
} from '../../../store/models';
|
|
14
21
|
import { getPaymentAmountForCycleSubscription, type PaymentDetail } from '../../payment';
|
|
15
22
|
import { getMainProductName } from '../../product';
|
|
16
23
|
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
@@ -268,14 +275,6 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
268
275
|
at,
|
|
269
276
|
productName,
|
|
270
277
|
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
278
|
})}`,
|
|
280
279
|
// @ts-expect-error
|
|
281
280
|
attachments: [
|
|
@@ -339,12 +338,37 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
339
338
|
type: 'text',
|
|
340
339
|
data: {
|
|
341
340
|
type: 'plain',
|
|
342
|
-
...(!canPay &&
|
|
343
|
-
|
|
344
|
-
|
|
341
|
+
...(!canPay &&
|
|
342
|
+
!isStripe && {
|
|
343
|
+
color: '#FF0000',
|
|
344
|
+
}),
|
|
345
345
|
text: `${paymentDetail.balance} ${paymentDetail.symbol}`,
|
|
346
346
|
},
|
|
347
347
|
},
|
|
348
|
+
...(!canPay && !isStripe
|
|
349
|
+
? [
|
|
350
|
+
{
|
|
351
|
+
type: 'text',
|
|
352
|
+
data: {
|
|
353
|
+
type: 'plain',
|
|
354
|
+
color: '#9397A1',
|
|
355
|
+
text: translate('notification.common.balanceReminder', locale),
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
type: 'text',
|
|
360
|
+
data: {
|
|
361
|
+
type: 'plain',
|
|
362
|
+
color: '#FF0000',
|
|
363
|
+
text: translate('notification.subscriptionWillRenew.unableToPayReason', locale, {
|
|
364
|
+
balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
|
|
365
|
+
price: `${paymentDetail.price} ${paymentDetail.symbol}`,
|
|
366
|
+
}),
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
]
|
|
370
|
+
: []),
|
|
371
|
+
|
|
348
372
|
{
|
|
349
373
|
type: 'text',
|
|
350
374
|
data: {
|
package/api/src/libs/payment.ts
CHANGED
|
@@ -357,10 +357,22 @@ export async function getPaymentAmountForCycleSubscription(
|
|
|
357
357
|
paymentCurrency: PaymentCurrency
|
|
358
358
|
) {
|
|
359
359
|
const subscriptionItems = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
|
|
360
|
+
if (subscriptionItems.length === 0) {
|
|
361
|
+
logger.info('subscription items not found in getPaymentAmountForCycleSubscription', {
|
|
362
|
+
subscription: subscription.id,
|
|
363
|
+
});
|
|
364
|
+
return 0;
|
|
365
|
+
}
|
|
360
366
|
let expandedItems = await Price.expand(
|
|
361
367
|
subscriptionItems.map((x) => ({ id: x.id, price_id: x.price_id, quantity: x.quantity })),
|
|
362
368
|
{ product: true }
|
|
363
369
|
);
|
|
370
|
+
if (expandedItems.length === 0) {
|
|
371
|
+
logger.info('expanded items not found in getPaymentAmountForCycleSubscription', {
|
|
372
|
+
subscription: subscription.id,
|
|
373
|
+
});
|
|
374
|
+
return 0;
|
|
375
|
+
}
|
|
364
376
|
const previousPeriodEnd =
|
|
365
377
|
subscription.status === 'trialing' ? subscription.trial_end : subscription.current_period_end;
|
|
366
378
|
const setup = getSubscriptionCycleSetup(subscription.pending_invoice_item_interval, previousPeriodEnd as number);
|
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
|
|
|
@@ -13,7 +13,7 @@ import sortBy from 'lodash/sortBy';
|
|
|
13
13
|
import uniq from 'lodash/uniq';
|
|
14
14
|
import type { WhereOptions } from 'sequelize';
|
|
15
15
|
|
|
16
|
-
import { MetadataSchema } from '
|
|
16
|
+
import { MetadataSchema } from '../libs/api';
|
|
17
17
|
import { checkPassportForPaymentLink } from '../integrations/blocklet/passport';
|
|
18
18
|
import { handleStripePaymentSucceed } from '../integrations/stripe/handlers/payment-intent';
|
|
19
19
|
import { handleStripeSubscriptionSucceed } from '../integrations/stripe/handlers/subscription';
|
|
@@ -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.6",
|
|
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.6",
|
|
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.6",
|
|
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": "5d2b9d8410b424e6aaf0c4f214e0ced6f25d612b"
|
|
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
|
})();
|
|
@@ -128,7 +128,7 @@ function SearchStatus({
|
|
|
128
128
|
<Add sx={{ color: 'text.secondary', cursor: 'pointer', fontSize: '1.2rem' }} />
|
|
129
129
|
)}
|
|
130
130
|
{t('common.status')}
|
|
131
|
-
<span>{search!.status}</span>
|
|
131
|
+
<span>{formatStatus ? formatStatus(search!.status) : search!.status}</span>
|
|
132
132
|
</Button>
|
|
133
133
|
<Menu
|
|
134
134
|
anchorEl={show}
|