@wopr-network/platform-core 1.13.3 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/.github/workflows/dependabot-auto-merge.yml +1 -2
  2. package/dist/api/routes/admin-credits.d.ts +2 -2
  3. package/dist/api/routes/admin-credits.js +9 -4
  4. package/dist/api/routes/quota.d.ts +2 -2
  5. package/dist/api/routes/verify-email.d.ts +3 -3
  6. package/dist/backup/on-demand-snapshot-service.d.ts +2 -2
  7. package/dist/billing/payram/webhook.d.ts +3 -3
  8. package/dist/billing/payram/webhook.js +5 -1
  9. package/dist/billing/payram/webhook.test.js +5 -4
  10. package/dist/billing/stripe/stripe-payment-processor.d.ts +2 -2
  11. package/dist/billing/stripe/stripe-payment-processor.test.js +7 -0
  12. package/dist/billing/stripe/tenant-store.d.ts +1 -1
  13. package/dist/billing/stripe/tenant-store.js +1 -1
  14. package/dist/credits/auto-topup-charge.d.ts +2 -2
  15. package/dist/credits/auto-topup-charge.js +5 -1
  16. package/dist/credits/auto-topup-charge.test.js +5 -4
  17. package/dist/credits/auto-topup-usage.d.ts +2 -2
  18. package/dist/credits/auto-topup-usage.test.js +53 -12
  19. package/dist/credits/credit-expiry-cron.d.ts +2 -2
  20. package/dist/credits/credit-expiry-cron.js +7 -4
  21. package/dist/credits/credit-expiry-cron.test.js +25 -8
  22. package/dist/credits/credit-ledger.d.ts +2 -2
  23. package/dist/credits/credit-ledger.js +1 -1
  24. package/dist/credits/dividend-cron.d.ts +4 -6
  25. package/dist/credits/dividend-cron.js +10 -16
  26. package/dist/credits/dividend-cron.test.js +31 -44
  27. package/dist/credits/dividend-repository.js +19 -22
  28. package/dist/credits/dividend-repository.test.js +4 -3
  29. package/dist/credits/index.d.ts +4 -2
  30. package/dist/credits/index.js +2 -1
  31. package/dist/credits/ledger.d.ts +195 -0
  32. package/dist/credits/ledger.js +561 -0
  33. package/dist/credits/ledger.test.js +418 -0
  34. package/dist/credits/signup-grant.d.ts +2 -2
  35. package/dist/credits/signup-grant.js +4 -4
  36. package/dist/credits/signup-grant.test.js +5 -3
  37. package/dist/credits/trial-balance-cron.d.ts +19 -0
  38. package/dist/credits/trial-balance-cron.js +30 -0
  39. package/dist/credits/trial-balance-cron.test.js +55 -0
  40. package/dist/db/schema/index.d.ts +1 -0
  41. package/dist/db/schema/index.js +1 -0
  42. package/dist/db/schema/ledger.d.ts +442 -0
  43. package/dist/db/schema/ledger.js +76 -0
  44. package/dist/gateway/credit-gate.d.ts +2 -2
  45. package/dist/gateway/credit-gate.js +5 -1
  46. package/dist/gateway/credit-gate.test.js +35 -33
  47. package/dist/gateway/protocol/deps.d.ts +2 -2
  48. package/dist/gateway/protocol/handlers.test.js +461 -0
  49. package/dist/gateway/proxy.d.ts +2 -2
  50. package/dist/gateway/types.d.ts +2 -2
  51. package/dist/metering/reconciliation-cron.test.js +9 -8
  52. package/dist/metering/reconciliation-repository.js +12 -10
  53. package/dist/metering/reconciliation-repository.test.js +9 -8
  54. package/dist/monetization/affiliate/affiliate-admin-repository.js +10 -8
  55. package/dist/monetization/affiliate/affiliate-admin-repository.test.js +32 -13
  56. package/dist/monetization/affiliate/credit-match.d.ts +2 -2
  57. package/dist/monetization/affiliate/credit-match.js +4 -1
  58. package/dist/monetization/affiliate/credit-match.test.js +58 -13
  59. package/dist/monetization/affiliate/new-user-bonus.d.ts +2 -2
  60. package/dist/monetization/affiliate/new-user-bonus.js +4 -1
  61. package/dist/monetization/affiliate/new-user-bonus.test.js +4 -3
  62. package/dist/monetization/credits/auto-topup-charge.d.ts +2 -2
  63. package/dist/monetization/credits/auto-topup-charge.js +5 -1
  64. package/dist/monetization/credits/auto-topup-charge.test.js +5 -4
  65. package/dist/monetization/credits/auto-topup-usage.d.ts +2 -2
  66. package/dist/monetization/credits/auto-topup-usage.test.js +53 -12
  67. package/dist/monetization/credits/bot-billing.d.ts +3 -3
  68. package/dist/monetization/credits/bot-billing.test.js +18 -5
  69. package/dist/monetization/credits/credit-expiry-cron.test.js +25 -8
  70. package/dist/monetization/credits/dividend-cron.d.ts +2 -4
  71. package/dist/monetization/credits/dividend-cron.js +7 -4
  72. package/dist/monetization/credits/dividend-cron.test.js +26 -46
  73. package/dist/monetization/credits/dividend-repository.js +15 -24
  74. package/dist/monetization/credits/dividend-repository.test.js +4 -3
  75. package/dist/monetization/credits/index.d.ts +2 -2
  76. package/dist/monetization/credits/index.js +1 -1
  77. package/dist/monetization/credits/member-usage.test.js +23 -10
  78. package/dist/monetization/credits/phone-billing.d.ts +2 -2
  79. package/dist/monetization/credits/phone-billing.js +5 -1
  80. package/dist/monetization/credits/phone-billing.test.js +9 -12
  81. package/dist/monetization/credits/runtime-cron.d.ts +2 -2
  82. package/dist/monetization/credits/runtime-cron.js +32 -8
  83. package/dist/monetization/credits/runtime-cron.test.js +28 -27
  84. package/dist/monetization/credits/runtime-scheduler.d.ts +2 -2
  85. package/dist/monetization/credits/runtime-scheduler.test.js +1 -1
  86. package/dist/monetization/credits/signup-grant.test.js +5 -3
  87. package/dist/monetization/credits/storage-tier-cron.test.js +3 -2
  88. package/dist/monetization/credits/trial-balance-cron.test.js +42 -0
  89. package/dist/monetization/feature-gate.d.ts +3 -3
  90. package/dist/monetization/index.d.ts +3 -3
  91. package/dist/monetization/index.js +1 -1
  92. package/dist/monetization/metering/reconciliation-cron.test.js +9 -8
  93. package/dist/monetization/metering/reconciliation-repository.js +11 -10
  94. package/dist/monetization/metering/reconciliation-repository.test.js +9 -8
  95. package/dist/monetization/payram/webhook.d.ts +2 -2
  96. package/dist/monetization/payram/webhook.js +5 -1
  97. package/dist/monetization/payram/webhook.test.js +5 -4
  98. package/dist/monetization/promotions/engine.d.ts +2 -2
  99. package/dist/monetization/promotions/engine.js +4 -1
  100. package/dist/monetization/promotions/engine.test.js +3 -1
  101. package/dist/monetization/repository-types.d.ts +1 -1
  102. package/dist/monetization/stripe/stripe-payment-processor.d.ts +2 -2
  103. package/dist/monetization/stripe/stripe-payment-processor.test.js +7 -0
  104. package/dist/monetization/stripe/webhook.d.ts +2 -2
  105. package/dist/monetization/stripe/webhook.js +70 -6
  106. package/dist/monetization/stripe/webhook.test.js +20 -15
  107. package/dist/onboarding/onboarding-service.d.ts +2 -2
  108. package/dist/onboarding/onboarding-service.js +6 -2
  109. package/drizzle/migrations/0003_double_entry_ledger.sql +82 -0
  110. package/drizzle/migrations/meta/_journal.json +7 -0
  111. package/package.json +1 -1
  112. package/src/api/routes/admin-credits.ts +11 -14
  113. package/src/api/routes/quota.ts +2 -2
  114. package/src/api/routes/verify-email.ts +4 -4
  115. package/src/backup/on-demand-snapshot-service.test.ts +3 -3
  116. package/src/backup/on-demand-snapshot-service.ts +3 -3
  117. package/src/billing/payram/webhook.test.ts +7 -5
  118. package/src/billing/payram/webhook.ts +8 -11
  119. package/src/billing/stripe/stripe-payment-processor.test.ts +10 -3
  120. package/src/billing/stripe/stripe-payment-processor.ts +3 -3
  121. package/src/billing/stripe/tenant-store.ts +1 -1
  122. package/src/credits/auto-topup-charge.test.ts +7 -5
  123. package/src/credits/auto-topup-charge.ts +7 -10
  124. package/src/credits/auto-topup-usage.test.ts +55 -13
  125. package/src/credits/auto-topup-usage.ts +2 -2
  126. package/src/credits/credit-expiry-cron.test.ts +26 -45
  127. package/src/credits/credit-expiry-cron.ts +9 -12
  128. package/src/credits/credit-ledger.ts +3 -3
  129. package/src/credits/dividend-cron.test.ts +38 -45
  130. package/src/credits/dividend-cron.ts +12 -26
  131. package/src/credits/dividend-repository.test.ts +4 -3
  132. package/src/credits/dividend-repository.ts +21 -23
  133. package/src/credits/index.ts +23 -4
  134. package/src/credits/ledger.test.ts +514 -0
  135. package/src/credits/ledger.ts +851 -0
  136. package/src/credits/signup-grant.test.ts +7 -4
  137. package/src/credits/signup-grant.ts +6 -12
  138. package/src/credits/trial-balance-cron.test.ts +68 -0
  139. package/src/credits/trial-balance-cron.ts +46 -0
  140. package/src/db/schema/index.ts +1 -0
  141. package/src/db/schema/ledger.ts +94 -0
  142. package/src/gateway/credit-gate-wiring.test.ts +3 -3
  143. package/src/gateway/credit-gate.test.ts +35 -33
  144. package/src/gateway/credit-gate.ts +6 -10
  145. package/src/gateway/gateway-routes.test.ts +5 -5
  146. package/src/gateway/protocol/deps.ts +2 -2
  147. package/src/gateway/protocol/handlers.test.ts +549 -1
  148. package/src/gateway/proxy.ts +2 -2
  149. package/src/gateway/route-mounting.test.ts +2 -2
  150. package/src/gateway/types.ts +2 -2
  151. package/src/metering/reconciliation-cron.test.ts +10 -9
  152. package/src/metering/reconciliation-repository.test.ts +10 -9
  153. package/src/metering/reconciliation-repository.ts +14 -11
  154. package/src/monetization/affiliate/affiliate-admin-repository.test.ts +32 -19
  155. package/src/monetization/affiliate/affiliate-admin-repository.ts +16 -8
  156. package/src/monetization/affiliate/credit-match.test.ts +60 -14
  157. package/src/monetization/affiliate/credit-match.ts +6 -9
  158. package/src/monetization/affiliate/new-user-bonus.test.ts +6 -4
  159. package/src/monetization/affiliate/new-user-bonus.ts +6 -9
  160. package/src/monetization/credits/auto-topup-charge.test.ts +7 -5
  161. package/src/monetization/credits/auto-topup-charge.ts +7 -10
  162. package/src/monetization/credits/auto-topup-usage.test.ts +55 -13
  163. package/src/monetization/credits/auto-topup-usage.ts +2 -2
  164. package/src/monetization/credits/bot-billing.test.ts +20 -6
  165. package/src/monetization/credits/bot-billing.ts +3 -3
  166. package/src/monetization/credits/credit-expiry-cron.test.ts +26 -45
  167. package/src/monetization/credits/dividend-cron.test.ts +34 -48
  168. package/src/monetization/credits/dividend-cron.ts +9 -14
  169. package/src/monetization/credits/dividend-repository.test.ts +4 -3
  170. package/src/monetization/credits/dividend-repository.ts +19 -25
  171. package/src/monetization/credits/index.ts +4 -4
  172. package/src/monetization/credits/member-usage.test.ts +25 -11
  173. package/src/monetization/credits/phone-billing.test.ts +18 -26
  174. package/src/monetization/credits/phone-billing.ts +7 -10
  175. package/src/monetization/credits/runtime-cron.test.ts +29 -28
  176. package/src/monetization/credits/runtime-cron.ts +34 -58
  177. package/src/monetization/credits/runtime-scheduler.test.ts +1 -1
  178. package/src/monetization/credits/runtime-scheduler.ts +2 -2
  179. package/src/monetization/credits/signup-grant.test.ts +7 -4
  180. package/src/monetization/credits/storage-tier-cron.test.ts +5 -3
  181. package/src/monetization/credits/trial-balance-cron.test.ts +52 -0
  182. package/src/monetization/feature-gate.ts +3 -3
  183. package/src/monetization/index.ts +4 -4
  184. package/src/monetization/metering/reconciliation-cron.test.ts +10 -9
  185. package/src/monetization/metering/reconciliation-repository.test.ts +11 -9
  186. package/src/monetization/metering/reconciliation-repository.ts +13 -11
  187. package/src/monetization/payram/webhook.test.ts +7 -5
  188. package/src/monetization/payram/webhook.ts +7 -10
  189. package/src/monetization/promotions/engine.test.ts +6 -5
  190. package/src/monetization/promotions/engine.ts +6 -3
  191. package/src/monetization/repository-types.ts +1 -1
  192. package/src/monetization/stripe/stripe-payment-processor.test.ts +10 -3
  193. package/src/monetization/stripe/stripe-payment-processor.ts +3 -3
  194. package/src/monetization/stripe/webhook.test.ts +22 -16
  195. package/src/monetization/stripe/webhook.ts +75 -50
  196. package/src/onboarding/onboarding-service.ts +8 -11
  197. package/dist/credits/credit-ledger-extra.test.js +0 -40
  198. package/dist/credits/credit-ledger.bench.js +0 -33
  199. package/dist/credits/credit-ledger.test.d.ts +0 -4
  200. package/dist/credits/credit-ledger.test.js +0 -203
  201. package/dist/credits/credit-transaction-repository.test.js +0 -232
  202. package/dist/monetization/credits/credit-ledger-extra.test.d.ts +0 -1
  203. package/dist/monetization/credits/credit-ledger-extra.test.js +0 -39
  204. package/dist/monetization/credits/credit-ledger.bench.d.ts +0 -1
  205. package/dist/monetization/credits/credit-ledger.bench.js +0 -32
  206. package/dist/monetization/credits/credit-ledger.test.d.ts +0 -4
  207. package/dist/monetization/credits/credit-ledger.test.js +0 -202
  208. package/dist/monetization/credits/credit-transaction-repository.test.d.ts +0 -1
  209. package/dist/monetization/credits/credit-transaction-repository.test.js +0 -232
  210. package/src/credits/credit-ledger-extra.test.ts +0 -57
  211. package/src/credits/credit-ledger.bench.ts +0 -56
  212. package/src/credits/credit-ledger.test.ts +0 -276
  213. package/src/credits/credit-transaction-repository.test.ts +0 -274
  214. package/src/monetization/credits/credit-ledger-extra.test.ts +0 -56
  215. package/src/monetization/credits/credit-ledger.bench.ts +0 -55
  216. package/src/monetization/credits/credit-ledger.test.ts +0 -275
  217. package/src/monetization/credits/credit-transaction-repository.test.ts +0 -274
  218. /package/dist/credits/{credit-ledger-extra.test.d.ts → ledger.test.d.ts} +0 -0
  219. /package/dist/credits/{credit-ledger.bench.d.ts → trial-balance-cron.test.d.ts} +0 -0
  220. /package/dist/{credits/credit-transaction-repository.test.d.ts → monetization/credits/trial-balance-cron.test.d.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
1
+ import type { ILedger } from "@wopr-network/platform-core/credits";
2
2
  import { Credit } from "@wopr-network/platform-core/credits";
3
3
  import type { ICouponRepository } from "./coupon-repository.js";
4
4
  import type { IPromotionRepository, Promotion } from "./promotion-repository.js";
@@ -22,7 +22,7 @@ interface PromotionEngineDeps {
22
22
  promotionRepo: IPromotionRepository;
23
23
  couponRepo: ICouponRepository;
24
24
  redemptionRepo: IRedemptionRepository;
25
- ledger: ICreditLedger;
25
+ ledger: ILedger;
26
26
  }
27
27
 
28
28
  export class PromotionEngine {
@@ -113,7 +113,10 @@ export class PromotionEngine {
113
113
  if (!granted) return null;
114
114
 
115
115
  // Grant credits
116
- const tx = await ledger.credit(ctx.tenantId, grantAmount, "promo", `Promotion: ${promo.name}`, refId);
116
+ const tx = await ledger.credit(ctx.tenantId, grantAmount, "promo", {
117
+ description: `Promotion: ${promo.name}`,
118
+ referenceId: refId,
119
+ });
117
120
 
118
121
  // Record redemption
119
122
  await redemptionRepo.create({
@@ -5,7 +5,7 @@ export type {
5
5
  ITenantCustomerRepository,
6
6
  PayRamChargeRecord,
7
7
  } from "@wopr-network/platform-core/billing";
8
- export type { IAutoTopupSettingsRepository, ICreditLedger } from "@wopr-network/platform-core/credits";
8
+ export type { IAutoTopupSettingsRepository, ILedger } from "@wopr-network/platform-core/credits";
9
9
  export type { IMeterAggregator, IMeterEmitter } from "@wopr-network/platform-core/metering";
10
10
  export type { FraudEvent, FraudEventInput, IAffiliateFraudRepository } from "./affiliate/affiliate-fraud-repository.js";
11
11
  export type {
@@ -4,7 +4,7 @@ import type {
4
4
  IWebhookSeenRepository,
5
5
  } from "@wopr-network/platform-core/billing";
6
6
  import { PaymentMethodOwnershipError } from "@wopr-network/platform-core/billing";
7
- import type { CreditTransaction, ICreditLedger } from "@wopr-network/platform-core/credits";
7
+ import type { ILedger, JournalEntry } from "@wopr-network/platform-core/credits";
8
8
  import { Credit } from "@wopr-network/platform-core/credits";
9
9
  import type Stripe from "stripe";
10
10
  import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -61,7 +61,8 @@ function createMocks() {
61
61
  buildCustomerIdMap: vi.fn(),
62
62
  };
63
63
 
64
- const creditLedger: ICreditLedger = {
64
+ const creditLedger: ILedger = {
65
+ post: vi.fn(),
65
66
  credit: vi.fn(),
66
67
  debit: vi.fn(),
67
68
  balance: vi.fn(),
@@ -72,6 +73,12 @@ function createMocks() {
72
73
  expiredCredits: vi.fn(),
73
74
  lifetimeSpend: vi.fn(),
74
75
  lifetimeSpendBatch: vi.fn().mockResolvedValue(new Map()),
76
+ trialBalance: vi.fn(),
77
+ accountBalance: vi.fn(),
78
+ seedSystemAccounts: vi.fn(),
79
+ existsByReferenceIdLike: vi.fn(),
80
+ sumPurchasesForPeriod: vi.fn(),
81
+ getActiveTenantIdsInWindow: vi.fn(),
75
82
  };
76
83
 
77
84
  const replayGuard: IWebhookSeenRepository = {
@@ -340,7 +347,7 @@ describe("StripePaymentProcessor", () => {
340
347
  status: "succeeded",
341
348
  } as unknown as Stripe.Response<Stripe.PaymentIntent>);
342
349
  vi.mocked(mocks.creditLedger.hasReferenceId).mockResolvedValue(false);
343
- vi.mocked(mocks.creditLedger.credit).mockResolvedValue({} as unknown as CreditTransaction);
350
+ vi.mocked(mocks.creditLedger.credit).mockResolvedValue({} as unknown as JournalEntry);
344
351
  vi.mocked(mocks.autoTopupEventLog.writeEvent).mockResolvedValue(undefined);
345
352
 
346
353
  const result = await processor.charge({
@@ -17,7 +17,7 @@ import {
17
17
  type SetupResult,
18
18
  type WebhookResult,
19
19
  } from "@wopr-network/platform-core/billing";
20
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
20
+ import type { ILedger } from "@wopr-network/platform-core/credits";
21
21
  import { Credit } from "@wopr-network/platform-core/credits";
22
22
  import type Stripe from "stripe";
23
23
  import { chargeAutoTopup } from "../credits/auto-topup-charge.js";
@@ -30,7 +30,7 @@ export interface StripePaymentProcessorDeps {
30
30
  tenantRepo: ITenantCustomerRepository;
31
31
  webhookSecret: string;
32
32
  priceMap?: CreditPriceMap;
33
- creditLedger: ICreditLedger;
33
+ creditLedger: ILedger;
34
34
  botBilling?: BotBilling;
35
35
  replayGuard: IWebhookSeenRepository;
36
36
  autoTopupEventLog?: IAutoTopupEventLogRepository;
@@ -43,7 +43,7 @@ export class StripePaymentProcessor implements IPaymentProcessor {
43
43
  private readonly tenantRepo: ITenantCustomerRepository;
44
44
  private readonly webhookSecret: string;
45
45
  private readonly priceMap: CreditPriceMap;
46
- private readonly creditLedger: ICreditLedger;
46
+ private readonly creditLedger: ILedger;
47
47
  private readonly botBilling?: BotBilling;
48
48
  private readonly replayGuard: IWebhookSeenRepository;
49
49
  private readonly autoTopupEventLog?: IAutoTopupEventLogRepository;
@@ -11,7 +11,7 @@ import {
11
11
  noOpReplayGuard,
12
12
  TenantCustomerRepository,
13
13
  } from "@wopr-network/platform-core/billing";
14
- import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
14
+ import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
15
15
  import type Stripe from "stripe";
16
16
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
17
17
  import { createTestDb, truncateAllTables } from "../../test/db.js";
@@ -28,7 +28,7 @@ function makeReplayGuard() {
28
28
 
29
29
  describe("handleWebhookEvent (credit model)", () => {
30
30
  let tenantRepo: TenantCustomerRepository;
31
- let creditLedger: CreditLedger;
31
+ let creditLedger: DrizzleLedger;
32
32
  let deps: WebhookDeps;
33
33
 
34
34
  beforeAll(async () => {
@@ -44,7 +44,9 @@ describe("handleWebhookEvent (credit model)", () => {
44
44
  beforeEach(async () => {
45
45
  await truncateAllTables(pool);
46
46
  tenantRepo = new TenantCustomerRepository(db);
47
- creditLedger = new CreditLedger(db);
47
+ creditLedger = new DrizzleLedger(db);
48
+
49
+ await creditLedger.seedSystemAccounts();
48
50
  deps = { tenantRepo, creditLedger, replayGuard: noOpReplayGuard };
49
51
  });
50
52
 
@@ -227,7 +229,7 @@ describe("handleWebhookEvent (credit model)", () => {
227
229
  const txns = await creditLedger.history("tenant-123");
228
230
  expect(txns).toHaveLength(1);
229
231
  expect(txns[0].description).toContain("cs_test_abc");
230
- expect(txns[0].type).toBe("purchase");
232
+ expect(txns[0].entryType).toBe("purchase");
231
233
  expect(txns[0].referenceId).toBe("cs_test_abc");
232
234
  });
233
235
 
@@ -870,7 +872,7 @@ describe("handleWebhookEvent (credit model)", () => {
870
872
  const txns = await creditLedger.history("tenant-renew-ref");
871
873
  expect(txns).toHaveLength(1);
872
874
  expect(txns[0].referenceId).toBe("in_renewal_abc");
873
- expect(txns[0].type).toBe("purchase");
875
+ expect(txns[0].entryType).toBe("purchase");
874
876
  expect(txns[0].description).toContain("in_renewal_abc");
875
877
  });
876
878
 
@@ -913,7 +915,7 @@ describe("handleWebhookEvent (credit model)", () => {
913
915
 
914
916
  it("debits the credit ledger for the refunded amount", async () => {
915
917
  await tenantRepo.upsert({ tenant: "tenant-ref-1", processorCustomerId: "cus_ref_abc" });
916
- await creditLedger.credit("tenant-ref-1", Credit.fromCents(5000), "purchase", "seed");
918
+ await creditLedger.credit("tenant-ref-1", Credit.fromCents(5000), "purchase", { description: "seed" });
917
919
 
918
920
  const result = await handleWebhookEvent(deps, makeChargeRefundedEvent());
919
921
 
@@ -940,7 +942,7 @@ describe("handleWebhookEvent (credit model)", () => {
940
942
 
941
943
  it("is idempotent — skips duplicate refund for same charge ID", async () => {
942
944
  await tenantRepo.upsert({ tenant: "tenant-ref-idem", processorCustomerId: "cus_ref_idem" });
943
- await creditLedger.credit("tenant-ref-idem", Credit.fromCents(5000), "purchase", "seed");
945
+ await creditLedger.credit("tenant-ref-idem", Credit.fromCents(5000), "purchase", { description: "seed" });
944
946
 
945
947
  const event = makeChargeRefundedEvent({ customer: "cus_ref_idem" });
946
948
 
@@ -973,7 +975,7 @@ describe("handleWebhookEvent (credit model)", () => {
973
975
 
974
976
  it("handles customer object instead of string", async () => {
975
977
  await tenantRepo.upsert({ tenant: "tenant-ref-obj", processorCustomerId: "cus_ref_obj" });
976
- await creditLedger.credit("tenant-ref-obj", Credit.fromCents(3000), "purchase", "seed");
978
+ await creditLedger.credit("tenant-ref-obj", Credit.fromCents(3000), "purchase", { description: "seed" });
977
979
 
978
980
  const result = await handleWebhookEvent(
979
981
  deps,
@@ -986,14 +988,14 @@ describe("handleWebhookEvent (credit model)", () => {
986
988
 
987
989
  it("records event ID as referenceId in the ledger transaction", async () => {
988
990
  await tenantRepo.upsert({ tenant: "tenant-ref-txn", processorCustomerId: "cus_ref_txn" });
989
- await creditLedger.credit("tenant-ref-txn", Credit.fromCents(5000), "purchase", "seed");
991
+ await creditLedger.credit("tenant-ref-txn", Credit.fromCents(5000), "purchase", { description: "seed" });
990
992
 
991
993
  await handleWebhookEvent(deps, makeChargeRefundedEvent({ customer: "cus_ref_txn", id: "ch_ref_txn_123" }));
992
994
 
993
995
  const txns = await creditLedger.history("tenant-ref-txn", { type: "refund" });
994
996
  expect(txns).toHaveLength(1);
995
997
  expect(txns[0].referenceId).toBe("evt_charge_ref_1");
996
- expect(txns[0].type).toBe("refund");
998
+ expect(txns[0].entryType).toBe("refund");
997
999
  expect(txns[0].description).toContain("ch_ref_txn_123");
998
1000
  });
999
1001
  });
@@ -1076,7 +1078,11 @@ describe("handleWebhookEvent (credit model)", () => {
1076
1078
 
1077
1079
  it("skips credit when referenceId already exists (inline grant ran first)", async () => {
1078
1080
  // Simulate inline grant already happened
1079
- await creditLedger.credit("t1", Credit.fromCents(500), "purchase", "Auto-topup", "pi_already_granted", "stripe");
1081
+ await creditLedger.credit("t1", Credit.fromCents(500), "purchase", {
1082
+ description: "Auto-topup",
1083
+ referenceId: "pi_already_granted",
1084
+ fundingSource: "stripe",
1085
+ });
1080
1086
 
1081
1087
  const event = {
1082
1088
  id: "evt_pi_success_2",
@@ -1172,7 +1178,7 @@ describe("handleWebhookEvent (credit model)", () => {
1172
1178
 
1173
1179
  it("freezes tenant credits and suspends bots on dispute", async () => {
1174
1180
  await tenantRepo.upsert({ tenant: "tenant-dispute-1", processorCustomerId: "cus_dispute_abc" });
1175
- await creditLedger.credit("tenant-dispute-1", Credit.fromCents(5000), "purchase", "seed");
1181
+ await creditLedger.credit("tenant-dispute-1", Credit.fromCents(5000), "purchase", { description: "seed" });
1176
1182
 
1177
1183
  const botBilling = {
1178
1184
  suspendAllForTenant: vi.fn(async () => ["bot-d1"]),
@@ -1201,7 +1207,7 @@ describe("handleWebhookEvent (credit model)", () => {
1201
1207
 
1202
1208
  it("is idempotent — skips duplicate debit for same dispute ID", async () => {
1203
1209
  await tenantRepo.upsert({ tenant: "tenant-dispute-idem", processorCustomerId: "cus_dispute_idem" });
1204
- await creditLedger.credit("tenant-dispute-idem", Credit.fromCents(5000), "purchase", "seed");
1210
+ await creditLedger.credit("tenant-dispute-idem", Credit.fromCents(5000), "purchase", { description: "seed" });
1205
1211
 
1206
1212
  const event = makeDisputeCreatedEvent("cus_dispute_idem");
1207
1213
 
@@ -1214,7 +1220,7 @@ describe("handleWebhookEvent (credit model)", () => {
1214
1220
 
1215
1221
  it("sends admin notification when notificationService is available", async () => {
1216
1222
  await tenantRepo.upsert({ tenant: "tenant-dispute-notify", processorCustomerId: "cus_dispute_notify" });
1217
- await creditLedger.credit("tenant-dispute-notify", Credit.fromCents(5000), "purchase", "seed");
1223
+ await creditLedger.credit("tenant-dispute-notify", Credit.fromCents(5000), "purchase", { description: "seed" });
1218
1224
 
1219
1225
  const notifyFn = vi.fn();
1220
1226
  const notificationService = {
@@ -1267,7 +1273,7 @@ describe("handleWebhookEvent (credit model)", () => {
1267
1273
 
1268
1274
  it("handles customer object (expanded) inside charge", async () => {
1269
1275
  await tenantRepo.upsert({ tenant: "tenant-dispute-obj", processorCustomerId: "cus_dispute_obj" });
1270
- await creditLedger.credit("tenant-dispute-obj", Credit.fromCents(3000), "purchase", "seed");
1276
+ await creditLedger.credit("tenant-dispute-obj", Credit.fromCents(3000), "purchase", { description: "seed" });
1271
1277
 
1272
1278
  const result = await handleWebhookEvent(deps, makeDisputeCreatedEvent({ id: "cus_dispute_obj" }));
1273
1279
 
@@ -1277,7 +1283,7 @@ describe("handleWebhookEvent (credit model)", () => {
1277
1283
 
1278
1284
  it("works without botBilling (no suspension, still handled)", async () => {
1279
1285
  await tenantRepo.upsert({ tenant: "tenant-dispute-no-bb", processorCustomerId: "cus_dispute_no_bb" });
1280
- await creditLedger.credit("tenant-dispute-no-bb", Credit.fromCents(5000), "purchase", "seed");
1286
+ await creditLedger.credit("tenant-dispute-no-bb", Credit.fromCents(5000), "purchase", { description: "seed" });
1281
1287
 
1282
1288
  const result = await handleWebhookEvent(deps, makeDisputeCreatedEvent("cus_dispute_no_bb"));
1283
1289
 
@@ -3,7 +3,7 @@ import type {
3
3
  IWebhookSeenRepository,
4
4
  TenantCustomerRepository,
5
5
  } from "@wopr-network/platform-core/billing";
6
- import type { CreditLedger } from "@wopr-network/platform-core/credits";
6
+ import type { ILedger } from "@wopr-network/platform-core/credits";
7
7
  import { Credit } from "@wopr-network/platform-core/credits";
8
8
  import type { NotificationService } from "@wopr-network/platform-core/email";
9
9
  import type Stripe from "stripe";
@@ -43,7 +43,7 @@ export interface WebhookResult {
43
43
  */
44
44
  export interface WebhookDeps {
45
45
  tenantRepo: TenantCustomerRepository;
46
- creditLedger: CreditLedger;
46
+ creditLedger: ILedger;
47
47
  /** Map of Stripe Price ID -> CreditPricePoint for bonus calculation. */
48
48
  priceMap?: CreditPriceMap;
49
49
  /** Bot billing manager for reactivation after credit purchase (WOP-447). */
@@ -62,6 +62,46 @@ export interface WebhookDeps {
62
62
  promotionEngine?: PromotionEngine;
63
63
  }
64
64
 
65
+ /**
66
+ * Extract the card fingerprint from a Stripe webhook payload object.
67
+ * Works for PaymentIntent (via latest_charge or charges.data[0]) and
68
+ * Invoice (via charge) objects where the Charge is expanded.
69
+ * Returns undefined when the nested object is only a string ID (not expanded).
70
+ */
71
+ function extractStripeFingerprint(obj: unknown): string | undefined {
72
+ if (!obj || typeof obj !== "object") return undefined;
73
+ const o = obj as Record<string, unknown>;
74
+
75
+ // PaymentIntent: pi.latest_charge expanded → Charge.payment_method_details.card.fingerprint
76
+ const latestCharge = o.latest_charge;
77
+ if (latestCharge && typeof latestCharge === "object") {
78
+ const pmd = (latestCharge as Record<string, unknown>).payment_method_details as Record<string, unknown> | undefined;
79
+ const fp = (pmd?.card as Record<string, unknown> | undefined)?.fingerprint;
80
+ if (typeof fp === "string") return fp;
81
+ }
82
+
83
+ // PaymentIntent (older API): pi.charges.data[0].payment_method_details.card.fingerprint
84
+ const charges = o.charges as { data?: Array<Record<string, unknown>> } | undefined;
85
+ const charge0 = charges?.data?.[0];
86
+ if (charge0) {
87
+ const pmd = charge0.payment_method_details as Record<string, unknown> | undefined;
88
+ const fp = (pmd?.card as Record<string, unknown> | undefined)?.fingerprint;
89
+ if (typeof fp === "string") return fp;
90
+ }
91
+
92
+ // Invoice: invoice.charge expanded → Charge.payment_method_details.card.fingerprint
93
+ const invoiceCharge = o.charge;
94
+ if (invoiceCharge && typeof invoiceCharge === "object") {
95
+ const pmd = (invoiceCharge as Record<string, unknown>).payment_method_details as
96
+ | Record<string, unknown>
97
+ | undefined;
98
+ const fp = (pmd?.card as Record<string, unknown> | undefined)?.fingerprint;
99
+ if (typeof fp === "string") return fp;
100
+ }
101
+
102
+ return undefined;
103
+ }
104
+
65
105
  /**
66
106
  * Process a Stripe webhook event.
67
107
  *
@@ -133,14 +173,12 @@ export async function handleWebhookEvent(deps: WebhookDeps, event: Stripe.Event)
133
173
  }
134
174
 
135
175
  // Credit the ledger with session ID as reference for idempotency.
136
- await deps.creditLedger.credit(
137
- tenant,
138
- Credit.fromCents(creditCents),
139
- "purchase",
140
- `Stripe credit purchase (session: ${stripeSessionId})`,
141
- stripeSessionId,
142
- "stripe",
143
- );
176
+ await deps.creditLedger.credit(tenant, Credit.fromCents(creditCents), "purchase", {
177
+ description: `Stripe credit purchase (session: ${stripeSessionId})`,
178
+ referenceId: stripeSessionId,
179
+ fundingSource: "stripe",
180
+ stripeFingerprint: extractStripeFingerprint(session),
181
+ });
144
182
 
145
183
  // New-user first-purchase bonus for referred users (WOP-950).
146
184
  // Must run before credit match so markFirstPurchase hasn't been called yet.
@@ -292,14 +330,12 @@ export async function handleWebhookEvent(deps: WebhookDeps, event: Stripe.Event)
292
330
 
293
331
  // Fallback grant — inline path failed or process crashed before granting.
294
332
  const source = pi.metadata?.wopr_source ?? "auto_topup_webhook_fallback";
295
- await deps.creditLedger.credit(
296
- tenant,
297
- Credit.fromCents(pi.amount),
298
- "purchase",
299
- `Auto-topup webhook fallback (${source})`,
300
- pi.id,
301
- "stripe",
302
- );
333
+ await deps.creditLedger.credit(tenant, Credit.fromCents(pi.amount), "purchase", {
334
+ description: `Auto-topup webhook fallback (${source})`,
335
+ referenceId: pi.id,
336
+ fundingSource: "stripe",
337
+ stripeFingerprint: extractStripeFingerprint(pi),
338
+ });
303
339
 
304
340
  result = {
305
341
  handled: true,
@@ -379,14 +415,12 @@ export async function handleWebhookEvent(deps: WebhookDeps, event: Stripe.Event)
379
415
  break;
380
416
  }
381
417
 
382
- await deps.creditLedger.credit(
383
- tenant,
384
- Credit.fromCents(amountPaid),
385
- "purchase",
386
- `Stripe subscription renewal (invoice: ${invoice.id})`,
387
- invoice.id,
388
- "stripe",
389
- );
418
+ await deps.creditLedger.credit(tenant, Credit.fromCents(amountPaid), "purchase", {
419
+ description: `Stripe subscription renewal (invoice: ${invoice.id})`,
420
+ referenceId: invoice.id,
421
+ fundingSource: "stripe",
422
+ stripeFingerprint: extractStripeFingerprint(invoice),
423
+ });
390
424
 
391
425
  // Reactivate suspended bots now that balance is positive (WOP-447).
392
426
  let reactivatedBots: string[] | undefined;
@@ -437,14 +471,11 @@ export async function handleWebhookEvent(deps: WebhookDeps, event: Stripe.Event)
437
471
  }
438
472
 
439
473
  // Debit the ledger. Allow negative balance — refund must always succeed.
440
- await deps.creditLedger.debit(
441
- tenant,
442
- Credit.fromCents(refundedCents),
443
- "refund",
444
- `Stripe refund (charge: ${charge.id})`,
445
- event.id,
446
- true, // allowNegative
447
- );
474
+ await deps.creditLedger.debit(tenant, Credit.fromCents(refundedCents), "refund", {
475
+ description: `Stripe refund (charge: ${charge.id})`,
476
+ referenceId: event.id,
477
+ allowNegative: true,
478
+ });
448
479
 
449
480
  logger.warn("Charge refunded — credits debited", { tenant, customerId, chargeId: charge.id, refundedCents });
450
481
 
@@ -483,14 +514,11 @@ export async function handleWebhookEvent(deps: WebhookDeps, event: Stripe.Event)
483
514
 
484
515
  // Debit disputed amount (allow negative). Idempotent via disputeId.
485
516
  if (disputedCents > 0 && !(await deps.creditLedger.hasReferenceId(disputeId))) {
486
- await deps.creditLedger.debit(
487
- tenant,
488
- Credit.fromCents(disputedCents),
489
- "correction",
490
- `Stripe dispute (dispute: ${disputeId}, reason: ${dispute.reason})`,
491
- disputeId,
492
- true, // allowNegative
493
- );
517
+ await deps.creditLedger.debit(tenant, Credit.fromCents(disputedCents), "correction", {
518
+ description: `Stripe dispute (dispute: ${disputeId}, reason: ${dispute.reason})`,
519
+ referenceId: disputeId,
520
+ allowNegative: true,
521
+ });
494
522
  }
495
523
 
496
524
  // Suspend all bots (non-fatal if botBilling not provided).
@@ -554,14 +582,11 @@ export async function handleWebhookEvent(deps: WebhookDeps, event: Stripe.Event)
554
582
  // Re-credit the disputed amount. Idempotent via reversal referenceId.
555
583
  const reversalRef = `${disputeId}:reversal`;
556
584
  if (disputedCents > 0 && !(await deps.creditLedger.hasReferenceId(reversalRef))) {
557
- await deps.creditLedger.credit(
558
- tenant,
559
- Credit.fromCents(disputedCents),
560
- "correction",
561
- `Stripe dispute won — credits restored (dispute: ${disputeId})`,
562
- reversalRef,
563
- "stripe",
564
- );
585
+ await deps.creditLedger.credit(tenant, Credit.fromCents(disputedCents), "correction", {
586
+ description: `Stripe dispute won — credits restored (dispute: ${disputeId})`,
587
+ referenceId: reversalRef,
588
+ fundingSource: "stripe",
589
+ });
565
590
  }
566
591
 
567
592
  // Reactivate bots (non-fatal).
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
2
+ import type { ILedger } from "@wopr-network/platform-core/credits";
3
3
  import { Credit } from "@wopr-network/platform-core/credits";
4
4
  import type { BudgetTier } from "../inference/budget-guard.js";
5
5
  import { checkSessionBudget } from "../inference/budget-guard.js";
@@ -30,7 +30,7 @@ export class OnboardingService {
30
30
  private readonly daemon: IDaemonManager,
31
31
  private readonly usageRepo?: ISessionUsageRepository,
32
32
  private readonly scriptRepo: IOnboardingScriptRepository = OnboardingService.DEFAULT_SCRIPT_REPO,
33
- private readonly creditLedger?: ICreditLedger,
33
+ private readonly creditLedger?: ILedger,
34
34
  private readonly resolveTenantId?: (userId: string) => Promise<string | null>,
35
35
  ) {}
36
36
 
@@ -137,15 +137,12 @@ export class OnboardingService {
137
137
  if (costCents > 0) {
138
138
  const tenantId = await this.resolveTenantId(session.userId);
139
139
  if (tenantId) {
140
- await this.creditLedger.debit(
141
- tenantId,
142
- Credit.fromCents(costCents),
143
- "onboarding_llm",
144
- `Onboarding session ${sessionId}`,
145
- `onboarding-${sessionId}-${Date.now()}`,
146
- true, // allowNegative — don't block onboarding mid-conversation
147
- session.userId,
148
- );
140
+ await this.creditLedger.debit(tenantId, Credit.fromCents(costCents), "onboarding_llm", {
141
+ description: `Onboarding session ${sessionId}`,
142
+ referenceId: `onboarding-${sessionId}-${Date.now()}`,
143
+ allowNegative: true,
144
+ attributedUserId: session.userId,
145
+ });
149
146
  }
150
147
  }
151
148
  }
@@ -1,40 +0,0 @@
1
- import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
2
- import { beginTestTransaction, createTestDb, endTestTransaction, rollbackTestTransaction } from "../test/db.js";
3
- import { Credit } from "./credit.js";
4
- import { CreditLedger, InsufficientBalanceError } from "./credit-ledger.js";
5
- let pool;
6
- let db;
7
- beforeAll(async () => {
8
- ({ db, pool } = await createTestDb());
9
- await beginTestTransaction(pool);
10
- });
11
- afterAll(async () => {
12
- await endTestTransaction(pool);
13
- await pool.close();
14
- });
15
- describe("CreditLedger concurrent debit safety", () => {
16
- let ledger;
17
- beforeEach(async () => {
18
- await rollbackTestTransaction(pool);
19
- ledger = new CreditLedger(db);
20
- });
21
- it("concurrent debits do not overdraw — at least one should fail with InsufficientBalanceError", async () => {
22
- // Fund with exactly 100 cents
23
- await ledger.credit("t1", Credit.fromCents(100), "purchase");
24
- // Fire two 100-cent debits concurrently — only one can succeed
25
- const results = await Promise.allSettled([
26
- ledger.debit("t1", Credit.fromCents(100), "bot_runtime", "debit-1"),
27
- ledger.debit("t1", Credit.fromCents(100), "bot_runtime", "debit-2"),
28
- ]);
29
- const fulfilled = results.filter((r) => r.status === "fulfilled");
30
- const rejected = results.filter((r) => r.status === "rejected");
31
- // Exactly one succeeds, one fails (PGlite serializes transactions so this is deterministic)
32
- expect(fulfilled).toHaveLength(1);
33
- expect(rejected).toHaveLength(1);
34
- const err = rejected[0].reason;
35
- expect(err).toBeInstanceOf(InsufficientBalanceError);
36
- // Final balance must be exactly 0, not negative
37
- const bal = await ledger.balance("t1");
38
- expect(bal.toCents()).toBe(0);
39
- });
40
- });
@@ -1,33 +0,0 @@
1
- import { afterAll, beforeAll, beforeEach, bench, describe } from "vitest";
2
- import { createTestDb, truncateAllTables } from "../test/db.js";
3
- import { Credit } from "./credit.js";
4
- import { CreditLedger } from "./credit-ledger.js";
5
- let db;
6
- let pool;
7
- beforeAll(async () => {
8
- ({ db, pool } = await createTestDb());
9
- });
10
- afterAll(async () => {
11
- await pool.close();
12
- });
13
- describe("CreditLedger throughput", () => {
14
- let ledger;
15
- beforeEach(async () => {
16
- await truncateAllTables(pool);
17
- ledger = new CreditLedger(db);
18
- });
19
- let creditIdx = 0;
20
- let debitIdx = 0;
21
- bench("credit operation", async () => {
22
- const tenant = `tenant-${creditIdx++ % 100}`;
23
- await ledger.credit(tenant, Credit.fromCents(100), "purchase", "bench");
24
- }, { iterations: 1_000 });
25
- bench("debit operation", async () => {
26
- const tenant = `tenant-${debitIdx++ % 100}`;
27
- await ledger.debit(tenant, Credit.fromCents(1), "adapter_usage", "bench");
28
- }, { iterations: 1_000 });
29
- bench("balance query", async () => {
30
- const tenant = `tenant-${debitIdx++ % 100}`;
31
- await ledger.balance(tenant);
32
- }, { iterations: 5_000 });
33
- });
@@ -1,4 +0,0 @@
1
- /**
2
- * Tests for CreditLedger — including the allowNegative debit parameter (WOP-821).
3
- */
4
- export {};