payment-kit 1.20.13 → 1.20.14
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/routes/checkout-sessions.ts +15 -2
- package/api/src/routes/coupons.ts +7 -0
- package/api/src/routes/credit-grants.ts +8 -1
- package/api/src/routes/credit-transactions.ts +153 -13
- package/api/src/routes/invoices.ts +35 -1
- package/api/src/routes/meter-events.ts +31 -3
- package/api/src/routes/meters.ts +4 -0
- package/api/src/routes/payment-currencies.ts +2 -1
- package/api/src/routes/promotion-codes.ts +2 -2
- package/api/src/routes/subscription-items.ts +4 -0
- package/api/src/routes/webhook-endpoints.ts +4 -0
- package/api/src/store/migrations/20250919-add-source-data.ts +20 -0
- package/api/src/store/models/credit-transaction.ts +5 -0
- package/api/src/store/models/meter-event.ts +22 -12
- package/api/src/store/models/types.ts +18 -0
- package/blocklet.yml +1 -1
- package/package.json +5 -5
- package/src/components/customer/credit-overview.tsx +1 -1
- package/src/components/customer/related-credit-grants.tsx +194 -0
- package/src/components/meter/add-usage-dialog.tsx +8 -0
- package/src/components/meter/events-list.tsx +93 -96
- package/src/components/product/form.tsx +0 -1
- package/src/locales/en.tsx +9 -0
- package/src/locales/zh.tsx +9 -0
- package/src/pages/admin/billing/invoices/detail.tsx +21 -2
- package/src/pages/customer/invoice/detail.tsx +11 -2
|
@@ -668,7 +668,9 @@ const getBeneficiaryName = async (beneficiary: PaymentBeneficiary) => {
|
|
|
668
668
|
return beneficiary.name || (await getUserOrAppInfo(beneficiary.address || ''))?.name || beneficiary.address;
|
|
669
669
|
};
|
|
670
670
|
|
|
671
|
-
export async function getCrossSellItem(
|
|
671
|
+
export async function getCrossSellItem(
|
|
672
|
+
checkoutSession: CheckoutSession
|
|
673
|
+
): Promise<{ error?: string } | (TPriceExpanded & { product: any; error?: string })> {
|
|
672
674
|
// FIXME: perhaps we can support cross sell even if the current session have multiple items
|
|
673
675
|
if (checkoutSession.line_items.length > 1) {
|
|
674
676
|
return { error: 'Cross sell not supported for checkoutSession with multiple line items' };
|
|
@@ -2334,8 +2336,12 @@ router.put('/:id/expire', auth, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
2334
2336
|
router.get('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
2335
2337
|
try {
|
|
2336
2338
|
const checkoutSession = req.doc as CheckoutSession;
|
|
2339
|
+
const skipError = req.query.skipError === 'true';
|
|
2337
2340
|
const result = await getCrossSellItem(checkoutSession);
|
|
2338
|
-
|
|
2341
|
+
|
|
2342
|
+
if (skipError && result.error) {
|
|
2343
|
+
return res.status(200).json(result);
|
|
2344
|
+
}
|
|
2339
2345
|
return res.status(result.error ? 400 : 200).json(result);
|
|
2340
2346
|
} catch (err) {
|
|
2341
2347
|
logger.error(err);
|
|
@@ -2633,6 +2639,10 @@ router.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async
|
|
|
2633
2639
|
return res.status(400).json({ error: 'Coupon no longer valid' });
|
|
2634
2640
|
}
|
|
2635
2641
|
|
|
2642
|
+
const now = dayjs().unix();
|
|
2643
|
+
const trialSetup = getSubscriptionTrialSetup(checkoutSession.subscription_data as any, currency.id);
|
|
2644
|
+
const isTrialing = trialSetup.trialInDays > 0 || trialSetup.trialEnd > now;
|
|
2645
|
+
|
|
2636
2646
|
// Apply discount with new currency
|
|
2637
2647
|
const discountResult = await applyDiscountsToLineItems({
|
|
2638
2648
|
lineItems: expandedItems,
|
|
@@ -2640,6 +2650,9 @@ router.post('/:id/recalculate-promotion', user, ensureCheckoutSessionOpen, async
|
|
|
2640
2650
|
couponId,
|
|
2641
2651
|
customerId: customer.id,
|
|
2642
2652
|
currency,
|
|
2653
|
+
billingContext: {
|
|
2654
|
+
trialing: isTrialing,
|
|
2655
|
+
},
|
|
2643
2656
|
});
|
|
2644
2657
|
|
|
2645
2658
|
// Check if discount can still be applied with the new currency
|
|
@@ -373,6 +373,13 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
373
373
|
return res.status(404).json({ error: 'Coupon not found' });
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
+
if (req.body.metadata) {
|
|
377
|
+
const { error: metadataError } = MetadataSchema.validate(req.body.metadata);
|
|
378
|
+
if (metadataError) {
|
|
379
|
+
return res.status(400).json({ error: `metadata invalid: ${metadataError.message}` });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
376
383
|
if (coupon.locked) {
|
|
377
384
|
const allowedUpdates = pick(value, ['name', 'metadata', 'description']);
|
|
378
385
|
if (Object.keys(allowedUpdates).length === 0) {
|
|
@@ -11,6 +11,7 @@ import { CreditGrant, Customer, PaymentCurrency, Price, Subscription } from '../
|
|
|
11
11
|
import { createCreditGrant } from '../libs/credit-grant';
|
|
12
12
|
import { getMeterPriceIdsFromSubscription } from '../libs/subscription';
|
|
13
13
|
import { blocklet } from '../libs/auth';
|
|
14
|
+
import { formatMetadata } from '../libs/util';
|
|
14
15
|
|
|
15
16
|
const router = Router();
|
|
16
17
|
const auth = authenticate<CreditGrant>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -264,7 +265,13 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
264
265
|
if (error) {
|
|
265
266
|
return res.status(400).json({ error: `Credit grant update request invalid: ${error.message}` });
|
|
266
267
|
}
|
|
267
|
-
|
|
268
|
+
if (req.body.metadata) {
|
|
269
|
+
const { error: metadataError } = MetadataSchema.validate(req.body.metadata);
|
|
270
|
+
if (metadataError) {
|
|
271
|
+
return res.status(400).json({ error: `metadata invalid: ${metadataError.message}` });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
await creditGrant.update({ metadata: formatMetadata(req.body.metadata) });
|
|
268
275
|
return res.json({ success: true });
|
|
269
276
|
});
|
|
270
277
|
|
|
@@ -3,9 +3,18 @@ import Joi from 'joi';
|
|
|
3
3
|
|
|
4
4
|
import { Op } from 'sequelize';
|
|
5
5
|
import { createListParamSchema, getOrder, getWhereFromKvQuery } from '../libs/api';
|
|
6
|
+
import { mergePaginate, type DataSource } from '../libs/pagination';
|
|
6
7
|
import logger from '../libs/logger';
|
|
7
8
|
import { authenticate } from '../libs/security';
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
CreditTransaction,
|
|
11
|
+
Customer,
|
|
12
|
+
CreditGrant,
|
|
13
|
+
Meter,
|
|
14
|
+
MeterEvent,
|
|
15
|
+
Subscription,
|
|
16
|
+
PaymentCurrency,
|
|
17
|
+
} from '../store/models';
|
|
9
18
|
|
|
10
19
|
const router = Router();
|
|
11
20
|
const authMine = authenticate<CreditTransaction>({ component: true, roles: ['owner', 'admin'], mine: true });
|
|
@@ -27,6 +36,7 @@ const listSchema = createListParamSchema<{
|
|
|
27
36
|
start?: number;
|
|
28
37
|
end?: number;
|
|
29
38
|
source?: string;
|
|
39
|
+
include_grants?: boolean;
|
|
30
40
|
}>({
|
|
31
41
|
customer_id: Joi.string().empty(''),
|
|
32
42
|
subscription_id: Joi.string().empty(''),
|
|
@@ -35,11 +45,13 @@ const listSchema = createListParamSchema<{
|
|
|
35
45
|
start: Joi.number().integer().optional(),
|
|
36
46
|
end: Joi.number().integer().optional(),
|
|
37
47
|
source: Joi.string().empty(''),
|
|
48
|
+
include_grants: Joi.boolean().optional(),
|
|
38
49
|
});
|
|
39
50
|
|
|
40
51
|
router.get('/', authMine, async (req, res) => {
|
|
41
52
|
try {
|
|
42
53
|
const { page, pageSize, ...query } = await listSchema.validateAsync(req.query, { stripUnknown: true });
|
|
54
|
+
const includeGrants = !!query.include_grants;
|
|
43
55
|
const where = getWhereFromKvQuery(query.q);
|
|
44
56
|
|
|
45
57
|
if (query.meter_id) {
|
|
@@ -73,6 +85,124 @@ router.get('/', authMine, async (req, res) => {
|
|
|
73
85
|
};
|
|
74
86
|
}
|
|
75
87
|
|
|
88
|
+
if (query.start && query.end) {
|
|
89
|
+
where.created_at = {
|
|
90
|
+
[Op.between]: [new Date(query.start * 1000), new Date(query.end * 1000)],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (includeGrants) {
|
|
95
|
+
if (!query.customer_id) {
|
|
96
|
+
return res.status(400).json({
|
|
97
|
+
error: 'customer_id is required when include_grants=true',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const orderDirection = query.o === 'asc' ? 'ASC' : 'DESC';
|
|
102
|
+
|
|
103
|
+
const transactionSource: DataSource<any> = {
|
|
104
|
+
async count() {
|
|
105
|
+
const count = await CreditTransaction.count({ where });
|
|
106
|
+
return count;
|
|
107
|
+
},
|
|
108
|
+
async fetch(limit, offset) {
|
|
109
|
+
const rows = await CreditTransaction.findAll({
|
|
110
|
+
where,
|
|
111
|
+
limit,
|
|
112
|
+
offset,
|
|
113
|
+
order: [['created_at', orderDirection]],
|
|
114
|
+
include: [
|
|
115
|
+
{ model: Customer, as: 'customer', attributes: ['id', 'name', 'email', 'did'] },
|
|
116
|
+
{ model: Meter, as: 'meter' },
|
|
117
|
+
{ model: Subscription, as: 'subscription', attributes: ['id', 'description', 'status'], required: false },
|
|
118
|
+
{ model: CreditGrant, as: 'creditGrant', attributes: ['id', 'name', 'currency_id'] },
|
|
119
|
+
{ model: MeterEvent, as: 'meterEvent', attributes: ['id', 'source_data'], required: false },
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
// Transform transactions
|
|
123
|
+
return rows.map((item: any) => ({
|
|
124
|
+
...item.toJSON(),
|
|
125
|
+
activity_type: 'transaction',
|
|
126
|
+
}));
|
|
127
|
+
},
|
|
128
|
+
meta: { type: 'database' },
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Grant where conditions
|
|
132
|
+
const grantWhere: any = {
|
|
133
|
+
customer_id: query.customer_id,
|
|
134
|
+
status: ['granted', 'depleted'],
|
|
135
|
+
};
|
|
136
|
+
if (query.start) {
|
|
137
|
+
grantWhere.created_at = {
|
|
138
|
+
[Op.gte]: new Date(query.start * 1000),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (typeof query.livemode === 'boolean') grantWhere.livemode = query.livemode;
|
|
142
|
+
|
|
143
|
+
const grantSource: DataSource<any> = {
|
|
144
|
+
async count() {
|
|
145
|
+
const { count } = await CreditGrant.findAndCountAll({ where: grantWhere });
|
|
146
|
+
return count;
|
|
147
|
+
},
|
|
148
|
+
async fetch(limit, offset) {
|
|
149
|
+
const { rows } = await CreditGrant.findAndCountAll({
|
|
150
|
+
where: grantWhere,
|
|
151
|
+
limit,
|
|
152
|
+
offset,
|
|
153
|
+
order: [['created_at', orderDirection]],
|
|
154
|
+
include: [
|
|
155
|
+
{ model: Customer, as: 'customer', attributes: ['id', 'name', 'email', 'did'] },
|
|
156
|
+
{
|
|
157
|
+
model: PaymentCurrency,
|
|
158
|
+
as: 'paymentCurrency',
|
|
159
|
+
attributes: ['id', 'symbol', 'decimal', 'maximum_precision'],
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
// Transform grants
|
|
164
|
+
return rows.map((item: any) => ({
|
|
165
|
+
...item.toJSON(),
|
|
166
|
+
activity_type: 'grant',
|
|
167
|
+
}));
|
|
168
|
+
},
|
|
169
|
+
meta: { type: 'database' },
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Define sort function
|
|
173
|
+
const sortFn = (a: any, b: any) => {
|
|
174
|
+
const aDate = new Date(a.created_at).getTime();
|
|
175
|
+
const bDate = new Date(b.created_at).getTime();
|
|
176
|
+
return query.o === 'asc' ? aDate - bDate : bDate - aDate;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Use mergePaginate
|
|
180
|
+
const result = await mergePaginate([transactionSource, grantSource], { page, pageSize }, sortFn);
|
|
181
|
+
|
|
182
|
+
// Load payment currencies for final result
|
|
183
|
+
const paymentCurrencies = await PaymentCurrency.findAll({
|
|
184
|
+
attributes: ['id', 'symbol', 'decimal', 'maximum_precision'],
|
|
185
|
+
where: { type: 'credit' },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const enhancedData = result.data.map((item) => ({
|
|
189
|
+
...item,
|
|
190
|
+
paymentCurrency: paymentCurrencies.find(
|
|
191
|
+
(x) => x.id === (item.activity_type === 'grant' ? item.currency_id : item.creditGrant?.currency_id)
|
|
192
|
+
),
|
|
193
|
+
}));
|
|
194
|
+
|
|
195
|
+
return res.json({
|
|
196
|
+
count: result.total,
|
|
197
|
+
list: enhancedData,
|
|
198
|
+
paging: result.paging,
|
|
199
|
+
meta: {
|
|
200
|
+
unified_cash_flow: true,
|
|
201
|
+
includes: ['transaction', 'grant'],
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
76
206
|
const { rows: list, count } = await CreditTransaction.findAndCountAll({
|
|
77
207
|
where,
|
|
78
208
|
order: getOrder(query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
|
|
@@ -97,27 +227,37 @@ router.get('/', authMine, async (req, res) => {
|
|
|
97
227
|
{
|
|
98
228
|
model: CreditGrant,
|
|
99
229
|
as: 'creditGrant',
|
|
100
|
-
attributes: ['id', 'name'],
|
|
230
|
+
attributes: ['id', 'name', 'currency_id'],
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
model: MeterEvent,
|
|
234
|
+
as: 'meterEvent',
|
|
235
|
+
attributes: ['id', 'source_data'], // Get source_data from related MeterEvent
|
|
236
|
+
required: false,
|
|
101
237
|
},
|
|
102
238
|
],
|
|
103
239
|
});
|
|
104
240
|
|
|
105
241
|
const paymentCurrencies = await PaymentCurrency.findAll({
|
|
106
242
|
attributes: ['id', 'symbol', 'decimal', 'maximum_precision'],
|
|
107
|
-
where: {
|
|
108
|
-
type: 'credit',
|
|
109
|
-
},
|
|
243
|
+
where: { type: 'credit' },
|
|
110
244
|
});
|
|
111
245
|
|
|
112
|
-
const result = list.map((item) => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
};
|
|
118
|
-
});
|
|
246
|
+
const result = list.map((item) => ({
|
|
247
|
+
...item.toJSON(),
|
|
248
|
+
activity_type: 'transaction',
|
|
249
|
+
paymentCurrency: paymentCurrencies.find((x) => x.id === (item as any).creditGrant?.currency_id),
|
|
250
|
+
}));
|
|
119
251
|
|
|
120
|
-
return res.json({
|
|
252
|
+
return res.json({
|
|
253
|
+
count,
|
|
254
|
+
list: result,
|
|
255
|
+
paging: { page, pageSize },
|
|
256
|
+
meta: {
|
|
257
|
+
unified_cash_flow: false,
|
|
258
|
+
includes: ['transaction'],
|
|
259
|
+
},
|
|
260
|
+
});
|
|
121
261
|
} catch (err) {
|
|
122
262
|
logger.error('Error listing credit transactions', err);
|
|
123
263
|
return res.status(400).json({ error: err.message });
|
|
@@ -23,7 +23,15 @@ import { Price } from '../store/models/price';
|
|
|
23
23
|
import { Product } from '../store/models/product';
|
|
24
24
|
import { Subscription } from '../store/models/subscription';
|
|
25
25
|
import { getReturnStakeInvoices, getStakingInvoices, retryUncollectibleInvoices } from '../libs/invoice';
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
CheckoutSession,
|
|
28
|
+
PaymentLink,
|
|
29
|
+
TInvoiceExpanded,
|
|
30
|
+
Discount,
|
|
31
|
+
Coupon,
|
|
32
|
+
PromotionCode,
|
|
33
|
+
CreditGrant,
|
|
34
|
+
} from '../store/models';
|
|
27
35
|
import { mergePaginate, defaultTimeOrderBy, getCachedOrFetch, DataSource } from '../libs/pagination';
|
|
28
36
|
import logger from '../libs/logger';
|
|
29
37
|
import { returnOverdraftProtectionQueue, returnStakeQueue } from '../queues/subscription';
|
|
@@ -678,6 +686,30 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
678
686
|
}
|
|
679
687
|
}
|
|
680
688
|
|
|
689
|
+
let relatedCreditGrants: any[] = [];
|
|
690
|
+
try {
|
|
691
|
+
relatedCreditGrants = await CreditGrant.findAll({
|
|
692
|
+
where: {
|
|
693
|
+
customer_id: doc.customer_id,
|
|
694
|
+
'metadata.invoice_id': doc.id,
|
|
695
|
+
} as any,
|
|
696
|
+
include: [
|
|
697
|
+
{
|
|
698
|
+
model: PaymentCurrency,
|
|
699
|
+
as: 'paymentCurrency',
|
|
700
|
+
attributes: ['id', 'symbol', 'decimal', 'name', 'type'],
|
|
701
|
+
},
|
|
702
|
+
],
|
|
703
|
+
order: [['created_at', 'DESC']],
|
|
704
|
+
});
|
|
705
|
+
} catch (error) {
|
|
706
|
+
logger.error('Failed to fetch related credit grants', {
|
|
707
|
+
error,
|
|
708
|
+
invoiceId: doc.id,
|
|
709
|
+
customerId: doc.customer_id,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
681
713
|
if (doc.metadata?.invoice_id || doc.metadata?.prev_invoice_id) {
|
|
682
714
|
const relatedInvoice = await Invoice.findByPk(doc.metadata.invoice_id || doc.metadata.prev_invoice_id, {
|
|
683
715
|
attributes: ['id', 'number', 'status', 'billing_reason'],
|
|
@@ -686,6 +718,7 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
686
718
|
...json,
|
|
687
719
|
discountDetails,
|
|
688
720
|
relatedInvoice,
|
|
721
|
+
relatedCreditGrants,
|
|
689
722
|
paymentLink,
|
|
690
723
|
checkoutSession,
|
|
691
724
|
});
|
|
@@ -693,6 +726,7 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
693
726
|
return res.json({
|
|
694
727
|
...json,
|
|
695
728
|
discountDetails,
|
|
729
|
+
relatedCreditGrants,
|
|
696
730
|
paymentLink,
|
|
697
731
|
checkoutSession,
|
|
698
732
|
});
|
|
@@ -14,6 +14,32 @@ const router = Router();
|
|
|
14
14
|
const auth = authenticate<MeterEvent>({ component: true, roles: ['owner', 'admin'] });
|
|
15
15
|
const authMine = authenticate<MeterEvent>({ component: true, roles: ['owner', 'admin'], mine: true });
|
|
16
16
|
|
|
17
|
+
const SourceDataSchema = Joi.alternatives()
|
|
18
|
+
.try(
|
|
19
|
+
Joi.object().pattern(Joi.string().max(40), Joi.string().max(256).allow('')).min(0),
|
|
20
|
+
Joi.array()
|
|
21
|
+
.items(
|
|
22
|
+
Joi.object({
|
|
23
|
+
key: Joi.string().max(40).required(),
|
|
24
|
+
label: Joi.alternatives()
|
|
25
|
+
.try(
|
|
26
|
+
Joi.string().max(100),
|
|
27
|
+
Joi.object({
|
|
28
|
+
zh: Joi.string().max(100).optional(),
|
|
29
|
+
en: Joi.string().max(100).optional(),
|
|
30
|
+
})
|
|
31
|
+
)
|
|
32
|
+
.required(),
|
|
33
|
+
value: Joi.string().max(256).allow('').optional(),
|
|
34
|
+
type: Joi.string().valid('text', 'image', 'url').optional(),
|
|
35
|
+
url: Joi.string().uri().optional(),
|
|
36
|
+
group: Joi.string().max(40).optional(),
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
.min(0)
|
|
40
|
+
)
|
|
41
|
+
.optional();
|
|
42
|
+
|
|
17
43
|
const meterEventSchema = Joi.object({
|
|
18
44
|
event_name: Joi.string().max(128).required(),
|
|
19
45
|
payload: Joi.object({
|
|
@@ -24,6 +50,7 @@ const meterEventSchema = Joi.object({
|
|
|
24
50
|
timestamp: Joi.number().integer().optional(),
|
|
25
51
|
identifier: Joi.string().max(255).required(),
|
|
26
52
|
metadata: MetadataSchema,
|
|
53
|
+
source_data: SourceDataSchema,
|
|
27
54
|
});
|
|
28
55
|
|
|
29
56
|
const listSchema = createListParamSchema<{
|
|
@@ -72,12 +99,12 @@ router.get('/', authMine, async (req, res) => {
|
|
|
72
99
|
}
|
|
73
100
|
|
|
74
101
|
if (query.start || query.end) {
|
|
75
|
-
where.
|
|
102
|
+
where.timestamp = {};
|
|
76
103
|
if (query.start) {
|
|
77
|
-
where.
|
|
104
|
+
where.timestamp[Op.gte] = Number(query.start);
|
|
78
105
|
}
|
|
79
106
|
if (query.end) {
|
|
80
|
-
where.
|
|
107
|
+
where.timestamp[Op.lte] = Number(query.end);
|
|
81
108
|
}
|
|
82
109
|
}
|
|
83
110
|
|
|
@@ -259,6 +286,7 @@ router.post('/', auth, async (req, res) => {
|
|
|
259
286
|
credit_pending: fromTokenToUnit(value, paymentCurrency.decimal).toString(),
|
|
260
287
|
created_via: req.user?.via || 'api',
|
|
261
288
|
metadata: formatMetadata(req.body.metadata),
|
|
289
|
+
source_data: req.body.source_data,
|
|
262
290
|
timestamp,
|
|
263
291
|
};
|
|
264
292
|
|
package/api/src/routes/meters.ts
CHANGED
|
@@ -156,6 +156,10 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
156
156
|
};
|
|
157
157
|
|
|
158
158
|
if (req.body.metadata) {
|
|
159
|
+
const { error: metadataError } = MetadataSchema.validate(req.body.metadata);
|
|
160
|
+
if (metadataError) {
|
|
161
|
+
return res.status(400).json({ error: `metadata invalid: ${metadataError.message}` });
|
|
162
|
+
}
|
|
159
163
|
updateData.metadata = formatMetadata(req.body.metadata);
|
|
160
164
|
}
|
|
161
165
|
|
|
@@ -19,6 +19,7 @@ import { depositVaultQueue } from '../queues/payment';
|
|
|
19
19
|
import { checkDepositVaultAmount } from '../libs/payment';
|
|
20
20
|
import { getTokenSummaryByDid } from '../integrations/arcblock/stake';
|
|
21
21
|
import { createPaymentLink } from './payment-links';
|
|
22
|
+
import { MetadataSchema } from '../libs/api';
|
|
22
23
|
|
|
23
24
|
const router = Router();
|
|
24
25
|
|
|
@@ -311,7 +312,7 @@ const updateCurrencySchema = Joi.object({
|
|
|
311
312
|
name: Joi.string().empty('').max(32).optional(),
|
|
312
313
|
description: Joi.string().empty('').max(255).optional(),
|
|
313
314
|
logo: Joi.string().empty('').optional(),
|
|
314
|
-
metadata:
|
|
315
|
+
metadata: MetadataSchema,
|
|
315
316
|
symbol: Joi.string().empty('').optional(),
|
|
316
317
|
}).unknown(true);
|
|
317
318
|
router.put('/:id', auth, async (req, res) => {
|
|
@@ -8,7 +8,7 @@ import { createIdGenerator, formatMetadata } from '../libs/util';
|
|
|
8
8
|
import { authenticate } from '../libs/security';
|
|
9
9
|
import { PromotionCode, Coupon, PaymentCurrency } from '../store/models';
|
|
10
10
|
import { getRedemptionData } from '../libs/discount/redemption';
|
|
11
|
-
import { createListParamSchema } from '../libs/api';
|
|
11
|
+
import { createListParamSchema, MetadataSchema } from '../libs/api';
|
|
12
12
|
import logger from '../libs/logger';
|
|
13
13
|
|
|
14
14
|
const router = Router();
|
|
@@ -249,7 +249,7 @@ router.put('/:id', authAdmin, async (req, res) => {
|
|
|
249
249
|
minimum_amount: Joi.number().positive().optional(),
|
|
250
250
|
minimum_amount_currency: Joi.string().optional(),
|
|
251
251
|
}).optional(),
|
|
252
|
-
metadata:
|
|
252
|
+
metadata: MetadataSchema,
|
|
253
253
|
});
|
|
254
254
|
|
|
255
255
|
const { error, value } = schema.validate(req.body, {
|
|
@@ -139,6 +139,10 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
if (updates.metadata) {
|
|
142
|
+
const { error: metadataError } = MetadataSchema.validate(updates.metadata);
|
|
143
|
+
if (metadataError) {
|
|
144
|
+
return res.status(400).json({ error: `metadata invalid: ${metadataError.message}` });
|
|
145
|
+
}
|
|
142
146
|
updates.metadata = formatMetadata(updates.metadata);
|
|
143
147
|
}
|
|
144
148
|
|
|
@@ -100,6 +100,10 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
100
100
|
'enabled_events',
|
|
101
101
|
]);
|
|
102
102
|
if (updates.metadata) {
|
|
103
|
+
const { error: metadataError } = MetadataSchema.validate(updates.metadata);
|
|
104
|
+
if (metadataError) {
|
|
105
|
+
return res.status(400).json({ error: `metadata invalid: ${metadataError.message}` });
|
|
106
|
+
}
|
|
103
107
|
updates.metadata = formatMetadata(updates.metadata);
|
|
104
108
|
}
|
|
105
109
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
import { Migration, safeApplyColumnChanges } from '../migrate';
|
|
3
|
+
|
|
4
|
+
export const up: Migration = async ({ context }) => {
|
|
5
|
+
await safeApplyColumnChanges(context, {
|
|
6
|
+
meter_events: [
|
|
7
|
+
{
|
|
8
|
+
name: 'source_data',
|
|
9
|
+
field: {
|
|
10
|
+
type: DataTypes.JSON,
|
|
11
|
+
allowNull: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const down: Migration = async ({ context }) => {
|
|
19
|
+
await context.removeColumn('meter_events', 'source_data');
|
|
20
|
+
};
|
|
@@ -135,6 +135,11 @@ export class CreditTransaction extends Model<
|
|
|
135
135
|
foreignKey: 'subscription_id',
|
|
136
136
|
as: 'subscription',
|
|
137
137
|
});
|
|
138
|
+
|
|
139
|
+
this.belongsTo(models.MeterEvent, {
|
|
140
|
+
foreignKey: 'source',
|
|
141
|
+
as: 'meterEvent',
|
|
142
|
+
});
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
public static async getUsageSummary({
|
|
@@ -14,7 +14,7 @@ import type { LiteralUnion } from 'type-fest';
|
|
|
14
14
|
import { BN } from '@ocap/util';
|
|
15
15
|
import { createEvent } from '../../libs/audit';
|
|
16
16
|
import { createIdGenerator } from '../../libs/util';
|
|
17
|
-
import { GroupedBN, GroupedStrList, MeterEventPayload, MeterEventStatus } from './types';
|
|
17
|
+
import { GroupedBN, GroupedStrList, MeterEventPayload, MeterEventStatus, SourceData } from './types';
|
|
18
18
|
import { Customer } from './customer';
|
|
19
19
|
import { Subscription, type TSubscription } from './subscription';
|
|
20
20
|
import { Meter, type TMeter } from './meter';
|
|
@@ -43,6 +43,7 @@ export class MeterEvent extends Model<InferAttributes<MeterEvent>, InferCreation
|
|
|
43
43
|
declare credit_consumed: string; // 已消费的credit数量
|
|
44
44
|
declare credit_pending: string; // 待消费的credit数量(债务)
|
|
45
45
|
declare metadata?: Record<string, any>;
|
|
46
|
+
declare source_data?: SourceData;
|
|
46
47
|
|
|
47
48
|
// 审计字段
|
|
48
49
|
declare created_by?: string;
|
|
@@ -215,18 +216,27 @@ export class MeterEvent extends Model<InferAttributes<MeterEvent>, InferCreation
|
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
public static initialize(sequelize: any) {
|
|
218
|
-
this.init(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
hooks: {
|
|
226
|
-
afterCreate: (model: MeterEvent, options) =>
|
|
227
|
-
createEvent('MeterEvent', 'billing.meter_event.created', model, options).catch(console.error),
|
|
219
|
+
this.init(
|
|
220
|
+
{
|
|
221
|
+
...this.GENESIS_ATTRIBUTES,
|
|
222
|
+
source_data: {
|
|
223
|
+
type: DataTypes.JSON,
|
|
224
|
+
allowNull: true,
|
|
225
|
+
},
|
|
228
226
|
},
|
|
229
|
-
|
|
227
|
+
{
|
|
228
|
+
sequelize,
|
|
229
|
+
modelName: 'MeterEvent',
|
|
230
|
+
tableName: 'meter_events',
|
|
231
|
+
createdAt: 'created_at',
|
|
232
|
+
updatedAt: 'updated_at',
|
|
233
|
+
indexes: [{ fields: ['identifier'], unique: true }, { fields: ['status'] }, { fields: ['event_name'] }],
|
|
234
|
+
hooks: {
|
|
235
|
+
afterCreate: (model: MeterEvent, options) =>
|
|
236
|
+
createEvent('MeterEvent', 'billing.meter_event.created', model, options).catch(console.error),
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
);
|
|
230
240
|
}
|
|
231
241
|
|
|
232
242
|
// 批量处理未处理的事件
|
|
@@ -817,3 +817,21 @@ export type Restrictions = {
|
|
|
817
817
|
minimum_amount?: string;
|
|
818
818
|
minimum_amount_currency?: string;
|
|
819
819
|
};
|
|
820
|
+
|
|
821
|
+
export type LocalizedText = {
|
|
822
|
+
zh: string;
|
|
823
|
+
en: string;
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
export type SimpleSourceData = Record<string, string>;
|
|
827
|
+
|
|
828
|
+
export type StructuredSourceDataField = {
|
|
829
|
+
key: string;
|
|
830
|
+
label: string | LocalizedText;
|
|
831
|
+
value: string;
|
|
832
|
+
type?: 'text' | 'image' | 'url';
|
|
833
|
+
url?: string;
|
|
834
|
+
group?: string;
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
export type SourceData = SimpleSourceData | StructuredSourceDataField[];
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.14",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"@blocklet/error": "^0.2.5",
|
|
57
57
|
"@blocklet/js-sdk": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
58
58
|
"@blocklet/logger": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
59
|
-
"@blocklet/payment-react": "1.20.
|
|
60
|
-
"@blocklet/payment-vendor": "1.20.
|
|
59
|
+
"@blocklet/payment-react": "1.20.14",
|
|
60
|
+
"@blocklet/payment-vendor": "1.20.14",
|
|
61
61
|
"@blocklet/sdk": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
62
62
|
"@blocklet/ui-react": "^3.1.41",
|
|
63
63
|
"@blocklet/uploader": "^0.2.11",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"devDependencies": {
|
|
127
127
|
"@abtnode/types": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
128
128
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
129
|
-
"@blocklet/payment-types": "1.20.
|
|
129
|
+
"@blocklet/payment-types": "1.20.14",
|
|
130
130
|
"@types/cookie-parser": "^1.4.9",
|
|
131
131
|
"@types/cors": "^2.8.19",
|
|
132
132
|
"@types/debug": "^4.1.12",
|
|
@@ -173,5 +173,5 @@
|
|
|
173
173
|
"parser": "typescript"
|
|
174
174
|
}
|
|
175
175
|
},
|
|
176
|
-
"gitHead": "
|
|
176
|
+
"gitHead": "365b60721b7f3a372c472f774cda290d56dad365"
|
|
177
177
|
}
|
|
@@ -324,7 +324,7 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
324
324
|
)}
|
|
325
325
|
{creditTab === CreditTab.GRANTS && <CreditGrantsList customer_id={customerId} mode={mode} key={creditTab} />}
|
|
326
326
|
{creditTab === CreditTab.TRANSACTIONS && (
|
|
327
|
-
<CreditTransactionsList customer_id={customerId} mode={mode} key={creditTab} />
|
|
327
|
+
<CreditTransactionsList customer_id={customerId} mode={mode} key={creditTab} includeGrants />
|
|
328
328
|
)}
|
|
329
329
|
</Box>
|
|
330
330
|
{autoRecharge.open && (
|