@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,5 +1,5 @@
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";
@@ -10,7 +10,7 @@ import { DrizzleAffiliateRepository } from "./drizzle-affiliate-repository.js";
10
10
  describe("processAffiliateCreditMatch", () => {
11
11
  let pool: PGlite;
12
12
  let db: DrizzleDb;
13
- let ledger: CreditLedger;
13
+ let ledger: DrizzleLedger;
14
14
  let affiliateRepo: DrizzleAffiliateRepository;
15
15
  let fraudRepo: DrizzleAffiliateFraudRepository;
16
16
 
@@ -24,13 +24,19 @@ describe("processAffiliateCreditMatch", () => {
24
24
 
25
25
  beforeEach(async () => {
26
26
  await truncateAllTables(pool);
27
- ledger = new CreditLedger(db);
27
+ ledger = new DrizzleLedger(db);
28
+
29
+ await ledger.seedSystemAccounts();
28
30
  affiliateRepo = new DrizzleAffiliateRepository(db);
29
31
  fraudRepo = new DrizzleAffiliateFraudRepository(db);
30
32
  });
31
33
 
32
34
  it("does nothing when tenant has no referral", async () => {
33
- await ledger.credit("buyer", Credit.fromCents(1000), "purchase", "first buy", "session-1", "stripe");
35
+ await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
36
+ description: "first buy",
37
+ referenceId: "session-1",
38
+ fundingSource: "stripe",
39
+ });
34
40
 
35
41
  const result = await processAffiliateCreditMatch({
36
42
  tenantId: "buyer",
@@ -45,8 +51,16 @@ describe("processAffiliateCreditMatch", () => {
45
51
  it("does nothing when tenant already has prior purchases", async () => {
46
52
  await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
47
53
 
48
- await ledger.credit("buyer", Credit.fromCents(500), "purchase", "old buy", "session-0", "stripe");
49
- await ledger.credit("buyer", Credit.fromCents(1000), "purchase", "new buy", "session-1", "stripe");
54
+ await ledger.credit("buyer", Credit.fromCents(500), "purchase", {
55
+ description: "old buy",
56
+ referenceId: "session-0",
57
+ fundingSource: "stripe",
58
+ });
59
+ await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
60
+ description: "new buy",
61
+ referenceId: "session-1",
62
+ fundingSource: "stripe",
63
+ });
50
64
 
51
65
  const result = await processAffiliateCreditMatch({
52
66
  tenantId: "buyer",
@@ -60,7 +74,11 @@ describe("processAffiliateCreditMatch", () => {
60
74
 
61
75
  it("credits referrer on first purchase with 100% match", async () => {
62
76
  await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
63
- await ledger.credit("buyer", Credit.fromCents(2000), "purchase", "first buy", "session-1", "stripe");
77
+ await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
78
+ description: "first buy",
79
+ referenceId: "session-1",
80
+ fundingSource: "stripe",
81
+ });
64
82
 
65
83
  const result = await processAffiliateCreditMatch({
66
84
  tenantId: "buyer",
@@ -83,7 +101,11 @@ describe("processAffiliateCreditMatch", () => {
83
101
 
84
102
  it("respects custom match rate", async () => {
85
103
  await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
86
- await ledger.credit("buyer", Credit.fromCents(2000), "purchase", "first buy", "session-1", "stripe");
104
+ await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
105
+ description: "first buy",
106
+ referenceId: "session-1",
107
+ fundingSource: "stripe",
108
+ });
87
109
 
88
110
  const result = await processAffiliateCreditMatch({
89
111
  tenantId: "buyer",
@@ -99,7 +121,11 @@ describe("processAffiliateCreditMatch", () => {
99
121
 
100
122
  it("is idempotent — second call returns null", async () => {
101
123
  await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
102
- await ledger.credit("buyer", Credit.fromCents(1000), "purchase", "first buy", "session-1", "stripe");
124
+ await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
125
+ description: "first buy",
126
+ referenceId: "session-1",
127
+ fundingSource: "stripe",
128
+ });
103
129
 
104
130
  const first = await processAffiliateCreditMatch({
105
131
  tenantId: "buyer",
@@ -123,7 +149,11 @@ describe("processAffiliateCreditMatch", () => {
123
149
  signupIp: "1.2.3.4",
124
150
  signupEmail: "alice+ref@gmail.com",
125
151
  });
126
- await ledger.credit("buyer", Credit.fromCents(2000), "purchase", "first buy", "session-1", "stripe");
152
+ await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
153
+ description: "first buy",
154
+ referenceId: "session-1",
155
+ fundingSource: "stripe",
156
+ });
127
157
 
128
158
  const result = await processAffiliateCreditMatch({
129
159
  tenantId: "buyer",
@@ -156,7 +186,11 @@ describe("processAffiliateCreditMatch", () => {
156
186
 
157
187
  // New referral
158
188
  await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
159
- await ledger.credit("buyer", Credit.fromCents(1000), "purchase", "first buy", "session-1", "stripe");
189
+ await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
190
+ description: "first buy",
191
+ referenceId: "session-1",
192
+ fundingSource: "stripe",
193
+ });
160
194
 
161
195
  const result = await processAffiliateCreditMatch({
162
196
  tenantId: "buyer",
@@ -184,7 +218,11 @@ describe("processAffiliateCreditMatch", () => {
184
218
 
185
219
  // New referral
186
220
  await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
187
- await ledger.credit("buyer", Credit.fromCents(1000), "purchase", "first buy", "session-1", "stripe");
221
+ await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
222
+ description: "first buy",
223
+ referenceId: "session-1",
224
+ fundingSource: "stripe",
225
+ });
188
226
 
189
227
  const result = await processAffiliateCreditMatch({
190
228
  tenantId: "buyer",
@@ -203,7 +241,11 @@ describe("processAffiliateCreditMatch", () => {
203
241
 
204
242
  it("allows payout when under both caps", async () => {
205
243
  await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
206
- await ledger.credit("buyer", Credit.fromCents(2000), "purchase", "first buy", "session-1", "stripe");
244
+ await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
245
+ description: "first buy",
246
+ referenceId: "session-1",
247
+ fundingSource: "stripe",
248
+ });
207
249
 
208
250
  const result = await processAffiliateCreditMatch({
209
251
  tenantId: "buyer",
@@ -223,7 +265,11 @@ describe("processAffiliateCreditMatch", () => {
223
265
  await affiliateRepo.recordReferral("referrer", "buyer", "abc123", {
224
266
  signupIp: "1.2.3.4",
225
267
  });
226
- await ledger.credit("buyer", Credit.fromCents(2000), "purchase", "first buy", "session-1", "stripe");
268
+ await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
269
+ description: "first buy",
270
+ referenceId: "session-1",
271
+ fundingSource: "stripe",
272
+ });
227
273
 
228
274
  const result = await processAffiliateCreditMatch({
229
275
  tenantId: "buyer",
@@ -1,4 +1,4 @@
1
- import type { Credit, ICreditLedger } from "@wopr-network/platform-core/credits";
1
+ import type { Credit, ILedger } from "@wopr-network/platform-core/credits";
2
2
  import { config } from "../../config/index.js";
3
3
  import type { IAffiliateFraudRepository } from "./affiliate-fraud-repository.js";
4
4
  import type { IAffiliateRepository } from "./drizzle-affiliate-repository.js";
@@ -11,7 +11,7 @@ const DEFAULT_MAX_MATCH_CREDITS_30D = config.billing.affiliateMaxMatchCredits30d
11
11
  export interface AffiliateCreditMatchDeps {
12
12
  tenantId: string;
13
13
  purchaseAmount: Credit;
14
- ledger: ICreditLedger;
14
+ ledger: ILedger;
15
15
  affiliateRepo: IAffiliateRepository;
16
16
  matchRate?: number;
17
17
  fraudRepo?: IAffiliateFraudRepository;
@@ -122,13 +122,10 @@ export async function processAffiliateCreditMatch(
122
122
  if (matchAmount.isZero() || matchAmount.isNegative()) return null;
123
123
 
124
124
  // 6. Credit the referrer
125
- await ledger.credit(
126
- referral.referrerTenantId,
127
- matchAmount,
128
- "affiliate_match",
129
- `Affiliate match for referred tenant ${tenantId}`,
130
- refId,
131
- );
125
+ await ledger.credit(referral.referrerTenantId, matchAmount, "affiliate_match", {
126
+ description: `Affiliate match for referred tenant ${tenantId}`,
127
+ referenceId: refId,
128
+ });
132
129
 
133
130
  // 7. Update referral record
134
131
  await affiliateRepo.markFirstPurchase(tenantId);
@@ -1,5 +1,5 @@
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";
@@ -9,7 +9,7 @@ import { DEFAULT_BONUS_RATE, grantNewUserBonus } from "./new-user-bonus.js";
9
9
  describe("grantNewUserBonus", () => {
10
10
  let pool: PGlite;
11
11
  let db: DrizzleDb;
12
- let ledger: CreditLedger;
12
+ let ledger: DrizzleLedger;
13
13
  let affiliateRepo: DrizzleAffiliateRepository;
14
14
 
15
15
  beforeAll(async () => {
@@ -22,7 +22,9 @@ describe("grantNewUserBonus", () => {
22
22
 
23
23
  beforeEach(async () => {
24
24
  await truncateAllTables(pool);
25
- ledger = new CreditLedger(db);
25
+ ledger = new DrizzleLedger(db);
26
+
27
+ await ledger.seedSystemAccounts();
26
28
  affiliateRepo = new DrizzleAffiliateRepository(db);
27
29
  });
28
30
 
@@ -48,7 +50,7 @@ describe("grantNewUserBonus", () => {
48
50
 
49
51
  const txns = await ledger.history("referred-1");
50
52
  expect(txns).toHaveLength(1);
51
- expect(txns[0].type).toBe("affiliate_bonus");
53
+ expect(txns[0].entryType).toBe("affiliate_bonus");
52
54
  expect(txns[0].referenceId).toBe("affiliate-bonus:referred-1");
53
55
  expect(txns[0].description).toContain("first-purchase bonus");
54
56
  });
@@ -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 { config } from "../../config/index.js";
4
4
  import type { IAffiliateRepository } from "./drizzle-affiliate-repository.js";
@@ -7,7 +7,7 @@ import type { IAffiliateRepository } from "./drizzle-affiliate-repository.js";
7
7
  export const DEFAULT_BONUS_RATE = config.billing.affiliateNewUserBonusRate;
8
8
 
9
9
  export interface NewUserBonusParams {
10
- ledger: ICreditLedger;
10
+ ledger: ILedger;
11
11
  affiliateRepo: IAffiliateRepository;
12
12
  referredTenantId: string;
13
13
  purchaseAmount: Credit;
@@ -57,13 +57,10 @@ export async function grantNewUserBonus(params: NewUserBonusParams): Promise<New
57
57
  await affiliateRepo.markFirstPurchase(referredTenantId);
58
58
 
59
59
  // 6. Credit the bonus
60
- await ledger.credit(
61
- referredTenantId,
62
- bonus,
63
- "affiliate_bonus",
64
- `New user first-purchase bonus (${Math.round(rate * 100)}%)`,
65
- refId,
66
- );
60
+ await ledger.credit(referredTenantId, bonus, "affiliate_bonus", {
61
+ description: `New user first-purchase bonus (${Math.round(rate * 100)}%)`,
62
+ referenceId: refId,
63
+ });
67
64
 
68
65
  return { granted: true, bonus };
69
66
  }
@@ -1,7 +1,7 @@
1
1
  import crypto from "node:crypto";
2
2
  import type { PGlite } from "@electric-sql/pglite";
3
3
  import type { ITenantCustomerRepository } from "@wopr-network/platform-core/billing";
4
- import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
4
+ import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
5
5
  import Stripe from "stripe";
6
6
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
7
7
  import type { DrizzleDb } from "../../db/index.js";
@@ -48,7 +48,7 @@ function mockTenantStore(stripeCustomerId = "cus_123") {
48
48
  describe("chargeAutoTopup", () => {
49
49
  let pool: PGlite;
50
50
  let db: DrizzleDb;
51
- let ledger: CreditLedger;
51
+ let ledger: DrizzleLedger;
52
52
 
53
53
  beforeAll(async () => {
54
54
  ({ db, pool } = await createTestDb());
@@ -60,7 +60,9 @@ describe("chargeAutoTopup", () => {
60
60
 
61
61
  beforeEach(async () => {
62
62
  await truncateAllTables(pool);
63
- ledger = new CreditLedger(db);
63
+ ledger = new DrizzleLedger(db);
64
+
65
+ await ledger.seedSystemAccounts();
64
66
  });
65
67
 
66
68
  it("charges Stripe and credits ledger on success", async () => {
@@ -79,8 +81,8 @@ describe("chargeAutoTopup", () => {
79
81
  expect(result.paymentReference).toEqual(expect.any(String));
80
82
  expect((await ledger.balance("t1")).toCents()).toBe(500);
81
83
  const history = await ledger.history("t1");
82
- expect(history[0].type).toBe("purchase");
83
- expect(history[0].fundingSource).toBe("stripe");
84
+ expect(history[0].entryType).toBe("purchase");
85
+ expect(history[0].metadata?.fundingSource).toBe("stripe");
84
86
  });
85
87
 
86
88
  it("writes success event to credit_auto_topup log", async () => {
@@ -1,5 +1,5 @@
1
1
  import type { ITenantCustomerRepository } from "@wopr-network/platform-core/billing";
2
- import type { Credit, ICreditLedger } from "@wopr-network/platform-core/credits";
2
+ import type { Credit, ILedger } from "@wopr-network/platform-core/credits";
3
3
  import Stripe from "stripe";
4
4
  import { logger } from "../../config/logger.js";
5
5
  import type { IAutoTopupEventLogRepository } from "./auto-topup-event-log-repository.js";
@@ -10,7 +10,7 @@ export const MAX_CONSECUTIVE_FAILURES = 3;
10
10
  export interface AutoTopupChargeDeps {
11
11
  stripe: Stripe;
12
12
  tenantRepo: ITenantCustomerRepository;
13
- creditLedger: ICreditLedger;
13
+ creditLedger: ILedger;
14
14
  eventLogRepo: IAutoTopupEventLogRepository;
15
15
  }
16
16
 
@@ -136,14 +136,11 @@ export async function chargeAutoTopup(
136
136
  // 5. Credit the ledger (idempotent via referenceId = PI ID)
137
137
  try {
138
138
  if (!(await deps.creditLedger.hasReferenceId(paymentIntent.id))) {
139
- await deps.creditLedger.credit(
140
- tenantId,
141
- amount,
142
- "purchase",
143
- `Auto-topup (${source})`,
144
- paymentIntent.id,
145
- "stripe",
146
- );
139
+ await deps.creditLedger.credit(tenantId, amount, "purchase", {
140
+ description: `Auto-topup (${source})`,
141
+ referenceId: paymentIntent.id,
142
+ fundingSource: "stripe",
143
+ });
147
144
  }
148
145
  } catch (err) {
149
146
  const message = `Stripe charge ${paymentIntent.id} succeeded but credit grant failed: ${err instanceof Error ? err.message : String(err)}`;
@@ -1,5 +1,5 @@
1
1
  import type { PGlite } from "@electric-sql/pglite";
2
- import { Credit, CreditLedger, DrizzleAutoTopupSettingsRepository } from "@wopr-network/platform-core/credits";
2
+ import { Credit, DrizzleAutoTopupSettingsRepository, DrizzleLedger } from "@wopr-network/platform-core/credits";
3
3
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
4
4
  import type { DrizzleDb } from "../../db/index.js";
5
5
  import { createTestDb, truncateAllTables } from "../../test/db.js";
@@ -8,7 +8,7 @@ import { maybeTriggerUsageTopup, type UsageTopupDeps } from "./auto-topup-usage.
8
8
  describe("maybeTriggerUsageTopup", () => {
9
9
  let pool: PGlite;
10
10
  let db: DrizzleDb;
11
- let ledger: CreditLedger;
11
+ let ledger: DrizzleLedger;
12
12
  let settingsRepo: DrizzleAutoTopupSettingsRepository;
13
13
 
14
14
  beforeAll(async () => {
@@ -21,7 +21,9 @@ describe("maybeTriggerUsageTopup", () => {
21
21
 
22
22
  beforeEach(async () => {
23
23
  await truncateAllTables(pool);
24
- ledger = new CreditLedger(db);
24
+ ledger = new DrizzleLedger(db);
25
+
26
+ await ledger.seedSystemAccounts();
25
27
  settingsRepo = new DrizzleAutoTopupSettingsRepository(db);
26
28
  });
27
29
 
@@ -35,7 +37,11 @@ describe("maybeTriggerUsageTopup", () => {
35
37
 
36
38
  it("does nothing when usage_enabled is false", async () => {
37
39
  await settingsRepo.upsert("t1", { usageEnabled: false });
38
- await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
40
+ await ledger.credit("t1", Credit.fromCents(50), "purchase", {
41
+ description: "buy",
42
+ referenceId: "ref-1",
43
+ fundingSource: "stripe",
44
+ });
39
45
  const mockCharge = vi.fn();
40
46
  const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
41
47
 
@@ -49,7 +55,11 @@ describe("maybeTriggerUsageTopup", () => {
49
55
  usageThreshold: Credit.fromCents(100),
50
56
  usageTopup: Credit.fromCents(500),
51
57
  });
52
- await ledger.credit("t1", Credit.fromCents(200), "purchase", "buy", "ref-1", "stripe");
58
+ await ledger.credit("t1", Credit.fromCents(200), "purchase", {
59
+ description: "buy",
60
+ referenceId: "ref-1",
61
+ fundingSource: "stripe",
62
+ });
53
63
  const mockCharge = vi.fn();
54
64
  const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
55
65
 
@@ -63,7 +73,11 @@ describe("maybeTriggerUsageTopup", () => {
63
73
  usageThreshold: Credit.fromCents(100),
64
74
  usageTopup: Credit.fromCents(500),
65
75
  });
66
- await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
76
+ await ledger.credit("t1", Credit.fromCents(50), "purchase", {
77
+ description: "buy",
78
+ referenceId: "ref-1",
79
+ fundingSource: "stripe",
80
+ });
67
81
  const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_123" });
68
82
  const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
69
83
 
@@ -74,7 +88,11 @@ describe("maybeTriggerUsageTopup", () => {
74
88
  it("skips when charge is already in-flight", async () => {
75
89
  await settingsRepo.upsert("t1", { usageEnabled: true, usageThreshold: Credit.fromCents(100) });
76
90
  await settingsRepo.setUsageChargeInFlight("t1", true);
77
- await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
91
+ await ledger.credit("t1", Credit.fromCents(50), "purchase", {
92
+ description: "buy",
93
+ referenceId: "ref-1",
94
+ fundingSource: "stripe",
95
+ });
78
96
  const mockCharge = vi.fn();
79
97
  const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
80
98
 
@@ -89,7 +107,11 @@ describe("maybeTriggerUsageTopup", () => {
89
107
  usageThreshold: Credit.fromCents(500),
90
108
  usageTopup: Credit.fromCents(2000),
91
109
  });
92
- await ledger.credit("t1", Credit.fromCents(100), "purchase", "buy", "ref-1", "stripe");
110
+ await ledger.credit("t1", Credit.fromCents(100), "purchase", {
111
+ description: "buy",
112
+ referenceId: "ref-1",
113
+ fundingSource: "stripe",
114
+ });
93
115
 
94
116
  const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_race" });
95
117
  const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
@@ -113,7 +135,11 @@ describe("maybeTriggerUsageTopup", () => {
113
135
  usageThreshold: Credit.fromCents(100),
114
136
  usageTopup: Credit.fromCents(500),
115
137
  });
116
- await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
138
+ await ledger.credit("t1", Credit.fromCents(50), "purchase", {
139
+ description: "buy",
140
+ referenceId: "ref-1",
141
+ fundingSource: "stripe",
142
+ });
117
143
  const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_123" });
118
144
  const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
119
145
 
@@ -135,7 +161,11 @@ describe("maybeTriggerUsageTopup", () => {
135
161
  usageThreshold: Credit.fromCents(100),
136
162
  usageTopup: Credit.fromCents(500),
137
163
  });
138
- await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
164
+ await ledger.credit("t1", Credit.fromCents(50), "purchase", {
165
+ description: "buy",
166
+ referenceId: "ref-1",
167
+ fundingSource: "stripe",
168
+ });
139
169
  const mockCharge = vi
140
170
  .fn()
141
171
  .mockRejectedValueOnce(new Error("Stripe network error"))
@@ -162,7 +192,11 @@ describe("maybeTriggerUsageTopup", () => {
162
192
  });
163
193
  await settingsRepo.incrementUsageFailures("t1");
164
194
  await settingsRepo.incrementUsageFailures("t1");
165
- await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
195
+ await ledger.credit("t1", Credit.fromCents(50), "purchase", {
196
+ description: "buy",
197
+ referenceId: "ref-1",
198
+ fundingSource: "stripe",
199
+ });
166
200
  const mockCharge = vi.fn().mockResolvedValue({ success: true });
167
201
  const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
168
202
 
@@ -176,7 +210,11 @@ describe("maybeTriggerUsageTopup", () => {
176
210
  usageThreshold: Credit.fromCents(100),
177
211
  usageTopup: Credit.fromCents(500),
178
212
  });
179
- await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
213
+ await ledger.credit("t1", Credit.fromCents(50), "purchase", {
214
+ description: "buy",
215
+ referenceId: "ref-1",
216
+ fundingSource: "stripe",
217
+ });
180
218
  const mockCharge = vi.fn().mockResolvedValue({ success: false, error: "declined" });
181
219
  const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
182
220
 
@@ -208,7 +246,11 @@ describe("maybeTriggerUsageTopup", () => {
208
246
  });
209
247
  await settingsRepo.incrementUsageFailures("t1");
210
248
  await settingsRepo.incrementUsageFailures("t1");
211
- await ledger.credit("t1", Credit.fromCents(50), "purchase", "buy", "ref-1", "stripe");
249
+ await ledger.credit("t1", Credit.fromCents(50), "purchase", {
250
+ description: "buy",
251
+ referenceId: "ref-1",
252
+ fundingSource: "stripe",
253
+ });
212
254
  const mockCharge = vi.fn().mockResolvedValue({ success: false, error: "declined" });
213
255
  const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
214
256
 
@@ -1,11 +1,11 @@
1
- import type { Credit, IAutoTopupSettingsRepository, ICreditLedger } from "@wopr-network/platform-core/credits";
1
+ import type { Credit, IAutoTopupSettingsRepository, ILedger } from "@wopr-network/platform-core/credits";
2
2
  import { logger } from "../../config/logger.js";
3
3
  import type { AutoTopupChargeResult } from "./auto-topup-charge.js";
4
4
  import { MAX_CONSECUTIVE_FAILURES } from "./auto-topup-charge.js";
5
5
 
6
6
  export interface UsageTopupDeps {
7
7
  settingsRepo: IAutoTopupSettingsRepository;
8
- creditLedger: ICreditLedger;
8
+ creditLedger: ILedger;
9
9
  /** Injected charge function (allows mocking in tests). */
10
10
  chargeAutoTopup: (tenantId: string, amount: Credit, source: string) => Promise<AutoTopupChargeResult>;
11
11
  /** Optional tenant status check. If provided and returns non-null, skip the charge. */
@@ -1,5 +1,5 @@
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 { sql } from "drizzle-orm";
4
4
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
5
5
  import type { DrizzleDb } from "../../db/index.js";
@@ -57,7 +57,7 @@ describe("BotBilling", () => {
57
57
  let pool: PGlite;
58
58
  let db: DrizzleDb;
59
59
  let billing: BotBilling;
60
- let ledger: CreditLedger;
60
+ let ledger: DrizzleLedger;
61
61
 
62
62
  beforeAll(async () => {
63
63
  ({ db, pool } = await createTestDb());
@@ -70,7 +70,9 @@ describe("BotBilling", () => {
70
70
  beforeEach(async () => {
71
71
  await truncateAllTables(pool);
72
72
  billing = new BotBilling(new DrizzleBotInstanceRepository(db));
73
- ledger = new CreditLedger(db);
73
+ ledger = new DrizzleLedger(db);
74
+
75
+ await ledger.seedSystemAccounts();
74
76
  });
75
77
 
76
78
  describe("registerBot", () => {
@@ -211,7 +213,11 @@ describe("BotBilling", () => {
211
213
  await billing.suspendBot("bot-1");
212
214
  await billing.suspendBot("bot-2");
213
215
 
214
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "test credit", "ref-1", "stripe");
216
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", {
217
+ description: "test credit",
218
+ referenceId: "ref-1",
219
+ fundingSource: "stripe",
220
+ });
215
221
  const reactivated = await billing.checkReactivation("tenant-1", ledger);
216
222
 
217
223
  expect(reactivated.sort()).toEqual(["bot-1", "bot-2"]);
@@ -231,14 +237,22 @@ describe("BotBilling", () => {
231
237
  await billing.registerBot("bot-1", "tenant-1", "bot-a");
232
238
  await billing.destroyBot("bot-1");
233
239
 
234
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "test credit", "ref-1", "stripe");
240
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", {
241
+ description: "test credit",
242
+ referenceId: "ref-1",
243
+ fundingSource: "stripe",
244
+ });
235
245
  const reactivated = await billing.checkReactivation("tenant-1", ledger);
236
246
 
237
247
  expect(reactivated).toEqual([]);
238
248
  });
239
249
 
240
250
  it("returns empty array for tenant with no bots", async () => {
241
- await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "test credit", "ref-1", "stripe");
251
+ await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", {
252
+ description: "test credit",
253
+ referenceId: "ref-1",
254
+ fundingSource: "stripe",
255
+ });
242
256
  const reactivated = await billing.checkReactivation("tenant-1", ledger);
243
257
  expect(reactivated).toEqual([]);
244
258
  });
@@ -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 { logger } from "../../config/logger.js";
4
4
  import type { IBotInstanceRepository } from "../../fleet/bot-instance-repository.js";
@@ -16,7 +16,7 @@ export interface IBotBilling {
16
16
  suspendBot(botId: string): Promise<void>;
17
17
  suspendAllForTenant(tenantId: string): Promise<string[]>;
18
18
  reactivateBot(botId: string): Promise<void>;
19
- checkReactivation(tenantId: string, ledger: ICreditLedger): Promise<string[]>;
19
+ checkReactivation(tenantId: string, ledger: ILedger): Promise<string[]>;
20
20
  destroyBot(botId: string): Promise<void>;
21
21
  destroyExpiredBots(): Promise<string[]>;
22
22
  getBotBilling(botId: string): Promise<unknown>;
@@ -98,7 +98,7 @@ export class DrizzleBotBilling implements IBotBilling {
98
98
  *
99
99
  * @returns IDs of reactivated bots.
100
100
  */
101
- async checkReactivation(tenantId: string, ledger: ICreditLedger): Promise<string[]> {
101
+ async checkReactivation(tenantId: string, ledger: ILedger): Promise<string[]> {
102
102
  const balance = await ledger.balance(tenantId);
103
103
  if (balance.isNegative() || balance.isZero()) return [];
104
104