payment-kit 1.13.149 → 1.13.151
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/integrations/stripe/resource.ts +2 -1
- package/api/src/libs/notification/template/subscription-refund-succeeded.ts +1 -1
- package/api/src/libs/subscription.ts +17 -9
- package/api/src/queues/invoice.ts +14 -0
- package/api/src/queues/refund.ts +1 -0
- package/api/src/routes/checkout-sessions.ts +1 -1
- package/api/src/routes/connect/shared.ts +7 -4
- package/api/src/routes/refunds.ts +14 -0
- package/api/src/routes/subscriptions.ts +2 -1
- package/api/src/store/migrations/20240225-refund-ext.ts +22 -0
- package/api/src/store/models/index.ts +1 -1
- package/api/src/store/models/refund.ts +29 -14
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/components/invoice/table.tsx +1 -1
- package/src/components/payment-intent/list.tsx +1 -1
- package/src/components/payment-link/after-pay.tsx +0 -19
- package/src/components/refund/list.tsx +6 -2
- package/src/components/section/header.tsx +3 -1
- package/src/components/subscription/items/usage-records.tsx +1 -1
- package/src/locales/en.tsx +1 -71
- package/src/locales/zh.tsx +1 -71
- package/src/pages/admin/billing/invoices/detail.tsx +7 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +7 -0
- package/src/pages/admin/index.tsx +4 -2
- package/src/pages/admin/payments/intents/detail.tsx +17 -13
- package/src/pages/admin/payments/links/create.tsx +1 -1
- package/src/pages/customer/invoice.tsx +32 -23
- package/src/pages/customer/refund/list.tsx +125 -0
|
@@ -3,6 +3,7 @@ import merge from 'lodash/merge';
|
|
|
3
3
|
|
|
4
4
|
import logger from '../../libs/logger';
|
|
5
5
|
import { getPriceUintAmountByCurrency } from '../../libs/session';
|
|
6
|
+
import { getSubscriptionItemPrice } from '../../libs/subscription';
|
|
6
7
|
import {
|
|
7
8
|
Customer,
|
|
8
9
|
PaymentCurrency,
|
|
@@ -244,7 +245,7 @@ export async function ensureStripeSubscription(
|
|
|
244
245
|
await Promise.all(
|
|
245
246
|
stripeSubscription.items.data.map(async (x: any) => {
|
|
246
247
|
const item = prices.find((y) => y.stripePrice.id === x.price.id);
|
|
247
|
-
const price = item
|
|
248
|
+
const price = getSubscriptionItemPrice(item); // local
|
|
248
249
|
let exist = await SubscriptionItem.findOne({
|
|
249
250
|
where: { price_id: price.id, subscription_id: internal.id },
|
|
250
251
|
});
|
|
@@ -76,7 +76,7 @@ export class SubscriptionRefundSucceededEmailTemplate
|
|
|
76
76
|
const productName = await getMainProductName(refund.subscription_id!);
|
|
77
77
|
const at: string = formatTime(refund.created_at);
|
|
78
78
|
|
|
79
|
-
const paymentInfo: string = `${fromUnitToToken(paymentIntent?.
|
|
79
|
+
const paymentInfo: string = `${fromUnitToToken(paymentIntent?.amount_received || '0', paymentCurrency.decimal)} ${
|
|
80
80
|
paymentCurrency.symbol
|
|
81
81
|
}`;
|
|
82
82
|
const refundInfo: string = `${fromUnitToToken(refund.amount, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
@@ -97,14 +97,14 @@ export function getSubscriptionCreateSetup(items: TLineItemExpanded[], currencyI
|
|
|
97
97
|
let setup = new BN(0);
|
|
98
98
|
|
|
99
99
|
items.forEach((x) => {
|
|
100
|
-
const price = x
|
|
100
|
+
const price = getSubscriptionItemPrice(x);
|
|
101
101
|
const unit = getPriceUintAmountByCurrency(price, currencyId);
|
|
102
102
|
if (price.type === 'one_time' || (price.type === 'recurring' && price.recurring?.usage_type === 'licensed')) {
|
|
103
103
|
setup = setup.add(new BN(unit).mul(new BN(x.quantity)));
|
|
104
104
|
}
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
-
const item = items.find((x) => x.
|
|
107
|
+
const item = items.find((x) => getSubscriptionItemPrice(x).type === 'recurring');
|
|
108
108
|
const recurring = (item?.upsell_price || item?.price)?.recurring as PriceRecurring;
|
|
109
109
|
const cycle = getRecurringPeriod(recurring);
|
|
110
110
|
const trial = trialInDays ? trialInDays * 24 * 60 * 60 * 1000 : 0;
|
|
@@ -148,7 +148,9 @@ export function getSubscriptionCycleAmount(items: TLineItemExpanded[], currencyI
|
|
|
148
148
|
let amount = new BN(0);
|
|
149
149
|
|
|
150
150
|
items.forEach((x) => {
|
|
151
|
-
amount = amount.add(
|
|
151
|
+
amount = amount.add(
|
|
152
|
+
new BN(getPriceUintAmountByCurrency(getSubscriptionItemPrice(x), currencyId)).mul(new BN(x.quantity))
|
|
153
|
+
);
|
|
152
154
|
});
|
|
153
155
|
|
|
154
156
|
return {
|
|
@@ -156,6 +158,10 @@ export function getSubscriptionCycleAmount(items: TLineItemExpanded[], currencyI
|
|
|
156
158
|
};
|
|
157
159
|
}
|
|
158
160
|
|
|
161
|
+
export function getSubscriptionItemPrice(item: TLineItemExpanded) {
|
|
162
|
+
return item.upsell_price || item.price;
|
|
163
|
+
}
|
|
164
|
+
|
|
159
165
|
export async function createProration(
|
|
160
166
|
subscription: Subscription,
|
|
161
167
|
setup: ReturnType<typeof getSubscriptionCreateSetup>,
|
|
@@ -175,9 +181,10 @@ export async function createProration(
|
|
|
175
181
|
// 1. get last invoice, and invoice items, filter invoice items that are in licensed recurring mode
|
|
176
182
|
const invoiceItems = await InvoiceItem.findAll({ where: { invoice_id: lastInvoice.id, proration: false } });
|
|
177
183
|
const invoiceItemsExpanded = await Price.expand(invoiceItems.map((x) => x.toJSON()));
|
|
178
|
-
const prorationItems = invoiceItemsExpanded.filter(
|
|
179
|
-
|
|
180
|
-
|
|
184
|
+
const prorationItems = invoiceItemsExpanded.filter((x) => {
|
|
185
|
+
const price = getSubscriptionItemPrice(x);
|
|
186
|
+
return price.type === 'recurring' && price.recurring?.usage_type === 'licensed';
|
|
187
|
+
});
|
|
181
188
|
|
|
182
189
|
// 2. calculate proration args based on the filtered invoice items
|
|
183
190
|
const precision = 10000;
|
|
@@ -191,7 +198,8 @@ export async function createProration(
|
|
|
191
198
|
let unused = new BN(0);
|
|
192
199
|
const prorations = await Promise.all(
|
|
193
200
|
prorationItems.map((x: TLineItemExpanded & { [key: string]: any }) => {
|
|
194
|
-
const
|
|
201
|
+
const price = getSubscriptionItemPrice(x);
|
|
202
|
+
const unitAmount = getPriceUintAmountByCurrency(price, subscription.currency_id);
|
|
195
203
|
const amount = new BN(unitAmount)
|
|
196
204
|
.mul(new BN(x.quantity))
|
|
197
205
|
.mul(new BN(prorationRate))
|
|
@@ -206,11 +214,11 @@ export async function createProration(
|
|
|
206
214
|
unused = unused.add(new BN(amount));
|
|
207
215
|
|
|
208
216
|
return {
|
|
209
|
-
price_id:
|
|
217
|
+
price_id: price.id,
|
|
210
218
|
amount: `-${amount}`,
|
|
211
219
|
quantity: x.quantity,
|
|
212
220
|
// @ts-ignore
|
|
213
|
-
description: `Unused time on ${
|
|
221
|
+
description: `Unused time on ${price.product.name} after ${dayjs().format('lll')}`,
|
|
214
222
|
period: {
|
|
215
223
|
start: lastInvoice.period_start,
|
|
216
224
|
end: lastInvoice.period_end,
|
|
@@ -78,6 +78,20 @@ export const handleInvoice = async (job: InvoiceJob) => {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
if (invoice.payment_intent_id) {
|
|
82
|
+
const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
|
|
83
|
+
if (
|
|
84
|
+
paymentIntent &&
|
|
85
|
+
['requires_action', 'requires_capture', 'requires_payment_method'].includes(paymentIntent.status)
|
|
86
|
+
) {
|
|
87
|
+
await paymentIntent.update({
|
|
88
|
+
amount_received: '0',
|
|
89
|
+
status: 'succeeded',
|
|
90
|
+
});
|
|
91
|
+
logger.info('invoice payment intent updated', paymentIntent.id);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
81
95
|
return;
|
|
82
96
|
}
|
|
83
97
|
|
package/api/src/queues/refund.ts
CHANGED
|
@@ -195,6 +195,7 @@ export const refundQueue = createQueue<RefundJob>({
|
|
|
195
195
|
|
|
196
196
|
export const startRefundQueue = async () => {
|
|
197
197
|
events.on('refund.created', (refund: Refund) => {
|
|
198
|
+
logger.info('schedule refund', { id: refund.id });
|
|
198
199
|
refundQueue.push({ id: refund.id, job: { refundId: refund.id } });
|
|
199
200
|
});
|
|
200
201
|
|
|
@@ -25,6 +25,7 @@ import { Product } from '../../store/models/product';
|
|
|
25
25
|
import { SetupIntent } from '../../store/models/setup-intent';
|
|
26
26
|
import { Subscription } from '../../store/models/subscription';
|
|
27
27
|
import { SubscriptionItem } from '../../store/models/subscription-item';
|
|
28
|
+
import { getSubscriptionItemPrice } from '../../libs/subscription';
|
|
28
29
|
|
|
29
30
|
type Result = {
|
|
30
31
|
checkoutSession: CheckoutSession;
|
|
@@ -446,9 +447,10 @@ export async function ensureInvoiceAndItems({
|
|
|
446
447
|
: [];
|
|
447
448
|
|
|
448
449
|
const getLineSetup = (x: TLineItemExpanded) => {
|
|
449
|
-
const price = x
|
|
450
|
+
const price = getSubscriptionItemPrice(x);
|
|
450
451
|
if (price.type === 'recurring' && trailing) {
|
|
451
452
|
return {
|
|
453
|
+
price,
|
|
452
454
|
amount: '0',
|
|
453
455
|
// @ts-ignore
|
|
454
456
|
description: trailing ? `${price.product.name} (trailing)` : price.product.name,
|
|
@@ -460,6 +462,7 @@ export async function ensureInvoiceAndItems({
|
|
|
460
462
|
}
|
|
461
463
|
|
|
462
464
|
return {
|
|
465
|
+
price,
|
|
463
466
|
amount: new BN(getPriceUintAmountByCurrency(price, props.currency_id)).mul(new BN(x.quantity)).toString(),
|
|
464
467
|
// @ts-ignore
|
|
465
468
|
description: price.product.name,
|
|
@@ -470,7 +473,7 @@ export async function ensureInvoiceAndItems({
|
|
|
470
473
|
const items = await Promise.all(
|
|
471
474
|
lineItems.map((x: TLineItemExpanded) => {
|
|
472
475
|
const setup = getLineSetup(x);
|
|
473
|
-
const price =
|
|
476
|
+
const { price } = setup;
|
|
474
477
|
let { quantity } = x;
|
|
475
478
|
if (price.type === 'recurring') {
|
|
476
479
|
if (price.recurring?.usage_type === 'metered' && !metered) {
|
|
@@ -489,10 +492,10 @@ export async function ensureInvoiceAndItems({
|
|
|
489
492
|
period: setup.period,
|
|
490
493
|
currency_id: props.currency_id,
|
|
491
494
|
customer_id: customer.id,
|
|
492
|
-
price_id:
|
|
495
|
+
price_id: price.id,
|
|
493
496
|
invoice_id: invoice.id,
|
|
494
497
|
subscription_id: subscription?.id,
|
|
495
|
-
subscription_item_id: subscriptionItems.find((si) => si.price_id ===
|
|
498
|
+
subscription_item_id: subscriptionItems.find((si) => si.price_id === price.id)?.id,
|
|
496
499
|
discountable: false,
|
|
497
500
|
discounts: [],
|
|
498
501
|
discount_amounts: [],
|
|
@@ -22,11 +22,15 @@ const paginationSchema = Joi.object<{
|
|
|
22
22
|
pageSize: number;
|
|
23
23
|
livemode?: boolean;
|
|
24
24
|
status?: string;
|
|
25
|
+
invoice_id: string;
|
|
26
|
+
subscription_id: string;
|
|
25
27
|
}>({
|
|
26
28
|
page: Joi.number().integer().min(1).default(1),
|
|
27
29
|
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
28
30
|
livemode: Joi.boolean().empty(''),
|
|
29
31
|
status: Joi.string().empty(''),
|
|
32
|
+
invoice_id: Joi.string().empty(''),
|
|
33
|
+
subscription_id: Joi.string().empty(''),
|
|
30
34
|
});
|
|
31
35
|
router.get('/', auth, async (req, res) => {
|
|
32
36
|
const { page, pageSize, livemode, status, ...query } = await paginationSchema.validateAsync(req.query, {
|
|
@@ -38,6 +42,14 @@ router.get('/', auth, async (req, res) => {
|
|
|
38
42
|
if (typeof livemode === 'boolean') {
|
|
39
43
|
where.livemode = livemode;
|
|
40
44
|
}
|
|
45
|
+
|
|
46
|
+
if (query.invoice_id) {
|
|
47
|
+
where.invoice_id = query.invoice_id;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (query.subscription_id) {
|
|
51
|
+
where.subscription_id = query.subscription_id;
|
|
52
|
+
}
|
|
41
53
|
if (status) {
|
|
42
54
|
where.status = status
|
|
43
55
|
.split(',')
|
|
@@ -60,6 +72,7 @@ router.get('/', auth, async (req, res) => {
|
|
|
60
72
|
include: [
|
|
61
73
|
{ model: Customer, as: 'customer' },
|
|
62
74
|
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
75
|
+
{ model: PaymentMethod, as: 'paymentMethod' },
|
|
63
76
|
// { model: PaymentIntent, as: 'paymentIntent' },
|
|
64
77
|
// { model: Invoice, as: 'invoice' },
|
|
65
78
|
// { model: Subscription, as: 'subscription' },
|
|
@@ -76,6 +89,7 @@ router.get('/:id', auth, async (req, res) => {
|
|
|
76
89
|
{ model: Customer, as: 'customer' },
|
|
77
90
|
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
78
91
|
{ model: PaymentIntent, as: 'paymentIntent' },
|
|
92
|
+
{ model: PaymentMethod, as: 'paymentMethod' },
|
|
79
93
|
{ model: Invoice, as: 'invoice' },
|
|
80
94
|
{ model: Subscription, as: 'subscription' },
|
|
81
95
|
],
|
|
@@ -312,6 +312,7 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
|
312
312
|
reason: 'requested_by_admin',
|
|
313
313
|
currency_id: subscription.currency_id,
|
|
314
314
|
customer_id: subscription.customer_id,
|
|
315
|
+
payment_method_id: subscription.default_payment_method_id,
|
|
315
316
|
payment_intent_id: result.lastInvoice.payment_intent_id as string,
|
|
316
317
|
invoice_id: result.lastInvoice.id,
|
|
317
318
|
subscription_id: subscription.id,
|
|
@@ -329,7 +330,7 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
|
|
|
329
330
|
unused_period_end: subscription.current_period_end,
|
|
330
331
|
},
|
|
331
332
|
});
|
|
332
|
-
logger.info('subscription cancel refund
|
|
333
|
+
logger.info('subscription cancel refund created', {
|
|
333
334
|
...req.params,
|
|
334
335
|
...req.body,
|
|
335
336
|
...pick(result, ['total', 'unused']),
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* eslint-disable no-await-in-loop */
|
|
2
|
+
import { DataTypes } from 'sequelize';
|
|
3
|
+
|
|
4
|
+
import { Migration, safeApplyColumnChanges } from '../migrate';
|
|
5
|
+
|
|
6
|
+
export const up: Migration = async ({ context }) => {
|
|
7
|
+
await safeApplyColumnChanges(context, {
|
|
8
|
+
refunds: [
|
|
9
|
+
{
|
|
10
|
+
name: 'payment_method_id',
|
|
11
|
+
field: {
|
|
12
|
+
type: DataTypes.STRING(30),
|
|
13
|
+
allowNull: true,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const down: Migration = async ({ context }) => {
|
|
21
|
+
await context.removeColumn('refunds', 'payment_method_id');
|
|
22
|
+
};
|
|
@@ -230,7 +230,7 @@ export type TPricingTableExpanded = TPricingTable & {
|
|
|
230
230
|
export type TRefundExpanded = TRefund & {
|
|
231
231
|
customer: TCustomer;
|
|
232
232
|
paymentCurrency: TPaymentCurrency;
|
|
233
|
-
paymentMethod
|
|
233
|
+
paymentMethod: TPaymentMethod;
|
|
234
234
|
paymentIntent: TPaymentIntent;
|
|
235
235
|
invoice?: TInvoice;
|
|
236
236
|
subscription?: TSubscription;
|
|
@@ -25,6 +25,7 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
|
|
|
25
25
|
declare currency_id: string;
|
|
26
26
|
declare customer_id: string;
|
|
27
27
|
declare payment_intent_id: string;
|
|
28
|
+
declare payment_method_id: string;
|
|
28
29
|
declare invoice_id?: string;
|
|
29
30
|
declare subscription_id?: string;
|
|
30
31
|
|
|
@@ -173,21 +174,30 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
|
|
|
173
174
|
};
|
|
174
175
|
|
|
175
176
|
public static initialize(sequelize: any) {
|
|
176
|
-
this.init(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
afterCreate: (model: Refund, options) =>
|
|
184
|
-
createEvent('Refund', 'refund.created', model, options).catch(console.error),
|
|
185
|
-
afterUpdate: (model: Refund, options) =>
|
|
186
|
-
createStatusEvent('Refund', 'refund', { canceled: 'canceled', succeeded: 'succeeded' }, model, options),
|
|
187
|
-
afterDestroy: (model: Refund, options) =>
|
|
188
|
-
createEvent('Refund', 'refund.deleted', model, options).catch(console.error),
|
|
177
|
+
this.init(
|
|
178
|
+
{
|
|
179
|
+
...Refund.GENESIS_ATTRIBUTES,
|
|
180
|
+
payment_method_id: {
|
|
181
|
+
type: DataTypes.STRING(30),
|
|
182
|
+
allowNull: true,
|
|
183
|
+
},
|
|
189
184
|
},
|
|
190
|
-
|
|
185
|
+
{
|
|
186
|
+
sequelize,
|
|
187
|
+
modelName: 'Refund',
|
|
188
|
+
tableName: 'refunds',
|
|
189
|
+
createdAt: 'created_at',
|
|
190
|
+
updatedAt: 'updated_at',
|
|
191
|
+
hooks: {
|
|
192
|
+
afterCreate: (model: Refund, options) =>
|
|
193
|
+
createEvent('Refund', 'refund.created', model, options).catch(console.error),
|
|
194
|
+
afterUpdate: (model: Refund, options) =>
|
|
195
|
+
createStatusEvent('Refund', 'refund', { canceled: 'canceled', succeeded: 'succeeded' }, model, options),
|
|
196
|
+
afterDestroy: (model: Refund, options) =>
|
|
197
|
+
createEvent('Refund', 'refund.deleted', model, options).catch(console.error),
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
);
|
|
191
201
|
}
|
|
192
202
|
|
|
193
203
|
public static associate(models: any) {
|
|
@@ -206,6 +216,11 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
|
|
|
206
216
|
foreignKey: 'id',
|
|
207
217
|
as: 'paymentIntent',
|
|
208
218
|
});
|
|
219
|
+
this.hasOne(models.PaymentMethod, {
|
|
220
|
+
sourceKey: 'payment_method_id',
|
|
221
|
+
foreignKey: 'id',
|
|
222
|
+
as: 'paymentMethod',
|
|
223
|
+
});
|
|
209
224
|
this.hasOne(models.Invoice, {
|
|
210
225
|
sourceKey: 'invoice_id',
|
|
211
226
|
foreignKey: 'id',
|
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.151",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "cross-env COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@arcblock/jwt": "^1.18.110",
|
|
51
51
|
"@arcblock/ux": "^2.9.29",
|
|
52
52
|
"@blocklet/logger": "1.16.23",
|
|
53
|
-
"@blocklet/payment-react": "1.13.
|
|
53
|
+
"@blocklet/payment-react": "1.13.151",
|
|
54
54
|
"@blocklet/sdk": "1.16.23",
|
|
55
55
|
"@blocklet/ui-react": "^2.9.29",
|
|
56
56
|
"@blocklet/uploader": "^0.0.73",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"devDependencies": {
|
|
111
111
|
"@abtnode/types": "1.16.23",
|
|
112
112
|
"@arcblock/eslint-config-ts": "^0.2.4",
|
|
113
|
-
"@blocklet/payment-types": "1.13.
|
|
113
|
+
"@blocklet/payment-types": "1.13.151",
|
|
114
114
|
"@types/cookie-parser": "^1.4.6",
|
|
115
115
|
"@types/cors": "^2.8.17",
|
|
116
116
|
"@types/dotenv-flow": "^3.3.3",
|
|
@@ -149,5 +149,5 @@
|
|
|
149
149
|
"parser": "typescript"
|
|
150
150
|
}
|
|
151
151
|
},
|
|
152
|
-
"gitHead": "
|
|
152
|
+
"gitHead": "1697697b65f9372e868fb10d5766edb3144f44f8"
|
|
153
153
|
}
|
|
@@ -62,7 +62,7 @@ export default function InvoiceTable({ invoice, simple }: Props) {
|
|
|
62
62
|
</TableCell>
|
|
63
63
|
)}
|
|
64
64
|
</TableRow>
|
|
65
|
-
{invoice.period_end && invoice.period_start && (
|
|
65
|
+
{invoice.period_end > 0 && invoice.period_start > 0 && (
|
|
66
66
|
<TableRow sx={{ borderBottom: '1px solid #eee' }}>
|
|
67
67
|
<TableCell align="left" colSpan={simple ? 4 : 5}>
|
|
68
68
|
<Typography component="span" variant="body1" color="text.secondary">
|
|
@@ -108,7 +108,7 @@ export default function PaymentList({ customer_id, invoice_id, features }: ListP
|
|
|
108
108
|
const item = data.list[index] as TPaymentIntentExpanded;
|
|
109
109
|
return (
|
|
110
110
|
<Typography component="strong" fontWeight={600}>
|
|
111
|
-
{fromUnitToToken(item?.
|
|
111
|
+
{fromUnitToToken(item?.amount_received, item?.paymentCurrency.decimal)}
|
|
112
112
|
{item?.paymentCurrency.symbol}
|
|
113
113
|
</Typography>
|
|
114
114
|
);
|
|
@@ -70,25 +70,6 @@ export default function AfterPay() {
|
|
|
70
70
|
)}
|
|
71
71
|
/>
|
|
72
72
|
)}
|
|
73
|
-
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
|
74
|
-
{t('admin.invoices')}
|
|
75
|
-
</Typography>
|
|
76
|
-
<Controller
|
|
77
|
-
name="invoice_creation.enabled"
|
|
78
|
-
control={control}
|
|
79
|
-
render={({ field }) => (
|
|
80
|
-
<FormControlLabel
|
|
81
|
-
control={
|
|
82
|
-
<Checkbox
|
|
83
|
-
checked={getValues().invoice_creation.enabled}
|
|
84
|
-
{...field}
|
|
85
|
-
onChange={(_, checked) => setValue(field.name, checked)}
|
|
86
|
-
/>
|
|
87
|
-
}
|
|
88
|
-
label={t('admin.paymentLink.createInvoice')}
|
|
89
|
-
/>
|
|
90
|
-
)}
|
|
91
|
-
/>
|
|
92
73
|
<Controller
|
|
93
74
|
name="nft_mint_settings.enabled"
|
|
94
75
|
control={control}
|
|
@@ -27,6 +27,7 @@ type SearchProps = {
|
|
|
27
27
|
page: number;
|
|
28
28
|
customer_id?: string;
|
|
29
29
|
invoice_id?: string;
|
|
30
|
+
subscription_id?: string;
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
type ListProps = {
|
|
@@ -37,6 +38,7 @@ type ListProps = {
|
|
|
37
38
|
};
|
|
38
39
|
customer_id?: string;
|
|
39
40
|
invoice_id?: string;
|
|
41
|
+
subscription_id?: string;
|
|
40
42
|
};
|
|
41
43
|
|
|
42
44
|
const getListKey = (props: ListProps) => {
|
|
@@ -50,16 +52,17 @@ const getListKey = (props: ListProps) => {
|
|
|
50
52
|
return 'refunds';
|
|
51
53
|
};
|
|
52
54
|
|
|
53
|
-
|
|
55
|
+
RefundList.defaultProps = {
|
|
54
56
|
features: {
|
|
55
57
|
customer: true,
|
|
56
58
|
filter: true,
|
|
57
59
|
},
|
|
58
60
|
customer_id: '',
|
|
59
61
|
invoice_id: '',
|
|
62
|
+
subscription_id: '',
|
|
60
63
|
};
|
|
61
64
|
|
|
62
|
-
export default function
|
|
65
|
+
export default function RefundList({ customer_id, invoice_id, subscription_id, features }: ListProps) {
|
|
63
66
|
const { t } = useLocaleContext();
|
|
64
67
|
const navigate = useNavigate();
|
|
65
68
|
|
|
@@ -70,6 +73,7 @@ export default function PaymentList({ customer_id, invoice_id, features }: ListP
|
|
|
70
73
|
status: '',
|
|
71
74
|
customer_id,
|
|
72
75
|
invoice_id,
|
|
76
|
+
subscription_id,
|
|
73
77
|
pageSize: persisted.rowsPerPage || 20,
|
|
74
78
|
page: persisted.page ? persisted.page + 1 : 1,
|
|
75
79
|
});
|
|
@@ -5,6 +5,7 @@ type Props = {
|
|
|
5
5
|
title: string;
|
|
6
6
|
children?: ReactNode;
|
|
7
7
|
mb?: number;
|
|
8
|
+
mt?: number;
|
|
8
9
|
};
|
|
9
10
|
|
|
10
11
|
export default function SectionHeader(props: Props) {
|
|
@@ -16,7 +17,7 @@ export default function SectionHeader(props: Props) {
|
|
|
16
17
|
alignItems="center"
|
|
17
18
|
flexWrap="wrap"
|
|
18
19
|
gap={1}
|
|
19
|
-
sx={{ mb: props.mb, pb: 1, width: 1, borderBottom: '1px solid #eee' }}>
|
|
20
|
+
sx={{ mb: props.mb, mt: props.mt, pb: 1, width: 1, borderBottom: '1px solid #eee' }}>
|
|
20
21
|
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
|
21
22
|
{props.title}
|
|
22
23
|
</Typography>
|
|
@@ -28,4 +29,5 @@ export default function SectionHeader(props: Props) {
|
|
|
28
29
|
SectionHeader.defaultProps = {
|
|
29
30
|
children: null,
|
|
30
31
|
mb: 1,
|
|
32
|
+
mt: 1,
|
|
31
33
|
};
|
|
@@ -48,7 +48,7 @@ export function UsageRecordDialog(props: { subscriptionId: string; id: string; o
|
|
|
48
48
|
options: {
|
|
49
49
|
customBodyRenderLite: (_: string, index: number) => {
|
|
50
50
|
const item = data.list[index] as TUsageRecord;
|
|
51
|
-
return formatToDatetime(item.timestamp);
|
|
51
|
+
return formatToDatetime(item.timestamp * 1000);
|
|
52
52
|
},
|
|
53
53
|
},
|
|
54
54
|
},
|
package/src/locales/en.tsx
CHANGED
|
@@ -2,78 +2,7 @@ import flat from 'flat';
|
|
|
2
2
|
|
|
3
3
|
export default flat({
|
|
4
4
|
common: {
|
|
5
|
-
id: 'ID',
|
|
6
|
-
url: 'URL',
|
|
7
|
-
createdAt: 'Created At',
|
|
8
|
-
updatedAt: 'Updated At',
|
|
9
|
-
resumesAt: 'Resume At',
|
|
10
|
-
actions: 'Actions',
|
|
11
|
-
options: 'Options',
|
|
12
|
-
advanced: 'Advanced options',
|
|
13
|
-
login: 'Login to access this page',
|
|
14
|
-
settings: 'Settings',
|
|
15
|
-
preview: 'Preview',
|
|
16
|
-
required: 'Required',
|
|
17
|
-
setup: 'Setup',
|
|
18
|
-
name: 'Name',
|
|
19
|
-
amount: 'Amount',
|
|
20
|
-
total: 'Total',
|
|
21
|
-
subtotal: 'Subtotal',
|
|
22
|
-
status: 'Status',
|
|
23
|
-
livemode: 'Test mode',
|
|
24
|
-
afterTime: 'After {time}',
|
|
25
|
-
timeAgo: '{time} ago',
|
|
26
|
-
save: 'Save',
|
|
27
|
-
saved: 'Changes saved',
|
|
28
|
-
remove: 'Remove',
|
|
29
|
-
removed: 'Resource removed',
|
|
30
|
-
confirm: 'Confirm',
|
|
31
|
-
cancel: 'Cancel',
|
|
32
|
-
every: 'every',
|
|
33
|
-
per: 'per {interval}',
|
|
34
|
-
slash: '/ {interval}',
|
|
35
|
-
unit: 'units',
|
|
36
|
-
edit: 'Edit',
|
|
37
|
-
quantity: 'Quantity',
|
|
38
|
-
yes: 'Yes',
|
|
39
|
-
no: 'No',
|
|
40
|
-
email: 'Email',
|
|
41
|
-
did: 'DID',
|
|
42
|
-
txHash: 'Transaction',
|
|
43
|
-
customer: 'Customer',
|
|
44
|
-
custom: 'Custom',
|
|
45
|
-
description: 'Description',
|
|
46
|
-
statementDescriptor: 'Statement descriptor',
|
|
47
|
-
loadMore: 'View more {resource}',
|
|
48
|
-
loadingMore: 'Loading more {resource}...',
|
|
49
|
-
noMore: 'No more {resource}',
|
|
50
|
-
copied: 'Copied',
|
|
51
|
-
previous: 'Back',
|
|
52
|
-
continue: 'Continue',
|
|
53
|
-
qty: 'Qty {count}',
|
|
54
|
-
each: '{unit} each',
|
|
55
|
-
trial: 'Free for {count} days',
|
|
56
|
-
billed: 'billed {rule}',
|
|
57
|
-
metered: 'based on usage',
|
|
58
|
-
hour: 'hour',
|
|
59
|
-
day: 'day',
|
|
60
|
-
week: 'week',
|
|
61
|
-
month: 'month',
|
|
62
|
-
year: 'year',
|
|
63
|
-
hourly: 'hourly',
|
|
64
|
-
daily: 'daily',
|
|
65
|
-
weekly: 'weekly',
|
|
66
|
-
monthly: 'monthly',
|
|
67
|
-
yearly: 'yearly',
|
|
68
|
-
month3: 'every 3 months',
|
|
69
|
-
month6: 'every 6 months',
|
|
70
|
-
recurring: 'every {count} {interval}',
|
|
71
5
|
redirecting: 'Redirecting...',
|
|
72
|
-
hours: 'hours',
|
|
73
|
-
days: 'days',
|
|
74
|
-
weeks: 'weeks',
|
|
75
|
-
months: 'months',
|
|
76
|
-
years: 'years',
|
|
77
6
|
metadata: {
|
|
78
7
|
label: 'Metadata',
|
|
79
8
|
add: 'Add more metadata',
|
|
@@ -306,6 +235,7 @@ export default flat({
|
|
|
306
235
|
view: 'View payment detail',
|
|
307
236
|
empty: 'No payment intent',
|
|
308
237
|
refund: 'Refund payment',
|
|
238
|
+
received: 'Received',
|
|
309
239
|
},
|
|
310
240
|
paymentMethod: {
|
|
311
241
|
_name: 'Payment Method',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -2,78 +2,7 @@ import flat from 'flat';
|
|
|
2
2
|
|
|
3
3
|
export default flat({
|
|
4
4
|
common: {
|
|
5
|
-
id: 'ID',
|
|
6
|
-
url: 'URL',
|
|
7
|
-
createdAt: '创建时间',
|
|
8
|
-
updatedAt: '更新时间',
|
|
9
|
-
resumesAt: '恢复时间',
|
|
10
|
-
actions: '操作',
|
|
11
|
-
options: '选项',
|
|
12
|
-
advanced: '高级选项',
|
|
13
|
-
settings: '设置',
|
|
14
|
-
preview: '预览',
|
|
15
|
-
required: '必填',
|
|
16
|
-
setup: '设置',
|
|
17
|
-
name: '姓名',
|
|
18
|
-
login: '登录以访问此页面',
|
|
19
|
-
amount: '金额',
|
|
20
|
-
total: '总计',
|
|
21
|
-
subtotal: '小计',
|
|
22
|
-
status: '状态',
|
|
23
|
-
livemode: '测试模式',
|
|
24
|
-
afterTime: '在{time}后',
|
|
25
|
-
timeAgo: '{time}前',
|
|
26
|
-
save: '保存',
|
|
27
|
-
saved: '更改已保存',
|
|
28
|
-
remove: '删除',
|
|
29
|
-
removed: '资源已删除',
|
|
30
|
-
confirm: '确认',
|
|
31
|
-
cancel: '取消',
|
|
32
|
-
every: '每',
|
|
33
|
-
per: '每{interval}',
|
|
34
|
-
slash: '每{interval}',
|
|
35
|
-
unit: '件',
|
|
36
|
-
edit: '编辑',
|
|
37
|
-
quantity: '数量',
|
|
38
|
-
yes: '是',
|
|
39
|
-
no: '否',
|
|
40
|
-
email: '邮箱',
|
|
41
|
-
did: 'DID',
|
|
42
|
-
txHash: '交易哈希',
|
|
43
|
-
customer: '客户',
|
|
44
|
-
custom: '自定义',
|
|
45
|
-
description: '描述',
|
|
46
|
-
statementDescriptor: '声明描述',
|
|
47
|
-
loadMore: '查看更多{resource}',
|
|
48
|
-
loadingMore: '正在加载更多{resource}...',
|
|
49
|
-
noMore: '没有更多{resource}',
|
|
50
|
-
copied: '已复制',
|
|
51
|
-
previous: '返回',
|
|
52
|
-
continue: '继续',
|
|
53
|
-
qty: '{count} 件',
|
|
54
|
-
each: '每件 {unit}',
|
|
55
|
-
trial: '免费试用 {count} 天',
|
|
56
|
-
billed: '{rule}收费',
|
|
57
|
-
metered: '按用量',
|
|
58
|
-
hour: '小时',
|
|
59
|
-
day: '天',
|
|
60
|
-
week: '周',
|
|
61
|
-
month: '月',
|
|
62
|
-
year: '年',
|
|
63
|
-
hourly: '按小时',
|
|
64
|
-
daily: '按天',
|
|
65
|
-
weekly: '按周',
|
|
66
|
-
monthly: '按月',
|
|
67
|
-
yearly: '按年',
|
|
68
|
-
month3: '按季度',
|
|
69
|
-
month6: '按半年',
|
|
70
|
-
recurring: '每{count}{interval}',
|
|
71
5
|
redirecting: '跳转中...',
|
|
72
|
-
hours: '小时',
|
|
73
|
-
days: '天',
|
|
74
|
-
weeks: '周',
|
|
75
|
-
months: '月',
|
|
76
|
-
years: '年',
|
|
77
6
|
metadata: {
|
|
78
7
|
label: '元数据',
|
|
79
8
|
add: '添加更多元数据',
|
|
@@ -298,6 +227,7 @@ export default flat({
|
|
|
298
227
|
view: '查看支付详情',
|
|
299
228
|
empty: '没有支付意向',
|
|
300
229
|
refund: '退款支付',
|
|
230
|
+
received: '实收金额',
|
|
301
231
|
},
|
|
302
232
|
paymentMethod: {
|
|
303
233
|
_name: '支付方式',
|
|
@@ -20,6 +20,7 @@ import InvoiceActions from '../../../../components/invoice/action';
|
|
|
20
20
|
import InvoiceTable from '../../../../components/invoice/table';
|
|
21
21
|
import MetadataEditor from '../../../../components/metadata/editor';
|
|
22
22
|
import PaymentList from '../../../../components/payment-intent/list';
|
|
23
|
+
import RefundList from '../../../../components/refund/list';
|
|
23
24
|
import SectionHeader from '../../../../components/section/header';
|
|
24
25
|
|
|
25
26
|
const fetchData = (id: string): Promise<TInvoiceExpanded> => {
|
|
@@ -145,6 +146,12 @@ export default function InvoiceDetail(props: { id: string }) {
|
|
|
145
146
|
<PaymentList features={{ customer: false, toolbar: false }} invoice_id={data.id} />
|
|
146
147
|
</Box>
|
|
147
148
|
</Box>
|
|
149
|
+
<Box className="section">
|
|
150
|
+
<SectionHeader title={t('admin.refunds')} mb={0} />
|
|
151
|
+
<Box className="section-body">
|
|
152
|
+
<RefundList features={{ customer: false, toolbar: false }} invoice_id={data.id} />
|
|
153
|
+
</Box>
|
|
154
|
+
</Box>
|
|
148
155
|
<Box className="section">
|
|
149
156
|
<SectionHeader title={t('admin.connections')} />
|
|
150
157
|
<Stack>
|
|
@@ -16,6 +16,7 @@ import EventList from '../../../../components/event/list';
|
|
|
16
16
|
import InfoRow from '../../../../components/info-row';
|
|
17
17
|
import InvoiceList from '../../../../components/invoice/list';
|
|
18
18
|
import MetadataEditor from '../../../../components/metadata/editor';
|
|
19
|
+
import RefundList from '../../../../components/refund/list';
|
|
19
20
|
import SectionHeader from '../../../../components/section/header';
|
|
20
21
|
import SubscriptionActions from '../../../../components/subscription/actions';
|
|
21
22
|
import SubscriptionItemList from '../../../../components/subscription/items';
|
|
@@ -205,6 +206,12 @@ export default function SubscriptionDetail(props: { id: string }) {
|
|
|
205
206
|
<InvoiceList features={{ customer: true, toolbar: false }} subscription_id={data.id} />
|
|
206
207
|
</Box>
|
|
207
208
|
</Box>
|
|
209
|
+
<Box className="section">
|
|
210
|
+
<SectionHeader title={t('admin.refunds')} mb={0} />
|
|
211
|
+
<Box className="section-body">
|
|
212
|
+
<RefundList features={{ customer: true, toolbar: false }} subscription_id={data.id} />
|
|
213
|
+
</Box>
|
|
214
|
+
</Box>
|
|
208
215
|
<Box className="section">
|
|
209
216
|
<SectionHeader title={t('admin.events')} />
|
|
210
217
|
<Box className="section-body">
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import { PaymentProvider, Switch, usePaymentContext } from '@blocklet/payment-react';
|
|
3
3
|
import { Box, Chip, Stack } from '@mui/material';
|
|
4
|
-
import React, { isValidElement, startTransition } from 'react';
|
|
4
|
+
import React, { Suspense, isValidElement, startTransition } from 'react';
|
|
5
5
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
6
6
|
|
|
7
7
|
import Layout from '../../components/layout/admin';
|
|
@@ -99,7 +99,9 @@ function Admin() {
|
|
|
99
99
|
</label>
|
|
100
100
|
</Stack>
|
|
101
101
|
</Stack>
|
|
102
|
-
<
|
|
102
|
+
<Suspense fallback={<div />}>
|
|
103
|
+
<div className="page-content">{isValidElement(TabComponent) ? TabComponent : <TabComponent />}</div>
|
|
104
|
+
</Suspense>
|
|
103
105
|
</Layout>
|
|
104
106
|
);
|
|
105
107
|
}
|
|
@@ -77,7 +77,8 @@ export default function PaymentIntentDetail(props: { id: string }) {
|
|
|
77
77
|
const onUpdateMetadata = createUpdater('metadata');
|
|
78
78
|
|
|
79
79
|
const currency = data.paymentCurrency;
|
|
80
|
-
const
|
|
80
|
+
const received = [fromUnitToToken(data?.amount_received, currency.decimal), currency.symbol].join(' ');
|
|
81
|
+
const total = [fromUnitToToken(data?.amount, currency.decimal), currency.symbol].join(' ');
|
|
81
82
|
|
|
82
83
|
return (
|
|
83
84
|
<Root direction="column" spacing={4} mb={4}>
|
|
@@ -96,7 +97,7 @@ export default function PaymentIntentDetail(props: { id: string }) {
|
|
|
96
97
|
<Box mt={2}>
|
|
97
98
|
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
|
98
99
|
<Stack direction="row" alignItems="center">
|
|
99
|
-
<Amount amount={
|
|
100
|
+
<Amount amount={received} sx={{ my: 0, fontSize: '2rem', lineHeight: '1rem' }} />
|
|
100
101
|
<Status label={data.status} color={getPaymentIntentStatusColor(data.status)} sx={{ ml: 2 }} />
|
|
101
102
|
</Stack>
|
|
102
103
|
<PaymentIntentActions data={data} variant="normal" />
|
|
@@ -117,7 +118,8 @@ export default function PaymentIntentDetail(props: { id: string }) {
|
|
|
117
118
|
<Box className="section">
|
|
118
119
|
<SectionHeader title={t('admin.details')} />
|
|
119
120
|
<Stack>
|
|
120
|
-
<InfoRow label={t('common.amount')} value={
|
|
121
|
+
<InfoRow label={t('common.amount')} value={total} />
|
|
122
|
+
<InfoRow label={t('admin.paymentIntent.received')} value={received} />
|
|
121
123
|
<InfoRow
|
|
122
124
|
label={t('common.status')}
|
|
123
125
|
value={
|
|
@@ -191,16 +193,18 @@ export default function PaymentIntentDetail(props: { id: string }) {
|
|
|
191
193
|
<Box className="section">
|
|
192
194
|
<SectionHeader title={t('admin.connections')} />
|
|
193
195
|
<Stack>
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
196
|
+
{data.subscription && (
|
|
197
|
+
<InfoRow
|
|
198
|
+
label={t('admin.subscription.name')}
|
|
199
|
+
value={<Link to={`/admin/billing/${data.subscription.id}`}>{data.subscription.id}</Link>}
|
|
200
|
+
/>
|
|
201
|
+
)}
|
|
202
|
+
{data.invoice_id && (
|
|
203
|
+
<InfoRow
|
|
204
|
+
label={t('admin.invoice.name')}
|
|
205
|
+
value={<Link to={`/admin/billing/${data.invoice_id}`}>{data.invoice_id}</Link>}
|
|
206
|
+
/>
|
|
207
|
+
)}
|
|
204
208
|
</Stack>
|
|
205
209
|
</Box>
|
|
206
210
|
<Box className="section">
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
3
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
PaymentProvider,
|
|
6
|
+
Status,
|
|
7
|
+
TxLink,
|
|
8
|
+
api,
|
|
9
|
+
formatError,
|
|
10
|
+
formatTime,
|
|
11
|
+
getInvoiceStatusColor,
|
|
12
|
+
} from '@blocklet/payment-react';
|
|
5
13
|
import type { TInvoiceExpanded } from '@blocklet/payment-types';
|
|
6
14
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
7
|
-
import { Alert, Box, Button, CircularProgress,
|
|
15
|
+
import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
|
|
8
16
|
import { useRequest, useSetState } from 'ahooks';
|
|
9
17
|
import { useEffect } from 'react';
|
|
10
18
|
import { Link, useParams, useSearchParams } from 'react-router-dom';
|
|
@@ -14,6 +22,7 @@ import InfoRow from '../../components/info-row';
|
|
|
14
22
|
import InvoiceTable from '../../components/invoice/table';
|
|
15
23
|
import SectionHeader from '../../components/section/header';
|
|
16
24
|
import { useSessionContext } from '../../contexts/session';
|
|
25
|
+
import CustomerRefundList from './refund/list';
|
|
17
26
|
|
|
18
27
|
const fetchData = (id: string): Promise<TInvoiceExpanded> => {
|
|
19
28
|
return api.get(`/api/invoices/${id}`).then((res) => res.data);
|
|
@@ -23,7 +32,7 @@ const fetchData = (id: string): Promise<TInvoiceExpanded> => {
|
|
|
23
32
|
export default function CustomerHome() {
|
|
24
33
|
const { t } = useLocaleContext();
|
|
25
34
|
const [searchParams] = useSearchParams();
|
|
26
|
-
const { connectApi } = useSessionContext();
|
|
35
|
+
const { session, connectApi } = useSessionContext();
|
|
27
36
|
const params = useParams<{ id: string }>();
|
|
28
37
|
const [state, setState] = useSetState({
|
|
29
38
|
downloading: false,
|
|
@@ -89,21 +98,19 @@ export default function CustomerHome() {
|
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
return (
|
|
92
|
-
<
|
|
93
|
-
<
|
|
101
|
+
<Box sx={{ maxWidth: '1200px' }}>
|
|
102
|
+
<PaymentProvider session={session} connect={connectApi}>
|
|
94
103
|
<Link to="/customer">
|
|
95
|
-
<Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal' }}>
|
|
104
|
+
<Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal', padding: '10px 0' }}>
|
|
96
105
|
<ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
|
|
97
106
|
<Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
|
|
98
107
|
{t('common.previous')}
|
|
99
108
|
</Typography>
|
|
100
109
|
</Stack>
|
|
101
110
|
</Link>
|
|
102
|
-
</Grid>
|
|
103
|
-
<Grid item xs={12} md={5}>
|
|
104
111
|
<Box>
|
|
105
|
-
<SectionHeader title={t('payment.customer.invoice.summary')} mb={0} />
|
|
106
|
-
<Stack sx={{ mt: 1 }}>
|
|
112
|
+
<SectionHeader title={t('payment.customer.invoice.summary')} mb={0} mt={1} />
|
|
113
|
+
<Stack sx={{ mt: 1, display: 'grid', gridTemplateColumns: '50% 50%' }}>
|
|
107
114
|
<InfoRow label={t('admin.invoice.number')} value={data.number} />
|
|
108
115
|
<InfoRow label={t('admin.invoice.from')} value={data.statement_descriptor || blocklet.appName} />
|
|
109
116
|
<InfoRow label={t('admin.invoice.customer')} value={data.customer.name} />
|
|
@@ -111,14 +118,12 @@ export default function CustomerHome() {
|
|
|
111
118
|
label={t('common.status')}
|
|
112
119
|
value={<Status label={data.status} color={getInvoiceStatusColor(data.status)} />}
|
|
113
120
|
/>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
data.period_start
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
/>
|
|
121
|
+
{data.period_start > 0 && data.period_end > 0 && (
|
|
122
|
+
<InfoRow
|
|
123
|
+
label={t('admin.subscription.currentPeriod')}
|
|
124
|
+
value={[formatTime(data.period_start * 1000), formatTime(data.period_end * 1000)].join(' ~ ')}
|
|
125
|
+
/>
|
|
126
|
+
)}
|
|
122
127
|
{data.status_transitions?.paid_at && (
|
|
123
128
|
<InfoRow label={t('admin.invoice.paidAt')} value={formatTime(data.status_transitions.paid_at * 1000)} />
|
|
124
129
|
)}
|
|
@@ -138,9 +143,7 @@ export default function CustomerHome() {
|
|
|
138
143
|
/>
|
|
139
144
|
</Stack>
|
|
140
145
|
</Box>
|
|
141
|
-
|
|
142
|
-
<Grid item xs={12} md={7}>
|
|
143
|
-
<SectionHeader title={t('payment.customer.invoice.details')} mb={0}>
|
|
146
|
+
<SectionHeader title={t('payment.customer.invoice.details')} mb={0} mt={1}>
|
|
144
147
|
{['open', 'paid', 'uncollectible'].includes(data.status) && (
|
|
145
148
|
<Button
|
|
146
149
|
variant="contained"
|
|
@@ -160,7 +163,13 @@ export default function CustomerHome() {
|
|
|
160
163
|
</Button>
|
|
161
164
|
)}
|
|
162
165
|
</Stack>
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
<Box className="section">
|
|
167
|
+
<SectionHeader title={t('admin.refunds')} mb={0} mt={0} />
|
|
168
|
+
<Box className="section-body">
|
|
169
|
+
<CustomerRefundList invoice_id={data.id} />
|
|
170
|
+
</Box>
|
|
171
|
+
</Box>
|
|
172
|
+
</PaymentProvider>
|
|
173
|
+
</Box>
|
|
165
174
|
);
|
|
166
175
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/* eslint-disable react/no-unstable-nested-components */
|
|
2
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
+
import { Status, TxLink, api, formatToDate, getPaymentIntentStatusColor } from '@blocklet/payment-react';
|
|
4
|
+
import type { Paginated, PaymentDetails, TPaymentIntentExpanded } from '@blocklet/payment-types';
|
|
5
|
+
import { Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
|
|
6
|
+
import { fromUnitToToken } from '@ocap/util';
|
|
7
|
+
import { useInfiniteScroll } from 'ahooks';
|
|
8
|
+
|
|
9
|
+
const groupByDate = (items: TPaymentIntentExpanded[]) => {
|
|
10
|
+
const grouped: { [key: string]: TPaymentIntentExpanded[] } = {};
|
|
11
|
+
items.forEach((item) => {
|
|
12
|
+
const date = new Date(item.created_at).toLocaleDateString();
|
|
13
|
+
if (!grouped[date]) {
|
|
14
|
+
grouped[date] = [];
|
|
15
|
+
}
|
|
16
|
+
grouped[date]?.push(item);
|
|
17
|
+
});
|
|
18
|
+
return grouped;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const fetchData = (params: Record<string, any> = {}): Promise<Paginated<TPaymentIntentExpanded>> => {
|
|
22
|
+
const search = new URLSearchParams();
|
|
23
|
+
Object.keys(params).forEach((key) => {
|
|
24
|
+
search.set(key, String(params[key]));
|
|
25
|
+
});
|
|
26
|
+
return api.get(`/api/refunds?${search.toString()}`).then((res) => res.data);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type Props = {
|
|
30
|
+
invoice_id: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const pageSize = 10;
|
|
34
|
+
|
|
35
|
+
export default function CustomerRefundList({ invoice_id }: Props) {
|
|
36
|
+
const { t } = useLocaleContext();
|
|
37
|
+
|
|
38
|
+
const { data, loadMore, loadingMore, loading } = useInfiniteScroll<Paginated<TPaymentIntentExpanded>>(
|
|
39
|
+
(d) => {
|
|
40
|
+
const page = d ? Math.ceil(d.list.length / pageSize) + 1 : 1;
|
|
41
|
+
return fetchData({ page, pageSize, invoice_id });
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
reloadDeps: [invoice_id],
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (loading || !data) {
|
|
49
|
+
return <CircularProgress />;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (data && data.list.length === 0) {
|
|
53
|
+
return <Typography color="text.secondary">{t('payment.customer.payment.empty')}</Typography>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const hasMore = data && data.list.length < data.count;
|
|
57
|
+
|
|
58
|
+
const grouped = groupByDate(data.list as any);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Stack direction="column" gap={1} sx={{ mt: 1 }}>
|
|
62
|
+
{Object.entries(grouped).map(([date, payments]) => (
|
|
63
|
+
<Box key={date}>
|
|
64
|
+
<Typography sx={{ fontWeight: 'bold', color: 'text.secondary', mt: 2, mb: 1 }}>{date}</Typography>
|
|
65
|
+
{payments.map((item: any) => {
|
|
66
|
+
return (
|
|
67
|
+
<Stack
|
|
68
|
+
key={item.id}
|
|
69
|
+
direction={{
|
|
70
|
+
xs: 'column',
|
|
71
|
+
sm: 'row',
|
|
72
|
+
}}
|
|
73
|
+
sx={{ my: 1 }}
|
|
74
|
+
gap={{
|
|
75
|
+
xs: 0.5,
|
|
76
|
+
sm: 1.5,
|
|
77
|
+
md: 3,
|
|
78
|
+
}}
|
|
79
|
+
flexWrap="nowrap">
|
|
80
|
+
<Box flex={3}>
|
|
81
|
+
<Typography>{formatToDate(item.created_at)}</Typography>
|
|
82
|
+
</Box>
|
|
83
|
+
<Box flex={2}>
|
|
84
|
+
<Typography textAlign="right">
|
|
85
|
+
{fromUnitToToken(item.amount, item.paymentCurrency.decimal)}
|
|
86
|
+
{item.paymentCurrency.symbol}
|
|
87
|
+
</Typography>
|
|
88
|
+
</Box>
|
|
89
|
+
<Box flex={3}>
|
|
90
|
+
<Status label={item.status} color={getPaymentIntentStatusColor(item.status)} />
|
|
91
|
+
</Box>
|
|
92
|
+
<Box flex={3}>
|
|
93
|
+
<Typography>{item.description || '-'}</Typography>
|
|
94
|
+
</Box>
|
|
95
|
+
<Box flex={3} sx={{ minWidth: '220px' }}>
|
|
96
|
+
{item.payment_details?.arcblock?.tx_hash && (
|
|
97
|
+
<TxLink
|
|
98
|
+
details={item.payment_details as PaymentDetails}
|
|
99
|
+
method={item.paymentMethod}
|
|
100
|
+
mode="customer"
|
|
101
|
+
/>
|
|
102
|
+
)}
|
|
103
|
+
</Box>
|
|
104
|
+
</Stack>
|
|
105
|
+
);
|
|
106
|
+
})}
|
|
107
|
+
</Box>
|
|
108
|
+
))}
|
|
109
|
+
<Box>
|
|
110
|
+
{hasMore && (
|
|
111
|
+
<Button variant="text" type="button" color="inherit" onClick={loadMore} disabled={loadingMore}>
|
|
112
|
+
{loadingMore
|
|
113
|
+
? t('common.loadingMore', { resource: t('payment.customer.payments') })
|
|
114
|
+
: t('common.loadMore', { resource: t('payment.customer.payments') })}
|
|
115
|
+
</Button>
|
|
116
|
+
)}
|
|
117
|
+
{!hasMore && data.count > pageSize && (
|
|
118
|
+
<Typography color="text.secondary">
|
|
119
|
+
{t('common.noMore', { resource: t('payment.customer.payments') })}
|
|
120
|
+
</Typography>
|
|
121
|
+
)}
|
|
122
|
+
</Box>
|
|
123
|
+
</Stack>
|
|
124
|
+
);
|
|
125
|
+
}
|