@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
@@ -5,10 +5,10 @@
5
5
  * budget checking, metering, provider configs, fetch, and service key resolution.
6
6
  */
7
7
 
8
- import type { Credit, ICreditLedger } from "@wopr-network/platform-core/credits";
8
+ import type { Credit, ILedger } from "@wopr-network/platform-core/credits";
9
9
  import type { MeterEmitter } from "@wopr-network/platform-core/metering";
10
10
  import type { IRateLimitRepository } from "../../api/rate-limit-repository.js";
11
- import type { BudgetChecker } from "../../monetization/budget/budget-checker.js";
11
+ import type { IBudgetChecker } from "../../monetization/budget/budget-checker.js";
12
12
  import type { CapabilityRateLimitConfig } from "../capability-rate-limit.js";
13
13
  import type { CircuitBreakerConfig } from "../circuit-breaker.js";
14
14
  import type { ICircuitBreakerRepository } from "../circuit-breaker-repository.js";
@@ -17,14 +17,14 @@ import type { FetchFn, GatewayTenant, ProviderConfig } from "../types.js";
17
17
 
18
18
  export interface ProtocolDeps {
19
19
  meter: MeterEmitter;
20
- budgetChecker: BudgetChecker;
21
- creditLedger?: ICreditLedger;
20
+ budgetChecker: IBudgetChecker;
21
+ creditLedger?: ILedger;
22
22
  topUpUrl: string;
23
23
  graceBufferCents?: number;
24
24
  providers: ProviderConfig;
25
25
  defaultMargin: number;
26
26
  fetchFn: FetchFn;
27
- resolveServiceKey: (key: string) => GatewayTenant | null;
27
+ resolveServiceKey: (key: string) => GatewayTenant | null | Promise<GatewayTenant | null>;
28
28
  /** Apply margin to a cost. Defaults to withMargin from adapters/types. */
29
29
  withMarginFn: (cost: Credit, margin: number) => Credit;
30
30
  rateLookupFn?: SellRateLookupFn;
@@ -26,7 +26,7 @@ import type { ProtocolDeps } from "./deps.js";
26
26
  // Auth middleware — OpenAI SDK sends Authorization: Bearer
27
27
  // ---------------------------------------------------------------------------
28
28
 
29
- function openaiAuth(resolveServiceKey: (key: string) => GatewayTenant | null) {
29
+ function openaiAuth(resolveServiceKey: (key: string) => GatewayTenant | null | Promise<GatewayTenant | null>) {
30
30
  return async (c: Context<GatewayAuthEnv>, next: Next) => {
31
31
  const authHeader = c.req.header("Authorization");
32
32
 
@@ -73,7 +73,7 @@ function openaiAuth(resolveServiceKey: (key: string) => GatewayTenant | null) {
73
73
  );
74
74
  }
75
75
 
76
- const tenant = resolveServiceKey(key);
76
+ const tenant = await resolveServiceKey(key);
77
77
  if (!tenant) {
78
78
  logger.warn("Invalid service key attempted (openai handler)", {
79
79
  keyPrefix: `${key.slice(0, 8)}...`,
@@ -10,7 +10,7 @@
10
10
  * 5. Return response to bot
11
11
  */
12
12
 
13
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
13
+ import type { ILedger } from "@wopr-network/platform-core/credits";
14
14
  import { Credit } from "@wopr-network/platform-core/credits";
15
15
  import type { MeterEmitter } from "@wopr-network/platform-core/metering";
16
16
  import type { Context } from "hono";
@@ -19,7 +19,7 @@ import { logger } from "../config/logger.js";
19
19
  import type { TTSOutput } from "../monetization/adapters/types.js";
20
20
  import { withMargin } from "../monetization/adapters/types.js";
21
21
  import { NoProviderAvailableError } from "../monetization/arbitrage/types.js";
22
- import type { BudgetChecker } from "../monetization/budget/budget-checker.js";
22
+ import type { IBudgetChecker } from "../monetization/budget/budget-checker.js";
23
23
  import { PHONE_NUMBER_MONTHLY_COST } from "../monetization/credits/phone-billing.js";
24
24
  import { creditBalanceCheck, debitCredits } from "./credit-gate.js";
25
25
  import { mapBudgetError, mapProviderError } from "./error-mapping.js";
@@ -62,8 +62,8 @@ const smsDeliveryStatusBodySchema = z.object({
62
62
  /** Shared state for all proxy handlers. */
63
63
  export interface ProxyDeps {
64
64
  meter: MeterEmitter;
65
- budgetChecker: BudgetChecker;
66
- creditLedger?: ICreditLedger;
65
+ budgetChecker: IBudgetChecker;
66
+ creditLedger?: ILedger;
67
67
  topUpUrl: string;
68
68
  graceBufferCents?: number;
69
69
  providers: ProviderConfig;
@@ -5,7 +5,7 @@
5
5
  * is correctly applied, and route ordering is correct.
6
6
  */
7
7
 
8
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
8
+ import type { ILedger } from "@wopr-network/platform-core/credits";
9
9
  import { Credit } from "@wopr-network/platform-core/credits";
10
10
  import { Hono } from "hono";
11
11
  import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -48,7 +48,7 @@ function buildTestConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig
48
48
  debit: vi.fn().mockResolvedValue(undefined),
49
49
  credit: vi.fn(),
50
50
  history: vi.fn(),
51
- } as unknown as ICreditLedger;
51
+ } as unknown as ILedger;
52
52
  const fetchFn = vi.fn().mockResolvedValue(
53
53
  new Response(
54
54
  JSON.stringify({
@@ -65,7 +65,7 @@ function buildTestConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig
65
65
 
66
66
  return {
67
67
  meter: meter as unknown as import("@wopr-network/platform-core/metering").MeterEmitter,
68
- budgetChecker: budgetChecker as unknown as import("../monetization/budget/budget-checker.js").BudgetChecker,
68
+ budgetChecker,
69
69
  creditLedger,
70
70
  providers: { openrouter: { apiKey: "or-test-key" } },
71
71
  fetchFn,
@@ -26,7 +26,9 @@ export interface GatewayAuthEnv {
26
26
  *
27
27
  * @param resolveServiceKey - Function that maps a service key to a tenant (or null)
28
28
  */
29
- export function serviceKeyAuth(resolveServiceKey: (key: string) => GatewayTenant | null) {
29
+ export function serviceKeyAuth(
30
+ resolveServiceKey: (key: string) => GatewayTenant | null | Promise<GatewayTenant | null>,
31
+ ) {
30
32
  return async (c: Context<GatewayAuthEnv>, next: Next) => {
31
33
  const authHeader = c.req.header("Authorization");
32
34
  if (!authHeader) {
@@ -70,7 +72,7 @@ export function serviceKeyAuth(resolveServiceKey: (key: string) => GatewayTenant
70
72
  );
71
73
  }
72
74
 
73
- const tenant = resolveServiceKey(serviceKey);
75
+ const tenant = await resolveServiceKey(serviceKey);
74
76
  if (!tenant) {
75
77
  logger.warn("Invalid service key attempted", {
76
78
  keyPrefix: `${serviceKey.slice(0, 8)}...`,
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Gateway service key repository.
3
+ *
4
+ * Stores SHA-256 hashes of per-instance service keys used to authenticate
5
+ * tenant containers against the metered inference gateway. Raw keys are
6
+ * NEVER stored — only hashes.
7
+ */
8
+
9
+ import { createHash, randomBytes } from "node:crypto";
10
+ import { and, eq, isNull } from "drizzle-orm";
11
+ import type { PlatformDb } from "../db/index.js";
12
+ import { gatewayServiceKeys } from "../db/schema/gateway-service-keys.js";
13
+ import type { GatewayTenant } from "./types.js";
14
+
15
+ /** Hash a raw key for storage/lookup. */
16
+ function hashKey(raw: string): string {
17
+ return createHash("sha256").update(raw).digest("hex");
18
+ }
19
+
20
+ export interface IServiceKeyRepository {
21
+ /** Generate a new service key for an instance. Returns the raw key (caller must store it). */
22
+ generate(tenantId: string, instanceId: string): Promise<string>;
23
+
24
+ /** Resolve a raw bearer token to a GatewayTenant. Returns null if not found or revoked. */
25
+ resolve(rawKey: string): Promise<GatewayTenant | null>;
26
+
27
+ /** Revoke the service key for a specific instance. */
28
+ revokeByInstance(instanceId: string): Promise<void>;
29
+
30
+ /** Revoke all service keys for a tenant (used when tenant is deleted). */
31
+ revokeByTenant(tenantId: string): Promise<void>;
32
+ }
33
+
34
+ export class DrizzleServiceKeyRepository implements IServiceKeyRepository {
35
+ constructor(private readonly db: PlatformDb) {}
36
+
37
+ async generate(tenantId: string, instanceId: string): Promise<string> {
38
+ const raw = randomBytes(32).toString("hex");
39
+ const hash = hashKey(raw);
40
+ const id = randomBytes(16).toString("hex");
41
+
42
+ await this.db.insert(gatewayServiceKeys).values({
43
+ id,
44
+ keyHash: hash,
45
+ tenantId,
46
+ instanceId,
47
+ createdAt: Date.now(),
48
+ });
49
+
50
+ return raw;
51
+ }
52
+
53
+ async resolve(rawKey: string): Promise<GatewayTenant | null> {
54
+ const hash = hashKey(rawKey);
55
+ const rows = await this.db
56
+ .select({
57
+ tenantId: gatewayServiceKeys.tenantId,
58
+ instanceId: gatewayServiceKeys.instanceId,
59
+ })
60
+ .from(gatewayServiceKeys)
61
+ .where(and(eq(gatewayServiceKeys.keyHash, hash), isNull(gatewayServiceKeys.revokedAt)))
62
+ .limit(1);
63
+
64
+ const row = rows[0];
65
+ if (!row) return null;
66
+
67
+ return {
68
+ id: row.tenantId,
69
+ instanceId: row.instanceId,
70
+ spendLimits: { maxSpendPerHour: null, maxSpendPerMonth: null },
71
+ };
72
+ }
73
+
74
+ async revokeByInstance(instanceId: string): Promise<void> {
75
+ await this.db
76
+ .update(gatewayServiceKeys)
77
+ .set({ revokedAt: Date.now() })
78
+ .where(and(eq(gatewayServiceKeys.instanceId, instanceId), isNull(gatewayServiceKeys.revokedAt)));
79
+ }
80
+
81
+ async revokeByTenant(tenantId: string): Promise<void> {
82
+ await this.db
83
+ .update(gatewayServiceKeys)
84
+ .set({ revokedAt: Date.now() })
85
+ .where(and(eq(gatewayServiceKeys.tenantId, tenantId), isNull(gatewayServiceKeys.revokedAt)));
86
+ }
87
+ }
@@ -6,10 +6,10 @@
6
6
  * budget-checks, proxies to upstream providers, meters usage, and responds.
7
7
  */
8
8
 
9
- import type { ICreditLedger } from "@wopr-network/platform-core/credits";
9
+ import type { ILedger } from "@wopr-network/platform-core/credits";
10
10
  import type { MeterEmitter } from "@wopr-network/platform-core/metering";
11
11
  import type { IRateLimitRepository } from "../api/rate-limit-repository.js";
12
- import type { BudgetChecker, SpendLimits } from "../monetization/budget/budget-checker.js";
12
+ import type { IBudgetChecker, SpendLimits } from "../monetization/budget/budget-checker.js";
13
13
  import type { CapabilityRateLimitConfig } from "./capability-rate-limit.js";
14
14
  import type { CircuitBreakerConfig } from "./circuit-breaker.js";
15
15
  import type { ICircuitBreakerRepository } from "./circuit-breaker-repository.js";
@@ -97,9 +97,9 @@ export interface GatewayConfig {
97
97
  /** MeterEmitter instance for usage tracking */
98
98
  meter: MeterEmitter;
99
99
  /** BudgetChecker instance for pre-call budget validation */
100
- budgetChecker: BudgetChecker;
100
+ budgetChecker: IBudgetChecker;
101
101
  /** CreditLedger instance for deducting credits after proxy calls (optional — if absent, credit deduction is skipped) */
102
- creditLedger?: ICreditLedger;
102
+ creditLedger?: ILedger;
103
103
  /** URL to direct users to when they need to add credits (default: "/dashboard/credits") */
104
104
  topUpUrl?: string;
105
105
  /** Maximum negative credit balance (in cents) before hard-stop. Default: 50 (-$0.50). */
@@ -115,7 +115,7 @@ export interface GatewayConfig {
115
115
  /** Optional cached rate lookup for model-specific token pricing (WOP-646) */
116
116
  rateLookupFn?: import("./rate-lookup.js").SellRateLookupFn;
117
117
  /** Function to resolve a service key to a tenant */
118
- resolveServiceKey: (key: string) => GatewayTenant | null;
118
+ resolveServiceKey: (key: string) => GatewayTenant | null | Promise<GatewayTenant | null>;
119
119
  /** Base URL for Twilio webhook signature verification (e.g., https://api.wopr.network/v1). Required for Twilio/Telnyx webhook endpoints. */
120
120
  webhookBaseUrl?: string;
121
121
  /** Resolve a tenant from an inbound webhook request (e.g., from a tenantId URL path param). Required when webhookBaseUrl is set. */
@@ -2,7 +2,7 @@ import crypto from "node:crypto";
2
2
  import type { PGlite } from "@electric-sql/pglite";
3
3
  import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
4
4
  import { Credit } from "../credits/credit.js";
5
- import { CreditLedger } from "../credits/credit-ledger.js";
5
+ import { DrizzleLedger } from "../credits/ledger.js";
6
6
  import type { PlatformDb } from "../db/index.js";
7
7
  import { usageSummaries } from "../db/schema/meter-events.js";
8
8
  import { createTestDb, truncateAllTables } from "../test/db.js";
@@ -18,7 +18,7 @@ const DAY_END = DAY_START + 24 * 60 * 60 * 1000;
18
18
  describe("runReconciliation", () => {
19
19
  let pool: PGlite;
20
20
  let db: PlatformDb;
21
- let ledger: CreditLedger;
21
+ let ledger: DrizzleLedger;
22
22
  let usageSummaryRepo: DrizzleUsageSummaryRepository;
23
23
  let adapterUsageRepo: DrizzleAdapterUsageRepository;
24
24
 
@@ -26,7 +26,7 @@ describe("runReconciliation", () => {
26
26
  const t = await createTestDb();
27
27
  pool = t.pool;
28
28
  db = t.db;
29
- ledger = new CreditLedger(db);
29
+ ledger = new DrizzleLedger(db);
30
30
  usageSummaryRepo = new DrizzleUsageSummaryRepository(db);
31
31
  adapterUsageRepo = new DrizzleAdapterUsageRepository(db);
32
32
  });
@@ -37,6 +37,7 @@ describe("runReconciliation", () => {
37
37
 
38
38
  beforeEach(async () => {
39
39
  await truncateAllTables(pool);
40
+ await ledger.seedSystemAccounts();
40
41
  });
41
42
 
42
43
  /** Insert a usage_summaries row directly. */
@@ -75,7 +76,7 @@ describe("runReconciliation", () => {
75
76
  await insertSummary({ tenant: "t1", totalCharge: charge.toRaw() });
76
77
 
77
78
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
78
- await ledger.debit("t1", charge, "adapter_usage", "chat usage");
79
+ await ledger.debit("t1", charge, "adapter_usage", { description: "chat usage" });
79
80
 
80
81
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
81
82
  expect(result.tenantsChecked).toBe(1);
@@ -86,7 +87,7 @@ describe("runReconciliation", () => {
86
87
  await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(100).toRaw() });
87
88
 
88
89
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
89
- await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", "chat usage");
90
+ await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", { description: "chat usage" });
90
91
 
91
92
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
92
93
  expect(result.tenantsChecked).toBe(1);
@@ -118,7 +119,7 @@ describe("runReconciliation", () => {
118
119
 
119
120
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
120
121
  // Debit as bot_runtime — should NOT count toward reconciliation
121
- await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", "daily runtime");
122
+ await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", { description: "daily runtime" });
122
123
 
123
124
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
124
125
  // Metered 20c, ledger adapter_usage = 0 => drift = 20c
@@ -150,12 +151,12 @@ describe("runReconciliation", () => {
150
151
  // t1: balanced
151
152
  await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(50).toRaw() });
152
153
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
153
- await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", "chat");
154
+ await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", { description: "chat" });
154
155
 
155
156
  // t2: drifted
156
157
  await insertSummary({ tenant: "t2", totalCharge: Credit.fromCents(100).toRaw() });
157
158
  await ledger.credit("t2", Credit.fromCents(500), "purchase");
158
- await ledger.debit("t2", Credit.fromCents(60), "adapter_usage", "chat");
159
+ await ledger.debit("t2", Credit.fromCents(60), "adapter_usage", { description: "chat" });
159
160
 
160
161
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
161
162
  expect(result.tenantsChecked).toBe(2);
@@ -193,7 +194,7 @@ describe("runReconciliation", () => {
193
194
  await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(50).toRaw() });
194
195
 
195
196
  await ledger.credit("t1", Credit.fromCents(500), "purchase");
196
- await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", "chat usage");
197
+ await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", { description: "chat usage" });
197
198
 
198
199
  const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
199
200
  expect(result.discrepancies).toHaveLength(1);
@@ -2,7 +2,7 @@ import crypto from "node:crypto";
2
2
  import type { PGlite } from "@electric-sql/pglite";
3
3
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
4
4
  import { Credit } from "../credits/credit.js";
5
- import { CreditLedger } from "../credits/credit-ledger.js";
5
+ import { DrizzleLedger } from "../credits/ledger.js";
6
6
  import type { PlatformDb } from "../db/index.js";
7
7
  import { createTestDb, seedUsageSummary, truncateAllTables } from "../test/db.js";
8
8
  import { DrizzleAdapterUsageRepository, DrizzleUsageSummaryRepository } from "./reconciliation-repository.js";
@@ -126,12 +126,13 @@ describe("DrizzleUsageSummaryRepository", () => {
126
126
 
127
127
  describe("DrizzleAdapterUsageRepository", () => {
128
128
  let repo: DrizzleAdapterUsageRepository;
129
- let ledger: CreditLedger;
129
+ let ledger: DrizzleLedger;
130
130
 
131
131
  beforeEach(async () => {
132
132
  await truncateAllTables(pool);
133
133
  repo = new DrizzleAdapterUsageRepository(db);
134
- ledger = new CreditLedger(db);
134
+ ledger = new DrizzleLedger(db);
135
+ await ledger.seedSystemAccounts();
135
136
  });
136
137
 
137
138
  it("returns empty array when no adapter_usage debits exist", async () => {
@@ -147,9 +148,9 @@ describe("DrizzleAdapterUsageRepository", () => {
147
148
  await ledger.credit("t1", Credit.fromCents(1000), "purchase");
148
149
  await ledger.credit("t2", Credit.fromCents(1000), "purchase");
149
150
 
150
- await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", "t1-debit-1");
151
- await ledger.debit("t1", Credit.fromCents(20), "adapter_usage", "t1-debit-2");
152
- await ledger.debit("t2", Credit.fromCents(50), "adapter_usage", "t2-debit-1");
151
+ await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", { description: "t1-debit-1" });
152
+ await ledger.debit("t1", Credit.fromCents(20), "adapter_usage", { description: "t1-debit-2" });
153
+ await ledger.debit("t2", Credit.fromCents(50), "adapter_usage", { description: "t2-debit-1" });
153
154
 
154
155
  // Query window covering today
155
156
  const today = new Date().toISOString().slice(0, 10);
@@ -168,8 +169,8 @@ describe("DrizzleAdapterUsageRepository", () => {
168
169
 
169
170
  it("excludes non-adapter_usage debit types", async () => {
170
171
  await ledger.credit("t1", Credit.fromCents(1000), "purchase");
171
- await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", "adapter debit");
172
- await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", "runtime debit");
172
+ await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", { description: "adapter debit" });
173
+ await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", { description: "runtime debit" });
173
174
 
174
175
  const today = new Date().toISOString().slice(0, 10);
175
176
  const startIso = `${today}T00:00:00Z`;
@@ -182,7 +183,7 @@ describe("DrizzleAdapterUsageRepository", () => {
182
183
 
183
184
  it("excludes credit transactions (positive amounts are not debits)", async () => {
184
185
  await ledger.credit("t1", Credit.fromCents(1000), "purchase");
185
- await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "real debit");
186
+ await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", { description: "real debit" });
186
187
 
187
188
  const today = new Date().toISOString().slice(0, 10);
188
189
  const startIso = `${today}T00:00:00Z`;
@@ -1,6 +1,6 @@
1
1
  import { and, eq, gte, lt, ne, sql } from "drizzle-orm";
2
2
  import type { PlatformDb } from "../db/index.js";
3
- import { creditTransactions } from "../db/schema/credits.js";
3
+ import { journalEntries, journalLines } from "../db/schema/ledger.js";
4
4
  import { usageSummaries } from "../db/schema/meter-events.js";
5
5
 
6
6
  // ---------------------------------------------------------------------------
@@ -59,24 +59,27 @@ export class DrizzleAdapterUsageRepository implements IAdapterUsageRepository {
59
59
  constructor(private readonly db: PlatformDb) {}
60
60
 
61
61
  async getAggregatedAdapterUsageDebits(startIso: string, endIso: string): Promise<AggregatedDebit[]> {
62
+ // Sum the debit-side journal line amounts for adapter_usage entries.
63
+ // In double-entry: DR tenant liability (2000:<tenantId>), CR revenue:adapter_usage (4010).
64
+ // The debit line on the tenant account represents the charge amount.
62
65
  const rows = await this.db
63
66
  .select({
64
- tenantId: creditTransactions.tenantId,
65
- // amount_credits stores negative values for debits; ABS gives the raw positive debit amount.
66
- // Use the raw column name in sql to bypass the custom creditColumn type serializer.
67
- // raw SQL: Drizzle cannot express ABS with COALESCE and SUM
68
- totalDebitRaw: sql<number>`COALESCE(SUM(ABS(amount_credits)), 0)`,
67
+ tenantId: journalEntries.tenantId,
68
+ // raw SQL: Drizzle cannot express COALESCE with SUM aggregation
69
+ totalDebitRaw: sql<number>`COALESCE(SUM(${journalLines.amount}), 0)`,
69
70
  })
70
- .from(creditTransactions)
71
+ .from(journalLines)
72
+ .innerJoin(journalEntries, eq(journalEntries.id, journalLines.journalEntryId))
71
73
  .where(
72
74
  and(
73
- eq(creditTransactions.type, "adapter_usage"),
75
+ eq(journalEntries.entryType, "adapter_usage"),
76
+ eq(journalLines.side, "debit"),
74
77
  // raw SQL: Drizzle cannot express timestamptz cast for text column date comparison
75
- sql`${creditTransactions.createdAt}::timestamptz >= ${startIso}::timestamptz`,
76
- sql`${creditTransactions.createdAt}::timestamptz < ${endIso}::timestamptz`,
78
+ sql`${journalEntries.postedAt}::timestamptz >= ${startIso}::timestamptz`,
79
+ sql`${journalEntries.postedAt}::timestamptz < ${endIso}::timestamptz`,
77
80
  ),
78
81
  )
79
- .groupBy(creditTransactions.tenantId);
82
+ .groupBy(journalEntries.tenantId);
80
83
 
81
84
  return rows.map((r) => ({ tenantId: r.tenantId, totalDebitRaw: Number(r.totalDebitRaw) }));
82
85
  }
@@ -1,10 +1,10 @@
1
1
  import type { PGlite } from "@electric-sql/pglite";
2
- import { sql } from "drizzle-orm";
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 { affiliateReferrals } from "../../db/schema/affiliate.js";
6
6
  import { affiliateFraudEvents } from "../../db/schema/affiliate-fraud.js";
7
- import { beginTestTransaction, createTestDb, endTestTransaction, rollbackTestTransaction } from "../../test/db.js";
7
+ import { createTestDb, truncateAllTables } from "../../test/db.js";
8
8
  import { ADMIN_BLOCK_SENTINEL, DrizzleAffiliateFraudAdminRepository } from "./affiliate-admin-repository.js";
9
9
 
10
10
  describe("DrizzleAffiliateFraudAdminRepository", () => {
@@ -14,16 +14,15 @@ describe("DrizzleAffiliateFraudAdminRepository", () => {
14
14
 
15
15
  beforeAll(async () => {
16
16
  ({ db, pool } = await createTestDb());
17
- await beginTestTransaction(pool);
18
17
  });
19
18
 
20
19
  afterAll(async () => {
21
- await endTestTransaction(pool);
22
20
  await pool.close();
23
21
  });
24
22
 
25
23
  beforeEach(async () => {
26
- await rollbackTestTransaction(pool);
24
+ await truncateAllTables(pool);
25
+ await new DrizzleLedger(db).seedSystemAccounts();
27
26
  repo = new DrizzleAffiliateFraudAdminRepository(db);
28
27
  });
29
28
 
@@ -162,11 +161,17 @@ describe("DrizzleAffiliateFraudAdminRepository", () => {
162
161
 
163
162
  describe("blockFingerprint", () => {
164
163
  it("should insert fraud events with ADMIN_BLOCK as referredTenantId", async () => {
165
- await db.execute(
166
- sql`INSERT INTO credit_transactions (id, tenant_id, amount_credits, balance_after_credits, type, created_at, stripe_fingerprint)
167
- VALUES ('ct-1', 't-alice', 0, 0, 'purchase', now(), 'fp_abc123'),
168
- ('ct-2', 't-bob', 0, 0, 'purchase', now(), 'fp_abc123')`,
169
- );
164
+ const ledger = new DrizzleLedger(db);
165
+ await ledger.credit("t-alice", Credit.fromCents(1), "purchase", {
166
+ description: "test purchase",
167
+ referenceId: "ref-alice-fp_abc123",
168
+ stripeFingerprint: "fp_abc123",
169
+ });
170
+ await ledger.credit("t-bob", Credit.fromCents(1), "purchase", {
171
+ description: "test purchase",
172
+ referenceId: "ref-bob-fp_abc123",
173
+ stripeFingerprint: "fp_abc123",
174
+ });
170
175
 
171
176
  await repo.blockFingerprint("fp_abc123", "admin-user-1");
172
177
 
@@ -185,11 +190,17 @@ describe("DrizzleAffiliateFraudAdminRepository", () => {
185
190
  });
186
191
 
187
192
  it("should use unique referralId per tenant to avoid unique constraint conflicts", async () => {
188
- await db.execute(
189
- sql`INSERT INTO credit_transactions (id, tenant_id, amount_credits, balance_after_credits, type, created_at, stripe_fingerprint)
190
- VALUES ('ct-3', 't-carol', 0, 0, 'purchase', now(), 'fp_def456'),
191
- ('ct-4', 't-dave', 0, 0, 'purchase', now(), 'fp_def456')`,
192
- );
193
+ const ledger = new DrizzleLedger(db);
194
+ await ledger.credit("t-carol", Credit.fromCents(1), "purchase", {
195
+ description: "test purchase",
196
+ referenceId: "ref-carol-fp_def456",
197
+ stripeFingerprint: "fp_def456",
198
+ });
199
+ await ledger.credit("t-dave", Credit.fromCents(1), "purchase", {
200
+ description: "test purchase",
201
+ referenceId: "ref-dave-fp_def456",
202
+ stripeFingerprint: "fp_def456",
203
+ });
193
204
 
194
205
  await repo.blockFingerprint("fp_def456", "admin-user-2");
195
206
 
@@ -204,10 +215,12 @@ describe("DrizzleAffiliateFraudAdminRepository", () => {
204
215
  });
205
216
 
206
217
  it("should be idempotent via onConflictDoNothing", async () => {
207
- await db.execute(
208
- sql`INSERT INTO credit_transactions (id, tenant_id, amount_credits, balance_after_credits, type, created_at, stripe_fingerprint)
209
- VALUES ('ct-5', 't-eve', 0, 0, 'purchase', now(), 'fp_ghi789')`,
210
- );
218
+ const ledger = new DrizzleLedger(db);
219
+ await ledger.credit("t-eve", Credit.fromCents(1), "purchase", {
220
+ description: "test purchase",
221
+ referenceId: "ref-eve-fp_ghi789",
222
+ stripeFingerprint: "fp_ghi789",
223
+ });
211
224
 
212
225
  await repo.blockFingerprint("fp_ghi789", "admin-user-3");
213
226
  await repo.blockFingerprint("fp_ghi789", "admin-user-3");
@@ -4,7 +4,7 @@ import { logger } from "../../config/logger.js";
4
4
  import type { DrizzleDb } from "../../db/index.js";
5
5
  import { affiliateReferrals } from "../../db/schema/affiliate.js";
6
6
  import { affiliateFraudEvents } from "../../db/schema/affiliate-fraud.js";
7
- import { creditTransactions } from "../../db/schema/credits.js";
7
+ import { journalEntries } from "../../db/schema/ledger.js";
8
8
 
9
9
  function parseSignals(raw: string): string[] {
10
10
  try {
@@ -119,13 +119,16 @@ export class DrizzleAffiliateFraudAdminRepository implements IAffiliateFraudAdmi
119
119
  }
120
120
 
121
121
  async listFingerprintClusters(): Promise<FingerprintCluster[]> {
122
+ // Query journal_entries.metadata->>'stripeFingerprint' for purchase entries
122
123
  // raw SQL: Drizzle cannot express HAVING COUNT(DISTINCT ...) with array_agg in a single query
123
124
  type ClusterRow = { stripe_fingerprint: string; tenant_ids: string[] };
124
125
  const rows = (await this.db.execute(sql`
125
- SELECT stripe_fingerprint, array_agg(DISTINCT tenant_id ORDER BY tenant_id) AS tenant_ids
126
- FROM credit_transactions
127
- WHERE stripe_fingerprint IS NOT NULL
128
- GROUP BY stripe_fingerprint
126
+ SELECT metadata->>'stripeFingerprint' AS stripe_fingerprint,
127
+ array_agg(DISTINCT tenant_id ORDER BY tenant_id) AS tenant_ids
128
+ FROM journal_entries
129
+ WHERE metadata->>'stripeFingerprint' IS NOT NULL
130
+ AND entry_type = 'purchase'
131
+ GROUP BY metadata->>'stripeFingerprint'
129
132
  HAVING COUNT(DISTINCT tenant_id) > 1
130
133
  ORDER BY COUNT(DISTINCT tenant_id) DESC
131
134
  `)) as unknown as { rows: ClusterRow[] };
@@ -138,9 +141,14 @@ export class DrizzleAffiliateFraudAdminRepository implements IAffiliateFraudAdmi
138
141
 
139
142
  async blockFingerprint(fingerprint: string, adminUserId: string): Promise<void> {
140
143
  const rows = await this.db
141
- .selectDistinct({ tenantId: creditTransactions.tenantId })
142
- .from(creditTransactions)
143
- .where(eq(creditTransactions.stripeFingerprint, fingerprint));
144
+ .selectDistinct({ tenantId: journalEntries.tenantId })
145
+ .from(journalEntries)
146
+ .where(
147
+ and(
148
+ eq(journalEntries.entryType, "purchase"),
149
+ sql`${journalEntries.metadata}->>'stripeFingerprint' = ${fingerprint}`,
150
+ ),
151
+ );
144
152
  const tenantIds = rows.map((r) => r.tenantId);
145
153
 
146
154
  const now = new Date().toISOString();