@wopr-network/platform-core 1.13.2 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/dist/api/routes/admin-credits.d.ts +2 -2
  2. package/dist/api/routes/admin-credits.js +9 -4
  3. package/dist/api/routes/quota.d.ts +2 -2
  4. package/dist/api/routes/verify-email.d.ts +3 -3
  5. package/dist/backup/on-demand-snapshot-service.d.ts +2 -2
  6. package/dist/billing/payram/webhook.d.ts +3 -3
  7. package/dist/billing/payram/webhook.js +5 -1
  8. package/dist/billing/payram/webhook.test.js +5 -4
  9. package/dist/billing/stripe/stripe-payment-processor.d.ts +2 -2
  10. package/dist/billing/stripe/stripe-payment-processor.test.js +7 -0
  11. package/dist/billing/stripe/tenant-store.d.ts +1 -1
  12. package/dist/billing/stripe/tenant-store.js +1 -1
  13. package/dist/credits/auto-topup-charge.d.ts +2 -2
  14. package/dist/credits/auto-topup-charge.js +5 -1
  15. package/dist/credits/auto-topup-charge.test.js +5 -4
  16. package/dist/credits/auto-topup-usage.d.ts +2 -2
  17. package/dist/credits/auto-topup-usage.test.js +53 -12
  18. package/dist/credits/credit-expiry-cron.d.ts +2 -2
  19. package/dist/credits/credit-expiry-cron.js +7 -4
  20. package/dist/credits/credit-expiry-cron.test.js +25 -8
  21. package/dist/credits/credit-ledger.d.ts +2 -2
  22. package/dist/credits/credit-ledger.js +1 -1
  23. package/dist/credits/dividend-cron.d.ts +4 -6
  24. package/dist/credits/dividend-cron.js +10 -16
  25. package/dist/credits/dividend-cron.test.js +31 -44
  26. package/dist/credits/dividend-repository.js +19 -22
  27. package/dist/credits/dividend-repository.test.js +4 -3
  28. package/dist/credits/index.d.ts +4 -2
  29. package/dist/credits/index.js +2 -1
  30. package/dist/credits/ledger.d.ts +195 -0
  31. package/dist/credits/ledger.js +561 -0
  32. package/dist/credits/ledger.test.js +418 -0
  33. package/dist/credits/signup-grant.d.ts +2 -2
  34. package/dist/credits/signup-grant.js +4 -4
  35. package/dist/credits/signup-grant.test.js +5 -3
  36. package/dist/credits/trial-balance-cron.d.ts +19 -0
  37. package/dist/credits/trial-balance-cron.js +30 -0
  38. package/dist/credits/trial-balance-cron.test.js +55 -0
  39. package/dist/db/schema/gateway-service-keys.d.ts +109 -0
  40. package/dist/db/schema/gateway-service-keys.js +18 -0
  41. package/dist/db/schema/index.d.ts +2 -0
  42. package/dist/db/schema/index.js +2 -0
  43. package/dist/db/schema/ledger.d.ts +442 -0
  44. package/dist/db/schema/ledger.js +76 -0
  45. package/dist/gateway/credit-gate.d.ts +2 -2
  46. package/dist/gateway/credit-gate.js +5 -1
  47. package/dist/gateway/credit-gate.test.js +35 -33
  48. package/dist/gateway/gateway-routes.test.js +1 -1
  49. package/dist/gateway/index.d.ts +2 -0
  50. package/dist/gateway/index.js +1 -0
  51. package/dist/gateway/protocol/anthropic.js +1 -1
  52. package/dist/gateway/protocol/deps.d.ts +5 -5
  53. package/dist/gateway/protocol/openai.js +1 -1
  54. package/dist/gateway/proxy.d.ts +4 -4
  55. package/dist/gateway/route-mounting.test.js +1 -1
  56. package/dist/gateway/service-key-auth.d.ts +1 -1
  57. package/dist/gateway/service-key-auth.js +1 -1
  58. package/dist/gateway/service-key-repository.d.ts +27 -0
  59. package/dist/gateway/service-key-repository.js +64 -0
  60. package/dist/gateway/types.d.ts +5 -5
  61. package/dist/metering/reconciliation-cron.test.js +9 -8
  62. package/dist/metering/reconciliation-repository.js +12 -10
  63. package/dist/metering/reconciliation-repository.test.js +9 -8
  64. package/dist/monetization/affiliate/affiliate-admin-repository.js +10 -8
  65. package/dist/monetization/affiliate/affiliate-admin-repository.test.js +32 -13
  66. package/dist/monetization/affiliate/credit-match.d.ts +2 -2
  67. package/dist/monetization/affiliate/credit-match.js +4 -1
  68. package/dist/monetization/affiliate/credit-match.test.js +58 -13
  69. package/dist/monetization/affiliate/new-user-bonus.d.ts +2 -2
  70. package/dist/monetization/affiliate/new-user-bonus.js +4 -1
  71. package/dist/monetization/affiliate/new-user-bonus.test.js +4 -3
  72. package/dist/monetization/credits/auto-topup-charge.d.ts +2 -2
  73. package/dist/monetization/credits/auto-topup-charge.js +5 -1
  74. package/dist/monetization/credits/auto-topup-charge.test.js +5 -4
  75. package/dist/monetization/credits/auto-topup-usage.d.ts +2 -2
  76. package/dist/monetization/credits/auto-topup-usage.test.js +53 -12
  77. package/dist/monetization/credits/bot-billing.d.ts +3 -3
  78. package/dist/monetization/credits/bot-billing.test.js +18 -5
  79. package/dist/monetization/credits/credit-expiry-cron.test.js +25 -8
  80. package/dist/monetization/credits/dividend-cron.d.ts +2 -4
  81. package/dist/monetization/credits/dividend-cron.js +7 -4
  82. package/dist/monetization/credits/dividend-cron.test.js +26 -46
  83. package/dist/monetization/credits/dividend-repository.js +15 -24
  84. package/dist/monetization/credits/dividend-repository.test.js +4 -3
  85. package/dist/monetization/credits/index.d.ts +2 -2
  86. package/dist/monetization/credits/index.js +1 -1
  87. package/dist/monetization/credits/member-usage.test.js +23 -10
  88. package/dist/monetization/credits/phone-billing.d.ts +2 -2
  89. package/dist/monetization/credits/phone-billing.js +5 -1
  90. package/dist/monetization/credits/phone-billing.test.js +9 -12
  91. package/dist/monetization/credits/runtime-cron.d.ts +2 -2
  92. package/dist/monetization/credits/runtime-cron.js +32 -8
  93. package/dist/monetization/credits/runtime-cron.test.js +28 -27
  94. package/dist/monetization/credits/runtime-scheduler.d.ts +2 -2
  95. package/dist/monetization/credits/runtime-scheduler.test.js +1 -1
  96. package/dist/monetization/credits/signup-grant.test.js +5 -3
  97. package/dist/monetization/credits/storage-tier-cron.test.js +3 -2
  98. package/dist/monetization/credits/trial-balance-cron.test.js +42 -0
  99. package/dist/monetization/feature-gate.d.ts +3 -3
  100. package/dist/monetization/index.d.ts +3 -3
  101. package/dist/monetization/index.js +1 -1
  102. package/dist/monetization/metering/reconciliation-cron.test.js +9 -8
  103. package/dist/monetization/metering/reconciliation-repository.js +11 -10
  104. package/dist/monetization/metering/reconciliation-repository.test.js +9 -8
  105. package/dist/monetization/payram/webhook.d.ts +2 -2
  106. package/dist/monetization/payram/webhook.js +5 -1
  107. package/dist/monetization/payram/webhook.test.js +5 -4
  108. package/dist/monetization/promotions/engine.d.ts +2 -2
  109. package/dist/monetization/promotions/engine.js +4 -1
  110. package/dist/monetization/promotions/engine.test.js +3 -1
  111. package/dist/monetization/repository-types.d.ts +1 -1
  112. package/dist/monetization/socket/socket.d.ts +3 -3
  113. package/dist/monetization/stripe/stripe-payment-processor.d.ts +2 -2
  114. package/dist/monetization/stripe/stripe-payment-processor.test.js +7 -0
  115. package/dist/monetization/stripe/webhook.d.ts +2 -2
  116. package/dist/monetization/stripe/webhook.js +70 -6
  117. package/dist/monetization/stripe/webhook.test.js +20 -15
  118. package/dist/onboarding/onboarding-service.d.ts +2 -2
  119. package/dist/onboarding/onboarding-service.js +6 -2
  120. package/drizzle/migrations/0002_gateway_service_keys.sql +14 -0
  121. package/drizzle/migrations/0003_double_entry_ledger.sql +82 -0
  122. package/drizzle/migrations/meta/_journal.json +14 -0
  123. package/package.json +1 -1
  124. package/src/api/routes/admin-credits.ts +11 -14
  125. package/src/api/routes/quota.ts +2 -2
  126. package/src/api/routes/verify-email.ts +4 -4
  127. package/src/backup/on-demand-snapshot-service.test.ts +3 -3
  128. package/src/backup/on-demand-snapshot-service.ts +3 -3
  129. package/src/billing/payram/webhook.test.ts +7 -5
  130. package/src/billing/payram/webhook.ts +8 -11
  131. package/src/billing/stripe/stripe-payment-processor.test.ts +10 -3
  132. package/src/billing/stripe/stripe-payment-processor.ts +3 -3
  133. package/src/billing/stripe/tenant-store.ts +1 -1
  134. package/src/credits/auto-topup-charge.test.ts +7 -5
  135. package/src/credits/auto-topup-charge.ts +7 -10
  136. package/src/credits/auto-topup-usage.test.ts +55 -13
  137. package/src/credits/auto-topup-usage.ts +2 -2
  138. package/src/credits/credit-expiry-cron.test.ts +26 -45
  139. package/src/credits/credit-expiry-cron.ts +9 -12
  140. package/src/credits/credit-ledger.ts +3 -3
  141. package/src/credits/dividend-cron.test.ts +38 -45
  142. package/src/credits/dividend-cron.ts +12 -26
  143. package/src/credits/dividend-repository.test.ts +4 -3
  144. package/src/credits/dividend-repository.ts +21 -23
  145. package/src/credits/index.ts +23 -4
  146. package/src/credits/ledger.test.ts +514 -0
  147. package/src/credits/ledger.ts +851 -0
  148. package/src/credits/signup-grant.test.ts +7 -4
  149. package/src/credits/signup-grant.ts +6 -12
  150. package/src/credits/trial-balance-cron.test.ts +68 -0
  151. package/src/credits/trial-balance-cron.ts +46 -0
  152. package/src/db/schema/gateway-service-keys.ts +23 -0
  153. package/src/db/schema/index.ts +2 -0
  154. package/src/db/schema/ledger.ts +94 -0
  155. package/src/gateway/credit-gate-wiring.test.ts +3 -3
  156. package/src/gateway/credit-gate.test.ts +35 -33
  157. package/src/gateway/credit-gate.ts +6 -10
  158. package/src/gateway/gateway-routes.test.ts +6 -6
  159. package/src/gateway/index.ts +2 -0
  160. package/src/gateway/protocol/anthropic.ts +2 -2
  161. package/src/gateway/protocol/deps.ts +5 -5
  162. package/src/gateway/protocol/openai.ts +2 -2
  163. package/src/gateway/proxy.ts +4 -4
  164. package/src/gateway/route-mounting.test.ts +3 -3
  165. package/src/gateway/service-key-auth.ts +4 -2
  166. package/src/gateway/service-key-repository.ts +87 -0
  167. package/src/gateway/types.ts +5 -5
  168. package/src/metering/reconciliation-cron.test.ts +10 -9
  169. package/src/metering/reconciliation-repository.test.ts +10 -9
  170. package/src/metering/reconciliation-repository.ts +14 -11
  171. package/src/monetization/affiliate/affiliate-admin-repository.test.ts +32 -19
  172. package/src/monetization/affiliate/affiliate-admin-repository.ts +16 -8
  173. package/src/monetization/affiliate/credit-match.test.ts +60 -14
  174. package/src/monetization/affiliate/credit-match.ts +6 -9
  175. package/src/monetization/affiliate/new-user-bonus.test.ts +6 -4
  176. package/src/monetization/affiliate/new-user-bonus.ts +6 -9
  177. package/src/monetization/credits/auto-topup-charge.test.ts +7 -5
  178. package/src/monetization/credits/auto-topup-charge.ts +7 -10
  179. package/src/monetization/credits/auto-topup-usage.test.ts +55 -13
  180. package/src/monetization/credits/auto-topup-usage.ts +2 -2
  181. package/src/monetization/credits/bot-billing.test.ts +20 -6
  182. package/src/monetization/credits/bot-billing.ts +3 -3
  183. package/src/monetization/credits/credit-expiry-cron.test.ts +26 -45
  184. package/src/monetization/credits/dividend-cron.test.ts +34 -48
  185. package/src/monetization/credits/dividend-cron.ts +9 -14
  186. package/src/monetization/credits/dividend-repository.test.ts +4 -3
  187. package/src/monetization/credits/dividend-repository.ts +19 -25
  188. package/src/monetization/credits/index.ts +4 -4
  189. package/src/monetization/credits/member-usage.test.ts +25 -11
  190. package/src/monetization/credits/phone-billing.test.ts +18 -26
  191. package/src/monetization/credits/phone-billing.ts +7 -10
  192. package/src/monetization/credits/runtime-cron.test.ts +29 -28
  193. package/src/monetization/credits/runtime-cron.ts +34 -58
  194. package/src/monetization/credits/runtime-scheduler.test.ts +1 -1
  195. package/src/monetization/credits/runtime-scheduler.ts +2 -2
  196. package/src/monetization/credits/signup-grant.test.ts +7 -4
  197. package/src/monetization/credits/storage-tier-cron.test.ts +5 -3
  198. package/src/monetization/credits/trial-balance-cron.test.ts +52 -0
  199. package/src/monetization/feature-gate.ts +3 -3
  200. package/src/monetization/index.ts +4 -4
  201. package/src/monetization/metering/reconciliation-cron.test.ts +10 -9
  202. package/src/monetization/metering/reconciliation-repository.test.ts +11 -9
  203. package/src/monetization/metering/reconciliation-repository.ts +13 -11
  204. package/src/monetization/payram/webhook.test.ts +7 -5
  205. package/src/monetization/payram/webhook.ts +7 -10
  206. package/src/monetization/promotions/engine.test.ts +6 -5
  207. package/src/monetization/promotions/engine.ts +6 -3
  208. package/src/monetization/repository-types.ts +1 -1
  209. package/src/monetization/socket/socket.ts +4 -4
  210. package/src/monetization/stripe/stripe-payment-processor.test.ts +10 -3
  211. package/src/monetization/stripe/stripe-payment-processor.ts +3 -3
  212. package/src/monetization/stripe/webhook.test.ts +22 -16
  213. package/src/monetization/stripe/webhook.ts +75 -50
  214. package/src/onboarding/onboarding-service.ts +8 -11
  215. package/dist/credits/credit-ledger-extra.test.js +0 -40
  216. package/dist/credits/credit-ledger.bench.js +0 -33
  217. package/dist/credits/credit-ledger.test.d.ts +0 -4
  218. package/dist/credits/credit-ledger.test.js +0 -203
  219. package/dist/credits/credit-transaction-repository.test.js +0 -232
  220. package/dist/monetization/credits/credit-ledger-extra.test.d.ts +0 -1
  221. package/dist/monetization/credits/credit-ledger-extra.test.js +0 -39
  222. package/dist/monetization/credits/credit-ledger.bench.d.ts +0 -1
  223. package/dist/monetization/credits/credit-ledger.bench.js +0 -32
  224. package/dist/monetization/credits/credit-ledger.test.d.ts +0 -4
  225. package/dist/monetization/credits/credit-ledger.test.js +0 -202
  226. package/dist/monetization/credits/credit-transaction-repository.test.d.ts +0 -1
  227. package/dist/monetization/credits/credit-transaction-repository.test.js +0 -232
  228. package/src/credits/credit-ledger-extra.test.ts +0 -57
  229. package/src/credits/credit-ledger.bench.ts +0 -56
  230. package/src/credits/credit-ledger.test.ts +0 -276
  231. package/src/credits/credit-transaction-repository.test.ts +0 -274
  232. package/src/monetization/credits/credit-ledger-extra.test.ts +0 -56
  233. package/src/monetization/credits/credit-ledger.bench.ts +0 -55
  234. package/src/monetization/credits/credit-ledger.test.ts +0 -275
  235. package/src/monetization/credits/credit-transaction-repository.test.ts +0 -274
  236. /package/dist/credits/{credit-ledger-extra.test.d.ts → ledger.test.d.ts} +0 -0
  237. /package/dist/credits/{credit-ledger.bench.d.ts → trial-balance-cron.test.d.ts} +0 -0
  238. /package/dist/{credits/credit-transaction-repository.test.d.ts → monetization/credits/trial-balance-cron.test.d.ts} +0 -0
@@ -1,16 +1,16 @@
1
1
  import type { PGlite } from "@electric-sql/pglite";
2
- import { Credit, DrizzleCreditLedger, runCreditExpiryCron } from "@wopr-network/platform-core/credits";
2
+ import { Credit, DrizzleLedger, runCreditExpiryCron } from "@wopr-network/platform-core/credits";
3
3
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
4
4
  import { createTestDb, truncateAllTables } from "../../test/db.js";
5
5
 
6
6
  describe("runCreditExpiryCron", () => {
7
7
  let pool: PGlite;
8
- let ledger: DrizzleCreditLedger;
8
+ let ledger: DrizzleLedger;
9
9
 
10
10
  beforeAll(async () => {
11
11
  const { db, pool: p } = await createTestDb();
12
12
  pool = p;
13
- ledger = new DrizzleCreditLedger(db);
13
+ ledger = new DrizzleLedger(db);
14
14
  });
15
15
 
16
16
  afterAll(async () => {
@@ -19,6 +19,7 @@ describe("runCreditExpiryCron", () => {
19
19
 
20
20
  beforeEach(async () => {
21
21
  await truncateAllTables(pool);
22
+ await ledger.seedSystemAccounts();
22
23
  });
23
24
 
24
25
  // All tests pass an explicit `now` parameter — hardcoded dates are time-independent
@@ -31,16 +32,11 @@ describe("runCreditExpiryCron", () => {
31
32
  });
32
33
 
33
34
  it("debits expired promotional credit grant", async () => {
34
- await ledger.credit(
35
- "tenant-1",
36
- Credit.fromCents(500),
37
- "promo",
38
- "New user bonus",
39
- "promo:tenant-1",
40
- undefined,
41
- undefined,
42
- "2026-01-10T00:00:00Z",
43
- );
35
+ await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
36
+ description: "New user bonus",
37
+ referenceId: "promo:tenant-1",
38
+ expiresAt: "2026-01-10T00:00:00Z",
39
+ });
44
40
 
45
41
  const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
46
42
  expect(result.processed).toBe(1);
@@ -51,16 +47,11 @@ describe("runCreditExpiryCron", () => {
51
47
  });
52
48
 
53
49
  it("does not debit non-expired credits", async () => {
54
- await ledger.credit(
55
- "tenant-1",
56
- Credit.fromCents(500),
57
- "promo",
58
- "Future bonus",
59
- "promo:tenant-1-future",
60
- undefined,
61
- undefined,
62
- "2026-02-01T00:00:00Z",
63
- );
50
+ await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
51
+ description: "Future bonus",
52
+ referenceId: "promo:tenant-1-future",
53
+ expiresAt: "2026-02-01T00:00:00Z",
54
+ });
64
55
 
65
56
  const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
66
57
  expect(result.processed).toBe(0);
@@ -70,7 +61,7 @@ describe("runCreditExpiryCron", () => {
70
61
  });
71
62
 
72
63
  it("does not debit credits without expires_at", async () => {
73
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "Top-up");
64
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "Top-up" });
74
65
 
75
66
  const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
76
67
  expect(result.processed).toBe(0);
@@ -80,17 +71,12 @@ describe("runCreditExpiryCron", () => {
80
71
  });
81
72
 
82
73
  it("only debits up to available balance when partially consumed", async () => {
83
- await ledger.credit(
84
- "tenant-1",
85
- Credit.fromCents(500),
86
- "promo",
87
- "Promo",
88
- "promo:partial",
89
- undefined,
90
- undefined,
91
- "2026-01-10T00:00:00Z",
92
- );
93
- await ledger.debit("tenant-1", Credit.fromCents(300), "bot_runtime", "Runtime");
74
+ await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
75
+ description: "Promo",
76
+ referenceId: "promo:partial",
77
+ expiresAt: "2026-01-10T00:00:00Z",
78
+ });
79
+ await ledger.debit("tenant-1", Credit.fromCents(300), "bot_runtime", { description: "Runtime" });
94
80
 
95
81
  const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
96
82
  expect(result.processed).toBe(1);
@@ -100,16 +86,11 @@ describe("runCreditExpiryCron", () => {
100
86
  });
101
87
 
102
88
  it("is idempotent -- does not double-debit on second run", async () => {
103
- await ledger.credit(
104
- "tenant-1",
105
- Credit.fromCents(500),
106
- "promo",
107
- "Promo",
108
- "promo:idemp",
109
- undefined,
110
- undefined,
111
- "2026-01-10T00:00:00Z",
112
- );
89
+ await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
90
+ description: "Promo",
91
+ referenceId: "promo:idemp",
92
+ expiresAt: "2026-01-10T00:00:00Z",
93
+ });
113
94
 
114
95
  await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
115
96
  const balanceAfterFirst = await ledger.balance("tenant-1");
@@ -1,43 +1,34 @@
1
1
  import type { PGlite } from "@electric-sql/pglite";
2
- import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
2
+ import { CREDIT_TYPE_ACCOUNT, Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
3
3
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
4
- import type { DrizzleDb } from "../../db/index.js";
5
- import { creditBalances, creditTransactions } from "../../db/schema/credits.js";
4
+ import type { PlatformDb } from "../../db/index.js";
6
5
  import { createTestDb, truncateAllTables } from "../../test/db.js";
7
- import { DrizzleCreditTransactionRepository } from "./credit-transaction-repository.js";
8
6
  import { type DividendCronConfig, runDividendCron } from "./dividend-cron.js";
9
7
 
10
- async function insertPurchase(db: DrizzleDb, tenantId: string, amountCents: number, createdAt: string): Promise<void> {
11
- const id = `test-${tenantId}-${Date.now()}-${Math.random()}`;
8
+ async function insertPurchase(
9
+ ledger: DrizzleLedger,
10
+ tenantId: string,
11
+ amountCents: number,
12
+ postedAt: string,
13
+ ): Promise<void> {
12
14
  const amount = Credit.fromCents(amountCents);
13
- await db.insert(creditTransactions).values({
14
- id,
15
+ await ledger.post({
16
+ entryType: "purchase",
15
17
  tenantId,
16
- amount,
17
- balanceAfter: amount,
18
- type: "purchase",
19
- createdAt,
18
+ description: `Test purchase ${amountCents}¢`,
19
+ referenceId: `test-purchase:${tenantId}:${postedAt}:${Math.random()}`,
20
+ postedAt,
21
+ lines: [
22
+ { accountCode: CREDIT_TYPE_ACCOUNT.purchase, amount, side: "debit" },
23
+ { accountCode: `2000:${tenantId}`, amount, side: "credit" },
24
+ ],
20
25
  });
21
- // Upsert credit_balances
22
- const existing = await db
23
- .select()
24
- .from(creditBalances)
25
- .where((await import("drizzle-orm")).eq(creditBalances.tenantId, tenantId));
26
- if (existing.length > 0) {
27
- await db
28
- .update(creditBalances)
29
- .set({ balance: existing[0].balance.add(amount) })
30
- .where((await import("drizzle-orm")).eq(creditBalances.tenantId, tenantId));
31
- } else {
32
- await db.insert(creditBalances).values({ tenantId, balance: amount });
33
- }
34
26
  }
35
27
 
36
28
  describe("runDividendCron", () => {
37
29
  let pool: PGlite;
38
- let db: DrizzleDb;
39
- let ledger: CreditLedger;
40
- let creditTransactionRepo: DrizzleCreditTransactionRepository;
30
+ let db: PlatformDb;
31
+ let ledger: DrizzleLedger;
41
32
 
42
33
  beforeAll(async () => {
43
34
  ({ db, pool } = await createTestDb());
@@ -49,13 +40,12 @@ describe("runDividendCron", () => {
49
40
 
50
41
  beforeEach(async () => {
51
42
  await truncateAllTables(pool);
52
- ledger = new CreditLedger(db);
53
- creditTransactionRepo = new DrizzleCreditTransactionRepository(db);
43
+ ledger = new DrizzleLedger(db);
44
+ await ledger.seedSystemAccounts();
54
45
  });
55
46
 
56
47
  function makeConfig(overrides?: Partial<DividendCronConfig>): DividendCronConfig {
57
48
  return {
58
- creditTransactionRepo,
59
49
  ledger,
60
50
  matchRate: 1.0,
61
51
  targetDate: "2026-02-20",
@@ -64,7 +54,7 @@ describe("runDividendCron", () => {
64
54
  }
65
55
 
66
56
  it("distributes dividend to eligible tenants", async () => {
67
- await insertPurchase(db, "t1", 1000, "2026-02-20 12:00:00");
57
+ await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
68
58
 
69
59
  const result = await runDividendCron(makeConfig());
70
60
 
@@ -75,7 +65,7 @@ describe("runDividendCron", () => {
75
65
  });
76
66
 
77
67
  it("is idempotent — skips if already ran for the date", async () => {
78
- await insertPurchase(db, "t1", 1000, "2026-02-20 12:00:00");
68
+ await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
79
69
 
80
70
  const result1 = await runDividendCron(makeConfig());
81
71
  expect(result1.distributed).toBe(1);
@@ -91,23 +81,22 @@ describe("runDividendCron", () => {
91
81
  });
92
82
 
93
83
  it("handles floor rounding — remainder is not distributed", async () => {
94
- await insertPurchase(db, "t1", 50, "2026-02-20 12:00:00");
95
- await insertPurchase(db, "t2", 30, "2026-02-20 12:00:00");
96
- await insertPurchase(db, "t3", 20, "2026-02-20 12:00:00");
84
+ await insertPurchase(ledger, "t1", 50, "2026-02-20 12:00:00");
85
+ await insertPurchase(ledger, "t2", 30, "2026-02-20 12:00:00");
86
+ await insertPurchase(ledger, "t3", 20, "2026-02-20 12:00:00");
97
87
 
98
88
  const result = await runDividendCron(makeConfig());
99
89
 
100
90
  expect(result.pool.toCents()).toBe(100);
101
91
  expect(result.activeCount).toBe(3);
102
92
  // Nanodollar precision: floor(1_000_000_000 raw / 3) = 333_333_333 raw each
103
- // Remainder = 1 nanodollar (not 1 cent — far less wasted with higher scale)
104
93
  expect(result.perUser.toRaw()).toBe(333_333_333);
105
94
  expect(result.distributed).toBe(3);
106
95
  });
107
96
 
108
97
  it("skips distribution when pool is zero", async () => {
109
98
  // Tenant purchased within 7 days but NOT on target date -> pool = 0
110
- await insertPurchase(db, "t1", 500, "2026-02-18 12:00:00");
99
+ await insertPurchase(ledger, "t1", 500, "2026-02-18 12:00:00");
111
100
 
112
101
  const result = await runDividendCron(makeConfig());
113
102
 
@@ -118,11 +107,9 @@ describe("runDividendCron", () => {
118
107
  });
119
108
 
120
109
  it("distributes sub-cent amounts at nanodollar precision", async () => {
121
- // 1 cent purchase, 3 active users: pool = 10_000_000 raw
122
- // floor(10_000_000 / 3) = 3_333_333 raw each — non-zero, gets distributed
123
- await insertPurchase(db, "t1", 1, "2026-02-20 12:00:00");
124
- await insertPurchase(db, "t2", 500, "2026-02-18 12:00:00");
125
- await insertPurchase(db, "t3", 500, "2026-02-17 12:00:00");
110
+ await insertPurchase(ledger, "t1", 1, "2026-02-20 12:00:00");
111
+ await insertPurchase(ledger, "t2", 500, "2026-02-18 12:00:00");
112
+ await insertPurchase(ledger, "t3", 500, "2026-02-17 12:00:00");
126
113
 
127
114
  const result = await runDividendCron(makeConfig({ matchRate: 1.0 }));
128
115
 
@@ -133,21 +120,20 @@ describe("runDividendCron", () => {
133
120
  });
134
121
 
135
122
  it("records transactions with correct type and referenceId", async () => {
136
- await insertPurchase(db, "t1", 1000, "2026-02-20 12:00:00");
123
+ await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
137
124
 
138
125
  await runDividendCron(makeConfig());
139
126
 
140
127
  const history = await ledger.history("t1", { type: "community_dividend" });
141
128
  expect(history).toHaveLength(1);
142
- expect(history[0].type).toBe("community_dividend");
129
+ expect(history[0].entryType).toBe("community_dividend");
143
130
  expect(history[0].referenceId).toBe("dividend:2026-02-20:t1");
144
- expect(history[0].amount.toCents()).toBe(1000);
145
131
  expect(history[0].description).toContain("Community dividend");
146
132
  });
147
133
 
148
134
  it("collects errors without stopping distribution to other tenants", async () => {
149
- await insertPurchase(db, "t1", 500, "2026-02-20 12:00:00");
150
- await insertPurchase(db, "t2", 500, "2026-02-20 12:00:00");
135
+ await insertPurchase(ledger, "t1", 500, "2026-02-20 12:00:00");
136
+ await insertPurchase(ledger, "t2", 500, "2026-02-20 12:00:00");
151
137
 
152
138
  const result = await runDividendCron(makeConfig());
153
139
 
@@ -1,11 +1,9 @@
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 { logger } from "../../config/logger.js";
4
- import type { ICreditTransactionRepository } from "./credit-transaction-repository.js";
5
4
 
6
5
  export interface DividendCronConfig {
7
- creditTransactionRepo: ICreditTransactionRepository;
8
- ledger: ICreditLedger;
6
+ ledger: ILedger;
9
7
  /** Fraction of daily purchases matched as dividend pool. Default 1.0 (100%). */
10
8
  matchRate: number;
11
9
  /** The date to compute dividend for, as YYYY-MM-DD string. Typically yesterday. */
@@ -43,7 +41,7 @@ export async function runDividendCron(cfg: DividendCronConfig): Promise<Dividend
43
41
  // Idempotency: check if any per-tenant dividend was already distributed for this date.
44
42
  // We look for any referenceId matching "dividend:YYYY-MM-DD:*".
45
43
  const sentinelPrefix = `dividend:${cfg.targetDate}:`;
46
- const alreadyRan = await cfg.creditTransactionRepo.existsByReferenceIdLike(`${sentinelPrefix}%`);
44
+ const alreadyRan = await cfg.ledger.existsByReferenceIdLike(`${sentinelPrefix}%`);
47
45
 
48
46
  if (alreadyRan) {
49
47
  result.skippedAlreadyRun = true;
@@ -55,7 +53,7 @@ export async function runDividendCron(cfg: DividendCronConfig): Promise<Dividend
55
53
  const dayStart = `${cfg.targetDate} 00:00:00`;
56
54
  const dayEnd = `${cfg.targetDate} 24:00:00`;
57
55
 
58
- const dailyPurchaseTotalCredit = await cfg.creditTransactionRepo.sumPurchasesForPeriod(dayStart, dayEnd);
56
+ const dailyPurchaseTotalCredit = await cfg.ledger.sumPurchasesForPeriod(dayStart, dayEnd);
59
57
  result.pool = dailyPurchaseTotalCredit.multiply(cfg.matchRate);
60
58
 
61
59
  // Step 2: Find all active tenants (purchased in last 7 days from target date).
@@ -64,7 +62,7 @@ export async function runDividendCron(cfg: DividendCronConfig): Promise<Dividend
64
62
  const windowStart = subtractDays(cfg.targetDate, 6);
65
63
  const windowStartTs = `${windowStart} 00:00:00`;
66
64
 
67
- const activeTenantIds = await cfg.creditTransactionRepo.getActiveTenantIdsInWindow(windowStartTs, dayEnd);
65
+ const activeTenantIds = await cfg.ledger.getActiveTenantIdsInWindow(windowStartTs, dayEnd);
68
66
  result.activeCount = activeTenantIds.length;
69
67
 
70
68
  // Step 3: Compute per-user share.
@@ -92,13 +90,10 @@ export async function runDividendCron(cfg: DividendCronConfig): Promise<Dividend
92
90
  for (const tenantId of activeTenantIds) {
93
91
  const perUserRef = `dividend:${cfg.targetDate}:${tenantId}`;
94
92
  try {
95
- await cfg.ledger.credit(
96
- tenantId,
97
- result.perUser,
98
- "community_dividend",
99
- `Community dividend for ${cfg.targetDate}: pool ${result.pool.toCents()}c / ${result.activeCount} users`,
100
- perUserRef,
101
- );
93
+ await cfg.ledger.credit(tenantId, result.perUser, "community_dividend", {
94
+ description: `Community dividend for ${cfg.targetDate}: pool ${result.pool.toCents()}c / ${result.activeCount} users`,
95
+ referenceId: perUserRef,
96
+ });
102
97
  result.distributed++;
103
98
  } catch (err) {
104
99
  const msg = err instanceof Error ? err.message : String(err);
@@ -1,6 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import type { PGlite } from "@electric-sql/pglite";
3
- import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
3
+ import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
4
4
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
5
5
  import type { DrizzleDb } from "../../db/index.js";
6
6
  import { adminUsers } from "../../db/schema/admin-users.js";
@@ -54,6 +54,7 @@ describe("DrizzleDividendRepository", () => {
54
54
 
55
55
  beforeEach(async () => {
56
56
  await truncateAllTables(pool);
57
+ await new DrizzleLedger(db).seedSystemAccounts();
57
58
  repo = new DrizzleDividendRepository(db);
58
59
  });
59
60
 
@@ -194,8 +195,8 @@ describe("DrizzleDividendRepository", () => {
194
195
  });
195
196
 
196
197
  it("marks user as eligible when they have a recent purchase", async () => {
197
- const ledger = new CreditLedger(db);
198
- await ledger.credit("t1", Credit.fromCents(100), "purchase", "recent buy");
198
+ const ledger = new DrizzleLedger(db);
199
+ await ledger.credit("t1", Credit.fromCents(100), "purchase", { description: "recent buy" });
199
200
 
200
201
  const stats = await repo.getStats("t1");
201
202
  expect(stats.userEligible).toBe(true);
@@ -2,8 +2,8 @@ import { Credit } from "@wopr-network/platform-core/credits";
2
2
  import { and, desc, eq, gte, lt, sql } from "drizzle-orm";
3
3
  import type { DrizzleDb } from "../../db/index.js";
4
4
  import { adminUsers } from "../../db/schema/admin-users.js";
5
- import { creditTransactions } from "../../db/schema/credits.js";
6
5
  import { dividendDistributions } from "../../db/schema/dividend-distributions.js";
6
+ import { journalEntries, journalLines } from "../../db/schema/ledger.js";
7
7
  import type { DividendHistoryEntry, DividendStats } from "../repository-types.js";
8
8
 
9
9
  export type { DividendHistoryEntry, DividendStats };
@@ -30,35 +30,32 @@ export class DrizzleDividendRepository implements IDividendRepository {
30
30
  constructor(private readonly db: DrizzleDb) {}
31
31
 
32
32
  async getStats(tenantId: string): Promise<DividendStats> {
33
- // 1. Pool = sum of purchase amounts from yesterday UTC
33
+ // 1. Pool = sum of purchase credit amounts from yesterday UTC
34
34
  const poolRow = (
35
35
  await this.db
36
- // raw SQL: Drizzle cannot express COALESCE(SUM(...), 0) aggregate
37
- .select({ total: sql<number>`COALESCE(SUM(${creditTransactions.amount}), 0)` })
38
- .from(creditTransactions)
36
+ .select({ total: sql<string>`COALESCE(SUM(${journalLines.amount}), 0)` })
37
+ .from(journalLines)
38
+ .innerJoin(journalEntries, eq(journalEntries.id, journalLines.journalEntryId))
39
39
  .where(
40
40
  and(
41
- eq(creditTransactions.type, "purchase"),
42
- // raw SQL: Drizzle cannot express date_trunc with interval arithmetic
43
- sql`${creditTransactions.createdAt}::timestamp >= date_trunc('day', timezone('UTC', now())) - INTERVAL '1 day'`,
44
- sql`${creditTransactions.createdAt}::timestamp < date_trunc('day', timezone('UTC', now()))`,
41
+ eq(journalEntries.entryType, "purchase"),
42
+ eq(journalLines.side, "credit"),
43
+ sql`${journalEntries.postedAt}::timestamp >= date_trunc('day', timezone('UTC', now())) - INTERVAL '1 day'`,
44
+ sql`${journalEntries.postedAt}::timestamp < date_trunc('day', timezone('UTC', now()))`,
45
45
  ),
46
46
  )
47
47
  )[0];
48
- const poolCents = poolRow?.total ?? 0;
49
- const pool = Credit.fromCents(poolCents);
48
+ const pool = Credit.fromRaw(Number(poolRow?.total ?? 0));
50
49
 
51
50
  // 2. Active users = distinct tenants with a purchase in the last 7 days
52
51
  const activeRow = (
53
52
  await this.db
54
- // raw SQL: Drizzle cannot express COUNT(DISTINCT col)
55
- .select({ count: sql<number>`COUNT(DISTINCT ${creditTransactions.tenantId})` })
56
- .from(creditTransactions)
53
+ .select({ count: sql<number>`COUNT(DISTINCT ${journalEntries.tenantId})` })
54
+ .from(journalEntries)
57
55
  .where(
58
56
  and(
59
- eq(creditTransactions.type, "purchase"),
60
- // raw SQL: Drizzle cannot express timestamp comparison with interval arithmetic
61
- sql`${creditTransactions.createdAt}::timestamp >= timezone('UTC', now()) - INTERVAL '7 days'`,
57
+ eq(journalEntries.entryType, "purchase"),
58
+ sql`${journalEntries.postedAt}::timestamp >= timezone('UTC', now()) - INTERVAL '7 days'`,
62
59
  ),
63
60
  )
64
61
  )[0];
@@ -75,10 +72,10 @@ export class DrizzleDividendRepository implements IDividendRepository {
75
72
  // 5. User eligibility — last purchase within 7 days
76
73
  const userPurchaseRow = (
77
74
  await this.db
78
- .select({ createdAt: creditTransactions.createdAt })
79
- .from(creditTransactions)
80
- .where(and(eq(creditTransactions.tenantId, tenantId), eq(creditTransactions.type, "purchase")))
81
- .orderBy(desc(creditTransactions.createdAt))
75
+ .select({ postedAt: journalEntries.postedAt })
76
+ .from(journalEntries)
77
+ .where(and(eq(journalEntries.tenantId, tenantId), eq(journalEntries.entryType, "purchase")))
78
+ .orderBy(desc(journalEntries.postedAt))
82
79
  .limit(1)
83
80
  )[0];
84
81
 
@@ -87,10 +84,7 @@ export class DrizzleDividendRepository implements IDividendRepository {
87
84
  let userWindowExpiresAt: string | null = null;
88
85
 
89
86
  if (userPurchaseRow) {
90
- const rawTs = userPurchaseRow.createdAt;
91
- // Parse the timestamp directly. PGlite may return ISO strings with or without
92
- // timezone suffix. JavaScript's Date constructor handles ISO 8601 strings natively.
93
- const lastPurchase = new Date(rawTs);
87
+ const lastPurchase = new Date(userPurchaseRow.postedAt);
94
88
  userLastPurchaseAt = lastPurchase.toISOString();
95
89
 
96
90
  const windowExpiry = new Date(lastPurchase.getTime() + 7 * 24 * 60 * 60 * 1000);
@@ -2,24 +2,24 @@ export type {
2
2
  AutoTopupSettings,
3
3
  CreditExpiryCronConfig,
4
4
  CreditExpiryCronResult,
5
- CreditTransaction,
6
5
  CreditType,
7
6
  DebitType,
8
7
  HistoryOptions,
9
8
  IAutoTopupSettingsRepository,
10
- ICreditLedger,
9
+ ILedger,
10
+ JournalEntry,
11
11
  TransactionType,
12
12
  } from "@wopr-network/platform-core/credits";
13
13
  export {
14
14
  ALLOWED_SCHEDULE_INTERVALS,
15
15
  ALLOWED_THRESHOLDS,
16
16
  ALLOWED_TOPUP_AMOUNTS,
17
- CreditLedger,
18
17
  computeNextScheduleAt,
19
18
  DrizzleAutoTopupSettingsRepository,
20
- DrizzleCreditLedger,
19
+ DrizzleLedger,
21
20
  grantSignupCredits,
22
21
  InsufficientBalanceError,
22
+ Ledger,
23
23
  runCreditExpiryCron,
24
24
  SIGNUP_GRANT,
25
25
  } from "@wopr-network/platform-core/credits";
@@ -1,13 +1,13 @@
1
1
  import type { PGlite } from "@electric-sql/pglite";
2
- import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
2
+ import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
3
3
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
4
4
  import type { DrizzleDb } from "../../db/index.js";
5
5
  import { createTestDb, truncateAllTables } from "../../test/db.js";
6
6
 
7
- describe("DrizzleCreditLedger.memberUsage", () => {
7
+ describe("DrizzleDrizzleLedger.memberUsage", () => {
8
8
  let pool: PGlite;
9
9
  let db: DrizzleDb;
10
- let ledger: CreditLedger;
10
+ let ledger: DrizzleLedger;
11
11
 
12
12
  beforeAll(async () => {
13
13
  ({ db, pool } = await createTestDb());
@@ -19,14 +19,25 @@ describe("DrizzleCreditLedger.memberUsage", () => {
19
19
 
20
20
  beforeEach(async () => {
21
21
  await truncateAllTables(pool);
22
- ledger = new CreditLedger(db);
22
+ ledger = new DrizzleLedger(db);
23
+
24
+ await ledger.seedSystemAccounts();
23
25
  });
24
26
 
25
27
  it("should aggregate debit totals per attributed user", async () => {
26
- await ledger.credit("org-1", Credit.fromCents(10000), "purchase", "Seed");
27
- await ledger.debit("org-1", Credit.fromCents(100), "adapter_usage", "Chat", undefined, false, "user-a");
28
- await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage", "Chat", undefined, false, "user-a");
29
- await ledger.debit("org-1", Credit.fromCents(300), "adapter_usage", "Chat", undefined, false, "user-b");
28
+ await ledger.credit("org-1", Credit.fromCents(10000), "purchase", { description: "Seed" });
29
+ await ledger.debit("org-1", Credit.fromCents(100), "adapter_usage", {
30
+ description: "Chat",
31
+ attributedUserId: "user-a",
32
+ });
33
+ await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage", {
34
+ description: "Chat",
35
+ attributedUserId: "user-a",
36
+ });
37
+ await ledger.debit("org-1", Credit.fromCents(300), "adapter_usage", {
38
+ description: "Chat",
39
+ attributedUserId: "user-b",
40
+ });
30
41
 
31
42
  const result = await ledger.memberUsage("org-1");
32
43
  expect(result).toHaveLength(2);
@@ -41,9 +52,12 @@ describe("DrizzleCreditLedger.memberUsage", () => {
41
52
  });
42
53
 
43
54
  it("should exclude transactions with null attributedUserId", async () => {
44
- await ledger.credit("org-1", Credit.fromCents(10000), "purchase", "Seed");
45
- await ledger.debit("org-1", Credit.fromCents(100), "bot_runtime", "Cron"); // no attributedUserId
46
- await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage", "Chat", undefined, false, "user-a");
55
+ await ledger.credit("org-1", Credit.fromCents(10000), "purchase", { description: "Seed" });
56
+ await ledger.debit("org-1", Credit.fromCents(100), "bot_runtime", { description: "Cron" }); // no attributedUserId
57
+ await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage", {
58
+ description: "Chat",
59
+ attributedUserId: "user-a",
60
+ });
47
61
 
48
62
  const result = await ledger.memberUsage("org-1");
49
63
  expect(result).toHaveLength(1);