payment-kit 1.13.150 → 1.13.152
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/subscription-refund-succeeded.ts +1 -1
- package/api/src/queues/invoice.ts +14 -0
- package/api/src/queues/refund.ts +1 -0
- package/api/src/routes/checkout-sessions.ts +9 -1
- package/api/src/routes/connect/shared.ts +1 -0
- package/api/src/routes/customers.ts +22 -1
- 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/customer.ts +21 -0
- package/api/src/store/models/index.ts +1 -1
- package/api/src/store/models/invoice.ts +37 -1
- package/api/src/store/models/payment-intent.ts +24 -2
- package/api/src/store/models/refund.ts +53 -16
- package/api/src/store/models/types.ts +2 -0
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/app.tsx +10 -0
- package/src/components/balance-list.tsx +43 -0
- package/src/components/info-metric.tsx +2 -2
- 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/global.css +0 -1
- package/src/locales/en.tsx +2 -71
- package/src/locales/zh.tsx +2 -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/customers/customers/detail.tsx +49 -32
- 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/index.tsx +55 -9
- package/src/pages/customer/invoice/past-due.tsx +77 -0
- package/src/pages/customer/invoice.tsx +58 -56
- package/src/pages/customer/refund/list.tsx +125 -0
|
@@ -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}`;
|
|
@@ -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
|
|
|
@@ -82,7 +82,7 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
|
|
|
82
82
|
terms_of_service: 'none',
|
|
83
83
|
},
|
|
84
84
|
invoice_creation: {
|
|
85
|
-
enabled:
|
|
85
|
+
enabled: true,
|
|
86
86
|
},
|
|
87
87
|
phone_number_collection: {
|
|
88
88
|
enabled: false,
|
|
@@ -504,6 +504,14 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
504
504
|
|
|
505
505
|
await customer.update(updates);
|
|
506
506
|
}
|
|
507
|
+
const canMakeNewPurchase = await customer.canMakeNewPurchase(checkoutSession.invoice_id);
|
|
508
|
+
if (!canMakeNewPurchase) {
|
|
509
|
+
return res.status(403).json({
|
|
510
|
+
code: 'CUSTOMER_LIMITED',
|
|
511
|
+
error: 'Customer can not make new purchase, maybe you have unpaid invoices from previous purchases',
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
507
515
|
await checkoutSession.update({ customer_id: customer.id, customer_did: req.user.did });
|
|
508
516
|
|
|
509
517
|
// payment intent is only created when checkout session is in payment mode
|
|
@@ -532,6 +532,7 @@ export async function ensureInvoiceForCollect(invoiceId: string) {
|
|
|
532
532
|
throw new Error(`Payment intent already canceled for invoice ${invoiceId}`);
|
|
533
533
|
}
|
|
534
534
|
if (paymentIntent.status === 'succeeded') {
|
|
535
|
+
await invoice.update({ status: 'paid' });
|
|
535
536
|
throw new Error(`Payment intent already succeeded for invoice ${invoiceId}`);
|
|
536
537
|
}
|
|
537
538
|
|
|
@@ -102,7 +102,12 @@ router.get('/me', user(), async (req, res) => {
|
|
|
102
102
|
|
|
103
103
|
try {
|
|
104
104
|
const doc = await Customer.findByPkOrDid(req.user.did as string);
|
|
105
|
-
|
|
105
|
+
if (!doc) {
|
|
106
|
+
res.status(404).json({ error: 'Customer not found' });
|
|
107
|
+
} else {
|
|
108
|
+
const summary = await doc.getSummary();
|
|
109
|
+
res.json({ ...doc.toJSON(), summary });
|
|
110
|
+
}
|
|
106
111
|
} catch (err) {
|
|
107
112
|
console.error(err);
|
|
108
113
|
res.json(null);
|
|
@@ -119,6 +124,22 @@ router.get('/:id', auth, async (req, res) => {
|
|
|
119
124
|
}
|
|
120
125
|
});
|
|
121
126
|
|
|
127
|
+
router.get('/:id/summary', auth, async (req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const doc = await Customer.findByPkOrDid(req.params.id as string);
|
|
130
|
+
if (!doc) {
|
|
131
|
+
res.status(404).json({ error: 'Customer not found' });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const result = await doc.getSummary();
|
|
136
|
+
res.json(result);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error(err);
|
|
139
|
+
res.json(null);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
122
143
|
// eslint-disable-next-line consistent-return
|
|
123
144
|
router.put('/:id', authPortal, async (req, res) => {
|
|
124
145
|
try {
|
|
@@ -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
|
+
};
|
|
@@ -154,6 +154,27 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
|
|
|
154
154
|
return `${this.invoice_prefix}-${padStart(sequence.toString(), 4, '0')}`;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
public async getSummary() {
|
|
158
|
+
const { PaymentIntent, Refund, Invoice } = this.sequelize.models;
|
|
159
|
+
const [paid, due, refunded] = await Promise.all([
|
|
160
|
+
// @ts-ignore
|
|
161
|
+
PaymentIntent!.getPaidAmountByCustomer(this.id),
|
|
162
|
+
// @ts-ignore
|
|
163
|
+
Invoice!.getUncollectibleAmountByCustomer(this.id),
|
|
164
|
+
// @ts-ignore
|
|
165
|
+
Refund!.getRefundAmountByCustomer(this.id),
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
return { paid, due, refunded };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public async canMakeNewPurchase(excludedInvoiceId: string = '') {
|
|
172
|
+
const { Invoice } = this.sequelize.models;
|
|
173
|
+
// @ts-ignore
|
|
174
|
+
const result = await Invoice!.getUncollectibleAmountByCustomer(this.id, excludedInvoiceId);
|
|
175
|
+
return Object.entries(result).every(([, amount]) => new BN(amount).lte(new BN(0)));
|
|
176
|
+
}
|
|
177
|
+
|
|
157
178
|
public getBalanceToApply(currencyId: string, amount: string) {
|
|
158
179
|
const tokens = this.token_balance || {};
|
|
159
180
|
const balance = tokens[currencyId] || '0';
|
|
@@ -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;
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/indent */
|
|
2
2
|
/* eslint-disable @typescript-eslint/lines-between-class-members */
|
|
3
|
-
import {
|
|
3
|
+
import { BN } from '@ocap/util';
|
|
4
|
+
import {
|
|
5
|
+
CreationOptional,
|
|
6
|
+
DataTypes,
|
|
7
|
+
InferAttributes,
|
|
8
|
+
InferCreationAttributes,
|
|
9
|
+
Model,
|
|
10
|
+
Op,
|
|
11
|
+
WhereOptions,
|
|
12
|
+
} from 'sequelize';
|
|
4
13
|
import type { LiteralUnion } from 'type-fest';
|
|
5
14
|
|
|
6
15
|
import { createEvent, createStatusEvent } from '../../libs/audit';
|
|
@@ -9,6 +18,7 @@ import type {
|
|
|
9
18
|
CustomerAddress,
|
|
10
19
|
CustomerShipping,
|
|
11
20
|
DiscountAmount,
|
|
21
|
+
GroupedBN,
|
|
12
22
|
PaymentError,
|
|
13
23
|
PaymentSettings,
|
|
14
24
|
SimpleCustomField,
|
|
@@ -511,6 +521,32 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
|
|
|
511
521
|
public isImmutable() {
|
|
512
522
|
return ['paid', 'void'].includes(this.status);
|
|
513
523
|
}
|
|
524
|
+
|
|
525
|
+
public static async getUncollectibleAmountByCustomer(
|
|
526
|
+
customerId: string,
|
|
527
|
+
excludedInvoiceId?: string
|
|
528
|
+
): Promise<GroupedBN> {
|
|
529
|
+
const where: WhereOptions<Invoice> = {
|
|
530
|
+
status: 'uncollectible',
|
|
531
|
+
customer_id: customerId,
|
|
532
|
+
amount_remaining: { [Op.gt]: '0' },
|
|
533
|
+
};
|
|
534
|
+
if (excludedInvoiceId) {
|
|
535
|
+
where.id = { [Op.not]: excludedInvoiceId };
|
|
536
|
+
}
|
|
537
|
+
const invoices = await Invoice.findAll({ where });
|
|
538
|
+
|
|
539
|
+
return invoices.reduce((acc: GroupedBN, invoice) => {
|
|
540
|
+
const key = invoice.currency_id;
|
|
541
|
+
if (!acc[key]) {
|
|
542
|
+
acc[key] = '0';
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
acc[key] = new BN(acc[key]).add(new BN(invoice.amount_remaining)).toString();
|
|
546
|
+
|
|
547
|
+
return acc;
|
|
548
|
+
}, {});
|
|
549
|
+
}
|
|
514
550
|
}
|
|
515
551
|
|
|
516
552
|
export type TInvoice = InferAttributes<Invoice>;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/lines-between-class-members */
|
|
2
2
|
/* eslint-disable @typescript-eslint/indent */
|
|
3
|
-
import {
|
|
3
|
+
import { BN } from '@ocap/util';
|
|
4
|
+
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model, Op } from 'sequelize';
|
|
4
5
|
import type { LiteralUnion } from 'type-fest';
|
|
5
6
|
|
|
6
7
|
import { createEvent, createStatusEvent } from '../../libs/audit';
|
|
7
8
|
import { createIdGenerator } from '../../libs/util';
|
|
8
|
-
import type { PaymentDetails, PaymentError } from './types';
|
|
9
|
+
import type { GroupedBN, PaymentDetails, PaymentError } from './types';
|
|
9
10
|
|
|
10
11
|
export const nextPaymentIntentId = createIdGenerator('pi', 24);
|
|
11
12
|
|
|
@@ -280,6 +281,27 @@ export class PaymentIntent extends Model<InferAttributes<PaymentIntent>, InferCr
|
|
|
280
281
|
public isImmutable() {
|
|
281
282
|
return ['canceled', 'succeeded'].includes(this.status);
|
|
282
283
|
}
|
|
284
|
+
|
|
285
|
+
public static async getPaidAmountByCustomer(customerId: string): Promise<GroupedBN> {
|
|
286
|
+
const payments = await PaymentIntent.findAll({
|
|
287
|
+
where: {
|
|
288
|
+
status: 'succeeded',
|
|
289
|
+
customer_id: customerId,
|
|
290
|
+
amount_received: { [Op.gt]: '0' },
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
return payments.reduce((acc: GroupedBN, payment) => {
|
|
295
|
+
const key = payment.currency_id;
|
|
296
|
+
if (!acc[key]) {
|
|
297
|
+
acc[key] = '0';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
acc[key] = new BN(acc[key]).add(new BN(payment.amount_received)).toString();
|
|
301
|
+
|
|
302
|
+
return acc;
|
|
303
|
+
}, {});
|
|
304
|
+
}
|
|
283
305
|
}
|
|
284
306
|
|
|
285
307
|
export type TPaymentIntent = InferAttributes<PaymentIntent>;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/lines-between-class-members */
|
|
2
2
|
/* eslint-disable @typescript-eslint/indent */
|
|
3
|
-
import {
|
|
3
|
+
import { BN } from '@ocap/util';
|
|
4
|
+
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model, Op } from 'sequelize';
|
|
4
5
|
import type { LiteralUnion } from 'type-fest';
|
|
5
6
|
|
|
6
7
|
import { createEvent, createStatusEvent } from '../../libs/audit';
|
|
7
8
|
import { createIdGenerator } from '../../libs/util';
|
|
8
|
-
import type { PaymentDetails, PaymentError } from './types';
|
|
9
|
+
import type { GroupedBN, PaymentDetails, PaymentError } from './types';
|
|
9
10
|
|
|
10
11
|
export const nextRefundId = createIdGenerator('re', 24);
|
|
11
12
|
|
|
@@ -25,6 +26,7 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
|
|
|
25
26
|
declare currency_id: string;
|
|
26
27
|
declare customer_id: string;
|
|
27
28
|
declare payment_intent_id: string;
|
|
29
|
+
declare payment_method_id: string;
|
|
28
30
|
declare invoice_id?: string;
|
|
29
31
|
declare subscription_id?: string;
|
|
30
32
|
|
|
@@ -173,21 +175,30 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
|
|
|
173
175
|
};
|
|
174
176
|
|
|
175
177
|
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),
|
|
178
|
+
this.init(
|
|
179
|
+
{
|
|
180
|
+
...Refund.GENESIS_ATTRIBUTES,
|
|
181
|
+
payment_method_id: {
|
|
182
|
+
type: DataTypes.STRING(30),
|
|
183
|
+
allowNull: true,
|
|
184
|
+
},
|
|
189
185
|
},
|
|
190
|
-
|
|
186
|
+
{
|
|
187
|
+
sequelize,
|
|
188
|
+
modelName: 'Refund',
|
|
189
|
+
tableName: 'refunds',
|
|
190
|
+
createdAt: 'created_at',
|
|
191
|
+
updatedAt: 'updated_at',
|
|
192
|
+
hooks: {
|
|
193
|
+
afterCreate: (model: Refund, options) =>
|
|
194
|
+
createEvent('Refund', 'refund.created', model, options).catch(console.error),
|
|
195
|
+
afterUpdate: (model: Refund, options) =>
|
|
196
|
+
createStatusEvent('Refund', 'refund', { canceled: 'canceled', succeeded: 'succeeded' }, model, options),
|
|
197
|
+
afterDestroy: (model: Refund, options) =>
|
|
198
|
+
createEvent('Refund', 'refund.deleted', model, options).catch(console.error),
|
|
199
|
+
},
|
|
200
|
+
}
|
|
201
|
+
);
|
|
191
202
|
}
|
|
192
203
|
|
|
193
204
|
public static associate(models: any) {
|
|
@@ -206,6 +217,11 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
|
|
|
206
217
|
foreignKey: 'id',
|
|
207
218
|
as: 'paymentIntent',
|
|
208
219
|
});
|
|
220
|
+
this.hasOne(models.PaymentMethod, {
|
|
221
|
+
sourceKey: 'payment_method_id',
|
|
222
|
+
foreignKey: 'id',
|
|
223
|
+
as: 'paymentMethod',
|
|
224
|
+
});
|
|
209
225
|
this.hasOne(models.Invoice, {
|
|
210
226
|
sourceKey: 'invoice_id',
|
|
211
227
|
foreignKey: 'id',
|
|
@@ -221,6 +237,27 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
|
|
|
221
237
|
public isImmutable() {
|
|
222
238
|
return ['canceled', 'succeeded'].includes(this.status);
|
|
223
239
|
}
|
|
240
|
+
|
|
241
|
+
public static async getRefundAmountByCustomer(customerId: string, status: string = 'succeeded'): Promise<GroupedBN> {
|
|
242
|
+
const refunds = await Refund.findAll({
|
|
243
|
+
where: {
|
|
244
|
+
status,
|
|
245
|
+
customer_id: customerId,
|
|
246
|
+
amount: { [Op.gt]: '0' },
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return refunds.reduce((acc: GroupedBN, refund) => {
|
|
251
|
+
const key = refund.currency_id;
|
|
252
|
+
if (!acc[key]) {
|
|
253
|
+
acc[key] = '0';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
acc[key] = new BN(acc[key]).add(new BN(refund.amount)).toString();
|
|
257
|
+
|
|
258
|
+
return acc;
|
|
259
|
+
}, {});
|
|
260
|
+
}
|
|
224
261
|
}
|
|
225
262
|
|
|
226
263
|
export type TRefund = InferAttributes<Refund>;
|
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.152",
|
|
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.152",
|
|
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.152",
|
|
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": "5d6d2b6a8c0422e1055ddcec4e3179fd734cdbc5"
|
|
153
153
|
}
|
package/src/app.tsx
CHANGED
|
@@ -21,6 +21,7 @@ const CheckoutPage = React.lazy(() => import('./pages/checkout'));
|
|
|
21
21
|
const AdminPage = React.lazy(() => import('./pages/admin'));
|
|
22
22
|
const CustomerHome = React.lazy(() => import('./pages/customer/index'));
|
|
23
23
|
const CustomerInvoice = React.lazy(() => import('./pages/customer/invoice'));
|
|
24
|
+
const CustomerInvoicePastDue = React.lazy(() => import('./pages/customer/invoice/past-due'));
|
|
24
25
|
const CustomerSubscriptionDetail = React.lazy(() => import('./pages/customer/subscription/detail'));
|
|
25
26
|
const CustomerSubscriptionUpdate = React.lazy(() => import('./pages/customer/subscription/update'));
|
|
26
27
|
|
|
@@ -79,6 +80,15 @@ function App() {
|
|
|
79
80
|
}
|
|
80
81
|
/>
|
|
81
82
|
<Route key="subscription-embed" path="/customer/embed/subscription" element={<MiniInvoiceList />} />,
|
|
83
|
+
<Route
|
|
84
|
+
key="customer-due"
|
|
85
|
+
path="/customer/invoice/past-due"
|
|
86
|
+
element={
|
|
87
|
+
<Layout>
|
|
88
|
+
<CustomerInvoicePastDue />
|
|
89
|
+
</Layout>
|
|
90
|
+
}
|
|
91
|
+
/>
|
|
82
92
|
<Route
|
|
83
93
|
key="customer-invoice"
|
|
84
94
|
path="/customer/invoice/:id"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { formatAmount, usePaymentContext } from '@blocklet/payment-react';
|
|
2
|
+
import type { GroupedBN } from '@blocklet/payment-types';
|
|
3
|
+
import { Stack, Typography } from '@mui/material';
|
|
4
|
+
import flatten from 'lodash/flatten';
|
|
5
|
+
import isEmpty from 'lodash/isEmpty';
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
data?: GroupedBN;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default function BalanceList(props: Props) {
|
|
12
|
+
const { settings } = usePaymentContext();
|
|
13
|
+
const currencies = flatten(settings.paymentMethods.map((method) => method.payment_currencies));
|
|
14
|
+
|
|
15
|
+
if (isEmpty(props.data)) {
|
|
16
|
+
return <Typography>None</Typography>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Stack direction="column" alignItems="flex-start" sx={{ width: '100%' }}>
|
|
21
|
+
{Object.entries(props.data).map(([currencyId, amount]) => {
|
|
22
|
+
const currency = currencies.find((c) => c.id === currencyId);
|
|
23
|
+
if (!currency) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return (
|
|
27
|
+
<Stack key={currencyId} sx={{ width: '100%' }} direction="row" spacing={1}>
|
|
28
|
+
<Typography sx={{ flex: 1 }} color="text.primary">
|
|
29
|
+
{formatAmount(amount, currency.decimal)}
|
|
30
|
+
</Typography>
|
|
31
|
+
<Typography sx={{ width: '32px' }} color="text.secondary">
|
|
32
|
+
{currency.symbol}
|
|
33
|
+
</Typography>
|
|
34
|
+
</Stack>
|
|
35
|
+
);
|
|
36
|
+
})}
|
|
37
|
+
</Stack>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
BalanceList.defaultProps = {
|
|
42
|
+
data: {},
|
|
43
|
+
};
|
|
@@ -12,7 +12,7 @@ export default function InfoMetric(props: Props) {
|
|
|
12
12
|
return (
|
|
13
13
|
<>
|
|
14
14
|
<Stack direction="column" alignItems="flex-start">
|
|
15
|
-
<Typography variant="body1" mb={1} color="text.secondary">
|
|
15
|
+
<Typography component="div" variant="body1" mb={1} color="text.secondary">
|
|
16
16
|
{props.label}
|
|
17
17
|
{!!props.tip && (
|
|
18
18
|
<Tooltip title={props.tip}>
|
|
@@ -20,7 +20,7 @@ export default function InfoMetric(props: Props) {
|
|
|
20
20
|
</Tooltip>
|
|
21
21
|
)}
|
|
22
22
|
</Typography>
|
|
23
|
-
<Typography variant="body1" color="text.primary">
|
|
23
|
+
<Typography component="div" variant="body1" color="text.primary" sx={{ width: '100%' }}>
|
|
24
24
|
{props.value}
|
|
25
25
|
</Typography>
|
|
26
26
|
</Stack>
|
|
@@ -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}
|