payment-kit 1.18.12 → 1.18.13
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/notification/template/one-time-payment-succeeded.ts +5 -3
- package/api/src/libs/notification/template/subscription-canceled.ts +3 -3
- package/api/src/libs/notification/template/subscription-refund-succeeded.ts +4 -3
- package/api/src/libs/notification/template/subscription-renew-failed.ts +5 -4
- package/api/src/libs/notification/template/subscription-renewed.ts +2 -1
- package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +3 -4
- package/api/src/libs/notification/template/subscription-succeeded.ts +2 -1
- package/api/src/libs/notification/template/subscription-upgraded.ts +6 -4
- package/api/src/libs/notification/template/subscription-will-canceled.ts +6 -3
- package/api/src/libs/notification/template/subscription-will-renew.ts +1 -1
- package/api/src/routes/customers.ts +79 -5
- package/api/src/routes/subscriptions.ts +13 -1
- package/api/src/store/models/invoice.ts +4 -2
- package/blocklet.yml +2 -2
- package/package.json +4 -4
- package/src/app.tsx +17 -17
- package/src/components/actions.tsx +32 -9
- package/src/components/copyable.tsx +2 -2
- package/src/components/layout/user.tsx +37 -0
- package/src/components/subscription/portal/actions.tsx +26 -5
- package/src/components/subscription/portal/list.tsx +24 -6
- package/src/components/subscription/status.tsx +2 -2
- package/src/libs/util.ts +15 -0
- package/src/pages/admin/payments/payouts/detail.tsx +6 -1
- package/src/pages/customer/index.tsx +247 -154
- package/src/pages/customer/invoice/detail.tsx +1 -1
- package/src/pages/customer/payout/detail.tsx +9 -2
- package/src/pages/customer/recharge.tsx +6 -2
- package/src/pages/customer/subscription/change-payment.tsx +1 -1
- package/src/pages/customer/subscription/change-plan.tsx +1 -1
- package/src/pages/customer/subscription/detail.tsx +8 -3
- package/src/pages/customer/subscription/embed.tsx +142 -84
|
@@ -87,9 +87,6 @@ export class OneTimePaymentSucceededEmailTemplate
|
|
|
87
87
|
const productName = await getMainProductNameByCheckoutSession(checkoutSession);
|
|
88
88
|
const at: string = formatTime(checkoutSession.created_at);
|
|
89
89
|
|
|
90
|
-
const paymentInfo: string = `${fromUnitToToken(checkoutSession?.amount_total, paymentCurrency.decimal)} ${
|
|
91
|
-
paymentCurrency.symbol
|
|
92
|
-
}`;
|
|
93
90
|
const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
|
|
94
91
|
const nftMintItem: NftMintItem | undefined = hasNft
|
|
95
92
|
? // @ts-expect-error
|
|
@@ -98,6 +95,11 @@ export class OneTimePaymentSucceededEmailTemplate
|
|
|
98
95
|
|
|
99
96
|
const paymentIntent = await PaymentIntent.findByPk(checkoutSession!.payment_intent_id);
|
|
100
97
|
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(paymentIntent!.payment_method_id);
|
|
98
|
+
|
|
99
|
+
const paymentInfo: string = `${fromUnitToToken(checkoutSession?.amount_total, paymentCurrency.decimal)} ${
|
|
100
|
+
paymentCurrency.symbol
|
|
101
|
+
}${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
|
|
102
|
+
|
|
101
103
|
// @ts-expect-error
|
|
102
104
|
const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
|
|
103
105
|
const viewSubscriptionLink = '';
|
|
@@ -76,9 +76,6 @@ export class SubscriptionCanceledEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
76
76
|
const locale = await getUserLocale(userDid);
|
|
77
77
|
const productName = await getMainProductName(subscription.id);
|
|
78
78
|
const at: string = formatTime(subscription.canceled_at * 1000);
|
|
79
|
-
|
|
80
|
-
// @ts-ignore
|
|
81
|
-
const paymentInfo: string = `${fromUnitToToken(invoice.total, invoice?.paymentCurrency?.decimal)} ${invoice?.paymentCurrency?.symbol}`;
|
|
82
79
|
const currentPeriodStart: string = formatTime(invoice.period_start * 1000);
|
|
83
80
|
const currentPeriodEnd: string = formatTime(invoice.period_end * 1000);
|
|
84
81
|
const duration: string = prettyMsI18n(
|
|
@@ -89,6 +86,9 @@ export class SubscriptionCanceledEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
89
86
|
);
|
|
90
87
|
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
|
|
91
88
|
|
|
89
|
+
// @ts-ignore
|
|
90
|
+
const paymentInfo: string = `${fromUnitToToken(invoice.total, invoice?.paymentCurrency?.decimal)} ${invoice?.paymentCurrency?.symbol}${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
|
|
91
|
+
|
|
92
92
|
const customerCancelRequest = subscription.cancelation_details?.reason === 'cancellation_requested';
|
|
93
93
|
let cancellationReason = '';
|
|
94
94
|
const { stakeEnough, stakeReturn, stakeSlash, hasStake } = await getSubscriptionStakeCancellation(
|
|
@@ -75,9 +75,6 @@ export class SubscriptionRefundSucceededEmailTemplate
|
|
|
75
75
|
const productName = await getMainProductName(refund.subscription_id!);
|
|
76
76
|
const at: string = formatTime(refund.created_at);
|
|
77
77
|
|
|
78
|
-
const paymentInfo: string = `${fromUnitToToken(paymentIntent?.amount_received || '0', paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
79
|
-
const refundInfo: string = `${fromUnitToToken(refund.amount, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
80
|
-
|
|
81
78
|
let invoice = null;
|
|
82
79
|
let currentPeriodStart;
|
|
83
80
|
let currentPeriodEnd;
|
|
@@ -105,6 +102,10 @@ export class SubscriptionRefundSucceededEmailTemplate
|
|
|
105
102
|
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(
|
|
106
103
|
refund.payment_method_id || invoice?.default_payment_method_id
|
|
107
104
|
);
|
|
105
|
+
|
|
106
|
+
const paymentInfo: string = `${fromUnitToToken(paymentIntent?.amount_received || '0', paymentCurrency.decimal)} ${paymentCurrency.symbol}${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
|
|
107
|
+
const refundInfo: string = `${fromUnitToToken(refund.amount, paymentCurrency.decimal)} ${paymentCurrency.symbol}${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
|
|
108
|
+
|
|
108
109
|
// @ts-expect-error
|
|
109
110
|
const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
|
|
110
111
|
const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
|
|
@@ -112,9 +112,6 @@ export class SubscriptionRenewFailedEmailTemplate
|
|
|
112
112
|
const nftMintItem: NftMintItem | undefined = hasNft
|
|
113
113
|
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]
|
|
114
114
|
: undefined;
|
|
115
|
-
const paymentInfo: string = `${fromUnitToToken(invoice.amount_remaining, paymentCurrency.decimal)} ${
|
|
116
|
-
paymentCurrency.symbol
|
|
117
|
-
}`;
|
|
118
115
|
|
|
119
116
|
const currentPeriodStart: string = formatTime(invoice.period_start * 1000);
|
|
120
117
|
const currentPeriodEnd: string = formatTime(invoice.period_end * 1000);
|
|
@@ -124,8 +121,12 @@ export class SubscriptionRenewFailedEmailTemplate
|
|
|
124
121
|
locale: getPrettyMsI18nLocale(locale),
|
|
125
122
|
}
|
|
126
123
|
);
|
|
127
|
-
|
|
128
124
|
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
|
|
125
|
+
|
|
126
|
+
const paymentInfo: string = `${fromUnitToToken(invoice.amount_remaining, paymentCurrency.decimal)} ${
|
|
127
|
+
paymentCurrency.symbol
|
|
128
|
+
}${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
|
|
129
|
+
|
|
129
130
|
const chainHost: string | undefined =
|
|
130
131
|
paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]?.api_host;
|
|
131
132
|
const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
|
|
@@ -102,7 +102,6 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
|
|
|
102
102
|
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]
|
|
103
103
|
: undefined;
|
|
104
104
|
const amountPaid = +fromUnitToToken(invoice.total, paymentCurrency.decimal);
|
|
105
|
-
const paymentInfo: string = `${fromUnitToToken(invoice.total, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
106
105
|
const currentPeriodStart: string = formatTime(invoice.period_start * 1000);
|
|
107
106
|
const currentPeriodEnd: string = formatTime(invoice.period_end * 1000);
|
|
108
107
|
const duration: string = prettyMsI18n(
|
|
@@ -113,6 +112,8 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
|
|
|
113
112
|
);
|
|
114
113
|
|
|
115
114
|
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
|
|
115
|
+
const paymentInfo: string = `${fromUnitToToken(invoice.total, paymentCurrency.decimal)} ${paymentCurrency.symbol}${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
|
|
116
|
+
|
|
116
117
|
const chainHost: string | undefined =
|
|
117
118
|
paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]?.api_host;
|
|
118
119
|
const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
|
|
@@ -10,7 +10,6 @@ import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
|
10
10
|
import { formatTime } from '../../time';
|
|
11
11
|
import { getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
|
|
12
12
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
13
|
-
import logger from '../../logger';
|
|
14
13
|
|
|
15
14
|
export interface SubscriptionStakeSlashSucceededEmailTemplateOptions {
|
|
16
15
|
paymentIntentId: string;
|
|
@@ -83,9 +82,10 @@ export class SubscriptionStakeSlashSucceededEmailTemplate
|
|
|
83
82
|
const productName = await getMainProductName(this.options.subscriptionId);
|
|
84
83
|
const at: string = formatTime(paymentIntent.created_at);
|
|
85
84
|
|
|
86
|
-
const slashInfo: string = `${fromUnitToToken(paymentIntent?.amount_received || '0', paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
87
|
-
|
|
88
85
|
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(paymentIntent.payment_method_id);
|
|
86
|
+
|
|
87
|
+
const slashInfo: string = `${fromUnitToToken(paymentIntent?.amount_received || '0', paymentCurrency.decimal)} ${paymentCurrency.symbol}${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
|
|
88
|
+
|
|
89
89
|
// @ts-expect-error
|
|
90
90
|
const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
|
|
91
91
|
const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
|
|
@@ -140,7 +140,6 @@ export class SubscriptionStakeSlashSucceededEmailTemplate
|
|
|
140
140
|
customActions,
|
|
141
141
|
} = await this.getContext();
|
|
142
142
|
|
|
143
|
-
logger.info('SubscriptionStakeSlashSucceededEmailTemplate getTemplate', { productName, at, userDid, slashInfo, viewSubscriptionLink, viewTxHashLink });
|
|
144
143
|
const template: BaseEmailTemplateType = {
|
|
145
144
|
title: `${translate('notification.subscriptionStakeSlashSucceeded.title', locale, {
|
|
146
145
|
productName,
|
|
@@ -120,7 +120,6 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
120
120
|
const oneTimeProductInfo = await getOneTimeProductInfo(subscription.latest_invoice_id as string, paymentCurrency);
|
|
121
121
|
const paymentAmount = await getPaymentAmountForCycleSubscription(subscription, paymentCurrency);
|
|
122
122
|
|
|
123
|
-
const paymentInfo: string = `${paymentAmount} ${paymentCurrency.symbol}`;
|
|
124
123
|
const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
|
|
125
124
|
const nftMintItem: NftMintItem | undefined = hasNft
|
|
126
125
|
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]
|
|
@@ -135,6 +134,8 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
135
134
|
);
|
|
136
135
|
|
|
137
136
|
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
|
|
137
|
+
const paymentInfo: string = `${paymentAmount} ${paymentCurrency.symbol}${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
|
|
138
|
+
|
|
138
139
|
// @FIXME: 获取 chainHost 困难的一批?
|
|
139
140
|
const chainHost: string | undefined =
|
|
140
141
|
paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]?.api_host;
|
|
@@ -88,10 +88,6 @@ export class SubscriptionUpgradedEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
88
88
|
const productName = await getMainProductName(subscription.id);
|
|
89
89
|
const at: string = formatTime(subscription.created_at);
|
|
90
90
|
|
|
91
|
-
const paymentInfo: string = `${fromUnitToToken(
|
|
92
|
-
paymentIntent?.amount || invoice.amount_paid,
|
|
93
|
-
paymentCurrency.decimal
|
|
94
|
-
)} ${paymentCurrency.symbol}`;
|
|
95
91
|
const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
|
|
96
92
|
const nftMintItem: NftMintItem | undefined = hasNft
|
|
97
93
|
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]
|
|
@@ -110,6 +106,12 @@ export class SubscriptionUpgradedEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
110
106
|
);
|
|
111
107
|
|
|
112
108
|
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
|
|
109
|
+
|
|
110
|
+
const paymentInfo: string = `${fromUnitToToken(
|
|
111
|
+
paymentIntent?.amount || invoice.amount_paid,
|
|
112
|
+
paymentCurrency.decimal
|
|
113
|
+
)} ${paymentCurrency.symbol}${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
|
|
114
|
+
|
|
113
115
|
// @ts-expect-error
|
|
114
116
|
const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
|
|
115
117
|
const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
|
|
@@ -6,7 +6,7 @@ import type { ManipulateType } from 'dayjs';
|
|
|
6
6
|
import dayjs from '../../dayjs';
|
|
7
7
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
8
8
|
import { translate } from '../../../locales';
|
|
9
|
-
import { Customer, Invoice, Subscription } from '../../../store/models';
|
|
9
|
+
import { Customer, Invoice, PaymentMethod, Subscription } from '../../../store/models';
|
|
10
10
|
import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
11
11
|
import { getCustomerInvoicePageUrl } from '../../invoice';
|
|
12
12
|
import logger from '../../logger';
|
|
@@ -82,7 +82,10 @@ export class SubscriptionWillCanceledEmailTemplate
|
|
|
82
82
|
where: {
|
|
83
83
|
id: subscription.latest_invoice_id,
|
|
84
84
|
},
|
|
85
|
-
include: [
|
|
85
|
+
include: [
|
|
86
|
+
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
87
|
+
{ model: PaymentMethod, as: 'paymentMethod' },
|
|
88
|
+
],
|
|
86
89
|
});
|
|
87
90
|
if (!invoice) {
|
|
88
91
|
throw new Error(`Invoice(${subscription.latest_invoice_id}) not found`);
|
|
@@ -94,7 +97,7 @@ export class SubscriptionWillCanceledEmailTemplate
|
|
|
94
97
|
const at: string = formatTime(cancelAt * 1000);
|
|
95
98
|
const willCancelDuration: string = getSimplifyDuration((cancelAt - now) * 1000, locale);
|
|
96
99
|
// @ts-ignore
|
|
97
|
-
const paymentInfo: string = `${fromUnitToToken(+invoice.total, invoice?.paymentCurrency?.decimal)} ${invoice?.paymentCurrency?.symbol}`;
|
|
100
|
+
const paymentInfo: string = `${fromUnitToToken(+invoice.total, invoice?.paymentCurrency?.decimal)} ${invoice?.paymentCurrency?.symbol}${invoice?.paymentMethod ? `(${invoice?.paymentMethod.name})` : ''}`;
|
|
98
101
|
|
|
99
102
|
let body: string = translate('notification.subscriptWillCanceled.body', locale, {
|
|
100
103
|
productName,
|
|
@@ -117,7 +117,7 @@ export class SubscriptionWillRenewEmailTemplate
|
|
|
117
117
|
const paidType: string = isPrePaid
|
|
118
118
|
? translate('notification.common.prepaid', locale)
|
|
119
119
|
: translate('notification.common.postpaid', locale);
|
|
120
|
-
const paymentInfo: string = `${paymentDetail?.price || '0'} ${paymentCurrency.symbol}`;
|
|
120
|
+
const paymentInfo: string = `${paymentDetail?.price || '0'} ${paymentCurrency.symbol}${paymentMethod ? ` (${paymentMethod.name})` : ''}`;
|
|
121
121
|
const currentPeriodStart: string = isPrePaid
|
|
122
122
|
? formatTime(invoice.period_end * 1000)
|
|
123
123
|
: formatTime(invoice.period_start * 1000);
|
|
@@ -11,6 +11,7 @@ import { formatMetadata } from '../libs/util';
|
|
|
11
11
|
import { Customer } from '../store/models/customer';
|
|
12
12
|
import { blocklet } from '../libs/auth';
|
|
13
13
|
import logger from '../libs/logger';
|
|
14
|
+
import { Invoice } from '../store/models';
|
|
14
15
|
|
|
15
16
|
const router = Router();
|
|
16
17
|
const auth = authenticate<Customer>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -86,26 +87,99 @@ router.get('/me', sessionMiddleware(), async (req, res) => {
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
try {
|
|
89
|
-
|
|
90
|
+
let doc = await Customer.findByPkOrDid(req.user.did as string);
|
|
90
91
|
const livemode = req.query.livemode ? !!req?.livemode : !!doc?.livemode;
|
|
91
92
|
if (!doc) {
|
|
92
93
|
if (req.query.fallback) {
|
|
93
94
|
const result = await blocklet.getUser(req.user.did);
|
|
94
|
-
res.json({ ...result.user, address: {}, livemode });
|
|
95
|
+
return res.json({ ...result.user, address: {}, livemode });
|
|
96
|
+
}
|
|
97
|
+
if (req.query.create) {
|
|
98
|
+
// create customer
|
|
99
|
+
const { user } = await blocklet.getUser(req.user.did);
|
|
100
|
+
const customer = await Customer.create({
|
|
101
|
+
livemode: true,
|
|
102
|
+
did: req.user.did,
|
|
103
|
+
name: user.fullName,
|
|
104
|
+
email: user.email,
|
|
105
|
+
phone: '',
|
|
106
|
+
address: {},
|
|
107
|
+
description: user.remark,
|
|
108
|
+
metadata: {},
|
|
109
|
+
balance: '0',
|
|
110
|
+
next_invoice_sequence: 1,
|
|
111
|
+
delinquent: false,
|
|
112
|
+
invoice_prefix: Customer.getInvoicePrefix(),
|
|
113
|
+
});
|
|
114
|
+
logger.info('customer created', {
|
|
115
|
+
customerId: customer.id,
|
|
116
|
+
did: customer.did,
|
|
117
|
+
});
|
|
118
|
+
doc = customer;
|
|
95
119
|
} else {
|
|
96
|
-
res.json({ error: 'Customer not found' });
|
|
120
|
+
return res.json({ error: 'Customer not found' });
|
|
97
121
|
}
|
|
98
|
-
}
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
99
124
|
const [summary, stake, token] = await Promise.all([
|
|
100
125
|
doc.getSummary(livemode),
|
|
101
126
|
getStakeSummaryByDid(doc.did, livemode),
|
|
102
127
|
getTokenSummaryByDid(doc.did, livemode),
|
|
103
128
|
]);
|
|
104
129
|
res.json({ ...doc.toJSON(), summary: { ...summary, stake, token }, livemode });
|
|
130
|
+
} catch (summaryErr) {
|
|
131
|
+
logger.error('get customer summary failed', summaryErr);
|
|
132
|
+
if (req.query.skipError) {
|
|
133
|
+
return res.json({
|
|
134
|
+
...doc.toJSON(),
|
|
135
|
+
summary: { stake: {}, token: {} },
|
|
136
|
+
livemode,
|
|
137
|
+
summaryError: summaryErr.message,
|
|
138
|
+
error: `Failed to get customer: ${summaryErr.message}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
throw summaryErr;
|
|
105
142
|
}
|
|
106
143
|
} catch (err) {
|
|
107
144
|
logger.error('get customer failed', err);
|
|
108
|
-
|
|
145
|
+
if (req.query.skipError) {
|
|
146
|
+
return res.json({
|
|
147
|
+
error: `Failed to get customer: ${err.message}`,
|
|
148
|
+
did: req.user?.did,
|
|
149
|
+
name: req.user?.fullName,
|
|
150
|
+
address: {},
|
|
151
|
+
livemode: !!req.query.livemode,
|
|
152
|
+
summary: { stake: {}, token: {} },
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return res.status(500).json({ error: `Failed to get customer: ${err.message}` });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// get overdue invoices
|
|
160
|
+
router.get('/:id/overdue/invoices', auth, async (req, res) => {
|
|
161
|
+
if (!req.user) {
|
|
162
|
+
return res.status(403).json({ error: 'Unauthorized' });
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const doc = await Customer.findByPkOrDid(req.params.id as string);
|
|
166
|
+
if (!doc) {
|
|
167
|
+
return res.status(404).json({ error: 'Customer not found' });
|
|
168
|
+
}
|
|
169
|
+
const [summary, detail, invoices] = await Invoice!.getUncollectibleAmount({
|
|
170
|
+
customerId: doc.id,
|
|
171
|
+
livemode: req.query.livemode ? !!req.query.livemode : doc.livemode,
|
|
172
|
+
});
|
|
173
|
+
const subscriptionCount = new Set(invoices.map((x) => x.subscription_id)).size;
|
|
174
|
+
return res.json({
|
|
175
|
+
summary,
|
|
176
|
+
invoices,
|
|
177
|
+
subscriptionCount,
|
|
178
|
+
detail,
|
|
179
|
+
});
|
|
180
|
+
} catch (err) {
|
|
181
|
+
logger.error(err);
|
|
182
|
+
return res.status(500).json({ error: `Failed to get overdue invoices: ${err.message}` });
|
|
109
183
|
}
|
|
110
184
|
});
|
|
111
185
|
|
|
@@ -97,6 +97,7 @@ const schema = createListParamSchema<{
|
|
|
97
97
|
activeFirst?: boolean;
|
|
98
98
|
price_id?: string;
|
|
99
99
|
order?: string | string[] | OrderItem | OrderItem[];
|
|
100
|
+
showTotalCount?: boolean;
|
|
100
101
|
}>({
|
|
101
102
|
status: Joi.string().empty(''),
|
|
102
103
|
customer_id: Joi.string().empty(''),
|
|
@@ -110,6 +111,7 @@ const schema = createListParamSchema<{
|
|
|
110
111
|
Joi.array().items(Joi.array().ordered(Joi.string(), Joi.string().valid('ASC', 'DESC').insensitive()))
|
|
111
112
|
)
|
|
112
113
|
.optional(),
|
|
114
|
+
showTotalCount: Joi.boolean().optional(),
|
|
113
115
|
});
|
|
114
116
|
|
|
115
117
|
const parseOrder = (orderStr: string): OrderItem => {
|
|
@@ -195,7 +197,17 @@ router.get('/', authMine, async (req, res) => {
|
|
|
195
197
|
// @ts-ignore
|
|
196
198
|
docs.forEach((x) => expandLineItems(x.items, products, prices));
|
|
197
199
|
|
|
198
|
-
|
|
200
|
+
if (query.showTotalCount) {
|
|
201
|
+
const totalCount = await Subscription.count({
|
|
202
|
+
where: {
|
|
203
|
+
customer_id: where.customer_id,
|
|
204
|
+
},
|
|
205
|
+
distinct: true,
|
|
206
|
+
});
|
|
207
|
+
res.json({ count, list: docs, paging: { page, pageSize }, totalCount });
|
|
208
|
+
} else {
|
|
209
|
+
res.json({ count, list: docs, paging: { page, pageSize } });
|
|
210
|
+
}
|
|
199
211
|
} catch (err) {
|
|
200
212
|
logger.error(err);
|
|
201
213
|
res.json({ count: 0, list: [], paging: { page, pageSize } });
|
|
@@ -530,7 +530,9 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
|
|
|
530
530
|
return ['paid', 'void'].includes(this.status);
|
|
531
531
|
}
|
|
532
532
|
|
|
533
|
-
private static async _getUncollectibleAmount(
|
|
533
|
+
private static async _getUncollectibleAmount(
|
|
534
|
+
where: WhereOptions<Invoice>
|
|
535
|
+
): Promise<[GroupedBN, GroupedStrList, Invoice[]]> {
|
|
534
536
|
const invoices = await Invoice.findAll({ where });
|
|
535
537
|
const summary: GroupedBN = {};
|
|
536
538
|
const detail: GroupedStrList = {};
|
|
@@ -548,7 +550,7 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
|
|
|
548
550
|
detail[key]!.push(invoice.id);
|
|
549
551
|
});
|
|
550
552
|
|
|
551
|
-
return [summary, detail];
|
|
553
|
+
return [summary, detail, invoices];
|
|
552
554
|
}
|
|
553
555
|
|
|
554
556
|
public static getUncollectibleAmount({
|
package/blocklet.yml
CHANGED
|
@@ -14,7 +14,7 @@ repository:
|
|
|
14
14
|
type: git
|
|
15
15
|
url: git+https://github.com/blocklet/payment-kit.git
|
|
16
16
|
specVersion: 1.2.8
|
|
17
|
-
version: 1.18.
|
|
17
|
+
version: 1.18.13
|
|
18
18
|
logo: logo.png
|
|
19
19
|
files:
|
|
20
20
|
- dist
|
|
@@ -113,7 +113,7 @@ navigation:
|
|
|
113
113
|
icon: ion:receipt-outline
|
|
114
114
|
link: /customer
|
|
115
115
|
section:
|
|
116
|
-
-
|
|
116
|
+
- userCenter
|
|
117
117
|
- sessionManager
|
|
118
118
|
role:
|
|
119
119
|
- owner
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.13",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@arcblock/validator": "^1.19.14",
|
|
54
54
|
"@blocklet/js-sdk": "^1.16.39",
|
|
55
55
|
"@blocklet/logger": "^1.16.39",
|
|
56
|
-
"@blocklet/payment-react": "1.18.
|
|
56
|
+
"@blocklet/payment-react": "1.18.13",
|
|
57
57
|
"@blocklet/sdk": "^1.16.39",
|
|
58
58
|
"@blocklet/ui-react": "^2.11.48",
|
|
59
59
|
"@blocklet/uploader": "^0.1.71",
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
"devDependencies": {
|
|
122
122
|
"@abtnode/types": "^1.16.39",
|
|
123
123
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
124
|
-
"@blocklet/payment-types": "1.18.
|
|
124
|
+
"@blocklet/payment-types": "1.18.13",
|
|
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": "b26aa859ba5961c7a8dedfc238a31476cf16036c"
|
|
171
171
|
}
|
package/src/app.tsx
CHANGED
|
@@ -13,7 +13,7 @@ import { Navigate, Route, BrowserRouter as Router, Routes } from 'react-router-d
|
|
|
13
13
|
import { joinURL } from 'ufo';
|
|
14
14
|
|
|
15
15
|
import ErrorFallback from './components/error-fallback';
|
|
16
|
-
import
|
|
16
|
+
import UserLayout from './components/layout/user';
|
|
17
17
|
import { TransitionProvider } from './components/progress-bar';
|
|
18
18
|
import { SessionProvider } from './contexts/session';
|
|
19
19
|
import { translations } from './locales';
|
|
@@ -68,45 +68,45 @@ function App() {
|
|
|
68
68
|
key="customer-home"
|
|
69
69
|
path="/customer"
|
|
70
70
|
element={
|
|
71
|
-
<
|
|
71
|
+
<UserLayout>
|
|
72
72
|
<CustomerHome />
|
|
73
|
-
</
|
|
73
|
+
</UserLayout>
|
|
74
74
|
}
|
|
75
75
|
/>
|
|
76
76
|
<Route
|
|
77
77
|
key="customer-subscription"
|
|
78
78
|
path="/customer/subscription/:id"
|
|
79
79
|
element={
|
|
80
|
-
<
|
|
80
|
+
<UserLayout>
|
|
81
81
|
<CustomerSubscriptionDetail />
|
|
82
|
-
</
|
|
82
|
+
</UserLayout>
|
|
83
83
|
}
|
|
84
84
|
/>
|
|
85
85
|
<Route
|
|
86
86
|
key="customer-subscription-change-plan"
|
|
87
87
|
path="/customer/subscription/:id/change-plan"
|
|
88
88
|
element={
|
|
89
|
-
<
|
|
89
|
+
<UserLayout>
|
|
90
90
|
<CustomerSubscriptionChangePlan />
|
|
91
|
-
</
|
|
91
|
+
</UserLayout>
|
|
92
92
|
}
|
|
93
93
|
/>
|
|
94
94
|
<Route
|
|
95
95
|
key="customer-subscription-change-payment"
|
|
96
96
|
path="/customer/subscription/:id/change-payment"
|
|
97
97
|
element={
|
|
98
|
-
<
|
|
98
|
+
<UserLayout>
|
|
99
99
|
<CustomerSubscriptionChangePayment />
|
|
100
|
-
</
|
|
100
|
+
</UserLayout>
|
|
101
101
|
}
|
|
102
102
|
/>
|
|
103
103
|
<Route
|
|
104
104
|
key="customer-recharge"
|
|
105
105
|
path="/customer/subscription/:id/recharge"
|
|
106
106
|
element={
|
|
107
|
-
<
|
|
107
|
+
<UserLayout>
|
|
108
108
|
<CustomerRecharge />
|
|
109
|
-
</
|
|
109
|
+
</UserLayout>
|
|
110
110
|
}
|
|
111
111
|
/>
|
|
112
112
|
<Route
|
|
@@ -125,27 +125,27 @@ function App() {
|
|
|
125
125
|
key="customer-due"
|
|
126
126
|
path="/customer/invoice/past-due"
|
|
127
127
|
element={
|
|
128
|
-
<
|
|
128
|
+
<UserLayout>
|
|
129
129
|
<CustomerInvoicePastDue />
|
|
130
|
-
</
|
|
130
|
+
</UserLayout>
|
|
131
131
|
}
|
|
132
132
|
/>
|
|
133
133
|
<Route
|
|
134
134
|
key="customer-invoice"
|
|
135
135
|
path="/customer/invoice/:id"
|
|
136
136
|
element={
|
|
137
|
-
<
|
|
137
|
+
<UserLayout>
|
|
138
138
|
<CustomerInvoiceDetail />
|
|
139
|
-
</
|
|
139
|
+
</UserLayout>
|
|
140
140
|
}
|
|
141
141
|
/>
|
|
142
142
|
<Route
|
|
143
143
|
key="customer-payout"
|
|
144
144
|
path="/customer/payout/:id"
|
|
145
145
|
element={
|
|
146
|
-
<
|
|
146
|
+
<UserLayout>
|
|
147
147
|
<CustomerPayoutDetail />
|
|
148
|
-
</
|
|
148
|
+
</UserLayout>
|
|
149
149
|
}
|
|
150
150
|
/>
|
|
151
151
|
<Route key="customer-fallback" path="/customer/*" element={<Navigate to="/customer" />} />,
|
|
@@ -16,7 +16,7 @@ type ActionItem = {
|
|
|
16
16
|
|
|
17
17
|
export type ActionsProps = {
|
|
18
18
|
actions: ActionItem[];
|
|
19
|
-
variant?: LiteralUnion<'compact' | 'normal', string>;
|
|
19
|
+
variant?: LiteralUnion<'compact' | 'normal' | 'outlined', string>;
|
|
20
20
|
sx?: any;
|
|
21
21
|
onOpenCallback?: Function;
|
|
22
22
|
};
|
|
@@ -58,17 +58,40 @@ export default function Actions(props: ActionsProps) {
|
|
|
58
58
|
}
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
const renderButton = () => {
|
|
62
|
+
if (props.variant === 'outlined') {
|
|
63
|
+
return (
|
|
64
|
+
<Button
|
|
65
|
+
aria-label="actions"
|
|
66
|
+
sx={{
|
|
67
|
+
minWidth: 0,
|
|
68
|
+
padding: '5px',
|
|
69
|
+
...props.sx,
|
|
70
|
+
}}
|
|
71
|
+
variant="outlined"
|
|
72
|
+
onClick={onOpen}
|
|
73
|
+
size="small">
|
|
74
|
+
<MoreHorizOutlined />
|
|
75
|
+
</Button>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (props.variant === 'compact') {
|
|
79
|
+
return (
|
|
64
80
|
<IconButton aria-label="actions" sx={props.sx} aria-haspopup="true" onClick={onOpen} size="small">
|
|
65
81
|
<MoreHorizOutlined />
|
|
66
82
|
</IconButton>
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return (
|
|
86
|
+
<Button sx={props.sx} onClick={onOpen} size="small" variant="contained" color="primary">
|
|
87
|
+
{t('common.actions')} <ExpandMoreOutlined fontSize="small" />
|
|
88
|
+
</Button>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
{renderButton()}
|
|
72
95
|
<Menu
|
|
73
96
|
anchorEl={anchorEl}
|
|
74
97
|
open={open}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CopyButton } from '@arcblock/ux/lib/ClickToCopy';
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
3
|
import { getWordBreakStyle } from '@blocklet/payment-react';
|
|
4
|
-
import {
|
|
4
|
+
import { Stack, Typography } from '@mui/material';
|
|
5
5
|
import type { ReactNode } from 'react';
|
|
6
6
|
|
|
7
7
|
interface CopyableProps {
|
|
@@ -35,7 +35,7 @@ export default function Copyable({ text, children, style }: CopyableProps) {
|
|
|
35
35
|
{text}
|
|
36
36
|
</Typography>
|
|
37
37
|
)}
|
|
38
|
-
<
|
|
38
|
+
<Typography sx={{ display: 'inline-flex', '> span': { display: 'inline-flex' } }}>{copyButton}</Typography>
|
|
39
39
|
</Stack>
|
|
40
40
|
)}
|
|
41
41
|
/>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* eslint-disable react-hooks/exhaustive-deps */
|
|
2
|
+
import { PaymentProvider } from '@blocklet/payment-react';
|
|
3
|
+
import { UserCenter } from '@blocklet/ui-react';
|
|
4
|
+
import { useEffect } from 'react';
|
|
5
|
+
|
|
6
|
+
import { useSessionContext } from '../../contexts/session';
|
|
7
|
+
|
|
8
|
+
export default function UserLayout(props: any) {
|
|
9
|
+
const { session, connectApi, events } = useSessionContext();
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
events.once('logout', () => {
|
|
13
|
+
window.location.href = `${window.location.origin}/.well-known/service/user`;
|
|
14
|
+
});
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (session.initialized && !session.user) {
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
session.login(() => {}, { openMode: 'redirect', redirect: window.location.href });
|
|
21
|
+
}
|
|
22
|
+
}, [session.initialized]);
|
|
23
|
+
|
|
24
|
+
if (session.user) {
|
|
25
|
+
return (
|
|
26
|
+
<PaymentProvider session={session} connect={connectApi}>
|
|
27
|
+
<UserCenter
|
|
28
|
+
currentTab={`${window.blocklet.prefix}customer`}
|
|
29
|
+
userDid={session.user.did}
|
|
30
|
+
notLoginContent="undefined">
|
|
31
|
+
{props.children}
|
|
32
|
+
</UserCenter>
|
|
33
|
+
</PaymentProvider>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|