@nehorai/credits 1.7.0 → 2.1.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/README.md +384 -153
- package/dist/adapters/index.cjs +308 -46
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.d.cts +3 -2
- package/dist/adapters/index.d.ts +3 -2
- package/dist/adapters/index.js +308 -46
- package/dist/adapters/index.js.map +1 -1
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.cjs +700 -76
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +583 -5
- package/dist/core/index.d.ts +583 -5
- package/dist/core/index.js +652 -76
- package/dist/core/index.js.map +1 -1
- package/dist/index.cjs +1772 -371
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -7
- package/dist/index.d.ts +44 -7
- package/dist/index.js +1716 -371
- package/dist/index.js.map +1 -1
- package/dist/repository/index.cjs +1403 -185
- package/dist/repository/index.cjs.map +1 -1
- package/dist/repository/index.d.cts +141 -15
- package/dist/repository/index.d.ts +141 -15
- package/dist/repository/index.js +1396 -185
- package/dist/repository/index.js.map +1 -1
- package/dist/sdk/index.cjs +23 -1
- package/dist/sdk/index.cjs.map +1 -1
- package/dist/sdk/index.d.cts +2 -2
- package/dist/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js +23 -1
- package/dist/sdk/index.js.map +1 -1
- package/dist/service/index.cjs +484 -106
- package/dist/service/index.cjs.map +1 -1
- package/dist/service/index.d.cts +68 -10
- package/dist/service/index.d.ts +68 -10
- package/dist/service/index.js +484 -106
- package/dist/service/index.js.map +1 -1
- package/dist/types-BhCdep_t.d.ts +646 -0
- package/dist/{types-CjLj3GW5.d.cts → types-Dzc443vy.d.cts} +44 -2
- package/dist/{types-CjLj3GW5.d.ts → types-Dzc443vy.d.ts} +44 -2
- package/dist/types-wtksMa4n.d.cts +646 -0
- package/package.json +71 -70
- package/LICENSE +0 -21
- package/dist/types-DFe5BoJY.d.cts +0 -329
- package/dist/types-_bSybQnb.d.ts +0 -329
package/dist/adapters/index.cjs
CHANGED
|
@@ -47,11 +47,13 @@ var creditSystemConfigSchema = import_zod.z.object({
|
|
|
47
47
|
/**
|
|
48
48
|
* Operation costs in credits
|
|
49
49
|
* Keys are operation type names (any non-empty string)
|
|
50
|
-
* Values are
|
|
50
|
+
* Values are non-negative numbers representing credit cost. 0 marks a free
|
|
51
|
+
* operation: `withCredits` runs it without touching the ledger. Amounts that
|
|
52
|
+
* move credits stay strictly positive, so `reserveCredits` refuses a 0.
|
|
51
53
|
*/
|
|
52
54
|
operationCosts: import_zod.z.record(
|
|
53
55
|
import_zod.z.string().min(1),
|
|
54
|
-
import_zod.z.number().
|
|
56
|
+
import_zod.z.number().nonnegative()
|
|
55
57
|
),
|
|
56
58
|
/**
|
|
57
59
|
* Display labels for operation types (English canonical labels).
|
|
@@ -285,57 +287,315 @@ function getOperationLabel(operationType) {
|
|
|
285
287
|
}
|
|
286
288
|
initializeConfig();
|
|
287
289
|
|
|
288
|
-
// src/core/
|
|
289
|
-
|
|
290
|
+
// src/core/errors.ts
|
|
291
|
+
var CreditErrorCode = {
|
|
292
|
+
INSUFFICIENT_CREDITS: "INSUFFICIENT_CREDITS",
|
|
293
|
+
RESERVATION_NOT_FOUND: "RESERVATION_NOT_FOUND",
|
|
294
|
+
RESERVATION_EXPIRED: "RESERVATION_EXPIRED",
|
|
295
|
+
RESERVATION_ALREADY_PROCESSED: "RESERVATION_ALREADY_PROCESSED",
|
|
296
|
+
USER_NOT_FOUND: "USER_NOT_FOUND",
|
|
297
|
+
INVALID_OPERATION_TYPE: "INVALID_OPERATION_TYPE",
|
|
298
|
+
CONFIGURATION_ERROR: "CONFIGURATION_ERROR",
|
|
299
|
+
DATABASE_ERROR: "DATABASE_ERROR",
|
|
300
|
+
/** Caller reused an idempotency key with a different immutable payload. */
|
|
301
|
+
IDEMPOTENCY_CONFLICT: "IDEMPOTENCY_CONFLICT",
|
|
302
|
+
/**
|
|
303
|
+
* Retryable infrastructure failure — serialization/deadlock, lock timeout,
|
|
304
|
+
* connection loss, pool exhaustion. Distinct from `DATABASE_ERROR`, which is
|
|
305
|
+
* a deterministic failure that will fail again identically on retry.
|
|
306
|
+
*/
|
|
307
|
+
TRANSIENT_ERROR: "TRANSIENT_ERROR",
|
|
308
|
+
/** The configured repository adapter does not implement the requested V2 method. */
|
|
309
|
+
UNSUPPORTED_OPERATION: "UNSUPPORTED_OPERATION",
|
|
310
|
+
/** Amount was not a finite positive number. */
|
|
311
|
+
INVALID_AMOUNT: "INVALID_AMOUNT",
|
|
312
|
+
/** Idempotency key was supplied but empty or whitespace-only. */
|
|
313
|
+
INVALID_IDEMPOTENCY_KEY: "INVALID_IDEMPOTENCY_KEY",
|
|
314
|
+
/**
|
|
315
|
+
* A persisted reservation holds a status this library does not define, so no
|
|
316
|
+
* transition can reason about it. Quarantined rather than cast; nothing moved.
|
|
317
|
+
*/
|
|
318
|
+
CORRUPT_RESERVATION_STATUS: "CORRUPT_RESERVATION_STATUS",
|
|
319
|
+
/**
|
|
320
|
+
* A persisted reservation does not record that its hold was placed
|
|
321
|
+
* atomically, so the credits it claims are not known to be reserved. Moving
|
|
322
|
+
* it would spend another hold's coverage; nothing moved.
|
|
323
|
+
*/
|
|
324
|
+
UNBACKED_RESERVATION: "UNBACKED_RESERVATION"
|
|
325
|
+
};
|
|
326
|
+
var CreditError = class _CreditError extends Error {
|
|
327
|
+
code;
|
|
328
|
+
details;
|
|
329
|
+
constructor(message, code, details) {
|
|
330
|
+
super(message);
|
|
331
|
+
this.name = "CreditError";
|
|
332
|
+
this.code = code;
|
|
333
|
+
this.details = details;
|
|
334
|
+
if (Error.captureStackTrace) {
|
|
335
|
+
Error.captureStackTrace(this, _CreditError);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Convert to JSON-serializable object
|
|
340
|
+
*/
|
|
341
|
+
toJSON() {
|
|
342
|
+
return {
|
|
343
|
+
name: this.name,
|
|
344
|
+
code: this.code,
|
|
345
|
+
message: this.message,
|
|
346
|
+
...this.details && { details: this.details }
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
function isCreditError(error) {
|
|
351
|
+
return error instanceof CreditError;
|
|
352
|
+
}
|
|
353
|
+
function isInsufficientCreditsError(error) {
|
|
354
|
+
if (isCreditError(error)) {
|
|
355
|
+
return error.code === CreditErrorCode.INSUFFICIENT_CREDITS;
|
|
356
|
+
}
|
|
357
|
+
if (error instanceof Error) {
|
|
358
|
+
return error.message.toLowerCase().includes("insufficient credits");
|
|
359
|
+
}
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
function createInsufficientCreditsError(required, available) {
|
|
363
|
+
return new CreditError(
|
|
364
|
+
`Insufficient credits: available ${available}, required ${required}`,
|
|
365
|
+
CreditErrorCode.INSUFFICIENT_CREDITS,
|
|
366
|
+
{ required, available, shortfall: sumAmounts(required, -available) }
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
function createReservationNotFoundError(reservationId) {
|
|
370
|
+
return new CreditError(
|
|
371
|
+
`Reservation ${reservationId} not found`,
|
|
372
|
+
CreditErrorCode.RESERVATION_NOT_FOUND,
|
|
373
|
+
{ reservationId }
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
function createReservationAlreadyProcessedError(reservationId, status) {
|
|
377
|
+
return new CreditError(
|
|
378
|
+
`Reservation ${reservationId} has already been ${status}`,
|
|
379
|
+
CreditErrorCode.RESERVATION_ALREADY_PROCESSED,
|
|
380
|
+
{ reservationId, status }
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
function createUnsupportedOperationError(operation, hint) {
|
|
384
|
+
return new CreditError(
|
|
385
|
+
hint ? `Repository does not support ${operation}. ${hint}` : `Repository does not support ${operation}`,
|
|
386
|
+
CreditErrorCode.UNSUPPORTED_OPERATION,
|
|
387
|
+
{ operation, ...hint ? { hint } : {} }
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/core/amount.ts
|
|
392
|
+
var CREDIT_AMOUNT_SCALE = 2;
|
|
393
|
+
var CREDIT_AMOUNT_MAX = 999999999999e-2;
|
|
394
|
+
var MAX_CENTS = 999999999999;
|
|
395
|
+
function toCents(value) {
|
|
396
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
397
|
+
const cents = Math.round(value * 100);
|
|
398
|
+
if (!Number.isSafeInteger(cents)) return null;
|
|
399
|
+
return value === cents / 100 ? cents : null;
|
|
400
|
+
}
|
|
401
|
+
function sumAmounts(...values) {
|
|
402
|
+
let cents = 0;
|
|
403
|
+
for (const value of values) {
|
|
404
|
+
const valueCents = toCents(value);
|
|
405
|
+
if (valueCents === null) return floatSum(values);
|
|
406
|
+
cents += valueCents;
|
|
407
|
+
}
|
|
408
|
+
return Number.isSafeInteger(cents) ? cents / 100 : floatSum(values);
|
|
409
|
+
}
|
|
410
|
+
function floatSum(values) {
|
|
411
|
+
let total = 0;
|
|
412
|
+
for (const value of values) total += value;
|
|
413
|
+
return total;
|
|
414
|
+
}
|
|
415
|
+
function isValidCreditAmount(value) {
|
|
416
|
+
if (typeof value !== "number") return false;
|
|
417
|
+
const cents = toCents(value);
|
|
418
|
+
return cents !== null && cents > 0 && cents <= MAX_CENTS;
|
|
419
|
+
}
|
|
420
|
+
function assertValidCreditAmount(value, context) {
|
|
421
|
+
if (isValidCreditAmount(value)) return;
|
|
422
|
+
throw new CreditError(
|
|
423
|
+
creditAmountReason(value),
|
|
424
|
+
CreditErrorCode.INVALID_AMOUNT,
|
|
425
|
+
{ amount: value, max: CREDIT_AMOUNT_MAX, scale: CREDIT_AMOUNT_SCALE, ...context }
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
function creditAmountReason(value) {
|
|
429
|
+
if (typeof value !== "number") {
|
|
430
|
+
return `Credit amount must be a number (got ${typeof value})`;
|
|
431
|
+
}
|
|
432
|
+
if (!Number.isFinite(value)) {
|
|
433
|
+
return `Credit amount must be finite (got ${String(value)})`;
|
|
434
|
+
}
|
|
435
|
+
if (value <= 0) {
|
|
436
|
+
return `Credit amount must be greater than zero (got ${value})`;
|
|
437
|
+
}
|
|
438
|
+
if (value > CREDIT_AMOUNT_MAX) {
|
|
439
|
+
return `Credit amount ${value} exceeds the maximum of ${CREDIT_AMOUNT_MAX}`;
|
|
440
|
+
}
|
|
441
|
+
return `Credit amount ${value} is not exact to ${CREDIT_AMOUNT_SCALE} decimal places`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/repository/types.ts
|
|
445
|
+
function supportsCreditsV2(repository) {
|
|
446
|
+
return typeof repository.reserveCreditsV2 === "function" && typeof repository.commitReservationV2 === "function" && typeof repository.releaseReservationV2 === "function" && typeof repository.expireReservationV2 === "function";
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/repository/flow.ts
|
|
450
|
+
async function reserveThroughRepository(repository, input) {
|
|
451
|
+
const v2 = supportsCreditsV2(repository);
|
|
452
|
+
if (!v2 && input.idempotencyKey !== void 0) {
|
|
453
|
+
throw createUnsupportedOperationError(
|
|
454
|
+
"reserveCredits with an idempotencyKey",
|
|
455
|
+
"This repository does not implement the V2 boundary, so it cannot enforce idempotency keys. Use a V2 repository (check with supportsCreditsV2) or omit the key and accept at-least-once holds."
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
assertValidCreditAmount(input.amount, { userId: input.userId });
|
|
459
|
+
if (v2) {
|
|
460
|
+
return repository.reserveCreditsV2(input);
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
const reservation = await repository.reserveCreditsAtomic(
|
|
464
|
+
input.userId,
|
|
465
|
+
input.amount,
|
|
466
|
+
input.operationType,
|
|
467
|
+
input.expiresAt
|
|
468
|
+
);
|
|
469
|
+
return { outcome: "created", reservation };
|
|
470
|
+
} catch (error) {
|
|
471
|
+
if (!isInsufficientCreditsError(error)) throw error;
|
|
472
|
+
const credits = await repository.getUserCredits(input.userId);
|
|
473
|
+
const available = credits ? sumAmounts(credits.balance, credits.bonusCredits, -credits.reserved) : 0;
|
|
474
|
+
return {
|
|
475
|
+
outcome: "insufficient",
|
|
476
|
+
available,
|
|
477
|
+
required: input.amount,
|
|
478
|
+
shortfall: Math.max(sumAmounts(input.amount, -available), 0)
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async function commitThroughRepository(repository, userId, reservationId, options) {
|
|
483
|
+
if (supportsCreditsV2(repository)) {
|
|
484
|
+
return repository.commitReservationV2(userId, reservationId, options);
|
|
485
|
+
}
|
|
290
486
|
const reservation = await repository.getReservation(userId, reservationId);
|
|
291
|
-
if (!reservation) {
|
|
292
|
-
|
|
487
|
+
if (!reservation) return { outcome: "not_found", reservationId };
|
|
488
|
+
if (reservation.status !== "reserved") {
|
|
489
|
+
return {
|
|
490
|
+
outcome: "already_terminal",
|
|
491
|
+
reservation,
|
|
492
|
+
terminalStatus: reservation.status
|
|
493
|
+
};
|
|
293
494
|
}
|
|
294
495
|
await repository.commitReservationAtomic(userId, reservationId);
|
|
295
496
|
const credits = await repository.getUserCredits(userId);
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
497
|
+
const balanceAfter = credits ? sumAmounts(credits.balance, credits.bonusCredits) : 0;
|
|
498
|
+
const entry = await repository.createJournalEntry({
|
|
499
|
+
userId,
|
|
500
|
+
entryType: "debit",
|
|
501
|
+
amount: reservation.amount,
|
|
502
|
+
balanceAfter,
|
|
503
|
+
source: "operation_commit",
|
|
504
|
+
referenceId: reservationId,
|
|
505
|
+
referenceType: "reservation",
|
|
506
|
+
description: options?.description ?? `Committed ${reservation.amount} credits for ${getOperationLabel(reservation.operationType)}`,
|
|
507
|
+
// Deterministic fields last: they carry the identity of the event, and a
|
|
508
|
+
// caller who happens to send an `operationType` or an `amount` must not
|
|
509
|
+
// relabel what actually moved.
|
|
510
|
+
metadata: {
|
|
511
|
+
...options?.metadata,
|
|
512
|
+
operationType: reservation.operationType,
|
|
513
|
+
amount: reservation.amount
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
return {
|
|
517
|
+
outcome: "committed",
|
|
518
|
+
reservation: { ...reservation, status: "committed" },
|
|
519
|
+
amount: reservation.amount,
|
|
520
|
+
balanceAfter,
|
|
521
|
+
journalEntryId: entry.id
|
|
522
|
+
};
|
|
311
523
|
}
|
|
312
|
-
async function
|
|
524
|
+
async function releaseThroughRepository(repository, userId, reservationId, options) {
|
|
525
|
+
if (supportsCreditsV2(repository)) {
|
|
526
|
+
return repository.releaseReservationV2(userId, reservationId, options);
|
|
527
|
+
}
|
|
313
528
|
const reservation = await repository.getReservation(userId, reservationId);
|
|
529
|
+
if (!reservation) {
|
|
530
|
+
await repository.releaseReservationAtomic(userId, reservationId);
|
|
531
|
+
return { outcome: "not_found", reservationId };
|
|
532
|
+
}
|
|
533
|
+
if (reservation.status !== "reserved") {
|
|
534
|
+
await repository.releaseReservationAtomic(userId, reservationId).catch(() => void 0);
|
|
535
|
+
return {
|
|
536
|
+
outcome: "already_terminal",
|
|
537
|
+
reservation,
|
|
538
|
+
terminalStatus: reservation.status
|
|
539
|
+
};
|
|
540
|
+
}
|
|
314
541
|
await repository.releaseReservationAtomic(userId, reservationId);
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
amount: reservation.amount
|
|
331
|
-
}
|
|
332
|
-
});
|
|
542
|
+
const credits = await repository.getUserCredits(userId);
|
|
543
|
+
const entry = await repository.createJournalEntry({
|
|
544
|
+
userId,
|
|
545
|
+
entryType: "credit",
|
|
546
|
+
// No credits changed hands — the hold just moved back into "available".
|
|
547
|
+
amount: 0,
|
|
548
|
+
balanceAfter: credits ? sumAmounts(credits.balance, credits.bonusCredits) : 0,
|
|
549
|
+
source: "operation_release",
|
|
550
|
+
referenceId: reservationId,
|
|
551
|
+
referenceType: "reservation",
|
|
552
|
+
description: options?.description ?? `Released ${reservation.amount} reserved credits for ${getOperationLabel(reservation.operationType)}`,
|
|
553
|
+
metadata: {
|
|
554
|
+
...options?.metadata,
|
|
555
|
+
operationType: reservation.operationType,
|
|
556
|
+
amount: reservation.amount
|
|
333
557
|
}
|
|
558
|
+
});
|
|
559
|
+
return {
|
|
560
|
+
outcome: "released",
|
|
561
|
+
reservation: { ...reservation, status: "released" },
|
|
562
|
+
amount: reservation.amount,
|
|
563
|
+
journalEntryId: entry.id
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/core/operations.ts
|
|
568
|
+
async function commitReservationWithJournal(repository, userId, reservationId) {
|
|
569
|
+
const outcome = await commitThroughRepository(repository, userId, reservationId);
|
|
570
|
+
if (outcome.outcome === "not_found") {
|
|
571
|
+
throw createReservationNotFoundError(reservationId);
|
|
572
|
+
}
|
|
573
|
+
if (outcome.outcome === "already_terminal" && outcome.terminalStatus !== "committed") {
|
|
574
|
+
throw createReservationAlreadyProcessedError(reservationId, outcome.terminalStatus);
|
|
334
575
|
}
|
|
335
576
|
}
|
|
336
|
-
async function
|
|
577
|
+
async function releaseReservationWithJournal(repository, userId, reservationId) {
|
|
578
|
+
await releaseThroughRepository(repository, userId, reservationId);
|
|
579
|
+
}
|
|
580
|
+
async function reserveCreditsForOperation(repository, userId, amount, operationType, expiryMs = 5 * 60 * 1e3, idempotencyKey) {
|
|
337
581
|
const expiresAt = new Date(Date.now() + expiryMs);
|
|
338
|
-
|
|
582
|
+
const outcome = await reserveThroughRepository(repository, {
|
|
583
|
+
userId,
|
|
584
|
+
amount,
|
|
585
|
+
operationType,
|
|
586
|
+
expiresAt,
|
|
587
|
+
idempotencyKey
|
|
588
|
+
});
|
|
589
|
+
if (outcome.outcome === "created" || outcome.outcome === "replayed") {
|
|
590
|
+
return outcome.reservation;
|
|
591
|
+
}
|
|
592
|
+
if (outcome.outcome === "insufficient") {
|
|
593
|
+
throw createInsufficientCreditsError(outcome.required, outcome.available);
|
|
594
|
+
}
|
|
595
|
+
throw createReservationAlreadyProcessedError(
|
|
596
|
+
outcome.existing.id,
|
|
597
|
+
outcome.existing.status
|
|
598
|
+
);
|
|
339
599
|
}
|
|
340
600
|
|
|
341
601
|
// src/adapters/generic.ts
|
|
@@ -383,9 +643,11 @@ function createGenericAdapter(config) {
|
|
|
383
643
|
return handler(user, data, dummyReservation);
|
|
384
644
|
}
|
|
385
645
|
let reservation;
|
|
646
|
+
let isFree = false;
|
|
386
647
|
try {
|
|
387
648
|
const cost = options.customCost ?? getOperationCost(options.operationType);
|
|
388
|
-
|
|
649
|
+
isFree = options.customCost === void 0 && cost === 0;
|
|
650
|
+
reservation = isFree ? { ...createDummyReservation(user.id, options.operationType), id: "free-operation" } : await reserveCreditsForOperation(
|
|
389
651
|
repository,
|
|
390
652
|
user.id,
|
|
391
653
|
cost,
|
|
@@ -408,7 +670,7 @@ function createGenericAdapter(config) {
|
|
|
408
670
|
try {
|
|
409
671
|
const result = await handler(user, data, reservation);
|
|
410
672
|
if (result.success) {
|
|
411
|
-
await commitReservationWithJournal(repository, user.id, reservation.id);
|
|
673
|
+
if (!isFree) await commitReservationWithJournal(repository, user.id, reservation.id);
|
|
412
674
|
logUsage({
|
|
413
675
|
userId: user.id,
|
|
414
676
|
operationType: options.operationType,
|
|
@@ -419,7 +681,7 @@ function createGenericAdapter(config) {
|
|
|
419
681
|
requestId
|
|
420
682
|
});
|
|
421
683
|
} else {
|
|
422
|
-
await releaseReservationWithJournal(repository, user.id, reservation.id);
|
|
684
|
+
if (!isFree) await releaseReservationWithJournal(repository, user.id, reservation.id);
|
|
423
685
|
logUsage({
|
|
424
686
|
userId: user.id,
|
|
425
687
|
operationType: options.operationType,
|
|
@@ -434,7 +696,7 @@ function createGenericAdapter(config) {
|
|
|
434
696
|
return result;
|
|
435
697
|
} catch (error) {
|
|
436
698
|
const message = error instanceof Error ? error.message : "An unexpected error occurred";
|
|
437
|
-
await releaseReservationWithJournal(repository, user.id, reservation.id);
|
|
699
|
+
if (!isFree) await releaseReservationWithJournal(repository, user.id, reservation.id);
|
|
438
700
|
logUsage({
|
|
439
701
|
userId: user.id,
|
|
440
702
|
operationType: options.operationType,
|