payment-kit 1.20.13 → 1.20.15
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/vendor-util/adapters/launcher-adapter.ts +1 -1
- package/api/src/libs/vendor-util/adapters/types.ts +2 -3
- package/api/src/libs/vendor-util/fulfillment.ts +16 -30
- package/api/src/queues/vendors/commission.ts +32 -42
- package/api/src/queues/vendors/fulfillment-coordinator.ts +68 -60
- package/api/src/queues/vendors/fulfillment.ts +5 -5
- package/api/src/queues/vendors/return-processor.ts +0 -1
- package/api/src/queues/vendors/status-check.ts +2 -2
- 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/vendor.ts +13 -4
- 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/checkout-session.ts +23 -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
- package/doc/vendor_fulfillment_system.md +0 -929
|
@@ -7,7 +7,7 @@ import { updateVendorFulfillmentStatus } from './fulfillment-coordinator';
|
|
|
7
7
|
|
|
8
8
|
type VendorFulfillmentJob = {
|
|
9
9
|
checkoutSessionId: string;
|
|
10
|
-
|
|
10
|
+
invoiceId: string;
|
|
11
11
|
vendorId: string;
|
|
12
12
|
vendorConfig: any;
|
|
13
13
|
retryOnError?: boolean;
|
|
@@ -19,7 +19,7 @@ export const handleVendorFulfillment = async (job: VendorFulfillmentJob) => {
|
|
|
19
19
|
jobKeys: Object.keys(job),
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
const { checkoutSessionId,
|
|
22
|
+
const { checkoutSessionId, invoiceId, vendorId, vendorConfig } = job;
|
|
23
23
|
|
|
24
24
|
try {
|
|
25
25
|
const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
|
|
@@ -31,7 +31,7 @@ export const handleVendorFulfillment = async (job: VendorFulfillmentJob) => {
|
|
|
31
31
|
checkoutSessionId,
|
|
32
32
|
amount_total: checkoutSession.amount_total,
|
|
33
33
|
customer_id: checkoutSession.customer_id || '',
|
|
34
|
-
|
|
34
|
+
invoiceId,
|
|
35
35
|
currency_id: checkoutSession.currency_id,
|
|
36
36
|
customer_did: checkoutSession.customer_did || '',
|
|
37
37
|
};
|
|
@@ -43,7 +43,7 @@ export const handleVendorFulfillment = async (job: VendorFulfillmentJob) => {
|
|
|
43
43
|
status: fulfillmentResult.status,
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
await updateVendorFulfillmentStatus(checkoutSessionId,
|
|
46
|
+
await updateVendorFulfillmentStatus(checkoutSessionId, invoiceId, vendorId, 'sent', {
|
|
47
47
|
orderId: fulfillmentResult.orderId,
|
|
48
48
|
commissionAmount: fulfillmentResult.commissionAmount,
|
|
49
49
|
serviceUrl: fulfillmentResult.serviceUrl,
|
|
@@ -55,7 +55,7 @@ export const handleVendorFulfillment = async (job: VendorFulfillmentJob) => {
|
|
|
55
55
|
error,
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
await updateVendorFulfillmentStatus(checkoutSessionId,
|
|
58
|
+
await updateVendorFulfillmentStatus(checkoutSessionId, invoiceId, vendorId, 'failed', {
|
|
59
59
|
lastError: error.message,
|
|
60
60
|
});
|
|
61
61
|
|
|
@@ -157,7 +157,6 @@ async function callVendorReturn(
|
|
|
157
157
|
const returnResult = await vendorAdapter.requestReturn({
|
|
158
158
|
orderId: vendor.order_id,
|
|
159
159
|
reason: 'Subscription canceled',
|
|
160
|
-
paymentIntentId: checkoutSession.payment_intent_id || '',
|
|
161
160
|
customParams: {
|
|
162
161
|
checkoutSessionId: checkoutSession.id,
|
|
163
162
|
subscriptionId: checkoutSession.subscription_id,
|
|
@@ -83,7 +83,7 @@ export const handleVendorStatusCheck = async (job: VendorStatusCheckJob) => {
|
|
|
83
83
|
id: `fulfillment-coordinator-${checkoutSessionId}-${vendorId}`,
|
|
84
84
|
job: {
|
|
85
85
|
checkoutSessionId,
|
|
86
|
-
|
|
86
|
+
invoiceId: checkoutSession?.invoice_id || '',
|
|
87
87
|
triggeredBy: 'vendor-status-check-timeout',
|
|
88
88
|
},
|
|
89
89
|
});
|
|
@@ -158,7 +158,7 @@ export const handleVendorStatusCheck = async (job: VendorStatusCheckJob) => {
|
|
|
158
158
|
id: `fulfillment-coordinator-${checkoutSessionId}-${vendorId}`,
|
|
159
159
|
job: {
|
|
160
160
|
checkoutSessionId,
|
|
161
|
-
|
|
161
|
+
invoiceId: checkoutSession?.invoice_id || '',
|
|
162
162
|
triggeredBy: 'vendor-status-check',
|
|
163
163
|
},
|
|
164
164
|
});
|
|
@@ -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
|
|
package/api/src/routes/vendor.ts
CHANGED
|
@@ -355,9 +355,12 @@ async function getVendorStatus(sessionId: string, isDetail = false) {
|
|
|
355
355
|
};
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
// FIXME: will remove payment_status @pengfei
|
|
359
|
+
const paymentStatus = doc.status === 'complete' ? 'paid' : 'unpaid';
|
|
360
|
+
|
|
358
361
|
if (doc.status !== 'complete') {
|
|
359
362
|
return {
|
|
360
|
-
payment_status:
|
|
363
|
+
payment_status: paymentStatus,
|
|
361
364
|
session_status: doc.status,
|
|
362
365
|
error: 'CheckoutSession not complete',
|
|
363
366
|
vendors: [],
|
|
@@ -365,7 +368,7 @@ async function getVendorStatus(sessionId: string, isDetail = false) {
|
|
|
365
368
|
}
|
|
366
369
|
if (!doc.vendor_info) {
|
|
367
370
|
return {
|
|
368
|
-
payment_status:
|
|
371
|
+
payment_status: paymentStatus,
|
|
369
372
|
session_status: doc.status,
|
|
370
373
|
error: 'Vendor info not found',
|
|
371
374
|
vendors: [],
|
|
@@ -373,7 +376,13 @@ async function getVendorStatus(sessionId: string, isDetail = false) {
|
|
|
373
376
|
}
|
|
374
377
|
|
|
375
378
|
const vendors = doc.vendor_info.map((item) => {
|
|
376
|
-
return getVendorStatusByVendorId(item.vendor_id, item.order_id, isDetail)
|
|
379
|
+
return getVendorStatusByVendorId(item.vendor_id, item.order_id, isDetail).then((status) => {
|
|
380
|
+
return {
|
|
381
|
+
error_message: item.error_message,
|
|
382
|
+
status: item.status,
|
|
383
|
+
...status,
|
|
384
|
+
};
|
|
385
|
+
});
|
|
377
386
|
});
|
|
378
387
|
|
|
379
388
|
const subscriptionId = doc.subscription_id;
|
|
@@ -390,7 +399,7 @@ async function getVendorStatus(sessionId: string, isDetail = false) {
|
|
|
390
399
|
}
|
|
391
400
|
|
|
392
401
|
return {
|
|
393
|
-
payment_status:
|
|
402
|
+
payment_status: paymentStatus,
|
|
394
403
|
session_status: doc.status,
|
|
395
404
|
subscriptionUrl: shortSubscriptionUrl,
|
|
396
405
|
vendors: await Promise.all(vendors),
|
|
@@ -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
|
+
};
|
|
@@ -623,6 +623,29 @@ export class CheckoutSession extends Model<InferAttributes<CheckoutSession>, Inf
|
|
|
623
623
|
return null;
|
|
624
624
|
}
|
|
625
625
|
}
|
|
626
|
+
|
|
627
|
+
public static async findByInvoiceId(invoiceId: string): Promise<CheckoutSession | null> {
|
|
628
|
+
try {
|
|
629
|
+
const invoice = await Invoice.findByPk(invoiceId);
|
|
630
|
+
if (!invoice) {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (invoice.checkout_session_id) {
|
|
635
|
+
return await CheckoutSession.findByPk(invoice.checkout_session_id);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (!invoice.subscription_id) {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Find original CheckoutSession through subscription
|
|
643
|
+
const checkoutSession = await CheckoutSession.findBySubscriptionId(invoice.subscription_id);
|
|
644
|
+
return checkoutSession;
|
|
645
|
+
} catch (error: any) {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
626
649
|
}
|
|
627
650
|
|
|
628
651
|
export type TCheckoutSession = InferAttributes<CheckoutSession>;
|