@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,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");
@@ -4,7 +4,7 @@
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
10
  import type { IBudgetChecker } from "../../monetization/budget/budget-checker.js";
@@ -16,7 +16,7 @@ import type { FetchFn, GatewayTenant, ProviderConfig } from "../types.js";
16
16
  export interface ProtocolDeps {
17
17
  meter: MeterEmitter;
18
18
  budgetChecker: IBudgetChecker;
19
- creditLedger?: ICreditLedger;
19
+ creditLedger?: ILedger;
20
20
  topUpUrl: string;
21
21
  graceBufferCents?: number;
22
22
  providers: ProviderConfig;
@@ -9,7 +9,7 @@
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
15
  import type { IBudgetChecker } from "../monetization/budget/budget-checker.js";
@@ -20,7 +20,7 @@ import type { FetchFn, GatewayConfig, ProviderConfig } from "./types.js";
20
20
  export interface ProxyDeps {
21
21
  meter: MeterEmitter;
22
22
  budgetChecker: IBudgetChecker;
23
- creditLedger?: ICreditLedger;
23
+ creditLedger?: ILedger;
24
24
  topUpUrl: string;
25
25
  graceBufferCents?: number;
26
26
  providers: ProviderConfig;
@@ -5,7 +5,7 @@
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
11
  import type { IBudgetChecker, SpendLimits } from "../monetization/budget/budget-checker.js";
@@ -103,7 +103,7 @@ export interface GatewayConfig {
103
103
  /** BudgetChecker instance for pre-call budget validation */
104
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). */
@@ -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();
@@ -3,7 +3,7 @@ import { and, count, desc, eq, gte, isNotNull, sql } from "drizzle-orm";
3
3
  import { logger } from "../../config/logger.js";
4
4
  import { affiliateReferrals } from "../../db/schema/affiliate.js";
5
5
  import { affiliateFraudEvents } from "../../db/schema/affiliate-fraud.js";
6
- import { creditTransactions } from "../../db/schema/credits.js";
6
+ import { journalEntries } from "../../db/schema/ledger.js";
7
7
  function parseSignals(raw) {
8
8
  try {
9
9
  const parsed = JSON.parse(raw);
@@ -84,10 +84,12 @@ export class DrizzleAffiliateFraudAdminRepository {
84
84
  }
85
85
  async listFingerprintClusters() {
86
86
  const rows = (await this.db.execute(sql `
87
- SELECT stripe_fingerprint, array_agg(DISTINCT tenant_id ORDER BY tenant_id) AS tenant_ids
88
- FROM credit_transactions
89
- WHERE stripe_fingerprint IS NOT NULL
90
- GROUP BY stripe_fingerprint
87
+ SELECT metadata->>'stripeFingerprint' AS stripe_fingerprint,
88
+ array_agg(DISTINCT tenant_id ORDER BY tenant_id) AS tenant_ids
89
+ FROM journal_entries
90
+ WHERE metadata->>'stripeFingerprint' IS NOT NULL
91
+ AND entry_type = 'purchase'
92
+ GROUP BY metadata->>'stripeFingerprint'
91
93
  HAVING COUNT(DISTINCT tenant_id) > 1
92
94
  ORDER BY COUNT(DISTINCT tenant_id) DESC
93
95
  `));
@@ -98,9 +100,9 @@ export class DrizzleAffiliateFraudAdminRepository {
98
100
  }
99
101
  async blockFingerprint(fingerprint, adminUserId) {
100
102
  const rows = await this.db
101
- .selectDistinct({ tenantId: creditTransactions.tenantId })
102
- .from(creditTransactions)
103
- .where(eq(creditTransactions.stripeFingerprint, fingerprint));
103
+ .selectDistinct({ tenantId: journalEntries.tenantId })
104
+ .from(journalEntries)
105
+ .where(and(eq(journalEntries.entryType, "purchase"), sql `${journalEntries.metadata}->>'stripeFingerprint' = ${fingerprint}`));
104
106
  const tenantIds = rows.map((r) => r.tenantId);
105
107
  const now = new Date().toISOString();
106
108
  for (const tenantId of tenantIds) {
@@ -1,8 +1,8 @@
1
- import { sql } from "drizzle-orm";
1
+ import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
2
2
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
3
3
  import { affiliateReferrals } from "../../db/schema/affiliate.js";
4
4
  import { affiliateFraudEvents } from "../../db/schema/affiliate-fraud.js";
5
- import { beginTestTransaction, createTestDb, endTestTransaction, rollbackTestTransaction } from "../../test/db.js";
5
+ import { createTestDb, truncateAllTables } from "../../test/db.js";
6
6
  import { ADMIN_BLOCK_SENTINEL, DrizzleAffiliateFraudAdminRepository } from "./affiliate-admin-repository.js";
7
7
  describe("DrizzleAffiliateFraudAdminRepository", () => {
8
8
  let pool;
@@ -10,14 +10,13 @@ describe("DrizzleAffiliateFraudAdminRepository", () => {
10
10
  let repo;
11
11
  beforeAll(async () => {
12
12
  ({ db, pool } = await createTestDb());
13
- await beginTestTransaction(pool);
14
13
  });
15
14
  afterAll(async () => {
16
- await endTestTransaction(pool);
17
15
  await pool.close();
18
16
  });
19
17
  beforeEach(async () => {
20
- await rollbackTestTransaction(pool);
18
+ await truncateAllTables(pool);
19
+ await new DrizzleLedger(db).seedSystemAccounts();
21
20
  repo = new DrizzleAffiliateFraudAdminRepository(db);
22
21
  });
23
22
  describe("listSuppressions", () => {
@@ -140,9 +139,17 @@ describe("DrizzleAffiliateFraudAdminRepository", () => {
140
139
  });
141
140
  describe("blockFingerprint", () => {
142
141
  it("should insert fraud events with ADMIN_BLOCK as referredTenantId", async () => {
143
- await db.execute(sql `INSERT INTO credit_transactions (id, tenant_id, amount_credits, balance_after_credits, type, created_at, stripe_fingerprint)
144
- VALUES ('ct-1', 't-alice', 0, 0, 'purchase', now(), 'fp_abc123'),
145
- ('ct-2', 't-bob', 0, 0, 'purchase', now(), 'fp_abc123')`);
142
+ const ledger = new DrizzleLedger(db);
143
+ await ledger.credit("t-alice", Credit.fromCents(1), "purchase", {
144
+ description: "test purchase",
145
+ referenceId: "ref-alice-fp_abc123",
146
+ stripeFingerprint: "fp_abc123",
147
+ });
148
+ await ledger.credit("t-bob", Credit.fromCents(1), "purchase", {
149
+ description: "test purchase",
150
+ referenceId: "ref-bob-fp_abc123",
151
+ stripeFingerprint: "fp_abc123",
152
+ });
146
153
  await repo.blockFingerprint("fp_abc123", "admin-user-1");
147
154
  const result = await repo.listSuppressions(50, 0);
148
155
  expect(result.events).toHaveLength(2);
@@ -156,9 +163,17 @@ describe("DrizzleAffiliateFraudAdminRepository", () => {
156
163
  expect(tenantIds).toEqual(["t-alice", "t-bob"]);
157
164
  });
158
165
  it("should use unique referralId per tenant to avoid unique constraint conflicts", async () => {
159
- await db.execute(sql `INSERT INTO credit_transactions (id, tenant_id, amount_credits, balance_after_credits, type, created_at, stripe_fingerprint)
160
- VALUES ('ct-3', 't-carol', 0, 0, 'purchase', now(), 'fp_def456'),
161
- ('ct-4', 't-dave', 0, 0, 'purchase', now(), 'fp_def456')`);
166
+ const ledger = new DrizzleLedger(db);
167
+ await ledger.credit("t-carol", Credit.fromCents(1), "purchase", {
168
+ description: "test purchase",
169
+ referenceId: "ref-carol-fp_def456",
170
+ stripeFingerprint: "fp_def456",
171
+ });
172
+ await ledger.credit("t-dave", Credit.fromCents(1), "purchase", {
173
+ description: "test purchase",
174
+ referenceId: "ref-dave-fp_def456",
175
+ stripeFingerprint: "fp_def456",
176
+ });
162
177
  await repo.blockFingerprint("fp_def456", "admin-user-2");
163
178
  const result = await repo.listSuppressions(50, 0);
164
179
  expect(result.events).toHaveLength(2);
@@ -169,8 +184,12 @@ describe("DrizzleAffiliateFraudAdminRepository", () => {
169
184
  expect(new Set(referralIds).size).toBe(2);
170
185
  });
171
186
  it("should be idempotent via onConflictDoNothing", async () => {
172
- await db.execute(sql `INSERT INTO credit_transactions (id, tenant_id, amount_credits, balance_after_credits, type, created_at, stripe_fingerprint)
173
- VALUES ('ct-5', 't-eve', 0, 0, 'purchase', now(), 'fp_ghi789')`);
187
+ const ledger = new DrizzleLedger(db);
188
+ await ledger.credit("t-eve", Credit.fromCents(1), "purchase", {
189
+ description: "test purchase",
190
+ referenceId: "ref-eve-fp_ghi789",
191
+ stripeFingerprint: "fp_ghi789",
192
+ });
174
193
  await repo.blockFingerprint("fp_ghi789", "admin-user-3");
175
194
  await repo.blockFingerprint("fp_ghi789", "admin-user-3");
176
195
  const result = await repo.listSuppressions(50, 0);
@@ -1,10 +1,10 @@
1
- import type { Credit, ICreditLedger } from "@wopr-network/platform-core/credits";
1
+ import type { Credit, ILedger } from "@wopr-network/platform-core/credits";
2
2
  import type { IAffiliateFraudRepository } from "./affiliate-fraud-repository.js";
3
3
  import type { IAffiliateRepository } from "./drizzle-affiliate-repository.js";
4
4
  export interface AffiliateCreditMatchDeps {
5
5
  tenantId: string;
6
6
  purchaseAmount: Credit;
7
- ledger: ICreditLedger;
7
+ ledger: ILedger;
8
8
  affiliateRepo: IAffiliateRepository;
9
9
  matchRate?: number;
10
10
  fraudRepo?: IAffiliateFraudRepository;
@@ -85,7 +85,10 @@ export async function processAffiliateCreditMatch(deps) {
85
85
  if (matchAmount.isZero() || matchAmount.isNegative())
86
86
  return null;
87
87
  // 6. Credit the referrer
88
- await ledger.credit(referral.referrerTenantId, matchAmount, "affiliate_match", `Affiliate match for referred tenant ${tenantId}`, refId);
88
+ await ledger.credit(referral.referrerTenantId, matchAmount, "affiliate_match", {
89
+ description: `Affiliate match for referred tenant ${tenantId}`,
90
+ referenceId: refId,
91
+ });
89
92
  // 7. Update referral record
90
93
  await affiliateRepo.markFirstPurchase(tenantId);
91
94
  await affiliateRepo.recordMatch(tenantId, matchAmount);