payment-kit 1.20.12 → 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/crons/index.ts +8 -0
- package/api/src/libs/env.ts +1 -0
- package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +1 -1
- package/api/src/libs/vendor-util/adapters/types.ts +1 -0
- package/api/src/libs/vendor-util/fulfillment.ts +1 -1
- package/api/src/queues/vendors/fulfillment-coordinator.ts +1 -29
- package/api/src/queues/vendors/return-processor.ts +184 -0
- package/api/src/queues/vendors/return-scanner.ts +119 -0
- package/api/src/queues/vendors/status-check.ts +1 -1
- 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 +89 -2
- package/api/src/routes/webhook-endpoints.ts +4 -0
- package/api/src/store/migrations/20250918-add-vendor-extends.ts +20 -0
- package/api/src/store/migrations/20250919-add-source-data.ts +20 -0
- package/api/src/store/models/checkout-session.ts +5 -2
- 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/product-vendor.ts +6 -0
- 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
|
@@ -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
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
1
2
|
import { Router } from 'express';
|
|
2
3
|
import Joi from 'joi';
|
|
3
4
|
|
|
4
|
-
import { VendorAuth } from '@blocklet/payment-vendor';
|
|
5
|
+
import { Auth as VendorAuth, middleware } from '@blocklet/payment-vendor';
|
|
5
6
|
import { joinURL } from 'ufo';
|
|
6
7
|
import { MetadataSchema } from '../libs/api';
|
|
7
8
|
import { wallet } from '../libs/auth';
|
|
@@ -9,7 +10,8 @@ import dayjs from '../libs/dayjs';
|
|
|
9
10
|
import logger from '../libs/logger';
|
|
10
11
|
import { authenticate } from '../libs/security';
|
|
11
12
|
import { formatToShortUrl } from '../libs/url';
|
|
12
|
-
import {
|
|
13
|
+
import { getBlockletJson } from '../libs/util';
|
|
14
|
+
import { CheckoutSession, Invoice, Subscription } from '../store/models';
|
|
13
15
|
import { ProductVendor } from '../store/models/product-vendor';
|
|
14
16
|
|
|
15
17
|
const authAdmin = authenticate<CheckoutSession>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -159,6 +161,8 @@ async function createVendor(req: any, res: any) {
|
|
|
159
161
|
return res.status(400).json({ error: 'Vendor key already exists' });
|
|
160
162
|
}
|
|
161
163
|
|
|
164
|
+
const blockletJson = await getBlockletJson(appUrl);
|
|
165
|
+
|
|
162
166
|
const vendor = await ProductVendor.create({
|
|
163
167
|
vendor_key: vendorKey,
|
|
164
168
|
vendor_type: vendorType || 'launcher',
|
|
@@ -170,6 +174,10 @@ async function createVendor(req: any, res: any) {
|
|
|
170
174
|
app_pid: appPid,
|
|
171
175
|
app_logo: appLogo,
|
|
172
176
|
metadata: metadata || {},
|
|
177
|
+
extends: {
|
|
178
|
+
appId: blockletJson?.appId,
|
|
179
|
+
appPk: blockletJson?.appPk,
|
|
180
|
+
},
|
|
173
181
|
created_by: req.user?.did || 'admin',
|
|
174
182
|
});
|
|
175
183
|
|
|
@@ -210,6 +218,8 @@ async function updateVendor(req: any, res: any) {
|
|
|
210
218
|
app_logo: appLogo,
|
|
211
219
|
} = value;
|
|
212
220
|
|
|
221
|
+
const blockletJson = await getBlockletJson(appUrl);
|
|
222
|
+
|
|
213
223
|
if (req.body.vendorKey && req.body.vendorKey !== vendor.vendor_key) {
|
|
214
224
|
const existingVendor = await ProductVendor.findOne({
|
|
215
225
|
where: { vendor_key: req.body.vendorKey },
|
|
@@ -229,6 +239,10 @@ async function updateVendor(req: any, res: any) {
|
|
|
229
239
|
app_pid: appPid,
|
|
230
240
|
app_logo: appLogo,
|
|
231
241
|
vendor_key: req.body.vendor_key,
|
|
242
|
+
extends: {
|
|
243
|
+
appId: blockletJson?.appId,
|
|
244
|
+
appPk: blockletJson?.appPk,
|
|
245
|
+
},
|
|
232
246
|
};
|
|
233
247
|
|
|
234
248
|
await vendor.update(Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined)));
|
|
@@ -362,9 +376,23 @@ async function getVendorStatus(sessionId: string, isDetail = false) {
|
|
|
362
376
|
return getVendorStatusByVendorId(item.vendor_id, item.order_id, isDetail);
|
|
363
377
|
});
|
|
364
378
|
|
|
379
|
+
const subscriptionId = doc.subscription_id;
|
|
380
|
+
let shortSubscriptionUrl = '';
|
|
381
|
+
|
|
382
|
+
if (isDetail && subscriptionId) {
|
|
383
|
+
const subscriptionUrl = getUrl(`/customer/subscription/${subscriptionId}`);
|
|
384
|
+
|
|
385
|
+
shortSubscriptionUrl = await formatToShortUrl({
|
|
386
|
+
url: subscriptionUrl,
|
|
387
|
+
maxVisits: 5,
|
|
388
|
+
validUntil: dayjs().add(20, 'minutes').format('YYYY-MM-DDTHH:mm:ss+00:00'),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
365
392
|
return {
|
|
366
393
|
payment_status: doc.payment_status,
|
|
367
394
|
session_status: doc.status,
|
|
395
|
+
subscriptionUrl: shortSubscriptionUrl,
|
|
368
396
|
vendors: await Promise.all(vendors),
|
|
369
397
|
error: null,
|
|
370
398
|
};
|
|
@@ -443,12 +471,71 @@ async function redirectToVendor(req: any, res: any) {
|
|
|
443
471
|
}
|
|
444
472
|
}
|
|
445
473
|
|
|
474
|
+
async function getVendorSubscription(req: any, res: any) {
|
|
475
|
+
const { sessionId } = req.params;
|
|
476
|
+
|
|
477
|
+
const checkoutSession = await CheckoutSession.findByPk(sessionId);
|
|
478
|
+
|
|
479
|
+
if (!checkoutSession) {
|
|
480
|
+
return res.status(404).json({ error: 'Checkout session not found' });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const subscription = await Subscription.findByPk(checkoutSession.subscription_id);
|
|
484
|
+
|
|
485
|
+
if (!subscription) {
|
|
486
|
+
return res.status(404).json({ error: 'Subscription not found' });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const invoices = await Invoice.findAll({
|
|
490
|
+
where: { subscription_id: subscription.id },
|
|
491
|
+
order: [['created_at', 'DESC']],
|
|
492
|
+
attributes: [
|
|
493
|
+
'id',
|
|
494
|
+
'amount_due',
|
|
495
|
+
'amount_paid',
|
|
496
|
+
'amount_remaining',
|
|
497
|
+
'status',
|
|
498
|
+
'currency_id',
|
|
499
|
+
'period_start',
|
|
500
|
+
'period_end',
|
|
501
|
+
'created_at',
|
|
502
|
+
'due_date',
|
|
503
|
+
'description',
|
|
504
|
+
'invoice_pdf',
|
|
505
|
+
],
|
|
506
|
+
limit: 20,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
return res.json({
|
|
510
|
+
subscription: subscription.toJSON(),
|
|
511
|
+
billing_history: invoices,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function handleSubscriptionRedirect(req: any, res: any) {
|
|
516
|
+
const { sessionId } = req.params;
|
|
517
|
+
|
|
518
|
+
const checkoutSession = await CheckoutSession.findByPk(sessionId);
|
|
519
|
+
if (!checkoutSession) {
|
|
520
|
+
return res.status(404).json({ error: 'Checkout session not found' });
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return res.redirect(getUrl(`/customer/subscription/${checkoutSession.subscription_id}`));
|
|
524
|
+
}
|
|
525
|
+
|
|
446
526
|
const router = Router();
|
|
447
527
|
|
|
528
|
+
const ensureVendorAuth = middleware.ensureVendorAuth((vendorPk: string) =>
|
|
529
|
+
ProductVendor.findOne({ where: { 'extends.appPk': vendorPk } }).then((v) => v as any)
|
|
530
|
+
);
|
|
531
|
+
|
|
448
532
|
// FIXME: Authentication not yet added, awaiting implementation @Pengfei
|
|
449
533
|
router.get('/order/:sessionId/status', validateParams(sessionIdParamSchema), getVendorFulfillmentStatus);
|
|
450
534
|
router.get('/order/:sessionId/detail', validateParams(sessionIdParamSchema), getVendorFulfillmentDetail);
|
|
451
535
|
|
|
536
|
+
router.get('/subscription/:sessionId/redirect', handleSubscriptionRedirect);
|
|
537
|
+
router.get('/subscription/:sessionId', ensureVendorAuth, getVendorSubscription);
|
|
538
|
+
|
|
452
539
|
router.get(
|
|
453
540
|
'/open/:subscriptionId',
|
|
454
541
|
authAdmin,
|
|
@@ -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 { safeApplyColumnChanges, type Migration } from '../migrate';
|
|
2
|
+
|
|
3
|
+
export const up: Migration = async ({ context }) => {
|
|
4
|
+
// Add extends column to product_vendors table
|
|
5
|
+
await safeApplyColumnChanges(context, {
|
|
6
|
+
product_vendors: [
|
|
7
|
+
{
|
|
8
|
+
name: 'extends',
|
|
9
|
+
field: {
|
|
10
|
+
type: 'JSON',
|
|
11
|
+
allowNull: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const down: Migration = async ({ context }) => {
|
|
19
|
+
await context.removeColumn('product_vendors', 'extends');
|
|
20
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -212,7 +212,9 @@ export class CheckoutSession extends Model<InferAttributes<CheckoutSession>, Inf
|
|
|
212
212
|
| 'cancelled'
|
|
213
213
|
| 'max_retries_exceeded'
|
|
214
214
|
| 'return_requested'
|
|
215
|
-
| 'sent'
|
|
215
|
+
| 'sent'
|
|
216
|
+
| 'returning'
|
|
217
|
+
| 'returned',
|
|
216
218
|
string
|
|
217
219
|
>;
|
|
218
220
|
declare vendor_info?: Array<{
|
|
@@ -227,7 +229,8 @@ export class CheckoutSession extends Model<InferAttributes<CheckoutSession>, Inf
|
|
|
227
229
|
| 'cancelled'
|
|
228
230
|
| 'max_retries_exceeded'
|
|
229
231
|
| 'return_requested'
|
|
230
|
-
| 'sent'
|
|
232
|
+
| 'sent'
|
|
233
|
+
| 'returned';
|
|
231
234
|
service_url?: string;
|
|
232
235
|
app_url?: string;
|
|
233
236
|
error_message?: string;
|
|
@@ -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({
|