payment-kit 1.22.31 → 1.23.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.
- package/api/src/index.ts +4 -0
- package/api/src/integrations/arcblock/token.ts +599 -0
- package/api/src/libs/credit-grant.ts +7 -6
- package/api/src/queues/credit-consume.ts +29 -4
- package/api/src/queues/credit-grant.ts +245 -50
- package/api/src/queues/credit-reconciliation.ts +253 -0
- package/api/src/queues/refund.ts +263 -30
- package/api/src/queues/token-transfer.ts +331 -0
- package/api/src/routes/checkout-sessions.ts +1 -1
- package/api/src/routes/credit-grants.ts +27 -7
- package/api/src/routes/credit-tokens.ts +38 -0
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/meter-events.ts +1 -1
- package/api/src/routes/meters.ts +32 -10
- package/api/src/routes/payment-currencies.ts +103 -0
- package/api/src/routes/products.ts +2 -2
- package/api/src/routes/settings.ts +4 -3
- package/api/src/store/migrations/20251120-add-token-config-to-currencies.ts +20 -0
- package/api/src/store/migrations/20251204-add-chain-fields.ts +74 -0
- package/api/src/store/models/credit-grant.ts +57 -10
- package/api/src/store/models/credit-transaction.ts +18 -1
- package/api/src/store/models/meter-event.ts +48 -25
- package/api/src/store/models/payment-currency.ts +31 -4
- package/api/src/store/models/refund.ts +12 -2
- package/api/src/store/models/types.ts +48 -0
- package/api/third.d.ts +2 -0
- package/blocklet.yml +1 -1
- package/package.json +7 -6
- package/src/components/customer/credit-overview.tsx +1 -1
- package/src/components/meter/form.tsx +191 -18
- package/src/components/price/form.tsx +49 -37
- package/src/locales/en.tsx +24 -0
- package/src/locales/zh.tsx +26 -0
- package/src/pages/admin/billing/meters/create.tsx +42 -13
- package/src/pages/admin/billing/meters/detail.tsx +56 -5
|
@@ -20,6 +20,7 @@ import { getDaysUntilCancel, getDueUnit, getMeterPriceIdsFromSubscription } from
|
|
|
20
20
|
import { events } from '../libs/event';
|
|
21
21
|
import { handlePastDueSubscriptionRecovery } from './payment';
|
|
22
22
|
import { checkAndTriggerAutoRecharge } from './auto-recharge';
|
|
23
|
+
import { addTokenTransferJob } from './token-transfer';
|
|
23
24
|
|
|
24
25
|
type CreditConsumptionJob = {
|
|
25
26
|
meterEventId: string;
|
|
@@ -329,11 +330,30 @@ async function processGrantConsumption(
|
|
|
329
330
|
creditGrantId: creditGrant.id,
|
|
330
331
|
consumeAmount,
|
|
331
332
|
});
|
|
333
|
+
|
|
332
334
|
const result = await creditGrant.consumeCredit(consumeAmount, {
|
|
333
335
|
subscription_id: context.subscription?.id,
|
|
334
336
|
meter_event_id: context.meterEvent.id,
|
|
335
337
|
});
|
|
336
|
-
|
|
338
|
+
|
|
339
|
+
const transactionId = await createCreditTransaction(context, creditGrant, consumeAmount, result);
|
|
340
|
+
|
|
341
|
+
if (CreditGrant.hasOnchainToken(creditGrant)) {
|
|
342
|
+
addTokenTransferJob({
|
|
343
|
+
creditTransactionId: transactionId,
|
|
344
|
+
creditGrantId: creditGrant.id,
|
|
345
|
+
customerDid: context.customer.did,
|
|
346
|
+
amount: consumeAmount,
|
|
347
|
+
paymentCurrencyId: context.meter.paymentCurrency.id,
|
|
348
|
+
meterEventId: context.meterEvent.id,
|
|
349
|
+
subscriptionId: context.meterEvent.getSubscriptionId(),
|
|
350
|
+
}).catch((error) => {
|
|
351
|
+
logger.error('Failed to add token transfer job', {
|
|
352
|
+
creditTransactionId: transactionId,
|
|
353
|
+
error,
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
}
|
|
337
357
|
}
|
|
338
358
|
|
|
339
359
|
async function createCreditTransaction(
|
|
@@ -341,7 +361,7 @@ async function createCreditTransaction(
|
|
|
341
361
|
creditGrant: CreditGrant,
|
|
342
362
|
consumeAmount: string,
|
|
343
363
|
consumeResult: any
|
|
344
|
-
): Promise<
|
|
364
|
+
): Promise<string> {
|
|
345
365
|
const meterEventId = context.meterEvent.id;
|
|
346
366
|
const creditGrantId = creditGrant.id;
|
|
347
367
|
|
|
@@ -360,7 +380,7 @@ async function createCreditTransaction(
|
|
|
360
380
|
existingTransactionId: existingTransaction.id,
|
|
361
381
|
});
|
|
362
382
|
context.transactions.push(existingTransaction.id);
|
|
363
|
-
return;
|
|
383
|
+
return existingTransaction.id;
|
|
364
384
|
}
|
|
365
385
|
|
|
366
386
|
logger.debug('start to create credit transaction', {
|
|
@@ -392,6 +412,8 @@ async function createCreditTransaction(
|
|
|
392
412
|
remaining_balance: consumeResult.remaining,
|
|
393
413
|
source: context.meterEvent.id,
|
|
394
414
|
description,
|
|
415
|
+
// 如果是链上 grant,会在后续 transfer 成功之后更新为 completed
|
|
416
|
+
transfer_status: CreditGrant.hasOnchainToken(creditGrant) ? 'pending' : 'completed',
|
|
395
417
|
metadata: {
|
|
396
418
|
...(context.meterEvent.metadata || {}),
|
|
397
419
|
meter_event_id: context.meterEvent.id,
|
|
@@ -407,6 +429,8 @@ async function createCreditTransaction(
|
|
|
407
429
|
meterEventId: context.meterEvent.id,
|
|
408
430
|
});
|
|
409
431
|
context.transactions.push(transaction.id);
|
|
432
|
+
|
|
433
|
+
return transaction.id;
|
|
410
434
|
} catch (error: any) {
|
|
411
435
|
// 处理唯一约束违反错误
|
|
412
436
|
if (error.name === 'SequelizeUniqueConstraintError' || error.message.includes('UNIQUE constraint failed')) {
|
|
@@ -426,8 +450,8 @@ async function createCreditTransaction(
|
|
|
426
450
|
|
|
427
451
|
if (duplicateTransaction) {
|
|
428
452
|
context.transactions.push(duplicateTransaction.id);
|
|
453
|
+
return duplicateTransaction.id;
|
|
429
454
|
}
|
|
430
|
-
return;
|
|
431
455
|
}
|
|
432
456
|
|
|
433
457
|
// 其他错误重新抛出
|
|
@@ -486,6 +510,7 @@ export async function handleCreditConsumption(job: CreditConsumptionJob) {
|
|
|
486
510
|
|
|
487
511
|
// Consume available credits (handles existing transactions internally)
|
|
488
512
|
const consumptionResult = await consumeAvailableCredits(context, totalRequiredAmount);
|
|
513
|
+
|
|
489
514
|
// Check for auto recharge after successful consumption
|
|
490
515
|
try {
|
|
491
516
|
await checkAndTriggerAutoRecharge(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// create credit grant
|
|
2
2
|
|
|
3
3
|
import { Op } from 'sequelize';
|
|
4
|
-
import { BN, fromTokenToUnit } from '@ocap/util';
|
|
4
|
+
import { BN, fromTokenToUnit, fromUnitToToken } from '@ocap/util';
|
|
5
5
|
import dayjs from '../libs/dayjs';
|
|
6
6
|
import createQueue from '../libs/queue';
|
|
7
7
|
import {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from '../store/models';
|
|
20
20
|
import { events } from '../libs/event';
|
|
21
21
|
import { calculateExpiresAt, createCreditGrant } from '../libs/credit-grant';
|
|
22
|
+
import { mintToken, transferTokenFromCustomer } from '../integrations/arcblock/token';
|
|
22
23
|
import logger from '../libs/logger';
|
|
23
24
|
|
|
24
25
|
type CreditGrantJob =
|
|
@@ -31,8 +32,217 @@ type CreditGrantJob =
|
|
|
31
32
|
action: 'create_from_invoice';
|
|
32
33
|
};
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Activate credit grants and handle on-chain minting when required.
|
|
37
|
+
* Non on-chain credits finish immediately; on-chain credits wait for mint success
|
|
38
|
+
*/
|
|
39
|
+
async function activateGrant(creditGrant: CreditGrant) {
|
|
40
|
+
const paymentCurrency = await PaymentCurrency.findByPk(creditGrant.currency_id);
|
|
41
|
+
const isOnChainCredit = paymentCurrency && PaymentCurrency.isOnChainCredit(paymentCurrency);
|
|
42
|
+
|
|
43
|
+
logger.info('Activating credit grant', {
|
|
44
|
+
creditGrantId: creditGrant.id,
|
|
45
|
+
customerId: creditGrant.customer_id,
|
|
46
|
+
currencyId: creditGrant.currency_id,
|
|
47
|
+
amount: creditGrant.amount,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (!isOnChainCredit) {
|
|
51
|
+
await finalizeGrant(creditGrant);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Skip mint for already granted credits (legacy grants or already minted)
|
|
56
|
+
// New grants that need mint will have status='pending' until mint succeeds
|
|
57
|
+
if (creditGrant.status === 'granted' || creditGrant.chain_status === 'mint_completed') {
|
|
58
|
+
logger.info('Grant already in granted status, skipping mint', {
|
|
59
|
+
creditGrantId: creditGrant.id,
|
|
60
|
+
status: creditGrant.status,
|
|
61
|
+
chainStatus: creditGrant.chain_status,
|
|
62
|
+
});
|
|
63
|
+
await finalizeGrant(creditGrant);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Verify customer
|
|
68
|
+
const customer = await Customer.findByPk(creditGrant.customer_id);
|
|
69
|
+
if (!customer?.did) {
|
|
70
|
+
throw new Error(`Customer DID not found for credit grant ${creditGrant.id}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Step 1: Mint token on-chain
|
|
74
|
+
// TODO: check if already minted
|
|
75
|
+
const mintAmount = fromUnitToToken(creditGrant.amount, paymentCurrency.decimal);
|
|
76
|
+
const hash = await mintToken({
|
|
77
|
+
paymentCurrency,
|
|
78
|
+
amount: mintAmount,
|
|
79
|
+
receiver: customer.did,
|
|
80
|
+
creditGrantId: creditGrant.id,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Step 2: Finalize grant with mint result
|
|
84
|
+
await finalizeGrant(creditGrant, {
|
|
85
|
+
chain_status: 'mint_completed',
|
|
86
|
+
chain_detail: {
|
|
87
|
+
...(creditGrant.chain_detail || {}),
|
|
88
|
+
mint: {
|
|
89
|
+
hash,
|
|
90
|
+
at: dayjs().unix(),
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Step 3: Emit mint success event for reconciliation
|
|
96
|
+
events.emit('customer.credit_grant.minted', creditGrant, { mintHash: hash });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function finalizeGrant(
|
|
100
|
+
creditGrant: CreditGrant,
|
|
101
|
+
updates?: Partial<Pick<CreditGrant, 'chain_status' | 'chain_detail'>>
|
|
102
|
+
) {
|
|
103
|
+
await creditGrant.update({ ...updates, status: 'granted' });
|
|
104
|
+
logger.info('Credit grant finalized', {
|
|
105
|
+
creditGrantId: creditGrant.id,
|
|
106
|
+
customerId: creditGrant.customer_id,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (creditGrant.expires_at) {
|
|
110
|
+
await addCreditGrantJob(creditGrant, 'expire');
|
|
111
|
+
logger.info('Credit grant expire job ensured', {
|
|
112
|
+
creditGrantId: creditGrant.id,
|
|
113
|
+
expiresAt: creditGrant.expires_at,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Expire credit grants and handle on-chain token transfer when required.
|
|
120
|
+
* Transfers remaining tokens to system wallet for grants that have been minted (chain_status === 'mint_completed').
|
|
121
|
+
* This is consistent with credit consumption - expired credits are treated the same as consumed credits.
|
|
122
|
+
*/
|
|
123
|
+
export async function expireGrant(creditGrant: CreditGrant) {
|
|
124
|
+
// Only transfer for grants that have been minted on-chain
|
|
125
|
+
const isOnChain = creditGrant.hasOnchainToken();
|
|
126
|
+
|
|
127
|
+
logger.info('Expiring credit grant', {
|
|
128
|
+
creditGrantId: creditGrant.id,
|
|
129
|
+
isOnChain,
|
|
130
|
+
chainStatus: creditGrant.chain_status,
|
|
131
|
+
remainingAmount: creditGrant.remaining_amount,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// For grants without on-chain tokens, just update status
|
|
135
|
+
if (!isOnChain) {
|
|
136
|
+
await creditGrant.update({ status: 'expired' });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const remainingAmount = new BN(creditGrant.remaining_amount || '0');
|
|
141
|
+
if (remainingAmount.lte(new BN(0))) {
|
|
142
|
+
// No remaining amount to transfer, just update status
|
|
143
|
+
await creditGrant.update({ status: 'expired' });
|
|
144
|
+
logger.info('Credit grant expired (no remaining amount to transfer)', {
|
|
145
|
+
creditGrantId: creditGrant.id,
|
|
146
|
+
customerId: creditGrant.customer_id,
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const paymentCurrency = await PaymentCurrency.findByPk(creditGrant.currency_id);
|
|
152
|
+
if (!paymentCurrency) {
|
|
153
|
+
logger.error('Payment currency not found for credit grant transfer', {
|
|
154
|
+
creditGrantId: creditGrant.id,
|
|
155
|
+
currencyId: creditGrant.currency_id,
|
|
156
|
+
customerId: creditGrant.customer_id,
|
|
157
|
+
});
|
|
158
|
+
await creditGrant.update({ status: 'expired' });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Get customer DID
|
|
163
|
+
const customer = await Customer.findByPk(creditGrant.customer_id);
|
|
164
|
+
if (!customer?.did) {
|
|
165
|
+
logger.error('Customer DID not found for credit grant transfer', {
|
|
166
|
+
creditGrantId: creditGrant.id,
|
|
167
|
+
customerId: creditGrant.customer_id,
|
|
168
|
+
});
|
|
169
|
+
await creditGrant.update({ status: 'expired' });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Transfer remaining tokens to system wallet (same as consumption flow)
|
|
174
|
+
const chainDetail = creditGrant.chain_detail || {};
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const hash = await transferTokenFromCustomer({
|
|
178
|
+
paymentCurrency,
|
|
179
|
+
customerDid: customer.did,
|
|
180
|
+
amount: remainingAmount.toString(),
|
|
181
|
+
data: {
|
|
182
|
+
reason: 'credit_expired',
|
|
183
|
+
creditGrantId: creditGrant.id,
|
|
184
|
+
},
|
|
185
|
+
checkBalance: false, // Don't check balance as we're transferring expired tokens
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
logger.info('Successfully transferred expired credit tokens to system wallet', {
|
|
189
|
+
creditGrantId: creditGrant.id,
|
|
190
|
+
customerId: creditGrant.customer_id,
|
|
191
|
+
amount: remainingAmount.toString(),
|
|
192
|
+
hash,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Update status and chain_detail with transfer result
|
|
196
|
+
await creditGrant.update({
|
|
197
|
+
status: 'expired',
|
|
198
|
+
chain_status: 'transfer_completed',
|
|
199
|
+
chain_detail: {
|
|
200
|
+
...chainDetail,
|
|
201
|
+
expired_transfer: {
|
|
202
|
+
hash,
|
|
203
|
+
at: dayjs().unix(),
|
|
204
|
+
amount: remainingAmount.toString(),
|
|
205
|
+
},
|
|
206
|
+
voided_reason: 'expired',
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Emit expired event for reconciliation
|
|
211
|
+
events.emit('customer.credit_grant.expired', creditGrant, {
|
|
212
|
+
transferHash: hash,
|
|
213
|
+
expiredAmount: remainingAmount.toString(),
|
|
214
|
+
});
|
|
215
|
+
} catch (error) {
|
|
216
|
+
logger.error('Failed to transfer expired credit tokens', {
|
|
217
|
+
creditGrantId: creditGrant.id,
|
|
218
|
+
customerId: creditGrant.customer_id,
|
|
219
|
+
amount: remainingAmount.toString(),
|
|
220
|
+
error: error.message,
|
|
221
|
+
stack: error.stack,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Update status to expired and record error, but don't fail the expiration
|
|
225
|
+
await creditGrant.update({
|
|
226
|
+
status: 'expired',
|
|
227
|
+
chain_status: 'transfer_failed',
|
|
228
|
+
chain_detail: {
|
|
229
|
+
...chainDetail,
|
|
230
|
+
expired_transfer: {
|
|
231
|
+
...chainDetail.expired_transfer,
|
|
232
|
+
error: error.message,
|
|
233
|
+
failed_at: dayjs().unix(),
|
|
234
|
+
},
|
|
235
|
+
voided_reason: 'expired',
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
34
241
|
const handleCreditGrantJob = async (job: CreditGrantJob) => {
|
|
35
|
-
logger.info('Handling credit grant job', {
|
|
242
|
+
logger.info('Handling credit grant job', {
|
|
243
|
+
action: job.action,
|
|
244
|
+
...(job.action === 'create_from_invoice' ? { invoiceId: job.invoiceId } : { creditGrantId: job.creditGrantId }),
|
|
245
|
+
});
|
|
36
246
|
if (job.action === 'create_from_invoice') {
|
|
37
247
|
await handleInvoiceCredit(job.invoiceId);
|
|
38
248
|
return;
|
|
@@ -50,37 +260,17 @@ const handleCreditGrantJob = async (job: CreditGrantJob) => {
|
|
|
50
260
|
|
|
51
261
|
if (action === 'activate') {
|
|
52
262
|
if (creditGrant.status !== 'pending') {
|
|
53
|
-
logger.warn('Credit grant not in pending status for activation', {
|
|
263
|
+
logger.warn('Credit grant not in pending status for activation, skipping', {
|
|
54
264
|
creditGrantId,
|
|
55
265
|
status: creditGrant.status,
|
|
56
266
|
});
|
|
57
267
|
return;
|
|
58
268
|
}
|
|
59
|
-
if (creditGrant.status === 'granted') {
|
|
60
|
-
logger.info('Credit grant already granted', {
|
|
61
|
-
creditGrantId,
|
|
62
|
-
});
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
269
|
if (
|
|
66
270
|
(!creditGrant.effective_at || creditGrant.effective_at <= now) &&
|
|
67
271
|
(!creditGrant.expires_at || creditGrant.expires_at > now)
|
|
68
272
|
) {
|
|
69
|
-
await creditGrant
|
|
70
|
-
status: 'granted',
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
logger.info('Credit grant activated', {
|
|
74
|
-
creditGrantId,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
if (creditGrant.expires_at) {
|
|
78
|
-
await addCreditGrantJob(creditGrant, 'expire', creditGrant.expires_at);
|
|
79
|
-
logger.info('Credit grant expire job scheduled', {
|
|
80
|
-
creditGrantId,
|
|
81
|
-
expiresAt: creditGrant.expires_at,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
273
|
+
await activateGrant(creditGrant);
|
|
84
274
|
}
|
|
85
275
|
}
|
|
86
276
|
|
|
@@ -93,12 +283,7 @@ const handleCreditGrantJob = async (job: CreditGrantJob) => {
|
|
|
93
283
|
return;
|
|
94
284
|
}
|
|
95
285
|
if (creditGrant.expires_at && creditGrant.expires_at <= now) {
|
|
96
|
-
await creditGrant
|
|
97
|
-
status: 'expired',
|
|
98
|
-
});
|
|
99
|
-
logger.info('Credit grant expired', {
|
|
100
|
-
creditGrantId,
|
|
101
|
-
});
|
|
286
|
+
await expireGrant(creditGrant);
|
|
102
287
|
}
|
|
103
288
|
}
|
|
104
289
|
};
|
|
@@ -163,6 +348,13 @@ export async function addCreditGrantJob(creditGrant: CreditGrant, action: 'activ
|
|
|
163
348
|
export async function scheduleCreditGrantJobs(creditGrant: CreditGrant) {
|
|
164
349
|
const now = dayjs().unix();
|
|
165
350
|
|
|
351
|
+
logger.info('Scheduling credit grant jobs', {
|
|
352
|
+
creditGrantId: creditGrant.id,
|
|
353
|
+
status: creditGrant.status,
|
|
354
|
+
effective_at: creditGrant.effective_at,
|
|
355
|
+
expires_at: creditGrant.expires_at,
|
|
356
|
+
});
|
|
357
|
+
|
|
166
358
|
if (creditGrant.status === 'pending') {
|
|
167
359
|
if (creditGrant.effective_at && creditGrant.effective_at > now) {
|
|
168
360
|
await addCreditGrantJob(creditGrant, 'activate');
|
|
@@ -170,16 +362,9 @@ export async function scheduleCreditGrantJobs(creditGrant: CreditGrant) {
|
|
|
170
362
|
}
|
|
171
363
|
if (!creditGrant.effective_at || creditGrant.effective_at <= now) {
|
|
172
364
|
if (!creditGrant.expires_at || creditGrant.expires_at > now) {
|
|
173
|
-
await creditGrant
|
|
174
|
-
logger.info('Credit grant immediately activated', {
|
|
175
|
-
creditGrantId: creditGrant.id,
|
|
176
|
-
customerId: creditGrant.customer_id,
|
|
177
|
-
});
|
|
178
|
-
if (creditGrant.expires_at) {
|
|
179
|
-
await addCreditGrantJob(creditGrant, 'expire');
|
|
180
|
-
}
|
|
365
|
+
await activateGrant(creditGrant);
|
|
181
366
|
} else {
|
|
182
|
-
await creditGrant
|
|
367
|
+
await expireGrant(creditGrant);
|
|
183
368
|
logger.info('Credit grant immediately expired', {
|
|
184
369
|
creditGrantId: creditGrant.id,
|
|
185
370
|
customerId: creditGrant.customer_id,
|
|
@@ -187,8 +372,9 @@ export async function scheduleCreditGrantJobs(creditGrant: CreditGrant) {
|
|
|
187
372
|
}
|
|
188
373
|
}
|
|
189
374
|
}
|
|
190
|
-
if (creditGrant.status === 'granted'
|
|
191
|
-
|
|
375
|
+
if (creditGrant.status === 'granted') {
|
|
376
|
+
// Retry mint if previously failed, and schedule expiration
|
|
377
|
+
await activateGrant(creditGrant);
|
|
192
378
|
}
|
|
193
379
|
}
|
|
194
380
|
|
|
@@ -257,8 +443,8 @@ creditGrantQueue.on('failed', ({ id, job, error }) => {
|
|
|
257
443
|
logger.error('Credit grant job failed', { id, job, error });
|
|
258
444
|
});
|
|
259
445
|
|
|
260
|
-
creditGrantQueue.on('finished', ({ id
|
|
261
|
-
logger.
|
|
446
|
+
creditGrantQueue.on('finished', ({ id }) => {
|
|
447
|
+
logger.debug('Credit grant job completed', { id });
|
|
262
448
|
});
|
|
263
449
|
|
|
264
450
|
async function handleInvoiceCredit(invoiceId: string) {
|
|
@@ -277,7 +463,7 @@ async function handleInvoiceCredit(invoiceId: string) {
|
|
|
277
463
|
})) as TInvoiceExpanded | null;
|
|
278
464
|
|
|
279
465
|
if (!invoice) {
|
|
280
|
-
logger.
|
|
466
|
+
logger.warn('Invoice not found', { invoiceId });
|
|
281
467
|
return;
|
|
282
468
|
}
|
|
283
469
|
|
|
@@ -354,6 +540,7 @@ async function handleInvoiceCredit(invoiceId: string) {
|
|
|
354
540
|
},
|
|
355
541
|
};
|
|
356
542
|
}
|
|
543
|
+
|
|
357
544
|
return {
|
|
358
545
|
price,
|
|
359
546
|
quantity,
|
|
@@ -361,7 +548,8 @@ async function handleInvoiceCredit(invoiceId: string) {
|
|
|
361
548
|
creditAmount,
|
|
362
549
|
applicablePrices,
|
|
363
550
|
expiredAt: calculateExpiresAt(
|
|
364
|
-
|
|
551
|
+
// eslint-disable-next-line no-unsafe-optional-chaining
|
|
552
|
+
+creditConfig?.valid_duration_value || 0,
|
|
365
553
|
creditConfig?.valid_duration_unit || 'days'
|
|
366
554
|
),
|
|
367
555
|
applicabilityConfig,
|
|
@@ -459,7 +647,7 @@ async function handleInvoiceCredit(invoiceId: string) {
|
|
|
459
647
|
} catch (error) {
|
|
460
648
|
logger.error('Failed to create Credit Grant from invoice', {
|
|
461
649
|
invoiceId,
|
|
462
|
-
error
|
|
650
|
+
error,
|
|
463
651
|
stack: error.stack,
|
|
464
652
|
});
|
|
465
653
|
await Invoice.update(
|
|
@@ -502,10 +690,11 @@ export async function addInvoiceCreditJob(invoiceId: string) {
|
|
|
502
690
|
|
|
503
691
|
const existingJob = await creditGrantQueue.get(jobId);
|
|
504
692
|
if (existingJob) {
|
|
505
|
-
logger.
|
|
693
|
+
logger.debug('Invoice credit job already exists, skipping duplicate', {
|
|
506
694
|
invoiceId,
|
|
507
695
|
jobId,
|
|
508
696
|
});
|
|
697
|
+
return;
|
|
509
698
|
}
|
|
510
699
|
|
|
511
700
|
await creditGrantQueue.push({
|
|
@@ -534,8 +723,14 @@ events.on('invoice.paid', async (invoice: Invoice) => {
|
|
|
534
723
|
}
|
|
535
724
|
});
|
|
536
725
|
|
|
537
|
-
events.on('customer.credit_grant.created', async (
|
|
726
|
+
events.on('customer.credit_grant.created', async (data: { id: string }) => {
|
|
538
727
|
try {
|
|
728
|
+
// Fetch instance since event emits dataValues (plain object)
|
|
729
|
+
const creditGrant = await CreditGrant.findByPk(data.id);
|
|
730
|
+
if (!creditGrant) {
|
|
731
|
+
logger.error('Credit grant not found for scheduling', { creditGrantId: data.id });
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
539
734
|
await scheduleCreditGrantJobs(creditGrant);
|
|
540
735
|
logger.info('Credit grant jobs scheduled', {
|
|
541
736
|
creditGrantId: creditGrant.id,
|
|
@@ -543,8 +738,8 @@ events.on('customer.credit_grant.created', async (creditGrant: CreditGrant) => {
|
|
|
543
738
|
});
|
|
544
739
|
} catch (error) {
|
|
545
740
|
logger.error('Failed to schedule credit grant jobs', {
|
|
546
|
-
creditGrantId:
|
|
547
|
-
error
|
|
741
|
+
creditGrantId: data.id,
|
|
742
|
+
error,
|
|
548
743
|
});
|
|
549
744
|
}
|
|
550
745
|
});
|