payment-kit 1.26.5 → 1.27.0

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 (46) hide show
  1. package/api/src/libs/payment.ts +113 -22
  2. package/api/src/libs/queue/index.ts +20 -9
  3. package/api/src/libs/queue/store.ts +11 -7
  4. package/api/src/libs/reference-cache.ts +115 -0
  5. package/api/src/queues/auto-recharge.ts +68 -21
  6. package/api/src/queues/credit-consume.ts +835 -206
  7. package/api/src/routes/checkout-sessions.ts +1 -1
  8. package/api/src/routes/customers.ts +15 -3
  9. package/api/src/routes/donations.ts +4 -4
  10. package/api/src/routes/index.ts +37 -8
  11. package/api/src/routes/invoices.ts +14 -3
  12. package/api/src/routes/meter-events.ts +41 -15
  13. package/api/src/routes/payment-links.ts +2 -2
  14. package/api/src/routes/prices.ts +1 -1
  15. package/api/src/routes/pricing-table.ts +3 -2
  16. package/api/src/routes/products.ts +2 -2
  17. package/api/src/routes/subscription-items.ts +12 -3
  18. package/api/src/routes/subscriptions.ts +27 -9
  19. package/api/src/store/migrations/20260306-checkout-session-indexes.ts +23 -0
  20. package/api/src/store/models/checkout-session.ts +3 -2
  21. package/api/src/store/models/coupon.ts +9 -6
  22. package/api/src/store/models/credit-grant.ts +4 -1
  23. package/api/src/store/models/credit-transaction.ts +3 -2
  24. package/api/src/store/models/customer.ts +9 -6
  25. package/api/src/store/models/exchange-rate-provider.ts +9 -6
  26. package/api/src/store/models/invoice.ts +3 -2
  27. package/api/src/store/models/meter-event.ts +6 -4
  28. package/api/src/store/models/meter.ts +9 -6
  29. package/api/src/store/models/payment-intent.ts +9 -6
  30. package/api/src/store/models/payment-link.ts +9 -6
  31. package/api/src/store/models/payout.ts +3 -2
  32. package/api/src/store/models/price.ts +9 -6
  33. package/api/src/store/models/pricing-table.ts +9 -6
  34. package/api/src/store/models/product.ts +9 -6
  35. package/api/src/store/models/promotion-code.ts +9 -6
  36. package/api/src/store/models/refund.ts +9 -6
  37. package/api/src/store/models/setup-intent.ts +6 -4
  38. package/api/src/store/sequelize.ts +8 -3
  39. package/api/tests/queues/credit-consume-batch.spec.ts +438 -0
  40. package/api/tests/queues/credit-consume.spec.ts +505 -0
  41. package/api/third.d.ts +1 -1
  42. package/blocklet.yml +1 -1
  43. package/package.json +8 -7
  44. package/scripts/benchmark-seed.js +247 -0
  45. package/src/components/customer/credit-overview.tsx +31 -42
  46. package/src/components/invoice-pdf/template.tsx +5 -4
@@ -0,0 +1,505 @@
1
+ import { BN } from '@ocap/util';
2
+
3
+ import { handleCreditConsumption } from '../../src/queues/credit-consume';
4
+
5
+ import { MeterEvent, CreditGrant, CreditTransaction, Customer, Subscription } from '../../src/store/models';
6
+ import { getCachedMeterExpanded } from '../../src/libs/reference-cache';
7
+ import { createEvent } from '../../src/libs/audit';
8
+ import { checkAndTriggerAutoRecharge } from '../../src/queues/auto-recharge';
9
+ import { handlePastDueSubscriptionRecovery } from '../../src/queues/payment';
10
+
11
+ // ============================================================================
12
+ // Mocks
13
+ // ============================================================================
14
+
15
+ jest.mock('../../src/libs/logger', () => ({
16
+ __esModule: true,
17
+ default: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() },
18
+ }));
19
+
20
+ jest.mock('../../src/libs/audit', () => ({
21
+ createEvent: jest.fn().mockResolvedValue(undefined),
22
+ }));
23
+
24
+ jest.mock('../../src/libs/event', () => ({
25
+ events: { on: jest.fn() },
26
+ }));
27
+
28
+ jest.mock('../../src/libs/lock', () => {
29
+ const lock = { acquire: jest.fn().mockResolvedValue(true), release: jest.fn(), locked: false };
30
+ // @ts-ignore expose for test manipulation
31
+ global.__mockLock = lock;
32
+ return {
33
+ getLock: jest.fn().mockReturnValue(lock),
34
+ Lock: jest.fn(),
35
+ };
36
+ });
37
+
38
+ jest.mock('../../src/libs/reference-cache', () => ({
39
+ getCachedMeterExpanded: jest.fn(),
40
+ }));
41
+
42
+ jest.mock('../../src/libs/subscription', () => ({
43
+ getDaysUntilCancel: jest.fn().mockReturnValue(7),
44
+ getDueUnit: jest.fn().mockReturnValue(86400),
45
+ getMeterPriceIdsFromSubscription: jest.fn().mockResolvedValue(['price_1']),
46
+ }));
47
+
48
+ jest.mock('../../src/libs/util', () => ({
49
+ MAX_RETRY_COUNT: 3,
50
+ getNextRetry: jest.fn().mockReturnValue(Math.floor(Date.now() / 1000) + 60),
51
+ formatCreditAmount: jest.fn().mockImplementation((amount, symbol) => `${symbol}${amount}`),
52
+ }));
53
+
54
+ jest.mock('../../src/queues/payment', () => ({
55
+ handlePastDueSubscriptionRecovery: jest.fn().mockResolvedValue(undefined),
56
+ }));
57
+
58
+ jest.mock('../../src/queues/auto-recharge', () => ({
59
+ checkAndTriggerAutoRecharge: jest.fn().mockResolvedValue(undefined),
60
+ }));
61
+
62
+ jest.mock('../../src/queues/token-transfer', () => ({
63
+ addTokenTransferJob: jest.fn().mockResolvedValue(undefined),
64
+ }));
65
+
66
+ // Queue mock — must mock before credit-consume is imported
67
+ jest.mock('../../src/libs/queue', () => {
68
+ const mockQueue = {
69
+ push: jest.fn(),
70
+ get: jest.fn().mockResolvedValue(null),
71
+ delete: jest.fn().mockResolvedValue(undefined),
72
+ on: jest.fn(),
73
+ };
74
+ return jest.fn().mockReturnValue(mockQueue);
75
+ });
76
+
77
+ jest.mock('../../src/store/models', () => ({
78
+ MeterEvent: {
79
+ findByPk: jest.fn(),
80
+ findAll: jest.fn(),
81
+ getPendingAmounts: jest.fn(),
82
+ },
83
+ CreditGrant: {
84
+ getAvailableCreditsForCustomer: jest.fn(),
85
+ hasOnchainToken: jest.fn().mockReturnValue(false),
86
+ },
87
+ CreditTransaction: {
88
+ findAll: jest.fn(),
89
+ findOne: jest.fn(),
90
+ create: jest.fn(),
91
+ },
92
+ Customer: {
93
+ findByPk: jest.fn(),
94
+ },
95
+ Subscription: {
96
+ findByPk: jest.fn(),
97
+ },
98
+ }));
99
+
100
+ // ============================================================================
101
+ // Helpers
102
+ // ============================================================================
103
+
104
+ function makeMeterEvent(overrides: Record<string, any> = {}) {
105
+ return {
106
+ id: 'me_1',
107
+ event_name: 'ai-meter-v2',
108
+ status: 'pending',
109
+ credit_consumed: '0',
110
+ credit_pending: '0',
111
+ attempt_count: 0,
112
+ metadata: {},
113
+ created_at: new Date(),
114
+ getCustomerId: jest.fn().mockReturnValue('cus_1'),
115
+ getSubscriptionId: jest.fn().mockReturnValue(undefined),
116
+ getValue: jest.fn().mockReturnValue('100'),
117
+ markAsProcessing: jest.fn().mockResolvedValue(undefined),
118
+ markAsRequiresAction: jest.fn().mockResolvedValue(undefined),
119
+ markAsRequiresCapture: jest.fn().mockResolvedValue(undefined),
120
+ update: jest.fn().mockResolvedValue(undefined),
121
+ ...overrides,
122
+ };
123
+ }
124
+
125
+ function makeMeter(overrides: Record<string, any> = {}) {
126
+ return {
127
+ id: 'meter_1',
128
+ name: 'AI Meter',
129
+ unit: 'token',
130
+ currency_id: 'cur_1',
131
+ paymentCurrency: { id: 'cur_1', symbol: '$', decimal: 2 },
132
+ ...overrides,
133
+ };
134
+ }
135
+
136
+ function makeCustomer(overrides: Record<string, any> = {}) {
137
+ return {
138
+ id: 'cus_1',
139
+ did: 'did:abt:abc',
140
+ ...overrides,
141
+ };
142
+ }
143
+
144
+ function makeGrant(id: string, amount: string, remaining: string, overrides: Record<string, any> = {}) {
145
+ return {
146
+ id,
147
+ customer_id: 'cus_1',
148
+ currency_id: 'cur_1',
149
+ amount,
150
+ remaining_amount: remaining,
151
+ status: 'granted',
152
+ metadata: {},
153
+ consumeCredit: jest.fn().mockImplementation(function returnConsumeCredit(this: any, consumeAmt: string) {
154
+ const newRemaining = new BN(this.remaining_amount).sub(new BN(consumeAmt));
155
+ this.remaining_amount = newRemaining.toString();
156
+ const depleted = newRemaining.lte(new BN(0));
157
+ return Promise.resolve({ consumed: consumeAmt, remaining: this.remaining_amount, depleted });
158
+ }),
159
+ save: jest.fn().mockResolvedValue(undefined),
160
+ ...overrides,
161
+ };
162
+ }
163
+
164
+ function makeSubscription(overrides: Record<string, any> = {}) {
165
+ return {
166
+ id: 'sub_1',
167
+ status: 'active',
168
+ current_period_start: Math.floor(Date.now() / 1000),
169
+ pending_invoice_item_interval: { interval: 'month' },
170
+ isActive: jest.fn().mockReturnValue(true),
171
+ update: jest.fn().mockResolvedValue(undefined),
172
+ ...overrides,
173
+ };
174
+ }
175
+
176
+ function setupBasicMocks(meterEvent: any, meter: any, customer: any, grants: any[], existingTx: any[] = []) {
177
+ (MeterEvent.findByPk as jest.Mock).mockResolvedValue(meterEvent);
178
+ (getCachedMeterExpanded as jest.Mock).mockResolvedValue(meter);
179
+ (Customer.findByPk as jest.Mock).mockResolvedValue(customer);
180
+ (CreditGrant.getAvailableCreditsForCustomer as jest.Mock).mockResolvedValue(grants);
181
+ (CreditTransaction.findAll as jest.Mock).mockResolvedValue(existingTx);
182
+ (CreditTransaction.create as jest.Mock).mockImplementation((data: any) =>
183
+ Promise.resolve({ id: `tx_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`, ...data })
184
+ );
185
+ }
186
+
187
+ // ============================================================================
188
+ // Tests
189
+ // ============================================================================
190
+
191
+ describe('credit-consume: handleCreditConsumption', () => {
192
+ const getLock = () => (global as any).__mockLock;
193
+
194
+ beforeEach(() => {
195
+ jest.clearAllMocks();
196
+ getLock().acquire.mockResolvedValue(true);
197
+ getLock().release.mockReset();
198
+ });
199
+
200
+ // ==========================================
201
+ // Scenario 1: Single event, sufficient balance
202
+ // ==========================================
203
+ it('scenario 1: consumes credit fully when grant has sufficient balance', async () => {
204
+ const event = makeMeterEvent({ getValue: jest.fn().mockReturnValue('100') });
205
+ const meter = makeMeter();
206
+ const customer = makeCustomer();
207
+ const grant = makeGrant('grant_1', '1000', '500');
208
+
209
+ setupBasicMocks(event, meter, customer, [grant]);
210
+
211
+ await handleCreditConsumption({ meterEventId: 'me_1' });
212
+
213
+ // Should mark as processing
214
+ expect(event.markAsProcessing).toHaveBeenCalledTimes(1);
215
+
216
+ // Should consume from grant
217
+ expect(grant.consumeCredit).toHaveBeenCalledWith('100', expect.objectContaining({ meter_event_id: 'me_1' }));
218
+
219
+ // Should mark event as completed
220
+ expect(event.update).toHaveBeenCalledWith(
221
+ expect.objectContaining({
222
+ status: 'completed',
223
+ credit_consumed: '100',
224
+ credit_pending: '0',
225
+ })
226
+ );
227
+
228
+ // Should trigger auto-recharge outside lock
229
+ expect(checkAndTriggerAutoRecharge).toHaveBeenCalled();
230
+
231
+ // Lock should be released
232
+ expect(getLock().release).toHaveBeenCalled();
233
+ });
234
+
235
+ // ==========================================
236
+ // Scenario 2: Single event, insufficient balance
237
+ // ==========================================
238
+ it('scenario 2: handles insufficient balance correctly', async () => {
239
+ const event = makeMeterEvent({ getValue: jest.fn().mockReturnValue('500') });
240
+ const meter = makeMeter();
241
+ const customer = makeCustomer();
242
+ const grant = makeGrant('grant_1', '200', '200');
243
+
244
+ setupBasicMocks(event, meter, customer, [grant]);
245
+
246
+ await expect(handleCreditConsumption({ meterEventId: 'me_1' })).rejects.toThrow('Insufficient credit balance');
247
+
248
+ // Should create insufficient event
249
+ expect(createEvent).toHaveBeenCalledWith(
250
+ 'Customer',
251
+ 'customer.credit.insufficient',
252
+ customer,
253
+ expect.objectContaining({
254
+ metadata: expect.objectContaining({ meter_event_id: 'me_1' }),
255
+ })
256
+ );
257
+
258
+ // Should save partial progress
259
+ expect(event.update).toHaveBeenCalledWith(
260
+ expect.objectContaining({
261
+ credit_consumed: '200',
262
+ credit_pending: '300',
263
+ })
264
+ );
265
+
266
+ // Lock must be released even on error
267
+ expect(getLock().release).toHaveBeenCalled();
268
+ });
269
+
270
+ // ==========================================
271
+ // Scenario 3: Exact balance → grant depleted
272
+ // ==========================================
273
+ it('scenario 3: fully consumes when balance exactly matches', async () => {
274
+ const event = makeMeterEvent({ getValue: jest.fn().mockReturnValue('200') });
275
+ const meter = makeMeter();
276
+ const customer = makeCustomer();
277
+ const grant = makeGrant('grant_1', '200', '200');
278
+
279
+ setupBasicMocks(event, meter, customer, [grant]);
280
+
281
+ await handleCreditConsumption({ meterEventId: 'me_1' });
282
+
283
+ expect(grant.consumeCredit).toHaveBeenCalledWith('200', expect.any(Object));
284
+ expect(event.update).toHaveBeenCalledWith(expect.objectContaining({ status: 'completed', credit_pending: '0' }));
285
+ // Grant remaining should be 0 after consumeCredit mock
286
+ expect(grant.remaining_amount).toBe('0');
287
+ });
288
+
289
+ // ==========================================
290
+ // Scenario 4: No grants available
291
+ // ==========================================
292
+ it('scenario 4: handles no grants scenario', async () => {
293
+ const event = makeMeterEvent({ getValue: jest.fn().mockReturnValue('100') });
294
+ const meter = makeMeter();
295
+ const customer = makeCustomer();
296
+
297
+ setupBasicMocks(event, meter, customer, []); // no grants
298
+
299
+ await expect(handleCreditConsumption({ meterEventId: 'me_1' })).rejects.toThrow('Insufficient credit balance');
300
+
301
+ expect(createEvent).toHaveBeenCalledWith('Customer', 'customer.credit.insufficient', customer, expect.any(Object));
302
+ });
303
+
304
+ // ==========================================
305
+ // Scenario 5: Idempotency — already completed
306
+ // ==========================================
307
+ it('scenario 5: skips already completed events', async () => {
308
+ const event = makeMeterEvent({ status: 'completed' });
309
+ (MeterEvent.findByPk as jest.Mock).mockResolvedValue(event);
310
+
311
+ await handleCreditConsumption({ meterEventId: 'me_1' });
312
+
313
+ // Should not acquire lock
314
+ expect(getLock().acquire).not.toHaveBeenCalled();
315
+ });
316
+
317
+ // ==========================================
318
+ // Scenario 13: With subscription — priceIds filtering
319
+ // ==========================================
320
+ it('scenario 13: loads subscription inside lock and uses priceIds', async () => {
321
+ const event = makeMeterEvent({
322
+ getSubscriptionId: jest.fn().mockReturnValue('sub_1'),
323
+ getValue: jest.fn().mockReturnValue('100'),
324
+ });
325
+ const meter = makeMeter();
326
+ const customer = makeCustomer();
327
+ const subscription = makeSubscription();
328
+ const grant = makeGrant('grant_1', '1000', '500');
329
+
330
+ setupBasicMocks(event, meter, customer, [grant]);
331
+ (Subscription.findByPk as jest.Mock).mockResolvedValue(subscription);
332
+
333
+ await handleCreditConsumption({ meterEventId: 'me_1' });
334
+
335
+ // Subscription should be loaded inside lock
336
+ expect(Subscription.findByPk).toHaveBeenCalledWith('sub_1');
337
+ // Grant query should use priceIds
338
+ expect(CreditGrant.getAvailableCreditsForCustomer).toHaveBeenCalledWith('cus_1', 'cur_1', ['price_1']);
339
+ });
340
+
341
+ // ==========================================
342
+ // Scenario 14: Subscription not found
343
+ // ==========================================
344
+ it('scenario 14: skips consumption when subscription not found', async () => {
345
+ const event = makeMeterEvent({
346
+ getSubscriptionId: jest.fn().mockReturnValue('sub_missing'),
347
+ getValue: jest.fn().mockReturnValue('100'),
348
+ });
349
+ const meter = makeMeter();
350
+ const customer = makeCustomer();
351
+
352
+ setupBasicMocks(event, meter, customer, []);
353
+ (Subscription.findByPk as jest.Mock).mockResolvedValue(null);
354
+
355
+ await handleCreditConsumption({ meterEventId: 'me_1' });
356
+
357
+ // Should skip — no grant consumption attempted
358
+ expect(CreditGrant.getAvailableCreditsForCustomer).not.toHaveBeenCalled();
359
+ expect(getLock().release).toHaveBeenCalled();
360
+ });
361
+
362
+ // ==========================================
363
+ // Scenario 17: Multi-grant cross consumption
364
+ // ==========================================
365
+ it('scenario 17: consumes across multiple grants', async () => {
366
+ const event = makeMeterEvent({ getValue: jest.fn().mockReturnValue('150') });
367
+ const meter = makeMeter();
368
+ const customer = makeCustomer();
369
+ const grant1 = makeGrant('grant_1', '100', '100');
370
+ const grant2 = makeGrant('grant_2', '200', '200');
371
+
372
+ setupBasicMocks(event, meter, customer, [grant1, grant2]);
373
+
374
+ await handleCreditConsumption({ meterEventId: 'me_1' });
375
+
376
+ // Should consume 100 from grant1 and 50 from grant2
377
+ expect(grant1.consumeCredit).toHaveBeenCalledWith('100', expect.any(Object));
378
+ expect(grant2.consumeCredit).toHaveBeenCalledWith('50', expect.any(Object));
379
+
380
+ // Should create 2 transactions
381
+ expect(CreditTransaction.create).toHaveBeenCalledTimes(2);
382
+
383
+ expect(event.update).toHaveBeenCalledWith(expect.objectContaining({ status: 'completed' }));
384
+ });
385
+
386
+ // ==========================================
387
+ // Scenario 22/23: Retry scheduling + max retries
388
+ // ==========================================
389
+ it('scenario 22: schedules retry on partial failure', async () => {
390
+ const event = makeMeterEvent({
391
+ getValue: jest.fn().mockReturnValue('500'),
392
+ attempt_count: 0,
393
+ });
394
+ const meter = makeMeter();
395
+ const customer = makeCustomer();
396
+ const grant = makeGrant('grant_1', '100', '100');
397
+
398
+ setupBasicMocks(event, meter, customer, [grant]);
399
+
400
+ await expect(handleCreditConsumption({ meterEventId: 'me_1' })).rejects.toThrow();
401
+
402
+ // Should schedule retry (attempt_count=0 < MAX_RETRY_COUNT=3)
403
+ expect(event.markAsRequiresCapture).toHaveBeenCalled();
404
+ });
405
+
406
+ it('scenario 23: marks as requires_action after max retries', async () => {
407
+ const event = makeMeterEvent({
408
+ getValue: jest.fn().mockReturnValue('500'),
409
+ attempt_count: 3, // >= MAX_RETRY_COUNT
410
+ });
411
+ const meter = makeMeter();
412
+ const customer = makeCustomer();
413
+
414
+ setupBasicMocks(event, meter, customer, []);
415
+
416
+ await expect(handleCreditConsumption({ meterEventId: 'me_1' })).rejects.toThrow();
417
+
418
+ expect(event.markAsRequiresAction).toHaveBeenCalled();
419
+ expect(event.markAsRequiresCapture).not.toHaveBeenCalled();
420
+ });
421
+
422
+ // ==========================================
423
+ // Scenario 26: Past due subscription recovery
424
+ // ==========================================
425
+ it('scenario 26: triggers past_due recovery on successful consumption', async () => {
426
+ const subscription = makeSubscription({ status: 'past_due' });
427
+ const event = makeMeterEvent({
428
+ getSubscriptionId: jest.fn().mockReturnValue('sub_1'),
429
+ getValue: jest.fn().mockReturnValue('50'),
430
+ });
431
+ const meter = makeMeter();
432
+ const customer = makeCustomer();
433
+ const grant = makeGrant('grant_1', '1000', '500');
434
+
435
+ setupBasicMocks(event, meter, customer, [grant]);
436
+ (Subscription.findByPk as jest.Mock).mockResolvedValue(subscription);
437
+
438
+ await handleCreditConsumption({ meterEventId: 'me_1' });
439
+
440
+ expect(handlePastDueSubscriptionRecovery).toHaveBeenCalledWith(subscription, null);
441
+ });
442
+
443
+ // ==========================================
444
+ // Scenario 30: Zero value event
445
+ // ==========================================
446
+ it('scenario 30: handles zero-value event', async () => {
447
+ const event = makeMeterEvent({ getValue: jest.fn().mockReturnValue('0') });
448
+ const meter = makeMeter();
449
+ const customer = makeCustomer();
450
+ const grant = makeGrant('grant_1', '100', '100');
451
+
452
+ setupBasicMocks(event, meter, customer, [grant]);
453
+
454
+ await handleCreditConsumption({ meterEventId: 'me_1' });
455
+
456
+ // No grants should be consumed
457
+ expect(grant.consumeCredit).not.toHaveBeenCalled();
458
+ // Event should be completed
459
+ expect(event.update).toHaveBeenCalledWith(expect.objectContaining({ status: 'completed', credit_pending: '0' }));
460
+ });
461
+
462
+ // ==========================================
463
+ // Scenario 33: Low balance threshold
464
+ // ==========================================
465
+ it('scenario 33: triggers low_balance event when remaining < threshold', async () => {
466
+ const event = makeMeterEvent({ getValue: jest.fn().mockReturnValue('95') });
467
+ const meter = makeMeter();
468
+ const customer = makeCustomer();
469
+ // Grant of 100 with 100 remaining → after consuming 95, only 5 left (5% < 10% threshold)
470
+ const grant = makeGrant('grant_1', '100', '100');
471
+
472
+ setupBasicMocks(event, meter, customer, [grant]);
473
+
474
+ await handleCreditConsumption({ meterEventId: 'me_1' });
475
+
476
+ expect(createEvent).toHaveBeenCalledWith(
477
+ 'Customer',
478
+ 'customer.credit.low_balance',
479
+ customer,
480
+ expect.objectContaining({
481
+ metadata: expect.objectContaining({ currency_id: 'cur_1' }),
482
+ })
483
+ );
484
+ });
485
+
486
+ // ==========================================
487
+ // Lock safety: always released
488
+ // ==========================================
489
+ it('always releases lock even when unexpected error occurs', async () => {
490
+ const event = makeMeterEvent();
491
+ const meter = makeMeter();
492
+ const customer = makeCustomer();
493
+
494
+ (MeterEvent.findByPk as jest.Mock).mockResolvedValue(event);
495
+ (getCachedMeterExpanded as jest.Mock).mockResolvedValue(meter);
496
+ (Customer.findByPk as jest.Mock).mockResolvedValue(customer);
497
+ // Throw inside lock
498
+ (CreditGrant.getAvailableCreditsForCustomer as jest.Mock).mockRejectedValue(new Error('DB crash'));
499
+ (CreditTransaction.findAll as jest.Mock).mockResolvedValue([]);
500
+
501
+ await expect(handleCreditConsumption({ meterEventId: 'me_1' })).rejects.toThrow('DB crash');
502
+
503
+ expect(getLock().release).toHaveBeenCalled();
504
+ });
505
+ });
package/api/third.d.ts CHANGED
@@ -35,7 +35,7 @@ namespace Express {
35
35
  t: (key: string, ...args: any[]) => string;
36
36
  doc?: any;
37
37
  customer?: any;
38
- currency: any;
38
+ baseCurrency: any;
39
39
  stripeEvent?: any;
40
40
  stripeClient?: any;
41
41
  }
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.26.5
17
+ version: 1.27.0
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.26.5",
3
+ "version": "1.27.0",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "prelint": "npm run types",
@@ -21,7 +21,8 @@
21
21
  "deploy": "pnpm run bundle && blocklet deploy .blocklet/bundle",
22
22
  "upload": "pnpm run bundle && blocklet upload .blocklet/release/blocklet.json",
23
23
  "bump-version": "zx --quiet scripts/bump-version.mjs",
24
- "sdk-test": "node scripts/sdk.js"
24
+ "sdk-test": "node scripts/sdk.js",
25
+ "benchmark-seed": "node scripts/benchmark-seed.js"
25
26
  },
26
27
  "lint-staged": {
27
28
  "*.{mjs,js,jsx,ts,tsx}": [
@@ -59,9 +60,9 @@
59
60
  "@blocklet/error": "^0.3.5",
60
61
  "@blocklet/js-sdk": "^1.17.8-beta-20260104-120132-cb5b1914",
61
62
  "@blocklet/logger": "^1.17.8-beta-20260104-120132-cb5b1914",
62
- "@blocklet/payment-broker-client": "1.26.5",
63
- "@blocklet/payment-react": "1.26.5",
64
- "@blocklet/payment-vendor": "1.26.5",
63
+ "@blocklet/payment-broker-client": "1.27.0",
64
+ "@blocklet/payment-react": "1.27.0",
65
+ "@blocklet/payment-vendor": "1.27.0",
65
66
  "@blocklet/sdk": "^1.17.8-beta-20260104-120132-cb5b1914",
66
67
  "@blocklet/ui-react": "^3.5.1",
67
68
  "@blocklet/uploader": "^0.3.19",
@@ -132,7 +133,7 @@
132
133
  "devDependencies": {
133
134
  "@abtnode/types": "^1.17.8-beta-20260104-120132-cb5b1914",
134
135
  "@arcblock/eslint-config-ts": "^0.3.3",
135
- "@blocklet/payment-types": "1.26.5",
136
+ "@blocklet/payment-types": "1.27.0",
136
137
  "@types/cookie-parser": "^1.4.9",
137
138
  "@types/cors": "^2.8.19",
138
139
  "@types/debug": "^4.1.12",
@@ -179,5 +180,5 @@
179
180
  "parser": "typescript"
180
181
  }
181
182
  },
182
- "gitHead": "2d208818d218494407989dd8cd460ab51d075952"
183
+ "gitHead": "5dbe5a31ef3bc6f37e600c73c9e5ef80a4cc2e32"
183
184
  }