@wopr-network/platform-core 1.13.3 → 1.14.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 (217) hide show
  1. package/dist/api/routes/admin-credits.d.ts +2 -2
  2. package/dist/api/routes/admin-credits.js +9 -4
  3. package/dist/api/routes/quota.d.ts +2 -2
  4. package/dist/api/routes/verify-email.d.ts +3 -3
  5. package/dist/backup/on-demand-snapshot-service.d.ts +2 -2
  6. package/dist/billing/payram/webhook.d.ts +3 -3
  7. package/dist/billing/payram/webhook.js +5 -1
  8. package/dist/billing/payram/webhook.test.js +5 -4
  9. package/dist/billing/stripe/stripe-payment-processor.d.ts +2 -2
  10. package/dist/billing/stripe/stripe-payment-processor.test.js +7 -0
  11. package/dist/billing/stripe/tenant-store.d.ts +1 -1
  12. package/dist/billing/stripe/tenant-store.js +1 -1
  13. package/dist/credits/auto-topup-charge.d.ts +2 -2
  14. package/dist/credits/auto-topup-charge.js +5 -1
  15. package/dist/credits/auto-topup-charge.test.js +5 -4
  16. package/dist/credits/auto-topup-usage.d.ts +2 -2
  17. package/dist/credits/auto-topup-usage.test.js +53 -12
  18. package/dist/credits/credit-expiry-cron.d.ts +2 -2
  19. package/dist/credits/credit-expiry-cron.js +7 -4
  20. package/dist/credits/credit-expiry-cron.test.js +25 -8
  21. package/dist/credits/credit-ledger.d.ts +2 -2
  22. package/dist/credits/credit-ledger.js +1 -1
  23. package/dist/credits/dividend-cron.d.ts +4 -6
  24. package/dist/credits/dividend-cron.js +10 -16
  25. package/dist/credits/dividend-cron.test.js +31 -44
  26. package/dist/credits/dividend-repository.js +19 -22
  27. package/dist/credits/dividend-repository.test.js +4 -3
  28. package/dist/credits/index.d.ts +4 -2
  29. package/dist/credits/index.js +2 -1
  30. package/dist/credits/ledger.d.ts +195 -0
  31. package/dist/credits/ledger.js +561 -0
  32. package/dist/credits/ledger.test.js +418 -0
  33. package/dist/credits/signup-grant.d.ts +2 -2
  34. package/dist/credits/signup-grant.js +4 -4
  35. package/dist/credits/signup-grant.test.js +5 -3
  36. package/dist/credits/trial-balance-cron.d.ts +19 -0
  37. package/dist/credits/trial-balance-cron.js +30 -0
  38. package/dist/credits/trial-balance-cron.test.js +55 -0
  39. package/dist/db/schema/index.d.ts +1 -0
  40. package/dist/db/schema/index.js +1 -0
  41. package/dist/db/schema/ledger.d.ts +442 -0
  42. package/dist/db/schema/ledger.js +76 -0
  43. package/dist/gateway/credit-gate.d.ts +2 -2
  44. package/dist/gateway/credit-gate.js +5 -1
  45. package/dist/gateway/credit-gate.test.js +35 -33
  46. package/dist/gateway/protocol/deps.d.ts +2 -2
  47. package/dist/gateway/proxy.d.ts +2 -2
  48. package/dist/gateway/types.d.ts +2 -2
  49. package/dist/metering/reconciliation-cron.test.js +9 -8
  50. package/dist/metering/reconciliation-repository.js +12 -10
  51. package/dist/metering/reconciliation-repository.test.js +9 -8
  52. package/dist/monetization/affiliate/affiliate-admin-repository.js +10 -8
  53. package/dist/monetization/affiliate/affiliate-admin-repository.test.js +32 -13
  54. package/dist/monetization/affiliate/credit-match.d.ts +2 -2
  55. package/dist/monetization/affiliate/credit-match.js +4 -1
  56. package/dist/monetization/affiliate/credit-match.test.js +58 -13
  57. package/dist/monetization/affiliate/new-user-bonus.d.ts +2 -2
  58. package/dist/monetization/affiliate/new-user-bonus.js +4 -1
  59. package/dist/monetization/affiliate/new-user-bonus.test.js +4 -3
  60. package/dist/monetization/credits/auto-topup-charge.d.ts +2 -2
  61. package/dist/monetization/credits/auto-topup-charge.js +5 -1
  62. package/dist/monetization/credits/auto-topup-charge.test.js +5 -4
  63. package/dist/monetization/credits/auto-topup-usage.d.ts +2 -2
  64. package/dist/monetization/credits/auto-topup-usage.test.js +53 -12
  65. package/dist/monetization/credits/bot-billing.d.ts +3 -3
  66. package/dist/monetization/credits/bot-billing.test.js +18 -5
  67. package/dist/monetization/credits/credit-expiry-cron.test.js +25 -8
  68. package/dist/monetization/credits/dividend-cron.d.ts +2 -4
  69. package/dist/monetization/credits/dividend-cron.js +7 -4
  70. package/dist/monetization/credits/dividend-cron.test.js +26 -46
  71. package/dist/monetization/credits/dividend-repository.js +15 -24
  72. package/dist/monetization/credits/dividend-repository.test.js +4 -3
  73. package/dist/monetization/credits/index.d.ts +2 -2
  74. package/dist/monetization/credits/index.js +1 -1
  75. package/dist/monetization/credits/member-usage.test.js +23 -10
  76. package/dist/monetization/credits/phone-billing.d.ts +2 -2
  77. package/dist/monetization/credits/phone-billing.js +5 -1
  78. package/dist/monetization/credits/phone-billing.test.js +9 -12
  79. package/dist/monetization/credits/runtime-cron.d.ts +2 -2
  80. package/dist/monetization/credits/runtime-cron.js +32 -8
  81. package/dist/monetization/credits/runtime-cron.test.js +28 -27
  82. package/dist/monetization/credits/runtime-scheduler.d.ts +2 -2
  83. package/dist/monetization/credits/runtime-scheduler.test.js +1 -1
  84. package/dist/monetization/credits/signup-grant.test.js +5 -3
  85. package/dist/monetization/credits/storage-tier-cron.test.js +3 -2
  86. package/dist/monetization/credits/trial-balance-cron.test.js +42 -0
  87. package/dist/monetization/feature-gate.d.ts +3 -3
  88. package/dist/monetization/index.d.ts +3 -3
  89. package/dist/monetization/index.js +1 -1
  90. package/dist/monetization/metering/reconciliation-cron.test.js +9 -8
  91. package/dist/monetization/metering/reconciliation-repository.js +11 -10
  92. package/dist/monetization/metering/reconciliation-repository.test.js +9 -8
  93. package/dist/monetization/payram/webhook.d.ts +2 -2
  94. package/dist/monetization/payram/webhook.js +5 -1
  95. package/dist/monetization/payram/webhook.test.js +5 -4
  96. package/dist/monetization/promotions/engine.d.ts +2 -2
  97. package/dist/monetization/promotions/engine.js +4 -1
  98. package/dist/monetization/promotions/engine.test.js +3 -1
  99. package/dist/monetization/repository-types.d.ts +1 -1
  100. package/dist/monetization/stripe/stripe-payment-processor.d.ts +2 -2
  101. package/dist/monetization/stripe/stripe-payment-processor.test.js +7 -0
  102. package/dist/monetization/stripe/webhook.d.ts +2 -2
  103. package/dist/monetization/stripe/webhook.js +70 -6
  104. package/dist/monetization/stripe/webhook.test.js +20 -15
  105. package/dist/onboarding/onboarding-service.d.ts +2 -2
  106. package/dist/onboarding/onboarding-service.js +6 -2
  107. package/drizzle/migrations/0003_double_entry_ledger.sql +82 -0
  108. package/drizzle/migrations/meta/_journal.json +7 -0
  109. package/package.json +1 -1
  110. package/src/api/routes/admin-credits.ts +11 -14
  111. package/src/api/routes/quota.ts +2 -2
  112. package/src/api/routes/verify-email.ts +4 -4
  113. package/src/backup/on-demand-snapshot-service.test.ts +3 -3
  114. package/src/backup/on-demand-snapshot-service.ts +3 -3
  115. package/src/billing/payram/webhook.test.ts +7 -5
  116. package/src/billing/payram/webhook.ts +8 -11
  117. package/src/billing/stripe/stripe-payment-processor.test.ts +10 -3
  118. package/src/billing/stripe/stripe-payment-processor.ts +3 -3
  119. package/src/billing/stripe/tenant-store.ts +1 -1
  120. package/src/credits/auto-topup-charge.test.ts +7 -5
  121. package/src/credits/auto-topup-charge.ts +7 -10
  122. package/src/credits/auto-topup-usage.test.ts +55 -13
  123. package/src/credits/auto-topup-usage.ts +2 -2
  124. package/src/credits/credit-expiry-cron.test.ts +26 -45
  125. package/src/credits/credit-expiry-cron.ts +9 -12
  126. package/src/credits/credit-ledger.ts +3 -3
  127. package/src/credits/dividend-cron.test.ts +38 -45
  128. package/src/credits/dividend-cron.ts +12 -26
  129. package/src/credits/dividend-repository.test.ts +4 -3
  130. package/src/credits/dividend-repository.ts +21 -23
  131. package/src/credits/index.ts +23 -4
  132. package/src/credits/ledger.test.ts +514 -0
  133. package/src/credits/ledger.ts +851 -0
  134. package/src/credits/signup-grant.test.ts +7 -4
  135. package/src/credits/signup-grant.ts +6 -12
  136. package/src/credits/trial-balance-cron.test.ts +68 -0
  137. package/src/credits/trial-balance-cron.ts +46 -0
  138. package/src/db/schema/index.ts +1 -0
  139. package/src/db/schema/ledger.ts +94 -0
  140. package/src/gateway/credit-gate-wiring.test.ts +3 -3
  141. package/src/gateway/credit-gate.test.ts +35 -33
  142. package/src/gateway/credit-gate.ts +6 -10
  143. package/src/gateway/gateway-routes.test.ts +5 -5
  144. package/src/gateway/protocol/deps.ts +2 -2
  145. package/src/gateway/proxy.ts +2 -2
  146. package/src/gateway/route-mounting.test.ts +2 -2
  147. package/src/gateway/types.ts +2 -2
  148. package/src/metering/reconciliation-cron.test.ts +10 -9
  149. package/src/metering/reconciliation-repository.test.ts +10 -9
  150. package/src/metering/reconciliation-repository.ts +14 -11
  151. package/src/monetization/affiliate/affiliate-admin-repository.test.ts +32 -19
  152. package/src/monetization/affiliate/affiliate-admin-repository.ts +16 -8
  153. package/src/monetization/affiliate/credit-match.test.ts +60 -14
  154. package/src/monetization/affiliate/credit-match.ts +6 -9
  155. package/src/monetization/affiliate/new-user-bonus.test.ts +6 -4
  156. package/src/monetization/affiliate/new-user-bonus.ts +6 -9
  157. package/src/monetization/credits/auto-topup-charge.test.ts +7 -5
  158. package/src/monetization/credits/auto-topup-charge.ts +7 -10
  159. package/src/monetization/credits/auto-topup-usage.test.ts +55 -13
  160. package/src/monetization/credits/auto-topup-usage.ts +2 -2
  161. package/src/monetization/credits/bot-billing.test.ts +20 -6
  162. package/src/monetization/credits/bot-billing.ts +3 -3
  163. package/src/monetization/credits/credit-expiry-cron.test.ts +26 -45
  164. package/src/monetization/credits/dividend-cron.test.ts +34 -48
  165. package/src/monetization/credits/dividend-cron.ts +9 -14
  166. package/src/monetization/credits/dividend-repository.test.ts +4 -3
  167. package/src/monetization/credits/dividend-repository.ts +19 -25
  168. package/src/monetization/credits/index.ts +4 -4
  169. package/src/monetization/credits/member-usage.test.ts +25 -11
  170. package/src/monetization/credits/phone-billing.test.ts +18 -26
  171. package/src/monetization/credits/phone-billing.ts +7 -10
  172. package/src/monetization/credits/runtime-cron.test.ts +29 -28
  173. package/src/monetization/credits/runtime-cron.ts +34 -58
  174. package/src/monetization/credits/runtime-scheduler.test.ts +1 -1
  175. package/src/monetization/credits/runtime-scheduler.ts +2 -2
  176. package/src/monetization/credits/signup-grant.test.ts +7 -4
  177. package/src/monetization/credits/storage-tier-cron.test.ts +5 -3
  178. package/src/monetization/credits/trial-balance-cron.test.ts +52 -0
  179. package/src/monetization/feature-gate.ts +3 -3
  180. package/src/monetization/index.ts +4 -4
  181. package/src/monetization/metering/reconciliation-cron.test.ts +10 -9
  182. package/src/monetization/metering/reconciliation-repository.test.ts +11 -9
  183. package/src/monetization/metering/reconciliation-repository.ts +13 -11
  184. package/src/monetization/payram/webhook.test.ts +7 -5
  185. package/src/monetization/payram/webhook.ts +7 -10
  186. package/src/monetization/promotions/engine.test.ts +6 -5
  187. package/src/monetization/promotions/engine.ts +6 -3
  188. package/src/monetization/repository-types.ts +1 -1
  189. package/src/monetization/stripe/stripe-payment-processor.test.ts +10 -3
  190. package/src/monetization/stripe/stripe-payment-processor.ts +3 -3
  191. package/src/monetization/stripe/webhook.test.ts +22 -16
  192. package/src/monetization/stripe/webhook.ts +75 -50
  193. package/src/onboarding/onboarding-service.ts +8 -11
  194. package/dist/credits/credit-ledger-extra.test.js +0 -40
  195. package/dist/credits/credit-ledger.bench.js +0 -33
  196. package/dist/credits/credit-ledger.test.d.ts +0 -4
  197. package/dist/credits/credit-ledger.test.js +0 -203
  198. package/dist/credits/credit-transaction-repository.test.js +0 -232
  199. package/dist/monetization/credits/credit-ledger-extra.test.d.ts +0 -1
  200. package/dist/monetization/credits/credit-ledger-extra.test.js +0 -39
  201. package/dist/monetization/credits/credit-ledger.bench.d.ts +0 -1
  202. package/dist/monetization/credits/credit-ledger.bench.js +0 -32
  203. package/dist/monetization/credits/credit-ledger.test.d.ts +0 -4
  204. package/dist/monetization/credits/credit-ledger.test.js +0 -202
  205. package/dist/monetization/credits/credit-transaction-repository.test.d.ts +0 -1
  206. package/dist/monetization/credits/credit-transaction-repository.test.js +0 -232
  207. package/src/credits/credit-ledger-extra.test.ts +0 -57
  208. package/src/credits/credit-ledger.bench.ts +0 -56
  209. package/src/credits/credit-ledger.test.ts +0 -276
  210. package/src/credits/credit-transaction-repository.test.ts +0 -274
  211. package/src/monetization/credits/credit-ledger-extra.test.ts +0 -56
  212. package/src/monetization/credits/credit-ledger.bench.ts +0 -55
  213. package/src/monetization/credits/credit-ledger.test.ts +0 -275
  214. package/src/monetization/credits/credit-transaction-repository.test.ts +0 -274
  215. /package/dist/credits/{credit-ledger-extra.test.d.ts → ledger.test.d.ts} +0 -0
  216. /package/dist/credits/{credit-ledger.bench.d.ts → trial-balance-cron.test.d.ts} +0 -0
  217. /package/dist/{credits/credit-transaction-repository.test.d.ts → monetization/credits/trial-balance-cron.test.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
1
+ import type { ILedger } from "@wopr-network/platform-core/credits";
2
2
  import { Credit, InsufficientBalanceError } from "@wopr-network/platform-core/credits";
3
3
  import { logger } from "../../config/logger.js";
4
4
  import type { IBotInstanceRepository } from "../../fleet/bot-instance-repository.js";
@@ -20,7 +20,7 @@ export type GetActiveBotCount = (tenantId: string) => number | Promise<number>;
20
20
  export const LOW_BALANCE_THRESHOLD = Credit.fromCents(100);
21
21
 
22
22
  export interface RuntimeCronConfig {
23
- ledger: ICreditLedger;
23
+ ledger: ILedger;
24
24
  getActiveBotCount: GetActiveBotCount;
25
25
  /** The date being billed, as YYYY-MM-DD. Used for idempotency. */
26
26
  date: string;
@@ -122,13 +122,10 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
122
122
 
123
123
  if (!balance.lessThan(totalCost)) {
124
124
  // Full deduction
125
- await cfg.ledger.debit(
126
- tenantId,
127
- totalCost,
128
- "bot_runtime",
129
- `Daily runtime: ${botCount} bot(s) x $${DAILY_BOT_COST.toDollars().toFixed(2)}`,
130
- runtimeRef,
131
- );
125
+ await cfg.ledger.debit(tenantId, totalCost, "bot_runtime", {
126
+ description: `Daily runtime: ${botCount} bot(s) x $${DAILY_BOT_COST.toDollars().toFixed(2)}`,
127
+ referenceId: runtimeRef,
128
+ });
132
129
 
133
130
  // Debit resource tier surcharges (if any)
134
131
  if (cfg.getResourceTierCosts) {
@@ -136,21 +133,15 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
136
133
  if (!tierCost.isZero()) {
137
134
  const balanceAfterRuntime = await cfg.ledger.balance(tenantId);
138
135
  if (!balanceAfterRuntime.lessThan(tierCost)) {
139
- await cfg.ledger.debit(
140
- tenantId,
141
- tierCost,
142
- "resource_upgrade",
143
- "Daily resource tier surcharge",
144
- `runtime-tier:${cfg.date}:${tenantId}`,
145
- );
136
+ await cfg.ledger.debit(tenantId, tierCost, "resource_upgrade", {
137
+ description: "Daily resource tier surcharge",
138
+ referenceId: `runtime-tier:${cfg.date}:${tenantId}`,
139
+ });
146
140
  } else if (balanceAfterRuntime.greaterThan(Credit.ZERO)) {
147
- await cfg.ledger.debit(
148
- tenantId,
149
- balanceAfterRuntime,
150
- "resource_upgrade",
151
- "Partial resource tier surcharge (balance exhausted)",
152
- `runtime-tier:${cfg.date}:${tenantId}`,
153
- );
141
+ await cfg.ledger.debit(tenantId, balanceAfterRuntime, "resource_upgrade", {
142
+ description: "Partial resource tier surcharge (balance exhausted)",
143
+ referenceId: `runtime-tier:${cfg.date}:${tenantId}`,
144
+ });
154
145
  }
155
146
  }
156
147
  }
@@ -190,23 +181,17 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
190
181
  if (!storageCost.isZero()) {
191
182
  const currentBalance = await cfg.ledger.balance(tenantId);
192
183
  if (!currentBalance.lessThan(storageCost)) {
193
- await cfg.ledger.debit(
194
- tenantId,
195
- storageCost,
196
- "storage_upgrade",
197
- "Daily storage tier surcharge",
198
- `runtime-storage:${cfg.date}:${tenantId}`,
199
- );
184
+ await cfg.ledger.debit(tenantId, storageCost, "storage_upgrade", {
185
+ description: "Daily storage tier surcharge",
186
+ referenceId: `runtime-storage:${cfg.date}:${tenantId}`,
187
+ });
200
188
  } else {
201
189
  // Partial debit — take what's left, then suspend
202
190
  if (currentBalance.greaterThan(Credit.ZERO)) {
203
- await cfg.ledger.debit(
204
- tenantId,
205
- currentBalance,
206
- "storage_upgrade",
207
- "Partial storage tier surcharge (balance exhausted)",
208
- `runtime-storage:${cfg.date}:${tenantId}`,
209
- );
191
+ await cfg.ledger.debit(tenantId, currentBalance, "storage_upgrade", {
192
+ description: "Partial storage tier surcharge (balance exhausted)",
193
+ referenceId: `runtime-storage:${cfg.date}:${tenantId}`,
194
+ });
210
195
  }
211
196
  if (!result.suspended.includes(tenantId)) {
212
197
  result.suspended.push(tenantId);
@@ -222,23 +207,17 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
222
207
  if (!addonCost.isZero()) {
223
208
  const currentBalance = await cfg.ledger.balance(tenantId);
224
209
  if (!currentBalance.lessThan(addonCost)) {
225
- await cfg.ledger.debit(
226
- tenantId,
227
- addonCost,
228
- "addon",
229
- "Daily infrastructure add-on charges",
230
- `runtime-addon:${cfg.date}:${tenantId}`,
231
- );
210
+ await cfg.ledger.debit(tenantId, addonCost, "addon", {
211
+ description: "Daily infrastructure add-on charges",
212
+ referenceId: `runtime-addon:${cfg.date}:${tenantId}`,
213
+ });
232
214
  } else {
233
215
  // Partial debit — take what's left, then suspend
234
216
  if (currentBalance.greaterThan(Credit.ZERO)) {
235
- await cfg.ledger.debit(
236
- tenantId,
237
- currentBalance,
238
- "addon",
239
- "Partial add-on charges (balance exhausted)",
240
- `runtime-addon:${cfg.date}:${tenantId}`,
241
- );
217
+ await cfg.ledger.debit(tenantId, currentBalance, "addon", {
218
+ description: "Partial add-on charges (balance exhausted)",
219
+ referenceId: `runtime-addon:${cfg.date}:${tenantId}`,
220
+ });
242
221
  }
243
222
  if (!result.suspended.includes(tenantId)) {
244
223
  result.suspended.push(tenantId);
@@ -250,13 +229,10 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
250
229
  } else {
251
230
  // Partial deduction — debit remaining balance, then suspend
252
231
  if (balance.greaterThan(Credit.ZERO)) {
253
- await cfg.ledger.debit(
254
- tenantId,
255
- balance,
256
- "bot_runtime",
257
- `Partial daily runtime (balance exhausted): ${botCount} bot(s)`,
258
- runtimeRef,
259
- );
232
+ await cfg.ledger.debit(tenantId, balance, "bot_runtime", {
233
+ description: `Partial daily runtime (balance exhausted): ${botCount} bot(s)`,
234
+ referenceId: runtimeRef,
235
+ });
260
236
  }
261
237
 
262
238
  if (cfg.onCreditsExhausted) {
@@ -1,7 +1,7 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { RUNTIME_INTERVAL_MS, startRuntimeScheduler } from "./runtime-scheduler.js";
3
3
 
4
- // Minimal ICreditLedger stub — only the methods runRuntimeDeductions calls.
4
+ // Minimal ILedger stub — only the methods runRuntimeDeductions calls.
5
5
  function makeLedger() {
6
6
  return {
7
7
  tenantsWithBalance: vi.fn().mockResolvedValue([]),
@@ -1,4 +1,4 @@
1
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
1
+ import type { ILedger } from "@wopr-network/platform-core/credits";
2
2
  import { logger } from "../../config/logger.js";
3
3
  import type { IBotInstanceRepository } from "../../fleet/bot-instance-repository.js";
4
4
  import { buildAddonCosts } from "../addons/addon-cron.js";
@@ -6,7 +6,7 @@ import type { ITenantAddonRepository } from "../addons/addon-repository.js";
6
6
  import { buildResourceTierCosts, runRuntimeDeductions } from "./runtime-cron.js";
7
7
 
8
8
  export interface RuntimeSchedulerDeps {
9
- ledger: ICreditLedger;
9
+ ledger: ILedger;
10
10
  botInstanceRepo: IBotInstanceRepository;
11
11
  tenantAddonRepo: ITenantAddonRepository;
12
12
  onSuspend?: (tenantId: string) => void;
@@ -1,5 +1,5 @@
1
1
  import type { PGlite } from "@electric-sql/pglite";
2
- import { CreditLedger, grantSignupCredits, SIGNUP_GRANT } from "@wopr-network/platform-core/credits";
2
+ import { DrizzleLedger, grantSignupCredits, SIGNUP_GRANT } from "@wopr-network/platform-core/credits";
3
3
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
4
4
  import type { DrizzleDb } from "../../db/index.js";
5
5
  import { createTestDb, truncateAllTables } from "../../test/db.js";
@@ -7,7 +7,7 @@ import { createTestDb, truncateAllTables } from "../../test/db.js";
7
7
  describe("grantSignupCredits", () => {
8
8
  let pool: PGlite;
9
9
  let db: DrizzleDb;
10
- let ledger: CreditLedger;
10
+ let ledger: DrizzleLedger;
11
11
 
12
12
  beforeAll(async () => {
13
13
  ({ db, pool } = await createTestDb());
@@ -19,7 +19,9 @@ describe("grantSignupCredits", () => {
19
19
 
20
20
  beforeEach(async () => {
21
21
  await truncateAllTables(pool);
22
- ledger = new CreditLedger(db);
22
+ ledger = new DrizzleLedger(db);
23
+
24
+ await ledger.seedSystemAccounts();
23
25
  });
24
26
 
25
27
  it("grants credits to a new tenant and returns true", async () => {
@@ -52,7 +54,8 @@ describe("grantSignupCredits", () => {
52
54
  const uniqueErr = Object.assign(new Error("duplicate key value violates unique constraint"), {
53
55
  code: "23505",
54
56
  });
55
- const racingLedger = new CreditLedger(db);
57
+ const racingLedger = new DrizzleLedger(db);
58
+ await racingLedger.seedSystemAccounts();
56
59
  vi.spyOn(racingLedger, "hasReferenceId").mockResolvedValue(false);
57
60
  vi.spyOn(racingLedger, "credit").mockRejectedValue(uniqueErr);
58
61
 
@@ -1,5 +1,5 @@
1
1
  import type { PGlite } from "@electric-sql/pglite";
2
- import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
2
+ import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
3
3
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
4
4
  import type { DrizzleDb } from "../../db/index.js";
5
5
  import { createTestDb, truncateAllTables } from "../../test/db.js";
@@ -9,7 +9,7 @@ describe("runtime cron with storage tiers", () => {
9
9
  const TODAY = "2025-01-01";
10
10
  let pool: PGlite;
11
11
  let db: DrizzleDb;
12
- let ledger: CreditLedger;
12
+ let ledger: DrizzleLedger;
13
13
 
14
14
  beforeAll(async () => {
15
15
  ({ db, pool } = await createTestDb());
@@ -21,7 +21,9 @@ describe("runtime cron with storage tiers", () => {
21
21
 
22
22
  beforeEach(async () => {
23
23
  await truncateAllTables(pool);
24
- ledger = new CreditLedger(db);
24
+ ledger = new DrizzleLedger(db);
25
+
26
+ await ledger.seedSystemAccounts();
25
27
  });
26
28
 
27
29
  it("debits base cost plus storage surcharge for pro tier", async () => {
@@ -0,0 +1,52 @@
1
+ import type { PGlite } from "@electric-sql/pglite";
2
+ import { Credit, DrizzleLedger, runTrialBalanceCron } from "@wopr-network/platform-core/credits";
3
+ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { createTestDb, truncateAllTables } from "../../test/db.js";
5
+
6
+ describe("runTrialBalanceCron", () => {
7
+ let pool: PGlite;
8
+ let ledger: DrizzleLedger;
9
+
10
+ beforeAll(async () => {
11
+ const { db, pool: p } = await createTestDb();
12
+ pool = p;
13
+ ledger = new DrizzleLedger(db);
14
+ });
15
+
16
+ afterAll(async () => {
17
+ await pool.close();
18
+ });
19
+
20
+ beforeEach(async () => {
21
+ await truncateAllTables(pool);
22
+ await ledger.seedSystemAccounts();
23
+ });
24
+
25
+ it("returns balanced when no entries exist", async () => {
26
+ const result = await runTrialBalanceCron({ ledger });
27
+ expect(result.balanced).toBe(true);
28
+ expect(result.differenceRaw).toBe(0);
29
+ });
30
+
31
+ it("returns balanced after normal credit and debit", async () => {
32
+ await ledger.credit("t1", Credit.fromCents(500), "purchase");
33
+ await ledger.debit("t1", Credit.fromCents(200), "bot_runtime");
34
+
35
+ const result = await runTrialBalanceCron({ ledger });
36
+ expect(result.balanced).toBe(true);
37
+ expect(result.differenceRaw).toBe(0);
38
+ });
39
+
40
+ it("logs an error on imbalance without throwing", async () => {
41
+ vi.spyOn(ledger, "trialBalance").mockResolvedValueOnce({
42
+ totalDebits: Credit.fromCents(1000),
43
+ totalCredits: Credit.fromCents(900),
44
+ balanced: false,
45
+ difference: Credit.fromCents(100),
46
+ });
47
+
48
+ const result = await runTrialBalanceCron({ ledger });
49
+ expect(result.balanced).toBe(false);
50
+ expect(result.differenceRaw).toBe(Credit.fromCents(100).toRaw());
51
+ });
52
+ });
@@ -1,4 +1,4 @@
1
- import type { CreditLedger } from "@wopr-network/platform-core/credits";
1
+ import type { ILedger } from "@wopr-network/platform-core/credits";
2
2
  import { Credit } from "@wopr-network/platform-core/credits";
3
3
  import type { Context, Next } from "hono";
4
4
  import { DAILY_BOT_COST } from "./credits/runtime-cron.js";
@@ -74,7 +74,7 @@ export function createFeatureGate(cfg: FeatureGateConfig) {
74
74
  /**
75
75
  * Convenience factory that creates a requireBalance middleware from a CreditLedger instance.
76
76
  */
77
- export function createBalanceGate(ledger: CreditLedger, userKey?: string, userIdField?: string) {
77
+ export function createBalanceGate(ledger: ILedger, userKey?: string, userIdField?: string) {
78
78
  return createFeatureGate({
79
79
  getUserBalance: (tenantId) => ledger.balance(tenantId),
80
80
  userKey,
@@ -94,7 +94,7 @@ export type ResolveTenantId = (c: Context) => string | undefined | Promise<strin
94
94
 
95
95
  export interface CreditGateConfig {
96
96
  /** CreditLedger instance used to check balance. */
97
- ledger: CreditLedger;
97
+ ledger: ILedger;
98
98
  /** Resolve the tenant ID from the request context. */
99
99
  resolveTenantId: ResolveTenantId;
100
100
  }
@@ -134,11 +134,12 @@ export { BudgetChecker, DrizzleBudgetChecker } from "./budget/index.js";
134
134
  // Credit ledger (WOP-384)
135
135
  export type {
136
136
  BillingState,
137
- CreditTransaction,
138
137
  CreditType,
139
138
  DebitType,
140
139
  GetActiveBotCount,
141
140
  HistoryOptions,
141
+ ILedger,
142
+ JournalEntry,
142
143
  OnSuspend,
143
144
  RuntimeCronConfig,
144
145
  RuntimeCronResult,
@@ -147,12 +148,12 @@ export type {
147
148
  export {
148
149
  BotBilling,
149
150
  buildResourceTierCosts,
150
- CreditLedger,
151
151
  DAILY_BOT_COST,
152
152
  DrizzleBotBilling,
153
- DrizzleCreditLedger,
153
+ DrizzleLedger,
154
154
  grantSignupCredits,
155
155
  InsufficientBalanceError,
156
+ Ledger,
156
157
  runRuntimeDeductions,
157
158
  SIGNUP_GRANT,
158
159
  SUSPENSION_GRACE_DAYS,
@@ -215,7 +216,6 @@ export {
215
216
  export type {
216
217
  IBotBilling,
217
218
  IBudgetChecker,
218
- ICreditLedger,
219
219
  IMeterAggregator,
220
220
  IMeterEmitter,
221
221
  IPayRamChargeRepository,
@@ -1,6 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import type { PGlite } from "@electric-sql/pglite";
3
- import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
3
+ import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
4
4
  import { runReconciliation } from "@wopr-network/platform-core/metering";
5
5
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
6
6
  import type { DrizzleDb } from "../../db/index.js";
@@ -17,7 +17,7 @@ const DAY_END = DAY_START + 24 * 60 * 60 * 1000;
17
17
  describe("runReconciliation", () => {
18
18
  let pool: PGlite;
19
19
  let db: DrizzleDb;
20
- let ledger: CreditLedger;
20
+ let ledger: DrizzleLedger;
21
21
  let usageSummaryRepo: DrizzleUsageSummaryRepository;
22
22
  let adapterUsageRepo: DrizzleAdapterUsageRepository;
23
23
 
@@ -25,7 +25,7 @@ describe("runReconciliation", () => {
25
25
  const t = await createTestDb();
26
26
  pool = t.pool;
27
27
  db = t.db;
28
- ledger = new CreditLedger(db);
28
+ ledger = new DrizzleLedger(db);
29
29
  usageSummaryRepo = new DrizzleUsageSummaryRepository(db);
30
30
  adapterUsageRepo = new DrizzleAdapterUsageRepository(db);
31
31
  });
@@ -36,6 +36,7 @@ describe("runReconciliation", () => {
36
36
 
37
37
  beforeEach(async () => {
38
38
  await truncateAllTables(pool);
39
+ await ledger.seedSystemAccounts();
39
40
  });
40
41
 
41
42
  /** Insert a usage_summaries row directly. */
@@ -74,7 +75,7 @@ describe("runReconciliation", () => {
74
75
  await insertSummary({ tenant: "t1", totalCharge: charge.toRaw() });
75
76
 
76
77
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
77
- await ledger.debit("t1", charge, "adapter_usage", "chat usage");
78
+ await ledger.debit("t1", charge, "adapter_usage", { description: "chat usage" });
78
79
 
79
80
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
80
81
  expect(result.tenantsChecked).toBe(1);
@@ -85,7 +86,7 @@ describe("runReconciliation", () => {
85
86
  await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(100).toRaw() });
86
87
 
87
88
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
88
- await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", "chat usage");
89
+ await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", { description: "chat usage" });
89
90
 
90
91
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
91
92
  expect(result.tenantsChecked).toBe(1);
@@ -117,7 +118,7 @@ describe("runReconciliation", () => {
117
118
 
118
119
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
119
120
  // Debit as bot_runtime — should NOT count toward reconciliation
120
- await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", "daily runtime");
121
+ await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", { description: "daily runtime" });
121
122
 
122
123
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
123
124
  // Metered 20c, ledger adapter_usage = 0 => drift = 20c
@@ -149,12 +150,12 @@ describe("runReconciliation", () => {
149
150
  // t1: balanced
150
151
  await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(50).toRaw() });
151
152
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
152
- await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", "chat");
153
+ await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", { description: "chat" });
153
154
 
154
155
  // t2: drifted
155
156
  await insertSummary({ tenant: "t2", totalCharge: Credit.fromCents(100).toRaw() });
156
157
  await ledger.credit("t2", Credit.fromCents(500), "purchase");
157
- await ledger.debit("t2", Credit.fromCents(60), "adapter_usage", "chat");
158
+ await ledger.debit("t2", Credit.fromCents(60), "adapter_usage", { description: "chat" });
158
159
 
159
160
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
160
161
  expect(result.tenantsChecked).toBe(2);
@@ -192,7 +193,7 @@ describe("runReconciliation", () => {
192
193
  await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(50).toRaw() });
193
194
 
194
195
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
195
- await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", "chat usage");
196
+ await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", { description: "chat usage" });
196
197
 
197
198
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
198
199
  expect(result.discrepancies).toHaveLength(1);
@@ -1,6 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import type { PGlite } from "@electric-sql/pglite";
3
- import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
3
+ import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
4
4
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
5
5
  import type { DrizzleDb } from "../../db/index.js";
6
6
  import { createTestDb, seedUsageSummary, truncateAllTables } from "../../test/db.js";
@@ -125,12 +125,14 @@ describe("DrizzleUsageSummaryRepository", () => {
125
125
 
126
126
  describe("DrizzleAdapterUsageRepository", () => {
127
127
  let repo: DrizzleAdapterUsageRepository;
128
- let ledger: CreditLedger;
128
+ let ledger: DrizzleLedger;
129
129
 
130
130
  beforeEach(async () => {
131
131
  await truncateAllTables(pool);
132
132
  repo = new DrizzleAdapterUsageRepository(db);
133
- ledger = new CreditLedger(db);
133
+ ledger = new DrizzleLedger(db);
134
+
135
+ await ledger.seedSystemAccounts();
134
136
  });
135
137
 
136
138
  it("returns empty array when no adapter_usage debits exist", async () => {
@@ -146,9 +148,9 @@ describe("DrizzleAdapterUsageRepository", () => {
146
148
  await ledger.credit("t1", Credit.fromCents(1000), "purchase");
147
149
  await ledger.credit("t2", Credit.fromCents(1000), "purchase");
148
150
 
149
- await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", "t1-debit-1");
150
- await ledger.debit("t1", Credit.fromCents(20), "adapter_usage", "t1-debit-2");
151
- await ledger.debit("t2", Credit.fromCents(50), "adapter_usage", "t2-debit-1");
151
+ await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", { description: "t1-debit-1" });
152
+ await ledger.debit("t1", Credit.fromCents(20), "adapter_usage", { description: "t1-debit-2" });
153
+ await ledger.debit("t2", Credit.fromCents(50), "adapter_usage", { description: "t2-debit-1" });
152
154
 
153
155
  // Query window covering today
154
156
  const today = new Date().toISOString().slice(0, 10);
@@ -167,8 +169,8 @@ describe("DrizzleAdapterUsageRepository", () => {
167
169
 
168
170
  it("excludes non-adapter_usage debit types", async () => {
169
171
  await ledger.credit("t1", Credit.fromCents(1000), "purchase");
170
- await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", "adapter debit");
171
- await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", "runtime debit");
172
+ await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", { description: "adapter debit" });
173
+ await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", { description: "runtime debit" });
172
174
 
173
175
  const today = new Date().toISOString().slice(0, 10);
174
176
  const startIso = `${today}T00:00:00Z`;
@@ -181,7 +183,7 @@ describe("DrizzleAdapterUsageRepository", () => {
181
183
 
182
184
  it("excludes credit transactions (positive amounts are not debits)", async () => {
183
185
  await ledger.credit("t1", Credit.fromCents(1000), "purchase");
184
- await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "real debit");
186
+ await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", { description: "real debit" });
185
187
 
186
188
  const today = new Date().toISOString().slice(0, 10);
187
189
  const startIso = `${today}T00:00:00Z`;
@@ -1,6 +1,6 @@
1
1
  import { and, eq, gte, lt, ne, sql } from "drizzle-orm";
2
2
  import type { DrizzleDb } from "../../db/index.js";
3
- import { creditTransactions } from "../../db/schema/credits.js";
3
+ import { journalEntries, journalLines } from "../../db/schema/ledger.js";
4
4
  import { usageSummaries } from "../../db/schema/meter-events.js";
5
5
 
6
6
  // ---------------------------------------------------------------------------
@@ -59,24 +59,26 @@ export class DrizzleAdapterUsageRepository implements IAdapterUsageRepository {
59
59
  constructor(private readonly db: DrizzleDb) {}
60
60
 
61
61
  async getAggregatedAdapterUsageDebits(startIso: string, endIso: string): Promise<AggregatedDebit[]> {
62
+ // Sum the debit-side journal line amounts for adapter_usage entries.
63
+ // In double-entry: DR tenant liability (2000:<tenantId>), CR revenue:adapter_usage (4010).
62
64
  const rows = await this.db
63
65
  .select({
64
- tenantId: creditTransactions.tenantId,
65
- // amount_credits stores negative values for debits; ABS gives the raw positive debit amount.
66
- // Use the raw column name in sql to bypass the custom creditColumn type serializer.
67
- // raw SQL: Drizzle cannot express ABS with COALESCE and SUM
68
- totalDebitRaw: sql<number>`COALESCE(SUM(ABS(amount_credits)), 0)`,
66
+ tenantId: journalEntries.tenantId,
67
+ // raw SQL: Drizzle cannot express COALESCE with SUM aggregation
68
+ totalDebitRaw: sql<number>`COALESCE(SUM(${journalLines.amount}), 0)`,
69
69
  })
70
- .from(creditTransactions)
70
+ .from(journalLines)
71
+ .innerJoin(journalEntries, eq(journalEntries.id, journalLines.journalEntryId))
71
72
  .where(
72
73
  and(
73
- eq(creditTransactions.type, "adapter_usage"),
74
+ eq(journalEntries.entryType, "adapter_usage"),
75
+ eq(journalLines.side, "debit"),
74
76
  // raw SQL: Drizzle cannot express timestamptz cast for text column date comparison
75
- sql`${creditTransactions.createdAt}::timestamptz >= ${startIso}::timestamptz`,
76
- sql`${creditTransactions.createdAt}::timestamptz < ${endIso}::timestamptz`,
77
+ sql`${journalEntries.postedAt}::timestamptz >= ${startIso}::timestamptz`,
78
+ sql`${journalEntries.postedAt}::timestamptz < ${endIso}::timestamptz`,
77
79
  ),
78
80
  )
79
- .groupBy(creditTransactions.tenantId);
81
+ .groupBy(journalEntries.tenantId);
80
82
 
81
83
  return rows.map((r) => ({ tenantId: r.tenantId, totalDebitRaw: Number(r.totalDebitRaw) }));
82
84
  }
@@ -12,7 +12,7 @@ import {
12
12
  noOpReplayGuard,
13
13
  PayRamChargeRepository,
14
14
  } from "@wopr-network/platform-core/billing";
15
- import { CreditLedger } from "@wopr-network/platform-core/credits";
15
+ import { DrizzleLedger } from "@wopr-network/platform-core/credits";
16
16
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
17
17
  import type { DrizzleDb } from "../../db/index.js";
18
18
  import { createTestDb, truncateAllTables } from "../../test/db.js";
@@ -44,13 +44,15 @@ afterAll(async () => {
44
44
 
45
45
  describe("handlePayRamWebhook", () => {
46
46
  let chargeStore: PayRamChargeRepository;
47
- let creditLedger: CreditLedger;
47
+ let creditLedger: DrizzleLedger;
48
48
  let deps: PayRamWebhookDeps;
49
49
 
50
50
  beforeEach(async () => {
51
51
  await truncateAllTables(pool);
52
52
  chargeStore = new PayRamChargeRepository(db);
53
- creditLedger = new CreditLedger(db);
53
+ creditLedger = new DrizzleLedger(db);
54
+
55
+ await creditLedger.seedSystemAccounts();
54
56
  deps = { chargeStore, creditLedger, replayGuard: noOpReplayGuard };
55
57
 
56
58
  // Create a default test charge
@@ -80,14 +82,14 @@ describe("handlePayRamWebhook", () => {
80
82
  const history = await creditLedger.history("tenant-a");
81
83
  expect(history).toHaveLength(1);
82
84
  expect(history[0].referenceId).toBe("payram:ref-test-001");
83
- expect(history[0].type).toBe("purchase");
85
+ expect(history[0].entryType).toBe("purchase");
84
86
  });
85
87
 
86
88
  it("records fundingSource as payram", async () => {
87
89
  await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
88
90
 
89
91
  const history = await creditLedger.history("tenant-a");
90
- expect(history[0].fundingSource).toBe("payram");
92
+ expect(history[0].metadata?.fundingSource).toBe("payram");
91
93
  });
92
94
 
93
95
  it("marks the charge as credited after FILLED", async () => {
@@ -4,13 +4,13 @@ import type {
4
4
  PayRamWebhookPayload,
5
5
  PayRamWebhookResult,
6
6
  } from "@wopr-network/platform-core/billing";
7
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
7
+ import type { ILedger } from "@wopr-network/platform-core/credits";
8
8
  import { Credit } from "@wopr-network/platform-core/credits";
9
9
  import type { BotBilling } from "../credits/bot-billing.js";
10
10
 
11
11
  export interface PayRamWebhookDeps {
12
12
  chargeStore: PayRamChargeRepository;
13
- creditLedger: ICreditLedger;
13
+ creditLedger: ILedger;
14
14
  botBilling?: BotBilling;
15
15
  replayGuard: IWebhookSeenRepository;
16
16
  }
@@ -60,14 +60,11 @@ export async function handlePayRamWebhook(
60
60
  // overpayment stays in the PayRam wallet as a buffer.
61
61
  const creditCents = charge.amountUsdCents;
62
62
 
63
- await creditLedger.credit(
64
- charge.tenantId,
65
- Credit.fromCents(creditCents),
66
- "purchase",
67
- `Crypto credit purchase via PayRam (ref: ${payload.reference_id}, ${payload.currency ?? "crypto"})`,
68
- `payram:${payload.reference_id}`,
69
- "payram",
70
- );
63
+ await creditLedger.credit(charge.tenantId, Credit.fromCents(creditCents), "purchase", {
64
+ description: `Crypto credit purchase via PayRam (ref: ${payload.reference_id}, ${payload.currency ?? "crypto"})`,
65
+ referenceId: `payram:${payload.reference_id}`,
66
+ fundingSource: "payram",
67
+ });
71
68
 
72
69
  await chargeStore.markCredited(payload.reference_id);
73
70
 
@@ -1,4 +1,4 @@
1
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
1
+ import type { ILedger } from "@wopr-network/platform-core/credits";
2
2
  import { Credit } from "@wopr-network/platform-core/credits";
3
3
  import { beforeEach, describe, expect, it, vi } from "vitest";
4
4
  import type { ICouponRepository } from "./coupon-repository.js";
@@ -40,7 +40,7 @@ describe("PromotionEngine", () => {
40
40
  let promotionRepo: IPromotionRepository;
41
41
  let couponRepo: ICouponRepository;
42
42
  let redemptionRepo: IRedemptionRepository;
43
- let ledger: ICreditLedger;
43
+ let ledger: ILedger;
44
44
  let engine: PromotionEngine;
45
45
 
46
46
  beforeEach(() => {
@@ -95,7 +95,7 @@ describe("PromotionEngine", () => {
95
95
  createdAt: new Date().toISOString(),
96
96
  }),
97
97
  hasReferenceId: vi.fn().mockResolvedValue(false),
98
- } as unknown as ICreditLedger;
98
+ } as unknown as ILedger;
99
99
 
100
100
  engine = new PromotionEngine({ promotionRepo, couponRepo, redemptionRepo, ledger });
101
101
  });
@@ -111,8 +111,9 @@ describe("PromotionEngine", () => {
111
111
  "tenant-1",
112
112
  expect.any(Object), // Credit instance
113
113
  "promo",
114
- expect.any(String),
115
- "promo:promo-1:tenant-1:1",
114
+ expect.objectContaining({
115
+ referenceId: "promo:promo-1:tenant-1:1",
116
+ }),
116
117
  );
117
118
  });
118
119