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.
Files changed (37) hide show
  1. package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +1 -1
  2. package/api/src/libs/vendor-util/adapters/types.ts +2 -3
  3. package/api/src/libs/vendor-util/fulfillment.ts +16 -30
  4. package/api/src/queues/vendors/commission.ts +32 -42
  5. package/api/src/queues/vendors/fulfillment-coordinator.ts +68 -60
  6. package/api/src/queues/vendors/fulfillment.ts +5 -5
  7. package/api/src/queues/vendors/return-processor.ts +0 -1
  8. package/api/src/queues/vendors/status-check.ts +2 -2
  9. package/api/src/routes/checkout-sessions.ts +15 -2
  10. package/api/src/routes/coupons.ts +7 -0
  11. package/api/src/routes/credit-grants.ts +8 -1
  12. package/api/src/routes/credit-transactions.ts +153 -13
  13. package/api/src/routes/invoices.ts +35 -1
  14. package/api/src/routes/meter-events.ts +31 -3
  15. package/api/src/routes/meters.ts +4 -0
  16. package/api/src/routes/payment-currencies.ts +2 -1
  17. package/api/src/routes/promotion-codes.ts +2 -2
  18. package/api/src/routes/subscription-items.ts +4 -0
  19. package/api/src/routes/vendor.ts +13 -4
  20. package/api/src/routes/webhook-endpoints.ts +4 -0
  21. package/api/src/store/migrations/20250919-add-source-data.ts +20 -0
  22. package/api/src/store/models/checkout-session.ts +23 -0
  23. package/api/src/store/models/credit-transaction.ts +5 -0
  24. package/api/src/store/models/meter-event.ts +22 -12
  25. package/api/src/store/models/types.ts +18 -0
  26. package/blocklet.yml +1 -1
  27. package/package.json +5 -5
  28. package/src/components/customer/credit-overview.tsx +1 -1
  29. package/src/components/customer/related-credit-grants.tsx +194 -0
  30. package/src/components/meter/add-usage-dialog.tsx +8 -0
  31. package/src/components/meter/events-list.tsx +93 -96
  32. package/src/components/product/form.tsx +0 -1
  33. package/src/locales/en.tsx +9 -0
  34. package/src/locales/zh.tsx +9 -0
  35. package/src/pages/admin/billing/invoices/detail.tsx +21 -2
  36. package/src/pages/customer/invoice/detail.tsx +11 -2
  37. 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
- paymentIntentId: string;
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, paymentIntentId, vendorId, vendorConfig } = job;
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
- payment_intent_id: checkoutSession.payment_intent_id || '',
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, paymentIntentId, vendorId, 'sent', {
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, paymentIntentId, vendorId, 'failed', {
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
- paymentIntentId: checkoutSession?.payment_intent_id || '',
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
- paymentIntentId: checkoutSession?.payment_intent_id || '',
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(checkoutSession: CheckoutSession) {
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
- // @ts-ignore
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
- await creditGrant.update({ metadata: req.body.metadata });
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 { CreditTransaction, Customer, CreditGrant, Meter, Subscription, PaymentCurrency } from '../store/models';
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
- return {
114
- ...item.toJSON(),
115
- // @ts-ignore
116
- paymentCurrency: paymentCurrencies.find((x) => x.id === item.meter?.currency_id),
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({ count, list: result, paging: { page, pageSize } });
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 { CheckoutSession, PaymentLink, TInvoiceExpanded, Discount, Coupon, PromotionCode } from '../store/models';
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.created_at = {};
102
+ where.timestamp = {};
76
103
  if (query.start) {
77
- where.created_at[Op.gte] = new Date(query.start * 1000);
104
+ where.timestamp[Op.gte] = Number(query.start);
78
105
  }
79
106
  if (query.end) {
80
- where.created_at[Op.lte] = new Date(query.end * 1000);
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
 
@@ -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: Joi.object().optional(),
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: Joi.object().optional(),
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
 
@@ -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: doc.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: doc.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: doc.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>;