@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,40 +1,25 @@
1
- import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
1
+ import { CREDIT_TYPE_ACCOUNT, Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
2
2
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
3
- import { creditBalances, creditTransactions } from "../../db/schema/credits.js";
4
3
  import { createTestDb, truncateAllTables } from "../../test/db.js";
5
- import { DrizzleCreditTransactionRepository } from "./credit-transaction-repository.js";
6
4
  import { runDividendCron } from "./dividend-cron.js";
7
- async function insertPurchase(db, tenantId, amountCents, createdAt) {
8
- const id = `test-${tenantId}-${Date.now()}-${Math.random()}`;
5
+ async function insertPurchase(ledger, tenantId, amountCents, postedAt) {
9
6
  const amount = Credit.fromCents(amountCents);
10
- await db.insert(creditTransactions).values({
11
- id,
7
+ await ledger.post({
8
+ entryType: "purchase",
12
9
  tenantId,
13
- amount,
14
- balanceAfter: amount,
15
- type: "purchase",
16
- createdAt,
10
+ description: `Test purchase ${amountCents}¢`,
11
+ referenceId: `test-purchase:${tenantId}:${postedAt}:${Math.random()}`,
12
+ postedAt,
13
+ lines: [
14
+ { accountCode: CREDIT_TYPE_ACCOUNT.purchase, amount, side: "debit" },
15
+ { accountCode: `2000:${tenantId}`, amount, side: "credit" },
16
+ ],
17
17
  });
18
- // Upsert credit_balances
19
- const existing = await db
20
- .select()
21
- .from(creditBalances)
22
- .where((await import("drizzle-orm")).eq(creditBalances.tenantId, tenantId));
23
- if (existing.length > 0) {
24
- await db
25
- .update(creditBalances)
26
- .set({ balance: existing[0].balance.add(amount) })
27
- .where((await import("drizzle-orm")).eq(creditBalances.tenantId, tenantId));
28
- }
29
- else {
30
- await db.insert(creditBalances).values({ tenantId, balance: amount });
31
- }
32
18
  }
33
19
  describe("runDividendCron", () => {
34
20
  let pool;
35
21
  let db;
36
22
  let ledger;
37
- let creditTransactionRepo;
38
23
  beforeAll(async () => {
39
24
  ({ db, pool } = await createTestDb());
40
25
  });
@@ -43,12 +28,11 @@ describe("runDividendCron", () => {
43
28
  });
44
29
  beforeEach(async () => {
45
30
  await truncateAllTables(pool);
46
- ledger = new CreditLedger(db);
47
- creditTransactionRepo = new DrizzleCreditTransactionRepository(db);
31
+ ledger = new DrizzleLedger(db);
32
+ await ledger.seedSystemAccounts();
48
33
  });
49
34
  function makeConfig(overrides) {
50
35
  return {
51
- creditTransactionRepo,
52
36
  ledger,
53
37
  matchRate: 1.0,
54
38
  targetDate: "2026-02-20",
@@ -56,7 +40,7 @@ describe("runDividendCron", () => {
56
40
  };
57
41
  }
58
42
  it("distributes dividend to eligible tenants", async () => {
59
- await insertPurchase(db, "t1", 1000, "2026-02-20 12:00:00");
43
+ await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
60
44
  const result = await runDividendCron(makeConfig());
61
45
  expect(result.distributed).toBe(1);
62
46
  expect(result.pool.toCents()).toBe(1000);
@@ -64,7 +48,7 @@ describe("runDividendCron", () => {
64
48
  expect(result.activeCount).toBe(1);
65
49
  });
66
50
  it("is idempotent — skips if already ran for the date", async () => {
67
- await insertPurchase(db, "t1", 1000, "2026-02-20 12:00:00");
51
+ await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
68
52
  const result1 = await runDividendCron(makeConfig());
69
53
  expect(result1.distributed).toBe(1);
70
54
  expect(result1.skippedAlreadyRun).toBe(false);
@@ -75,20 +59,19 @@ describe("runDividendCron", () => {
75
59
  expect((await ledger.balance("t1")).equals(balanceAfterFirst)).toBe(true);
76
60
  });
77
61
  it("handles floor rounding — remainder is not distributed", async () => {
78
- await insertPurchase(db, "t1", 50, "2026-02-20 12:00:00");
79
- await insertPurchase(db, "t2", 30, "2026-02-20 12:00:00");
80
- await insertPurchase(db, "t3", 20, "2026-02-20 12:00:00");
62
+ await insertPurchase(ledger, "t1", 50, "2026-02-20 12:00:00");
63
+ await insertPurchase(ledger, "t2", 30, "2026-02-20 12:00:00");
64
+ await insertPurchase(ledger, "t3", 20, "2026-02-20 12:00:00");
81
65
  const result = await runDividendCron(makeConfig());
82
66
  expect(result.pool.toCents()).toBe(100);
83
67
  expect(result.activeCount).toBe(3);
84
68
  // Nanodollar precision: floor(1_000_000_000 raw / 3) = 333_333_333 raw each
85
- // Remainder = 1 nanodollar (not 1 cent — far less wasted with higher scale)
86
69
  expect(result.perUser.toRaw()).toBe(333_333_333);
87
70
  expect(result.distributed).toBe(3);
88
71
  });
89
72
  it("skips distribution when pool is zero", async () => {
90
73
  // Tenant purchased within 7 days but NOT on target date -> pool = 0
91
- await insertPurchase(db, "t1", 500, "2026-02-18 12:00:00");
74
+ await insertPurchase(ledger, "t1", 500, "2026-02-18 12:00:00");
92
75
  const result = await runDividendCron(makeConfig());
93
76
  expect(result.pool.toCents()).toBe(0);
94
77
  expect(result.activeCount).toBe(1);
@@ -96,11 +79,9 @@ describe("runDividendCron", () => {
96
79
  expect(result.distributed).toBe(0);
97
80
  });
98
81
  it("distributes sub-cent amounts at nanodollar precision", async () => {
99
- // 1 cent purchase, 3 active users: pool = 10_000_000 raw
100
- // floor(10_000_000 / 3) = 3_333_333 raw each — non-zero, gets distributed
101
- await insertPurchase(db, "t1", 1, "2026-02-20 12:00:00");
102
- await insertPurchase(db, "t2", 500, "2026-02-18 12:00:00");
103
- await insertPurchase(db, "t3", 500, "2026-02-17 12:00:00");
82
+ await insertPurchase(ledger, "t1", 1, "2026-02-20 12:00:00");
83
+ await insertPurchase(ledger, "t2", 500, "2026-02-18 12:00:00");
84
+ await insertPurchase(ledger, "t3", 500, "2026-02-17 12:00:00");
104
85
  const result = await runDividendCron(makeConfig({ matchRate: 1.0 }));
105
86
  expect(result.pool.toCents()).toBe(1);
106
87
  expect(result.activeCount).toBe(3);
@@ -108,18 +89,17 @@ describe("runDividendCron", () => {
108
89
  expect(result.distributed).toBe(3);
109
90
  });
110
91
  it("records transactions with correct type and referenceId", async () => {
111
- await insertPurchase(db, "t1", 1000, "2026-02-20 12:00:00");
92
+ await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
112
93
  await runDividendCron(makeConfig());
113
94
  const history = await ledger.history("t1", { type: "community_dividend" });
114
95
  expect(history).toHaveLength(1);
115
- expect(history[0].type).toBe("community_dividend");
96
+ expect(history[0].entryType).toBe("community_dividend");
116
97
  expect(history[0].referenceId).toBe("dividend:2026-02-20:t1");
117
- expect(history[0].amount.toCents()).toBe(1000);
118
98
  expect(history[0].description).toContain("Community dividend");
119
99
  });
120
100
  it("collects errors without stopping distribution to other tenants", async () => {
121
- await insertPurchase(db, "t1", 500, "2026-02-20 12:00:00");
122
- await insertPurchase(db, "t2", 500, "2026-02-20 12:00:00");
101
+ await insertPurchase(ledger, "t1", 500, "2026-02-20 12:00:00");
102
+ await insertPurchase(ledger, "t2", 500, "2026-02-20 12:00:00");
123
103
  const result = await runDividendCron(makeConfig());
124
104
  expect(result.distributed).toBe(2);
125
105
  expect(result.errors).toEqual([]);
@@ -1,32 +1,26 @@
1
1
  import { Credit } from "@wopr-network/platform-core/credits";
2
2
  import { and, desc, eq, gte, lt, sql } from "drizzle-orm";
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
  export class DrizzleDividendRepository {
7
7
  db;
8
8
  constructor(db) {
9
9
  this.db = db;
10
10
  }
11
11
  async getStats(tenantId) {
12
- // 1. Pool = sum of purchase amounts from yesterday UTC
12
+ // 1. Pool = sum of purchase credit amounts from yesterday UTC
13
13
  const poolRow = (await this.db
14
- // raw SQL: Drizzle cannot express COALESCE(SUM(...), 0) aggregate
15
- .select({ total: sql `COALESCE(SUM(${creditTransactions.amount}), 0)` })
16
- .from(creditTransactions)
17
- .where(and(eq(creditTransactions.type, "purchase"),
18
- // raw SQL: Drizzle cannot express date_trunc with interval arithmetic
19
- sql `${creditTransactions.createdAt}::timestamp >= date_trunc('day', timezone('UTC', now())) - INTERVAL '1 day'`, sql `${creditTransactions.createdAt}::timestamp < date_trunc('day', timezone('UTC', now()))`)))[0];
20
- const poolCents = poolRow?.total ?? 0;
21
- const pool = Credit.fromCents(poolCents);
14
+ .select({ total: sql `COALESCE(SUM(${journalLines.amount}), 0)` })
15
+ .from(journalLines)
16
+ .innerJoin(journalEntries, eq(journalEntries.id, journalLines.journalEntryId))
17
+ .where(and(eq(journalEntries.entryType, "purchase"), eq(journalLines.side, "credit"), sql `${journalEntries.postedAt}::timestamp >= date_trunc('day', timezone('UTC', now())) - INTERVAL '1 day'`, sql `${journalEntries.postedAt}::timestamp < date_trunc('day', timezone('UTC', now()))`)))[0];
18
+ const pool = Credit.fromRaw(Number(poolRow?.total ?? 0));
22
19
  // 2. Active users = distinct tenants with a purchase in the last 7 days
23
20
  const activeRow = (await this.db
24
- // raw SQL: Drizzle cannot express COUNT(DISTINCT col)
25
- .select({ count: sql `COUNT(DISTINCT ${creditTransactions.tenantId})` })
26
- .from(creditTransactions)
27
- .where(and(eq(creditTransactions.type, "purchase"),
28
- // raw SQL: Drizzle cannot express timestamp comparison with interval arithmetic
29
- sql `${creditTransactions.createdAt}::timestamp >= timezone('UTC', now()) - INTERVAL '7 days'`)))[0];
21
+ .select({ count: sql `COUNT(DISTINCT ${journalEntries.tenantId})` })
22
+ .from(journalEntries)
23
+ .where(and(eq(journalEntries.entryType, "purchase"), sql `${journalEntries.postedAt}::timestamp >= timezone('UTC', now()) - INTERVAL '7 days'`)))[0];
30
24
  const activeUsers = activeRow?.count ?? 0;
31
25
  // 3. Per-user projection (avoid division by zero)
32
26
  const perUser = activeUsers > 0 ? Credit.fromRaw(Math.floor(pool.toRaw() / activeUsers)) : Credit.ZERO;
@@ -36,19 +30,16 @@ export class DrizzleDividendRepository {
36
30
  const nextDistributionAt = nextMidnight.toISOString();
37
31
  // 5. User eligibility — last purchase within 7 days
38
32
  const userPurchaseRow = (await this.db
39
- .select({ createdAt: creditTransactions.createdAt })
40
- .from(creditTransactions)
41
- .where(and(eq(creditTransactions.tenantId, tenantId), eq(creditTransactions.type, "purchase")))
42
- .orderBy(desc(creditTransactions.createdAt))
33
+ .select({ postedAt: journalEntries.postedAt })
34
+ .from(journalEntries)
35
+ .where(and(eq(journalEntries.tenantId, tenantId), eq(journalEntries.entryType, "purchase")))
36
+ .orderBy(desc(journalEntries.postedAt))
43
37
  .limit(1))[0];
44
38
  let userEligible = false;
45
39
  let userLastPurchaseAt = null;
46
40
  let userWindowExpiresAt = null;
47
41
  if (userPurchaseRow) {
48
- const rawTs = userPurchaseRow.createdAt;
49
- // Parse the timestamp directly. PGlite may return ISO strings with or without
50
- // timezone suffix. JavaScript's Date constructor handles ISO 8601 strings natively.
51
- const lastPurchase = new Date(rawTs);
42
+ const lastPurchase = new Date(userPurchaseRow.postedAt);
52
43
  userLastPurchaseAt = lastPurchase.toISOString();
53
44
  const windowExpiry = new Date(lastPurchase.getTime() + 7 * 24 * 60 * 60 * 1000);
54
45
  userWindowExpiresAt = windowExpiry.toISOString();
@@ -1,5 +1,5 @@
1
1
  import crypto from "node:crypto";
2
- import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
2
+ import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
3
3
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
4
4
  import { adminUsers } from "../../db/schema/admin-users.js";
5
5
  import { dividendDistributions } from "../../db/schema/dividend-distributions.js";
@@ -39,6 +39,7 @@ describe("DrizzleDividendRepository", () => {
39
39
  let repo;
40
40
  beforeEach(async () => {
41
41
  await truncateAllTables(pool);
42
+ await new DrizzleLedger(db).seedSystemAccounts();
42
43
  repo = new DrizzleDividendRepository(db);
43
44
  });
44
45
  // --- getHistory() ---
@@ -152,8 +153,8 @@ describe("DrizzleDividendRepository", () => {
152
153
  expect(stats.nextDistributionAt).toEqual(expect.any(String));
153
154
  });
154
155
  it("marks user as eligible when they have a recent purchase", async () => {
155
- const ledger = new CreditLedger(db);
156
- await ledger.credit("t1", Credit.fromCents(100), "purchase", "recent buy");
156
+ const ledger = new DrizzleLedger(db);
157
+ await ledger.credit("t1", Credit.fromCents(100), "purchase", { description: "recent buy" });
157
158
  const stats = await repo.getStats("t1");
158
159
  expect(stats.userEligible).toBe(true);
159
160
  expect(stats.userLastPurchaseAt).toEqual(expect.any(String));
@@ -1,5 +1,5 @@
1
- export type { AutoTopupSettings, CreditExpiryCronConfig, CreditExpiryCronResult, CreditTransaction, CreditType, DebitType, HistoryOptions, IAutoTopupSettingsRepository, ICreditLedger, TransactionType, } from "@wopr-network/platform-core/credits";
2
- export { ALLOWED_SCHEDULE_INTERVALS, ALLOWED_THRESHOLDS, ALLOWED_TOPUP_AMOUNTS, CreditLedger, computeNextScheduleAt, DrizzleAutoTopupSettingsRepository, DrizzleCreditLedger, grantSignupCredits, InsufficientBalanceError, runCreditExpiryCron, SIGNUP_GRANT, } from "@wopr-network/platform-core/credits";
1
+ export type { AutoTopupSettings, CreditExpiryCronConfig, CreditExpiryCronResult, CreditType, DebitType, HistoryOptions, IAutoTopupSettingsRepository, ILedger, JournalEntry, TransactionType, } from "@wopr-network/platform-core/credits";
2
+ export { ALLOWED_SCHEDULE_INTERVALS, ALLOWED_THRESHOLDS, ALLOWED_TOPUP_AMOUNTS, computeNextScheduleAt, DrizzleAutoTopupSettingsRepository, DrizzleLedger, grantSignupCredits, InsufficientBalanceError, Ledger, runCreditExpiryCron, SIGNUP_GRANT, } from "@wopr-network/platform-core/credits";
3
3
  export type { BillingState, IBotBilling } from "./bot-billing.js";
4
4
  export { BotBilling, DrizzleBotBilling, SUSPENSION_GRACE_DAYS } from "./bot-billing.js";
5
5
  export type { DividendDigestConfig, DividendDigestResult } from "./dividend-digest-cron.js";
@@ -1,4 +1,4 @@
1
- export { ALLOWED_SCHEDULE_INTERVALS, ALLOWED_THRESHOLDS, ALLOWED_TOPUP_AMOUNTS, CreditLedger, computeNextScheduleAt, DrizzleAutoTopupSettingsRepository, DrizzleCreditLedger, grantSignupCredits, InsufficientBalanceError, runCreditExpiryCron, SIGNUP_GRANT, } from "@wopr-network/platform-core/credits";
1
+ export { ALLOWED_SCHEDULE_INTERVALS, ALLOWED_THRESHOLDS, ALLOWED_TOPUP_AMOUNTS, computeNextScheduleAt, DrizzleAutoTopupSettingsRepository, DrizzleLedger, grantSignupCredits, InsufficientBalanceError, Ledger, runCreditExpiryCron, SIGNUP_GRANT, } from "@wopr-network/platform-core/credits";
2
2
  export { BotBilling, DrizzleBotBilling, SUSPENSION_GRACE_DAYS } from "./bot-billing.js";
3
3
  export { runDividendDigestCron } from "./dividend-digest-cron.js";
4
4
  export { buildResourceTierCosts, DAILY_BOT_COST, runRuntimeDeductions } from "./runtime-cron.js";
@@ -1,7 +1,7 @@
1
- import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
1
+ import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
2
2
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
3
3
  import { createTestDb, truncateAllTables } from "../../test/db.js";
4
- describe("DrizzleCreditLedger.memberUsage", () => {
4
+ describe("DrizzleDrizzleLedger.memberUsage", () => {
5
5
  let pool;
6
6
  let db;
7
7
  let ledger;
@@ -13,13 +13,23 @@ describe("DrizzleCreditLedger.memberUsage", () => {
13
13
  });
14
14
  beforeEach(async () => {
15
15
  await truncateAllTables(pool);
16
- ledger = new CreditLedger(db);
16
+ ledger = new DrizzleLedger(db);
17
+ await ledger.seedSystemAccounts();
17
18
  });
18
19
  it("should aggregate debit totals per attributed user", async () => {
19
- await ledger.credit("org-1", Credit.fromCents(10000), "purchase", "Seed");
20
- await ledger.debit("org-1", Credit.fromCents(100), "adapter_usage", "Chat", undefined, false, "user-a");
21
- await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage", "Chat", undefined, false, "user-a");
22
- await ledger.debit("org-1", Credit.fromCents(300), "adapter_usage", "Chat", undefined, false, "user-b");
20
+ await ledger.credit("org-1", Credit.fromCents(10000), "purchase", { description: "Seed" });
21
+ await ledger.debit("org-1", Credit.fromCents(100), "adapter_usage", {
22
+ description: "Chat",
23
+ attributedUserId: "user-a",
24
+ });
25
+ await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage", {
26
+ description: "Chat",
27
+ attributedUserId: "user-a",
28
+ });
29
+ await ledger.debit("org-1", Credit.fromCents(300), "adapter_usage", {
30
+ description: "Chat",
31
+ attributedUserId: "user-b",
32
+ });
23
33
  const result = await ledger.memberUsage("org-1");
24
34
  expect(result).toHaveLength(2);
25
35
  const userA = result.find((r) => r.userId === "user-a");
@@ -30,9 +40,12 @@ describe("DrizzleCreditLedger.memberUsage", () => {
30
40
  expect(userB?.transactionCount).toBe(1);
31
41
  });
32
42
  it("should exclude transactions with null attributedUserId", async () => {
33
- await ledger.credit("org-1", Credit.fromCents(10000), "purchase", "Seed");
34
- await ledger.debit("org-1", Credit.fromCents(100), "bot_runtime", "Cron"); // no attributedUserId
35
- await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage", "Chat", undefined, false, "user-a");
43
+ await ledger.credit("org-1", Credit.fromCents(10000), "purchase", { description: "Seed" });
44
+ await ledger.debit("org-1", Credit.fromCents(100), "bot_runtime", { description: "Cron" }); // no attributedUserId
45
+ await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage", {
46
+ description: "Chat",
47
+ attributedUserId: "user-a",
48
+ });
36
49
  const result = await ledger.memberUsage("org-1");
37
50
  expect(result).toHaveLength(1);
38
51
  expect(result[0]?.userId).toBe("user-a");
@@ -1,10 +1,10 @@
1
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
1
+ import type { ILedger } from "@wopr-network/platform-core/credits";
2
2
  import type { IMeterEmitter } from "@wopr-network/platform-core/metering";
3
3
  import type { IPhoneNumberRepository } from "./drizzle-phone-number-repository.js";
4
4
  export type { IPhoneNumberRepository } from "./drizzle-phone-number-repository.js";
5
5
  /** Phone number monthly wholesale cost in USD. Exported for proxy.ts to import. */
6
6
  export declare const PHONE_NUMBER_MONTHLY_COST = 1.15;
7
- export declare function runMonthlyPhoneBilling(phoneRepo: IPhoneNumberRepository, ledger: ICreditLedger, meter: IMeterEmitter): Promise<{
7
+ export declare function runMonthlyPhoneBilling(phoneRepo: IPhoneNumberRepository, ledger: ILedger, meter: IMeterEmitter): Promise<{
8
8
  processed: number;
9
9
  billed: {
10
10
  tenantId: string;
@@ -23,7 +23,11 @@ export async function runMonthlyPhoneBilling(phoneRepo, ledger, meter) {
23
23
  try {
24
24
  const costCredit = Credit.fromDollars(PHONE_NUMBER_MONTHLY_COST);
25
25
  const chargeCredit = withMargin(costCredit, PHONE_NUMBER_MARGIN);
26
- await ledger.debit(number.tenantId, chargeCredit, "addon", "Monthly phone number fee", `phone-billing:${number.sid}:${now.toISOString().slice(0, 7)}`, true);
26
+ await ledger.debit(number.tenantId, chargeCredit, "addon", {
27
+ description: "Monthly phone number fee",
28
+ referenceId: `phone-billing:${number.sid}:${now.toISOString().slice(0, 7)}`,
29
+ allowNegative: true,
30
+ });
27
31
  meter.emit({
28
32
  tenant: number.tenantId,
29
33
  cost: costCredit,
@@ -1,19 +1,16 @@
1
- import { Credit, InsufficientBalanceError, } from "@wopr-network/platform-core/credits";
1
+ import { Credit, InsufficientBalanceError } from "@wopr-network/platform-core/credits";
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { PHONE_NUMBER_MONTHLY_COST, runMonthlyPhoneBilling } from "./phone-billing.js";
4
4
  function makeTx(tenantId) {
5
5
  return {
6
6
  id: "tx-1",
7
+ postedAt: new Date().toISOString(),
8
+ entryType: "addon",
7
9
  tenantId,
8
- amount: Credit.fromDollars(1),
9
- balanceAfter: Credit.fromDollars(100),
10
- type: "addon",
11
10
  description: "Monthly phone number fee",
12
11
  referenceId: null,
13
- fundingSource: null,
14
- attributedUserId: null,
15
- createdAt: new Date().toISOString(),
16
- expiresAt: null,
12
+ metadata: null,
13
+ lines: [],
17
14
  };
18
15
  }
19
16
  function makeNumber(overrides = {}) {
@@ -85,16 +82,16 @@ describe("runMonthlyPhoneBilling", () => {
85
82
  expect(result.failed).toEqual([]);
86
83
  // Verify debit was called with the margined charge amount
87
84
  expect(ledger.debit).toHaveBeenCalledOnce();
88
- const [tenantId, chargeAmount, type, description, referenceId, allowNegative] = ledger.debit.mock.calls[0];
85
+ const [tenantId, chargeAmount, type, opts] = ledger.debit.mock.calls[0];
89
86
  expect(tenantId).toBe("tenant-1");
90
87
  // chargeCredit = Credit.fromDollars(1.15).multiply(2.6)
91
88
  const expectedCharge = Credit.fromDollars(1.15).multiply(2.6);
92
89
  expect(chargeAmount.toRaw()).toBe(expectedCharge.toRaw());
93
90
  expect(type).toBe("addon");
94
- expect(description).toBe("Monthly phone number fee");
91
+ expect(opts.description).toBe("Monthly phone number fee");
95
92
  const expectedMonth = `${NOW.getFullYear()}-${String(NOW.getMonth() + 1).padStart(2, "0")}`;
96
- expect(referenceId).toMatch(new RegExp(`^phone-billing:PN-abc123:${expectedMonth}$`));
97
- expect(allowNegative).toBe(true);
93
+ expect(opts.referenceId).toMatch(new RegExp(`^phone-billing:PN-abc123:${expectedMonth}$`));
94
+ expect(opts.allowNegative).toBe(true);
98
95
  // Verify meter emission
99
96
  expect(meter.emit).toHaveBeenCalledOnce();
100
97
  const event = meter.emit.mock.calls[0][0];
@@ -1,4 +1,4 @@
1
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
1
+ import type { ILedger } from "@wopr-network/platform-core/credits";
2
2
  import { Credit } from "@wopr-network/platform-core/credits";
3
3
  import type { IBotInstanceRepository } from "../../fleet/bot-instance-repository.js";
4
4
  /**
@@ -13,7 +13,7 @@ export type GetActiveBotCount = (tenantId: string) => number | Promise<number>;
13
13
  /** Low balance threshold ($1.00 = 20% of signup grant). */
14
14
  export declare const LOW_BALANCE_THRESHOLD: Credit;
15
15
  export interface RuntimeCronConfig {
16
- ledger: ICreditLedger;
16
+ ledger: ILedger;
17
17
  getActiveBotCount: GetActiveBotCount;
18
18
  /** The date being billed, as YYYY-MM-DD. Used for idempotency. */
19
19
  date: string;
@@ -69,17 +69,26 @@ export async function runRuntimeDeductions(cfg) {
69
69
  const totalCost = DAILY_BOT_COST.multiply(botCount);
70
70
  if (!balance.lessThan(totalCost)) {
71
71
  // Full deduction
72
- await cfg.ledger.debit(tenantId, totalCost, "bot_runtime", `Daily runtime: ${botCount} bot(s) x $${DAILY_BOT_COST.toDollars().toFixed(2)}`, runtimeRef);
72
+ await cfg.ledger.debit(tenantId, totalCost, "bot_runtime", {
73
+ description: `Daily runtime: ${botCount} bot(s) x $${DAILY_BOT_COST.toDollars().toFixed(2)}`,
74
+ referenceId: runtimeRef,
75
+ });
73
76
  // Debit resource tier surcharges (if any)
74
77
  if (cfg.getResourceTierCosts) {
75
78
  const tierCost = await cfg.getResourceTierCosts(tenantId);
76
79
  if (!tierCost.isZero()) {
77
80
  const balanceAfterRuntime = await cfg.ledger.balance(tenantId);
78
81
  if (!balanceAfterRuntime.lessThan(tierCost)) {
79
- await cfg.ledger.debit(tenantId, tierCost, "resource_upgrade", "Daily resource tier surcharge", `runtime-tier:${cfg.date}:${tenantId}`);
82
+ await cfg.ledger.debit(tenantId, tierCost, "resource_upgrade", {
83
+ description: "Daily resource tier surcharge",
84
+ referenceId: `runtime-tier:${cfg.date}:${tenantId}`,
85
+ });
80
86
  }
81
87
  else if (balanceAfterRuntime.greaterThan(Credit.ZERO)) {
82
- await cfg.ledger.debit(tenantId, balanceAfterRuntime, "resource_upgrade", "Partial resource tier surcharge (balance exhausted)", `runtime-tier:${cfg.date}:${tenantId}`);
88
+ await cfg.ledger.debit(tenantId, balanceAfterRuntime, "resource_upgrade", {
89
+ description: "Partial resource tier surcharge (balance exhausted)",
90
+ referenceId: `runtime-tier:${cfg.date}:${tenantId}`,
91
+ });
83
92
  }
84
93
  }
85
94
  }
@@ -110,12 +119,18 @@ export async function runRuntimeDeductions(cfg) {
110
119
  if (!storageCost.isZero()) {
111
120
  const currentBalance = await cfg.ledger.balance(tenantId);
112
121
  if (!currentBalance.lessThan(storageCost)) {
113
- await cfg.ledger.debit(tenantId, storageCost, "storage_upgrade", "Daily storage tier surcharge", `runtime-storage:${cfg.date}:${tenantId}`);
122
+ await cfg.ledger.debit(tenantId, storageCost, "storage_upgrade", {
123
+ description: "Daily storage tier surcharge",
124
+ referenceId: `runtime-storage:${cfg.date}:${tenantId}`,
125
+ });
114
126
  }
115
127
  else {
116
128
  // Partial debit — take what's left, then suspend
117
129
  if (currentBalance.greaterThan(Credit.ZERO)) {
118
- await cfg.ledger.debit(tenantId, currentBalance, "storage_upgrade", "Partial storage tier surcharge (balance exhausted)", `runtime-storage:${cfg.date}:${tenantId}`);
130
+ await cfg.ledger.debit(tenantId, currentBalance, "storage_upgrade", {
131
+ description: "Partial storage tier surcharge (balance exhausted)",
132
+ referenceId: `runtime-storage:${cfg.date}:${tenantId}`,
133
+ });
119
134
  }
120
135
  if (!result.suspended.includes(tenantId)) {
121
136
  result.suspended.push(tenantId);
@@ -131,12 +146,18 @@ export async function runRuntimeDeductions(cfg) {
131
146
  if (!addonCost.isZero()) {
132
147
  const currentBalance = await cfg.ledger.balance(tenantId);
133
148
  if (!currentBalance.lessThan(addonCost)) {
134
- await cfg.ledger.debit(tenantId, addonCost, "addon", "Daily infrastructure add-on charges", `runtime-addon:${cfg.date}:${tenantId}`);
149
+ await cfg.ledger.debit(tenantId, addonCost, "addon", {
150
+ description: "Daily infrastructure add-on charges",
151
+ referenceId: `runtime-addon:${cfg.date}:${tenantId}`,
152
+ });
135
153
  }
136
154
  else {
137
155
  // Partial debit — take what's left, then suspend
138
156
  if (currentBalance.greaterThan(Credit.ZERO)) {
139
- await cfg.ledger.debit(tenantId, currentBalance, "addon", "Partial add-on charges (balance exhausted)", `runtime-addon:${cfg.date}:${tenantId}`);
157
+ await cfg.ledger.debit(tenantId, currentBalance, "addon", {
158
+ description: "Partial add-on charges (balance exhausted)",
159
+ referenceId: `runtime-addon:${cfg.date}:${tenantId}`,
160
+ });
140
161
  }
141
162
  if (!result.suspended.includes(tenantId)) {
142
163
  result.suspended.push(tenantId);
@@ -150,7 +171,10 @@ export async function runRuntimeDeductions(cfg) {
150
171
  else {
151
172
  // Partial deduction — debit remaining balance, then suspend
152
173
  if (balance.greaterThan(Credit.ZERO)) {
153
- await cfg.ledger.debit(tenantId, balance, "bot_runtime", `Partial daily runtime (balance exhausted): ${botCount} bot(s)`, runtimeRef);
174
+ await cfg.ledger.debit(tenantId, balance, "bot_runtime", {
175
+ description: `Partial daily runtime (balance exhausted): ${botCount} bot(s)`,
176
+ referenceId: runtimeRef,
177
+ });
154
178
  }
155
179
  if (cfg.onCreditsExhausted) {
156
180
  await cfg.onCreditsExhausted(tenantId);