@wopr-network/platform-core 1.13.3 → 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/index.d.ts +1 -0
- package/dist/db/schema/index.js +1 -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/protocol/deps.d.ts +2 -2
- package/dist/gateway/proxy.d.ts +2 -2
- package/dist/gateway/types.d.ts +2 -2
- 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/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/0003_double_entry_ledger.sql +82 -0
- package/drizzle/migrations/meta/_journal.json +7 -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/index.ts +1 -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 +5 -5
- package/src/gateway/protocol/deps.ts +2 -2
- package/src/gateway/proxy.ts +2 -2
- package/src/gateway/route-mounting.test.ts +2 -2
- package/src/gateway/types.ts +2 -2
- 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/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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for gateway credit gate — grace buffer and credits_exhausted behavior (WOP-821).
|
|
3
3
|
*/
|
|
4
|
-
import { Credit,
|
|
4
|
+
import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
7
|
import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
@@ -39,35 +39,36 @@ afterAll(async () => {
|
|
|
39
39
|
describe("creditBalanceCheck grace buffer", () => {
|
|
40
40
|
beforeEach(async () => {
|
|
41
41
|
await truncateAllTables(pool);
|
|
42
|
+
await new DrizzleLedger(db).seedSystemAccounts();
|
|
42
43
|
});
|
|
43
44
|
it("returns null when balance is above estimated cost (passes)", async () => {
|
|
44
|
-
const ledger = new
|
|
45
|
-
await ledger.credit("t1", Credit.fromCents(500), "purchase", "setup");
|
|
45
|
+
const ledger = new DrizzleLedger(db);
|
|
46
|
+
await ledger.credit("t1", Credit.fromCents(500), "purchase", { description: "setup" });
|
|
46
47
|
const c = await buildHonoContext("t1");
|
|
47
48
|
const deps = { creditLedger: ledger, topUpUrl: "/billing" };
|
|
48
49
|
expect(await creditBalanceCheck(c, deps, 1)).toBeNull();
|
|
49
50
|
});
|
|
50
51
|
it("returns null when balance is zero but within default grace buffer (passes)", async () => {
|
|
51
52
|
// Balance at exactly 0 — within the -50 grace buffer
|
|
52
|
-
const ledger = new
|
|
53
|
-
await ledger.credit("t1", Credit.fromCents(10), "purchase", "setup");
|
|
54
|
-
await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "drain");
|
|
53
|
+
const ledger = new DrizzleLedger(db);
|
|
54
|
+
await ledger.credit("t1", Credit.fromCents(10), "purchase", { description: "setup" });
|
|
55
|
+
await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", { description: "drain" });
|
|
55
56
|
const c = await buildHonoContext("t1");
|
|
56
57
|
const deps = { creditLedger: ledger, topUpUrl: "/billing" };
|
|
57
58
|
expect(await creditBalanceCheck(c, deps, 0)).toBeNull();
|
|
58
59
|
});
|
|
59
60
|
it("returns null when balance is -49 (within 50-cent grace buffer)", async () => {
|
|
60
|
-
const ledger = new
|
|
61
|
-
await ledger.credit("t1", Credit.fromCents(1), "purchase", "setup");
|
|
62
|
-
await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", "drain",
|
|
61
|
+
const ledger = new DrizzleLedger(db);
|
|
62
|
+
await ledger.credit("t1", Credit.fromCents(1), "purchase", { description: "setup" });
|
|
63
|
+
await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", { description: "drain", allowNegative: true }); // balance = -49
|
|
63
64
|
const c = await buildHonoContext("t1");
|
|
64
65
|
const deps = { creditLedger: ledger, topUpUrl: "/billing" };
|
|
65
66
|
expect(await creditBalanceCheck(c, deps, 0)).toBeNull();
|
|
66
67
|
});
|
|
67
68
|
it("returns credits_exhausted when balance is at -50 (at grace buffer limit)", async () => {
|
|
68
|
-
const ledger = new
|
|
69
|
-
await ledger.credit("t1", Credit.fromCents(1), "purchase", "setup");
|
|
70
|
-
await ledger.debit("t1", Credit.fromCents(51), "adapter_usage", "drain",
|
|
69
|
+
const ledger = new DrizzleLedger(db);
|
|
70
|
+
await ledger.credit("t1", Credit.fromCents(1), "purchase", { description: "setup" });
|
|
71
|
+
await ledger.debit("t1", Credit.fromCents(51), "adapter_usage", { description: "drain", allowNegative: true }); // balance = -50
|
|
71
72
|
const c = await buildHonoContext("t1");
|
|
72
73
|
const deps = { creditLedger: ledger, topUpUrl: "/billing" };
|
|
73
74
|
const result = await creditBalanceCheck(c, deps, 0);
|
|
@@ -75,9 +76,9 @@ describe("creditBalanceCheck grace buffer", () => {
|
|
|
75
76
|
expect(result?.code).toBe("credits_exhausted");
|
|
76
77
|
});
|
|
77
78
|
it("returns credits_exhausted when balance is at -51 (beyond grace buffer)", async () => {
|
|
78
|
-
const ledger = new
|
|
79
|
-
await ledger.credit("t1", Credit.fromCents(1), "purchase", "setup");
|
|
80
|
-
await ledger.debit("t1", Credit.fromCents(52), "adapter_usage", "drain",
|
|
79
|
+
const ledger = new DrizzleLedger(db);
|
|
80
|
+
await ledger.credit("t1", Credit.fromCents(1), "purchase", { description: "setup" });
|
|
81
|
+
await ledger.debit("t1", Credit.fromCents(52), "adapter_usage", { description: "drain", allowNegative: true }); // balance = -51
|
|
81
82
|
const c = await buildHonoContext("t1");
|
|
82
83
|
const deps = { creditLedger: ledger, topUpUrl: "/billing" };
|
|
83
84
|
const result = await creditBalanceCheck(c, deps, 0);
|
|
@@ -85,9 +86,9 @@ describe("creditBalanceCheck grace buffer", () => {
|
|
|
85
86
|
expect(result?.code).toBe("credits_exhausted");
|
|
86
87
|
});
|
|
87
88
|
it("returns credits_exhausted when custom graceBufferCents=0 and balance is 0", async () => {
|
|
88
|
-
const ledger = new
|
|
89
|
-
await ledger.credit("t1", Credit.fromCents(10), "purchase", "setup");
|
|
90
|
-
await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "drain"); // balance = 0
|
|
89
|
+
const ledger = new DrizzleLedger(db);
|
|
90
|
+
await ledger.credit("t1", Credit.fromCents(10), "purchase", { description: "setup" });
|
|
91
|
+
await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", { description: "drain" }); // balance = 0
|
|
91
92
|
const c = await buildHonoContext("t1");
|
|
92
93
|
const deps = { creditLedger: ledger, topUpUrl: "/billing", graceBufferCents: 0 };
|
|
93
94
|
const result = await creditBalanceCheck(c, deps, 0);
|
|
@@ -95,8 +96,8 @@ describe("creditBalanceCheck grace buffer", () => {
|
|
|
95
96
|
expect(result?.code).toBe("credits_exhausted");
|
|
96
97
|
});
|
|
97
98
|
it("returns insufficient_credits when balance positive but below estimated cost", async () => {
|
|
98
|
-
const ledger = new
|
|
99
|
-
await ledger.credit("t1", Credit.fromCents(5), "purchase", "setup");
|
|
99
|
+
const ledger = new DrizzleLedger(db);
|
|
100
|
+
await ledger.credit("t1", Credit.fromCents(5), "purchase", { description: "setup" });
|
|
100
101
|
const c = await buildHonoContext("t1");
|
|
101
102
|
const deps = { creditLedger: ledger, topUpUrl: "/billing" };
|
|
102
103
|
const result = await creditBalanceCheck(c, deps, 10);
|
|
@@ -110,26 +111,27 @@ describe("creditBalanceCheck grace buffer", () => {
|
|
|
110
111
|
describe("debitCredits with allowNegative and onBalanceExhausted", () => {
|
|
111
112
|
beforeEach(async () => {
|
|
112
113
|
await truncateAllTables(pool);
|
|
114
|
+
await new DrizzleLedger(db).seedSystemAccounts();
|
|
113
115
|
});
|
|
114
116
|
it("debit with cost that would exceed balance succeeds (allowNegative=true)", async () => {
|
|
115
|
-
const ledger = new
|
|
116
|
-
await ledger.credit("t1", Credit.fromCents(5), "purchase", "setup"); // balance = 5 cents
|
|
117
|
+
const ledger = new DrizzleLedger(db);
|
|
118
|
+
await ledger.credit("t1", Credit.fromCents(5), "purchase", { description: "setup" }); // balance = 5 cents
|
|
117
119
|
// costUsd = $0.10 = 10 cents, margin = 1.0
|
|
118
120
|
// This should push balance negative without throwing
|
|
119
121
|
await expect(debitCredits({ creditLedger: ledger, topUpUrl: "/billing" }, "t1", 0.1, 1.0, "chat-completions", "openrouter")).resolves.not.toThrow();
|
|
120
122
|
expect((await ledger.balance("t1")).isNegative()).toBe(true);
|
|
121
123
|
});
|
|
122
124
|
it("fires onBalanceExhausted when debit causes balance to cross zero", async () => {
|
|
123
|
-
const ledger = new
|
|
124
|
-
await ledger.credit("t1", Credit.fromCents(5), "purchase", "setup"); // balance = 5 cents
|
|
125
|
+
const ledger = new DrizzleLedger(db);
|
|
126
|
+
await ledger.credit("t1", Credit.fromCents(5), "purchase", { description: "setup" }); // balance = 5 cents
|
|
125
127
|
const onBalanceExhausted = vi.fn();
|
|
126
128
|
// costUsd = $0.10 = 10 cents with margin 1.0 → chargeCents = 10, pushes balance to -5
|
|
127
129
|
await debitCredits({ creditLedger: ledger, topUpUrl: "/billing", onBalanceExhausted }, "t1", 0.1, 1.0, "chat-completions", "openrouter");
|
|
128
130
|
expect(onBalanceExhausted).toHaveBeenCalledWith("t1", -5);
|
|
129
131
|
});
|
|
130
132
|
it("does NOT fire onBalanceExhausted when balance stays positive after debit", async () => {
|
|
131
|
-
const ledger = new
|
|
132
|
-
await ledger.credit("t1", Credit.fromCents(500), "purchase", "setup"); // balance = 500 cents
|
|
133
|
+
const ledger = new DrizzleLedger(db);
|
|
134
|
+
await ledger.credit("t1", Credit.fromCents(500), "purchase", { description: "setup" }); // balance = 500 cents
|
|
133
135
|
const onBalanceExhausted = vi.fn();
|
|
134
136
|
// costUsd = $0.01 = 1 cent → balance stays at 499
|
|
135
137
|
await debitCredits({ creditLedger: ledger, topUpUrl: "/billing", onBalanceExhausted }, "t1", 0.01, 1.0, "chat-completions", "openrouter");
|
|
@@ -137,8 +139,8 @@ describe("debitCredits with allowNegative and onBalanceExhausted", () => {
|
|
|
137
139
|
expect((await ledger.balance("t1")).greaterThan(Credit.ZERO)).toBe(true);
|
|
138
140
|
});
|
|
139
141
|
it("onBalanceExhausted callback receives correct tenantId and negative balance", async () => {
|
|
140
|
-
const ledger = new
|
|
141
|
-
await ledger.credit("t1", Credit.fromCents(3), "purchase", "setup"); // balance = 3 cents
|
|
142
|
+
const ledger = new DrizzleLedger(db);
|
|
143
|
+
await ledger.credit("t1", Credit.fromCents(3), "purchase", { description: "setup" }); // balance = 3 cents
|
|
142
144
|
const onBalanceExhausted = vi.fn();
|
|
143
145
|
// costUsd = $0.05 = 5 cents with margin 1.0 → pushes balance to -2
|
|
144
146
|
await debitCredits({ creditLedger: ledger, topUpUrl: "/billing", onBalanceExhausted }, "t1", 0.05, 1.0, "chat-completions", "openrouter");
|
|
@@ -146,17 +148,17 @@ describe("debitCredits with allowNegative and onBalanceExhausted", () => {
|
|
|
146
148
|
expect(onBalanceExhausted).toHaveBeenCalledWith("t1", -2);
|
|
147
149
|
});
|
|
148
150
|
it("calls onSpendAlertCrossed after successful debit", async () => {
|
|
149
|
-
const ledger = new
|
|
150
|
-
await ledger.credit("t1", Credit.fromCents(500), "purchase", "setup");
|
|
151
|
+
const ledger = new DrizzleLedger(db);
|
|
152
|
+
await ledger.credit("t1", Credit.fromCents(500), "purchase", { description: "setup" });
|
|
151
153
|
const onSpendAlertCrossed = vi.fn();
|
|
152
154
|
await debitCredits({ creditLedger: ledger, topUpUrl: "/billing", onSpendAlertCrossed }, "t1", 0.05, 1.0, "chat-completions", "openrouter");
|
|
153
155
|
expect(onSpendAlertCrossed).toHaveBeenCalledWith("t1");
|
|
154
156
|
});
|
|
155
157
|
it("does NOT fire onBalanceExhausted when balance was already negative before debit", async () => {
|
|
156
|
-
const ledger = new
|
|
158
|
+
const ledger = new DrizzleLedger(db);
|
|
157
159
|
// Start with negative balance: credit 5, debit 10 → balance = -5
|
|
158
|
-
await ledger.credit("t1", Credit.fromCents(5), "purchase", "setup");
|
|
159
|
-
await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "drain",
|
|
160
|
+
await ledger.credit("t1", Credit.fromCents(5), "purchase", { description: "setup" });
|
|
161
|
+
await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", { description: "drain", allowNegative: true });
|
|
160
162
|
const onBalanceExhausted = vi.fn();
|
|
161
163
|
// Another debit of 1 cent — balance goes from -5 to -6, but was already negative
|
|
162
164
|
await debitCredits({ creditLedger: ledger, topUpUrl: "/billing", onBalanceExhausted }, "t1", 0.01, 1.0, "chat-completions", "openrouter");
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Both the Anthropic and OpenAI handlers need the same set of services:
|
|
5
5
|
* budget checking, metering, provider configs, fetch, and service key resolution.
|
|
6
6
|
*/
|
|
7
|
-
import type { Credit,
|
|
7
|
+
import type { Credit, ILedger } from "@wopr-network/platform-core/credits";
|
|
8
8
|
import type { MeterEmitter } from "@wopr-network/platform-core/metering";
|
|
9
9
|
import type { IRateLimitRepository } from "../../api/rate-limit-repository.js";
|
|
10
10
|
import type { IBudgetChecker } from "../../monetization/budget/budget-checker.js";
|
|
@@ -16,7 +16,7 @@ import type { FetchFn, GatewayTenant, ProviderConfig } from "../types.js";
|
|
|
16
16
|
export interface ProtocolDeps {
|
|
17
17
|
meter: MeterEmitter;
|
|
18
18
|
budgetChecker: IBudgetChecker;
|
|
19
|
-
creditLedger?:
|
|
19
|
+
creditLedger?: ILedger;
|
|
20
20
|
topUpUrl: string;
|
|
21
21
|
graceBufferCents?: number;
|
|
22
22
|
providers: ProviderConfig;
|
package/dist/gateway/proxy.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 4. Emit meter event
|
|
10
10
|
* 5. Return response to bot
|
|
11
11
|
*/
|
|
12
|
-
import type {
|
|
12
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
13
13
|
import type { MeterEmitter } from "@wopr-network/platform-core/metering";
|
|
14
14
|
import type { Context } from "hono";
|
|
15
15
|
import type { IBudgetChecker } from "../monetization/budget/budget-checker.js";
|
|
@@ -20,7 +20,7 @@ import type { FetchFn, GatewayConfig, ProviderConfig } from "./types.js";
|
|
|
20
20
|
export interface ProxyDeps {
|
|
21
21
|
meter: MeterEmitter;
|
|
22
22
|
budgetChecker: IBudgetChecker;
|
|
23
|
-
creditLedger?:
|
|
23
|
+
creditLedger?: ILedger;
|
|
24
24
|
topUpUrl: string;
|
|
25
25
|
graceBufferCents?: number;
|
|
26
26
|
providers: ProviderConfig;
|
package/dist/gateway/types.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* /v1/... endpoints using WOPR service keys. The gateway authenticates,
|
|
6
6
|
* budget-checks, proxies to upstream providers, meters usage, and responds.
|
|
7
7
|
*/
|
|
8
|
-
import type {
|
|
8
|
+
import type { 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
11
|
import type { IBudgetChecker, SpendLimits } from "../monetization/budget/budget-checker.js";
|
|
@@ -103,7 +103,7 @@ export interface GatewayConfig {
|
|
|
103
103
|
/** BudgetChecker instance for pre-call budget validation */
|
|
104
104
|
budgetChecker: IBudgetChecker;
|
|
105
105
|
/** CreditLedger instance for deducting credits after proxy calls (optional — if absent, credit deduction is skipped) */
|
|
106
|
-
creditLedger?:
|
|
106
|
+
creditLedger?: ILedger;
|
|
107
107
|
/** URL to direct users to when they need to add credits (default: "/dashboard/credits") */
|
|
108
108
|
topUpUrl?: string;
|
|
109
109
|
/** Maximum negative credit balance (in cents) before hard-stop. Default: 50 (-$0.50). */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { Credit } from "../credits/credit.js";
|
|
4
|
-
import {
|
|
4
|
+
import { DrizzleLedger } from "../credits/ledger.js";
|
|
5
5
|
import { usageSummaries } from "../db/schema/meter-events.js";
|
|
6
6
|
import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
7
7
|
import { runReconciliation } from "./reconciliation-cron.js";
|
|
@@ -21,7 +21,7 @@ describe("runReconciliation", () => {
|
|
|
21
21
|
const t = await createTestDb();
|
|
22
22
|
pool = t.pool;
|
|
23
23
|
db = t.db;
|
|
24
|
-
ledger = new
|
|
24
|
+
ledger = new DrizzleLedger(db);
|
|
25
25
|
usageSummaryRepo = new DrizzleUsageSummaryRepository(db);
|
|
26
26
|
adapterUsageRepo = new DrizzleAdapterUsageRepository(db);
|
|
27
27
|
});
|
|
@@ -30,6 +30,7 @@ describe("runReconciliation", () => {
|
|
|
30
30
|
});
|
|
31
31
|
beforeEach(async () => {
|
|
32
32
|
await truncateAllTables(pool);
|
|
33
|
+
await ledger.seedSystemAccounts();
|
|
33
34
|
});
|
|
34
35
|
/** Insert a usage_summaries row directly. */
|
|
35
36
|
async function insertSummary(opts) {
|
|
@@ -57,7 +58,7 @@ describe("runReconciliation", () => {
|
|
|
57
58
|
const charge = Credit.fromCents(50);
|
|
58
59
|
await insertSummary({ tenant: "t1", totalCharge: charge.toRaw() });
|
|
59
60
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
60
|
-
await ledger.debit("t1", charge, "adapter_usage", "chat usage");
|
|
61
|
+
await ledger.debit("t1", charge, "adapter_usage", { description: "chat usage" });
|
|
61
62
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
62
63
|
expect(result.tenantsChecked).toBe(1);
|
|
63
64
|
expect(result.discrepancies).toEqual([]);
|
|
@@ -65,7 +66,7 @@ describe("runReconciliation", () => {
|
|
|
65
66
|
it("detects drift when metered charge exceeds ledger debit", async () => {
|
|
66
67
|
await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(100).toRaw() });
|
|
67
68
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
68
|
-
await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", "chat usage");
|
|
69
|
+
await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", { description: "chat usage" });
|
|
69
70
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
70
71
|
expect(result.tenantsChecked).toBe(1);
|
|
71
72
|
expect(result.discrepancies).toHaveLength(1);
|
|
@@ -91,7 +92,7 @@ describe("runReconciliation", () => {
|
|
|
91
92
|
await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(20).toRaw() });
|
|
92
93
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
93
94
|
// Debit as bot_runtime — should NOT count toward reconciliation
|
|
94
|
-
await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", "daily runtime");
|
|
95
|
+
await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", { description: "daily runtime" });
|
|
95
96
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
96
97
|
// Metered 20c, ledger adapter_usage = 0 => drift = 20c
|
|
97
98
|
expect(result.discrepancies).toHaveLength(1);
|
|
@@ -119,11 +120,11 @@ describe("runReconciliation", () => {
|
|
|
119
120
|
// t1: balanced
|
|
120
121
|
await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(50).toRaw() });
|
|
121
122
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
122
|
-
await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", "chat");
|
|
123
|
+
await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", { description: "chat" });
|
|
123
124
|
// t2: drifted
|
|
124
125
|
await insertSummary({ tenant: "t2", totalCharge: Credit.fromCents(100).toRaw() });
|
|
125
126
|
await ledger.credit("t2", Credit.fromCents(500), "purchase");
|
|
126
|
-
await ledger.debit("t2", Credit.fromCents(60), "adapter_usage", "chat");
|
|
127
|
+
await ledger.debit("t2", Credit.fromCents(60), "adapter_usage", { description: "chat" });
|
|
127
128
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
128
129
|
expect(result.tenantsChecked).toBe(2);
|
|
129
130
|
expect(result.discrepancies).toHaveLength(1);
|
|
@@ -154,7 +155,7 @@ describe("runReconciliation", () => {
|
|
|
154
155
|
// Metered 50c but debited 80c (over-billed)
|
|
155
156
|
await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(50).toRaw() });
|
|
156
157
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
157
|
-
await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", "chat usage");
|
|
158
|
+
await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", { description: "chat usage" });
|
|
158
159
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
159
160
|
expect(result.discrepancies).toHaveLength(1);
|
|
160
161
|
expect(result.discrepancies[0].driftRaw).toBe(Credit.fromCents(-30).toRaw());
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { and, eq, gte, lt, ne, sql } from "drizzle-orm";
|
|
2
|
-
import {
|
|
2
|
+
import { journalEntries, journalLines } from "../db/schema/ledger.js";
|
|
3
3
|
import { usageSummaries } from "../db/schema/meter-events.js";
|
|
4
4
|
export class DrizzleUsageSummaryRepository {
|
|
5
5
|
db;
|
|
@@ -25,19 +25,21 @@ export class DrizzleAdapterUsageRepository {
|
|
|
25
25
|
this.db = db;
|
|
26
26
|
}
|
|
27
27
|
async getAggregatedAdapterUsageDebits(startIso, endIso) {
|
|
28
|
+
// Sum the debit-side journal line amounts for adapter_usage entries.
|
|
29
|
+
// In double-entry: DR tenant liability (2000:<tenantId>), CR revenue:adapter_usage (4010).
|
|
30
|
+
// The debit line on the tenant account represents the charge amount.
|
|
28
31
|
const rows = await this.db
|
|
29
32
|
.select({
|
|
30
|
-
tenantId:
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
// raw SQL: Drizzle cannot express ABS with COALESCE and SUM
|
|
34
|
-
totalDebitRaw: sql `COALESCE(SUM(ABS(amount_credits)), 0)`,
|
|
33
|
+
tenantId: journalEntries.tenantId,
|
|
34
|
+
// raw SQL: Drizzle cannot express COALESCE with SUM aggregation
|
|
35
|
+
totalDebitRaw: sql `COALESCE(SUM(${journalLines.amount}), 0)`,
|
|
35
36
|
})
|
|
36
|
-
.from(
|
|
37
|
-
.
|
|
37
|
+
.from(journalLines)
|
|
38
|
+
.innerJoin(journalEntries, eq(journalEntries.id, journalLines.journalEntryId))
|
|
39
|
+
.where(and(eq(journalEntries.entryType, "adapter_usage"), eq(journalLines.side, "debit"),
|
|
38
40
|
// raw SQL: Drizzle cannot express timestamptz cast for text column date comparison
|
|
39
|
-
sql `${
|
|
40
|
-
.groupBy(
|
|
41
|
+
sql `${journalEntries.postedAt}::timestamptz >= ${startIso}::timestamptz`, sql `${journalEntries.postedAt}::timestamptz < ${endIso}::timestamptz`))
|
|
42
|
+
.groupBy(journalEntries.tenantId);
|
|
41
43
|
return rows.map((r) => ({ tenantId: r.tenantId, totalDebitRaw: Number(r.totalDebitRaw) }));
|
|
42
44
|
}
|
|
43
45
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
3
|
import { Credit } from "../credits/credit.js";
|
|
4
|
-
import {
|
|
4
|
+
import { DrizzleLedger } from "../credits/ledger.js";
|
|
5
5
|
import { createTestDb, seedUsageSummary, truncateAllTables } from "../test/db.js";
|
|
6
6
|
import { DrizzleAdapterUsageRepository, DrizzleUsageSummaryRepository } from "./reconciliation-repository.js";
|
|
7
7
|
let pool;
|
|
@@ -108,7 +108,8 @@ describe("DrizzleAdapterUsageRepository", () => {
|
|
|
108
108
|
beforeEach(async () => {
|
|
109
109
|
await truncateAllTables(pool);
|
|
110
110
|
repo = new DrizzleAdapterUsageRepository(db);
|
|
111
|
-
ledger = new
|
|
111
|
+
ledger = new DrizzleLedger(db);
|
|
112
|
+
await ledger.seedSystemAccounts();
|
|
112
113
|
});
|
|
113
114
|
it("returns empty array when no adapter_usage debits exist", async () => {
|
|
114
115
|
const today = new Date().toISOString().slice(0, 10);
|
|
@@ -121,9 +122,9 @@ describe("DrizzleAdapterUsageRepository", () => {
|
|
|
121
122
|
// Fund tenants
|
|
122
123
|
await ledger.credit("t1", Credit.fromCents(1000), "purchase");
|
|
123
124
|
await ledger.credit("t2", Credit.fromCents(1000), "purchase");
|
|
124
|
-
await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", "t1-debit-1");
|
|
125
|
-
await ledger.debit("t1", Credit.fromCents(20), "adapter_usage", "t1-debit-2");
|
|
126
|
-
await ledger.debit("t2", Credit.fromCents(50), "adapter_usage", "t2-debit-1");
|
|
125
|
+
await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", { description: "t1-debit-1" });
|
|
126
|
+
await ledger.debit("t1", Credit.fromCents(20), "adapter_usage", { description: "t1-debit-2" });
|
|
127
|
+
await ledger.debit("t2", Credit.fromCents(50), "adapter_usage", { description: "t2-debit-1" });
|
|
127
128
|
// Query window covering today
|
|
128
129
|
const today = new Date().toISOString().slice(0, 10);
|
|
129
130
|
const startIso = `${today}T00:00:00Z`;
|
|
@@ -137,8 +138,8 @@ describe("DrizzleAdapterUsageRepository", () => {
|
|
|
137
138
|
});
|
|
138
139
|
it("excludes non-adapter_usage debit types", async () => {
|
|
139
140
|
await ledger.credit("t1", Credit.fromCents(1000), "purchase");
|
|
140
|
-
await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", "adapter debit");
|
|
141
|
-
await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", "runtime debit");
|
|
141
|
+
await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", { description: "adapter debit" });
|
|
142
|
+
await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", { description: "runtime debit" });
|
|
142
143
|
const today = new Date().toISOString().slice(0, 10);
|
|
143
144
|
const startIso = `${today}T00:00:00Z`;
|
|
144
145
|
const endIso = new Date(new Date(startIso).getTime() + 86400000).toISOString();
|
|
@@ -148,7 +149,7 @@ describe("DrizzleAdapterUsageRepository", () => {
|
|
|
148
149
|
});
|
|
149
150
|
it("excludes credit transactions (positive amounts are not debits)", async () => {
|
|
150
151
|
await ledger.credit("t1", Credit.fromCents(1000), "purchase");
|
|
151
|
-
await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "real debit");
|
|
152
|
+
await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", { description: "real debit" });
|
|
152
153
|
const today = new Date().toISOString().slice(0, 10);
|
|
153
154
|
const startIso = `${today}T00:00:00Z`;
|
|
154
155
|
const endIso = new Date(new Date(startIso).getTime() + 86400000).toISOString();
|
|
@@ -3,7 +3,7 @@ import { and, count, desc, eq, gte, isNotNull, sql } from "drizzle-orm";
|
|
|
3
3
|
import { logger } from "../../config/logger.js";
|
|
4
4
|
import { affiliateReferrals } from "../../db/schema/affiliate.js";
|
|
5
5
|
import { affiliateFraudEvents } from "../../db/schema/affiliate-fraud.js";
|
|
6
|
-
import {
|
|
6
|
+
import { journalEntries } from "../../db/schema/ledger.js";
|
|
7
7
|
function parseSignals(raw) {
|
|
8
8
|
try {
|
|
9
9
|
const parsed = JSON.parse(raw);
|
|
@@ -84,10 +84,12 @@ export class DrizzleAffiliateFraudAdminRepository {
|
|
|
84
84
|
}
|
|
85
85
|
async listFingerprintClusters() {
|
|
86
86
|
const rows = (await this.db.execute(sql `
|
|
87
|
-
SELECT
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
SELECT metadata->>'stripeFingerprint' AS stripe_fingerprint,
|
|
88
|
+
array_agg(DISTINCT tenant_id ORDER BY tenant_id) AS tenant_ids
|
|
89
|
+
FROM journal_entries
|
|
90
|
+
WHERE metadata->>'stripeFingerprint' IS NOT NULL
|
|
91
|
+
AND entry_type = 'purchase'
|
|
92
|
+
GROUP BY metadata->>'stripeFingerprint'
|
|
91
93
|
HAVING COUNT(DISTINCT tenant_id) > 1
|
|
92
94
|
ORDER BY COUNT(DISTINCT tenant_id) DESC
|
|
93
95
|
`));
|
|
@@ -98,9 +100,9 @@ export class DrizzleAffiliateFraudAdminRepository {
|
|
|
98
100
|
}
|
|
99
101
|
async blockFingerprint(fingerprint, adminUserId) {
|
|
100
102
|
const rows = await this.db
|
|
101
|
-
.selectDistinct({ tenantId:
|
|
102
|
-
.from(
|
|
103
|
-
.where(eq(
|
|
103
|
+
.selectDistinct({ tenantId: journalEntries.tenantId })
|
|
104
|
+
.from(journalEntries)
|
|
105
|
+
.where(and(eq(journalEntries.entryType, "purchase"), sql `${journalEntries.metadata}->>'stripeFingerprint' = ${fingerprint}`));
|
|
104
106
|
const tenantIds = rows.map((r) => r.tenantId);
|
|
105
107
|
const now = new Date().toISOString();
|
|
106
108
|
for (const tenantId of tenantIds) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
3
|
import { affiliateReferrals } from "../../db/schema/affiliate.js";
|
|
4
4
|
import { affiliateFraudEvents } from "../../db/schema/affiliate-fraud.js";
|
|
5
|
-
import {
|
|
5
|
+
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
6
6
|
import { ADMIN_BLOCK_SENTINEL, DrizzleAffiliateFraudAdminRepository } from "./affiliate-admin-repository.js";
|
|
7
7
|
describe("DrizzleAffiliateFraudAdminRepository", () => {
|
|
8
8
|
let pool;
|
|
@@ -10,14 +10,13 @@ describe("DrizzleAffiliateFraudAdminRepository", () => {
|
|
|
10
10
|
let repo;
|
|
11
11
|
beforeAll(async () => {
|
|
12
12
|
({ db, pool } = await createTestDb());
|
|
13
|
-
await beginTestTransaction(pool);
|
|
14
13
|
});
|
|
15
14
|
afterAll(async () => {
|
|
16
|
-
await endTestTransaction(pool);
|
|
17
15
|
await pool.close();
|
|
18
16
|
});
|
|
19
17
|
beforeEach(async () => {
|
|
20
|
-
await
|
|
18
|
+
await truncateAllTables(pool);
|
|
19
|
+
await new DrizzleLedger(db).seedSystemAccounts();
|
|
21
20
|
repo = new DrizzleAffiliateFraudAdminRepository(db);
|
|
22
21
|
});
|
|
23
22
|
describe("listSuppressions", () => {
|
|
@@ -140,9 +139,17 @@ describe("DrizzleAffiliateFraudAdminRepository", () => {
|
|
|
140
139
|
});
|
|
141
140
|
describe("blockFingerprint", () => {
|
|
142
141
|
it("should insert fraud events with ADMIN_BLOCK as referredTenantId", async () => {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
const ledger = new DrizzleLedger(db);
|
|
143
|
+
await ledger.credit("t-alice", Credit.fromCents(1), "purchase", {
|
|
144
|
+
description: "test purchase",
|
|
145
|
+
referenceId: "ref-alice-fp_abc123",
|
|
146
|
+
stripeFingerprint: "fp_abc123",
|
|
147
|
+
});
|
|
148
|
+
await ledger.credit("t-bob", Credit.fromCents(1), "purchase", {
|
|
149
|
+
description: "test purchase",
|
|
150
|
+
referenceId: "ref-bob-fp_abc123",
|
|
151
|
+
stripeFingerprint: "fp_abc123",
|
|
152
|
+
});
|
|
146
153
|
await repo.blockFingerprint("fp_abc123", "admin-user-1");
|
|
147
154
|
const result = await repo.listSuppressions(50, 0);
|
|
148
155
|
expect(result.events).toHaveLength(2);
|
|
@@ -156,9 +163,17 @@ describe("DrizzleAffiliateFraudAdminRepository", () => {
|
|
|
156
163
|
expect(tenantIds).toEqual(["t-alice", "t-bob"]);
|
|
157
164
|
});
|
|
158
165
|
it("should use unique referralId per tenant to avoid unique constraint conflicts", async () => {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
166
|
+
const ledger = new DrizzleLedger(db);
|
|
167
|
+
await ledger.credit("t-carol", Credit.fromCents(1), "purchase", {
|
|
168
|
+
description: "test purchase",
|
|
169
|
+
referenceId: "ref-carol-fp_def456",
|
|
170
|
+
stripeFingerprint: "fp_def456",
|
|
171
|
+
});
|
|
172
|
+
await ledger.credit("t-dave", Credit.fromCents(1), "purchase", {
|
|
173
|
+
description: "test purchase",
|
|
174
|
+
referenceId: "ref-dave-fp_def456",
|
|
175
|
+
stripeFingerprint: "fp_def456",
|
|
176
|
+
});
|
|
162
177
|
await repo.blockFingerprint("fp_def456", "admin-user-2");
|
|
163
178
|
const result = await repo.listSuppressions(50, 0);
|
|
164
179
|
expect(result.events).toHaveLength(2);
|
|
@@ -169,8 +184,12 @@ describe("DrizzleAffiliateFraudAdminRepository", () => {
|
|
|
169
184
|
expect(new Set(referralIds).size).toBe(2);
|
|
170
185
|
});
|
|
171
186
|
it("should be idempotent via onConflictDoNothing", async () => {
|
|
172
|
-
|
|
173
|
-
|
|
187
|
+
const ledger = new DrizzleLedger(db);
|
|
188
|
+
await ledger.credit("t-eve", Credit.fromCents(1), "purchase", {
|
|
189
|
+
description: "test purchase",
|
|
190
|
+
referenceId: "ref-eve-fp_ghi789",
|
|
191
|
+
stripeFingerprint: "fp_ghi789",
|
|
192
|
+
});
|
|
174
193
|
await repo.blockFingerprint("fp_ghi789", "admin-user-3");
|
|
175
194
|
await repo.blockFingerprint("fp_ghi789", "admin-user-3");
|
|
176
195
|
const result = await repo.listSuppressions(50, 0);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { Credit,
|
|
1
|
+
import type { Credit, ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import type { IAffiliateFraudRepository } from "./affiliate-fraud-repository.js";
|
|
3
3
|
import type { IAffiliateRepository } from "./drizzle-affiliate-repository.js";
|
|
4
4
|
export interface AffiliateCreditMatchDeps {
|
|
5
5
|
tenantId: string;
|
|
6
6
|
purchaseAmount: Credit;
|
|
7
|
-
ledger:
|
|
7
|
+
ledger: ILedger;
|
|
8
8
|
affiliateRepo: IAffiliateRepository;
|
|
9
9
|
matchRate?: number;
|
|
10
10
|
fraudRepo?: IAffiliateFraudRepository;
|
|
@@ -85,7 +85,10 @@ export async function processAffiliateCreditMatch(deps) {
|
|
|
85
85
|
if (matchAmount.isZero() || matchAmount.isNegative())
|
|
86
86
|
return null;
|
|
87
87
|
// 6. Credit the referrer
|
|
88
|
-
await ledger.credit(referral.referrerTenantId, matchAmount, "affiliate_match",
|
|
88
|
+
await ledger.credit(referral.referrerTenantId, matchAmount, "affiliate_match", {
|
|
89
|
+
description: `Affiliate match for referred tenant ${tenantId}`,
|
|
90
|
+
referenceId: refId,
|
|
91
|
+
});
|
|
89
92
|
// 7. Update referral record
|
|
90
93
|
await affiliateRepo.markFirstPurchase(tenantId);
|
|
91
94
|
await affiliateRepo.recordMatch(tenantId, matchAmount);
|