@wopr-network/platform-core 1.13.2 → 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 (238) 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/gateway-service-keys.d.ts +109 -0
  40. package/dist/db/schema/gateway-service-keys.js +18 -0
  41. package/dist/db/schema/index.d.ts +2 -0
  42. package/dist/db/schema/index.js +2 -0
  43. package/dist/db/schema/ledger.d.ts +442 -0
  44. package/dist/db/schema/ledger.js +76 -0
  45. package/dist/gateway/credit-gate.d.ts +2 -2
  46. package/dist/gateway/credit-gate.js +5 -1
  47. package/dist/gateway/credit-gate.test.js +35 -33
  48. package/dist/gateway/gateway-routes.test.js +1 -1
  49. package/dist/gateway/index.d.ts +2 -0
  50. package/dist/gateway/index.js +1 -0
  51. package/dist/gateway/protocol/anthropic.js +1 -1
  52. package/dist/gateway/protocol/deps.d.ts +5 -5
  53. package/dist/gateway/protocol/openai.js +1 -1
  54. package/dist/gateway/proxy.d.ts +4 -4
  55. package/dist/gateway/route-mounting.test.js +1 -1
  56. package/dist/gateway/service-key-auth.d.ts +1 -1
  57. package/dist/gateway/service-key-auth.js +1 -1
  58. package/dist/gateway/service-key-repository.d.ts +27 -0
  59. package/dist/gateway/service-key-repository.js +64 -0
  60. package/dist/gateway/types.d.ts +5 -5
  61. package/dist/metering/reconciliation-cron.test.js +9 -8
  62. package/dist/metering/reconciliation-repository.js +12 -10
  63. package/dist/metering/reconciliation-repository.test.js +9 -8
  64. package/dist/monetization/affiliate/affiliate-admin-repository.js +10 -8
  65. package/dist/monetization/affiliate/affiliate-admin-repository.test.js +32 -13
  66. package/dist/monetization/affiliate/credit-match.d.ts +2 -2
  67. package/dist/monetization/affiliate/credit-match.js +4 -1
  68. package/dist/monetization/affiliate/credit-match.test.js +58 -13
  69. package/dist/monetization/affiliate/new-user-bonus.d.ts +2 -2
  70. package/dist/monetization/affiliate/new-user-bonus.js +4 -1
  71. package/dist/monetization/affiliate/new-user-bonus.test.js +4 -3
  72. package/dist/monetization/credits/auto-topup-charge.d.ts +2 -2
  73. package/dist/monetization/credits/auto-topup-charge.js +5 -1
  74. package/dist/monetization/credits/auto-topup-charge.test.js +5 -4
  75. package/dist/monetization/credits/auto-topup-usage.d.ts +2 -2
  76. package/dist/monetization/credits/auto-topup-usage.test.js +53 -12
  77. package/dist/monetization/credits/bot-billing.d.ts +3 -3
  78. package/dist/monetization/credits/bot-billing.test.js +18 -5
  79. package/dist/monetization/credits/credit-expiry-cron.test.js +25 -8
  80. package/dist/monetization/credits/dividend-cron.d.ts +2 -4
  81. package/dist/monetization/credits/dividend-cron.js +7 -4
  82. package/dist/monetization/credits/dividend-cron.test.js +26 -46
  83. package/dist/monetization/credits/dividend-repository.js +15 -24
  84. package/dist/monetization/credits/dividend-repository.test.js +4 -3
  85. package/dist/monetization/credits/index.d.ts +2 -2
  86. package/dist/monetization/credits/index.js +1 -1
  87. package/dist/monetization/credits/member-usage.test.js +23 -10
  88. package/dist/monetization/credits/phone-billing.d.ts +2 -2
  89. package/dist/monetization/credits/phone-billing.js +5 -1
  90. package/dist/monetization/credits/phone-billing.test.js +9 -12
  91. package/dist/monetization/credits/runtime-cron.d.ts +2 -2
  92. package/dist/monetization/credits/runtime-cron.js +32 -8
  93. package/dist/monetization/credits/runtime-cron.test.js +28 -27
  94. package/dist/monetization/credits/runtime-scheduler.d.ts +2 -2
  95. package/dist/monetization/credits/runtime-scheduler.test.js +1 -1
  96. package/dist/monetization/credits/signup-grant.test.js +5 -3
  97. package/dist/monetization/credits/storage-tier-cron.test.js +3 -2
  98. package/dist/monetization/credits/trial-balance-cron.test.js +42 -0
  99. package/dist/monetization/feature-gate.d.ts +3 -3
  100. package/dist/monetization/index.d.ts +3 -3
  101. package/dist/monetization/index.js +1 -1
  102. package/dist/monetization/metering/reconciliation-cron.test.js +9 -8
  103. package/dist/monetization/metering/reconciliation-repository.js +11 -10
  104. package/dist/monetization/metering/reconciliation-repository.test.js +9 -8
  105. package/dist/monetization/payram/webhook.d.ts +2 -2
  106. package/dist/monetization/payram/webhook.js +5 -1
  107. package/dist/monetization/payram/webhook.test.js +5 -4
  108. package/dist/monetization/promotions/engine.d.ts +2 -2
  109. package/dist/monetization/promotions/engine.js +4 -1
  110. package/dist/monetization/promotions/engine.test.js +3 -1
  111. package/dist/monetization/repository-types.d.ts +1 -1
  112. package/dist/monetization/socket/socket.d.ts +3 -3
  113. package/dist/monetization/stripe/stripe-payment-processor.d.ts +2 -2
  114. package/dist/monetization/stripe/stripe-payment-processor.test.js +7 -0
  115. package/dist/monetization/stripe/webhook.d.ts +2 -2
  116. package/dist/monetization/stripe/webhook.js +70 -6
  117. package/dist/monetization/stripe/webhook.test.js +20 -15
  118. package/dist/onboarding/onboarding-service.d.ts +2 -2
  119. package/dist/onboarding/onboarding-service.js +6 -2
  120. package/drizzle/migrations/0002_gateway_service_keys.sql +14 -0
  121. package/drizzle/migrations/0003_double_entry_ledger.sql +82 -0
  122. package/drizzle/migrations/meta/_journal.json +14 -0
  123. package/package.json +1 -1
  124. package/src/api/routes/admin-credits.ts +11 -14
  125. package/src/api/routes/quota.ts +2 -2
  126. package/src/api/routes/verify-email.ts +4 -4
  127. package/src/backup/on-demand-snapshot-service.test.ts +3 -3
  128. package/src/backup/on-demand-snapshot-service.ts +3 -3
  129. package/src/billing/payram/webhook.test.ts +7 -5
  130. package/src/billing/payram/webhook.ts +8 -11
  131. package/src/billing/stripe/stripe-payment-processor.test.ts +10 -3
  132. package/src/billing/stripe/stripe-payment-processor.ts +3 -3
  133. package/src/billing/stripe/tenant-store.ts +1 -1
  134. package/src/credits/auto-topup-charge.test.ts +7 -5
  135. package/src/credits/auto-topup-charge.ts +7 -10
  136. package/src/credits/auto-topup-usage.test.ts +55 -13
  137. package/src/credits/auto-topup-usage.ts +2 -2
  138. package/src/credits/credit-expiry-cron.test.ts +26 -45
  139. package/src/credits/credit-expiry-cron.ts +9 -12
  140. package/src/credits/credit-ledger.ts +3 -3
  141. package/src/credits/dividend-cron.test.ts +38 -45
  142. package/src/credits/dividend-cron.ts +12 -26
  143. package/src/credits/dividend-repository.test.ts +4 -3
  144. package/src/credits/dividend-repository.ts +21 -23
  145. package/src/credits/index.ts +23 -4
  146. package/src/credits/ledger.test.ts +514 -0
  147. package/src/credits/ledger.ts +851 -0
  148. package/src/credits/signup-grant.test.ts +7 -4
  149. package/src/credits/signup-grant.ts +6 -12
  150. package/src/credits/trial-balance-cron.test.ts +68 -0
  151. package/src/credits/trial-balance-cron.ts +46 -0
  152. package/src/db/schema/gateway-service-keys.ts +23 -0
  153. package/src/db/schema/index.ts +2 -0
  154. package/src/db/schema/ledger.ts +94 -0
  155. package/src/gateway/credit-gate-wiring.test.ts +3 -3
  156. package/src/gateway/credit-gate.test.ts +35 -33
  157. package/src/gateway/credit-gate.ts +6 -10
  158. package/src/gateway/gateway-routes.test.ts +6 -6
  159. package/src/gateway/index.ts +2 -0
  160. package/src/gateway/protocol/anthropic.ts +2 -2
  161. package/src/gateway/protocol/deps.ts +5 -5
  162. package/src/gateway/protocol/openai.ts +2 -2
  163. package/src/gateway/proxy.ts +4 -4
  164. package/src/gateway/route-mounting.test.ts +3 -3
  165. package/src/gateway/service-key-auth.ts +4 -2
  166. package/src/gateway/service-key-repository.ts +87 -0
  167. package/src/gateway/types.ts +5 -5
  168. package/src/metering/reconciliation-cron.test.ts +10 -9
  169. package/src/metering/reconciliation-repository.test.ts +10 -9
  170. package/src/metering/reconciliation-repository.ts +14 -11
  171. package/src/monetization/affiliate/affiliate-admin-repository.test.ts +32 -19
  172. package/src/monetization/affiliate/affiliate-admin-repository.ts +16 -8
  173. package/src/monetization/affiliate/credit-match.test.ts +60 -14
  174. package/src/monetization/affiliate/credit-match.ts +6 -9
  175. package/src/monetization/affiliate/new-user-bonus.test.ts +6 -4
  176. package/src/monetization/affiliate/new-user-bonus.ts +6 -9
  177. package/src/monetization/credits/auto-topup-charge.test.ts +7 -5
  178. package/src/monetization/credits/auto-topup-charge.ts +7 -10
  179. package/src/monetization/credits/auto-topup-usage.test.ts +55 -13
  180. package/src/monetization/credits/auto-topup-usage.ts +2 -2
  181. package/src/monetization/credits/bot-billing.test.ts +20 -6
  182. package/src/monetization/credits/bot-billing.ts +3 -3
  183. package/src/monetization/credits/credit-expiry-cron.test.ts +26 -45
  184. package/src/monetization/credits/dividend-cron.test.ts +34 -48
  185. package/src/monetization/credits/dividend-cron.ts +9 -14
  186. package/src/monetization/credits/dividend-repository.test.ts +4 -3
  187. package/src/monetization/credits/dividend-repository.ts +19 -25
  188. package/src/monetization/credits/index.ts +4 -4
  189. package/src/monetization/credits/member-usage.test.ts +25 -11
  190. package/src/monetization/credits/phone-billing.test.ts +18 -26
  191. package/src/monetization/credits/phone-billing.ts +7 -10
  192. package/src/monetization/credits/runtime-cron.test.ts +29 -28
  193. package/src/monetization/credits/runtime-cron.ts +34 -58
  194. package/src/monetization/credits/runtime-scheduler.test.ts +1 -1
  195. package/src/monetization/credits/runtime-scheduler.ts +2 -2
  196. package/src/monetization/credits/signup-grant.test.ts +7 -4
  197. package/src/monetization/credits/storage-tier-cron.test.ts +5 -3
  198. package/src/monetization/credits/trial-balance-cron.test.ts +52 -0
  199. package/src/monetization/feature-gate.ts +3 -3
  200. package/src/monetization/index.ts +4 -4
  201. package/src/monetization/metering/reconciliation-cron.test.ts +10 -9
  202. package/src/monetization/metering/reconciliation-repository.test.ts +11 -9
  203. package/src/monetization/metering/reconciliation-repository.ts +13 -11
  204. package/src/monetization/payram/webhook.test.ts +7 -5
  205. package/src/monetization/payram/webhook.ts +7 -10
  206. package/src/monetization/promotions/engine.test.ts +6 -5
  207. package/src/monetization/promotions/engine.ts +6 -3
  208. package/src/monetization/repository-types.ts +1 -1
  209. package/src/monetization/socket/socket.ts +4 -4
  210. package/src/monetization/stripe/stripe-payment-processor.test.ts +10 -3
  211. package/src/monetization/stripe/stripe-payment-processor.ts +3 -3
  212. package/src/monetization/stripe/webhook.test.ts +22 -16
  213. package/src/monetization/stripe/webhook.ts +75 -50
  214. package/src/onboarding/onboarding-service.ts +8 -11
  215. package/dist/credits/credit-ledger-extra.test.js +0 -40
  216. package/dist/credits/credit-ledger.bench.js +0 -33
  217. package/dist/credits/credit-ledger.test.d.ts +0 -4
  218. package/dist/credits/credit-ledger.test.js +0 -203
  219. package/dist/credits/credit-transaction-repository.test.js +0 -232
  220. package/dist/monetization/credits/credit-ledger-extra.test.d.ts +0 -1
  221. package/dist/monetization/credits/credit-ledger-extra.test.js +0 -39
  222. package/dist/monetization/credits/credit-ledger.bench.d.ts +0 -1
  223. package/dist/monetization/credits/credit-ledger.bench.js +0 -32
  224. package/dist/monetization/credits/credit-ledger.test.d.ts +0 -4
  225. package/dist/monetization/credits/credit-ledger.test.js +0 -202
  226. package/dist/monetization/credits/credit-transaction-repository.test.d.ts +0 -1
  227. package/dist/monetization/credits/credit-transaction-repository.test.js +0 -232
  228. package/src/credits/credit-ledger-extra.test.ts +0 -57
  229. package/src/credits/credit-ledger.bench.ts +0 -56
  230. package/src/credits/credit-ledger.test.ts +0 -276
  231. package/src/credits/credit-transaction-repository.test.ts +0 -274
  232. package/src/monetization/credits/credit-ledger-extra.test.ts +0 -56
  233. package/src/monetization/credits/credit-ledger.bench.ts +0 -55
  234. package/src/monetization/credits/credit-ledger.test.ts +0 -275
  235. package/src/monetization/credits/credit-transaction-repository.test.ts +0 -274
  236. /package/dist/credits/{credit-ledger-extra.test.d.ts → ledger.test.d.ts} +0 -0
  237. /package/dist/credits/{credit-ledger.bench.d.ts → trial-balance-cron.test.d.ts} +0 -0
  238. /package/dist/{credits/credit-transaction-repository.test.d.ts → monetization/credits/trial-balance-cron.test.d.ts} +0 -0
@@ -71,7 +71,11 @@ export async function debitCredits(deps, tenantId, costUsd, margin, capability,
71
71
  return;
72
72
  }
73
73
  try {
74
- await deps.creditLedger.debit(tenantId, chargeCredit, "adapter_usage", `Gateway ${capability} via ${provider}`, undefined, true, attributedUserId);
74
+ await deps.creditLedger.debit(tenantId, chargeCredit, "adapter_usage", {
75
+ description: `Gateway ${capability} via ${provider}`,
76
+ allowNegative: true,
77
+ attributedUserId,
78
+ });
75
79
  // Only fire on first zero-crossing (balance was positive before, now ≤ 0)
76
80
  if (deps.onBalanceExhausted) {
77
81
  const newBalance = await deps.creditLedger.balance(tenantId);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Tests for gateway credit gate — grace buffer and credits_exhausted behavior (WOP-821).
3
3
  */
4
- import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
4
+ import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
5
5
  import { Hono } from "hono";
6
6
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
7
7
  import { createTestDb, truncateAllTables } from "../test/db.js";
@@ -39,35 +39,36 @@ afterAll(async () => {
39
39
  describe("creditBalanceCheck grace buffer", () => {
40
40
  beforeEach(async () => {
41
41
  await truncateAllTables(pool);
42
+ await new DrizzleLedger(db).seedSystemAccounts();
42
43
  });
43
44
  it("returns null when balance is above estimated cost (passes)", async () => {
44
- const ledger = new CreditLedger(db);
45
- await ledger.credit("t1", Credit.fromCents(500), "purchase", "setup");
45
+ const ledger = new DrizzleLedger(db);
46
+ await ledger.credit("t1", Credit.fromCents(500), "purchase", { description: "setup" });
46
47
  const c = await buildHonoContext("t1");
47
48
  const deps = { creditLedger: ledger, topUpUrl: "/billing" };
48
49
  expect(await creditBalanceCheck(c, deps, 1)).toBeNull();
49
50
  });
50
51
  it("returns null when balance is zero but within default grace buffer (passes)", async () => {
51
52
  // Balance at exactly 0 — within the -50 grace buffer
52
- const ledger = new CreditLedger(db);
53
- await ledger.credit("t1", Credit.fromCents(10), "purchase", "setup");
54
- await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "drain");
53
+ const ledger = new DrizzleLedger(db);
54
+ await ledger.credit("t1", Credit.fromCents(10), "purchase", { description: "setup" });
55
+ await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", { description: "drain" });
55
56
  const c = await buildHonoContext("t1");
56
57
  const deps = { creditLedger: ledger, topUpUrl: "/billing" };
57
58
  expect(await creditBalanceCheck(c, deps, 0)).toBeNull();
58
59
  });
59
60
  it("returns null when balance is -49 (within 50-cent grace buffer)", async () => {
60
- const ledger = new CreditLedger(db);
61
- await ledger.credit("t1", Credit.fromCents(1), "purchase", "setup");
62
- await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", "drain", undefined, true); // balance = -49
61
+ const ledger = new DrizzleLedger(db);
62
+ await ledger.credit("t1", Credit.fromCents(1), "purchase", { description: "setup" });
63
+ await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", { description: "drain", allowNegative: true }); // balance = -49
63
64
  const c = await buildHonoContext("t1");
64
65
  const deps = { creditLedger: ledger, topUpUrl: "/billing" };
65
66
  expect(await creditBalanceCheck(c, deps, 0)).toBeNull();
66
67
  });
67
68
  it("returns credits_exhausted when balance is at -50 (at grace buffer limit)", async () => {
68
- const ledger = new CreditLedger(db);
69
- await ledger.credit("t1", Credit.fromCents(1), "purchase", "setup");
70
- await ledger.debit("t1", Credit.fromCents(51), "adapter_usage", "drain", undefined, true); // balance = -50
69
+ const ledger = new DrizzleLedger(db);
70
+ await ledger.credit("t1", Credit.fromCents(1), "purchase", { description: "setup" });
71
+ await ledger.debit("t1", Credit.fromCents(51), "adapter_usage", { description: "drain", allowNegative: true }); // balance = -50
71
72
  const c = await buildHonoContext("t1");
72
73
  const deps = { creditLedger: ledger, topUpUrl: "/billing" };
73
74
  const result = await creditBalanceCheck(c, deps, 0);
@@ -75,9 +76,9 @@ describe("creditBalanceCheck grace buffer", () => {
75
76
  expect(result?.code).toBe("credits_exhausted");
76
77
  });
77
78
  it("returns credits_exhausted when balance is at -51 (beyond grace buffer)", async () => {
78
- const ledger = new CreditLedger(db);
79
- await ledger.credit("t1", Credit.fromCents(1), "purchase", "setup");
80
- await ledger.debit("t1", Credit.fromCents(52), "adapter_usage", "drain", undefined, true); // balance = -51
79
+ const ledger = new DrizzleLedger(db);
80
+ await ledger.credit("t1", Credit.fromCents(1), "purchase", { description: "setup" });
81
+ await ledger.debit("t1", Credit.fromCents(52), "adapter_usage", { description: "drain", allowNegative: true }); // balance = -51
81
82
  const c = await buildHonoContext("t1");
82
83
  const deps = { creditLedger: ledger, topUpUrl: "/billing" };
83
84
  const result = await creditBalanceCheck(c, deps, 0);
@@ -85,9 +86,9 @@ describe("creditBalanceCheck grace buffer", () => {
85
86
  expect(result?.code).toBe("credits_exhausted");
86
87
  });
87
88
  it("returns credits_exhausted when custom graceBufferCents=0 and balance is 0", async () => {
88
- const ledger = new CreditLedger(db);
89
- await ledger.credit("t1", Credit.fromCents(10), "purchase", "setup");
90
- await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "drain"); // balance = 0
89
+ const ledger = new DrizzleLedger(db);
90
+ await ledger.credit("t1", Credit.fromCents(10), "purchase", { description: "setup" });
91
+ await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", { description: "drain" }); // balance = 0
91
92
  const c = await buildHonoContext("t1");
92
93
  const deps = { creditLedger: ledger, topUpUrl: "/billing", graceBufferCents: 0 };
93
94
  const result = await creditBalanceCheck(c, deps, 0);
@@ -95,8 +96,8 @@ describe("creditBalanceCheck grace buffer", () => {
95
96
  expect(result?.code).toBe("credits_exhausted");
96
97
  });
97
98
  it("returns insufficient_credits when balance positive but below estimated cost", async () => {
98
- const ledger = new CreditLedger(db);
99
- await ledger.credit("t1", Credit.fromCents(5), "purchase", "setup");
99
+ const ledger = new DrizzleLedger(db);
100
+ await ledger.credit("t1", Credit.fromCents(5), "purchase", { description: "setup" });
100
101
  const c = await buildHonoContext("t1");
101
102
  const deps = { creditLedger: ledger, topUpUrl: "/billing" };
102
103
  const result = await creditBalanceCheck(c, deps, 10);
@@ -110,26 +111,27 @@ describe("creditBalanceCheck grace buffer", () => {
110
111
  describe("debitCredits with allowNegative and onBalanceExhausted", () => {
111
112
  beforeEach(async () => {
112
113
  await truncateAllTables(pool);
114
+ await new DrizzleLedger(db).seedSystemAccounts();
113
115
  });
114
116
  it("debit with cost that would exceed balance succeeds (allowNegative=true)", async () => {
115
- const ledger = new CreditLedger(db);
116
- await ledger.credit("t1", Credit.fromCents(5), "purchase", "setup"); // balance = 5 cents
117
+ const ledger = new DrizzleLedger(db);
118
+ await ledger.credit("t1", Credit.fromCents(5), "purchase", { description: "setup" }); // balance = 5 cents
117
119
  // costUsd = $0.10 = 10 cents, margin = 1.0
118
120
  // This should push balance negative without throwing
119
121
  await expect(debitCredits({ creditLedger: ledger, topUpUrl: "/billing" }, "t1", 0.1, 1.0, "chat-completions", "openrouter")).resolves.not.toThrow();
120
122
  expect((await ledger.balance("t1")).isNegative()).toBe(true);
121
123
  });
122
124
  it("fires onBalanceExhausted when debit causes balance to cross zero", async () => {
123
- const ledger = new CreditLedger(db);
124
- await ledger.credit("t1", Credit.fromCents(5), "purchase", "setup"); // balance = 5 cents
125
+ const ledger = new DrizzleLedger(db);
126
+ await ledger.credit("t1", Credit.fromCents(5), "purchase", { description: "setup" }); // balance = 5 cents
125
127
  const onBalanceExhausted = vi.fn();
126
128
  // costUsd = $0.10 = 10 cents with margin 1.0 → chargeCents = 10, pushes balance to -5
127
129
  await debitCredits({ creditLedger: ledger, topUpUrl: "/billing", onBalanceExhausted }, "t1", 0.1, 1.0, "chat-completions", "openrouter");
128
130
  expect(onBalanceExhausted).toHaveBeenCalledWith("t1", -5);
129
131
  });
130
132
  it("does NOT fire onBalanceExhausted when balance stays positive after debit", async () => {
131
- const ledger = new CreditLedger(db);
132
- await ledger.credit("t1", Credit.fromCents(500), "purchase", "setup"); // balance = 500 cents
133
+ const ledger = new DrizzleLedger(db);
134
+ await ledger.credit("t1", Credit.fromCents(500), "purchase", { description: "setup" }); // balance = 500 cents
133
135
  const onBalanceExhausted = vi.fn();
134
136
  // costUsd = $0.01 = 1 cent → balance stays at 499
135
137
  await debitCredits({ creditLedger: ledger, topUpUrl: "/billing", onBalanceExhausted }, "t1", 0.01, 1.0, "chat-completions", "openrouter");
@@ -137,8 +139,8 @@ describe("debitCredits with allowNegative and onBalanceExhausted", () => {
137
139
  expect((await ledger.balance("t1")).greaterThan(Credit.ZERO)).toBe(true);
138
140
  });
139
141
  it("onBalanceExhausted callback receives correct tenantId and negative balance", async () => {
140
- const ledger = new CreditLedger(db);
141
- await ledger.credit("t1", Credit.fromCents(3), "purchase", "setup"); // balance = 3 cents
142
+ const ledger = new DrizzleLedger(db);
143
+ await ledger.credit("t1", Credit.fromCents(3), "purchase", { description: "setup" }); // balance = 3 cents
142
144
  const onBalanceExhausted = vi.fn();
143
145
  // costUsd = $0.05 = 5 cents with margin 1.0 → pushes balance to -2
144
146
  await debitCredits({ creditLedger: ledger, topUpUrl: "/billing", onBalanceExhausted }, "t1", 0.05, 1.0, "chat-completions", "openrouter");
@@ -146,17 +148,17 @@ describe("debitCredits with allowNegative and onBalanceExhausted", () => {
146
148
  expect(onBalanceExhausted).toHaveBeenCalledWith("t1", -2);
147
149
  });
148
150
  it("calls onSpendAlertCrossed after successful debit", async () => {
149
- const ledger = new CreditLedger(db);
150
- await ledger.credit("t1", Credit.fromCents(500), "purchase", "setup");
151
+ const ledger = new DrizzleLedger(db);
152
+ await ledger.credit("t1", Credit.fromCents(500), "purchase", { description: "setup" });
151
153
  const onSpendAlertCrossed = vi.fn();
152
154
  await debitCredits({ creditLedger: ledger, topUpUrl: "/billing", onSpendAlertCrossed }, "t1", 0.05, 1.0, "chat-completions", "openrouter");
153
155
  expect(onSpendAlertCrossed).toHaveBeenCalledWith("t1");
154
156
  });
155
157
  it("does NOT fire onBalanceExhausted when balance was already negative before debit", async () => {
156
- const ledger = new CreditLedger(db);
158
+ const ledger = new DrizzleLedger(db);
157
159
  // Start with negative balance: credit 5, debit 10 → balance = -5
158
- await ledger.credit("t1", Credit.fromCents(5), "purchase", "setup");
159
- await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "drain", undefined, true);
160
+ await ledger.credit("t1", Credit.fromCents(5), "purchase", { description: "setup" });
161
+ await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", { description: "drain", allowNegative: true });
160
162
  const onBalanceExhausted = vi.fn();
161
163
  // Another debit of 1 cent — balance goes from -5 to -6, but was already negative
162
164
  await debitCredits({ creditLedger: ledger, topUpUrl: "/billing", onBalanceExhausted }, "t1", 0.01, 1.0, "chat-completions", "openrouter");
@@ -55,7 +55,7 @@ function buildTestConfig(overrides = {}) {
55
55
  }));
56
56
  return {
57
57
  meter: meter,
58
- budgetChecker: budgetChecker,
58
+ budgetChecker,
59
59
  creditLedger,
60
60
  providers: { openrouter: { apiKey: "or-test-key" } },
61
61
  fetchFn,
@@ -16,6 +16,8 @@ export { anthropicToOpenAI, createAnthropicRoutes, createOpenAIRoutes, estimateA
16
16
  export { buildProxyDeps, type ProxyDeps, phoneNumberList, phoneNumberProvision, phoneNumberRelease, smsDeliveryStatus, smsInbound, smsOutbound, } from "./proxy.js";
17
17
  export { createGatewayRoutes } from "./routes.js";
18
18
  export { type GatewayAuthEnv, serviceKeyAuth } from "./service-key-auth.js";
19
+ export type { IServiceKeyRepository } from "./service-key-repository.js";
20
+ export { DrizzleServiceKeyRepository } from "./service-key-repository.js";
19
21
  export { type SpendingCapConfig, type SpendingCaps, spendingCapCheck } from "./spending-cap.js";
20
22
  export type { ISpendingCapStore, SpendingCapRecord } from "./spending-cap-store.js";
21
23
  export { proxySSEStream } from "./streaming.js";
@@ -16,6 +16,7 @@ export { anthropicToOpenAI, createAnthropicRoutes, createOpenAIRoutes, estimateA
16
16
  export { buildProxyDeps, phoneNumberList, phoneNumberProvision, phoneNumberRelease, smsDeliveryStatus, smsInbound, smsOutbound, } from "./proxy.js";
17
17
  export { createGatewayRoutes } from "./routes.js";
18
18
  export { serviceKeyAuth } from "./service-key-auth.js";
19
+ export { DrizzleServiceKeyRepository } from "./service-key-repository.js";
19
20
  export { spendingCapCheck } from "./spending-cap.js";
20
21
  export { proxySSEStream } from "./streaming.js";
21
22
  export { validateTwilioSignature } from "./twilio-signature.js";
@@ -46,7 +46,7 @@ function anthropicAuth(resolveServiceKey) {
46
46
  },
47
47
  }, 401);
48
48
  }
49
- const tenant = resolveServiceKey(key);
49
+ const tenant = await resolveServiceKey(key);
50
50
  if (!tenant) {
51
51
  logger.warn("Invalid service key attempted (anthropic handler)");
52
52
  return c.json({
@@ -4,10 +4,10 @@
4
4
  * Both the Anthropic and OpenAI handlers need the same set of services:
5
5
  * budget checking, metering, provider configs, fetch, and service key resolution.
6
6
  */
7
- import type { Credit, ICreditLedger } from "@wopr-network/platform-core/credits";
7
+ import type { Credit, ILedger } from "@wopr-network/platform-core/credits";
8
8
  import type { MeterEmitter } from "@wopr-network/platform-core/metering";
9
9
  import type { IRateLimitRepository } from "../../api/rate-limit-repository.js";
10
- import type { BudgetChecker } from "../../monetization/budget/budget-checker.js";
10
+ import type { IBudgetChecker } from "../../monetization/budget/budget-checker.js";
11
11
  import type { CapabilityRateLimitConfig } from "../capability-rate-limit.js";
12
12
  import type { CircuitBreakerConfig } from "../circuit-breaker.js";
13
13
  import type { ICircuitBreakerRepository } from "../circuit-breaker-repository.js";
@@ -15,14 +15,14 @@ import type { SellRateLookupFn } from "../rate-lookup.js";
15
15
  import type { FetchFn, GatewayTenant, ProviderConfig } from "../types.js";
16
16
  export interface ProtocolDeps {
17
17
  meter: MeterEmitter;
18
- budgetChecker: BudgetChecker;
19
- creditLedger?: ICreditLedger;
18
+ budgetChecker: IBudgetChecker;
19
+ creditLedger?: ILedger;
20
20
  topUpUrl: string;
21
21
  graceBufferCents?: number;
22
22
  providers: ProviderConfig;
23
23
  defaultMargin: number;
24
24
  fetchFn: FetchFn;
25
- resolveServiceKey: (key: string) => GatewayTenant | null;
25
+ resolveServiceKey: (key: string) => GatewayTenant | null | Promise<GatewayTenant | null>;
26
26
  /** Apply margin to a cost. Defaults to withMargin from adapters/types. */
27
27
  withMarginFn: (cost: Credit, margin: number) => Credit;
28
28
  rateLookupFn?: SellRateLookupFn;
@@ -53,7 +53,7 @@ function openaiAuth(resolveServiceKey) {
53
53
  },
54
54
  }, 401);
55
55
  }
56
- const tenant = resolveServiceKey(key);
56
+ const tenant = await resolveServiceKey(key);
57
57
  if (!tenant) {
58
58
  logger.warn("Invalid service key attempted (openai handler)", {
59
59
  keyPrefix: `${key.slice(0, 8)}...`,
@@ -9,18 +9,18 @@
9
9
  * 4. Emit meter event
10
10
  * 5. Return response to bot
11
11
  */
12
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
12
+ import type { ILedger } from "@wopr-network/platform-core/credits";
13
13
  import type { MeterEmitter } from "@wopr-network/platform-core/metering";
14
14
  import type { Context } from "hono";
15
- import type { BudgetChecker } from "../monetization/budget/budget-checker.js";
15
+ import type { IBudgetChecker } from "../monetization/budget/budget-checker.js";
16
16
  import type { SellRateLookupFn } from "./rate-lookup.js";
17
17
  import type { GatewayAuthEnv } from "./service-key-auth.js";
18
18
  import type { FetchFn, GatewayConfig, ProviderConfig } from "./types.js";
19
19
  /** Shared state for all proxy handlers. */
20
20
  export interface ProxyDeps {
21
21
  meter: MeterEmitter;
22
- budgetChecker: BudgetChecker;
23
- creditLedger?: ICreditLedger;
22
+ budgetChecker: IBudgetChecker;
23
+ creditLedger?: ILedger;
24
24
  topUpUrl: string;
25
25
  graceBufferCents?: number;
26
26
  providers: ProviderConfig;
@@ -51,7 +51,7 @@ function buildTestConfig(overrides = {}) {
51
51
  }));
52
52
  return {
53
53
  meter: meter,
54
- budgetChecker: budgetChecker,
54
+ budgetChecker,
55
55
  creditLedger,
56
56
  providers: { openrouter: { apiKey: "or-test-key" } },
57
57
  fetchFn,
@@ -22,7 +22,7 @@ export interface GatewayAuthEnv {
22
22
  *
23
23
  * @param resolveServiceKey - Function that maps a service key to a tenant (or null)
24
24
  */
25
- export declare function serviceKeyAuth(resolveServiceKey: (key: string) => GatewayTenant | null): (c: Context<GatewayAuthEnv>, next: Next) => Promise<void | (Response & import("hono").TypedResponse<{
25
+ export declare function serviceKeyAuth(resolveServiceKey: (key: string) => GatewayTenant | null | Promise<GatewayTenant | null>): (c: Context<GatewayAuthEnv>, next: Next) => Promise<void | (Response & import("hono").TypedResponse<{
26
26
  error: {
27
27
  message: string;
28
28
  type: string;
@@ -47,7 +47,7 @@ export function serviceKeyAuth(resolveServiceKey) {
47
47
  },
48
48
  }, 401);
49
49
  }
50
- const tenant = resolveServiceKey(serviceKey);
50
+ const tenant = await resolveServiceKey(serviceKey);
51
51
  if (!tenant) {
52
52
  logger.warn("Invalid service key attempted", {
53
53
  keyPrefix: `${serviceKey.slice(0, 8)}...`,
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Gateway service key repository.
3
+ *
4
+ * Stores SHA-256 hashes of per-instance service keys used to authenticate
5
+ * tenant containers against the metered inference gateway. Raw keys are
6
+ * NEVER stored — only hashes.
7
+ */
8
+ import type { PlatformDb } from "../db/index.js";
9
+ import type { GatewayTenant } from "./types.js";
10
+ export interface IServiceKeyRepository {
11
+ /** Generate a new service key for an instance. Returns the raw key (caller must store it). */
12
+ generate(tenantId: string, instanceId: string): Promise<string>;
13
+ /** Resolve a raw bearer token to a GatewayTenant. Returns null if not found or revoked. */
14
+ resolve(rawKey: string): Promise<GatewayTenant | null>;
15
+ /** Revoke the service key for a specific instance. */
16
+ revokeByInstance(instanceId: string): Promise<void>;
17
+ /** Revoke all service keys for a tenant (used when tenant is deleted). */
18
+ revokeByTenant(tenantId: string): Promise<void>;
19
+ }
20
+ export declare class DrizzleServiceKeyRepository implements IServiceKeyRepository {
21
+ private readonly db;
22
+ constructor(db: PlatformDb);
23
+ generate(tenantId: string, instanceId: string): Promise<string>;
24
+ resolve(rawKey: string): Promise<GatewayTenant | null>;
25
+ revokeByInstance(instanceId: string): Promise<void>;
26
+ revokeByTenant(tenantId: string): Promise<void>;
27
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Gateway service key repository.
3
+ *
4
+ * Stores SHA-256 hashes of per-instance service keys used to authenticate
5
+ * tenant containers against the metered inference gateway. Raw keys are
6
+ * NEVER stored — only hashes.
7
+ */
8
+ import { createHash, randomBytes } from "node:crypto";
9
+ import { and, eq, isNull } from "drizzle-orm";
10
+ import { gatewayServiceKeys } from "../db/schema/gateway-service-keys.js";
11
+ /** Hash a raw key for storage/lookup. */
12
+ function hashKey(raw) {
13
+ return createHash("sha256").update(raw).digest("hex");
14
+ }
15
+ export class DrizzleServiceKeyRepository {
16
+ db;
17
+ constructor(db) {
18
+ this.db = db;
19
+ }
20
+ async generate(tenantId, instanceId) {
21
+ const raw = randomBytes(32).toString("hex");
22
+ const hash = hashKey(raw);
23
+ const id = randomBytes(16).toString("hex");
24
+ await this.db.insert(gatewayServiceKeys).values({
25
+ id,
26
+ keyHash: hash,
27
+ tenantId,
28
+ instanceId,
29
+ createdAt: Date.now(),
30
+ });
31
+ return raw;
32
+ }
33
+ async resolve(rawKey) {
34
+ const hash = hashKey(rawKey);
35
+ const rows = await this.db
36
+ .select({
37
+ tenantId: gatewayServiceKeys.tenantId,
38
+ instanceId: gatewayServiceKeys.instanceId,
39
+ })
40
+ .from(gatewayServiceKeys)
41
+ .where(and(eq(gatewayServiceKeys.keyHash, hash), isNull(gatewayServiceKeys.revokedAt)))
42
+ .limit(1);
43
+ const row = rows[0];
44
+ if (!row)
45
+ return null;
46
+ return {
47
+ id: row.tenantId,
48
+ instanceId: row.instanceId,
49
+ spendLimits: { maxSpendPerHour: null, maxSpendPerMonth: null },
50
+ };
51
+ }
52
+ async revokeByInstance(instanceId) {
53
+ await this.db
54
+ .update(gatewayServiceKeys)
55
+ .set({ revokedAt: Date.now() })
56
+ .where(and(eq(gatewayServiceKeys.instanceId, instanceId), isNull(gatewayServiceKeys.revokedAt)));
57
+ }
58
+ async revokeByTenant(tenantId) {
59
+ await this.db
60
+ .update(gatewayServiceKeys)
61
+ .set({ revokedAt: Date.now() })
62
+ .where(and(eq(gatewayServiceKeys.tenantId, tenantId), isNull(gatewayServiceKeys.revokedAt)));
63
+ }
64
+ }
@@ -5,10 +5,10 @@
5
5
  * /v1/... endpoints using WOPR service keys. The gateway authenticates,
6
6
  * budget-checks, proxies to upstream providers, meters usage, and responds.
7
7
  */
8
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
8
+ import type { ILedger } from "@wopr-network/platform-core/credits";
9
9
  import type { MeterEmitter } from "@wopr-network/platform-core/metering";
10
10
  import type { IRateLimitRepository } from "../api/rate-limit-repository.js";
11
- import type { BudgetChecker, SpendLimits } from "../monetization/budget/budget-checker.js";
11
+ import type { IBudgetChecker, SpendLimits } from "../monetization/budget/budget-checker.js";
12
12
  import type { CapabilityRateLimitConfig } from "./capability-rate-limit.js";
13
13
  import type { CircuitBreakerConfig } from "./circuit-breaker.js";
14
14
  import type { ICircuitBreakerRepository } from "./circuit-breaker-repository.js";
@@ -101,9 +101,9 @@ export interface GatewayConfig {
101
101
  /** MeterEmitter instance for usage tracking */
102
102
  meter: MeterEmitter;
103
103
  /** BudgetChecker instance for pre-call budget validation */
104
- budgetChecker: BudgetChecker;
104
+ budgetChecker: IBudgetChecker;
105
105
  /** CreditLedger instance for deducting credits after proxy calls (optional — if absent, credit deduction is skipped) */
106
- creditLedger?: ICreditLedger;
106
+ creditLedger?: ILedger;
107
107
  /** URL to direct users to when they need to add credits (default: "/dashboard/credits") */
108
108
  topUpUrl?: string;
109
109
  /** Maximum negative credit balance (in cents) before hard-stop. Default: 50 (-$0.50). */
@@ -119,7 +119,7 @@ export interface GatewayConfig {
119
119
  /** Optional cached rate lookup for model-specific token pricing (WOP-646) */
120
120
  rateLookupFn?: import("./rate-lookup.js").SellRateLookupFn;
121
121
  /** Function to resolve a service key to a tenant */
122
- resolveServiceKey: (key: string) => GatewayTenant | null;
122
+ resolveServiceKey: (key: string) => GatewayTenant | null | Promise<GatewayTenant | null>;
123
123
  /** Base URL for Twilio webhook signature verification (e.g., https://api.wopr.network/v1). Required for Twilio/Telnyx webhook endpoints. */
124
124
  webhookBaseUrl?: string;
125
125
  /** Resolve a tenant from an inbound webhook request (e.g., from a tenantId URL path param). Required when webhookBaseUrl is set. */
@@ -1,7 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { Credit } from "../credits/credit.js";
4
- import { CreditLedger } from "../credits/credit-ledger.js";
4
+ import { DrizzleLedger } from "../credits/ledger.js";
5
5
  import { usageSummaries } from "../db/schema/meter-events.js";
6
6
  import { createTestDb, truncateAllTables } from "../test/db.js";
7
7
  import { runReconciliation } from "./reconciliation-cron.js";
@@ -21,7 +21,7 @@ describe("runReconciliation", () => {
21
21
  const t = await createTestDb();
22
22
  pool = t.pool;
23
23
  db = t.db;
24
- ledger = new CreditLedger(db);
24
+ ledger = new DrizzleLedger(db);
25
25
  usageSummaryRepo = new DrizzleUsageSummaryRepository(db);
26
26
  adapterUsageRepo = new DrizzleAdapterUsageRepository(db);
27
27
  });
@@ -30,6 +30,7 @@ describe("runReconciliation", () => {
30
30
  });
31
31
  beforeEach(async () => {
32
32
  await truncateAllTables(pool);
33
+ await ledger.seedSystemAccounts();
33
34
  });
34
35
  /** Insert a usage_summaries row directly. */
35
36
  async function insertSummary(opts) {
@@ -57,7 +58,7 @@ describe("runReconciliation", () => {
57
58
  const charge = Credit.fromCents(50);
58
59
  await insertSummary({ tenant: "t1", totalCharge: charge.toRaw() });
59
60
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
60
- await ledger.debit("t1", charge, "adapter_usage", "chat usage");
61
+ await ledger.debit("t1", charge, "adapter_usage", { description: "chat usage" });
61
62
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
62
63
  expect(result.tenantsChecked).toBe(1);
63
64
  expect(result.discrepancies).toEqual([]);
@@ -65,7 +66,7 @@ describe("runReconciliation", () => {
65
66
  it("detects drift when metered charge exceeds ledger debit", async () => {
66
67
  await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(100).toRaw() });
67
68
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
68
- await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", "chat usage");
69
+ await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", { description: "chat usage" });
69
70
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
70
71
  expect(result.tenantsChecked).toBe(1);
71
72
  expect(result.discrepancies).toHaveLength(1);
@@ -91,7 +92,7 @@ describe("runReconciliation", () => {
91
92
  await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(20).toRaw() });
92
93
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
93
94
  // Debit as bot_runtime — should NOT count toward reconciliation
94
- await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", "daily runtime");
95
+ await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", { description: "daily runtime" });
95
96
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
96
97
  // Metered 20c, ledger adapter_usage = 0 => drift = 20c
97
98
  expect(result.discrepancies).toHaveLength(1);
@@ -119,11 +120,11 @@ describe("runReconciliation", () => {
119
120
  // t1: balanced
120
121
  await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(50).toRaw() });
121
122
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
122
- await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", "chat");
123
+ await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", { description: "chat" });
123
124
  // t2: drifted
124
125
  await insertSummary({ tenant: "t2", totalCharge: Credit.fromCents(100).toRaw() });
125
126
  await ledger.credit("t2", Credit.fromCents(500), "purchase");
126
- await ledger.debit("t2", Credit.fromCents(60), "adapter_usage", "chat");
127
+ await ledger.debit("t2", Credit.fromCents(60), "adapter_usage", { description: "chat" });
127
128
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
128
129
  expect(result.tenantsChecked).toBe(2);
129
130
  expect(result.discrepancies).toHaveLength(1);
@@ -154,7 +155,7 @@ describe("runReconciliation", () => {
154
155
  // Metered 50c but debited 80c (over-billed)
155
156
  await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(50).toRaw() });
156
157
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
157
- await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", "chat usage");
158
+ await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", { description: "chat usage" });
158
159
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
159
160
  expect(result.discrepancies).toHaveLength(1);
160
161
  expect(result.discrepancies[0].driftRaw).toBe(Credit.fromCents(-30).toRaw());
@@ -1,5 +1,5 @@
1
1
  import { and, eq, gte, lt, ne, sql } from "drizzle-orm";
2
- import { creditTransactions } from "../db/schema/credits.js";
2
+ import { journalEntries, journalLines } from "../db/schema/ledger.js";
3
3
  import { usageSummaries } from "../db/schema/meter-events.js";
4
4
  export class DrizzleUsageSummaryRepository {
5
5
  db;
@@ -25,19 +25,21 @@ export class DrizzleAdapterUsageRepository {
25
25
  this.db = db;
26
26
  }
27
27
  async getAggregatedAdapterUsageDebits(startIso, endIso) {
28
+ // Sum the debit-side journal line amounts for adapter_usage entries.
29
+ // In double-entry: DR tenant liability (2000:<tenantId>), CR revenue:adapter_usage (4010).
30
+ // The debit line on the tenant account represents the charge amount.
28
31
  const rows = await this.db
29
32
  .select({
30
- tenantId: creditTransactions.tenantId,
31
- // amount_credits stores negative values for debits; ABS gives the raw positive debit amount.
32
- // Use the raw column name in sql to bypass the custom creditColumn type serializer.
33
- // raw SQL: Drizzle cannot express ABS with COALESCE and SUM
34
- totalDebitRaw: sql `COALESCE(SUM(ABS(amount_credits)), 0)`,
33
+ tenantId: journalEntries.tenantId,
34
+ // raw SQL: Drizzle cannot express COALESCE with SUM aggregation
35
+ totalDebitRaw: sql `COALESCE(SUM(${journalLines.amount}), 0)`,
35
36
  })
36
- .from(creditTransactions)
37
- .where(and(eq(creditTransactions.type, "adapter_usage"),
37
+ .from(journalLines)
38
+ .innerJoin(journalEntries, eq(journalEntries.id, journalLines.journalEntryId))
39
+ .where(and(eq(journalEntries.entryType, "adapter_usage"), eq(journalLines.side, "debit"),
38
40
  // raw SQL: Drizzle cannot express timestamptz cast for text column date comparison
39
- sql `${creditTransactions.createdAt}::timestamptz >= ${startIso}::timestamptz`, sql `${creditTransactions.createdAt}::timestamptz < ${endIso}::timestamptz`))
40
- .groupBy(creditTransactions.tenantId);
41
+ sql `${journalEntries.postedAt}::timestamptz >= ${startIso}::timestamptz`, sql `${journalEntries.postedAt}::timestamptz < ${endIso}::timestamptz`))
42
+ .groupBy(journalEntries.tenantId);
41
43
  return rows.map((r) => ({ tenantId: r.tenantId, totalDebitRaw: Number(r.totalDebitRaw) }));
42
44
  }
43
45
  }
@@ -1,7 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
3
3
  import { Credit } from "../credits/credit.js";
4
- import { CreditLedger } from "../credits/credit-ledger.js";
4
+ import { DrizzleLedger } from "../credits/ledger.js";
5
5
  import { createTestDb, seedUsageSummary, truncateAllTables } from "../test/db.js";
6
6
  import { DrizzleAdapterUsageRepository, DrizzleUsageSummaryRepository } from "./reconciliation-repository.js";
7
7
  let pool;
@@ -108,7 +108,8 @@ describe("DrizzleAdapterUsageRepository", () => {
108
108
  beforeEach(async () => {
109
109
  await truncateAllTables(pool);
110
110
  repo = new DrizzleAdapterUsageRepository(db);
111
- ledger = new CreditLedger(db);
111
+ ledger = new DrizzleLedger(db);
112
+ await ledger.seedSystemAccounts();
112
113
  });
113
114
  it("returns empty array when no adapter_usage debits exist", async () => {
114
115
  const today = new Date().toISOString().slice(0, 10);
@@ -121,9 +122,9 @@ describe("DrizzleAdapterUsageRepository", () => {
121
122
  // Fund tenants
122
123
  await ledger.credit("t1", Credit.fromCents(1000), "purchase");
123
124
  await ledger.credit("t2", Credit.fromCents(1000), "purchase");
124
- await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", "t1-debit-1");
125
- await ledger.debit("t1", Credit.fromCents(20), "adapter_usage", "t1-debit-2");
126
- await ledger.debit("t2", Credit.fromCents(50), "adapter_usage", "t2-debit-1");
125
+ await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", { description: "t1-debit-1" });
126
+ await ledger.debit("t1", Credit.fromCents(20), "adapter_usage", { description: "t1-debit-2" });
127
+ await ledger.debit("t2", Credit.fromCents(50), "adapter_usage", { description: "t2-debit-1" });
127
128
  // Query window covering today
128
129
  const today = new Date().toISOString().slice(0, 10);
129
130
  const startIso = `${today}T00:00:00Z`;
@@ -137,8 +138,8 @@ describe("DrizzleAdapterUsageRepository", () => {
137
138
  });
138
139
  it("excludes non-adapter_usage debit types", async () => {
139
140
  await ledger.credit("t1", Credit.fromCents(1000), "purchase");
140
- await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", "adapter debit");
141
- await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", "runtime debit");
141
+ await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", { description: "adapter debit" });
142
+ await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", { description: "runtime debit" });
142
143
  const today = new Date().toISOString().slice(0, 10);
143
144
  const startIso = `${today}T00:00:00Z`;
144
145
  const endIso = new Date(new Date(startIso).getTime() + 86400000).toISOString();
@@ -148,7 +149,7 @@ describe("DrizzleAdapterUsageRepository", () => {
148
149
  });
149
150
  it("excludes credit transactions (positive amounts are not debits)", async () => {
150
151
  await ledger.credit("t1", Credit.fromCents(1000), "purchase");
151
- await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "real debit");
152
+ await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", { description: "real debit" });
152
153
  const today = new Date().toISOString().slice(0, 10);
153
154
  const startIso = `${today}T00:00:00Z`;
154
155
  const endIso = new Date(new Date(startIso).getTime() + 86400000).toISOString();