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.
@@ -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
 
@@ -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(this.GENESIS_ATTRIBUTES, {
219
- sequelize,
220
- modelName: 'MeterEvent',
221
- tableName: 'meter_events',
222
- createdAt: 'created_at',
223
- updatedAt: 'updated_at',
224
- indexes: [{ fields: ['identifier'], unique: true }, { fields: ['status'] }, { fields: ['event_name'] }],
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
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.20.13
17
+ version: 1.20.14
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.20.13",
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.13",
60
- "@blocklet/payment-vendor": "1.20.13",
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.13",
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": "0cbe918549f7d06561b5307fb947bfbbd2250984"
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 && (