@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.
- package/dist/api/routes/admin-credits.d.ts +2 -2
- package/dist/api/routes/admin-credits.js +9 -4
- package/dist/api/routes/quota.d.ts +2 -2
- package/dist/api/routes/verify-email.d.ts +3 -3
- package/dist/backup/on-demand-snapshot-service.d.ts +2 -2
- package/dist/billing/payram/webhook.d.ts +3 -3
- package/dist/billing/payram/webhook.js +5 -1
- package/dist/billing/payram/webhook.test.js +5 -4
- package/dist/billing/stripe/stripe-payment-processor.d.ts +2 -2
- package/dist/billing/stripe/stripe-payment-processor.test.js +7 -0
- package/dist/billing/stripe/tenant-store.d.ts +1 -1
- package/dist/billing/stripe/tenant-store.js +1 -1
- package/dist/credits/auto-topup-charge.d.ts +2 -2
- package/dist/credits/auto-topup-charge.js +5 -1
- package/dist/credits/auto-topup-charge.test.js +5 -4
- package/dist/credits/auto-topup-usage.d.ts +2 -2
- package/dist/credits/auto-topup-usage.test.js +53 -12
- package/dist/credits/credit-expiry-cron.d.ts +2 -2
- package/dist/credits/credit-expiry-cron.js +7 -4
- package/dist/credits/credit-expiry-cron.test.js +25 -8
- package/dist/credits/credit-ledger.d.ts +2 -2
- package/dist/credits/credit-ledger.js +1 -1
- package/dist/credits/dividend-cron.d.ts +4 -6
- package/dist/credits/dividend-cron.js +10 -16
- package/dist/credits/dividend-cron.test.js +31 -44
- package/dist/credits/dividend-repository.js +19 -22
- package/dist/credits/dividend-repository.test.js +4 -3
- package/dist/credits/index.d.ts +4 -2
- package/dist/credits/index.js +2 -1
- package/dist/credits/ledger.d.ts +195 -0
- package/dist/credits/ledger.js +561 -0
- package/dist/credits/ledger.test.js +418 -0
- package/dist/credits/signup-grant.d.ts +2 -2
- package/dist/credits/signup-grant.js +4 -4
- package/dist/credits/signup-grant.test.js +5 -3
- package/dist/credits/trial-balance-cron.d.ts +19 -0
- package/dist/credits/trial-balance-cron.js +30 -0
- package/dist/credits/trial-balance-cron.test.js +55 -0
- package/dist/db/schema/gateway-service-keys.d.ts +109 -0
- package/dist/db/schema/gateway-service-keys.js +18 -0
- package/dist/db/schema/index.d.ts +2 -0
- package/dist/db/schema/index.js +2 -0
- package/dist/db/schema/ledger.d.ts +442 -0
- package/dist/db/schema/ledger.js +76 -0
- package/dist/gateway/credit-gate.d.ts +2 -2
- package/dist/gateway/credit-gate.js +5 -1
- package/dist/gateway/credit-gate.test.js +35 -33
- package/dist/gateway/gateway-routes.test.js +1 -1
- package/dist/gateway/index.d.ts +2 -0
- package/dist/gateway/index.js +1 -0
- package/dist/gateway/protocol/anthropic.js +1 -1
- package/dist/gateway/protocol/deps.d.ts +5 -5
- package/dist/gateway/protocol/openai.js +1 -1
- package/dist/gateway/proxy.d.ts +4 -4
- package/dist/gateway/route-mounting.test.js +1 -1
- package/dist/gateway/service-key-auth.d.ts +1 -1
- package/dist/gateway/service-key-auth.js +1 -1
- package/dist/gateway/service-key-repository.d.ts +27 -0
- package/dist/gateway/service-key-repository.js +64 -0
- package/dist/gateway/types.d.ts +5 -5
- package/dist/metering/reconciliation-cron.test.js +9 -8
- package/dist/metering/reconciliation-repository.js +12 -10
- package/dist/metering/reconciliation-repository.test.js +9 -8
- package/dist/monetization/affiliate/affiliate-admin-repository.js +10 -8
- package/dist/monetization/affiliate/affiliate-admin-repository.test.js +32 -13
- package/dist/monetization/affiliate/credit-match.d.ts +2 -2
- package/dist/monetization/affiliate/credit-match.js +4 -1
- package/dist/monetization/affiliate/credit-match.test.js +58 -13
- package/dist/monetization/affiliate/new-user-bonus.d.ts +2 -2
- package/dist/monetization/affiliate/new-user-bonus.js +4 -1
- package/dist/monetization/affiliate/new-user-bonus.test.js +4 -3
- package/dist/monetization/credits/auto-topup-charge.d.ts +2 -2
- package/dist/monetization/credits/auto-topup-charge.js +5 -1
- package/dist/monetization/credits/auto-topup-charge.test.js +5 -4
- package/dist/monetization/credits/auto-topup-usage.d.ts +2 -2
- package/dist/monetization/credits/auto-topup-usage.test.js +53 -12
- package/dist/monetization/credits/bot-billing.d.ts +3 -3
- package/dist/monetization/credits/bot-billing.test.js +18 -5
- package/dist/monetization/credits/credit-expiry-cron.test.js +25 -8
- package/dist/monetization/credits/dividend-cron.d.ts +2 -4
- package/dist/monetization/credits/dividend-cron.js +7 -4
- package/dist/monetization/credits/dividend-cron.test.js +26 -46
- package/dist/monetization/credits/dividend-repository.js +15 -24
- package/dist/monetization/credits/dividend-repository.test.js +4 -3
- package/dist/monetization/credits/index.d.ts +2 -2
- package/dist/monetization/credits/index.js +1 -1
- package/dist/monetization/credits/member-usage.test.js +23 -10
- package/dist/monetization/credits/phone-billing.d.ts +2 -2
- package/dist/monetization/credits/phone-billing.js +5 -1
- package/dist/monetization/credits/phone-billing.test.js +9 -12
- package/dist/monetization/credits/runtime-cron.d.ts +2 -2
- package/dist/monetization/credits/runtime-cron.js +32 -8
- package/dist/monetization/credits/runtime-cron.test.js +28 -27
- package/dist/monetization/credits/runtime-scheduler.d.ts +2 -2
- package/dist/monetization/credits/runtime-scheduler.test.js +1 -1
- package/dist/monetization/credits/signup-grant.test.js +5 -3
- package/dist/monetization/credits/storage-tier-cron.test.js +3 -2
- package/dist/monetization/credits/trial-balance-cron.test.js +42 -0
- package/dist/monetization/feature-gate.d.ts +3 -3
- package/dist/monetization/index.d.ts +3 -3
- package/dist/monetization/index.js +1 -1
- package/dist/monetization/metering/reconciliation-cron.test.js +9 -8
- package/dist/monetization/metering/reconciliation-repository.js +11 -10
- package/dist/monetization/metering/reconciliation-repository.test.js +9 -8
- package/dist/monetization/payram/webhook.d.ts +2 -2
- package/dist/monetization/payram/webhook.js +5 -1
- package/dist/monetization/payram/webhook.test.js +5 -4
- package/dist/monetization/promotions/engine.d.ts +2 -2
- package/dist/monetization/promotions/engine.js +4 -1
- package/dist/monetization/promotions/engine.test.js +3 -1
- package/dist/monetization/repository-types.d.ts +1 -1
- package/dist/monetization/socket/socket.d.ts +3 -3
- package/dist/monetization/stripe/stripe-payment-processor.d.ts +2 -2
- package/dist/monetization/stripe/stripe-payment-processor.test.js +7 -0
- package/dist/monetization/stripe/webhook.d.ts +2 -2
- package/dist/monetization/stripe/webhook.js +70 -6
- package/dist/monetization/stripe/webhook.test.js +20 -15
- package/dist/onboarding/onboarding-service.d.ts +2 -2
- package/dist/onboarding/onboarding-service.js +6 -2
- package/drizzle/migrations/0002_gateway_service_keys.sql +14 -0
- package/drizzle/migrations/0003_double_entry_ledger.sql +82 -0
- package/drizzle/migrations/meta/_journal.json +14 -0
- package/package.json +1 -1
- package/src/api/routes/admin-credits.ts +11 -14
- package/src/api/routes/quota.ts +2 -2
- package/src/api/routes/verify-email.ts +4 -4
- package/src/backup/on-demand-snapshot-service.test.ts +3 -3
- package/src/backup/on-demand-snapshot-service.ts +3 -3
- package/src/billing/payram/webhook.test.ts +7 -5
- package/src/billing/payram/webhook.ts +8 -11
- package/src/billing/stripe/stripe-payment-processor.test.ts +10 -3
- package/src/billing/stripe/stripe-payment-processor.ts +3 -3
- package/src/billing/stripe/tenant-store.ts +1 -1
- package/src/credits/auto-topup-charge.test.ts +7 -5
- package/src/credits/auto-topup-charge.ts +7 -10
- package/src/credits/auto-topup-usage.test.ts +55 -13
- package/src/credits/auto-topup-usage.ts +2 -2
- package/src/credits/credit-expiry-cron.test.ts +26 -45
- package/src/credits/credit-expiry-cron.ts +9 -12
- package/src/credits/credit-ledger.ts +3 -3
- package/src/credits/dividend-cron.test.ts +38 -45
- package/src/credits/dividend-cron.ts +12 -26
- package/src/credits/dividend-repository.test.ts +4 -3
- package/src/credits/dividend-repository.ts +21 -23
- package/src/credits/index.ts +23 -4
- package/src/credits/ledger.test.ts +514 -0
- package/src/credits/ledger.ts +851 -0
- package/src/credits/signup-grant.test.ts +7 -4
- package/src/credits/signup-grant.ts +6 -12
- package/src/credits/trial-balance-cron.test.ts +68 -0
- package/src/credits/trial-balance-cron.ts +46 -0
- package/src/db/schema/gateway-service-keys.ts +23 -0
- package/src/db/schema/index.ts +2 -0
- package/src/db/schema/ledger.ts +94 -0
- package/src/gateway/credit-gate-wiring.test.ts +3 -3
- package/src/gateway/credit-gate.test.ts +35 -33
- package/src/gateway/credit-gate.ts +6 -10
- package/src/gateway/gateway-routes.test.ts +6 -6
- package/src/gateway/index.ts +2 -0
- package/src/gateway/protocol/anthropic.ts +2 -2
- package/src/gateway/protocol/deps.ts +5 -5
- package/src/gateway/protocol/openai.ts +2 -2
- package/src/gateway/proxy.ts +4 -4
- package/src/gateway/route-mounting.test.ts +3 -3
- package/src/gateway/service-key-auth.ts +4 -2
- package/src/gateway/service-key-repository.ts +87 -0
- package/src/gateway/types.ts +5 -5
- package/src/metering/reconciliation-cron.test.ts +10 -9
- package/src/metering/reconciliation-repository.test.ts +10 -9
- package/src/metering/reconciliation-repository.ts +14 -11
- package/src/monetization/affiliate/affiliate-admin-repository.test.ts +32 -19
- package/src/monetization/affiliate/affiliate-admin-repository.ts +16 -8
- package/src/monetization/affiliate/credit-match.test.ts +60 -14
- package/src/monetization/affiliate/credit-match.ts +6 -9
- package/src/monetization/affiliate/new-user-bonus.test.ts +6 -4
- package/src/monetization/affiliate/new-user-bonus.ts +6 -9
- package/src/monetization/credits/auto-topup-charge.test.ts +7 -5
- package/src/monetization/credits/auto-topup-charge.ts +7 -10
- package/src/monetization/credits/auto-topup-usage.test.ts +55 -13
- package/src/monetization/credits/auto-topup-usage.ts +2 -2
- package/src/monetization/credits/bot-billing.test.ts +20 -6
- package/src/monetization/credits/bot-billing.ts +3 -3
- package/src/monetization/credits/credit-expiry-cron.test.ts +26 -45
- package/src/monetization/credits/dividend-cron.test.ts +34 -48
- package/src/monetization/credits/dividend-cron.ts +9 -14
- package/src/monetization/credits/dividend-repository.test.ts +4 -3
- package/src/monetization/credits/dividend-repository.ts +19 -25
- package/src/monetization/credits/index.ts +4 -4
- package/src/monetization/credits/member-usage.test.ts +25 -11
- package/src/monetization/credits/phone-billing.test.ts +18 -26
- package/src/monetization/credits/phone-billing.ts +7 -10
- package/src/monetization/credits/runtime-cron.test.ts +29 -28
- package/src/monetization/credits/runtime-cron.ts +34 -58
- package/src/monetization/credits/runtime-scheduler.test.ts +1 -1
- package/src/monetization/credits/runtime-scheduler.ts +2 -2
- package/src/monetization/credits/signup-grant.test.ts +7 -4
- package/src/monetization/credits/storage-tier-cron.test.ts +5 -3
- package/src/monetization/credits/trial-balance-cron.test.ts +52 -0
- package/src/monetization/feature-gate.ts +3 -3
- package/src/monetization/index.ts +4 -4
- package/src/monetization/metering/reconciliation-cron.test.ts +10 -9
- package/src/monetization/metering/reconciliation-repository.test.ts +11 -9
- package/src/monetization/metering/reconciliation-repository.ts +13 -11
- package/src/monetization/payram/webhook.test.ts +7 -5
- package/src/monetization/payram/webhook.ts +7 -10
- package/src/monetization/promotions/engine.test.ts +6 -5
- package/src/monetization/promotions/engine.ts +6 -3
- package/src/monetization/repository-types.ts +1 -1
- package/src/monetization/socket/socket.ts +4 -4
- package/src/monetization/stripe/stripe-payment-processor.test.ts +10 -3
- package/src/monetization/stripe/stripe-payment-processor.ts +3 -3
- package/src/monetization/stripe/webhook.test.ts +22 -16
- package/src/monetization/stripe/webhook.ts +75 -50
- package/src/onboarding/onboarding-service.ts +8 -11
- package/dist/credits/credit-ledger-extra.test.js +0 -40
- package/dist/credits/credit-ledger.bench.js +0 -33
- package/dist/credits/credit-ledger.test.d.ts +0 -4
- package/dist/credits/credit-ledger.test.js +0 -203
- package/dist/credits/credit-transaction-repository.test.js +0 -232
- package/dist/monetization/credits/credit-ledger-extra.test.d.ts +0 -1
- package/dist/monetization/credits/credit-ledger-extra.test.js +0 -39
- package/dist/monetization/credits/credit-ledger.bench.d.ts +0 -1
- package/dist/monetization/credits/credit-ledger.bench.js +0 -32
- package/dist/monetization/credits/credit-ledger.test.d.ts +0 -4
- package/dist/monetization/credits/credit-ledger.test.js +0 -202
- package/dist/monetization/credits/credit-transaction-repository.test.d.ts +0 -1
- package/dist/monetization/credits/credit-transaction-repository.test.js +0 -232
- package/src/credits/credit-ledger-extra.test.ts +0 -57
- package/src/credits/credit-ledger.bench.ts +0 -56
- package/src/credits/credit-ledger.test.ts +0 -276
- package/src/credits/credit-transaction-repository.test.ts +0 -274
- package/src/monetization/credits/credit-ledger-extra.test.ts +0 -56
- package/src/monetization/credits/credit-ledger.bench.ts +0 -55
- package/src/monetization/credits/credit-ledger.test.ts +0 -275
- package/src/monetization/credits/credit-transaction-repository.test.ts +0 -274
- /package/dist/credits/{credit-ledger-extra.test.d.ts → ledger.test.d.ts} +0 -0
- /package/dist/credits/{credit-ledger.bench.d.ts → trial-balance-cron.test.d.ts} +0 -0
- /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,
|
|
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 {
|
|
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:
|
|
21
|
-
creditLedger?:
|
|
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)}...`,
|
package/src/gateway/proxy.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 5. Return response to bot
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type {
|
|
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 {
|
|
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:
|
|
66
|
-
creditLedger?:
|
|
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 {
|
|
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
|
|
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
|
|
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(
|
|
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
|
+
}
|
package/src/gateway/types.ts
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* budget-checks, proxies to upstream providers, meters usage, and responds.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
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 {
|
|
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:
|
|
100
|
+
budgetChecker: IBudgetChecker;
|
|
101
101
|
/** CreditLedger instance for deducting credits after proxy calls (optional — if absent, credit deduction is skipped) */
|
|
102
|
-
creditLedger?:
|
|
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 {
|
|
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:
|
|
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
|
|
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 {
|
|
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:
|
|
129
|
+
let ledger: DrizzleLedger;
|
|
130
130
|
|
|
131
131
|
beforeEach(async () => {
|
|
132
132
|
await truncateAllTables(pool);
|
|
133
133
|
repo = new DrizzleAdapterUsageRepository(db);
|
|
134
|
-
ledger = new
|
|
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 {
|
|
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:
|
|
65
|
-
//
|
|
66
|
-
|
|
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(
|
|
71
|
+
.from(journalLines)
|
|
72
|
+
.innerJoin(journalEntries, eq(journalEntries.id, journalLines.journalEntryId))
|
|
71
73
|
.where(
|
|
72
74
|
and(
|
|
73
|
-
eq(
|
|
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`${
|
|
76
|
-
sql`${
|
|
78
|
+
sql`${journalEntries.postedAt}::timestamptz >= ${startIso}::timestamptz`,
|
|
79
|
+
sql`${journalEntries.postedAt}::timestamptz < ${endIso}::timestamptz`,
|
|
77
80
|
),
|
|
78
81
|
)
|
|
79
|
-
.groupBy(
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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 {
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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:
|
|
142
|
-
.from(
|
|
143
|
-
.where(
|
|
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();
|