@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,44 +1,40 @@
1
1
  import type { PGlite } from "@electric-sql/pglite";
2
2
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
3
3
  import type { PlatformDb } from "../db/index.js";
4
- import { creditBalances, creditTransactions } from "../db/schema/credits.js";
5
4
  import { createTestDb, truncateAllTables } from "../test/db.js";
6
5
  import { Credit } from "./credit.js";
7
- import { CreditLedger } from "./credit-ledger.js";
8
- import { DrizzleCreditTransactionRepository } from "./credit-transaction-repository.js";
9
6
  import { type DividendCronConfig, runDividendCron } from "./dividend-cron.js";
10
-
11
- async function insertPurchase(db: PlatformDb, tenantId: string, amountCents: number, createdAt: string): Promise<void> {
12
- const id = `test-${tenantId}-${Date.now()}-${Math.random()}`;
7
+ import { CREDIT_TYPE_ACCOUNT, DrizzleLedger } from "./ledger.js";
8
+
9
+ /**
10
+ * Insert a backdated purchase entry into the double-entry ledger.
11
+ * Uses post() with postedAt override to simulate historical purchases.
12
+ */
13
+ async function insertPurchase(
14
+ ledger: DrizzleLedger,
15
+ tenantId: string,
16
+ amountCents: number,
17
+ postedAt: string,
18
+ ): Promise<void> {
13
19
  const amount = Credit.fromCents(amountCents);
14
- await db.insert(creditTransactions).values({
15
- id,
20
+ // Purchase: DR cash (1000), CR unearned_revenue (2000:<tenantId>)
21
+ await ledger.post({
22
+ entryType: "purchase",
16
23
  tenantId,
17
- amount,
18
- balanceAfter: amount,
19
- type: "purchase",
20
- createdAt,
24
+ description: `Test purchase ${amountCents}¢`,
25
+ referenceId: `test-purchase:${tenantId}:${postedAt}:${Math.random()}`,
26
+ postedAt,
27
+ lines: [
28
+ { accountCode: CREDIT_TYPE_ACCOUNT.purchase, amount, side: "debit" },
29
+ { accountCode: `2000:${tenantId}`, amount, side: "credit" },
30
+ ],
21
31
  });
22
- // Upsert credit_balances
23
- const existing = await db
24
- .select()
25
- .from(creditBalances)
26
- .where((await import("drizzle-orm")).eq(creditBalances.tenantId, tenantId));
27
- if (existing.length > 0) {
28
- await db
29
- .update(creditBalances)
30
- .set({ balance: existing[0].balance.add(amount) })
31
- .where((await import("drizzle-orm")).eq(creditBalances.tenantId, tenantId));
32
- } else {
33
- await db.insert(creditBalances).values({ tenantId, balance: amount });
34
- }
35
32
  }
36
33
 
37
34
  describe("runDividendCron", () => {
38
35
  let pool: PGlite;
39
36
  let db: PlatformDb;
40
- let ledger: CreditLedger;
41
- let creditTransactionRepo: DrizzleCreditTransactionRepository;
37
+ let ledger: DrizzleLedger;
42
38
 
43
39
  beforeAll(async () => {
44
40
  ({ db, pool } = await createTestDb());
@@ -50,13 +46,12 @@ describe("runDividendCron", () => {
50
46
 
51
47
  beforeEach(async () => {
52
48
  await truncateAllTables(pool);
53
- ledger = new CreditLedger(db);
54
- creditTransactionRepo = new DrizzleCreditTransactionRepository(db);
49
+ ledger = new DrizzleLedger(db);
50
+ await ledger.seedSystemAccounts();
55
51
  });
56
52
 
57
53
  function makeConfig(overrides?: Partial<DividendCronConfig>): DividendCronConfig {
58
54
  return {
59
- creditTransactionRepo,
60
55
  ledger,
61
56
  matchRate: 1.0,
62
57
  targetDate: "2026-02-20",
@@ -65,7 +60,7 @@ describe("runDividendCron", () => {
65
60
  }
66
61
 
67
62
  it("distributes dividend to eligible tenants", async () => {
68
- await insertPurchase(db, "t1", 1000, "2026-02-20 12:00:00");
63
+ await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
69
64
 
70
65
  const result = await runDividendCron(makeConfig());
71
66
 
@@ -76,7 +71,7 @@ describe("runDividendCron", () => {
76
71
  });
77
72
 
78
73
  it("is idempotent — skips if already ran for the date", async () => {
79
- await insertPurchase(db, "t1", 1000, "2026-02-20 12:00:00");
74
+ await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
80
75
 
81
76
  const result1 = await runDividendCron(makeConfig());
82
77
  expect(result1.distributed).toBe(1);
@@ -92,23 +87,22 @@ describe("runDividendCron", () => {
92
87
  });
93
88
 
94
89
  it("handles floor rounding — remainder is not distributed", async () => {
95
- await insertPurchase(db, "t1", 50, "2026-02-20 12:00:00");
96
- await insertPurchase(db, "t2", 30, "2026-02-20 12:00:00");
97
- await insertPurchase(db, "t3", 20, "2026-02-20 12:00:00");
90
+ await insertPurchase(ledger, "t1", 50, "2026-02-20 12:00:00");
91
+ await insertPurchase(ledger, "t2", 30, "2026-02-20 12:00:00");
92
+ await insertPurchase(ledger, "t3", 20, "2026-02-20 12:00:00");
98
93
 
99
94
  const result = await runDividendCron(makeConfig());
100
95
 
101
96
  expect(result.pool.toCents()).toBe(100);
102
97
  expect(result.activeCount).toBe(3);
103
98
  // Nanodollar precision: floor(1_000_000_000 raw / 3) = 333_333_333 raw each
104
- // Remainder = 1 nanodollar (not 1 cent — far less wasted with higher scale)
105
99
  expect(result.perUser.toRaw()).toBe(333_333_333);
106
100
  expect(result.distributed).toBe(3);
107
101
  });
108
102
 
109
103
  it("skips distribution when pool is zero", async () => {
110
104
  // Tenant purchased within 7 days but NOT on target date -> pool = 0
111
- await insertPurchase(db, "t1", 500, "2026-02-18 12:00:00");
105
+ await insertPurchase(ledger, "t1", 500, "2026-02-18 12:00:00");
112
106
 
113
107
  const result = await runDividendCron(makeConfig());
114
108
 
@@ -121,9 +115,9 @@ describe("runDividendCron", () => {
121
115
  it("distributes sub-cent amounts at nanodollar precision", async () => {
122
116
  // 1 cent purchase, 3 active users: pool = 10_000_000 raw
123
117
  // floor(10_000_000 / 3) = 3_333_333 raw each — non-zero, gets distributed
124
- await insertPurchase(db, "t1", 1, "2026-02-20 12:00:00");
125
- await insertPurchase(db, "t2", 500, "2026-02-18 12:00:00");
126
- await insertPurchase(db, "t3", 500, "2026-02-17 12:00:00");
118
+ await insertPurchase(ledger, "t1", 1, "2026-02-20 12:00:00");
119
+ await insertPurchase(ledger, "t2", 500, "2026-02-18 12:00:00");
120
+ await insertPurchase(ledger, "t3", 500, "2026-02-17 12:00:00");
127
121
 
128
122
  const result = await runDividendCron(makeConfig({ matchRate: 1.0 }));
129
123
 
@@ -134,21 +128,20 @@ describe("runDividendCron", () => {
134
128
  });
135
129
 
136
130
  it("records transactions with correct type and referenceId", async () => {
137
- await insertPurchase(db, "t1", 1000, "2026-02-20 12:00:00");
131
+ await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
138
132
 
139
133
  await runDividendCron(makeConfig());
140
134
 
141
135
  const history = await ledger.history("t1", { type: "community_dividend" });
142
136
  expect(history).toHaveLength(1);
143
- expect(history[0].type).toBe("community_dividend");
137
+ expect(history[0].entryType).toBe("community_dividend");
144
138
  expect(history[0].referenceId).toBe("dividend:2026-02-20:t1");
145
- expect(history[0].amount.toCents()).toBe(1000);
146
139
  expect(history[0].description).toContain("Community dividend");
147
140
  });
148
141
 
149
142
  it("collects errors without stopping distribution to other tenants", async () => {
150
- await insertPurchase(db, "t1", 500, "2026-02-20 12:00:00");
151
- await insertPurchase(db, "t2", 500, "2026-02-20 12:00:00");
143
+ await insertPurchase(ledger, "t1", 500, "2026-02-20 12:00:00");
144
+ await insertPurchase(ledger, "t2", 500, "2026-02-20 12:00:00");
152
145
 
153
146
  const result = await runDividendCron(makeConfig());
154
147
 
@@ -1,11 +1,9 @@
1
1
  import { logger } from "../config/logger.js";
2
2
  import { Credit } from "./credit.js";
3
- import type { ICreditLedger } from "./credit-ledger.js";
4
- import type { ICreditTransactionRepository } from "./credit-transaction-repository.js";
3
+ import type { ILedger } from "./ledger.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. */
@@ -25,8 +23,8 @@ export interface DividendCronResult {
25
23
  * Compute and distribute the community dividend for a given day.
26
24
  *
27
25
  * 1. Check idempotency — skip if already run for this date.
28
- * 2. Sum all 'purchase' transactions for the target date.
29
- * 3. Find all tenants with a 'purchase' transaction in the last 7 days.
26
+ * 2. Sum all 'purchase' entries for the target date.
27
+ * 3. Find all tenants with a 'purchase' entry in the last 7 days.
30
28
  * 4. Compute pool = sum × matchRate, per-user share = floor(pool / activeCount).
31
29
  * 5. Credit each active tenant with their share.
32
30
  */
@@ -40,10 +38,8 @@ export async function runDividendCron(cfg: DividendCronConfig): Promise<Dividend
40
38
  errors: [],
41
39
  };
42
40
 
43
- // Idempotency: check if any per-tenant dividend was already distributed for this date.
44
- // We look for any referenceId matching "dividend:YYYY-MM-DD:*".
45
41
  const sentinelPrefix = `dividend:${cfg.targetDate}:`;
46
- const alreadyRan = await cfg.creditTransactionRepo.existsByReferenceIdLike(`${sentinelPrefix}%`);
42
+ const alreadyRan = await cfg.ledger.existsByReferenceIdLike(`${sentinelPrefix}%`);
47
43
 
48
44
  if (alreadyRan) {
49
45
  result.skippedAlreadyRun = true;
@@ -51,23 +47,18 @@ export async function runDividendCron(cfg: DividendCronConfig): Promise<Dividend
51
47
  return result;
52
48
  }
53
49
 
54
- // Step 1: Sum all purchase amounts for the target date.
55
50
  const dayStart = `${cfg.targetDate} 00:00:00`;
56
51
  const dayEnd = `${cfg.targetDate} 24:00:00`;
57
52
 
58
- const dailyPurchaseTotalCredit = await cfg.creditTransactionRepo.sumPurchasesForPeriod(dayStart, dayEnd);
59
- result.pool = dailyPurchaseTotalCredit.multiply(cfg.matchRate);
53
+ const dailyPurchaseTotal = await cfg.ledger.sumPurchasesForPeriod(dayStart, dayEnd);
54
+ result.pool = dailyPurchaseTotal.multiply(cfg.matchRate);
60
55
 
61
- // Step 2: Find all active tenants (purchased in last 7 days from target date).
62
- // The 7-day window is: [targetDate - 6 days 00:00:00, targetDate 24:00:00)
63
- // This gives a full 7-day range ending at the end of targetDate.
64
56
  const windowStart = subtractDays(cfg.targetDate, 6);
65
57
  const windowStartTs = `${windowStart} 00:00:00`;
66
58
 
67
- const activeTenantIds = await cfg.creditTransactionRepo.getActiveTenantIdsInWindow(windowStartTs, dayEnd);
59
+ const activeTenantIds = await cfg.ledger.getActiveTenantIdsInWindow(windowStartTs, dayEnd);
68
60
  result.activeCount = activeTenantIds.length;
69
61
 
70
- // Step 3: Compute per-user share.
71
62
  if (result.pool.isZero() || result.activeCount <= 0) {
72
63
  logger.info("Dividend cron: no pool or no active tenants", {
73
64
  targetDate: cfg.targetDate,
@@ -88,17 +79,13 @@ export async function runDividendCron(cfg: DividendCronConfig): Promise<Dividend
88
79
  return result;
89
80
  }
90
81
 
91
- // Step 4: Distribute to each active tenant.
92
82
  for (const tenantId of activeTenantIds) {
93
83
  const perUserRef = `dividend:${cfg.targetDate}:${tenantId}`;
94
84
  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
- );
85
+ await cfg.ledger.credit(tenantId, result.perUser, "community_dividend", {
86
+ description: `Community dividend for ${cfg.targetDate}: pool ${result.pool.toCents()}c / ${result.activeCount} users`,
87
+ referenceId: perUserRef,
88
+ });
102
89
  result.distributed++;
103
90
  } catch (err) {
104
91
  const msg = err instanceof Error ? err.message : String(err);
@@ -119,7 +106,6 @@ export async function runDividendCron(cfg: DividendCronConfig): Promise<Dividend
119
106
  return result;
120
107
  }
121
108
 
122
- /** Subtract N days from a YYYY-MM-DD date string, returning YYYY-MM-DD. */
123
109
  function subtractDays(dateStr: string, days: number): string {
124
110
  const d = new Date(`${dateStr}T00:00:00Z`);
125
111
  d.setUTCDate(d.getUTCDate() - days);
@@ -6,8 +6,8 @@ import { adminUsers } from "../db/schema/admin-users.js";
6
6
  import { dividendDistributions } from "../db/schema/dividend-distributions.js";
7
7
  import { createTestDb, truncateAllTables } from "../test/db.js";
8
8
  import { Credit } from "./credit.js";
9
- import { CreditLedger } from "./credit-ledger.js";
10
9
  import { DrizzleDividendRepository } from "./dividend-repository.js";
10
+ import { DrizzleLedger } from "./ledger.js";
11
11
 
12
12
  let pool: PGlite;
13
13
  let db: PlatformDb;
@@ -55,6 +55,7 @@ describe("DrizzleDividendRepository", () => {
55
55
 
56
56
  beforeEach(async () => {
57
57
  await truncateAllTables(pool);
58
+ await new DrizzleLedger(db).seedSystemAccounts();
58
59
  repo = new DrizzleDividendRepository(db);
59
60
  });
60
61
 
@@ -195,8 +196,8 @@ describe("DrizzleDividendRepository", () => {
195
196
  });
196
197
 
197
198
  it("marks user as eligible when they have a recent purchase", async () => {
198
- const ledger = new CreditLedger(db);
199
- await ledger.credit("t1", Credit.fromCents(100), "purchase", "recent buy");
199
+ const ledger = new DrizzleLedger(db);
200
+ await ledger.credit("t1", Credit.fromCents(100), "purchase", { description: "recent buy" });
200
201
 
201
202
  const stats = await repo.getStats("t1");
202
203
  expect(stats.userEligible).toBe(true);
@@ -1,8 +1,8 @@
1
1
  import { and, desc, eq, gte, lt, sql } from "drizzle-orm";
2
2
  import type { PlatformDb } from "../db/index.js";
3
3
  import { adminUsers } from "../db/schema/admin-users.js";
4
- import { creditTransactions } from "../db/schema/credits.js";
5
4
  import { dividendDistributions } from "../db/schema/dividend-distributions.js";
5
+ import { journalEntries, journalLines } from "../db/schema/ledger.js";
6
6
  import { Credit } from "./credit.js";
7
7
  import type { DividendHistoryEntry, DividendStats } from "./repository-types.js";
8
8
 
@@ -30,35 +30,36 @@ export class DrizzleDividendRepository implements IDividendRepository {
30
30
  constructor(private readonly db: PlatformDb) {}
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
+ // In double-entry: purchase entries have a credit line on the tenant liability account.
35
+ // Sum those credit line amounts for entries posted yesterday.
34
36
  const poolRow = (
35
37
  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)
38
+ .select({ total: sql<string>`COALESCE(SUM(${journalLines.amount}), 0)` })
39
+ .from(journalLines)
40
+ .innerJoin(journalEntries, eq(journalEntries.id, journalLines.journalEntryId))
39
41
  .where(
40
42
  and(
41
- eq(creditTransactions.type, "purchase"),
43
+ eq(journalEntries.entryType, "purchase"),
44
+ eq(journalLines.side, "credit"),
42
45
  // 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()))`,
46
+ sql`${journalEntries.postedAt}::timestamp >= date_trunc('day', timezone('UTC', now())) - INTERVAL '1 day'`,
47
+ sql`${journalEntries.postedAt}::timestamp < date_trunc('day', timezone('UTC', now()))`,
45
48
  ),
46
49
  )
47
50
  )[0];
48
- const poolCents = poolRow?.total ?? 0;
49
- const pool = Credit.fromCents(poolCents);
51
+ const pool = Credit.fromRaw(Number(poolRow?.total ?? 0));
50
52
 
51
53
  // 2. Active users = distinct tenants with a purchase in the last 7 days
52
54
  const activeRow = (
53
55
  await this.db
54
- // raw SQL: Drizzle cannot express COUNT(DISTINCT col)
55
- .select({ count: sql<number>`COUNT(DISTINCT ${creditTransactions.tenantId})` })
56
- .from(creditTransactions)
56
+ .select({ count: sql<number>`COUNT(DISTINCT ${journalEntries.tenantId})` })
57
+ .from(journalEntries)
57
58
  .where(
58
59
  and(
59
- eq(creditTransactions.type, "purchase"),
60
+ eq(journalEntries.entryType, "purchase"),
60
61
  // raw SQL: Drizzle cannot express timestamp comparison with interval arithmetic
61
- sql`${creditTransactions.createdAt}::timestamp >= timezone('UTC', now()) - INTERVAL '7 days'`,
62
+ sql`${journalEntries.postedAt}::timestamp >= timezone('UTC', now()) - INTERVAL '7 days'`,
62
63
  ),
63
64
  )
64
65
  )[0];
@@ -75,10 +76,10 @@ export class DrizzleDividendRepository implements IDividendRepository {
75
76
  // 5. User eligibility — last purchase within 7 days
76
77
  const userPurchaseRow = (
77
78
  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))
79
+ .select({ postedAt: journalEntries.postedAt })
80
+ .from(journalEntries)
81
+ .where(and(eq(journalEntries.tenantId, tenantId), eq(journalEntries.entryType, "purchase")))
82
+ .orderBy(desc(journalEntries.postedAt))
82
83
  .limit(1)
83
84
  )[0];
84
85
 
@@ -87,10 +88,7 @@ export class DrizzleDividendRepository implements IDividendRepository {
87
88
  let userWindowExpiresAt: string | null = null;
88
89
 
89
90
  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);
91
+ const lastPurchase = new Date(userPurchaseRow.postedAt);
94
92
  userLastPurchaseAt = lastPurchase.toISOString();
95
93
 
96
94
  const windowExpiry = new Date(lastPurchase.getTime() + 7 * 24 * 60 * 60 * 1000);
@@ -12,14 +12,33 @@ export {
12
12
  export { Credit } from "./credit.js";
13
13
  export type { CreditExpiryCronConfig, CreditExpiryCronResult } from "./credit-expiry-cron.js";
14
14
  export { runCreditExpiryCron } from "./credit-expiry-cron.js";
15
+ // -- Double-entry ledger (new) --
15
16
  export type {
16
- CreditTransaction,
17
+ AccountType,
18
+ CreditOpts,
17
19
  CreditType,
20
+ DebitOpts,
18
21
  DebitType,
19
22
  HistoryOptions,
20
- ICreditLedger,
23
+ ILedger,
24
+ JournalEntry,
25
+ JournalLine,
26
+ MemberUsageSummary,
27
+ PostEntryInput,
28
+ Side,
29
+ SystemAccount,
21
30
  TransactionType,
22
- } from "./credit-ledger.js";
23
- export { CreditLedger, DrizzleCreditLedger, InsufficientBalanceError } from "./credit-ledger.js";
31
+ TrialBalance,
32
+ } from "./ledger.js";
33
+ export {
34
+ CREDIT_TYPE_ACCOUNT,
35
+ DEBIT_TYPE_ACCOUNT,
36
+ DrizzleLedger,
37
+ InsufficientBalanceError,
38
+ Ledger,
39
+ SYSTEM_ACCOUNTS,
40
+ } from "./ledger.js";
24
41
  export { grantSignupCredits, SIGNUP_GRANT } from "./signup-grant.js";
25
42
  export type { ITenantCustomerRepository, TenantCustomerRow } from "./tenant-customer-repository.js";
43
+ export type { TrialBalanceCronConfig, TrialBalanceCronResult } from "./trial-balance-cron.js";
44
+ export { runTrialBalanceCron } from "./trial-balance-cron.js";