@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,6 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import type { PGlite } from "@electric-sql/pglite";
3
- import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
3
+ import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
4
4
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
5
5
  import type { DrizzleDb } from "../../db/index.js";
6
6
  import { adminUsers } from "../../db/schema/admin-users.js";
@@ -54,6 +54,7 @@ describe("DrizzleDividendRepository", () => {
54
54
 
55
55
  beforeEach(async () => {
56
56
  await truncateAllTables(pool);
57
+ await new DrizzleLedger(db).seedSystemAccounts();
57
58
  repo = new DrizzleDividendRepository(db);
58
59
  });
59
60
 
@@ -194,8 +195,8 @@ describe("DrizzleDividendRepository", () => {
194
195
  });
195
196
 
196
197
  it("marks user as eligible when they have a recent purchase", async () => {
197
- const ledger = new CreditLedger(db);
198
- await ledger.credit("t1", Credit.fromCents(100), "purchase", "recent buy");
198
+ const ledger = new DrizzleLedger(db);
199
+ await ledger.credit("t1", Credit.fromCents(100), "purchase", { description: "recent buy" });
199
200
 
200
201
  const stats = await repo.getStats("t1");
201
202
  expect(stats.userEligible).toBe(true);
@@ -2,8 +2,8 @@ import { Credit } from "@wopr-network/platform-core/credits";
2
2
  import { and, desc, eq, gte, lt, sql } from "drizzle-orm";
3
3
  import type { DrizzleDb } from "../../db/index.js";
4
4
  import { adminUsers } from "../../db/schema/admin-users.js";
5
- import { creditTransactions } from "../../db/schema/credits.js";
6
5
  import { dividendDistributions } from "../../db/schema/dividend-distributions.js";
6
+ import { journalEntries, journalLines } from "../../db/schema/ledger.js";
7
7
  import type { DividendHistoryEntry, DividendStats } from "../repository-types.js";
8
8
 
9
9
  export type { DividendHistoryEntry, DividendStats };
@@ -30,35 +30,32 @@ export class DrizzleDividendRepository implements IDividendRepository {
30
30
  constructor(private readonly db: DrizzleDb) {}
31
31
 
32
32
  async getStats(tenantId: string): Promise<DividendStats> {
33
- // 1. Pool = sum of purchase amounts from yesterday UTC
33
+ // 1. Pool = sum of purchase credit amounts from yesterday UTC
34
34
  const poolRow = (
35
35
  await this.db
36
- // raw SQL: Drizzle cannot express COALESCE(SUM(...), 0) aggregate
37
- .select({ total: sql<number>`COALESCE(SUM(${creditTransactions.amount}), 0)` })
38
- .from(creditTransactions)
36
+ .select({ total: sql<string>`COALESCE(SUM(${journalLines.amount}), 0)` })
37
+ .from(journalLines)
38
+ .innerJoin(journalEntries, eq(journalEntries.id, journalLines.journalEntryId))
39
39
  .where(
40
40
  and(
41
- eq(creditTransactions.type, "purchase"),
42
- // raw SQL: Drizzle cannot express date_trunc with interval arithmetic
43
- sql`${creditTransactions.createdAt}::timestamp >= date_trunc('day', timezone('UTC', now())) - INTERVAL '1 day'`,
44
- sql`${creditTransactions.createdAt}::timestamp < date_trunc('day', timezone('UTC', now()))`,
41
+ eq(journalEntries.entryType, "purchase"),
42
+ eq(journalLines.side, "credit"),
43
+ sql`${journalEntries.postedAt}::timestamp >= date_trunc('day', timezone('UTC', now())) - INTERVAL '1 day'`,
44
+ sql`${journalEntries.postedAt}::timestamp < date_trunc('day', timezone('UTC', now()))`,
45
45
  ),
46
46
  )
47
47
  )[0];
48
- const poolCents = poolRow?.total ?? 0;
49
- const pool = Credit.fromCents(poolCents);
48
+ const pool = Credit.fromRaw(Number(poolRow?.total ?? 0));
50
49
 
51
50
  // 2. Active users = distinct tenants with a purchase in the last 7 days
52
51
  const activeRow = (
53
52
  await this.db
54
- // raw SQL: Drizzle cannot express COUNT(DISTINCT col)
55
- .select({ count: sql<number>`COUNT(DISTINCT ${creditTransactions.tenantId})` })
56
- .from(creditTransactions)
53
+ .select({ count: sql<number>`COUNT(DISTINCT ${journalEntries.tenantId})` })
54
+ .from(journalEntries)
57
55
  .where(
58
56
  and(
59
- eq(creditTransactions.type, "purchase"),
60
- // raw SQL: Drizzle cannot express timestamp comparison with interval arithmetic
61
- sql`${creditTransactions.createdAt}::timestamp >= timezone('UTC', now()) - INTERVAL '7 days'`,
57
+ eq(journalEntries.entryType, "purchase"),
58
+ sql`${journalEntries.postedAt}::timestamp >= timezone('UTC', now()) - INTERVAL '7 days'`,
62
59
  ),
63
60
  )
64
61
  )[0];
@@ -75,10 +72,10 @@ export class DrizzleDividendRepository implements IDividendRepository {
75
72
  // 5. User eligibility — last purchase within 7 days
76
73
  const userPurchaseRow = (
77
74
  await this.db
78
- .select({ createdAt: creditTransactions.createdAt })
79
- .from(creditTransactions)
80
- .where(and(eq(creditTransactions.tenantId, tenantId), eq(creditTransactions.type, "purchase")))
81
- .orderBy(desc(creditTransactions.createdAt))
75
+ .select({ postedAt: journalEntries.postedAt })
76
+ .from(journalEntries)
77
+ .where(and(eq(journalEntries.tenantId, tenantId), eq(journalEntries.entryType, "purchase")))
78
+ .orderBy(desc(journalEntries.postedAt))
82
79
  .limit(1)
83
80
  )[0];
84
81
 
@@ -87,10 +84,7 @@ export class DrizzleDividendRepository implements IDividendRepository {
87
84
  let userWindowExpiresAt: string | null = null;
88
85
 
89
86
  if (userPurchaseRow) {
90
- const rawTs = userPurchaseRow.createdAt;
91
- // Parse the timestamp directly. PGlite may return ISO strings with or without
92
- // timezone suffix. JavaScript's Date constructor handles ISO 8601 strings natively.
93
- const lastPurchase = new Date(rawTs);
87
+ const lastPurchase = new Date(userPurchaseRow.postedAt);
94
88
  userLastPurchaseAt = lastPurchase.toISOString();
95
89
 
96
90
  const windowExpiry = new Date(lastPurchase.getTime() + 7 * 24 * 60 * 60 * 1000);
@@ -2,24 +2,24 @@ export type {
2
2
  AutoTopupSettings,
3
3
  CreditExpiryCronConfig,
4
4
  CreditExpiryCronResult,
5
- CreditTransaction,
6
5
  CreditType,
7
6
  DebitType,
8
7
  HistoryOptions,
9
8
  IAutoTopupSettingsRepository,
10
- ICreditLedger,
9
+ ILedger,
10
+ JournalEntry,
11
11
  TransactionType,
12
12
  } from "@wopr-network/platform-core/credits";
13
13
  export {
14
14
  ALLOWED_SCHEDULE_INTERVALS,
15
15
  ALLOWED_THRESHOLDS,
16
16
  ALLOWED_TOPUP_AMOUNTS,
17
- CreditLedger,
18
17
  computeNextScheduleAt,
19
18
  DrizzleAutoTopupSettingsRepository,
20
- DrizzleCreditLedger,
19
+ DrizzleLedger,
21
20
  grantSignupCredits,
22
21
  InsufficientBalanceError,
22
+ Ledger,
23
23
  runCreditExpiryCron,
24
24
  SIGNUP_GRANT,
25
25
  } from "@wopr-network/platform-core/credits";
@@ -1,13 +1,13 @@
1
1
  import type { PGlite } from "@electric-sql/pglite";
2
- import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
2
+ import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
3
3
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
4
4
  import type { DrizzleDb } from "../../db/index.js";
5
5
  import { createTestDb, truncateAllTables } from "../../test/db.js";
6
6
 
7
- describe("DrizzleCreditLedger.memberUsage", () => {
7
+ describe("DrizzleDrizzleLedger.memberUsage", () => {
8
8
  let pool: PGlite;
9
9
  let db: DrizzleDb;
10
- let ledger: CreditLedger;
10
+ let ledger: DrizzleLedger;
11
11
 
12
12
  beforeAll(async () => {
13
13
  ({ db, pool } = await createTestDb());
@@ -19,14 +19,25 @@ describe("DrizzleCreditLedger.memberUsage", () => {
19
19
 
20
20
  beforeEach(async () => {
21
21
  await truncateAllTables(pool);
22
- ledger = new CreditLedger(db);
22
+ ledger = new DrizzleLedger(db);
23
+
24
+ await ledger.seedSystemAccounts();
23
25
  });
24
26
 
25
27
  it("should aggregate debit totals per attributed user", async () => {
26
- await ledger.credit("org-1", Credit.fromCents(10000), "purchase", "Seed");
27
- await ledger.debit("org-1", Credit.fromCents(100), "adapter_usage", "Chat", undefined, false, "user-a");
28
- await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage", "Chat", undefined, false, "user-a");
29
- await ledger.debit("org-1", Credit.fromCents(300), "adapter_usage", "Chat", undefined, false, "user-b");
28
+ await ledger.credit("org-1", Credit.fromCents(10000), "purchase", { description: "Seed" });
29
+ await ledger.debit("org-1", Credit.fromCents(100), "adapter_usage", {
30
+ description: "Chat",
31
+ attributedUserId: "user-a",
32
+ });
33
+ await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage", {
34
+ description: "Chat",
35
+ attributedUserId: "user-a",
36
+ });
37
+ await ledger.debit("org-1", Credit.fromCents(300), "adapter_usage", {
38
+ description: "Chat",
39
+ attributedUserId: "user-b",
40
+ });
30
41
 
31
42
  const result = await ledger.memberUsage("org-1");
32
43
  expect(result).toHaveLength(2);
@@ -41,9 +52,12 @@ describe("DrizzleCreditLedger.memberUsage", () => {
41
52
  });
42
53
 
43
54
  it("should exclude transactions with null attributedUserId", async () => {
44
- await ledger.credit("org-1", Credit.fromCents(10000), "purchase", "Seed");
45
- await ledger.debit("org-1", Credit.fromCents(100), "bot_runtime", "Cron"); // no attributedUserId
46
- await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage", "Chat", undefined, false, "user-a");
55
+ await ledger.credit("org-1", Credit.fromCents(10000), "purchase", { description: "Seed" });
56
+ await ledger.debit("org-1", Credit.fromCents(100), "bot_runtime", { description: "Cron" }); // no attributedUserId
57
+ await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage", {
58
+ description: "Chat",
59
+ attributedUserId: "user-a",
60
+ });
47
61
 
48
62
  const result = await ledger.memberUsage("org-1");
49
63
  expect(result).toHaveLength(1);
@@ -1,28 +1,20 @@
1
- import {
2
- Credit,
3
- type CreditTransaction,
4
- type ICreditLedger,
5
- InsufficientBalanceError,
6
- } from "@wopr-network/platform-core/credits";
1
+ import { Credit, type ILedger, InsufficientBalanceError, type JournalEntry } from "@wopr-network/platform-core/credits";
7
2
  import type { IMeterEmitter } from "@wopr-network/platform-core/metering";
8
3
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
9
4
  import type { IPhoneNumberRepository } from "./drizzle-phone-number-repository.js";
10
5
  import { PHONE_NUMBER_MONTHLY_COST, runMonthlyPhoneBilling } from "./phone-billing.js";
11
6
  import type { ProvisionedPhoneNumber } from "./repository-types.js";
12
7
 
13
- function makeTx(tenantId: string): CreditTransaction {
8
+ function makeTx(tenantId: string): JournalEntry {
14
9
  return {
15
10
  id: "tx-1",
11
+ postedAt: new Date().toISOString(),
12
+ entryType: "addon",
16
13
  tenantId,
17
- amount: Credit.fromDollars(1),
18
- balanceAfter: Credit.fromDollars(100),
19
- type: "addon",
20
14
  description: "Monthly phone number fee",
21
15
  referenceId: null,
22
- fundingSource: null,
23
- attributedUserId: null,
24
- createdAt: new Date().toISOString(),
25
- expiresAt: null,
16
+ metadata: null,
17
+ lines: [],
26
18
  };
27
19
  }
28
20
 
@@ -108,7 +100,7 @@ describe("runMonthlyPhoneBilling", () => {
108
100
 
109
101
  const result = await runMonthlyPhoneBilling(
110
102
  phoneRepo as unknown as IPhoneNumberRepository,
111
- ledger as unknown as ICreditLedger,
103
+ ledger as unknown as ILedger,
112
104
  meter as unknown as IMeterEmitter,
113
105
  );
114
106
 
@@ -128,7 +120,7 @@ describe("runMonthlyPhoneBilling", () => {
128
120
 
129
121
  const result = await runMonthlyPhoneBilling(
130
122
  phoneRepo as unknown as IPhoneNumberRepository,
131
- ledger as unknown as ICreditLedger,
123
+ ledger as unknown as ILedger,
132
124
  meter as unknown as IMeterEmitter,
133
125
  );
134
126
 
@@ -138,16 +130,16 @@ describe("runMonthlyPhoneBilling", () => {
138
130
 
139
131
  // Verify debit was called with the margined charge amount
140
132
  expect(ledger.debit).toHaveBeenCalledOnce();
141
- const [tenantId, chargeAmount, type, description, referenceId, allowNegative] = ledger.debit.mock.calls[0];
133
+ const [tenantId, chargeAmount, type, opts] = ledger.debit.mock.calls[0];
142
134
  expect(tenantId).toBe("tenant-1");
143
135
  // chargeCredit = Credit.fromDollars(1.15).multiply(2.6)
144
136
  const expectedCharge = Credit.fromDollars(1.15).multiply(2.6);
145
137
  expect(chargeAmount.toRaw()).toBe(expectedCharge.toRaw());
146
138
  expect(type).toBe("addon");
147
- expect(description).toBe("Monthly phone number fee");
139
+ expect(opts.description).toBe("Monthly phone number fee");
148
140
  const expectedMonth = `${NOW.getFullYear()}-${String(NOW.getMonth() + 1).padStart(2, "0")}`;
149
- expect(referenceId).toMatch(new RegExp(`^phone-billing:PN-abc123:${expectedMonth}$`));
150
- expect(allowNegative).toBe(true);
141
+ expect(opts.referenceId).toMatch(new RegExp(`^phone-billing:PN-abc123:${expectedMonth}$`));
142
+ expect(opts.allowNegative).toBe(true);
151
143
 
152
144
  // Verify meter emission
153
145
  expect(meter.emit).toHaveBeenCalledOnce();
@@ -170,7 +162,7 @@ describe("runMonthlyPhoneBilling", () => {
170
162
 
171
163
  const result = await runMonthlyPhoneBilling(
172
164
  phoneRepo as unknown as IPhoneNumberRepository,
173
- ledger as unknown as ICreditLedger,
165
+ ledger as unknown as ILedger,
174
166
  meter as unknown as IMeterEmitter,
175
167
  );
176
168
 
@@ -191,7 +183,7 @@ describe("runMonthlyPhoneBilling", () => {
191
183
 
192
184
  const result = await runMonthlyPhoneBilling(
193
185
  phoneRepo as unknown as IPhoneNumberRepository,
194
- ledger as unknown as ICreditLedger,
186
+ ledger as unknown as ILedger,
195
187
  meter as unknown as IMeterEmitter,
196
188
  );
197
189
 
@@ -208,7 +200,7 @@ describe("runMonthlyPhoneBilling", () => {
208
200
 
209
201
  const result = await runMonthlyPhoneBilling(
210
202
  phoneRepo as unknown as IPhoneNumberRepository,
211
- ledger as unknown as ICreditLedger,
203
+ ledger as unknown as ILedger,
212
204
  meter as unknown as IMeterEmitter,
213
205
  );
214
206
 
@@ -229,7 +221,7 @@ describe("runMonthlyPhoneBilling", () => {
229
221
 
230
222
  const result = await runMonthlyPhoneBilling(
231
223
  phoneRepo as unknown as IPhoneNumberRepository,
232
- ledger as unknown as ICreditLedger,
224
+ ledger as unknown as ILedger,
233
225
  meter as unknown as IMeterEmitter,
234
226
  );
235
227
 
@@ -252,7 +244,7 @@ describe("runMonthlyPhoneBilling", () => {
252
244
 
253
245
  const result = await runMonthlyPhoneBilling(
254
246
  phoneRepo as unknown as IPhoneNumberRepository,
255
- ledger as unknown as ICreditLedger,
247
+ ledger as unknown as ILedger,
256
248
  meter as unknown as IMeterEmitter,
257
249
  );
258
250
 
@@ -271,7 +263,7 @@ describe("runMonthlyPhoneBilling", () => {
271
263
 
272
264
  const result = await runMonthlyPhoneBilling(
273
265
  phoneRepo as unknown as IPhoneNumberRepository,
274
- ledger as unknown as ICreditLedger,
266
+ ledger as unknown as ILedger,
275
267
  meter as unknown as IMeterEmitter,
276
268
  );
277
269
 
@@ -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, InsufficientBalanceError } from "@wopr-network/platform-core/credits";
3
3
  import type { IMeterEmitter } from "@wopr-network/platform-core/metering";
4
4
  import { logger } from "../../config/logger.js";
@@ -15,7 +15,7 @@ const PHONE_NUMBER_MARGIN = 2.6;
15
15
 
16
16
  export async function runMonthlyPhoneBilling(
17
17
  phoneRepo: IPhoneNumberRepository,
18
- ledger: ICreditLedger,
18
+ ledger: ILedger,
19
19
  meter: IMeterEmitter,
20
20
  ): Promise<{
21
21
  processed: number;
@@ -45,14 +45,11 @@ export async function runMonthlyPhoneBilling(
45
45
  const costCredit = Credit.fromDollars(PHONE_NUMBER_MONTHLY_COST);
46
46
  const chargeCredit = withMargin(costCredit, PHONE_NUMBER_MARGIN);
47
47
 
48
- await ledger.debit(
49
- number.tenantId,
50
- chargeCredit,
51
- "addon",
52
- "Monthly phone number fee",
53
- `phone-billing:${number.sid}:${now.toISOString().slice(0, 7)}`,
54
- true,
55
- );
48
+ await ledger.debit(number.tenantId, chargeCredit, "addon", {
49
+ description: "Monthly phone number fee",
50
+ referenceId: `phone-billing:${number.sid}:${now.toISOString().slice(0, 7)}`,
51
+ allowNegative: true,
52
+ });
56
53
 
57
54
  meter.emit({
58
55
  tenant: number.tenantId,
@@ -1,5 +1,5 @@
1
1
  import type { PGlite } from "@electric-sql/pglite";
2
- import { Credit, CreditLedger, InsufficientBalanceError } from "@wopr-network/platform-core/credits";
2
+ import { Credit, DrizzleLedger, InsufficientBalanceError } from "@wopr-network/platform-core/credits";
3
3
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
4
4
  import { RESOURCE_TIERS } from "../../fleet/resource-tiers.js";
5
5
  import { createTestDb, truncateAllTables } from "../../test/db.js";
@@ -8,12 +8,12 @@ import { buildResourceTierCosts, DAILY_BOT_COST, runRuntimeDeductions } from "./
8
8
  describe("runRuntimeDeductions", () => {
9
9
  const TODAY = "2025-01-01";
10
10
  let pool: PGlite;
11
- let ledger: CreditLedger;
11
+ let ledger: DrizzleLedger;
12
12
 
13
13
  beforeAll(async () => {
14
14
  const { db, pool: p } = await createTestDb();
15
15
  pool = p;
16
- ledger = new CreditLedger(db);
16
+ ledger = new DrizzleLedger(db);
17
17
  });
18
18
 
19
19
  afterAll(async () => {
@@ -22,6 +22,7 @@ describe("runRuntimeDeductions", () => {
22
22
 
23
23
  beforeEach(async () => {
24
24
  await truncateAllTables(pool);
25
+ await ledger.seedSystemAccounts();
25
26
  });
26
27
 
27
28
  it("DAILY_BOT_COST equals 17 cents", () => {
@@ -40,7 +41,7 @@ describe("runRuntimeDeductions", () => {
40
41
  });
41
42
 
42
43
  it("skips tenants with zero active bots", async () => {
43
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
44
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
44
45
  const result = await runRuntimeDeductions({
45
46
  ledger,
46
47
  date: TODAY,
@@ -51,7 +52,7 @@ describe("runRuntimeDeductions", () => {
51
52
  });
52
53
 
53
54
  it("deducts full amount when balance is sufficient", async () => {
54
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
55
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
55
56
  const result = await runRuntimeDeductions({
56
57
  ledger,
57
58
  date: TODAY,
@@ -63,7 +64,7 @@ describe("runRuntimeDeductions", () => {
63
64
  });
64
65
 
65
66
  it("partial deduction and suspension when balance is insufficient", async () => {
66
- await ledger.credit("tenant-1", Credit.fromCents(10), "purchase", "top-up");
67
+ await ledger.credit("tenant-1", Credit.fromCents(10), "purchase", { description: "top-up" });
67
68
  const onSuspend = vi.fn();
68
69
  const result = await runRuntimeDeductions({
69
70
  ledger,
@@ -78,9 +79,9 @@ describe("runRuntimeDeductions", () => {
78
79
  });
79
80
 
80
81
  it("suspends with zero partial when balance exactly zero", async () => {
81
- await ledger.credit("tenant-1", Credit.fromCents(100), "purchase", "top-up");
82
- await ledger.debit("tenant-1", Credit.fromCents(100), "bot_runtime", "drain");
83
- await ledger.credit("tenant-1", Credit.fromCents(1), "purchase", "tiny");
82
+ await ledger.credit("tenant-1", Credit.fromCents(100), "purchase", { description: "top-up" });
83
+ await ledger.debit("tenant-1", Credit.fromCents(100), "bot_runtime", { description: "drain" });
84
+ await ledger.credit("tenant-1", Credit.fromCents(1), "purchase", { description: "tiny" });
84
85
 
85
86
  const onSuspend = vi.fn();
86
87
  const result = await runRuntimeDeductions({
@@ -95,7 +96,7 @@ describe("runRuntimeDeductions", () => {
95
96
  });
96
97
 
97
98
  it("suspends without onSuspend callback", async () => {
98
- await ledger.credit("tenant-1", Credit.fromCents(5), "purchase", "top-up");
99
+ await ledger.credit("tenant-1", Credit.fromCents(5), "purchase", { description: "top-up" });
99
100
  const result = await runRuntimeDeductions({
100
101
  ledger,
101
102
  date: TODAY,
@@ -106,7 +107,7 @@ describe("runRuntimeDeductions", () => {
106
107
  });
107
108
 
108
109
  it("handles errors from getActiveBotCount gracefully", async () => {
109
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
110
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
110
111
  const result = await runRuntimeDeductions({
111
112
  ledger,
112
113
  date: TODAY,
@@ -120,8 +121,8 @@ describe("runRuntimeDeductions", () => {
120
121
  });
121
122
 
122
123
  it("handles InsufficientBalanceError from ledger.debit", async () => {
123
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
124
- await ledger.debit("tenant-1", Credit.fromCents(499), "bot_runtime", "drain");
124
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
125
+ await ledger.debit("tenant-1", Credit.fromCents(499), "bot_runtime", { description: "drain" });
125
126
  const onSuspend = vi.fn();
126
127
  const result = await runRuntimeDeductions({
127
128
  ledger,
@@ -134,7 +135,7 @@ describe("runRuntimeDeductions", () => {
134
135
  });
135
136
 
136
137
  it("catches InsufficientBalanceError from debit and suspends", async () => {
137
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
138
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
138
139
  vi.spyOn(ledger, "debit").mockRejectedValue(
139
140
  new InsufficientBalanceError(Credit.fromCents(0), Credit.fromCents(17)),
140
141
  );
@@ -152,7 +153,7 @@ describe("runRuntimeDeductions", () => {
152
153
  });
153
154
 
154
155
  it("catches InsufficientBalanceError without onSuspend callback", async () => {
155
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
156
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
156
157
  vi.spyOn(ledger, "debit").mockRejectedValue(
157
158
  new InsufficientBalanceError(Credit.fromCents(0), Credit.fromCents(17)),
158
159
  );
@@ -167,8 +168,8 @@ describe("runRuntimeDeductions", () => {
167
168
  });
168
169
 
169
170
  it("processes multiple tenants", async () => {
170
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
171
- await ledger.credit("tenant-2", Credit.fromCents(10), "purchase", "top-up");
171
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
172
+ await ledger.credit("tenant-2", Credit.fromCents(10), "purchase", { description: "top-up" });
172
173
  const onSuspend = vi.fn();
173
174
  const result = await runRuntimeDeductions({
174
175
  ledger,
@@ -182,7 +183,7 @@ describe("runRuntimeDeductions", () => {
182
183
  });
183
184
 
184
185
  it("fires onLowBalance when balance drops below 100 cents threshold", async () => {
185
- await ledger.credit("tenant-1", Credit.fromCents(110), "purchase", "top-up");
186
+ await ledger.credit("tenant-1", Credit.fromCents(110), "purchase", { description: "top-up" });
186
187
  const onLowBalance = vi.fn();
187
188
  await runRuntimeDeductions({
188
189
  ledger,
@@ -197,7 +198,7 @@ describe("runRuntimeDeductions", () => {
197
198
  });
198
199
 
199
200
  it("does NOT fire onLowBalance when balance was already below threshold before deduction", async () => {
200
- await ledger.credit("tenant-1", Credit.fromCents(90), "purchase", "top-up");
201
+ await ledger.credit("tenant-1", Credit.fromCents(90), "purchase", { description: "top-up" });
201
202
  const onLowBalance = vi.fn();
202
203
  await runRuntimeDeductions({
203
204
  ledger,
@@ -209,7 +210,7 @@ describe("runRuntimeDeductions", () => {
209
210
  });
210
211
 
211
212
  it("fires onCreditsExhausted when full deduction causes balance to drop to 0", async () => {
212
- await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", "top-up");
213
+ await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", { description: "top-up" });
213
214
  const onCreditsExhausted = vi.fn();
214
215
  await runRuntimeDeductions({
215
216
  ledger,
@@ -223,7 +224,7 @@ describe("runRuntimeDeductions", () => {
223
224
 
224
225
  it("suspends tenant when full deduction causes balance to drop to exactly 0", async () => {
225
226
  // Balance = exactly 1 bot * DAILY_BOT_COST = 17 cents → full deduction → 0
226
- await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", "top-up");
227
+ await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", { description: "top-up" });
227
228
  const onSuspend = vi.fn();
228
229
  const onCreditsExhausted = vi.fn();
229
230
  const result = await runRuntimeDeductions({
@@ -240,7 +241,7 @@ describe("runRuntimeDeductions", () => {
240
241
  });
241
242
 
242
243
  it("fires onCreditsExhausted on partial deduction when balance hits 0", async () => {
243
- await ledger.credit("tenant-1", Credit.fromCents(10), "purchase", "top-up");
244
+ await ledger.credit("tenant-1", Credit.fromCents(10), "purchase", { description: "top-up" });
244
245
  const onCreditsExhausted = vi.fn();
245
246
  await runRuntimeDeductions({
246
247
  ledger,
@@ -253,7 +254,7 @@ describe("runRuntimeDeductions", () => {
253
254
  });
254
255
 
255
256
  it("partially debits resource tier surcharge when balance is positive but insufficient", async () => {
256
- await ledger.credit("tenant-1", Credit.fromCents(30), "purchase", "top-up");
257
+ await ledger.credit("tenant-1", Credit.fromCents(30), "purchase", { description: "top-up" });
257
258
  const result = await runRuntimeDeductions({
258
259
  ledger,
259
260
  date: TODAY,
@@ -265,7 +266,7 @@ describe("runRuntimeDeductions", () => {
265
266
  });
266
267
 
267
268
  it("skips resource tier partial debit when balance is exactly 0 after runtime", async () => {
268
- await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", "top-up");
269
+ await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", { description: "top-up" });
269
270
  const onCreditsExhausted = vi.fn();
270
271
  const result = await runRuntimeDeductions({
271
272
  ledger,
@@ -284,7 +285,7 @@ describe("runRuntimeDeductions", () => {
284
285
  // triggering the zero-crossing suspend in the runtime block.
285
286
  // Storage cost (5 cents) then tries to suspend again via its else-branch (balance 0 < 5).
286
287
  // The !result.suspended.includes(tenantId) guard must prevent onSuspend being called twice.
287
- await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", "top-up");
288
+ await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", { description: "top-up" });
288
289
  const onSuspend = vi.fn();
289
290
  const result = await runRuntimeDeductions({
290
291
  ledger,
@@ -301,7 +302,7 @@ describe("runRuntimeDeductions", () => {
301
302
  it("buildResourceTierCosts: deducts pro tier surcharge via getResourceTierCosts", async () => {
302
303
  const proTierCost = RESOURCE_TIERS.pro.dailyCost.toCents();
303
304
  const startBalance = 17 + proTierCost + 10;
304
- await ledger.credit("tenant-1", Credit.fromCents(startBalance), "purchase", "top-up");
305
+ await ledger.credit("tenant-1", Credit.fromCents(startBalance), "purchase", { description: "top-up" });
305
306
 
306
307
  const mockRepo = {
307
308
  getResourceTier: async (_botId: string): Promise<string | null> => "pro",
@@ -324,7 +325,7 @@ describe("runRuntimeDeductions", () => {
324
325
  });
325
326
 
326
327
  it("treats unique constraint violation from concurrent debit as already-billed (skip, not error)", async () => {
327
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
328
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
328
329
  const uniqueErr = Object.assign(new Error("duplicate key value violates unique constraint"), { code: "23505" });
329
330
  vi.spyOn(ledger, "debit").mockRejectedValueOnce(uniqueErr);
330
331
  const result = await runRuntimeDeductions({
@@ -338,7 +339,7 @@ describe("runRuntimeDeductions", () => {
338
339
  });
339
340
 
340
341
  it("is idempotent — second run on same date does not double-deduct", async () => {
341
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
342
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
342
343
  const cfg = {
343
344
  ledger,
344
345
  getActiveBotCount: async () => 1,