@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
@@ -2,13 +2,13 @@ import type { PGlite } from "@electric-sql/pglite";
2
2
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import type { PlatformDb } from "../db/index.js";
4
4
  import { createTestDb, truncateAllTables } from "../test/db.js";
5
- import { CreditLedger } from "./credit-ledger.js";
5
+ import { DrizzleLedger } from "./ledger.js";
6
6
  import { grantSignupCredits, SIGNUP_GRANT } from "./signup-grant.js";
7
7
 
8
8
  describe("grantSignupCredits", () => {
9
9
  let pool: PGlite;
10
10
  let db: PlatformDb;
11
- let ledger: CreditLedger;
11
+ let ledger: DrizzleLedger;
12
12
 
13
13
  beforeAll(async () => {
14
14
  ({ db, pool } = await createTestDb());
@@ -20,7 +20,9 @@ describe("grantSignupCredits", () => {
20
20
 
21
21
  beforeEach(async () => {
22
22
  await truncateAllTables(pool);
23
- ledger = new CreditLedger(db);
23
+ ledger = new DrizzleLedger(db);
24
+
25
+ await ledger.seedSystemAccounts();
24
26
  });
25
27
 
26
28
  it("grants credits to a new tenant and returns true", async () => {
@@ -53,7 +55,8 @@ describe("grantSignupCredits", () => {
53
55
  const uniqueErr = Object.assign(new Error("duplicate key value violates unique constraint"), {
54
56
  code: "23505",
55
57
  });
56
- const racingLedger = new CreditLedger(db);
58
+ const racingLedger = new DrizzleLedger(db);
59
+ await racingLedger.seedSystemAccounts();
57
60
  vi.spyOn(racingLedger, "hasReferenceId").mockResolvedValue(false);
58
61
  vi.spyOn(racingLedger, "credit").mockRejectedValue(uniqueErr);
59
62
 
@@ -1,5 +1,5 @@
1
1
  import { Credit } from "./credit.js";
2
- import type { ICreditLedger } from "./credit-ledger.js";
2
+ import type { ILedger } from "./ledger.js";
3
3
 
4
4
  /** Signup grant amount: $5.00 */
5
5
  export const SIGNUP_GRANT = Credit.fromDollars(5);
@@ -11,25 +11,19 @@ export const SIGNUP_GRANT = Credit.fromDollars(5);
11
11
  *
12
12
  * @returns true if the grant was applied, false if already granted.
13
13
  */
14
- export async function grantSignupCredits(ledger: ICreditLedger, tenantId: string): Promise<boolean> {
14
+ export async function grantSignupCredits(ledger: ILedger, tenantId: string): Promise<boolean> {
15
15
  const refId = `signup:${tenantId}`;
16
16
 
17
- // Idempotency check
18
17
  if (await ledger.hasReferenceId(refId)) {
19
18
  return false;
20
19
  }
21
20
 
22
21
  try {
23
- await ledger.credit(
24
- tenantId,
25
- SIGNUP_GRANT,
26
- "signup_grant",
27
- "Welcome bonus — $5.00 credit on email verification",
28
- refId,
29
- );
22
+ await ledger.credit(tenantId, SIGNUP_GRANT, "signup_grant", {
23
+ description: "Welcome bonus — $5.00 credit on email verification",
24
+ referenceId: refId,
25
+ });
30
26
  } catch (err) {
31
- // Concurrent verify-email request won the race and already inserted the same referenceId.
32
- // Treat unique constraint violation as a no-op (idempotent).
33
27
  if (isUniqueConstraintViolation(err)) return false;
34
28
  throw err;
35
29
  }
@@ -0,0 +1,68 @@
1
+ import type { PGlite } from "@electric-sql/pglite";
2
+ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { createTestDb, truncateAllTables } from "../test/db.js";
4
+ import { Credit } from "./credit.js";
5
+ import { DrizzleLedger } from "./ledger.js";
6
+ import { runTrialBalanceCron } from "./trial-balance-cron.js";
7
+
8
+ describe("runTrialBalanceCron", () => {
9
+ let pool: PGlite;
10
+ let ledger: DrizzleLedger;
11
+
12
+ beforeAll(async () => {
13
+ const { db, pool: p } = await createTestDb();
14
+ pool = p;
15
+ ledger = new DrizzleLedger(db);
16
+ });
17
+
18
+ afterAll(async () => {
19
+ await pool.close();
20
+ });
21
+
22
+ beforeEach(async () => {
23
+ await truncateAllTables(pool);
24
+ await ledger.seedSystemAccounts();
25
+ });
26
+
27
+ it("returns balanced when no entries exist", async () => {
28
+ const result = await runTrialBalanceCron({ ledger });
29
+ expect(result.balanced).toBe(true);
30
+ expect(result.differenceRaw).toBe(0);
31
+ });
32
+
33
+ it("returns balanced after normal credit and debit", async () => {
34
+ await ledger.credit("t1", Credit.fromCents(500), "purchase");
35
+ await ledger.debit("t1", Credit.fromCents(200), "bot_runtime");
36
+
37
+ const result = await runTrialBalanceCron({ ledger });
38
+ expect(result.balanced).toBe(true);
39
+ expect(result.differenceRaw).toBe(0);
40
+ });
41
+
42
+ it("logs an error on imbalance", async () => {
43
+ // Inject an imbalance by mocking trialBalance to return unbalanced data
44
+ const errorSpy = vi.spyOn(ledger, "trialBalance").mockResolvedValueOnce({
45
+ totalDebits: Credit.fromCents(1000),
46
+ totalCredits: Credit.fromCents(900),
47
+ balanced: false,
48
+ difference: Credit.fromCents(100),
49
+ });
50
+
51
+ const result = await runTrialBalanceCron({ ledger });
52
+ expect(result.balanced).toBe(false);
53
+ expect(result.differenceRaw).toBe(Credit.fromCents(100).toRaw());
54
+
55
+ errorSpy.mockRestore();
56
+ });
57
+
58
+ it("does not throw on imbalance", async () => {
59
+ vi.spyOn(ledger, "trialBalance").mockResolvedValueOnce({
60
+ totalDebits: Credit.fromCents(500),
61
+ totalCredits: Credit.fromCents(400),
62
+ balanced: false,
63
+ difference: Credit.fromCents(100),
64
+ });
65
+
66
+ await expect(runTrialBalanceCron({ ledger })).resolves.not.toThrow();
67
+ });
68
+ });
@@ -0,0 +1,46 @@
1
+ import { logger } from "../config/logger.js";
2
+ import type { ILedger } from "./ledger.js";
3
+
4
+ export interface TrialBalanceCronConfig {
5
+ ledger: ILedger;
6
+ }
7
+
8
+ export interface TrialBalanceCronResult {
9
+ balanced: boolean;
10
+ totalDebits: number;
11
+ totalCredits: number;
12
+ /** Absolute difference in raw units (nanodollars). Zero when balanced. */
13
+ differenceRaw: number;
14
+ }
15
+
16
+ /**
17
+ * Run a trial balance check: assert that sum(debit lines) === sum(credit lines)
18
+ * across all journal entries.
19
+ *
20
+ * Designed to run hourly. Logs an error on imbalance but never throws —
21
+ * an imbalance is historical and requires human investigation, not automated action.
22
+ */
23
+ export async function runTrialBalanceCron(cfg: TrialBalanceCronConfig): Promise<TrialBalanceCronResult> {
24
+ const tb = await cfg.ledger.trialBalance();
25
+
26
+ const result: TrialBalanceCronResult = {
27
+ balanced: tb.balanced,
28
+ totalDebits: tb.totalDebits.toRaw(),
29
+ totalCredits: tb.totalCredits.toRaw(),
30
+ differenceRaw: tb.difference.toRaw(),
31
+ };
32
+
33
+ if (!tb.balanced) {
34
+ logger.error("LEDGER IMBALANCE DETECTED — books do not balance", {
35
+ totalDebits: tb.totalDebits.toDisplayString(),
36
+ totalCredits: tb.totalCredits.toDisplayString(),
37
+ difference: tb.difference.toDisplayString(),
38
+ });
39
+ } else {
40
+ logger.info("Trial balance check passed", {
41
+ totalDebits: tb.totalDebits.toDisplayString(),
42
+ });
43
+ }
44
+
45
+ return result;
46
+ }
@@ -25,6 +25,7 @@ export * from "./gateway-service-keys.js";
25
25
  export * from "./gpu-allocations.js";
26
26
  export * from "./gpu-configurations.js";
27
27
  export * from "./gpu-nodes.js";
28
+ export * from "./ledger.js";
28
29
  export * from "./marketplace-plugins.js";
29
30
  export * from "./meter-events.js";
30
31
  export * from "./node-registration-tokens.js";
@@ -0,0 +1,94 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { bigint, index, jsonb, pgEnum, pgTable, text, uniqueIndex } from "drizzle-orm/pg-core";
3
+
4
+ export const accountTypeEnum = pgEnum("account_type", ["asset", "liability", "equity", "revenue", "expense"]);
5
+
6
+ export const entrySideEnum = pgEnum("entry_side", ["debit", "credit"]);
7
+
8
+ /**
9
+ * Chart of accounts — every account that can appear in a journal line.
10
+ *
11
+ * System accounts (tenant_id IS NULL) are seeded at migration time.
12
+ * Per-tenant liability accounts are created lazily on first transaction.
13
+ */
14
+ export const accounts = pgTable(
15
+ "accounts",
16
+ {
17
+ id: text("id").primaryKey(),
18
+ code: text("code").notNull(),
19
+ name: text("name").notNull(),
20
+ type: accountTypeEnum("type").notNull(),
21
+ normalSide: entrySideEnum("normal_side").notNull(),
22
+ tenantId: text("tenant_id"), // NULL = system account
23
+ createdAt: text("created_at").notNull().default(sql`(now())`),
24
+ },
25
+ (table) => [
26
+ uniqueIndex("idx_accounts_code").on(table.code),
27
+ index("idx_accounts_tenant").on(table.tenantId).where(sql`${table.tenantId} IS NOT NULL`),
28
+ index("idx_accounts_type").on(table.type),
29
+ ],
30
+ );
31
+
32
+ /**
33
+ * Journal entries — the header for each balanced transaction.
34
+ * One business event = one journal entry = two or more journal lines that sum to zero.
35
+ */
36
+ export const journalEntries = pgTable(
37
+ "journal_entries",
38
+ {
39
+ id: text("id").primaryKey(),
40
+ postedAt: text("posted_at").notNull().default(sql`(now())`),
41
+ entryType: text("entry_type").notNull(), // purchase, usage, grant, refund, dividend, expiry, correction
42
+ description: text("description"),
43
+ referenceId: text("reference_id"),
44
+ tenantId: text("tenant_id").notNull(),
45
+ metadata: jsonb("metadata"), // funding_source, attributed_user_id, stripe_fingerprint, etc.
46
+ createdBy: text("created_by"), // system, admin:<id>, cron:expiry, etc.
47
+ },
48
+ (table) => [
49
+ uniqueIndex("idx_je_reference").on(table.referenceId).where(sql`${table.referenceId} IS NOT NULL`),
50
+ index("idx_je_tenant").on(table.tenantId),
51
+ index("idx_je_type").on(table.entryType),
52
+ index("idx_je_posted").on(table.postedAt),
53
+ index("idx_je_tenant_posted").on(table.tenantId, table.postedAt),
54
+ ],
55
+ );
56
+
57
+ /**
58
+ * Journal lines — the individual debits and credits within a journal entry.
59
+ * Invariant: for every journal_entry, SUM(debit amounts) = SUM(credit amounts).
60
+ * Amount is always positive; `side` determines the direction.
61
+ * Stored in nanodollars (Credit.toRaw()).
62
+ */
63
+ export const journalLines = pgTable(
64
+ "journal_lines",
65
+ {
66
+ id: text("id").primaryKey(),
67
+ journalEntryId: text("journal_entry_id")
68
+ .notNull()
69
+ .references(() => journalEntries.id),
70
+ accountId: text("account_id")
71
+ .notNull()
72
+ .references(() => accounts.id),
73
+ amount: bigint("amount", { mode: "number" }).notNull(), // nanodollars, always positive
74
+ side: entrySideEnum("side").notNull(),
75
+ },
76
+ (table) => [
77
+ index("idx_jl_entry").on(table.journalEntryId),
78
+ index("idx_jl_account").on(table.accountId),
79
+ index("idx_jl_account_side").on(table.accountId, table.side),
80
+ ],
81
+ );
82
+
83
+ /**
84
+ * Materialized account balances — cache derived from journal_lines.
85
+ * Updated atomically within the same transaction as the journal line insert.
86
+ * Can always be reconstructed from journal_lines if corrupted.
87
+ */
88
+ export const accountBalances = pgTable("account_balances", {
89
+ accountId: text("account_id")
90
+ .primaryKey()
91
+ .references(() => accounts.id),
92
+ balance: bigint("balance", { mode: "number" }).notNull().default(0), // net balance in nanodollars
93
+ lastUpdated: text("last_updated").notNull().default(sql`(now())`),
94
+ });
@@ -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 { describe, expect, it, vi } from "vitest";
4
4
  import { type CreditGateDeps, debitCredits } from "./credit-gate.js";
@@ -9,7 +9,7 @@ describe("onDebitComplete wiring", () => {
9
9
  const balance = vi.fn().mockResolvedValue(Credit.fromCents(500));
10
10
  const debit = vi.fn().mockResolvedValue(undefined);
11
11
  const deps: CreditGateDeps = {
12
- creditLedger: { balance, debit } as unknown as ICreditLedger,
12
+ creditLedger: { balance, debit } as unknown as ILedger,
13
13
  topUpUrl: "/billing",
14
14
  onDebitComplete,
15
15
  };
@@ -25,7 +25,7 @@ describe("onDebitComplete wiring", () => {
25
25
  const { InsufficientBalanceError } = await import("@wopr-network/platform-core/credits");
26
26
  const debit = vi.fn().mockRejectedValue(new InsufficientBalanceError(Credit.fromCents(0), Credit.fromCents(100)));
27
27
  const deps: CreditGateDeps = {
28
- creditLedger: { debit } as unknown as ICreditLedger,
28
+ creditLedger: { debit } as unknown as ILedger,
29
29
  topUpUrl: "/billing",
30
30
  onDebitComplete,
31
31
  };
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { PGlite } from "@electric-sql/pglite";
6
- import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
6
+ import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
7
7
  import { Hono } from "hono";
8
8
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
9
9
  import type { DrizzleDb } from "../db/index.js";
@@ -51,11 +51,12 @@ afterAll(async () => {
51
51
  describe("creditBalanceCheck grace buffer", () => {
52
52
  beforeEach(async () => {
53
53
  await truncateAllTables(pool);
54
+ await new DrizzleLedger(db).seedSystemAccounts();
54
55
  });
55
56
 
56
57
  it("returns null when balance is above estimated cost (passes)", async () => {
57
- const ledger = new CreditLedger(db);
58
- await ledger.credit("t1", Credit.fromCents(500), "purchase", "setup");
58
+ const ledger = new DrizzleLedger(db);
59
+ await ledger.credit("t1", Credit.fromCents(500), "purchase", { description: "setup" });
59
60
  const c = await buildHonoContext("t1");
60
61
  const deps: CreditGateDeps = { creditLedger: ledger, topUpUrl: "/billing" };
61
62
  expect(await creditBalanceCheck(c, deps, 1)).toBeNull();
@@ -63,27 +64,27 @@ describe("creditBalanceCheck grace buffer", () => {
63
64
 
64
65
  it("returns null when balance is zero but within default grace buffer (passes)", async () => {
65
66
  // Balance at exactly 0 — within the -50 grace buffer
66
- const ledger = new CreditLedger(db);
67
- await ledger.credit("t1", Credit.fromCents(10), "purchase", "setup");
68
- await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "drain");
67
+ const ledger = new DrizzleLedger(db);
68
+ await ledger.credit("t1", Credit.fromCents(10), "purchase", { description: "setup" });
69
+ await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", { description: "drain" });
69
70
  const c = await buildHonoContext("t1");
70
71
  const deps: CreditGateDeps = { creditLedger: ledger, topUpUrl: "/billing" };
71
72
  expect(await creditBalanceCheck(c, deps, 0)).toBeNull();
72
73
  });
73
74
 
74
75
  it("returns null when balance is -49 (within 50-cent grace buffer)", async () => {
75
- const ledger = new CreditLedger(db);
76
- await ledger.credit("t1", Credit.fromCents(1), "purchase", "setup");
77
- await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", "drain", undefined, true); // balance = -49
76
+ const ledger = new DrizzleLedger(db);
77
+ await ledger.credit("t1", Credit.fromCents(1), "purchase", { description: "setup" });
78
+ await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", { description: "drain", allowNegative: true }); // balance = -49
78
79
  const c = await buildHonoContext("t1");
79
80
  const deps: CreditGateDeps = { creditLedger: ledger, topUpUrl: "/billing" };
80
81
  expect(await creditBalanceCheck(c, deps, 0)).toBeNull();
81
82
  });
82
83
 
83
84
  it("returns credits_exhausted when balance is at -50 (at grace buffer limit)", async () => {
84
- const ledger = new CreditLedger(db);
85
- await ledger.credit("t1", Credit.fromCents(1), "purchase", "setup");
86
- await ledger.debit("t1", Credit.fromCents(51), "adapter_usage", "drain", undefined, true); // balance = -50
85
+ const ledger = new DrizzleLedger(db);
86
+ await ledger.credit("t1", Credit.fromCents(1), "purchase", { description: "setup" });
87
+ await ledger.debit("t1", Credit.fromCents(51), "adapter_usage", { description: "drain", allowNegative: true }); // balance = -50
87
88
  const c = await buildHonoContext("t1");
88
89
  const deps: CreditGateDeps = { creditLedger: ledger, topUpUrl: "/billing" };
89
90
  const result = await creditBalanceCheck(c, deps, 0);
@@ -92,9 +93,9 @@ describe("creditBalanceCheck grace buffer", () => {
92
93
  });
93
94
 
94
95
  it("returns credits_exhausted when balance is at -51 (beyond grace buffer)", async () => {
95
- const ledger = new CreditLedger(db);
96
- await ledger.credit("t1", Credit.fromCents(1), "purchase", "setup");
97
- await ledger.debit("t1", Credit.fromCents(52), "adapter_usage", "drain", undefined, true); // balance = -51
96
+ const ledger = new DrizzleLedger(db);
97
+ await ledger.credit("t1", Credit.fromCents(1), "purchase", { description: "setup" });
98
+ await ledger.debit("t1", Credit.fromCents(52), "adapter_usage", { description: "drain", allowNegative: true }); // balance = -51
98
99
  const c = await buildHonoContext("t1");
99
100
  const deps: CreditGateDeps = { creditLedger: ledger, topUpUrl: "/billing" };
100
101
  const result = await creditBalanceCheck(c, deps, 0);
@@ -103,9 +104,9 @@ describe("creditBalanceCheck grace buffer", () => {
103
104
  });
104
105
 
105
106
  it("returns credits_exhausted when custom graceBufferCents=0 and balance is 0", async () => {
106
- const ledger = new CreditLedger(db);
107
- await ledger.credit("t1", Credit.fromCents(10), "purchase", "setup");
108
- await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "drain"); // balance = 0
107
+ const ledger = new DrizzleLedger(db);
108
+ await ledger.credit("t1", Credit.fromCents(10), "purchase", { description: "setup" });
109
+ await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", { description: "drain" }); // balance = 0
109
110
  const c = await buildHonoContext("t1");
110
111
  const deps: CreditGateDeps = { creditLedger: ledger, topUpUrl: "/billing", graceBufferCents: 0 };
111
112
  const result = await creditBalanceCheck(c, deps, 0);
@@ -114,8 +115,8 @@ describe("creditBalanceCheck grace buffer", () => {
114
115
  });
115
116
 
116
117
  it("returns insufficient_credits when balance positive but below estimated cost", async () => {
117
- const ledger = new CreditLedger(db);
118
- await ledger.credit("t1", Credit.fromCents(5), "purchase", "setup");
118
+ const ledger = new DrizzleLedger(db);
119
+ await ledger.credit("t1", Credit.fromCents(5), "purchase", { description: "setup" });
119
120
  const c = await buildHonoContext("t1");
120
121
  const deps: CreditGateDeps = { creditLedger: ledger, topUpUrl: "/billing" };
121
122
  const result = await creditBalanceCheck(c, deps, 10);
@@ -131,11 +132,12 @@ describe("creditBalanceCheck grace buffer", () => {
131
132
  describe("debitCredits with allowNegative and onBalanceExhausted", () => {
132
133
  beforeEach(async () => {
133
134
  await truncateAllTables(pool);
135
+ await new DrizzleLedger(db).seedSystemAccounts();
134
136
  });
135
137
 
136
138
  it("debit with cost that would exceed balance succeeds (allowNegative=true)", async () => {
137
- const ledger = new CreditLedger(db);
138
- await ledger.credit("t1", Credit.fromCents(5), "purchase", "setup"); // balance = 5 cents
139
+ const ledger = new DrizzleLedger(db);
140
+ await ledger.credit("t1", Credit.fromCents(5), "purchase", { description: "setup" }); // balance = 5 cents
139
141
 
140
142
  // costUsd = $0.10 = 10 cents, margin = 1.0
141
143
  // This should push balance negative without throwing
@@ -147,8 +149,8 @@ describe("debitCredits with allowNegative and onBalanceExhausted", () => {
147
149
  });
148
150
 
149
151
  it("fires onBalanceExhausted when debit causes balance to cross zero", async () => {
150
- const ledger = new CreditLedger(db);
151
- await ledger.credit("t1", Credit.fromCents(5), "purchase", "setup"); // balance = 5 cents
152
+ const ledger = new DrizzleLedger(db);
153
+ await ledger.credit("t1", Credit.fromCents(5), "purchase", { description: "setup" }); // balance = 5 cents
152
154
 
153
155
  const onBalanceExhausted = vi.fn();
154
156
  // costUsd = $0.10 = 10 cents with margin 1.0 → chargeCents = 10, pushes balance to -5
@@ -165,8 +167,8 @@ describe("debitCredits with allowNegative and onBalanceExhausted", () => {
165
167
  });
166
168
 
167
169
  it("does NOT fire onBalanceExhausted when balance stays positive after debit", async () => {
168
- const ledger = new CreditLedger(db);
169
- await ledger.credit("t1", Credit.fromCents(500), "purchase", "setup"); // balance = 500 cents
170
+ const ledger = new DrizzleLedger(db);
171
+ await ledger.credit("t1", Credit.fromCents(500), "purchase", { description: "setup" }); // balance = 500 cents
170
172
 
171
173
  const onBalanceExhausted = vi.fn();
172
174
  // costUsd = $0.01 = 1 cent → balance stays at 499
@@ -184,8 +186,8 @@ describe("debitCredits with allowNegative and onBalanceExhausted", () => {
184
186
  });
185
187
 
186
188
  it("onBalanceExhausted callback receives correct tenantId and negative balance", async () => {
187
- const ledger = new CreditLedger(db);
188
- await ledger.credit("t1", Credit.fromCents(3), "purchase", "setup"); // balance = 3 cents
189
+ const ledger = new DrizzleLedger(db);
190
+ await ledger.credit("t1", Credit.fromCents(3), "purchase", { description: "setup" }); // balance = 3 cents
189
191
 
190
192
  const onBalanceExhausted = vi.fn();
191
193
  // costUsd = $0.05 = 5 cents with margin 1.0 → pushes balance to -2
@@ -203,8 +205,8 @@ describe("debitCredits with allowNegative and onBalanceExhausted", () => {
203
205
  });
204
206
 
205
207
  it("calls onSpendAlertCrossed after successful debit", async () => {
206
- const ledger = new CreditLedger(db);
207
- await ledger.credit("t1", Credit.fromCents(500), "purchase", "setup");
208
+ const ledger = new DrizzleLedger(db);
209
+ await ledger.credit("t1", Credit.fromCents(500), "purchase", { description: "setup" });
208
210
 
209
211
  const onSpendAlertCrossed = vi.fn();
210
212
  await debitCredits(
@@ -219,10 +221,10 @@ describe("debitCredits with allowNegative and onBalanceExhausted", () => {
219
221
  });
220
222
 
221
223
  it("does NOT fire onBalanceExhausted when balance was already negative before debit", async () => {
222
- const ledger = new CreditLedger(db);
224
+ const ledger = new DrizzleLedger(db);
223
225
  // Start with negative balance: credit 5, debit 10 → balance = -5
224
- await ledger.credit("t1", Credit.fromCents(5), "purchase", "setup");
225
- await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "drain", undefined, true);
226
+ await ledger.credit("t1", Credit.fromCents(5), "purchase", { description: "setup" });
227
+ await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", { description: "drain", allowNegative: true });
226
228
 
227
229
  const onBalanceExhausted = vi.fn();
228
230
  // Another debit of 1 cent — balance goes from -5 to -6, but was already negative
@@ -6,7 +6,7 @@
6
6
  * gateway endpoints.
7
7
  */
8
8
 
9
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
9
+ import type { ILedger } from "@wopr-network/platform-core/credits";
10
10
  import { Credit, InsufficientBalanceError } from "@wopr-network/platform-core/credits";
11
11
  import type { Context } from "hono";
12
12
  import { logger } from "../config/logger.js";
@@ -14,7 +14,7 @@ import { withMargin } from "../monetization/adapters/types.js";
14
14
  import type { GatewayAuthEnv } from "./service-key-auth.js";
15
15
 
16
16
  export interface CreditGateDeps {
17
- creditLedger?: ICreditLedger;
17
+ creditLedger?: ILedger;
18
18
  topUpUrl: string;
19
19
  /** Maximum negative balance allowed before hard-stop, in cents. Default: 50 (-$0.50). */
20
20
  graceBufferCents?: number;
@@ -118,15 +118,11 @@ export async function debitCredits(
118
118
  }
119
119
 
120
120
  try {
121
- await deps.creditLedger.debit(
122
- tenantId,
123
- chargeCredit,
124
- "adapter_usage",
125
- `Gateway ${capability} via ${provider}`,
126
- undefined,
127
- true,
121
+ await deps.creditLedger.debit(tenantId, chargeCredit, "adapter_usage", {
122
+ description: `Gateway ${capability} via ${provider}`,
123
+ allowNegative: true,
128
124
  attributedUserId,
129
- );
125
+ });
130
126
 
131
127
  // Only fire on first zero-crossing (balance was positive before, now ≤ 0)
132
128
  if (deps.onBalanceExhausted) {
@@ -7,7 +7,7 @@
7
7
  * Uses mocked deps — no PGlite required.
8
8
  */
9
9
 
10
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
10
+ import type { ILedger } from "@wopr-network/platform-core/credits";
11
11
  import { Credit } from "@wopr-network/platform-core/credits";
12
12
  import { Hono } from "hono";
13
13
  import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -51,7 +51,7 @@ function buildTestConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig
51
51
  debit: vi.fn().mockResolvedValue(undefined),
52
52
  credit: vi.fn(),
53
53
  history: vi.fn(),
54
- } as unknown as ICreditLedger;
54
+ } as unknown as ILedger;
55
55
  const fetchFn = vi.fn().mockResolvedValue(
56
56
  new Response(
57
57
  JSON.stringify({
@@ -163,7 +163,7 @@ describe("gateway routes — credit check", () => {
163
163
  debit: vi.fn(),
164
164
  credit: vi.fn(),
165
165
  history: vi.fn(),
166
- } as unknown as ICreditLedger,
166
+ } as unknown as ILedger,
167
167
  });
168
168
  const app = buildApp(config);
169
169
  const res = await chatRequest(app, VALID_KEY);
@@ -183,7 +183,7 @@ describe("gateway routes — credit check", () => {
183
183
  debit: vi.fn(),
184
184
  credit: vi.fn(),
185
185
  history: vi.fn(),
186
- } as unknown as ICreditLedger,
186
+ } as unknown as ILedger,
187
187
  });
188
188
  const app = buildApp(config);
189
189
  const res = await chatRequest(app, VALID_KEY);
@@ -200,7 +200,7 @@ describe("gateway routes — credit check", () => {
200
200
  debit: vi.fn(),
201
201
  credit: vi.fn(),
202
202
  history: vi.fn(),
203
- } as unknown as ICreditLedger,
203
+ } as unknown as ILedger,
204
204
  fetchFn,
205
205
  });
206
206
  const app = buildApp(config);
@@ -5,7 +5,7 @@
5
5
  * budget checking, metering, provider configs, fetch, and service key resolution.
6
6
  */
7
7
 
8
- import type { Credit, ICreditLedger } from "@wopr-network/platform-core/credits";
8
+ import type { Credit, ILedger } from "@wopr-network/platform-core/credits";
9
9
  import type { MeterEmitter } from "@wopr-network/platform-core/metering";
10
10
  import type { IRateLimitRepository } from "../../api/rate-limit-repository.js";
11
11
  import type { IBudgetChecker } from "../../monetization/budget/budget-checker.js";
@@ -18,7 +18,7 @@ import type { FetchFn, GatewayTenant, ProviderConfig } from "../types.js";
18
18
  export interface ProtocolDeps {
19
19
  meter: MeterEmitter;
20
20
  budgetChecker: IBudgetChecker;
21
- creditLedger?: ICreditLedger;
21
+ creditLedger?: ILedger;
22
22
  topUpUrl: string;
23
23
  graceBufferCents?: number;
24
24
  providers: ProviderConfig;