@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
@@ -82,7 +82,10 @@ export class PromotionEngine {
82
82
  if (!granted)
83
83
  return null;
84
84
  // Grant credits
85
- const tx = await ledger.credit(ctx.tenantId, grantAmount, "promo", `Promotion: ${promo.name}`, refId);
85
+ const tx = await ledger.credit(ctx.tenantId, grantAmount, "promo", {
86
+ description: `Promotion: ${promo.name}`,
87
+ referenceId: refId,
88
+ });
86
89
  // Record redemption
87
90
  await redemptionRepo.create({
88
91
  promotionId: promo.id,
@@ -96,7 +96,9 @@ describe("PromotionEngine", () => {
96
96
  });
97
97
  expect(results).toHaveLength(1);
98
98
  expect(ledger.credit).toHaveBeenCalledWith("tenant-1", expect.any(Object), // Credit instance
99
- "promo", expect.any(String), "promo:promo-1:tenant-1:1");
99
+ "promo", expect.objectContaining({
100
+ referenceId: "promo:promo-1:tenant-1:1",
101
+ }));
100
102
  });
101
103
  it("skips if already redeemed (idempotency)", async () => {
102
104
  vi.mocked(ledger.hasReferenceId).mockResolvedValue(true);
@@ -1,5 +1,5 @@
1
1
  export type { IPayRamChargeRepository, ITenantCustomerRepository, PayRamChargeRecord, } from "@wopr-network/platform-core/billing";
2
- export type { IAutoTopupSettingsRepository, ICreditLedger } from "@wopr-network/platform-core/credits";
2
+ export type { IAutoTopupSettingsRepository, ILedger } from "@wopr-network/platform-core/credits";
3
3
  export type { IMeterAggregator, IMeterEmitter } from "@wopr-network/platform-core/metering";
4
4
  export type { FraudEvent, FraudEventInput, IAffiliateFraudRepository } from "./affiliate/affiliate-fraud-repository.js";
5
5
  export type { AffiliateCode, AffiliateReferral, AffiliateStats, IAffiliateRepository, } from "./affiliate/drizzle-affiliate-repository.js";
@@ -12,12 +12,12 @@
12
12
  import type { MeterEmitter } from "@wopr-network/platform-core/metering";
13
13
  import type { AdapterCapability, ProviderAdapter } from "../adapters/types.js";
14
14
  import type { ArbitrageRouter } from "../arbitrage/router.js";
15
- import type { BudgetChecker, SpendLimits } from "../budget/budget-checker.js";
15
+ import type { IBudgetChecker, SpendLimits } from "../budget/budget-checker.js";
16
16
  export interface SocketConfig {
17
17
  /** MeterEmitter instance for usage tracking */
18
18
  meter: MeterEmitter;
19
- /** BudgetChecker instance for pre-call budget validation */
20
- budgetChecker?: BudgetChecker;
19
+ /** IBudgetChecker instance for pre-call budget validation */
20
+ budgetChecker?: IBudgetChecker;
21
21
  /** Default margin multiplier (default: 1.3) */
22
22
  defaultMargin?: number;
23
23
  /** ArbitrageRouter for cost-optimized routing (GPU-first, cheapest, 5xx failover) */
@@ -1,6 +1,6 @@
1
1
  import type { IWebhookSeenRepository } from "@wopr-network/platform-core/billing";
2
2
  import { type ChargeOpts, type ChargeResult, type CheckoutOpts, type CheckoutSession, type CreditPriceMap, type Invoice, type IPaymentProcessor, type ITenantCustomerRepository, type PortalOpts, type SavedPaymentMethod, type SetupResult, type WebhookResult } from "@wopr-network/platform-core/billing";
3
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
3
+ import type { ILedger } from "@wopr-network/platform-core/credits";
4
4
  import type Stripe from "stripe";
5
5
  import type { IAutoTopupEventLogRepository } from "../credits/auto-topup-event-log-repository.js";
6
6
  import type { BotBilling } from "../credits/bot-billing.js";
@@ -9,7 +9,7 @@ export interface StripePaymentProcessorDeps {
9
9
  tenantRepo: ITenantCustomerRepository;
10
10
  webhookSecret: string;
11
11
  priceMap?: CreditPriceMap;
12
- creditLedger: ICreditLedger;
12
+ creditLedger: ILedger;
13
13
  botBilling?: BotBilling;
14
14
  replayGuard: IWebhookSeenRepository;
15
15
  autoTopupEventLog?: IAutoTopupEventLogRepository;
@@ -45,6 +45,7 @@ function createMocks() {
45
45
  buildCustomerIdMap: vi.fn(),
46
46
  };
47
47
  const creditLedger = {
48
+ post: vi.fn(),
48
49
  credit: vi.fn(),
49
50
  debit: vi.fn(),
50
51
  balance: vi.fn(),
@@ -55,6 +56,12 @@ function createMocks() {
55
56
  expiredCredits: vi.fn(),
56
57
  lifetimeSpend: vi.fn(),
57
58
  lifetimeSpendBatch: vi.fn().mockResolvedValue(new Map()),
59
+ trialBalance: vi.fn(),
60
+ accountBalance: vi.fn(),
61
+ seedSystemAccounts: vi.fn(),
62
+ existsByReferenceIdLike: vi.fn(),
63
+ sumPurchasesForPeriod: vi.fn(),
64
+ getActiveTenantIdsInWindow: vi.fn(),
58
65
  };
59
66
  const replayGuard = {
60
67
  isDuplicate: vi.fn(),
@@ -1,5 +1,5 @@
1
1
  import type { CreditPriceMap, IWebhookSeenRepository, TenantCustomerRepository } from "@wopr-network/platform-core/billing";
2
- import type { CreditLedger } 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 { NotificationService } from "@wopr-network/platform-core/email";
5
5
  import type Stripe from "stripe";
@@ -34,7 +34,7 @@ export interface WebhookResult {
34
34
  */
35
35
  export interface WebhookDeps {
36
36
  tenantRepo: TenantCustomerRepository;
37
- creditLedger: CreditLedger;
37
+ creditLedger: ILedger;
38
38
  /** Map of Stripe Price ID -> CreditPricePoint for bonus calculation. */
39
39
  priceMap?: CreditPriceMap;
40
40
  /** Bot billing manager for reactivation after credit purchase (WOP-447). */
@@ -2,6 +2,43 @@ import { Credit } from "@wopr-network/platform-core/credits";
2
2
  import { logger } from "../../config/logger.js";
3
3
  import { processAffiliateCreditMatch } from "../affiliate/credit-match.js";
4
4
  import { grantNewUserBonus } from "../affiliate/new-user-bonus.js";
5
+ /**
6
+ * Extract the card fingerprint from a Stripe webhook payload object.
7
+ * Works for PaymentIntent (via latest_charge or charges.data[0]) and
8
+ * Invoice (via charge) objects where the Charge is expanded.
9
+ * Returns undefined when the nested object is only a string ID (not expanded).
10
+ */
11
+ function extractStripeFingerprint(obj) {
12
+ if (!obj || typeof obj !== "object")
13
+ return undefined;
14
+ const o = obj;
15
+ // PaymentIntent: pi.latest_charge expanded → Charge.payment_method_details.card.fingerprint
16
+ const latestCharge = o.latest_charge;
17
+ if (latestCharge && typeof latestCharge === "object") {
18
+ const pmd = latestCharge.payment_method_details;
19
+ const fp = pmd?.card?.fingerprint;
20
+ if (typeof fp === "string")
21
+ return fp;
22
+ }
23
+ // PaymentIntent (older API): pi.charges.data[0].payment_method_details.card.fingerprint
24
+ const charges = o.charges;
25
+ const charge0 = charges?.data?.[0];
26
+ if (charge0) {
27
+ const pmd = charge0.payment_method_details;
28
+ const fp = pmd?.card?.fingerprint;
29
+ if (typeof fp === "string")
30
+ return fp;
31
+ }
32
+ // Invoice: invoice.charge expanded → Charge.payment_method_details.card.fingerprint
33
+ const invoiceCharge = o.charge;
34
+ if (invoiceCharge && typeof invoiceCharge === "object") {
35
+ const pmd = invoiceCharge.payment_method_details;
36
+ const fp = pmd?.card?.fingerprint;
37
+ if (typeof fp === "string")
38
+ return fp;
39
+ }
40
+ return undefined;
41
+ }
5
42
  /**
6
43
  * Process a Stripe webhook event.
7
44
  *
@@ -64,7 +101,12 @@ export async function handleWebhookEvent(deps, event) {
64
101
  creditCents = amountPaid;
65
102
  }
66
103
  // Credit the ledger with session ID as reference for idempotency.
67
- await deps.creditLedger.credit(tenant, Credit.fromCents(creditCents), "purchase", `Stripe credit purchase (session: ${stripeSessionId})`, stripeSessionId, "stripe");
104
+ await deps.creditLedger.credit(tenant, Credit.fromCents(creditCents), "purchase", {
105
+ description: `Stripe credit purchase (session: ${stripeSessionId})`,
106
+ referenceId: stripeSessionId,
107
+ fundingSource: "stripe",
108
+ stripeFingerprint: extractStripeFingerprint(session),
109
+ });
68
110
  // New-user first-purchase bonus for referred users (WOP-950).
69
111
  // Must run before credit match so markFirstPurchase hasn't been called yet.
70
112
  let affiliateBonusCredit;
@@ -198,7 +240,12 @@ export async function handleWebhookEvent(deps, event) {
198
240
  }
199
241
  // Fallback grant — inline path failed or process crashed before granting.
200
242
  const source = pi.metadata?.wopr_source ?? "auto_topup_webhook_fallback";
201
- await deps.creditLedger.credit(tenant, Credit.fromCents(pi.amount), "purchase", `Auto-topup webhook fallback (${source})`, pi.id, "stripe");
243
+ await deps.creditLedger.credit(tenant, Credit.fromCents(pi.amount), "purchase", {
244
+ description: `Auto-topup webhook fallback (${source})`,
245
+ referenceId: pi.id,
246
+ fundingSource: "stripe",
247
+ stripeFingerprint: extractStripeFingerprint(pi),
248
+ });
202
249
  result = {
203
250
  handled: true,
204
251
  event_type: event.type,
@@ -261,7 +308,12 @@ export async function handleWebhookEvent(deps, event) {
261
308
  result = { handled: true, event_type: event.type, tenant, creditedCents: 0 };
262
309
  break;
263
310
  }
264
- await deps.creditLedger.credit(tenant, Credit.fromCents(amountPaid), "purchase", `Stripe subscription renewal (invoice: ${invoice.id})`, invoice.id, "stripe");
311
+ await deps.creditLedger.credit(tenant, Credit.fromCents(amountPaid), "purchase", {
312
+ description: `Stripe subscription renewal (invoice: ${invoice.id})`,
313
+ referenceId: invoice.id,
314
+ fundingSource: "stripe",
315
+ stripeFingerprint: extractStripeFingerprint(invoice),
316
+ });
265
317
  // Reactivate suspended bots now that balance is positive (WOP-447).
266
318
  let reactivatedBots;
267
319
  if (deps.botBilling) {
@@ -304,7 +356,11 @@ export async function handleWebhookEvent(deps, event) {
304
356
  break;
305
357
  }
306
358
  // Debit the ledger. Allow negative balance — refund must always succeed.
307
- await deps.creditLedger.debit(tenant, Credit.fromCents(refundedCents), "refund", `Stripe refund (charge: ${charge.id})`, event.id, true);
359
+ await deps.creditLedger.debit(tenant, Credit.fromCents(refundedCents), "refund", {
360
+ description: `Stripe refund (charge: ${charge.id})`,
361
+ referenceId: event.id,
362
+ allowNegative: true,
363
+ });
308
364
  logger.warn("Charge refunded — credits debited", { tenant, customerId, chargeId: charge.id, refundedCents });
309
365
  result = { handled: true, event_type: event.type, tenant, debitedCents: refundedCents };
310
366
  break;
@@ -334,7 +390,11 @@ export async function handleWebhookEvent(deps, event) {
334
390
  await deps.tenantRepo.setBillingHold(tenant, true);
335
391
  // Debit disputed amount (allow negative). Idempotent via disputeId.
336
392
  if (disputedCents > 0 && !(await deps.creditLedger.hasReferenceId(disputeId))) {
337
- await deps.creditLedger.debit(tenant, Credit.fromCents(disputedCents), "correction", `Stripe dispute (dispute: ${disputeId}, reason: ${dispute.reason})`, disputeId, true);
393
+ await deps.creditLedger.debit(tenant, Credit.fromCents(disputedCents), "correction", {
394
+ description: `Stripe dispute (dispute: ${disputeId}, reason: ${dispute.reason})`,
395
+ referenceId: disputeId,
396
+ allowNegative: true,
397
+ });
338
398
  }
339
399
  // Suspend all bots (non-fatal if botBilling not provided).
340
400
  let suspendedBots;
@@ -388,7 +448,11 @@ export async function handleWebhookEvent(deps, event) {
388
448
  // Re-credit the disputed amount. Idempotent via reversal referenceId.
389
449
  const reversalRef = `${disputeId}:reversal`;
390
450
  if (disputedCents > 0 && !(await deps.creditLedger.hasReferenceId(reversalRef))) {
391
- await deps.creditLedger.credit(tenant, Credit.fromCents(disputedCents), "correction", `Stripe dispute won — credits restored (dispute: ${disputeId})`, reversalRef, "stripe");
451
+ await deps.creditLedger.credit(tenant, Credit.fromCents(disputedCents), "correction", {
452
+ description: `Stripe dispute won — credits restored (dispute: ${disputeId})`,
453
+ referenceId: reversalRef,
454
+ fundingSource: "stripe",
455
+ });
392
456
  }
393
457
  // Reactivate bots (non-fatal).
394
458
  let reactivatedBots;
@@ -1,5 +1,5 @@
1
1
  import { CREDIT_PRICE_POINTS, DrizzleWebhookSeenRepository, noOpReplayGuard, TenantCustomerRepository, } from "@wopr-network/platform-core/billing";
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, vi } from "vitest";
4
4
  import { createTestDb, truncateAllTables } from "../../test/db.js";
5
5
  import { DrizzleAffiliateRepository } from "../affiliate/drizzle-affiliate-repository.js";
@@ -24,7 +24,8 @@ describe("handleWebhookEvent (credit model)", () => {
24
24
  beforeEach(async () => {
25
25
  await truncateAllTables(pool);
26
26
  tenantRepo = new TenantCustomerRepository(db);
27
- creditLedger = new CreditLedger(db);
27
+ creditLedger = new DrizzleLedger(db);
28
+ await creditLedger.seedSystemAccounts();
28
29
  deps = { tenantRepo, creditLedger, replayGuard: noOpReplayGuard };
29
30
  });
30
31
  // ---------------------------------------------------------------------------
@@ -170,7 +171,7 @@ describe("handleWebhookEvent (credit model)", () => {
170
171
  const txns = await creditLedger.history("tenant-123");
171
172
  expect(txns).toHaveLength(1);
172
173
  expect(txns[0].description).toContain("cs_test_abc");
173
- expect(txns[0].type).toBe("purchase");
174
+ expect(txns[0].entryType).toBe("purchase");
174
175
  expect(txns[0].referenceId).toBe("cs_test_abc");
175
176
  });
176
177
  it("grants affiliate match credits on first purchase for referred tenant (WOP-949)", async () => {
@@ -656,7 +657,7 @@ describe("handleWebhookEvent (credit model)", () => {
656
657
  const txns = await creditLedger.history("tenant-renew-ref");
657
658
  expect(txns).toHaveLength(1);
658
659
  expect(txns[0].referenceId).toBe("in_renewal_abc");
659
- expect(txns[0].type).toBe("purchase");
660
+ expect(txns[0].entryType).toBe("purchase");
660
661
  expect(txns[0].description).toContain("in_renewal_abc");
661
662
  });
662
663
  it("handles missing botBilling gracefully (no reactivation, still credits)", async () => {
@@ -690,7 +691,7 @@ describe("handleWebhookEvent (credit model)", () => {
690
691
  }
691
692
  it("debits the credit ledger for the refunded amount", async () => {
692
693
  await tenantRepo.upsert({ tenant: "tenant-ref-1", processorCustomerId: "cus_ref_abc" });
693
- await creditLedger.credit("tenant-ref-1", Credit.fromCents(5000), "purchase", "seed");
694
+ await creditLedger.credit("tenant-ref-1", Credit.fromCents(5000), "purchase", { description: "seed" });
694
695
  const result = await handleWebhookEvent(deps, makeChargeRefundedEvent());
695
696
  expect(result.handled).toBe(true);
696
697
  expect(result.event_type).toBe("charge.refunded");
@@ -707,7 +708,7 @@ describe("handleWebhookEvent (credit model)", () => {
707
708
  });
708
709
  it("is idempotent — skips duplicate refund for same charge ID", async () => {
709
710
  await tenantRepo.upsert({ tenant: "tenant-ref-idem", processorCustomerId: "cus_ref_idem" });
710
- await creditLedger.credit("tenant-ref-idem", Credit.fromCents(5000), "purchase", "seed");
711
+ await creditLedger.credit("tenant-ref-idem", Credit.fromCents(5000), "purchase", { description: "seed" });
711
712
  const event = makeChargeRefundedEvent({ customer: "cus_ref_idem" });
712
713
  const first = await handleWebhookEvent(deps, event);
713
714
  expect(first.debitedCents).toBe(2500);
@@ -727,19 +728,19 @@ describe("handleWebhookEvent (credit model)", () => {
727
728
  });
728
729
  it("handles customer object instead of string", async () => {
729
730
  await tenantRepo.upsert({ tenant: "tenant-ref-obj", processorCustomerId: "cus_ref_obj" });
730
- await creditLedger.credit("tenant-ref-obj", Credit.fromCents(3000), "purchase", "seed");
731
+ await creditLedger.credit("tenant-ref-obj", Credit.fromCents(3000), "purchase", { description: "seed" });
731
732
  const result = await handleWebhookEvent(deps, makeChargeRefundedEvent({ customer: { id: "cus_ref_obj" }, amount_refunded: 1500 }));
732
733
  expect(result.handled).toBe(true);
733
734
  expect(result.debitedCents).toBe(1500);
734
735
  });
735
736
  it("records event ID as referenceId in the ledger transaction", async () => {
736
737
  await tenantRepo.upsert({ tenant: "tenant-ref-txn", processorCustomerId: "cus_ref_txn" });
737
- await creditLedger.credit("tenant-ref-txn", Credit.fromCents(5000), "purchase", "seed");
738
+ await creditLedger.credit("tenant-ref-txn", Credit.fromCents(5000), "purchase", { description: "seed" });
738
739
  await handleWebhookEvent(deps, makeChargeRefundedEvent({ customer: "cus_ref_txn", id: "ch_ref_txn_123" }));
739
740
  const txns = await creditLedger.history("tenant-ref-txn", { type: "refund" });
740
741
  expect(txns).toHaveLength(1);
741
742
  expect(txns[0].referenceId).toBe("evt_charge_ref_1");
742
- expect(txns[0].type).toBe("refund");
743
+ expect(txns[0].entryType).toBe("refund");
743
744
  expect(txns[0].description).toContain("ch_ref_txn_123");
744
745
  });
745
746
  });
@@ -810,7 +811,11 @@ describe("handleWebhookEvent (credit model)", () => {
810
811
  });
811
812
  it("skips credit when referenceId already exists (inline grant ran first)", async () => {
812
813
  // Simulate inline grant already happened
813
- await creditLedger.credit("t1", Credit.fromCents(500), "purchase", "Auto-topup", "pi_already_granted", "stripe");
814
+ await creditLedger.credit("t1", Credit.fromCents(500), "purchase", {
815
+ description: "Auto-topup",
816
+ referenceId: "pi_already_granted",
817
+ fundingSource: "stripe",
818
+ });
814
819
  const event = {
815
820
  id: "evt_pi_success_2",
816
821
  type: "payment_intent.succeeded",
@@ -891,7 +896,7 @@ describe("handleWebhookEvent (credit model)", () => {
891
896
  }
892
897
  it("freezes tenant credits and suspends bots on dispute", async () => {
893
898
  await tenantRepo.upsert({ tenant: "tenant-dispute-1", processorCustomerId: "cus_dispute_abc" });
894
- await creditLedger.credit("tenant-dispute-1", Credit.fromCents(5000), "purchase", "seed");
899
+ await creditLedger.credit("tenant-dispute-1", Credit.fromCents(5000), "purchase", { description: "seed" });
895
900
  const botBilling = {
896
901
  suspendAllForTenant: vi.fn(async () => ["bot-d1"]),
897
902
  };
@@ -912,7 +917,7 @@ describe("handleWebhookEvent (credit model)", () => {
912
917
  });
913
918
  it("is idempotent — skips duplicate debit for same dispute ID", async () => {
914
919
  await tenantRepo.upsert({ tenant: "tenant-dispute-idem", processorCustomerId: "cus_dispute_idem" });
915
- await creditLedger.credit("tenant-dispute-idem", Credit.fromCents(5000), "purchase", "seed");
920
+ await creditLedger.credit("tenant-dispute-idem", Credit.fromCents(5000), "purchase", { description: "seed" });
916
921
  const event = makeDisputeCreatedEvent("cus_dispute_idem");
917
922
  await handleWebhookEvent(deps, event);
918
923
  await handleWebhookEvent(deps, event);
@@ -921,7 +926,7 @@ describe("handleWebhookEvent (credit model)", () => {
921
926
  });
922
927
  it("sends admin notification when notificationService is available", async () => {
923
928
  await tenantRepo.upsert({ tenant: "tenant-dispute-notify", processorCustomerId: "cus_dispute_notify" });
924
- await creditLedger.credit("tenant-dispute-notify", Credit.fromCents(5000), "purchase", "seed");
929
+ await creditLedger.credit("tenant-dispute-notify", Credit.fromCents(5000), "purchase", { description: "seed" });
925
930
  const notifyFn = vi.fn();
926
931
  const notificationService = {
927
932
  notifyDisputeCreated: notifyFn,
@@ -957,14 +962,14 @@ describe("handleWebhookEvent (credit model)", () => {
957
962
  });
958
963
  it("handles customer object (expanded) inside charge", async () => {
959
964
  await tenantRepo.upsert({ tenant: "tenant-dispute-obj", processorCustomerId: "cus_dispute_obj" });
960
- await creditLedger.credit("tenant-dispute-obj", Credit.fromCents(3000), "purchase", "seed");
965
+ await creditLedger.credit("tenant-dispute-obj", Credit.fromCents(3000), "purchase", { description: "seed" });
961
966
  const result = await handleWebhookEvent(deps, makeDisputeCreatedEvent({ id: "cus_dispute_obj" }));
962
967
  expect(result.handled).toBe(true);
963
968
  expect(result.tenant).toBe("tenant-dispute-obj");
964
969
  });
965
970
  it("works without botBilling (no suspension, still handled)", async () => {
966
971
  await tenantRepo.upsert({ tenant: "tenant-dispute-no-bb", processorCustomerId: "cus_dispute_no_bb" });
967
- await creditLedger.credit("tenant-dispute-no-bb", Credit.fromCents(5000), "purchase", "seed");
972
+ await creditLedger.credit("tenant-dispute-no-bb", Credit.fromCents(5000), "purchase", { description: "seed" });
968
973
  const result = await handleWebhookEvent(deps, makeDisputeCreatedEvent("cus_dispute_no_bb"));
969
974
  expect(result.handled).toBe(true);
970
975
  expect(result.suspendedBots).toBeUndefined();
@@ -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 type { ISessionUsageRepository } from "../inference/session-usage-repository.js";
3
3
  import type { OnboardingConfig } from "./config.js";
4
4
  import type { IDaemonManager } from "./daemon-manager.js";
@@ -16,7 +16,7 @@ export declare class OnboardingService {
16
16
  private readonly resolveTenantId?;
17
17
  private static readonly DEFAULT_PROMPT;
18
18
  private static readonly DEFAULT_SCRIPT_REPO;
19
- constructor(repo: IOnboardingSessionRepository, client: IWoprClient, config: OnboardingConfig, daemon: IDaemonManager, usageRepo?: ISessionUsageRepository | undefined, scriptRepo?: IOnboardingScriptRepository, creditLedger?: ICreditLedger | undefined, resolveTenantId?: ((userId: string) => Promise<string | null>) | undefined);
19
+ constructor(repo: IOnboardingSessionRepository, client: IWoprClient, config: OnboardingConfig, daemon: IDaemonManager, usageRepo?: ISessionUsageRepository | undefined, scriptRepo?: IOnboardingScriptRepository, creditLedger?: ILedger | undefined, resolveTenantId?: ((userId: string) => Promise<string | null>) | undefined);
20
20
  createSession(opts: {
21
21
  userId?: string;
22
22
  anonymousId?: string;
@@ -120,8 +120,12 @@ export class OnboardingService {
120
120
  if (costCents > 0) {
121
121
  const tenantId = await this.resolveTenantId(session.userId);
122
122
  if (tenantId) {
123
- await this.creditLedger.debit(tenantId, Credit.fromCents(costCents), "onboarding_llm", `Onboarding session ${sessionId}`, `onboarding-${sessionId}-${Date.now()}`, true, // allowNegative — don't block onboarding mid-conversation
124
- session.userId);
123
+ await this.creditLedger.debit(tenantId, Credit.fromCents(costCents), "onboarding_llm", {
124
+ description: `Onboarding session ${sessionId}`,
125
+ referenceId: `onboarding-${sessionId}-${Date.now()}`,
126
+ allowNegative: true,
127
+ attributedUserId: session.userId,
128
+ });
125
129
  }
126
130
  }
127
131
  }
@@ -0,0 +1,14 @@
1
+ CREATE TABLE "gateway_service_keys" (
2
+ "id" text PRIMARY KEY NOT NULL,
3
+ "key_hash" text NOT NULL,
4
+ "tenant_id" text NOT NULL,
5
+ "instance_id" text NOT NULL,
6
+ "created_at" bigint NOT NULL,
7
+ "revoked_at" bigint
8
+ );
9
+ --> statement-breakpoint
10
+ CREATE UNIQUE INDEX "idx_gateway_service_keys_hash" ON "gateway_service_keys" USING btree ("key_hash");
11
+ --> statement-breakpoint
12
+ CREATE INDEX "idx_gateway_service_keys_tenant" ON "gateway_service_keys" USING btree ("tenant_id");
13
+ --> statement-breakpoint
14
+ CREATE INDEX "idx_gateway_service_keys_instance" ON "gateway_service_keys" USING btree ("instance_id");
@@ -0,0 +1,82 @@
1
+ -- Double-entry ledger: accounts, journal_entries, journal_lines, account_balances
2
+ -- Replaces single-entry credit_transactions + credit_balances
3
+
4
+ DO $$ BEGIN
5
+ CREATE TYPE "public"."account_type" AS ENUM('asset','liability','equity','revenue','expense');
6
+ EXCEPTION WHEN duplicate_object THEN NULL;
7
+ END $$;--> statement-breakpoint
8
+ DO $$ BEGIN
9
+ CREATE TYPE "public"."entry_side" AS ENUM('debit','credit');
10
+ EXCEPTION WHEN duplicate_object THEN NULL;
11
+ END $$;--> statement-breakpoint
12
+ CREATE TABLE "accounts" (
13
+ "id" text PRIMARY KEY NOT NULL,
14
+ "code" text NOT NULL,
15
+ "name" text NOT NULL,
16
+ "type" "account_type" NOT NULL,
17
+ "normal_side" "entry_side" NOT NULL,
18
+ "tenant_id" text,
19
+ "created_at" text DEFAULT (now()) NOT NULL
20
+ );--> statement-breakpoint
21
+ CREATE TABLE "journal_entries" (
22
+ "id" text PRIMARY KEY NOT NULL,
23
+ "posted_at" text DEFAULT (now()) NOT NULL,
24
+ "entry_type" text NOT NULL,
25
+ "description" text,
26
+ "reference_id" text,
27
+ "tenant_id" text NOT NULL,
28
+ "metadata" jsonb,
29
+ "created_by" text
30
+ );--> statement-breakpoint
31
+ CREATE TABLE "journal_lines" (
32
+ "id" text PRIMARY KEY NOT NULL,
33
+ "journal_entry_id" text NOT NULL REFERENCES "journal_entries"("id"),
34
+ "account_id" text NOT NULL REFERENCES "accounts"("id"),
35
+ "amount" bigint NOT NULL,
36
+ "side" "entry_side" NOT NULL
37
+ );--> statement-breakpoint
38
+ CREATE TABLE "account_balances" (
39
+ "account_id" text PRIMARY KEY NOT NULL REFERENCES "accounts"("id"),
40
+ "balance" bigint DEFAULT 0 NOT NULL,
41
+ "last_updated" text DEFAULT (now()) NOT NULL
42
+ );--> statement-breakpoint
43
+ CREATE UNIQUE INDEX "idx_accounts_code" ON "accounts" USING btree ("code");--> statement-breakpoint
44
+ CREATE INDEX "idx_accounts_tenant" ON "accounts" USING btree ("tenant_id") WHERE "tenant_id" IS NOT NULL;--> statement-breakpoint
45
+ CREATE INDEX "idx_accounts_type" ON "accounts" USING btree ("type");--> statement-breakpoint
46
+ CREATE UNIQUE INDEX "idx_je_reference" ON "journal_entries" USING btree ("reference_id") WHERE "reference_id" IS NOT NULL;--> statement-breakpoint
47
+ CREATE INDEX "idx_je_tenant" ON "journal_entries" USING btree ("tenant_id");--> statement-breakpoint
48
+ CREATE INDEX "idx_je_type" ON "journal_entries" USING btree ("entry_type");--> statement-breakpoint
49
+ CREATE INDEX "idx_je_posted" ON "journal_entries" USING btree ("posted_at");--> statement-breakpoint
50
+ CREATE INDEX "idx_je_tenant_posted" ON "journal_entries" USING btree ("tenant_id","posted_at");--> statement-breakpoint
51
+ CREATE INDEX "idx_jl_entry" ON "journal_lines" USING btree ("journal_entry_id");--> statement-breakpoint
52
+ CREATE INDEX "idx_jl_account" ON "journal_lines" USING btree ("account_id");--> statement-breakpoint
53
+ CREATE INDEX "idx_jl_account_side" ON "journal_lines" USING btree ("account_id","side");--> statement-breakpoint
54
+
55
+ -- Seed system accounts
56
+ INSERT INTO "accounts" ("id", "code", "name", "type", "normal_side") VALUES
57
+ (gen_random_uuid(), '1000', 'Cash', 'asset', 'debit'),
58
+ (gen_random_uuid(), '1100', 'Stripe Receivable', 'asset', 'debit'),
59
+ (gen_random_uuid(), '3000', 'Retained Earnings', 'equity', 'credit'),
60
+ (gen_random_uuid(), '4000', 'Revenue: Bot Runtime', 'revenue', 'credit'),
61
+ (gen_random_uuid(), '4010', 'Revenue: Adapter Usage', 'revenue', 'credit'),
62
+ (gen_random_uuid(), '4020', 'Revenue: Addon', 'revenue', 'credit'),
63
+ (gen_random_uuid(), '4030', 'Revenue: Storage Upgrade', 'revenue', 'credit'),
64
+ (gen_random_uuid(), '4040', 'Revenue: Resource Upgrade', 'revenue', 'credit'),
65
+ (gen_random_uuid(), '4050', 'Revenue: Onboarding LLM', 'revenue', 'credit'),
66
+ (gen_random_uuid(), '4060', 'Revenue: Expired Credits', 'revenue', 'credit'),
67
+ (gen_random_uuid(), '5000', 'Expense: Signup Grant', 'expense', 'debit'),
68
+ (gen_random_uuid(), '5010', 'Expense: Admin Grant', 'expense', 'debit'),
69
+ (gen_random_uuid(), '5020', 'Expense: Promo', 'expense', 'debit'),
70
+ (gen_random_uuid(), '5030', 'Expense: Referral', 'expense', 'debit'),
71
+ (gen_random_uuid(), '5040', 'Expense: Affiliate', 'expense', 'debit'),
72
+ (gen_random_uuid(), '5050', 'Expense: Bounty', 'expense', 'debit'),
73
+ (gen_random_uuid(), '5060', 'Expense: Dividend', 'expense', 'debit'),
74
+ (gen_random_uuid(), '5070', 'Expense: Correction', 'expense', 'debit');--> statement-breakpoint
75
+
76
+ -- Initialize balance rows for all seeded system accounts
77
+ INSERT INTO "account_balances" ("account_id", "balance")
78
+ SELECT "id", 0 FROM "accounts";--> statement-breakpoint
79
+
80
+ -- Drop old single-entry tables
81
+ DROP TABLE IF EXISTS "credit_transactions" CASCADE;--> statement-breakpoint
82
+ DROP TABLE IF EXISTS "credit_balances" CASCADE;
@@ -15,6 +15,20 @@
15
15
  "when": 1773279600000,
16
16
  "tag": "0001_infrastructure_extraction",
17
17
  "breakpoints": true
18
+ },
19
+ {
20
+ "idx": 2,
21
+ "version": "7",
22
+ "when": 1741795200000,
23
+ "tag": "0002_gateway_service_keys",
24
+ "breakpoints": true
25
+ },
26
+ {
27
+ "idx": 3,
28
+ "version": "7",
29
+ "when": 1741881600000,
30
+ "tag": "0003_double_entry_ledger",
31
+ "breakpoints": true
18
32
  }
19
33
  ]
20
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wopr-network/platform-core",
3
- "version": "1.13.2",
3
+ "version": "1.14.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,7 +1,7 @@
1
1
  import { Hono } from "hono";
2
2
  import { z } from "zod";
3
3
  import type { AuthEnv } from "../../auth/index.js";
4
- import type { ICreditLedger } from "../../credits/index.js";
4
+ import type { ILedger } from "../../credits/index.js";
5
5
  import { Credit, InsufficientBalanceError } from "../../credits/index.js";
6
6
  import type { AdminAuditLogger } from "./admin-audit-helper.js";
7
7
  import { safeAuditLog } from "./admin-audit-helper.js";
@@ -31,7 +31,7 @@ function parseIntParam(value: string | undefined): number | undefined {
31
31
  * Pass a ledger directly or a factory for lazy init.
32
32
  */
33
33
  export function createAdminCreditApiRoutes(
34
- ledgerOrFactory: ICreditLedger | (() => ICreditLedger),
34
+ ledgerOrFactory: ILedger | (() => ILedger),
35
35
  auditLogger?: () => AdminAuditLogger,
36
36
  ): Hono<AuthEnv> {
37
37
  const ledgerFactory = typeof ledgerOrFactory === "function" ? ledgerOrFactory : () => ledgerOrFactory;
@@ -66,15 +66,10 @@ export function createAdminCreditApiRoutes(
66
66
  const adminUser = user?.id ?? "unknown";
67
67
  let result: Awaited<ReturnType<typeof ledger.credit>>;
68
68
  try {
69
- result = await ledger.credit(
70
- tenant,
71
- Credit.fromCents(amountCents),
72
- "admin_grant",
73
- reason,
74
- undefined,
75
- undefined,
76
- adminUser,
77
- );
69
+ result = await ledger.credit(tenant, Credit.fromCents(amountCents), "admin_grant", {
70
+ description: reason,
71
+ createdBy: adminUser,
72
+ });
78
73
  } catch (err) {
79
74
  safeAuditLog(auditLogger, {
80
75
  adminUser,
@@ -129,7 +124,7 @@ export function createAdminCreditApiRoutes(
129
124
  const adminUser = user?.id ?? "unknown";
130
125
  let result: Awaited<ReturnType<typeof ledger.credit>>;
131
126
  try {
132
- result = await ledger.credit(tenant, Credit.fromCents(amountCents), "admin_grant", reason);
127
+ result = await ledger.credit(tenant, Credit.fromCents(amountCents), "admin_grant", { description: reason });
133
128
  } catch (err) {
134
129
  safeAuditLog(auditLogger, {
135
130
  adminUser,
@@ -188,9 +183,11 @@ export function createAdminCreditApiRoutes(
188
183
  let result: Awaited<ReturnType<typeof ledger.credit>>;
189
184
  try {
190
185
  if (amountCents >= 0) {
191
- result = await ledger.credit(tenant, Credit.fromCents(amountCents), "promo", reason);
186
+ result = await ledger.credit(tenant, Credit.fromCents(amountCents), "correction", { description: reason });
192
187
  } else {
193
- result = await ledger.debit(tenant, Credit.fromCents(Math.abs(amountCents)), "correction", reason);
188
+ result = await ledger.debit(tenant, Credit.fromCents(Math.abs(amountCents)), "correction", {
189
+ description: reason,
190
+ });
194
191
  }
195
192
  } catch (err) {
196
193
  safeAuditLog(auditLogger, {
@@ -1,5 +1,5 @@
1
1
  import { Hono } from "hono";
2
- import type { ICreditLedger } from "../../credits/credit-ledger.js";
2
+ import type { ILedger } from "../../credits/ledger.js";
3
3
  import { checkInstanceQuota, DEFAULT_INSTANCE_LIMITS } from "../../monetization/quotas/quota-check.js";
4
4
  import { buildResourceLimits, DEFAULT_RESOURCE_CONFIG } from "../../monetization/quotas/resource-limits.js";
5
5
 
@@ -8,7 +8,7 @@ import { buildResourceLimits, DEFAULT_RESOURCE_CONFIG } from "../../monetization
8
8
  *
9
9
  * @param ledgerFactory - Factory returning the credit ledger
10
10
  */
11
- export function createQuotaRoutes(ledgerFactory: () => ICreditLedger): Hono {
11
+ export function createQuotaRoutes(ledgerFactory: () => ILedger): Hono {
12
12
  const routes = new Hono();
13
13
 
14
14
  /**
@@ -1,14 +1,14 @@
1
1
  import { Hono } from "hono";
2
2
  import type { Pool } from "pg";
3
3
  import { logger } from "../../config/logger.js";
4
- import type { ICreditLedger } from "../../credits/index.js";
4
+ import type { ILedger } from "../../credits/index.js";
5
5
  import { grantSignupCredits } from "../../credits/index.js";
6
6
  import { getEmailClient } from "../../email/client.js";
7
7
  import { verifyToken, welcomeTemplate } from "../../email/index.js";
8
8
 
9
9
  export interface VerifyEmailRouteDeps {
10
10
  pool: Pool;
11
- creditLedger: ICreditLedger;
11
+ creditLedger: ILedger;
12
12
  }
13
13
 
14
14
  export interface VerifyEmailRouteConfig {
@@ -32,7 +32,7 @@ export function createVerifyEmailRoutes(deps: VerifyEmailRouteDeps, config?: Ver
32
32
  */
33
33
  export function createVerifyEmailRoutesLazy(
34
34
  poolFactory: () => Pool,
35
- creditLedgerFactory: () => ICreditLedger,
35
+ creditLedgerFactory: () => ILedger,
36
36
  config?: VerifyEmailRouteConfig,
37
37
  ): Hono {
38
38
  return buildRoutes(poolFactory, creditLedgerFactory, config);
@@ -40,7 +40,7 @@ export function createVerifyEmailRoutesLazy(
40
40
 
41
41
  function buildRoutes(
42
42
  poolFactory: () => Pool,
43
- creditLedgerFactory: () => ICreditLedger,
43
+ creditLedgerFactory: () => ILedger,
44
44
  config?: VerifyEmailRouteConfig,
45
45
  ): Hono {
46
46
  const uiOrigin = config?.uiOrigin ?? process.env.UI_ORIGIN ?? "http://localhost:3001";