@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 { Credit, CreditLedger, InsufficientBalanceError } from "@wopr-network/platform-core/credits";
1
+ import { Credit, DrizzleLedger, InsufficientBalanceError } from "@wopr-network/platform-core/credits";
2
2
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { RESOURCE_TIERS } from "../../fleet/resource-tiers.js";
4
4
  import { createTestDb, truncateAllTables } from "../../test/db.js";
@@ -10,13 +10,14 @@ describe("runRuntimeDeductions", () => {
10
10
  beforeAll(async () => {
11
11
  const { db, pool: p } = await createTestDb();
12
12
  pool = p;
13
- ledger = new CreditLedger(db);
13
+ ledger = new DrizzleLedger(db);
14
14
  });
15
15
  afterAll(async () => {
16
16
  await pool.close();
17
17
  });
18
18
  beforeEach(async () => {
19
19
  await truncateAllTables(pool);
20
+ await ledger.seedSystemAccounts();
20
21
  });
21
22
  it("DAILY_BOT_COST equals 17 cents", () => {
22
23
  expect(DAILY_BOT_COST.toCents()).toBe(17);
@@ -32,7 +33,7 @@ describe("runRuntimeDeductions", () => {
32
33
  expect(result.errors).toEqual([]);
33
34
  });
34
35
  it("skips tenants with zero active bots", async () => {
35
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
36
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
36
37
  const result = await runRuntimeDeductions({
37
38
  ledger,
38
39
  date: TODAY,
@@ -42,7 +43,7 @@ describe("runRuntimeDeductions", () => {
42
43
  expect((await ledger.balance("tenant-1")).toCents()).toBe(500);
43
44
  });
44
45
  it("deducts full amount when balance is sufficient", async () => {
45
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
46
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
46
47
  const result = await runRuntimeDeductions({
47
48
  ledger,
48
49
  date: TODAY,
@@ -53,7 +54,7 @@ describe("runRuntimeDeductions", () => {
53
54
  expect((await ledger.balance("tenant-1")).toCents()).toBe(500 - 2 * 17);
54
55
  });
55
56
  it("partial deduction and suspension when balance is insufficient", async () => {
56
- await ledger.credit("tenant-1", Credit.fromCents(10), "purchase", "top-up");
57
+ await ledger.credit("tenant-1", Credit.fromCents(10), "purchase", { description: "top-up" });
57
58
  const onSuspend = vi.fn();
58
59
  const result = await runRuntimeDeductions({
59
60
  ledger,
@@ -67,9 +68,9 @@ describe("runRuntimeDeductions", () => {
67
68
  expect((await ledger.balance("tenant-1")).toCents()).toBe(0);
68
69
  });
69
70
  it("suspends with zero partial when balance exactly zero", async () => {
70
- await ledger.credit("tenant-1", Credit.fromCents(100), "purchase", "top-up");
71
- await ledger.debit("tenant-1", Credit.fromCents(100), "bot_runtime", "drain");
72
- await ledger.credit("tenant-1", Credit.fromCents(1), "purchase", "tiny");
71
+ await ledger.credit("tenant-1", Credit.fromCents(100), "purchase", { description: "top-up" });
72
+ await ledger.debit("tenant-1", Credit.fromCents(100), "bot_runtime", { description: "drain" });
73
+ await ledger.credit("tenant-1", Credit.fromCents(1), "purchase", { description: "tiny" });
73
74
  const onSuspend = vi.fn();
74
75
  const result = await runRuntimeDeductions({
75
76
  ledger,
@@ -82,7 +83,7 @@ describe("runRuntimeDeductions", () => {
82
83
  expect((await ledger.balance("tenant-1")).toCents()).toBe(0);
83
84
  });
84
85
  it("suspends without onSuspend callback", async () => {
85
- await ledger.credit("tenant-1", Credit.fromCents(5), "purchase", "top-up");
86
+ await ledger.credit("tenant-1", Credit.fromCents(5), "purchase", { description: "top-up" });
86
87
  const result = await runRuntimeDeductions({
87
88
  ledger,
88
89
  date: TODAY,
@@ -92,7 +93,7 @@ describe("runRuntimeDeductions", () => {
92
93
  expect(result.processed).toBe(1);
93
94
  });
94
95
  it("handles errors from getActiveBotCount gracefully", async () => {
95
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
96
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
96
97
  const result = await runRuntimeDeductions({
97
98
  ledger,
98
99
  date: TODAY,
@@ -105,8 +106,8 @@ describe("runRuntimeDeductions", () => {
105
106
  expect(result.errors[0]).toContain("db connection failed");
106
107
  });
107
108
  it("handles InsufficientBalanceError from ledger.debit", async () => {
108
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
109
- await ledger.debit("tenant-1", Credit.fromCents(499), "bot_runtime", "drain");
109
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
110
+ await ledger.debit("tenant-1", Credit.fromCents(499), "bot_runtime", { description: "drain" });
110
111
  const onSuspend = vi.fn();
111
112
  const result = await runRuntimeDeductions({
112
113
  ledger,
@@ -118,7 +119,7 @@ describe("runRuntimeDeductions", () => {
118
119
  expect(onSuspend).toHaveBeenCalledWith("tenant-1");
119
120
  });
120
121
  it("catches InsufficientBalanceError from debit and suspends", async () => {
121
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
122
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
122
123
  vi.spyOn(ledger, "debit").mockRejectedValue(new InsufficientBalanceError(Credit.fromCents(0), Credit.fromCents(17)));
123
124
  const onSuspend = vi.fn();
124
125
  const result = await runRuntimeDeductions({
@@ -133,7 +134,7 @@ describe("runRuntimeDeductions", () => {
133
134
  vi.restoreAllMocks();
134
135
  });
135
136
  it("catches InsufficientBalanceError without onSuspend callback", async () => {
136
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
137
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
137
138
  vi.spyOn(ledger, "debit").mockRejectedValue(new InsufficientBalanceError(Credit.fromCents(0), Credit.fromCents(17)));
138
139
  const result = await runRuntimeDeductions({
139
140
  ledger,
@@ -145,8 +146,8 @@ describe("runRuntimeDeductions", () => {
145
146
  vi.restoreAllMocks();
146
147
  });
147
148
  it("processes multiple tenants", async () => {
148
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
149
- await ledger.credit("tenant-2", Credit.fromCents(10), "purchase", "top-up");
149
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
150
+ await ledger.credit("tenant-2", Credit.fromCents(10), "purchase", { description: "top-up" });
150
151
  const onSuspend = vi.fn();
151
152
  const result = await runRuntimeDeductions({
152
153
  ledger,
@@ -159,7 +160,7 @@ describe("runRuntimeDeductions", () => {
159
160
  expect(result.suspended).not.toContain("tenant-1");
160
161
  });
161
162
  it("fires onLowBalance when balance drops below 100 cents threshold", async () => {
162
- await ledger.credit("tenant-1", Credit.fromCents(110), "purchase", "top-up");
163
+ await ledger.credit("tenant-1", Credit.fromCents(110), "purchase", { description: "top-up" });
163
164
  const onLowBalance = vi.fn();
164
165
  await runRuntimeDeductions({
165
166
  ledger,
@@ -173,7 +174,7 @@ describe("runRuntimeDeductions", () => {
173
174
  expect(calledBalance.toCents()).toBe(93);
174
175
  });
175
176
  it("does NOT fire onLowBalance when balance was already below threshold before deduction", async () => {
176
- await ledger.credit("tenant-1", Credit.fromCents(90), "purchase", "top-up");
177
+ await ledger.credit("tenant-1", Credit.fromCents(90), "purchase", { description: "top-up" });
177
178
  const onLowBalance = vi.fn();
178
179
  await runRuntimeDeductions({
179
180
  ledger,
@@ -184,7 +185,7 @@ describe("runRuntimeDeductions", () => {
184
185
  expect(onLowBalance).not.toHaveBeenCalled();
185
186
  });
186
187
  it("fires onCreditsExhausted when full deduction causes balance to drop to 0", async () => {
187
- await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", "top-up");
188
+ await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", { description: "top-up" });
188
189
  const onCreditsExhausted = vi.fn();
189
190
  await runRuntimeDeductions({
190
191
  ledger,
@@ -197,7 +198,7 @@ describe("runRuntimeDeductions", () => {
197
198
  });
198
199
  it("suspends tenant when full deduction causes balance to drop to exactly 0", async () => {
199
200
  // Balance = exactly 1 bot * DAILY_BOT_COST = 17 cents → full deduction → 0
200
- await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", "top-up");
201
+ await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", { description: "top-up" });
201
202
  const onSuspend = vi.fn();
202
203
  const onCreditsExhausted = vi.fn();
203
204
  const result = await runRuntimeDeductions({
@@ -213,7 +214,7 @@ describe("runRuntimeDeductions", () => {
213
214
  expect((await ledger.balance("tenant-1")).toCents()).toBe(0);
214
215
  });
215
216
  it("fires onCreditsExhausted on partial deduction when balance hits 0", async () => {
216
- await ledger.credit("tenant-1", Credit.fromCents(10), "purchase", "top-up");
217
+ await ledger.credit("tenant-1", Credit.fromCents(10), "purchase", { description: "top-up" });
217
218
  const onCreditsExhausted = vi.fn();
218
219
  await runRuntimeDeductions({
219
220
  ledger,
@@ -225,7 +226,7 @@ describe("runRuntimeDeductions", () => {
225
226
  expect((await ledger.balance("tenant-1")).toCents()).toBe(0);
226
227
  });
227
228
  it("partially debits resource tier surcharge when balance is positive but insufficient", async () => {
228
- await ledger.credit("tenant-1", Credit.fromCents(30), "purchase", "top-up");
229
+ await ledger.credit("tenant-1", Credit.fromCents(30), "purchase", { description: "top-up" });
229
230
  const result = await runRuntimeDeductions({
230
231
  ledger,
231
232
  date: TODAY,
@@ -236,7 +237,7 @@ describe("runRuntimeDeductions", () => {
236
237
  expect((await ledger.balance("tenant-1")).toCents()).toBe(0);
237
238
  });
238
239
  it("skips resource tier partial debit when balance is exactly 0 after runtime", async () => {
239
- await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", "top-up");
240
+ await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", { description: "top-up" });
240
241
  const onCreditsExhausted = vi.fn();
241
242
  const result = await runRuntimeDeductions({
242
243
  ledger,
@@ -254,7 +255,7 @@ describe("runRuntimeDeductions", () => {
254
255
  // triggering the zero-crossing suspend in the runtime block.
255
256
  // Storage cost (5 cents) then tries to suspend again via its else-branch (balance 0 < 5).
256
257
  // The !result.suspended.includes(tenantId) guard must prevent onSuspend being called twice.
257
- await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", "top-up");
258
+ await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", { description: "top-up" });
258
259
  const onSuspend = vi.fn();
259
260
  const result = await runRuntimeDeductions({
260
261
  ledger,
@@ -270,7 +271,7 @@ describe("runRuntimeDeductions", () => {
270
271
  it("buildResourceTierCosts: deducts pro tier surcharge via getResourceTierCosts", async () => {
271
272
  const proTierCost = RESOURCE_TIERS.pro.dailyCost.toCents();
272
273
  const startBalance = 17 + proTierCost + 10;
273
- await ledger.credit("tenant-1", Credit.fromCents(startBalance), "purchase", "top-up");
274
+ await ledger.credit("tenant-1", Credit.fromCents(startBalance), "purchase", { description: "top-up" });
274
275
  const mockRepo = {
275
276
  getResourceTier: async (_botId) => "pro",
276
277
  };
@@ -285,7 +286,7 @@ describe("runRuntimeDeductions", () => {
285
286
  expect((await ledger.balance("tenant-1")).toCents()).toBe(expected);
286
287
  });
287
288
  it("treats unique constraint violation from concurrent debit as already-billed (skip, not error)", async () => {
288
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
289
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
289
290
  const uniqueErr = Object.assign(new Error("duplicate key value violates unique constraint"), { code: "23505" });
290
291
  vi.spyOn(ledger, "debit").mockRejectedValueOnce(uniqueErr);
291
292
  const result = await runRuntimeDeductions({
@@ -298,7 +299,7 @@ describe("runRuntimeDeductions", () => {
298
299
  vi.restoreAllMocks();
299
300
  });
300
301
  it("is idempotent — second run on same date does not double-deduct", async () => {
301
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
302
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
302
303
  const cfg = {
303
304
  ledger,
304
305
  getActiveBotCount: async () => 1,
@@ -1,8 +1,8 @@
1
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
1
+ import type { ILedger } from "@wopr-network/platform-core/credits";
2
2
  import type { IBotInstanceRepository } from "../../fleet/bot-instance-repository.js";
3
3
  import type { ITenantAddonRepository } from "../addons/addon-repository.js";
4
4
  export interface RuntimeSchedulerDeps {
5
- ledger: ICreditLedger;
5
+ ledger: ILedger;
6
6
  botInstanceRepo: IBotInstanceRepository;
7
7
  tenantAddonRepo: ITenantAddonRepository;
8
8
  onSuspend?: (tenantId: string) => void;
@@ -1,6 +1,6 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { RUNTIME_INTERVAL_MS, startRuntimeScheduler } from "./runtime-scheduler.js";
3
- // Minimal ICreditLedger stub — only the methods runRuntimeDeductions calls.
3
+ // Minimal ILedger stub — only the methods runRuntimeDeductions calls.
4
4
  function makeLedger() {
5
5
  return {
6
6
  tenantsWithBalance: vi.fn().mockResolvedValue([]),
@@ -1,4 +1,4 @@
1
- import { CreditLedger, grantSignupCredits, SIGNUP_GRANT } from "@wopr-network/platform-core/credits";
1
+ import { DrizzleLedger, grantSignupCredits, SIGNUP_GRANT } from "@wopr-network/platform-core/credits";
2
2
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { createTestDb, truncateAllTables } from "../../test/db.js";
4
4
  describe("grantSignupCredits", () => {
@@ -13,7 +13,8 @@ describe("grantSignupCredits", () => {
13
13
  });
14
14
  beforeEach(async () => {
15
15
  await truncateAllTables(pool);
16
- ledger = new CreditLedger(db);
16
+ ledger = new DrizzleLedger(db);
17
+ await ledger.seedSystemAccounts();
17
18
  });
18
19
  it("grants credits to a new tenant and returns true", async () => {
19
20
  const result = await grantSignupCredits(ledger, "tenant-1");
@@ -41,7 +42,8 @@ describe("grantSignupCredits", () => {
41
42
  const uniqueErr = Object.assign(new Error("duplicate key value violates unique constraint"), {
42
43
  code: "23505",
43
44
  });
44
- const racingLedger = new CreditLedger(db);
45
+ const racingLedger = new DrizzleLedger(db);
46
+ await racingLedger.seedSystemAccounts();
45
47
  vi.spyOn(racingLedger, "hasReferenceId").mockResolvedValue(false);
46
48
  vi.spyOn(racingLedger, "credit").mockRejectedValue(uniqueErr);
47
49
  const result = await grantSignupCredits(racingLedger, "tenant-race");
@@ -1,4 +1,4 @@
1
- import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
1
+ import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
2
2
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
3
3
  import { createTestDb, truncateAllTables } from "../../test/db.js";
4
4
  import { runRuntimeDeductions } from "./runtime-cron.js";
@@ -15,7 +15,8 @@ describe("runtime cron with storage tiers", () => {
15
15
  });
16
16
  beforeEach(async () => {
17
17
  await truncateAllTables(pool);
18
- ledger = new CreditLedger(db);
18
+ ledger = new DrizzleLedger(db);
19
+ await ledger.seedSystemAccounts();
19
20
  });
20
21
  it("debits base cost plus storage surcharge for pro tier", async () => {
21
22
  await ledger.credit("t1", Credit.fromCents(1000), "purchase");
@@ -0,0 +1,42 @@
1
+ import { Credit, DrizzleLedger, runTrialBalanceCron } from "@wopr-network/platform-core/credits";
2
+ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { createTestDb, truncateAllTables } from "../../test/db.js";
4
+ describe("runTrialBalanceCron", () => {
5
+ let pool;
6
+ let ledger;
7
+ beforeAll(async () => {
8
+ const { db, pool: p } = await createTestDb();
9
+ pool = p;
10
+ ledger = new DrizzleLedger(db);
11
+ });
12
+ afterAll(async () => {
13
+ await pool.close();
14
+ });
15
+ beforeEach(async () => {
16
+ await truncateAllTables(pool);
17
+ await ledger.seedSystemAccounts();
18
+ });
19
+ it("returns balanced when no entries exist", async () => {
20
+ const result = await runTrialBalanceCron({ ledger });
21
+ expect(result.balanced).toBe(true);
22
+ expect(result.differenceRaw).toBe(0);
23
+ });
24
+ it("returns balanced after normal credit and debit", async () => {
25
+ await ledger.credit("t1", Credit.fromCents(500), "purchase");
26
+ await ledger.debit("t1", Credit.fromCents(200), "bot_runtime");
27
+ const result = await runTrialBalanceCron({ ledger });
28
+ expect(result.balanced).toBe(true);
29
+ expect(result.differenceRaw).toBe(0);
30
+ });
31
+ it("logs an error on imbalance without throwing", async () => {
32
+ vi.spyOn(ledger, "trialBalance").mockResolvedValueOnce({
33
+ totalDebits: Credit.fromCents(1000),
34
+ totalCredits: Credit.fromCents(900),
35
+ balanced: false,
36
+ difference: Credit.fromCents(100),
37
+ });
38
+ const result = await runTrialBalanceCron({ ledger });
39
+ expect(result.balanced).toBe(false);
40
+ expect(result.differenceRaw).toBe(Credit.fromCents(100).toRaw());
41
+ });
42
+ });
@@ -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
  /**
@@ -37,7 +37,7 @@ export declare function createFeatureGate(cfg: FeatureGateConfig): {
37
37
  /**
38
38
  * Convenience factory that creates a requireBalance middleware from a CreditLedger instance.
39
39
  */
40
- export declare function createBalanceGate(ledger: CreditLedger, userKey?: string, userIdField?: string): {
40
+ export declare function createBalanceGate(ledger: ILedger, userKey?: string, userIdField?: string): {
41
41
  requireBalance: (minBalance?: Credit) => (c: Context, next: Next) => Promise<void | (Response & import("hono").TypedResponse<{
42
42
  error: string;
43
43
  }, 401, "json">) | (Response & import("hono").TypedResponse<{
@@ -54,7 +54,7 @@ export declare function createBalanceGate(ledger: CreditLedger, userKey?: string
54
54
  export type ResolveTenantId = (c: Context) => string | undefined | Promise<string | undefined>;
55
55
  export interface CreditGateConfig {
56
56
  /** CreditLedger instance used to check balance. */
57
- ledger: CreditLedger;
57
+ ledger: ILedger;
58
58
  /** Resolve the tenant ID from the request context. */
59
59
  resolveTenantId: ResolveTenantId;
60
60
  }
@@ -39,8 +39,8 @@ export { type AdapterCapability, type AdapterResult, type EmbeddingsInput, type
39
39
  export { type ArbitrageRequest, ArbitrageRouter, type ArbitrageRouterConfig, type MarginRecord, type ModelProviderEntry, NoProviderAvailableError, ProviderRegistry, type ProviderRegistryConfig, type RoutingDecision, } from "./arbitrage/index.js";
40
40
  export type { BudgetCheckerConfig, BudgetCheckResult, SpendLimits } from "./budget/index.js";
41
41
  export { BudgetChecker, DrizzleBudgetChecker } from "./budget/index.js";
42
- export type { BillingState, CreditTransaction, CreditType, DebitType, GetActiveBotCount, HistoryOptions, OnSuspend, RuntimeCronConfig, RuntimeCronResult, TransactionType, } from "./credits/index.js";
43
- export { BotBilling, buildResourceTierCosts, CreditLedger, DAILY_BOT_COST, DrizzleBotBilling, DrizzleCreditLedger, grantSignupCredits, InsufficientBalanceError, runRuntimeDeductions, SIGNUP_GRANT, SUSPENSION_GRACE_DAYS, } from "./credits/index.js";
42
+ export type { BillingState, CreditType, DebitType, GetActiveBotCount, HistoryOptions, ILedger, JournalEntry, OnSuspend, RuntimeCronConfig, RuntimeCronResult, TransactionType, } from "./credits/index.js";
43
+ export { BotBilling, buildResourceTierCosts, DAILY_BOT_COST, DrizzleBotBilling, DrizzleLedger, grantSignupCredits, InsufficientBalanceError, Ledger, runRuntimeDeductions, SIGNUP_GRANT, SUSPENSION_GRACE_DAYS, } from "./credits/index.js";
44
44
  export { type CreditGateConfig, createBalanceGate, createCreditGate, createFeatureGate, type FeatureGateConfig, type GetUserBalance, type ResolveTenantId, } from "./feature-gate.js";
45
45
  export type { BillingPeriod, BillingPeriodSummary, MeterEventRow, UsageSummary, } from "./metering/index.js";
46
46
  export { DrizzleMeterAggregator, DrizzleMeterEmitter, MeterAggregator, MeterEmitter, } from "./metering/index.js";
@@ -48,7 +48,7 @@ export type { PayRamBillingConfig, PayRamCheckoutOpts, PayRamConfig, PayRamPayme
48
48
  export { createPayRamCheckout, createPayRamClient, DrizzlePayRamChargeRepository, handlePayRamWebhook, loadPayRamConfig, MIN_PAYMENT_USD, PayRamChargeRepository, } from "./payram/index.js";
49
49
  export { checkInstanceQuota, DEFAULT_INSTANCE_LIMITS, type InstanceLimits, type QuotaCheckResult, } from "./quotas/quota-check.js";
50
50
  export { buildResourceLimits, type ContainerResourceLimits, DEFAULT_RESOURCE_CONFIG, type ResourceConfig, } from "./quotas/resource-limits.js";
51
- export type { IBotBilling, IBudgetChecker, ICreditLedger, IMeterAggregator, IMeterEmitter, IPayRamChargeRepository, ITenantCustomerRepository, PayRamChargeRecord, } from "./repository-types.js";
51
+ export type { IBotBilling, IBudgetChecker, IMeterAggregator, IMeterEmitter, IPayRamChargeRepository, ITenantCustomerRepository, PayRamChargeRecord, } from "./repository-types.js";
52
52
  export { AdapterSocket, type SocketConfig, type SocketRequest } from "./socket/socket.js";
53
53
  export type { CreditCheckoutOpts, CreditPriceMap, CreditPricePoint, PortalSessionOpts, StripeBillingConfig, TenantCustomerRow, WebhookDeps, WebhookResult, } from "./stripe/index.js";
54
54
  export { CREDIT_PRICE_POINTS, createCreditCheckoutSession, createPortalSession, createStripeClient, DrizzleTenantCustomerRepository, getConfiguredPriceIds, getCreditAmountForPurchase, handleWebhookEvent, loadCreditPriceMap, loadStripeConfig, lookupCreditPrice, TenantCustomerRepository, } from "./stripe/index.js";
@@ -49,7 +49,7 @@ export { withMargin, } from "./adapters/types.js";
49
49
  // Arbitrage router — multi-provider routing for maximum margin (WOP-463)
50
50
  export { ArbitrageRouter, NoProviderAvailableError, ProviderRegistry, } from "./arbitrage/index.js";
51
51
  export { BudgetChecker, DrizzleBudgetChecker } from "./budget/index.js";
52
- export { BotBilling, buildResourceTierCosts, CreditLedger, DAILY_BOT_COST, DrizzleBotBilling, DrizzleCreditLedger, grantSignupCredits, InsufficientBalanceError, runRuntimeDeductions, SIGNUP_GRANT, SUSPENSION_GRACE_DAYS, } from "./credits/index.js";
52
+ export { BotBilling, buildResourceTierCosts, DAILY_BOT_COST, DrizzleBotBilling, DrizzleLedger, grantSignupCredits, InsufficientBalanceError, Ledger, runRuntimeDeductions, SIGNUP_GRANT, SUSPENSION_GRACE_DAYS, } from "./credits/index.js";
53
53
  // Feature gating middleware (WOP-384 — replaced tier gates with balance gates)
54
54
  export { createBalanceGate, createCreditGate, createFeatureGate, } from "./feature-gate.js";
55
55
  export { DrizzleMeterAggregator, DrizzleMeterEmitter, MeterAggregator, MeterEmitter, } from "./metering/index.js";
@@ -1,5 +1,5 @@
1
1
  import crypto from "node:crypto";
2
- import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
2
+ import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
3
3
  import { runReconciliation } from "@wopr-network/platform-core/metering";
4
4
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
5
5
  import { usageSummaries } from "../../db/schema/meter-events.js";
@@ -20,7 +20,7 @@ describe("runReconciliation", () => {
20
20
  const t = await createTestDb();
21
21
  pool = t.pool;
22
22
  db = t.db;
23
- ledger = new CreditLedger(db);
23
+ ledger = new DrizzleLedger(db);
24
24
  usageSummaryRepo = new DrizzleUsageSummaryRepository(db);
25
25
  adapterUsageRepo = new DrizzleAdapterUsageRepository(db);
26
26
  });
@@ -29,6 +29,7 @@ describe("runReconciliation", () => {
29
29
  });
30
30
  beforeEach(async () => {
31
31
  await truncateAllTables(pool);
32
+ await ledger.seedSystemAccounts();
32
33
  });
33
34
  /** Insert a usage_summaries row directly. */
34
35
  async function insertSummary(opts) {
@@ -56,7 +57,7 @@ describe("runReconciliation", () => {
56
57
  const charge = Credit.fromCents(50);
57
58
  await insertSummary({ tenant: "t1", totalCharge: charge.toRaw() });
58
59
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
59
- await ledger.debit("t1", charge, "adapter_usage", "chat usage");
60
+ await ledger.debit("t1", charge, "adapter_usage", { description: "chat usage" });
60
61
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
61
62
  expect(result.tenantsChecked).toBe(1);
62
63
  expect(result.discrepancies).toEqual([]);
@@ -64,7 +65,7 @@ describe("runReconciliation", () => {
64
65
  it("detects drift when metered charge exceeds ledger debit", async () => {
65
66
  await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(100).toRaw() });
66
67
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
67
- await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", "chat usage");
68
+ await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", { description: "chat usage" });
68
69
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
69
70
  expect(result.tenantsChecked).toBe(1);
70
71
  expect(result.discrepancies).toHaveLength(1);
@@ -90,7 +91,7 @@ describe("runReconciliation", () => {
90
91
  await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(20).toRaw() });
91
92
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
92
93
  // Debit as bot_runtime — should NOT count toward reconciliation
93
- await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", "daily runtime");
94
+ await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", { description: "daily runtime" });
94
95
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
95
96
  // Metered 20c, ledger adapter_usage = 0 => drift = 20c
96
97
  expect(result.discrepancies).toHaveLength(1);
@@ -118,11 +119,11 @@ describe("runReconciliation", () => {
118
119
  // t1: balanced
119
120
  await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(50).toRaw() });
120
121
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
121
- await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", "chat");
122
+ await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", { description: "chat" });
122
123
  // t2: drifted
123
124
  await insertSummary({ tenant: "t2", totalCharge: Credit.fromCents(100).toRaw() });
124
125
  await ledger.credit("t2", Credit.fromCents(500), "purchase");
125
- await ledger.debit("t2", Credit.fromCents(60), "adapter_usage", "chat");
126
+ await ledger.debit("t2", Credit.fromCents(60), "adapter_usage", { description: "chat" });
126
127
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
127
128
  expect(result.tenantsChecked).toBe(2);
128
129
  expect(result.discrepancies).toHaveLength(1);
@@ -153,7 +154,7 @@ describe("runReconciliation", () => {
153
154
  // Metered 50c but debited 80c (over-billed)
154
155
  await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(50).toRaw() });
155
156
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
156
- await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", "chat usage");
157
+ await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", { description: "chat usage" });
157
158
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
158
159
  expect(result.discrepancies).toHaveLength(1);
159
160
  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,20 @@ 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).
28
30
  const rows = await this.db
29
31
  .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)`,
32
+ tenantId: journalEntries.tenantId,
33
+ // raw SQL: Drizzle cannot express COALESCE with SUM aggregation
34
+ totalDebitRaw: sql `COALESCE(SUM(${journalLines.amount}), 0)`,
35
35
  })
36
- .from(creditTransactions)
37
- .where(and(eq(creditTransactions.type, "adapter_usage"),
36
+ .from(journalLines)
37
+ .innerJoin(journalEntries, eq(journalEntries.id, journalLines.journalEntryId))
38
+ .where(and(eq(journalEntries.entryType, "adapter_usage"), eq(journalLines.side, "debit"),
38
39
  // 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);
40
+ sql `${journalEntries.postedAt}::timestamptz >= ${startIso}::timestamptz`, sql `${journalEntries.postedAt}::timestamptz < ${endIso}::timestamptz`))
41
+ .groupBy(journalEntries.tenantId);
41
42
  return rows.map((r) => ({ tenantId: r.tenantId, totalDebitRaw: Number(r.totalDebitRaw) }));
42
43
  }
43
44
  }
@@ -1,5 +1,5 @@
1
1
  import crypto from "node:crypto";
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 { createTestDb, seedUsageSummary, truncateAllTables } from "../../test/db.js";
5
5
  import { DrizzleAdapterUsageRepository, DrizzleUsageSummaryRepository } from "./reconciliation-repository.js";
@@ -107,7 +107,8 @@ describe("DrizzleAdapterUsageRepository", () => {
107
107
  beforeEach(async () => {
108
108
  await truncateAllTables(pool);
109
109
  repo = new DrizzleAdapterUsageRepository(db);
110
- ledger = new CreditLedger(db);
110
+ ledger = new DrizzleLedger(db);
111
+ await ledger.seedSystemAccounts();
111
112
  });
112
113
  it("returns empty array when no adapter_usage debits exist", async () => {
113
114
  const today = new Date().toISOString().slice(0, 10);
@@ -120,9 +121,9 @@ describe("DrizzleAdapterUsageRepository", () => {
120
121
  // Fund tenants
121
122
  await ledger.credit("t1", Credit.fromCents(1000), "purchase");
122
123
  await ledger.credit("t2", Credit.fromCents(1000), "purchase");
123
- await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", "t1-debit-1");
124
- await ledger.debit("t1", Credit.fromCents(20), "adapter_usage", "t1-debit-2");
125
- await ledger.debit("t2", Credit.fromCents(50), "adapter_usage", "t2-debit-1");
124
+ await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", { description: "t1-debit-1" });
125
+ await ledger.debit("t1", Credit.fromCents(20), "adapter_usage", { description: "t1-debit-2" });
126
+ await ledger.debit("t2", Credit.fromCents(50), "adapter_usage", { description: "t2-debit-1" });
126
127
  // Query window covering today
127
128
  const today = new Date().toISOString().slice(0, 10);
128
129
  const startIso = `${today}T00:00:00Z`;
@@ -136,8 +137,8 @@ describe("DrizzleAdapterUsageRepository", () => {
136
137
  });
137
138
  it("excludes non-adapter_usage debit types", async () => {
138
139
  await ledger.credit("t1", Credit.fromCents(1000), "purchase");
139
- await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", "adapter debit");
140
- await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", "runtime debit");
140
+ await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", { description: "adapter debit" });
141
+ await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", { description: "runtime debit" });
141
142
  const today = new Date().toISOString().slice(0, 10);
142
143
  const startIso = `${today}T00:00:00Z`;
143
144
  const endIso = new Date(new Date(startIso).getTime() + 86400000).toISOString();
@@ -147,7 +148,7 @@ describe("DrizzleAdapterUsageRepository", () => {
147
148
  });
148
149
  it("excludes credit transactions (positive amounts are not debits)", async () => {
149
150
  await ledger.credit("t1", Credit.fromCents(1000), "purchase");
150
- await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "real debit");
151
+ await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", { description: "real debit" });
151
152
  const today = new Date().toISOString().slice(0, 10);
152
153
  const startIso = `${today}T00:00:00Z`;
153
154
  const endIso = new Date(new Date(startIso).getTime() + 86400000).toISOString();
@@ -1,9 +1,9 @@
1
1
  import type { IWebhookSeenRepository, PayRamChargeRepository, PayRamWebhookPayload, PayRamWebhookResult } from "@wopr-network/platform-core/billing";
2
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
2
+ import type { ILedger } from "@wopr-network/platform-core/credits";
3
3
  import type { BotBilling } from "../credits/bot-billing.js";
4
4
  export interface PayRamWebhookDeps {
5
5
  chargeStore: PayRamChargeRepository;
6
- creditLedger: ICreditLedger;
6
+ creditLedger: ILedger;
7
7
  botBilling?: BotBilling;
8
8
  replayGuard: IWebhookSeenRepository;
9
9
  }
@@ -36,7 +36,11 @@ export async function handlePayRamWebhook(deps, payload) {
36
36
  // For OVER_FILLED, we still credit the requested amount — the
37
37
  // overpayment stays in the PayRam wallet as a buffer.
38
38
  const creditCents = charge.amountUsdCents;
39
- await creditLedger.credit(charge.tenantId, Credit.fromCents(creditCents), "purchase", `Crypto credit purchase via PayRam (ref: ${payload.reference_id}, ${payload.currency ?? "crypto"})`, `payram:${payload.reference_id}`, "payram");
39
+ await creditLedger.credit(charge.tenantId, Credit.fromCents(creditCents), "purchase", {
40
+ description: `Crypto credit purchase via PayRam (ref: ${payload.reference_id}, ${payload.currency ?? "crypto"})`,
41
+ referenceId: `payram:${payload.reference_id}`,
42
+ fundingSource: "payram",
43
+ });
40
44
  await chargeStore.markCredited(payload.reference_id);
41
45
  // Reactivate suspended bots (same as Stripe webhook, WOP-447).
42
46
  let reactivatedBots;
@@ -5,7 +5,7 @@
5
5
  * no-op status, idempotency, replay guard, and bot reactivation.
6
6
  */
7
7
  import { DrizzleWebhookSeenRepository, noOpReplayGuard, PayRamChargeRepository, } from "@wopr-network/platform-core/billing";
8
- import { CreditLedger } from "@wopr-network/platform-core/credits";
8
+ import { DrizzleLedger } from "@wopr-network/platform-core/credits";
9
9
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
10
10
  import { createTestDb, truncateAllTables } from "../../test/db.js";
11
11
  import { handlePayRamWebhook } from "./webhook.js";
@@ -35,7 +35,8 @@ describe("handlePayRamWebhook", () => {
35
35
  beforeEach(async () => {
36
36
  await truncateAllTables(pool);
37
37
  chargeStore = new PayRamChargeRepository(db);
38
- creditLedger = new CreditLedger(db);
38
+ creditLedger = new DrizzleLedger(db);
39
+ await creditLedger.seedSystemAccounts();
39
40
  deps = { chargeStore, creditLedger, replayGuard: noOpReplayGuard };
40
41
  // Create a default test charge
41
42
  await chargeStore.create("ref-test-001", "tenant-a", 2500);
@@ -58,12 +59,12 @@ describe("handlePayRamWebhook", () => {
58
59
  const history = await creditLedger.history("tenant-a");
59
60
  expect(history).toHaveLength(1);
60
61
  expect(history[0].referenceId).toBe("payram:ref-test-001");
61
- expect(history[0].type).toBe("purchase");
62
+ expect(history[0].entryType).toBe("purchase");
62
63
  });
63
64
  it("records fundingSource as payram", async () => {
64
65
  await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
65
66
  const history = await creditLedger.history("tenant-a");
66
- expect(history[0].fundingSource).toBe("payram");
67
+ expect(history[0].metadata?.fundingSource).toBe("payram");
67
68
  });
68
69
  it("marks the charge as credited after FILLED", async () => {
69
70
  await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
@@ -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 type { ICouponRepository } from "./coupon-repository.js";
4
4
  import type { IPromotionRepository } from "./promotion-repository.js";
@@ -19,7 +19,7 @@ interface PromotionEngineDeps {
19
19
  promotionRepo: IPromotionRepository;
20
20
  couponRepo: ICouponRepository;
21
21
  redemptionRepo: IRedemptionRepository;
22
- ledger: ICreditLedger;
22
+ ledger: ILedger;
23
23
  }
24
24
  export declare class PromotionEngine {
25
25
  #private;