@wopr-network/platform-core 1.13.3 → 1.14.1

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 (220) hide show
  1. package/.github/workflows/dependabot-auto-merge.yml +1 -2
  2. package/dist/api/routes/admin-credits.d.ts +2 -2
  3. package/dist/api/routes/admin-credits.js +9 -4
  4. package/dist/api/routes/quota.d.ts +2 -2
  5. package/dist/api/routes/verify-email.d.ts +3 -3
  6. package/dist/backup/on-demand-snapshot-service.d.ts +2 -2
  7. package/dist/billing/payram/webhook.d.ts +3 -3
  8. package/dist/billing/payram/webhook.js +5 -1
  9. package/dist/billing/payram/webhook.test.js +5 -4
  10. package/dist/billing/stripe/stripe-payment-processor.d.ts +2 -2
  11. package/dist/billing/stripe/stripe-payment-processor.test.js +7 -0
  12. package/dist/billing/stripe/tenant-store.d.ts +1 -1
  13. package/dist/billing/stripe/tenant-store.js +1 -1
  14. package/dist/credits/auto-topup-charge.d.ts +2 -2
  15. package/dist/credits/auto-topup-charge.js +5 -1
  16. package/dist/credits/auto-topup-charge.test.js +5 -4
  17. package/dist/credits/auto-topup-usage.d.ts +2 -2
  18. package/dist/credits/auto-topup-usage.test.js +53 -12
  19. package/dist/credits/credit-expiry-cron.d.ts +2 -2
  20. package/dist/credits/credit-expiry-cron.js +7 -4
  21. package/dist/credits/credit-expiry-cron.test.js +25 -8
  22. package/dist/credits/credit-ledger.d.ts +2 -2
  23. package/dist/credits/credit-ledger.js +1 -1
  24. package/dist/credits/dividend-cron.d.ts +4 -6
  25. package/dist/credits/dividend-cron.js +10 -16
  26. package/dist/credits/dividend-cron.test.js +31 -44
  27. package/dist/credits/dividend-repository.js +19 -22
  28. package/dist/credits/dividend-repository.test.js +4 -3
  29. package/dist/credits/index.d.ts +4 -2
  30. package/dist/credits/index.js +2 -1
  31. package/dist/credits/ledger.d.ts +195 -0
  32. package/dist/credits/ledger.js +561 -0
  33. package/dist/credits/ledger.test.js +418 -0
  34. package/dist/credits/signup-grant.d.ts +2 -2
  35. package/dist/credits/signup-grant.js +4 -4
  36. package/dist/credits/signup-grant.test.js +5 -3
  37. package/dist/credits/trial-balance-cron.d.ts +19 -0
  38. package/dist/credits/trial-balance-cron.js +30 -0
  39. package/dist/credits/trial-balance-cron.test.js +55 -0
  40. package/dist/db/schema/index.d.ts +1 -0
  41. package/dist/db/schema/index.js +1 -0
  42. package/dist/db/schema/ledger.d.ts +442 -0
  43. package/dist/db/schema/ledger.js +76 -0
  44. package/dist/gateway/credit-gate.d.ts +2 -2
  45. package/dist/gateway/credit-gate.js +5 -1
  46. package/dist/gateway/credit-gate.test.js +35 -33
  47. package/dist/gateway/protocol/deps.d.ts +2 -2
  48. package/dist/gateway/protocol/handlers.test.js +461 -0
  49. package/dist/gateway/proxy.d.ts +2 -2
  50. package/dist/gateway/types.d.ts +2 -2
  51. package/dist/metering/reconciliation-cron.test.js +9 -8
  52. package/dist/metering/reconciliation-repository.js +12 -10
  53. package/dist/metering/reconciliation-repository.test.js +9 -8
  54. package/dist/monetization/affiliate/affiliate-admin-repository.js +10 -8
  55. package/dist/monetization/affiliate/affiliate-admin-repository.test.js +32 -13
  56. package/dist/monetization/affiliate/credit-match.d.ts +2 -2
  57. package/dist/monetization/affiliate/credit-match.js +4 -1
  58. package/dist/monetization/affiliate/credit-match.test.js +58 -13
  59. package/dist/monetization/affiliate/new-user-bonus.d.ts +2 -2
  60. package/dist/monetization/affiliate/new-user-bonus.js +4 -1
  61. package/dist/monetization/affiliate/new-user-bonus.test.js +4 -3
  62. package/dist/monetization/credits/auto-topup-charge.d.ts +2 -2
  63. package/dist/monetization/credits/auto-topup-charge.js +5 -1
  64. package/dist/monetization/credits/auto-topup-charge.test.js +5 -4
  65. package/dist/monetization/credits/auto-topup-usage.d.ts +2 -2
  66. package/dist/monetization/credits/auto-topup-usage.test.js +53 -12
  67. package/dist/monetization/credits/bot-billing.d.ts +3 -3
  68. package/dist/monetization/credits/bot-billing.test.js +18 -5
  69. package/dist/monetization/credits/credit-expiry-cron.test.js +25 -8
  70. package/dist/monetization/credits/dividend-cron.d.ts +2 -4
  71. package/dist/monetization/credits/dividend-cron.js +7 -4
  72. package/dist/monetization/credits/dividend-cron.test.js +26 -46
  73. package/dist/monetization/credits/dividend-repository.js +15 -24
  74. package/dist/monetization/credits/dividend-repository.test.js +4 -3
  75. package/dist/monetization/credits/index.d.ts +2 -2
  76. package/dist/monetization/credits/index.js +1 -1
  77. package/dist/monetization/credits/member-usage.test.js +23 -10
  78. package/dist/monetization/credits/phone-billing.d.ts +2 -2
  79. package/dist/monetization/credits/phone-billing.js +5 -1
  80. package/dist/monetization/credits/phone-billing.test.js +9 -12
  81. package/dist/monetization/credits/runtime-cron.d.ts +2 -2
  82. package/dist/monetization/credits/runtime-cron.js +32 -8
  83. package/dist/monetization/credits/runtime-cron.test.js +28 -27
  84. package/dist/monetization/credits/runtime-scheduler.d.ts +2 -2
  85. package/dist/monetization/credits/runtime-scheduler.test.js +1 -1
  86. package/dist/monetization/credits/signup-grant.test.js +5 -3
  87. package/dist/monetization/credits/storage-tier-cron.test.js +3 -2
  88. package/dist/monetization/credits/trial-balance-cron.test.js +42 -0
  89. package/dist/monetization/feature-gate.d.ts +3 -3
  90. package/dist/monetization/index.d.ts +3 -3
  91. package/dist/monetization/index.js +1 -1
  92. package/dist/monetization/metering/reconciliation-cron.test.js +9 -8
  93. package/dist/monetization/metering/reconciliation-repository.js +11 -10
  94. package/dist/monetization/metering/reconciliation-repository.test.js +9 -8
  95. package/dist/monetization/payram/webhook.d.ts +2 -2
  96. package/dist/monetization/payram/webhook.js +5 -1
  97. package/dist/monetization/payram/webhook.test.js +5 -4
  98. package/dist/monetization/promotions/engine.d.ts +2 -2
  99. package/dist/monetization/promotions/engine.js +4 -1
  100. package/dist/monetization/promotions/engine.test.js +3 -1
  101. package/dist/monetization/repository-types.d.ts +1 -1
  102. package/dist/monetization/stripe/stripe-payment-processor.d.ts +2 -2
  103. package/dist/monetization/stripe/stripe-payment-processor.test.js +7 -0
  104. package/dist/monetization/stripe/webhook.d.ts +2 -2
  105. package/dist/monetization/stripe/webhook.js +70 -6
  106. package/dist/monetization/stripe/webhook.test.js +20 -15
  107. package/dist/onboarding/onboarding-service.d.ts +2 -2
  108. package/dist/onboarding/onboarding-service.js +6 -2
  109. package/drizzle/migrations/0003_double_entry_ledger.sql +82 -0
  110. package/drizzle/migrations/meta/_journal.json +7 -0
  111. package/package.json +1 -1
  112. package/src/api/routes/admin-credits.ts +11 -14
  113. package/src/api/routes/quota.ts +2 -2
  114. package/src/api/routes/verify-email.ts +4 -4
  115. package/src/backup/on-demand-snapshot-service.test.ts +3 -3
  116. package/src/backup/on-demand-snapshot-service.ts +3 -3
  117. package/src/billing/payram/webhook.test.ts +7 -5
  118. package/src/billing/payram/webhook.ts +8 -11
  119. package/src/billing/stripe/stripe-payment-processor.test.ts +10 -3
  120. package/src/billing/stripe/stripe-payment-processor.ts +3 -3
  121. package/src/billing/stripe/tenant-store.ts +1 -1
  122. package/src/credits/auto-topup-charge.test.ts +7 -5
  123. package/src/credits/auto-topup-charge.ts +7 -10
  124. package/src/credits/auto-topup-usage.test.ts +55 -13
  125. package/src/credits/auto-topup-usage.ts +2 -2
  126. package/src/credits/credit-expiry-cron.test.ts +26 -45
  127. package/src/credits/credit-expiry-cron.ts +9 -12
  128. package/src/credits/credit-ledger.ts +3 -3
  129. package/src/credits/dividend-cron.test.ts +38 -45
  130. package/src/credits/dividend-cron.ts +12 -26
  131. package/src/credits/dividend-repository.test.ts +4 -3
  132. package/src/credits/dividend-repository.ts +21 -23
  133. package/src/credits/index.ts +23 -4
  134. package/src/credits/ledger.test.ts +514 -0
  135. package/src/credits/ledger.ts +851 -0
  136. package/src/credits/signup-grant.test.ts +7 -4
  137. package/src/credits/signup-grant.ts +6 -12
  138. package/src/credits/trial-balance-cron.test.ts +68 -0
  139. package/src/credits/trial-balance-cron.ts +46 -0
  140. package/src/db/schema/index.ts +1 -0
  141. package/src/db/schema/ledger.ts +94 -0
  142. package/src/gateway/credit-gate-wiring.test.ts +3 -3
  143. package/src/gateway/credit-gate.test.ts +35 -33
  144. package/src/gateway/credit-gate.ts +6 -10
  145. package/src/gateway/gateway-routes.test.ts +5 -5
  146. package/src/gateway/protocol/deps.ts +2 -2
  147. package/src/gateway/protocol/handlers.test.ts +549 -1
  148. package/src/gateway/proxy.ts +2 -2
  149. package/src/gateway/route-mounting.test.ts +2 -2
  150. package/src/gateway/types.ts +2 -2
  151. package/src/metering/reconciliation-cron.test.ts +10 -9
  152. package/src/metering/reconciliation-repository.test.ts +10 -9
  153. package/src/metering/reconciliation-repository.ts +14 -11
  154. package/src/monetization/affiliate/affiliate-admin-repository.test.ts +32 -19
  155. package/src/monetization/affiliate/affiliate-admin-repository.ts +16 -8
  156. package/src/monetization/affiliate/credit-match.test.ts +60 -14
  157. package/src/monetization/affiliate/credit-match.ts +6 -9
  158. package/src/monetization/affiliate/new-user-bonus.test.ts +6 -4
  159. package/src/monetization/affiliate/new-user-bonus.ts +6 -9
  160. package/src/monetization/credits/auto-topup-charge.test.ts +7 -5
  161. package/src/monetization/credits/auto-topup-charge.ts +7 -10
  162. package/src/monetization/credits/auto-topup-usage.test.ts +55 -13
  163. package/src/monetization/credits/auto-topup-usage.ts +2 -2
  164. package/src/monetization/credits/bot-billing.test.ts +20 -6
  165. package/src/monetization/credits/bot-billing.ts +3 -3
  166. package/src/monetization/credits/credit-expiry-cron.test.ts +26 -45
  167. package/src/monetization/credits/dividend-cron.test.ts +34 -48
  168. package/src/monetization/credits/dividend-cron.ts +9 -14
  169. package/src/monetization/credits/dividend-repository.test.ts +4 -3
  170. package/src/monetization/credits/dividend-repository.ts +19 -25
  171. package/src/monetization/credits/index.ts +4 -4
  172. package/src/monetization/credits/member-usage.test.ts +25 -11
  173. package/src/monetization/credits/phone-billing.test.ts +18 -26
  174. package/src/monetization/credits/phone-billing.ts +7 -10
  175. package/src/monetization/credits/runtime-cron.test.ts +29 -28
  176. package/src/monetization/credits/runtime-cron.ts +34 -58
  177. package/src/monetization/credits/runtime-scheduler.test.ts +1 -1
  178. package/src/monetization/credits/runtime-scheduler.ts +2 -2
  179. package/src/monetization/credits/signup-grant.test.ts +7 -4
  180. package/src/monetization/credits/storage-tier-cron.test.ts +5 -3
  181. package/src/monetization/credits/trial-balance-cron.test.ts +52 -0
  182. package/src/monetization/feature-gate.ts +3 -3
  183. package/src/monetization/index.ts +4 -4
  184. package/src/monetization/metering/reconciliation-cron.test.ts +10 -9
  185. package/src/monetization/metering/reconciliation-repository.test.ts +11 -9
  186. package/src/monetization/metering/reconciliation-repository.ts +13 -11
  187. package/src/monetization/payram/webhook.test.ts +7 -5
  188. package/src/monetization/payram/webhook.ts +7 -10
  189. package/src/monetization/promotions/engine.test.ts +6 -5
  190. package/src/monetization/promotions/engine.ts +6 -3
  191. package/src/monetization/repository-types.ts +1 -1
  192. package/src/monetization/stripe/stripe-payment-processor.test.ts +10 -3
  193. package/src/monetization/stripe/stripe-payment-processor.ts +3 -3
  194. package/src/monetization/stripe/webhook.test.ts +22 -16
  195. package/src/monetization/stripe/webhook.ts +75 -50
  196. package/src/onboarding/onboarding-service.ts +8 -11
  197. package/dist/credits/credit-ledger-extra.test.js +0 -40
  198. package/dist/credits/credit-ledger.bench.js +0 -33
  199. package/dist/credits/credit-ledger.test.d.ts +0 -4
  200. package/dist/credits/credit-ledger.test.js +0 -203
  201. package/dist/credits/credit-transaction-repository.test.js +0 -232
  202. package/dist/monetization/credits/credit-ledger-extra.test.d.ts +0 -1
  203. package/dist/monetization/credits/credit-ledger-extra.test.js +0 -39
  204. package/dist/monetization/credits/credit-ledger.bench.d.ts +0 -1
  205. package/dist/monetization/credits/credit-ledger.bench.js +0 -32
  206. package/dist/monetization/credits/credit-ledger.test.d.ts +0 -4
  207. package/dist/monetization/credits/credit-ledger.test.js +0 -202
  208. package/dist/monetization/credits/credit-transaction-repository.test.d.ts +0 -1
  209. package/dist/monetization/credits/credit-transaction-repository.test.js +0 -232
  210. package/src/credits/credit-ledger-extra.test.ts +0 -57
  211. package/src/credits/credit-ledger.bench.ts +0 -56
  212. package/src/credits/credit-ledger.test.ts +0 -276
  213. package/src/credits/credit-transaction-repository.test.ts +0 -274
  214. package/src/monetization/credits/credit-ledger-extra.test.ts +0 -56
  215. package/src/monetization/credits/credit-ledger.bench.ts +0 -55
  216. package/src/monetization/credits/credit-ledger.test.ts +0 -275
  217. package/src/monetization/credits/credit-transaction-repository.test.ts +0 -274
  218. /package/dist/credits/{credit-ledger-extra.test.d.ts → ledger.test.d.ts} +0 -0
  219. /package/dist/credits/{credit-ledger.bench.d.ts → trial-balance-cron.test.d.ts} +0 -0
  220. /package/dist/{credits/credit-transaction-repository.test.d.ts → monetization/credits/trial-balance-cron.test.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import { Credit, CreditLedger, DrizzleAutoTopupSettingsRepository } from "@wopr-network/platform-core/credits";
1
+ import { Credit, DrizzleAutoTopupSettingsRepository, DrizzleLedger } from "@wopr-network/platform-core/credits";
2
2
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { createTestDb, truncateAllTables } from "../../test/db.js";
4
4
  import { maybeTriggerUsageTopup } from "./auto-topup-usage.js";
@@ -15,7 +15,8 @@ describe("maybeTriggerUsageTopup", () => {
15
15
  });
16
16
  beforeEach(async () => {
17
17
  await truncateAllTables(pool);
18
- ledger = new CreditLedger(db);
18
+ ledger = new DrizzleLedger(db);
19
+ await ledger.seedSystemAccounts();
19
20
  settingsRepo = new DrizzleAutoTopupSettingsRepository(db);
20
21
  });
21
22
  it("does nothing when tenant has no auto-topup settings", async () => {
@@ -26,7 +27,11 @@ describe("maybeTriggerUsageTopup", () => {
26
27
  });
27
28
  it("does nothing when usage_enabled is false", async () => {
28
29
  await settingsRepo.upsert("t1", { usageEnabled: false });
29
- await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
30
+ await ledger.credit("t1", Credit.fromCents(50), "purchase", {
31
+ description: "buy",
32
+ referenceId: "ref-1",
33
+ fundingSource: "stripe",
34
+ });
30
35
  const mockCharge = vi.fn();
31
36
  const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
32
37
  await maybeTriggerUsageTopup(deps, "t1");
@@ -38,7 +43,11 @@ describe("maybeTriggerUsageTopup", () => {
38
43
  usageThreshold: Credit.fromCents(100),
39
44
  usageTopup: Credit.fromCents(500),
40
45
  });
41
- await ledger.credit("t1", Credit.fromCents(200), "purchase", "buy", "ref-1", "stripe");
46
+ await ledger.credit("t1", Credit.fromCents(200), "purchase", {
47
+ description: "buy",
48
+ referenceId: "ref-1",
49
+ fundingSource: "stripe",
50
+ });
42
51
  const mockCharge = vi.fn();
43
52
  const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
44
53
  await maybeTriggerUsageTopup(deps, "t1");
@@ -50,7 +59,11 @@ describe("maybeTriggerUsageTopup", () => {
50
59
  usageThreshold: Credit.fromCents(100),
51
60
  usageTopup: Credit.fromCents(500),
52
61
  });
53
- await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
62
+ await ledger.credit("t1", Credit.fromCents(50), "purchase", {
63
+ description: "buy",
64
+ referenceId: "ref-1",
65
+ fundingSource: "stripe",
66
+ });
54
67
  const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_123" });
55
68
  const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
56
69
  await maybeTriggerUsageTopup(deps, "t1");
@@ -59,7 +72,11 @@ describe("maybeTriggerUsageTopup", () => {
59
72
  it("skips when charge is already in-flight", async () => {
60
73
  await settingsRepo.upsert("t1", { usageEnabled: true, usageThreshold: Credit.fromCents(100) });
61
74
  await settingsRepo.setUsageChargeInFlight("t1", true);
62
- await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
75
+ await ledger.credit("t1", Credit.fromCents(50), "purchase", {
76
+ description: "buy",
77
+ referenceId: "ref-1",
78
+ fundingSource: "stripe",
79
+ });
63
80
  const mockCharge = vi.fn();
64
81
  const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
65
82
  await maybeTriggerUsageTopup(deps, "t1");
@@ -72,7 +89,11 @@ describe("maybeTriggerUsageTopup", () => {
72
89
  usageThreshold: Credit.fromCents(500),
73
90
  usageTopup: Credit.fromCents(2000),
74
91
  });
75
- await ledger.credit("t1", Credit.fromCents(100), "purchase", "buy", "ref-1", "stripe");
92
+ await ledger.credit("t1", Credit.fromCents(100), "purchase", {
93
+ description: "buy",
94
+ referenceId: "ref-1",
95
+ fundingSource: "stripe",
96
+ });
76
97
  const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_race" });
77
98
  const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
78
99
  // Fire two concurrent calls — both see balance < threshold,
@@ -91,7 +112,11 @@ describe("maybeTriggerUsageTopup", () => {
91
112
  usageThreshold: Credit.fromCents(100),
92
113
  usageTopup: Credit.fromCents(500),
93
114
  });
94
- await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
115
+ await ledger.credit("t1", Credit.fromCents(50), "purchase", {
116
+ description: "buy",
117
+ referenceId: "ref-1",
118
+ fundingSource: "stripe",
119
+ });
95
120
  const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_123" });
96
121
  const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
97
122
  // First call — triggers charge, flag set then cleared
@@ -109,7 +134,11 @@ describe("maybeTriggerUsageTopup", () => {
109
134
  usageThreshold: Credit.fromCents(100),
110
135
  usageTopup: Credit.fromCents(500),
111
136
  });
112
- await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
137
+ await ledger.credit("t1", Credit.fromCents(50), "purchase", {
138
+ description: "buy",
139
+ referenceId: "ref-1",
140
+ fundingSource: "stripe",
141
+ });
113
142
  const mockCharge = vi
114
143
  .fn()
115
144
  .mockRejectedValueOnce(new Error("Stripe network error"))
@@ -132,7 +161,11 @@ describe("maybeTriggerUsageTopup", () => {
132
161
  });
133
162
  await settingsRepo.incrementUsageFailures("t1");
134
163
  await settingsRepo.incrementUsageFailures("t1");
135
- await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
164
+ await ledger.credit("t1", Credit.fromCents(50), "purchase", {
165
+ description: "buy",
166
+ referenceId: "ref-1",
167
+ fundingSource: "stripe",
168
+ });
136
169
  const mockCharge = vi.fn().mockResolvedValue({ success: true });
137
170
  const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
138
171
  await maybeTriggerUsageTopup(deps, "t1");
@@ -144,7 +177,11 @@ describe("maybeTriggerUsageTopup", () => {
144
177
  usageThreshold: Credit.fromCents(100),
145
178
  usageTopup: Credit.fromCents(500),
146
179
  });
147
- await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
180
+ await ledger.credit("t1", Credit.fromCents(50), "purchase", {
181
+ description: "buy",
182
+ referenceId: "ref-1",
183
+ fundingSource: "stripe",
184
+ });
148
185
  const mockCharge = vi.fn().mockResolvedValue({ success: false, error: "declined" });
149
186
  const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
150
187
  await maybeTriggerUsageTopup(deps, "t1");
@@ -170,7 +207,11 @@ describe("maybeTriggerUsageTopup", () => {
170
207
  });
171
208
  await settingsRepo.incrementUsageFailures("t1");
172
209
  await settingsRepo.incrementUsageFailures("t1");
173
- await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
210
+ await ledger.credit("t1", Credit.fromCents(50), "purchase", {
211
+ description: "buy",
212
+ referenceId: "ref-1",
213
+ fundingSource: "stripe",
214
+ });
174
215
  const mockCharge = vi.fn().mockResolvedValue({ success: false, error: "declined" });
175
216
  const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
176
217
  await maybeTriggerUsageTopup(deps, "t1");
@@ -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
  import type { INodeCommandBus } from "../../fleet/node-command-bus.js";
@@ -11,7 +11,7 @@ export interface IBotBilling {
11
11
  suspendBot(botId: string): Promise<void>;
12
12
  suspendAllForTenant(tenantId: string): Promise<string[]>;
13
13
  reactivateBot(botId: string): Promise<void>;
14
- checkReactivation(tenantId: string, ledger: ICreditLedger): Promise<string[]>;
14
+ checkReactivation(tenantId: string, ledger: ILedger): Promise<string[]>;
15
15
  destroyBot(botId: string): Promise<void>;
16
16
  destroyExpiredBots(): Promise<string[]>;
17
17
  getBotBilling(botId: string): Promise<unknown>;
@@ -56,7 +56,7 @@ export declare class DrizzleBotBilling implements IBotBilling {
56
56
  *
57
57
  * @returns IDs of reactivated bots.
58
58
  */
59
- checkReactivation(tenantId: string, ledger: ICreditLedger): Promise<string[]>;
59
+ checkReactivation(tenantId: string, ledger: ILedger): Promise<string[]>;
60
60
  /**
61
61
  * Mark a bot as destroyed.
62
62
  * Sets billingState='destroyed'. Actual Docker cleanup is handled by the caller.
@@ -1,4 +1,4 @@
1
- import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
1
+ import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
2
2
  import { sql } from "drizzle-orm";
3
3
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
4
4
  import { botInstances } from "../../db/schema/bot-instances.js";
@@ -61,7 +61,8 @@ describe("BotBilling", () => {
61
61
  beforeEach(async () => {
62
62
  await truncateAllTables(pool);
63
63
  billing = new BotBilling(new DrizzleBotInstanceRepository(db));
64
- ledger = new CreditLedger(db);
64
+ ledger = new DrizzleLedger(db);
65
+ await ledger.seedSystemAccounts();
65
66
  });
66
67
  describe("registerBot", () => {
67
68
  it("registers a bot in active billing state", async () => {
@@ -178,7 +179,11 @@ describe("BotBilling", () => {
178
179
  await billing.registerBot("bot-2", "tenant-1", "bot-b");
179
180
  await billing.suspendBot("bot-1");
180
181
  await billing.suspendBot("bot-2");
181
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "test credit", "ref-1", "stripe");
182
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", {
183
+ description: "test credit",
184
+ referenceId: "ref-1",
185
+ fundingSource: "stripe",
186
+ });
182
187
  const reactivated = await billing.checkReactivation("tenant-1", ledger);
183
188
  expect(reactivated.sort()).toEqual(["bot-1", "bot-2"]);
184
189
  expect(await billing.getActiveBotCount("tenant-1")).toBe(2);
@@ -193,12 +198,20 @@ describe("BotBilling", () => {
193
198
  it("does not reactivate destroyed bots", async () => {
194
199
  await billing.registerBot("bot-1", "tenant-1", "bot-a");
195
200
  await billing.destroyBot("bot-1");
196
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "test credit", "ref-1", "stripe");
201
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", {
202
+ description: "test credit",
203
+ referenceId: "ref-1",
204
+ fundingSource: "stripe",
205
+ });
197
206
  const reactivated = await billing.checkReactivation("tenant-1", ledger);
198
207
  expect(reactivated).toEqual([]);
199
208
  });
200
209
  it("returns empty array for tenant with no bots", async () => {
201
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "test credit", "ref-1", "stripe");
210
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", {
211
+ description: "test credit",
212
+ referenceId: "ref-1",
213
+ fundingSource: "stripe",
214
+ });
202
215
  const reactivated = await billing.checkReactivation("tenant-1", ledger);
203
216
  expect(reactivated).toEqual([]);
204
217
  });
@@ -1,4 +1,4 @@
1
- import { Credit, DrizzleCreditLedger, runCreditExpiryCron } from "@wopr-network/platform-core/credits";
1
+ import { Credit, DrizzleLedger, runCreditExpiryCron } from "@wopr-network/platform-core/credits";
2
2
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
3
3
  import { createTestDb, truncateAllTables } from "../../test/db.js";
4
4
  describe("runCreditExpiryCron", () => {
@@ -7,13 +7,14 @@ describe("runCreditExpiryCron", () => {
7
7
  beforeAll(async () => {
8
8
  const { db, pool: p } = await createTestDb();
9
9
  pool = p;
10
- ledger = new DrizzleCreditLedger(db);
10
+ ledger = new DrizzleLedger(db);
11
11
  });
12
12
  afterAll(async () => {
13
13
  await pool.close();
14
14
  });
15
15
  beforeEach(async () => {
16
16
  await truncateAllTables(pool);
17
+ await ledger.seedSystemAccounts();
17
18
  });
18
19
  // All tests pass an explicit `now` parameter — hardcoded dates are time-independent
19
20
  // because runCreditExpiryCron never reads the system clock.
@@ -24,7 +25,11 @@ describe("runCreditExpiryCron", () => {
24
25
  expect(result.errors).toEqual([]);
25
26
  });
26
27
  it("debits expired promotional credit grant", async () => {
27
- await ledger.credit("tenant-1", Credit.fromCents(500), "promo", "New user bonus", "promo:tenant-1", undefined, undefined, "2026-01-10T00:00:00Z");
28
+ await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
29
+ description: "New user bonus",
30
+ referenceId: "promo:tenant-1",
31
+ expiresAt: "2026-01-10T00:00:00Z",
32
+ });
28
33
  const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
29
34
  expect(result.processed).toBe(1);
30
35
  expect(result.expired).toContain("tenant-1");
@@ -32,29 +37,41 @@ describe("runCreditExpiryCron", () => {
32
37
  expect(balance.toCents()).toBe(0);
33
38
  });
34
39
  it("does not debit non-expired credits", async () => {
35
- await ledger.credit("tenant-1", Credit.fromCents(500), "promo", "Future bonus", "promo:tenant-1-future", undefined, undefined, "2026-02-01T00:00:00Z");
40
+ await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
41
+ description: "Future bonus",
42
+ referenceId: "promo:tenant-1-future",
43
+ expiresAt: "2026-02-01T00:00:00Z",
44
+ });
36
45
  const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
37
46
  expect(result.processed).toBe(0);
38
47
  const balance = await ledger.balance("tenant-1");
39
48
  expect(balance.toCents()).toBe(500);
40
49
  });
41
50
  it("does not debit credits without expires_at", async () => {
42
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "Top-up");
51
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "Top-up" });
43
52
  const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
44
53
  expect(result.processed).toBe(0);
45
54
  const balance = await ledger.balance("tenant-1");
46
55
  expect(balance.toCents()).toBe(500);
47
56
  });
48
57
  it("only debits up to available balance when partially consumed", async () => {
49
- await ledger.credit("tenant-1", Credit.fromCents(500), "promo", "Promo", "promo:partial", undefined, undefined, "2026-01-10T00:00:00Z");
50
- await ledger.debit("tenant-1", Credit.fromCents(300), "bot_runtime", "Runtime");
58
+ await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
59
+ description: "Promo",
60
+ referenceId: "promo:partial",
61
+ expiresAt: "2026-01-10T00:00:00Z",
62
+ });
63
+ await ledger.debit("tenant-1", Credit.fromCents(300), "bot_runtime", { description: "Runtime" });
51
64
  const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
52
65
  expect(result.processed).toBe(1);
53
66
  const balance = await ledger.balance("tenant-1");
54
67
  expect(balance.toCents()).toBe(0);
55
68
  });
56
69
  it("is idempotent -- does not double-debit on second run", async () => {
57
- await ledger.credit("tenant-1", Credit.fromCents(500), "promo", "Promo", "promo:idemp", undefined, undefined, "2026-01-10T00:00:00Z");
70
+ await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
71
+ description: "Promo",
72
+ referenceId: "promo:idemp",
73
+ expiresAt: "2026-01-10T00:00:00Z",
74
+ });
58
75
  await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
59
76
  const balanceAfterFirst = await ledger.balance("tenant-1");
60
77
  const result2 = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
@@ -1,9 +1,7 @@
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
- import type { ICreditTransactionRepository } from "./credit-transaction-repository.js";
4
3
  export interface DividendCronConfig {
5
- creditTransactionRepo: ICreditTransactionRepository;
6
- ledger: ICreditLedger;
4
+ ledger: ILedger;
7
5
  /** Fraction of daily purchases matched as dividend pool. Default 1.0 (100%). */
8
6
  matchRate: number;
9
7
  /** The date to compute dividend for, as YYYY-MM-DD string. Typically yesterday. */
@@ -21,7 +21,7 @@ export async function runDividendCron(cfg) {
21
21
  // Idempotency: check if any per-tenant dividend was already distributed for this date.
22
22
  // We look for any referenceId matching "dividend:YYYY-MM-DD:*".
23
23
  const sentinelPrefix = `dividend:${cfg.targetDate}:`;
24
- const alreadyRan = await cfg.creditTransactionRepo.existsByReferenceIdLike(`${sentinelPrefix}%`);
24
+ const alreadyRan = await cfg.ledger.existsByReferenceIdLike(`${sentinelPrefix}%`);
25
25
  if (alreadyRan) {
26
26
  result.skippedAlreadyRun = true;
27
27
  logger.info("Dividend cron already ran for this date", { targetDate: cfg.targetDate });
@@ -30,14 +30,14 @@ export async function runDividendCron(cfg) {
30
30
  // Step 1: Sum all purchase amounts for the target date.
31
31
  const dayStart = `${cfg.targetDate} 00:00:00`;
32
32
  const dayEnd = `${cfg.targetDate} 24:00:00`;
33
- const dailyPurchaseTotalCredit = await cfg.creditTransactionRepo.sumPurchasesForPeriod(dayStart, dayEnd);
33
+ const dailyPurchaseTotalCredit = await cfg.ledger.sumPurchasesForPeriod(dayStart, dayEnd);
34
34
  result.pool = dailyPurchaseTotalCredit.multiply(cfg.matchRate);
35
35
  // Step 2: Find all active tenants (purchased in last 7 days from target date).
36
36
  // The 7-day window is: [targetDate - 6 days 00:00:00, targetDate 24:00:00)
37
37
  // This gives a full 7-day range ending at the end of targetDate.
38
38
  const windowStart = subtractDays(cfg.targetDate, 6);
39
39
  const windowStartTs = `${windowStart} 00:00:00`;
40
- const activeTenantIds = await cfg.creditTransactionRepo.getActiveTenantIdsInWindow(windowStartTs, dayEnd);
40
+ const activeTenantIds = await cfg.ledger.getActiveTenantIdsInWindow(windowStartTs, dayEnd);
41
41
  result.activeCount = activeTenantIds.length;
42
42
  // Step 3: Compute per-user share.
43
43
  if (result.pool.isZero() || result.activeCount <= 0) {
@@ -61,7 +61,10 @@ export async function runDividendCron(cfg) {
61
61
  for (const tenantId of activeTenantIds) {
62
62
  const perUserRef = `dividend:${cfg.targetDate}:${tenantId}`;
63
63
  try {
64
- await cfg.ledger.credit(tenantId, result.perUser, "community_dividend", `Community dividend for ${cfg.targetDate}: pool ${result.pool.toCents()}c / ${result.activeCount} users`, perUserRef);
64
+ await cfg.ledger.credit(tenantId, result.perUser, "community_dividend", {
65
+ description: `Community dividend for ${cfg.targetDate}: pool ${result.pool.toCents()}c / ${result.activeCount} users`,
66
+ referenceId: perUserRef,
67
+ });
65
68
  result.distributed++;
66
69
  }
67
70
  catch (err) {
@@ -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";