@wopr-network/platform-core 1.13.2 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/dist/api/routes/admin-credits.d.ts +2 -2
  2. package/dist/api/routes/admin-credits.js +9 -4
  3. package/dist/api/routes/quota.d.ts +2 -2
  4. package/dist/api/routes/verify-email.d.ts +3 -3
  5. package/dist/backup/on-demand-snapshot-service.d.ts +2 -2
  6. package/dist/billing/payram/webhook.d.ts +3 -3
  7. package/dist/billing/payram/webhook.js +5 -1
  8. package/dist/billing/payram/webhook.test.js +5 -4
  9. package/dist/billing/stripe/stripe-payment-processor.d.ts +2 -2
  10. package/dist/billing/stripe/stripe-payment-processor.test.js +7 -0
  11. package/dist/billing/stripe/tenant-store.d.ts +1 -1
  12. package/dist/billing/stripe/tenant-store.js +1 -1
  13. package/dist/credits/auto-topup-charge.d.ts +2 -2
  14. package/dist/credits/auto-topup-charge.js +5 -1
  15. package/dist/credits/auto-topup-charge.test.js +5 -4
  16. package/dist/credits/auto-topup-usage.d.ts +2 -2
  17. package/dist/credits/auto-topup-usage.test.js +53 -12
  18. package/dist/credits/credit-expiry-cron.d.ts +2 -2
  19. package/dist/credits/credit-expiry-cron.js +7 -4
  20. package/dist/credits/credit-expiry-cron.test.js +25 -8
  21. package/dist/credits/credit-ledger.d.ts +2 -2
  22. package/dist/credits/credit-ledger.js +1 -1
  23. package/dist/credits/dividend-cron.d.ts +4 -6
  24. package/dist/credits/dividend-cron.js +10 -16
  25. package/dist/credits/dividend-cron.test.js +31 -44
  26. package/dist/credits/dividend-repository.js +19 -22
  27. package/dist/credits/dividend-repository.test.js +4 -3
  28. package/dist/credits/index.d.ts +4 -2
  29. package/dist/credits/index.js +2 -1
  30. package/dist/credits/ledger.d.ts +195 -0
  31. package/dist/credits/ledger.js +561 -0
  32. package/dist/credits/ledger.test.js +418 -0
  33. package/dist/credits/signup-grant.d.ts +2 -2
  34. package/dist/credits/signup-grant.js +4 -4
  35. package/dist/credits/signup-grant.test.js +5 -3
  36. package/dist/credits/trial-balance-cron.d.ts +19 -0
  37. package/dist/credits/trial-balance-cron.js +30 -0
  38. package/dist/credits/trial-balance-cron.test.js +55 -0
  39. package/dist/db/schema/gateway-service-keys.d.ts +109 -0
  40. package/dist/db/schema/gateway-service-keys.js +18 -0
  41. package/dist/db/schema/index.d.ts +2 -0
  42. package/dist/db/schema/index.js +2 -0
  43. package/dist/db/schema/ledger.d.ts +442 -0
  44. package/dist/db/schema/ledger.js +76 -0
  45. package/dist/gateway/credit-gate.d.ts +2 -2
  46. package/dist/gateway/credit-gate.js +5 -1
  47. package/dist/gateway/credit-gate.test.js +35 -33
  48. package/dist/gateway/gateway-routes.test.js +1 -1
  49. package/dist/gateway/index.d.ts +2 -0
  50. package/dist/gateway/index.js +1 -0
  51. package/dist/gateway/protocol/anthropic.js +1 -1
  52. package/dist/gateway/protocol/deps.d.ts +5 -5
  53. package/dist/gateway/protocol/openai.js +1 -1
  54. package/dist/gateway/proxy.d.ts +4 -4
  55. package/dist/gateway/route-mounting.test.js +1 -1
  56. package/dist/gateway/service-key-auth.d.ts +1 -1
  57. package/dist/gateway/service-key-auth.js +1 -1
  58. package/dist/gateway/service-key-repository.d.ts +27 -0
  59. package/dist/gateway/service-key-repository.js +64 -0
  60. package/dist/gateway/types.d.ts +5 -5
  61. package/dist/metering/reconciliation-cron.test.js +9 -8
  62. package/dist/metering/reconciliation-repository.js +12 -10
  63. package/dist/metering/reconciliation-repository.test.js +9 -8
  64. package/dist/monetization/affiliate/affiliate-admin-repository.js +10 -8
  65. package/dist/monetization/affiliate/affiliate-admin-repository.test.js +32 -13
  66. package/dist/monetization/affiliate/credit-match.d.ts +2 -2
  67. package/dist/monetization/affiliate/credit-match.js +4 -1
  68. package/dist/monetization/affiliate/credit-match.test.js +58 -13
  69. package/dist/monetization/affiliate/new-user-bonus.d.ts +2 -2
  70. package/dist/monetization/affiliate/new-user-bonus.js +4 -1
  71. package/dist/monetization/affiliate/new-user-bonus.test.js +4 -3
  72. package/dist/monetization/credits/auto-topup-charge.d.ts +2 -2
  73. package/dist/monetization/credits/auto-topup-charge.js +5 -1
  74. package/dist/monetization/credits/auto-topup-charge.test.js +5 -4
  75. package/dist/monetization/credits/auto-topup-usage.d.ts +2 -2
  76. package/dist/monetization/credits/auto-topup-usage.test.js +53 -12
  77. package/dist/monetization/credits/bot-billing.d.ts +3 -3
  78. package/dist/monetization/credits/bot-billing.test.js +18 -5
  79. package/dist/monetization/credits/credit-expiry-cron.test.js +25 -8
  80. package/dist/monetization/credits/dividend-cron.d.ts +2 -4
  81. package/dist/monetization/credits/dividend-cron.js +7 -4
  82. package/dist/monetization/credits/dividend-cron.test.js +26 -46
  83. package/dist/monetization/credits/dividend-repository.js +15 -24
  84. package/dist/monetization/credits/dividend-repository.test.js +4 -3
  85. package/dist/monetization/credits/index.d.ts +2 -2
  86. package/dist/monetization/credits/index.js +1 -1
  87. package/dist/monetization/credits/member-usage.test.js +23 -10
  88. package/dist/monetization/credits/phone-billing.d.ts +2 -2
  89. package/dist/monetization/credits/phone-billing.js +5 -1
  90. package/dist/monetization/credits/phone-billing.test.js +9 -12
  91. package/dist/monetization/credits/runtime-cron.d.ts +2 -2
  92. package/dist/monetization/credits/runtime-cron.js +32 -8
  93. package/dist/monetization/credits/runtime-cron.test.js +28 -27
  94. package/dist/monetization/credits/runtime-scheduler.d.ts +2 -2
  95. package/dist/monetization/credits/runtime-scheduler.test.js +1 -1
  96. package/dist/monetization/credits/signup-grant.test.js +5 -3
  97. package/dist/monetization/credits/storage-tier-cron.test.js +3 -2
  98. package/dist/monetization/credits/trial-balance-cron.test.js +42 -0
  99. package/dist/monetization/feature-gate.d.ts +3 -3
  100. package/dist/monetization/index.d.ts +3 -3
  101. package/dist/monetization/index.js +1 -1
  102. package/dist/monetization/metering/reconciliation-cron.test.js +9 -8
  103. package/dist/monetization/metering/reconciliation-repository.js +11 -10
  104. package/dist/monetization/metering/reconciliation-repository.test.js +9 -8
  105. package/dist/monetization/payram/webhook.d.ts +2 -2
  106. package/dist/monetization/payram/webhook.js +5 -1
  107. package/dist/monetization/payram/webhook.test.js +5 -4
  108. package/dist/monetization/promotions/engine.d.ts +2 -2
  109. package/dist/monetization/promotions/engine.js +4 -1
  110. package/dist/monetization/promotions/engine.test.js +3 -1
  111. package/dist/monetization/repository-types.d.ts +1 -1
  112. package/dist/monetization/socket/socket.d.ts +3 -3
  113. package/dist/monetization/stripe/stripe-payment-processor.d.ts +2 -2
  114. package/dist/monetization/stripe/stripe-payment-processor.test.js +7 -0
  115. package/dist/monetization/stripe/webhook.d.ts +2 -2
  116. package/dist/monetization/stripe/webhook.js +70 -6
  117. package/dist/monetization/stripe/webhook.test.js +20 -15
  118. package/dist/onboarding/onboarding-service.d.ts +2 -2
  119. package/dist/onboarding/onboarding-service.js +6 -2
  120. package/drizzle/migrations/0002_gateway_service_keys.sql +14 -0
  121. package/drizzle/migrations/0003_double_entry_ledger.sql +82 -0
  122. package/drizzle/migrations/meta/_journal.json +14 -0
  123. package/package.json +1 -1
  124. package/src/api/routes/admin-credits.ts +11 -14
  125. package/src/api/routes/quota.ts +2 -2
  126. package/src/api/routes/verify-email.ts +4 -4
  127. package/src/backup/on-demand-snapshot-service.test.ts +3 -3
  128. package/src/backup/on-demand-snapshot-service.ts +3 -3
  129. package/src/billing/payram/webhook.test.ts +7 -5
  130. package/src/billing/payram/webhook.ts +8 -11
  131. package/src/billing/stripe/stripe-payment-processor.test.ts +10 -3
  132. package/src/billing/stripe/stripe-payment-processor.ts +3 -3
  133. package/src/billing/stripe/tenant-store.ts +1 -1
  134. package/src/credits/auto-topup-charge.test.ts +7 -5
  135. package/src/credits/auto-topup-charge.ts +7 -10
  136. package/src/credits/auto-topup-usage.test.ts +55 -13
  137. package/src/credits/auto-topup-usage.ts +2 -2
  138. package/src/credits/credit-expiry-cron.test.ts +26 -45
  139. package/src/credits/credit-expiry-cron.ts +9 -12
  140. package/src/credits/credit-ledger.ts +3 -3
  141. package/src/credits/dividend-cron.test.ts +38 -45
  142. package/src/credits/dividend-cron.ts +12 -26
  143. package/src/credits/dividend-repository.test.ts +4 -3
  144. package/src/credits/dividend-repository.ts +21 -23
  145. package/src/credits/index.ts +23 -4
  146. package/src/credits/ledger.test.ts +514 -0
  147. package/src/credits/ledger.ts +851 -0
  148. package/src/credits/signup-grant.test.ts +7 -4
  149. package/src/credits/signup-grant.ts +6 -12
  150. package/src/credits/trial-balance-cron.test.ts +68 -0
  151. package/src/credits/trial-balance-cron.ts +46 -0
  152. package/src/db/schema/gateway-service-keys.ts +23 -0
  153. package/src/db/schema/index.ts +2 -0
  154. package/src/db/schema/ledger.ts +94 -0
  155. package/src/gateway/credit-gate-wiring.test.ts +3 -3
  156. package/src/gateway/credit-gate.test.ts +35 -33
  157. package/src/gateway/credit-gate.ts +6 -10
  158. package/src/gateway/gateway-routes.test.ts +6 -6
  159. package/src/gateway/index.ts +2 -0
  160. package/src/gateway/protocol/anthropic.ts +2 -2
  161. package/src/gateway/protocol/deps.ts +5 -5
  162. package/src/gateway/protocol/openai.ts +2 -2
  163. package/src/gateway/proxy.ts +4 -4
  164. package/src/gateway/route-mounting.test.ts +3 -3
  165. package/src/gateway/service-key-auth.ts +4 -2
  166. package/src/gateway/service-key-repository.ts +87 -0
  167. package/src/gateway/types.ts +5 -5
  168. package/src/metering/reconciliation-cron.test.ts +10 -9
  169. package/src/metering/reconciliation-repository.test.ts +10 -9
  170. package/src/metering/reconciliation-repository.ts +14 -11
  171. package/src/monetization/affiliate/affiliate-admin-repository.test.ts +32 -19
  172. package/src/monetization/affiliate/affiliate-admin-repository.ts +16 -8
  173. package/src/monetization/affiliate/credit-match.test.ts +60 -14
  174. package/src/monetization/affiliate/credit-match.ts +6 -9
  175. package/src/monetization/affiliate/new-user-bonus.test.ts +6 -4
  176. package/src/monetization/affiliate/new-user-bonus.ts +6 -9
  177. package/src/monetization/credits/auto-topup-charge.test.ts +7 -5
  178. package/src/monetization/credits/auto-topup-charge.ts +7 -10
  179. package/src/monetization/credits/auto-topup-usage.test.ts +55 -13
  180. package/src/monetization/credits/auto-topup-usage.ts +2 -2
  181. package/src/monetization/credits/bot-billing.test.ts +20 -6
  182. package/src/monetization/credits/bot-billing.ts +3 -3
  183. package/src/monetization/credits/credit-expiry-cron.test.ts +26 -45
  184. package/src/monetization/credits/dividend-cron.test.ts +34 -48
  185. package/src/monetization/credits/dividend-cron.ts +9 -14
  186. package/src/monetization/credits/dividend-repository.test.ts +4 -3
  187. package/src/monetization/credits/dividend-repository.ts +19 -25
  188. package/src/monetization/credits/index.ts +4 -4
  189. package/src/monetization/credits/member-usage.test.ts +25 -11
  190. package/src/monetization/credits/phone-billing.test.ts +18 -26
  191. package/src/monetization/credits/phone-billing.ts +7 -10
  192. package/src/monetization/credits/runtime-cron.test.ts +29 -28
  193. package/src/monetization/credits/runtime-cron.ts +34 -58
  194. package/src/monetization/credits/runtime-scheduler.test.ts +1 -1
  195. package/src/monetization/credits/runtime-scheduler.ts +2 -2
  196. package/src/monetization/credits/signup-grant.test.ts +7 -4
  197. package/src/monetization/credits/storage-tier-cron.test.ts +5 -3
  198. package/src/monetization/credits/trial-balance-cron.test.ts +52 -0
  199. package/src/monetization/feature-gate.ts +3 -3
  200. package/src/monetization/index.ts +4 -4
  201. package/src/monetization/metering/reconciliation-cron.test.ts +10 -9
  202. package/src/monetization/metering/reconciliation-repository.test.ts +11 -9
  203. package/src/monetization/metering/reconciliation-repository.ts +13 -11
  204. package/src/monetization/payram/webhook.test.ts +7 -5
  205. package/src/monetization/payram/webhook.ts +7 -10
  206. package/src/monetization/promotions/engine.test.ts +6 -5
  207. package/src/monetization/promotions/engine.ts +6 -3
  208. package/src/monetization/repository-types.ts +1 -1
  209. package/src/monetization/socket/socket.ts +4 -4
  210. package/src/monetization/stripe/stripe-payment-processor.test.ts +10 -3
  211. package/src/monetization/stripe/stripe-payment-processor.ts +3 -3
  212. package/src/monetization/stripe/webhook.test.ts +22 -16
  213. package/src/monetization/stripe/webhook.ts +75 -50
  214. package/src/onboarding/onboarding-service.ts +8 -11
  215. package/dist/credits/credit-ledger-extra.test.js +0 -40
  216. package/dist/credits/credit-ledger.bench.js +0 -33
  217. package/dist/credits/credit-ledger.test.d.ts +0 -4
  218. package/dist/credits/credit-ledger.test.js +0 -203
  219. package/dist/credits/credit-transaction-repository.test.js +0 -232
  220. package/dist/monetization/credits/credit-ledger-extra.test.d.ts +0 -1
  221. package/dist/monetization/credits/credit-ledger-extra.test.js +0 -39
  222. package/dist/monetization/credits/credit-ledger.bench.d.ts +0 -1
  223. package/dist/monetization/credits/credit-ledger.bench.js +0 -32
  224. package/dist/monetization/credits/credit-ledger.test.d.ts +0 -4
  225. package/dist/monetization/credits/credit-ledger.test.js +0 -202
  226. package/dist/monetization/credits/credit-transaction-repository.test.d.ts +0 -1
  227. package/dist/monetization/credits/credit-transaction-repository.test.js +0 -232
  228. package/src/credits/credit-ledger-extra.test.ts +0 -57
  229. package/src/credits/credit-ledger.bench.ts +0 -56
  230. package/src/credits/credit-ledger.test.ts +0 -276
  231. package/src/credits/credit-transaction-repository.test.ts +0 -274
  232. package/src/monetization/credits/credit-ledger-extra.test.ts +0 -56
  233. package/src/monetization/credits/credit-ledger.bench.ts +0 -55
  234. package/src/monetization/credits/credit-ledger.test.ts +0 -275
  235. package/src/monetization/credits/credit-transaction-repository.test.ts +0 -274
  236. /package/dist/credits/{credit-ledger-extra.test.d.ts → ledger.test.d.ts} +0 -0
  237. /package/dist/credits/{credit-ledger.bench.d.ts → trial-balance-cron.test.d.ts} +0 -0
  238. /package/dist/{credits/credit-transaction-repository.test.d.ts → monetization/credits/trial-balance-cron.test.d.ts} +0 -0
@@ -1,232 +0,0 @@
1
- import crypto from "node:crypto";
2
- import { Credit } from "@wopr-network/platform-core/credits";
3
- import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
4
- import { creditTransactions } from "../../db/schema/credits.js";
5
- import { beginTestTransaction, createTestDb, endTestTransaction, rollbackTestTransaction } from "../../test/db.js";
6
- import { DrizzleCreditTransactionRepository } from "./credit-transaction-repository.js";
7
- let pool;
8
- let db;
9
- beforeAll(async () => {
10
- ({ db, pool } = await createTestDb());
11
- await beginTestTransaction(pool);
12
- });
13
- afterAll(async () => {
14
- await endTestTransaction(pool);
15
- await pool.close();
16
- });
17
- /** Seed a credit_transactions row directly. */
18
- async function seedTx(opts) {
19
- await db.insert(creditTransactions).values({
20
- id: crypto.randomUUID(),
21
- tenantId: opts.tenantId,
22
- amount: opts.amount,
23
- balanceAfter: opts.balanceAfter ?? opts.amount,
24
- type: opts.type,
25
- description: "test",
26
- referenceId: opts.referenceId ?? null,
27
- createdAt: opts.createdAt ?? new Date().toISOString(),
28
- });
29
- }
30
- describe("DrizzleCreditTransactionRepository", () => {
31
- let repo;
32
- beforeEach(async () => {
33
- await rollbackTestTransaction(pool);
34
- repo = new DrizzleCreditTransactionRepository(db);
35
- });
36
- describe("existsByReferenceIdLike()", () => {
37
- it("returns false when no transactions exist", async () => {
38
- const result = await repo.existsByReferenceIdLike("div-%");
39
- expect(result).toBe(false);
40
- });
41
- it("returns true when a matching referenceId exists", async () => {
42
- await seedTx({
43
- tenantId: "t1",
44
- amount: Credit.fromCents(100),
45
- type: "purchase",
46
- referenceId: "div-2026-01-01",
47
- });
48
- const result = await repo.existsByReferenceIdLike("div-%");
49
- expect(result).toBe(true);
50
- });
51
- it("returns false when no referenceId matches the pattern", async () => {
52
- await seedTx({
53
- tenantId: "t1",
54
- amount: Credit.fromCents(100),
55
- type: "purchase",
56
- referenceId: "purchase-abc",
57
- });
58
- const result = await repo.existsByReferenceIdLike("div-%");
59
- expect(result).toBe(false);
60
- });
61
- it("matches partial patterns with wildcards", async () => {
62
- await seedTx({
63
- tenantId: "t1",
64
- amount: Credit.fromCents(50),
65
- type: "community_dividend",
66
- referenceId: "div-2026-02-15-t1",
67
- });
68
- expect(await repo.existsByReferenceIdLike("div-2026-02-%")).toBe(true);
69
- expect(await repo.existsByReferenceIdLike("div-2026-03-%")).toBe(false);
70
- });
71
- });
72
- describe("sumPurchasesForPeriod()", () => {
73
- it("returns Credit.ZERO when no transactions exist", async () => {
74
- const sum = await repo.sumPurchasesForPeriod("2026-01-01T00:00:00Z", "2026-02-01T00:00:00Z");
75
- expect(sum.toRaw()).toBe(0);
76
- });
77
- it("sums only purchase-type transactions", async () => {
78
- await seedTx({
79
- tenantId: "t1",
80
- amount: Credit.fromCents(100),
81
- type: "purchase",
82
- createdAt: "2026-01-15T12:00:00Z",
83
- });
84
- await seedTx({
85
- tenantId: "t1",
86
- amount: Credit.fromCents(200),
87
- type: "signup_grant",
88
- createdAt: "2026-01-15T12:00:00Z",
89
- });
90
- const sum = await repo.sumPurchasesForPeriod("2026-01-01T00:00:00Z", "2026-02-01T00:00:00Z");
91
- expect(sum.toRaw()).toBe(Credit.fromCents(100).toRaw());
92
- });
93
- it("respects half-open interval [start, end)", async () => {
94
- // Exactly at start — included
95
- await seedTx({
96
- tenantId: "t1",
97
- amount: Credit.fromCents(10),
98
- type: "purchase",
99
- createdAt: "2026-01-01T00:00:00Z",
100
- });
101
- // Inside window
102
- await seedTx({
103
- tenantId: "t1",
104
- amount: Credit.fromCents(20),
105
- type: "purchase",
106
- createdAt: "2026-01-15T00:00:00Z",
107
- });
108
- // Exactly at end — excluded
109
- await seedTx({
110
- tenantId: "t1",
111
- amount: Credit.fromCents(40),
112
- type: "purchase",
113
- createdAt: "2026-02-01T00:00:00Z",
114
- });
115
- const sum = await repo.sumPurchasesForPeriod("2026-01-01T00:00:00Z", "2026-02-01T00:00:00Z");
116
- expect(sum.toRaw()).toBe(Credit.fromCents(30).toRaw()); // 10 + 20, not 40
117
- });
118
- it("sums across all tenants (not tenant-scoped)", async () => {
119
- await seedTx({
120
- tenantId: "t1",
121
- amount: Credit.fromCents(50),
122
- type: "purchase",
123
- createdAt: "2026-01-10T00:00:00Z",
124
- });
125
- await seedTx({
126
- tenantId: "t2",
127
- amount: Credit.fromCents(75),
128
- type: "purchase",
129
- createdAt: "2026-01-10T00:00:00Z",
130
- });
131
- const sum = await repo.sumPurchasesForPeriod("2026-01-01T00:00:00Z", "2026-02-01T00:00:00Z");
132
- expect(sum.toRaw()).toBe(Credit.fromCents(125).toRaw()); // 50 + 75
133
- });
134
- });
135
- describe("getActiveTenantIdsInWindow()", () => {
136
- it("returns empty array when no transactions exist", async () => {
137
- const ids = await repo.getActiveTenantIdsInWindow("2026-01-01T00:00:00Z", "2026-02-01T00:00:00Z");
138
- expect(ids).toEqual([]);
139
- });
140
- it("returns distinct tenantIds with purchase transactions in window", async () => {
141
- await seedTx({
142
- tenantId: "t1",
143
- amount: Credit.fromCents(10),
144
- type: "purchase",
145
- createdAt: "2026-01-10T00:00:00Z",
146
- });
147
- // t1 again — should not duplicate
148
- await seedTx({
149
- tenantId: "t1",
150
- amount: Credit.fromCents(20),
151
- type: "purchase",
152
- createdAt: "2026-01-11T00:00:00Z",
153
- });
154
- await seedTx({
155
- tenantId: "t2",
156
- amount: Credit.fromCents(30),
157
- type: "purchase",
158
- createdAt: "2026-01-12T00:00:00Z",
159
- });
160
- const ids = await repo.getActiveTenantIdsInWindow("2026-01-01T00:00:00Z", "2026-02-01T00:00:00Z");
161
- expect(ids.sort()).toEqual(["t1", "t2"]);
162
- });
163
- it("excludes non-purchase transaction types", async () => {
164
- await seedTx({
165
- tenantId: "t1",
166
- amount: Credit.fromCents(100),
167
- type: "signup_grant",
168
- createdAt: "2026-01-10T00:00:00Z",
169
- });
170
- const ids = await repo.getActiveTenantIdsInWindow("2026-01-01T00:00:00Z", "2026-02-01T00:00:00Z");
171
- expect(ids).toEqual([]);
172
- });
173
- it("respects half-open interval [start, end)", async () => {
174
- // Before window
175
- await seedTx({
176
- tenantId: "t-before",
177
- amount: Credit.fromCents(10),
178
- type: "purchase",
179
- createdAt: "2025-12-31T23:59:59Z",
180
- });
181
- // At start — included
182
- await seedTx({
183
- tenantId: "t-start",
184
- amount: Credit.fromCents(10),
185
- type: "purchase",
186
- createdAt: "2026-01-01T00:00:00Z",
187
- });
188
- // At end — excluded
189
- await seedTx({
190
- tenantId: "t-end",
191
- amount: Credit.fromCents(10),
192
- type: "purchase",
193
- createdAt: "2026-02-01T00:00:00Z",
194
- });
195
- const ids = await repo.getActiveTenantIdsInWindow("2026-01-01T00:00:00Z", "2026-02-01T00:00:00Z");
196
- expect(ids).toEqual(["t-start"]);
197
- });
198
- });
199
- describe("referenceId uniqueness", () => {
200
- it("rejects duplicate referenceId (database unique constraint)", async () => {
201
- await seedTx({
202
- tenantId: "t1",
203
- amount: Credit.fromCents(100),
204
- type: "purchase",
205
- referenceId: "unique-ref-1",
206
- });
207
- await expect(seedTx({
208
- tenantId: "t1",
209
- amount: Credit.fromCents(200),
210
- type: "purchase",
211
- referenceId: "unique-ref-1",
212
- })).rejects.toThrow(); // PG unique constraint violation
213
- });
214
- it("allows null referenceId on multiple rows", async () => {
215
- await seedTx({
216
- tenantId: "t1",
217
- amount: Credit.fromCents(100),
218
- type: "purchase",
219
- referenceId: undefined, // null
220
- });
221
- await seedTx({
222
- tenantId: "t1",
223
- amount: Credit.fromCents(200),
224
- type: "purchase",
225
- referenceId: undefined, // null
226
- });
227
- // Both inserted — no constraint violation for nulls
228
- const result = await repo.existsByReferenceIdLike("%");
229
- expect(result).toBe(false); // LIKE '%' won't match null referenceIds
230
- });
231
- });
232
- });
@@ -1,57 +0,0 @@
1
- /**
2
- * Additional CreditLedger tests — concurrent debit safety.
3
- * memberUsage() tests live in member-usage.test.ts.
4
- */
5
- import type { PGlite } from "@electric-sql/pglite";
6
- import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
7
- import type { PlatformDb } from "../db/index.js";
8
- import { beginTestTransaction, createTestDb, endTestTransaction, rollbackTestTransaction } from "../test/db.js";
9
- import { Credit } from "./credit.js";
10
- import { CreditLedger, InsufficientBalanceError } from "./credit-ledger.js";
11
-
12
- let pool: PGlite;
13
- let db: PlatformDb;
14
-
15
- beforeAll(async () => {
16
- ({ db, pool } = await createTestDb());
17
- await beginTestTransaction(pool);
18
- });
19
-
20
- afterAll(async () => {
21
- await endTestTransaction(pool);
22
- await pool.close();
23
- });
24
-
25
- describe("CreditLedger concurrent debit safety", () => {
26
- let ledger: CreditLedger;
27
-
28
- beforeEach(async () => {
29
- await rollbackTestTransaction(pool);
30
- ledger = new CreditLedger(db);
31
- });
32
-
33
- it("concurrent debits do not overdraw — at least one should fail with InsufficientBalanceError", async () => {
34
- // Fund with exactly 100 cents
35
- await ledger.credit("t1", Credit.fromCents(100), "purchase");
36
-
37
- // Fire two 100-cent debits concurrently — only one can succeed
38
- const results = await Promise.allSettled([
39
- ledger.debit("t1", Credit.fromCents(100), "bot_runtime", "debit-1"),
40
- ledger.debit("t1", Credit.fromCents(100), "bot_runtime", "debit-2"),
41
- ]);
42
-
43
- const fulfilled = results.filter((r) => r.status === "fulfilled");
44
- const rejected = results.filter((r) => r.status === "rejected");
45
-
46
- // Exactly one succeeds, one fails (PGlite serializes transactions so this is deterministic)
47
- expect(fulfilled).toHaveLength(1);
48
- expect(rejected).toHaveLength(1);
49
-
50
- const err = (rejected[0] as PromiseRejectedResult).reason;
51
- expect(err).toBeInstanceOf(InsufficientBalanceError);
52
-
53
- // Final balance must be exactly 0, not negative
54
- const bal = await ledger.balance("t1");
55
- expect(bal.toCents()).toBe(0);
56
- });
57
- });
@@ -1,56 +0,0 @@
1
- import type { PGlite } from "@electric-sql/pglite";
2
- import { afterAll, beforeAll, beforeEach, bench, describe } from "vitest";
3
- import type { PlatformDb } from "../db/index.js";
4
- import { createTestDb, truncateAllTables } from "../test/db.js";
5
- import { Credit } from "./credit.js";
6
- import { CreditLedger } from "./credit-ledger.js";
7
-
8
- let db: PlatformDb;
9
- let pool: PGlite;
10
-
11
- beforeAll(async () => {
12
- ({ db, pool } = await createTestDb());
13
- });
14
-
15
- afterAll(async () => {
16
- await pool.close();
17
- });
18
-
19
- describe("CreditLedger throughput", () => {
20
- let ledger: CreditLedger;
21
-
22
- beforeEach(async () => {
23
- await truncateAllTables(pool);
24
- ledger = new CreditLedger(db);
25
- });
26
-
27
- let creditIdx = 0;
28
- let debitIdx = 0;
29
-
30
- bench(
31
- "credit operation",
32
- async () => {
33
- const tenant = `tenant-${creditIdx++ % 100}`;
34
- await ledger.credit(tenant, Credit.fromCents(100), "purchase", "bench");
35
- },
36
- { iterations: 1_000 },
37
- );
38
-
39
- bench(
40
- "debit operation",
41
- async () => {
42
- const tenant = `tenant-${debitIdx++ % 100}`;
43
- await ledger.debit(tenant, Credit.fromCents(1), "adapter_usage", "bench");
44
- },
45
- { iterations: 1_000 },
46
- );
47
-
48
- bench(
49
- "balance query",
50
- async () => {
51
- const tenant = `tenant-${debitIdx++ % 100}`;
52
- await ledger.balance(tenant);
53
- },
54
- { iterations: 5_000 },
55
- );
56
- });
@@ -1,276 +0,0 @@
1
- /**
2
- * Tests for CreditLedger — including the allowNegative debit parameter (WOP-821).
3
- */
4
-
5
- import type { PGlite } from "@electric-sql/pglite";
6
- import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
7
- import type { PlatformDb } from "../db/index.js";
8
- import { createTestDb, truncateAllTables } from "../test/db.js";
9
- import { Credit } from "./credit.js";
10
- import { CreditLedger, InsufficientBalanceError } from "./credit-ledger.js";
11
-
12
- // TOP OF FILE - shared across ALL describes
13
- let pool: PGlite;
14
- let db: PlatformDb;
15
-
16
- beforeAll(async () => {
17
- ({ db, pool } = await createTestDb());
18
- });
19
-
20
- afterAll(async () => {
21
- await pool.close();
22
- });
23
-
24
- describe("CreditLedger core methods", () => {
25
- let ledger: CreditLedger;
26
-
27
- beforeEach(async () => {
28
- await truncateAllTables(pool);
29
- ledger = new CreditLedger(db);
30
- });
31
-
32
- // --- credit() ---
33
-
34
- describe("credit()", () => {
35
- it("happy path: credits a tenant and returns correct transaction fields", async () => {
36
- const txn = await ledger.credit(
37
- "t1",
38
- Credit.fromCents(100),
39
- "purchase",
40
- "Initial deposit",
41
- "ref-001",
42
- "stripe",
43
- "user-abc",
44
- );
45
-
46
- expect(txn.tenantId).toBe("t1");
47
- expect(txn.amount.toCents()).toBe(100);
48
- expect(txn.balanceAfter.toCents()).toBe(100);
49
- expect(txn.type).toBe("purchase");
50
- expect(txn.description).toBe("Initial deposit");
51
- expect(txn.referenceId).toBe("ref-001");
52
- expect(txn.fundingSource).toBe("stripe");
53
- expect(txn.attributedUserId).toBe("user-abc");
54
- expect(txn.id).toEqual(expect.any(String));
55
- expect(txn.createdAt).toEqual(expect.any(String));
56
- });
57
-
58
- it("multiple credits accumulate balance correctly", async () => {
59
- await ledger.credit("t1", Credit.fromCents(100), "purchase");
60
- await ledger.credit("t1", Credit.fromCents(50), "promo");
61
-
62
- const bal = await ledger.balance("t1");
63
- expect(bal.toCents()).toBe(150);
64
- });
65
-
66
- it("rejects zero amount", async () => {
67
- await expect(ledger.credit("t1", Credit.fromCents(0), "purchase")).rejects.toThrow(
68
- "amount must be positive for credits",
69
- );
70
- });
71
-
72
- it("rejects negative amount", async () => {
73
- await expect(ledger.credit("t1", Credit.fromRaw(-1), "purchase")).rejects.toThrow(
74
- "amount must be positive for credits",
75
- );
76
- });
77
-
78
- it("optional fields default to null", async () => {
79
- const txn = await ledger.credit("t1", Credit.fromCents(10), "signup_grant");
80
-
81
- expect(txn.description).toBeNull();
82
- expect(txn.referenceId).toBeNull();
83
- expect(txn.fundingSource).toBeNull();
84
- expect(txn.attributedUserId).toBeNull();
85
- });
86
- });
87
-
88
- // --- balance() ---
89
-
90
- describe("balance()", () => {
91
- it("returns Credit.ZERO for a tenant with no transactions", async () => {
92
- const bal = await ledger.balance("nonexistent");
93
- expect(bal.toCents()).toBe(0);
94
- expect(bal.isZero()).toBe(true);
95
- });
96
-
97
- it("reflects credits and debits accurately", async () => {
98
- await ledger.credit("t1", Credit.fromCents(200), "purchase");
99
- await ledger.debit("t1", Credit.fromCents(50), "bot_runtime");
100
-
101
- const bal = await ledger.balance("t1");
102
- expect(bal.toCents()).toBe(150);
103
- });
104
- });
105
-
106
- // --- history() ---
107
-
108
- describe("history()", () => {
109
- it("returns transactions in reverse chronological order (newest first)", async () => {
110
- await ledger.credit("t1", Credit.fromCents(100), "purchase", "first");
111
- await ledger.credit("t1", Credit.fromCents(200), "promo", "second");
112
- await ledger.debit("t1", Credit.fromCents(50), "bot_runtime", "third");
113
-
114
- const hist = await ledger.history("t1");
115
-
116
- expect(hist).toHaveLength(3);
117
- // newest first
118
- expect(hist[0].description).toBe("third");
119
- expect(hist[1].description).toBe("second");
120
- expect(hist[2].description).toBe("first");
121
- });
122
-
123
- it("all CreditTransaction fields are populated", async () => {
124
- await ledger.credit("t1", Credit.fromCents(100), "purchase", "desc", "ref-1", "stripe", "user-1");
125
-
126
- const hist = await ledger.history("t1");
127
- expect(hist).toHaveLength(1);
128
-
129
- const txn = hist[0];
130
- expect(txn.id).toEqual(expect.any(String));
131
- expect(txn.tenantId).toBe("t1");
132
- expect(txn.amount.toCents()).toBe(100);
133
- expect(txn.balanceAfter.toCents()).toBe(100);
134
- expect(txn.type).toBe("purchase");
135
- expect(txn.description).toBe("desc");
136
- expect(txn.referenceId).toBe("ref-1");
137
- expect(txn.fundingSource).toBe("stripe");
138
- expect(txn.attributedUserId).toBe("user-1");
139
- expect(txn.createdAt).toEqual(expect.any(String));
140
- });
141
-
142
- it("respects limit and offset for pagination", async () => {
143
- // Insert 5 transactions
144
- for (let i = 1; i <= 5; i++) {
145
- await ledger.credit("t1", Credit.fromCents(10 * i), "purchase", `txn-${i}`);
146
- }
147
-
148
- const page1 = await ledger.history("t1", { limit: 2, offset: 0 });
149
- expect(page1).toHaveLength(2);
150
- expect(page1[0].description).toBe("txn-5"); // newest first
151
- expect(page1[1].description).toBe("txn-4");
152
-
153
- const page2 = await ledger.history("t1", { limit: 2, offset: 2 });
154
- expect(page2).toHaveLength(2);
155
- expect(page2[0].description).toBe("txn-3");
156
- expect(page2[1].description).toBe("txn-2");
157
- });
158
-
159
- it("filters by type when provided", async () => {
160
- await ledger.credit("t1", Credit.fromCents(100), "purchase", "buy");
161
- await ledger.credit("t1", Credit.fromCents(50), "promo", "free");
162
- await ledger.debit("t1", Credit.fromCents(10), "bot_runtime", "usage");
163
-
164
- const purchases = await ledger.history("t1", { type: "purchase" });
165
- expect(purchases).toHaveLength(1);
166
- expect(purchases[0].description).toBe("buy");
167
- });
168
-
169
- it("returns empty array for tenant with no transactions", async () => {
170
- const hist = await ledger.history("nonexistent");
171
- expect(hist).toEqual([]);
172
- });
173
- });
174
-
175
- // --- hasReferenceId() ---
176
-
177
- describe("hasReferenceId()", () => {
178
- it("returns false for a reference ID that does not exist", async () => {
179
- expect(await ledger.hasReferenceId("nonexistent-ref")).toBe(false);
180
- });
181
-
182
- it("returns true for a reference ID used in a credit", async () => {
183
- await ledger.credit("t1", Credit.fromCents(100), "purchase", "desc", "ref-unique");
184
-
185
- expect(await ledger.hasReferenceId("ref-unique")).toBe(true);
186
- });
187
-
188
- it("returns true for a reference ID used in a debit", async () => {
189
- await ledger.credit("t1", Credit.fromCents(100), "purchase");
190
- await ledger.debit("t1", Credit.fromCents(10), "bot_runtime", "desc", "debit-ref");
191
-
192
- expect(await ledger.hasReferenceId("debit-ref")).toBe(true);
193
- });
194
-
195
- it("detects reference IDs across different tenants", async () => {
196
- await ledger.credit("t1", Credit.fromCents(100), "purchase", "desc", "cross-tenant-ref");
197
-
198
- // hasReferenceId is global, not tenant-scoped
199
- expect(await ledger.hasReferenceId("cross-tenant-ref")).toBe(true);
200
- });
201
- });
202
-
203
- // --- tenantsWithBalance() ---
204
-
205
- describe("tenantsWithBalance()", () => {
206
- it("returns empty array when no tenants exist", async () => {
207
- const result = await ledger.tenantsWithBalance();
208
- expect(result).toEqual([]);
209
- });
210
-
211
- it("returns only tenants with positive balance", async () => {
212
- // t1: positive balance (100 cents)
213
- await ledger.credit("t1", Credit.fromCents(100), "purchase");
214
-
215
- // t2: zero balance (credit then debit same amount)
216
- await ledger.credit("t2", Credit.fromCents(50), "purchase");
217
- await ledger.debit("t2", Credit.fromCents(50), "bot_runtime");
218
-
219
- // t3: negative balance (via allowNegative)
220
- await ledger.credit("t3", Credit.fromCents(10), "purchase");
221
- await ledger.debit("t3", Credit.fromCents(20), "bot_runtime", undefined, undefined, true);
222
-
223
- // t4: positive balance (200 cents)
224
- await ledger.credit("t4", Credit.fromCents(200), "signup_grant");
225
-
226
- const result = await ledger.tenantsWithBalance();
227
-
228
- const tenantIds = result.map((r) => r.tenantId).sort();
229
- expect(tenantIds).toEqual(["t1", "t4"]);
230
-
231
- const t1 = result.find((r) => r.tenantId === "t1");
232
- expect(t1?.balance.toCents()).toBe(100);
233
-
234
- const t4 = result.find((r) => r.tenantId === "t4");
235
- expect(t4?.balance.toCents()).toBe(200);
236
- });
237
-
238
- it("excludes tenants with exactly zero balance", async () => {
239
- await ledger.credit("t1", Credit.fromCents(100), "purchase");
240
- await ledger.debit("t1", Credit.fromCents(100), "bot_runtime");
241
-
242
- const result = await ledger.tenantsWithBalance();
243
- expect(result).toEqual([]);
244
- });
245
- });
246
- });
247
-
248
- describe("CreditLedger.debit with allowNegative", () => {
249
- let ledger: CreditLedger;
250
-
251
- beforeEach(async () => {
252
- await truncateAllTables(pool);
253
- ledger = new CreditLedger(db);
254
- });
255
-
256
- it("debit with allowNegative=false (default) throws InsufficientBalanceError when balance insufficient", async () => {
257
- await ledger.credit("t1", Credit.fromCents(5), "purchase", "setup");
258
- await expect(ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "test")).rejects.toThrow(
259
- InsufficientBalanceError,
260
- );
261
- });
262
-
263
- it("debit with allowNegative=true allows negative balance", async () => {
264
- await ledger.credit("t1", Credit.fromCents(5), "purchase", "setup");
265
- const txn = await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "test", undefined, true);
266
- expect(txn).not.toBeNull();
267
- expect((await ledger.balance("t1")).toCents()).toBe(-5);
268
- });
269
-
270
- it("debit with allowNegative=true records correct transaction with negative amount and negative balanceAfter", async () => {
271
- await ledger.credit("t1", Credit.fromCents(5), "purchase", "setup");
272
- const txn = await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "test", undefined, true);
273
- expect(txn.amount.toCents()).toBe(-10);
274
- expect(txn.balanceAfter.toCents()).toBe(-5);
275
- });
276
- });