payment-kit 1.19.0 → 1.19.1

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 (133) hide show
  1. package/api/src/crons/index.ts +8 -0
  2. package/api/src/index.ts +4 -0
  3. package/api/src/libs/credit-grant.ts +146 -0
  4. package/api/src/libs/env.ts +1 -0
  5. package/api/src/libs/invoice.ts +4 -3
  6. package/api/src/libs/notification/template/base.ts +388 -2
  7. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +149 -0
  8. package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +151 -0
  9. package/api/src/libs/notification/template/customer-credit-insufficient.ts +254 -0
  10. package/api/src/libs/notification/template/subscription-canceled.ts +193 -202
  11. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +215 -237
  12. package/api/src/libs/notification/template/subscription-renewed.ts +130 -200
  13. package/api/src/libs/notification/template/subscription-succeeded.ts +100 -202
  14. package/api/src/libs/notification/template/subscription-trial-start.ts +142 -188
  15. package/api/src/libs/notification/template/subscription-trial-will-end.ts +146 -174
  16. package/api/src/libs/notification/template/subscription-upgraded.ts +96 -192
  17. package/api/src/libs/notification/template/subscription-will-canceled.ts +94 -135
  18. package/api/src/libs/notification/template/subscription-will-renew.ts +220 -245
  19. package/api/src/libs/payment.ts +69 -0
  20. package/api/src/libs/queue/index.ts +3 -2
  21. package/api/src/libs/session.ts +8 -0
  22. package/api/src/libs/subscription.ts +74 -3
  23. package/api/src/libs/ws.ts +23 -1
  24. package/api/src/locales/en.ts +33 -0
  25. package/api/src/locales/zh.ts +31 -0
  26. package/api/src/queues/credit-consume.ts +715 -0
  27. package/api/src/queues/credit-grant.ts +572 -0
  28. package/api/src/queues/notification.ts +173 -128
  29. package/api/src/queues/payment.ts +210 -122
  30. package/api/src/queues/subscription.ts +179 -0
  31. package/api/src/routes/checkout-sessions.ts +157 -9
  32. package/api/src/routes/connect/shared.ts +3 -2
  33. package/api/src/routes/credit-grants.ts +241 -0
  34. package/api/src/routes/credit-transactions.ts +208 -0
  35. package/api/src/routes/index.ts +8 -0
  36. package/api/src/routes/meter-events.ts +347 -0
  37. package/api/src/routes/meters.ts +219 -0
  38. package/api/src/routes/payment-currencies.ts +14 -2
  39. package/api/src/routes/payment-links.ts +1 -1
  40. package/api/src/routes/payment-methods.ts +14 -2
  41. package/api/src/routes/prices.ts +43 -0
  42. package/api/src/routes/pricing-table.ts +13 -7
  43. package/api/src/routes/products.ts +63 -4
  44. package/api/src/routes/settings.ts +1 -1
  45. package/api/src/routes/subscriptions.ts +4 -0
  46. package/api/src/store/migrations/20250610-billing-credit.ts +43 -0
  47. package/api/src/store/models/credit-grant.ts +486 -0
  48. package/api/src/store/models/credit-transaction.ts +268 -0
  49. package/api/src/store/models/customer.ts +8 -0
  50. package/api/src/store/models/index.ts +52 -1
  51. package/api/src/store/models/meter-event.ts +423 -0
  52. package/api/src/store/models/meter.ts +176 -0
  53. package/api/src/store/models/payment-currency.ts +66 -14
  54. package/api/src/store/models/price.ts +6 -0
  55. package/api/src/store/models/product.ts +2 -2
  56. package/api/src/store/models/subscription.ts +24 -0
  57. package/api/src/store/models/types.ts +28 -2
  58. package/api/tests/libs/subscription.spec.ts +53 -0
  59. package/blocklet.yml +9 -1
  60. package/package.json +4 -4
  61. package/scripts/sdk.js +233 -1
  62. package/src/app.tsx +10 -0
  63. package/src/components/collapse.tsx +11 -1
  64. package/src/components/customer/credit-grant-item-list.tsx +99 -0
  65. package/src/components/customer/credit-overview.tsx +233 -0
  66. package/src/components/customer/form.tsx +5 -2
  67. package/src/components/invoice/list.tsx +19 -1
  68. package/src/components/metadata/form.tsx +286 -90
  69. package/src/components/meter/actions.tsx +101 -0
  70. package/src/components/meter/add-usage-dialog.tsx +239 -0
  71. package/src/components/meter/events-list.tsx +657 -0
  72. package/src/components/meter/form.tsx +245 -0
  73. package/src/components/meter/products.tsx +264 -0
  74. package/src/components/meter/usage-guide.tsx +174 -0
  75. package/src/components/payment-currency/form.tsx +2 -0
  76. package/src/components/payment-intent/list.tsx +19 -1
  77. package/src/components/payment-link/preview.tsx +1 -1
  78. package/src/components/payment-link/product-select.tsx +52 -12
  79. package/src/components/payment-method/arcblock.tsx +2 -0
  80. package/src/components/payment-method/base.tsx +2 -0
  81. package/src/components/payment-method/bitcoin.tsx +2 -0
  82. package/src/components/payment-method/ethereum.tsx +2 -0
  83. package/src/components/payment-method/stripe.tsx +2 -0
  84. package/src/components/payouts/list.tsx +19 -1
  85. package/src/components/price/currency-select.tsx +51 -31
  86. package/src/components/price/form.tsx +881 -407
  87. package/src/components/pricing-table/preview.tsx +1 -1
  88. package/src/components/product/add-price.tsx +9 -7
  89. package/src/components/product/create.tsx +7 -4
  90. package/src/components/product/edit-price.tsx +21 -12
  91. package/src/components/product/features.tsx +17 -7
  92. package/src/components/product/form.tsx +104 -89
  93. package/src/components/refund/list.tsx +19 -1
  94. package/src/components/section/header.tsx +5 -18
  95. package/src/components/subscription/items/index.tsx +1 -1
  96. package/src/components/subscription/metrics.tsx +37 -5
  97. package/src/components/subscription/portal/actions.tsx +2 -1
  98. package/src/contexts/products.tsx +26 -9
  99. package/src/hooks/subscription.ts +34 -0
  100. package/src/libs/meter-utils.ts +196 -0
  101. package/src/libs/util.ts +4 -0
  102. package/src/locales/en.tsx +385 -4
  103. package/src/locales/zh.tsx +364 -0
  104. package/src/pages/admin/billing/index.tsx +61 -33
  105. package/src/pages/admin/billing/invoices/detail.tsx +1 -1
  106. package/src/pages/admin/billing/meters/create.tsx +60 -0
  107. package/src/pages/admin/billing/meters/detail.tsx +435 -0
  108. package/src/pages/admin/billing/meters/index.tsx +210 -0
  109. package/src/pages/admin/billing/meters/meter-event.tsx +346 -0
  110. package/src/pages/admin/billing/subscriptions/detail.tsx +47 -14
  111. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +391 -0
  112. package/src/pages/admin/customers/customers/detail.tsx +22 -10
  113. package/src/pages/admin/customers/index.tsx +5 -0
  114. package/src/pages/admin/developers/events/detail.tsx +1 -1
  115. package/src/pages/admin/developers/index.tsx +1 -1
  116. package/src/pages/admin/payments/intents/detail.tsx +1 -1
  117. package/src/pages/admin/payments/payouts/detail.tsx +1 -1
  118. package/src/pages/admin/payments/refunds/detail.tsx +1 -1
  119. package/src/pages/admin/products/index.tsx +3 -2
  120. package/src/pages/admin/products/links/detail.tsx +1 -1
  121. package/src/pages/admin/products/prices/actions.tsx +16 -4
  122. package/src/pages/admin/products/prices/detail.tsx +30 -3
  123. package/src/pages/admin/products/prices/list.tsx +8 -1
  124. package/src/pages/admin/products/pricing-tables/detail.tsx +1 -1
  125. package/src/pages/admin/products/products/create.tsx +233 -57
  126. package/src/pages/admin/products/products/detail.tsx +2 -1
  127. package/src/pages/admin/settings/payment-methods/index.tsx +3 -0
  128. package/src/pages/customer/credit-grant/detail.tsx +308 -0
  129. package/src/pages/customer/index.tsx +35 -2
  130. package/src/pages/customer/recharge/account.tsx +5 -5
  131. package/src/pages/customer/subscription/change-payment.tsx +4 -2
  132. package/src/pages/customer/subscription/detail.tsx +48 -14
  133. package/src/pages/customer/subscription/embed.tsx +1 -1
@@ -0,0 +1,486 @@
1
+ /* eslint-disable @typescript-eslint/lines-between-class-members */
2
+ import { BN } from '@ocap/util';
3
+ import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, literal, Model, Op } from 'sequelize';
4
+ import type { LiteralUnion } from 'type-fest';
5
+
6
+ import { createEvent } from '../../libs/audit';
7
+ import { createIdGenerator } from '../../libs/util';
8
+ import dayjs from '../../libs/dayjs';
9
+ import { CreditGrantApplicabilityConfig } from './types';
10
+ import logger from '../../libs/logger';
11
+ import { PaymentCurrency, TPaymentCurrency } from './payment-currency';
12
+
13
+ const CREDIT_GRANT_STATUS_EVENTS = {
14
+ depleted: 'depleted',
15
+ expired: 'expired',
16
+ voided: 'voided',
17
+ };
18
+
19
+ type CreditGrantSummary = {
20
+ paymentCurrency: TPaymentCurrency;
21
+ totalAmount: string;
22
+ remainingAmount: string;
23
+ grantCount: number;
24
+ };
25
+
26
+ // 创建状态变化事件
27
+ async function createCreditGrantStatusEvent(model: CreditGrant, options: any): Promise<void> {
28
+ if (model.changed('status')) {
29
+ const previousStatus = model.previous('status');
30
+ const currentStatus = model.status;
31
+ if (previousStatus === 'pending' && currentStatus === 'granted') {
32
+ await createEvent('CreditGrant', 'customer.credit_grant.granted', model, options);
33
+ return;
34
+ }
35
+ const statusEvent = CREDIT_GRANT_STATUS_EVENTS[currentStatus as keyof typeof CREDIT_GRANT_STATUS_EVENTS];
36
+ if (statusEvent) {
37
+ await createEvent('CreditGrant', `customer.credit_grant.${statusEvent}`, model, options);
38
+ }
39
+ }
40
+ }
41
+
42
+ export const nextCreditGrantId = createIdGenerator('credgr', 14);
43
+
44
+ export class CreditGrant extends Model<InferAttributes<CreditGrant>, InferCreationAttributes<CreditGrant>> {
45
+ declare id: CreationOptional<string>;
46
+ declare object: CreationOptional<string>;
47
+ declare amount: string;
48
+ declare currency_id: string;
49
+ declare applicability_config?: CreditGrantApplicabilityConfig;
50
+ declare category: LiteralUnion<'paid' | 'promotional', string>;
51
+ declare customer_id: string;
52
+ declare effective_at?: number;
53
+ declare expires_at?: number;
54
+ declare livemode: boolean;
55
+ declare metadata?: Record<string, any>;
56
+ declare name?: string;
57
+ declare priority: number; // 0-100, 0为最高优先级
58
+ declare test_clock?: string;
59
+ declare voided_at?: number;
60
+
61
+ // 状态相关字段
62
+ declare status: LiteralUnion<'pending' | 'granted' | 'depleted' | 'expired' | 'voided', string>;
63
+ declare remaining_amount: string; // 剩余金额
64
+
65
+ // 审计字段
66
+ declare created_by?: string;
67
+ declare updated_by?: string;
68
+ declare created_via: LiteralUnion<'api' | 'dashboard' | 'portal', string>;
69
+
70
+ declare created_at: CreationOptional<Date>;
71
+ declare updated_at: CreationOptional<Date>;
72
+
73
+ public static readonly GENESIS_ATTRIBUTES = {
74
+ id: {
75
+ type: DataTypes.STRING(18),
76
+ primaryKey: true,
77
+ allowNull: false,
78
+ defaultValue: nextCreditGrantId,
79
+ },
80
+ object: {
81
+ type: DataTypes.STRING(32),
82
+ defaultValue: 'credit_grant',
83
+ allowNull: false,
84
+ },
85
+ amount: {
86
+ type: DataTypes.STRING(32),
87
+ allowNull: false,
88
+ },
89
+ currency_id: {
90
+ type: DataTypes.STRING(15),
91
+ allowNull: false,
92
+ },
93
+ applicability_config: {
94
+ type: DataTypes.JSON,
95
+ allowNull: true,
96
+ },
97
+ category: {
98
+ type: DataTypes.ENUM('paid', 'promotional'),
99
+ allowNull: false,
100
+ },
101
+ customer_id: {
102
+ type: DataTypes.STRING(18),
103
+ allowNull: false,
104
+ },
105
+ effective_at: {
106
+ type: DataTypes.INTEGER,
107
+ allowNull: true,
108
+ },
109
+ expires_at: {
110
+ type: DataTypes.INTEGER,
111
+ allowNull: true,
112
+ },
113
+ livemode: {
114
+ type: DataTypes.BOOLEAN,
115
+ allowNull: false,
116
+ },
117
+ metadata: {
118
+ type: DataTypes.JSON,
119
+ allowNull: true,
120
+ },
121
+ name: {
122
+ type: DataTypes.STRING(255),
123
+ allowNull: true,
124
+ },
125
+ priority: {
126
+ type: DataTypes.INTEGER,
127
+ defaultValue: 50,
128
+ allowNull: false,
129
+ validate: {
130
+ min: 0,
131
+ max: 100,
132
+ },
133
+ },
134
+ test_clock: {
135
+ type: DataTypes.STRING(32),
136
+ allowNull: true,
137
+ },
138
+ voided_at: {
139
+ type: DataTypes.INTEGER,
140
+ allowNull: true,
141
+ },
142
+ status: {
143
+ type: DataTypes.ENUM('pending', 'granted', 'depleted', 'expired', 'voided'),
144
+ defaultValue: 'granted',
145
+ allowNull: false,
146
+ },
147
+ remaining_amount: {
148
+ type: DataTypes.STRING(32),
149
+ allowNull: false,
150
+ },
151
+ created_by: {
152
+ type: DataTypes.STRING(40),
153
+ allowNull: true,
154
+ },
155
+ updated_by: {
156
+ type: DataTypes.STRING(40),
157
+ allowNull: true,
158
+ },
159
+ created_via: {
160
+ type: DataTypes.ENUM('api', 'dashboard', 'portal'),
161
+ allowNull: false,
162
+ },
163
+ created_at: {
164
+ type: DataTypes.DATE,
165
+ defaultValue: DataTypes.NOW,
166
+ allowNull: false,
167
+ },
168
+ updated_at: {
169
+ type: DataTypes.DATE,
170
+ defaultValue: DataTypes.NOW,
171
+ allowNull: false,
172
+ },
173
+ };
174
+
175
+ // check if Credit Grant is available
176
+ public isAvailable(): boolean {
177
+ const now = dayjs().unix();
178
+
179
+ if (this.status !== 'granted') {
180
+ return false;
181
+ }
182
+
183
+ if (this.effective_at && this.effective_at > now) {
184
+ return false;
185
+ }
186
+
187
+ if (this.expires_at && this.expires_at <= now) {
188
+ return false;
189
+ }
190
+
191
+ if (new BN(this.remaining_amount).lte(new BN(0))) {
192
+ return false;
193
+ }
194
+
195
+ return true;
196
+ }
197
+
198
+ // consume credit
199
+ public async consumeCredit(
200
+ amount: string,
201
+ context: {
202
+ subscription_id?: string;
203
+ meter_event_id?: string;
204
+ },
205
+ dryRun: boolean = false
206
+ ): Promise<{
207
+ consumed: string;
208
+ remaining: string;
209
+ depleted: boolean;
210
+ }> {
211
+ const requestedAmount = new BN(amount);
212
+ const availableAmount = new BN(this.remaining_amount);
213
+
214
+ logger.debug('Consume credit in model', {
215
+ requestedAmount: requestedAmount.toString(),
216
+ availableAmount: availableAmount.toString(),
217
+ grantId: this.id,
218
+ });
219
+
220
+ if (requestedAmount.lte(new BN(0))) {
221
+ throw new Error('Amount must be greater than 0');
222
+ }
223
+
224
+ if (!this.isAvailable()) {
225
+ throw new Error('Credit Grant is not available for consumption');
226
+ }
227
+
228
+ // calculate actual consumable amount
229
+ const consumedAmount = requestedAmount.lte(availableAmount) ? requestedAmount : availableAmount;
230
+ const newRemainingAmount = availableAmount.sub(consumedAmount);
231
+ const isDepleted = newRemainingAmount.lte(new BN(0));
232
+
233
+ if (!dryRun) {
234
+ // update remaining amount
235
+ this.remaining_amount = newRemainingAmount.toString();
236
+
237
+ // if depleted, update status
238
+ if (isDepleted) {
239
+ this.status = 'depleted';
240
+ }
241
+
242
+ await this.save();
243
+
244
+ await createEvent('CreditGrant', 'customer.credit_grant.consumed', this).catch(console.error);
245
+
246
+ // check low balance warning
247
+ const originalAmount = new BN(this.amount);
248
+ const threshold = originalAmount.mul(new BN(10)).div(new BN(100)); // 10%
249
+ if (newRemainingAmount.gt(new BN(0)) && newRemainingAmount.lte(threshold)) {
250
+ await createEvent('CreditGrant', 'customer.credit_grant.low_balance', this, {
251
+ metadata: context,
252
+ }).catch(console.error);
253
+ }
254
+ }
255
+
256
+ return {
257
+ consumed: consumedAmount.toString(),
258
+ remaining: newRemainingAmount.toString(),
259
+ depleted: isDepleted,
260
+ };
261
+ }
262
+
263
+ // check if Credit Grant is applicable to specified price
264
+ public isApplicableToPrice(priceId: string): boolean {
265
+ if (!this.applicability_config) {
266
+ return true; // no limit config, default to applicable to all
267
+ }
268
+
269
+ const { scope } = this.applicability_config;
270
+
271
+ // if specified price list, check if in the list
272
+ if (scope.prices && Array.isArray(scope.prices) && scope.prices.length > 0) {
273
+ return scope.prices.includes(priceId);
274
+ }
275
+
276
+ return true;
277
+ }
278
+
279
+ public static initialize(sequelize: any) {
280
+ this.init(this.GENESIS_ATTRIBUTES, {
281
+ sequelize,
282
+ modelName: 'CreditGrant',
283
+ tableName: 'credit_grants',
284
+ createdAt: 'created_at',
285
+ updatedAt: 'updated_at',
286
+ indexes: [
287
+ { fields: ['status'], name: 'idx_credit_grant_status' },
288
+ { fields: ['effective_at'], name: 'idx_credit_grant_effective_at' },
289
+ { fields: ['expires_at'], name: 'idx_credit_grant_expires_at' },
290
+ ],
291
+ hooks: {
292
+ afterCreate: (model: CreditGrant, options) => {
293
+ createEvent('CreditGrant', 'customer.credit_grant.created', model, options).catch(console.error);
294
+ if (!model.effective_at || model.effective_at <= Math.floor(Date.now() / 1000)) {
295
+ createEvent('CreditGrant', 'customer.credit_grant.granted', model, options).catch(console.error);
296
+ }
297
+ },
298
+ afterUpdate: (model: CreditGrant, options) => {
299
+ createEvent('CreditGrant', 'customer.credit_grant.updated', model, options).catch(console.error);
300
+
301
+ if (model.changed('status')) {
302
+ createCreditGrantStatusEvent(model, options).catch(console.error);
303
+ }
304
+ },
305
+ afterDestroy: (model: CreditGrant, options) =>
306
+ createEvent('CreditGrant', 'customer.credit_grant.deleted', model, options).catch(console.error),
307
+ },
308
+ });
309
+ }
310
+
311
+ public static associate(models: any) {
312
+ this.belongsTo(models.Customer, {
313
+ foreignKey: 'customer_id',
314
+ as: 'customer',
315
+ });
316
+
317
+ this.hasOne(models.PaymentCurrency, {
318
+ foreignKey: 'id',
319
+ sourceKey: 'currency_id',
320
+ as: 'paymentCurrency',
321
+ });
322
+
323
+ this.hasMany(models.CreditTransaction, {
324
+ foreignKey: 'credit_grant_id',
325
+ as: 'transactions',
326
+ });
327
+ }
328
+
329
+ // get available Credit Grants for customer (sorted by priority)
330
+ public static async getAvailableCreditsForCustomer(
331
+ customerId: string,
332
+ currencyId: string,
333
+ priceIds?: string[] | string
334
+ ): Promise<CreditGrant[]> {
335
+ const now = dayjs().unix();
336
+
337
+ const whereClause: any = {
338
+ customer_id: customerId,
339
+ currency_id: currencyId,
340
+ status: 'granted',
341
+ remaining_amount: { [Op.gt]: '0' },
342
+ [Op.and]: [
343
+ // effective_at check: null表示立即生效,或者已经生效
344
+ {
345
+ [Op.or]: [{ effective_at: null }, { effective_at: { [Op.lte]: now } }],
346
+ },
347
+ // expires_at check: null表示永不过期,或者还未过期
348
+ {
349
+ [Op.or]: [{ expires_at: null }, { expires_at: { [Op.gt]: now } }],
350
+ },
351
+ ],
352
+ };
353
+
354
+ const grants = await this.findAll({
355
+ where: whereClause,
356
+ order: [
357
+ // 1. 先应用较早过期的 credit grants (expires_at 为 null 视为永不过期,排在后面)
358
+ [literal('CASE WHEN expires_at IS NULL THEN 1 ELSE 0 END'), 'ASC'],
359
+ ['expires_at', 'ASC'],
360
+ // 2. 然后是 promotional 类型的 credit grants
361
+ [literal("CASE WHEN category = 'promotional' THEN 0 ELSE 1 END"), 'ASC'],
362
+ // 3. 再是较早生效的 credit grants (effective_at 为 null 视为立即生效,排在前面)
363
+ [literal('CASE WHEN effective_at IS NULL THEN 0 ELSE 1 END'), 'ASC'],
364
+ ['effective_at', 'ASC'],
365
+ // 4. 最后是较早创建的 credit grants
366
+ ['created_at', 'ASC'],
367
+ ],
368
+ });
369
+
370
+ if (!grants || grants.length === 0) {
371
+ return [];
372
+ }
373
+
374
+ const existPrice = Array.isArray(priceIds) ? priceIds.length > 0 : priceIds;
375
+ // if specified price id, further filter applicable grants
376
+ if (existPrice && priceIds) {
377
+ const priceIdList = Array.isArray(priceIds) ? priceIds : [priceIds];
378
+ return grants.filter((grant) => priceIdList.some((priceId) => grant.isApplicableToPrice(priceId)));
379
+ }
380
+
381
+ return grants;
382
+ }
383
+
384
+ // batch set expired
385
+ public static async expireCredits(grantIds: string[]): Promise<number> {
386
+ const now = dayjs().unix();
387
+
388
+ const [affectedCount] = await this.update(
389
+ {
390
+ status: 'expired',
391
+ expires_at: now,
392
+ },
393
+ {
394
+ where: {
395
+ id: { [Op.in]: grantIds },
396
+ status: { [Op.in]: ['pending', 'granted'] },
397
+ },
398
+ }
399
+ );
400
+
401
+ return affectedCount;
402
+ }
403
+
404
+ /**
405
+ * 获取客户的有效信用额度总额(按货币分组)
406
+ */
407
+ public static async getEffectiveCreditSummary({
408
+ customerId,
409
+ currencyId: searchCurrencyId,
410
+ priceIds,
411
+ }: {
412
+ customerId: string;
413
+ currencyId?: string[] | string;
414
+ priceIds?: string[];
415
+ }): Promise<Record<string, CreditGrantSummary>> {
416
+ const summary: Record<
417
+ string,
418
+ { paymentCurrency: TPaymentCurrency; totalAmount: string; remainingAmount: string; grantCount: number }
419
+ > = {};
420
+
421
+ let targetCurrencyIds: string[] = [];
422
+ if (searchCurrencyId) {
423
+ targetCurrencyIds = typeof searchCurrencyId === 'string' ? [searchCurrencyId] : searchCurrencyId;
424
+ }
425
+
426
+ if (!searchCurrencyId) {
427
+ const grantsWithCurrency = await this.findAll({
428
+ where: {
429
+ customer_id: customerId,
430
+ },
431
+ attributes: ['currency_id'],
432
+ group: ['currency_id'],
433
+ raw: true,
434
+ });
435
+
436
+ targetCurrencyIds = grantsWithCurrency.map((grant: any) => grant.currency_id);
437
+ }
438
+
439
+ await Promise.all(
440
+ targetCurrencyIds.map(async (currencyId: string) => {
441
+ const paymentCurrency = await PaymentCurrency.findByPk(currencyId);
442
+ if (!paymentCurrency) {
443
+ return null;
444
+ }
445
+ if (!paymentCurrency.isCredit()) {
446
+ return null;
447
+ }
448
+
449
+ const availableGrants = await this.getAvailableCreditsForCustomer(customerId, currencyId, priceIds);
450
+
451
+ if (availableGrants.length > 0) {
452
+ let totalAmount = '0';
453
+ let remainingAmount = '0';
454
+ let grantCount = 0;
455
+
456
+ availableGrants.forEach((grant) => {
457
+ totalAmount = new BN(totalAmount).add(new BN(grant.amount)).toString();
458
+ remainingAmount = new BN(remainingAmount).add(new BN(grant.remaining_amount)).toString();
459
+ grantCount += 1;
460
+ });
461
+
462
+ const result = {
463
+ paymentCurrency,
464
+ totalAmount,
465
+ remainingAmount,
466
+ grantCount,
467
+ };
468
+ summary[currencyId] = result;
469
+
470
+ return result;
471
+ }
472
+ summary[currencyId] = {
473
+ paymentCurrency,
474
+ totalAmount: '0',
475
+ remainingAmount: '0',
476
+ grantCount: 0,
477
+ };
478
+ return null;
479
+ })
480
+ );
481
+
482
+ return summary;
483
+ }
484
+ }
485
+
486
+ export type TCreditGrant = InferAttributes<CreditGrant>;