@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
|
@@ -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);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Credit,
|
|
1
|
+
import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
3
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
4
4
|
import { DrizzleAffiliateFraudRepository } from "./affiliate-fraud-repository.js";
|
|
@@ -18,12 +18,17 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
18
18
|
});
|
|
19
19
|
beforeEach(async () => {
|
|
20
20
|
await truncateAllTables(pool);
|
|
21
|
-
ledger = new
|
|
21
|
+
ledger = new DrizzleLedger(db);
|
|
22
|
+
await ledger.seedSystemAccounts();
|
|
22
23
|
affiliateRepo = new DrizzleAffiliateRepository(db);
|
|
23
24
|
fraudRepo = new DrizzleAffiliateFraudRepository(db);
|
|
24
25
|
});
|
|
25
26
|
it("does nothing when tenant has no referral", async () => {
|
|
26
|
-
await ledger.credit("buyer", Credit.fromCents(1000), "purchase",
|
|
27
|
+
await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
|
|
28
|
+
description: "first buy",
|
|
29
|
+
referenceId: "session-1",
|
|
30
|
+
fundingSource: "stripe",
|
|
31
|
+
});
|
|
27
32
|
const result = await processAffiliateCreditMatch({
|
|
28
33
|
tenantId: "buyer",
|
|
29
34
|
purchaseAmount: Credit.fromCents(1000),
|
|
@@ -34,8 +39,16 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
34
39
|
});
|
|
35
40
|
it("does nothing when tenant already has prior purchases", async () => {
|
|
36
41
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
37
|
-
await ledger.credit("buyer", Credit.fromCents(500), "purchase",
|
|
38
|
-
|
|
42
|
+
await ledger.credit("buyer", Credit.fromCents(500), "purchase", {
|
|
43
|
+
description: "old buy",
|
|
44
|
+
referenceId: "session-0",
|
|
45
|
+
fundingSource: "stripe",
|
|
46
|
+
});
|
|
47
|
+
await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
|
|
48
|
+
description: "new buy",
|
|
49
|
+
referenceId: "session-1",
|
|
50
|
+
fundingSource: "stripe",
|
|
51
|
+
});
|
|
39
52
|
const result = await processAffiliateCreditMatch({
|
|
40
53
|
tenantId: "buyer",
|
|
41
54
|
purchaseAmount: Credit.fromCents(1000),
|
|
@@ -46,7 +59,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
46
59
|
});
|
|
47
60
|
it("credits referrer on first purchase with 100% match", async () => {
|
|
48
61
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
49
|
-
await ledger.credit("buyer", Credit.fromCents(2000), "purchase",
|
|
62
|
+
await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
|
|
63
|
+
description: "first buy",
|
|
64
|
+
referenceId: "session-1",
|
|
65
|
+
fundingSource: "stripe",
|
|
66
|
+
});
|
|
50
67
|
const result = await processAffiliateCreditMatch({
|
|
51
68
|
tenantId: "buyer",
|
|
52
69
|
purchaseAmount: Credit.fromCents(2000),
|
|
@@ -65,7 +82,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
65
82
|
});
|
|
66
83
|
it("respects custom match rate", async () => {
|
|
67
84
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
68
|
-
await ledger.credit("buyer", Credit.fromCents(2000), "purchase",
|
|
85
|
+
await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
|
|
86
|
+
description: "first buy",
|
|
87
|
+
referenceId: "session-1",
|
|
88
|
+
fundingSource: "stripe",
|
|
89
|
+
});
|
|
69
90
|
const result = await processAffiliateCreditMatch({
|
|
70
91
|
tenantId: "buyer",
|
|
71
92
|
purchaseAmount: Credit.fromCents(2000),
|
|
@@ -78,7 +99,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
78
99
|
});
|
|
79
100
|
it("is idempotent — second call returns null", async () => {
|
|
80
101
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
81
|
-
await ledger.credit("buyer", Credit.fromCents(1000), "purchase",
|
|
102
|
+
await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
|
|
103
|
+
description: "first buy",
|
|
104
|
+
referenceId: "session-1",
|
|
105
|
+
fundingSource: "stripe",
|
|
106
|
+
});
|
|
82
107
|
const first = await processAffiliateCreditMatch({
|
|
83
108
|
tenantId: "buyer",
|
|
84
109
|
purchaseAmount: Credit.fromCents(1000),
|
|
@@ -99,7 +124,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
99
124
|
signupIp: "1.2.3.4",
|
|
100
125
|
signupEmail: "alice+ref@gmail.com",
|
|
101
126
|
});
|
|
102
|
-
await ledger.credit("buyer", Credit.fromCents(2000), "purchase",
|
|
127
|
+
await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
|
|
128
|
+
description: "first buy",
|
|
129
|
+
referenceId: "session-1",
|
|
130
|
+
fundingSource: "stripe",
|
|
131
|
+
});
|
|
103
132
|
const result = await processAffiliateCreditMatch({
|
|
104
133
|
tenantId: "buyer",
|
|
105
134
|
purchaseAmount: Credit.fromCents(2000),
|
|
@@ -127,7 +156,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
127
156
|
}
|
|
128
157
|
// New referral
|
|
129
158
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
130
|
-
await ledger.credit("buyer", Credit.fromCents(1000), "purchase",
|
|
159
|
+
await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
|
|
160
|
+
description: "first buy",
|
|
161
|
+
referenceId: "session-1",
|
|
162
|
+
fundingSource: "stripe",
|
|
163
|
+
});
|
|
131
164
|
const result = await processAffiliateCreditMatch({
|
|
132
165
|
tenantId: "buyer",
|
|
133
166
|
purchaseAmount: Credit.fromCents(1000),
|
|
@@ -151,7 +184,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
151
184
|
}
|
|
152
185
|
// New referral
|
|
153
186
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
154
|
-
await ledger.credit("buyer", Credit.fromCents(1000), "purchase",
|
|
187
|
+
await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
|
|
188
|
+
description: "first buy",
|
|
189
|
+
referenceId: "session-1",
|
|
190
|
+
fundingSource: "stripe",
|
|
191
|
+
});
|
|
155
192
|
const result = await processAffiliateCreditMatch({
|
|
156
193
|
tenantId: "buyer",
|
|
157
194
|
purchaseAmount: Credit.fromCents(1000),
|
|
@@ -167,7 +204,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
167
204
|
});
|
|
168
205
|
it("allows payout when under both caps", async () => {
|
|
169
206
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
170
|
-
await ledger.credit("buyer", Credit.fromCents(2000), "purchase",
|
|
207
|
+
await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
|
|
208
|
+
description: "first buy",
|
|
209
|
+
referenceId: "session-1",
|
|
210
|
+
fundingSource: "stripe",
|
|
211
|
+
});
|
|
171
212
|
const result = await processAffiliateCreditMatch({
|
|
172
213
|
tenantId: "buyer",
|
|
173
214
|
purchaseAmount: Credit.fromCents(2000),
|
|
@@ -184,7 +225,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
184
225
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123", {
|
|
185
226
|
signupIp: "1.2.3.4",
|
|
186
227
|
});
|
|
187
|
-
await ledger.credit("buyer", Credit.fromCents(2000), "purchase",
|
|
228
|
+
await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
|
|
229
|
+
description: "first buy",
|
|
230
|
+
referenceId: "session-1",
|
|
231
|
+
fundingSource: "stripe",
|
|
232
|
+
});
|
|
188
233
|
const result = await processAffiliateCreditMatch({
|
|
189
234
|
tenantId: "buyer",
|
|
190
235
|
purchaseAmount: Credit.fromCents(2000),
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { Credit } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import type { IAffiliateRepository } from "./drizzle-affiliate-repository.js";
|
|
4
4
|
/** Default bonus rate: 20% of purchase amount. Override with AFFILIATE_NEW_USER_BONUS_RATE env var. */
|
|
5
5
|
export declare const DEFAULT_BONUS_RATE: number;
|
|
6
6
|
export interface NewUserBonusParams {
|
|
7
|
-
ledger:
|
|
7
|
+
ledger: ILedger;
|
|
8
8
|
affiliateRepo: IAffiliateRepository;
|
|
9
9
|
referredTenantId: string;
|
|
10
10
|
purchaseAmount: Credit;
|
|
@@ -34,6 +34,9 @@ export async function grantNewUserBonus(params) {
|
|
|
34
34
|
// 5. Mark first purchase on the referral row (no-op if already set)
|
|
35
35
|
await affiliateRepo.markFirstPurchase(referredTenantId);
|
|
36
36
|
// 6. Credit the bonus
|
|
37
|
-
await ledger.credit(referredTenantId, bonus, "affiliate_bonus",
|
|
37
|
+
await ledger.credit(referredTenantId, bonus, "affiliate_bonus", {
|
|
38
|
+
description: `New user first-purchase bonus (${Math.round(rate * 100)}%)`,
|
|
39
|
+
referenceId: refId,
|
|
40
|
+
});
|
|
38
41
|
return { granted: true, bonus };
|
|
39
42
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Credit,
|
|
1
|
+
import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
3
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
4
4
|
import { DrizzleAffiliateRepository } from "./drizzle-affiliate-repository.js";
|
|
@@ -16,7 +16,8 @@ describe("grantNewUserBonus", () => {
|
|
|
16
16
|
});
|
|
17
17
|
beforeEach(async () => {
|
|
18
18
|
await truncateAllTables(pool);
|
|
19
|
-
ledger = new
|
|
19
|
+
ledger = new DrizzleLedger(db);
|
|
20
|
+
await ledger.seedSystemAccounts();
|
|
20
21
|
affiliateRepo = new DrizzleAffiliateRepository(db);
|
|
21
22
|
});
|
|
22
23
|
it("DEFAULT_BONUS_RATE equals 0.20", () => {
|
|
@@ -37,7 +38,7 @@ describe("grantNewUserBonus", () => {
|
|
|
37
38
|
expect((await ledger.balance("referred-1")).toCents()).toBe(1000);
|
|
38
39
|
const txns = await ledger.history("referred-1");
|
|
39
40
|
expect(txns).toHaveLength(1);
|
|
40
|
-
expect(txns[0].
|
|
41
|
+
expect(txns[0].entryType).toBe("affiliate_bonus");
|
|
41
42
|
expect(txns[0].referenceId).toBe("affiliate-bonus:referred-1");
|
|
42
43
|
expect(txns[0].description).toContain("first-purchase bonus");
|
|
43
44
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ITenantCustomerRepository } from "@wopr-network/platform-core/billing";
|
|
2
|
-
import type { Credit,
|
|
2
|
+
import type { Credit, ILedger } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import Stripe from "stripe";
|
|
4
4
|
import type { IAutoTopupEventLogRepository } from "./auto-topup-event-log-repository.js";
|
|
5
5
|
/** After this many consecutive Stripe failures, the auto-topup mode is disabled. */
|
|
@@ -7,7 +7,7 @@ export declare const MAX_CONSECUTIVE_FAILURES = 3;
|
|
|
7
7
|
export interface AutoTopupChargeDeps {
|
|
8
8
|
stripe: Stripe;
|
|
9
9
|
tenantRepo: ITenantCustomerRepository;
|
|
10
|
-
creditLedger:
|
|
10
|
+
creditLedger: ILedger;
|
|
11
11
|
eventLogRepo: IAutoTopupEventLogRepository;
|
|
12
12
|
}
|
|
13
13
|
export interface AutoTopupChargeResult {
|
|
@@ -113,7 +113,11 @@ export async function chargeAutoTopup(deps, tenantId, amount, source) {
|
|
|
113
113
|
// 5. Credit the ledger (idempotent via referenceId = PI ID)
|
|
114
114
|
try {
|
|
115
115
|
if (!(await deps.creditLedger.hasReferenceId(paymentIntent.id))) {
|
|
116
|
-
await deps.creditLedger.credit(tenantId, amount, "purchase",
|
|
116
|
+
await deps.creditLedger.credit(tenantId, amount, "purchase", {
|
|
117
|
+
description: `Auto-topup (${source})`,
|
|
118
|
+
referenceId: paymentIntent.id,
|
|
119
|
+
fundingSource: "stripe",
|
|
120
|
+
});
|
|
117
121
|
}
|
|
118
122
|
}
|
|
119
123
|
catch (err) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import { Credit,
|
|
2
|
+
import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import Stripe from "stripe";
|
|
4
4
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { creditAutoTopup } from "../../db/schema/credit-auto-topup.js";
|
|
@@ -46,7 +46,8 @@ describe("chargeAutoTopup", () => {
|
|
|
46
46
|
});
|
|
47
47
|
beforeEach(async () => {
|
|
48
48
|
await truncateAllTables(pool);
|
|
49
|
-
ledger = new
|
|
49
|
+
ledger = new DrizzleLedger(db);
|
|
50
|
+
await ledger.seedSystemAccounts();
|
|
50
51
|
});
|
|
51
52
|
it("charges Stripe and credits ledger on success", async () => {
|
|
52
53
|
const stripe = mockStripe();
|
|
@@ -62,8 +63,8 @@ describe("chargeAutoTopup", () => {
|
|
|
62
63
|
expect(result.paymentReference).toEqual(expect.any(String));
|
|
63
64
|
expect((await ledger.balance("t1")).toCents()).toBe(500);
|
|
64
65
|
const history = await ledger.history("t1");
|
|
65
|
-
expect(history[0].
|
|
66
|
-
expect(history[0].fundingSource).toBe("stripe");
|
|
66
|
+
expect(history[0].entryType).toBe("purchase");
|
|
67
|
+
expect(history[0].metadata?.fundingSource).toBe("stripe");
|
|
67
68
|
});
|
|
68
69
|
it("writes success event to credit_auto_topup log", async () => {
|
|
69
70
|
const stripe = mockStripe();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Credit, IAutoTopupSettingsRepository,
|
|
1
|
+
import type { Credit, IAutoTopupSettingsRepository, ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import type { AutoTopupChargeResult } from "./auto-topup-charge.js";
|
|
3
3
|
export interface UsageTopupDeps {
|
|
4
4
|
settingsRepo: IAutoTopupSettingsRepository;
|
|
5
|
-
creditLedger:
|
|
5
|
+
creditLedger: ILedger;
|
|
6
6
|
/** Injected charge function (allows mocking in tests). */
|
|
7
7
|
chargeAutoTopup: (tenantId: string, amount: Credit, source: string) => Promise<AutoTopupChargeResult>;
|
|
8
8
|
/** Optional tenant status check. If provided and returns non-null, skip the charge. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Credit,
|
|
1
|
+
import { Credit, DrizzleAutoTopupSettingsRepository, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
4
4
|
import { maybeTriggerUsageTopup } from "./auto-topup-usage.js";
|
|
@@ -15,7 +15,8 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
15
15
|
});
|
|
16
16
|
beforeEach(async () => {
|
|
17
17
|
await truncateAllTables(pool);
|
|
18
|
-
ledger = new
|
|
18
|
+
ledger = new DrizzleLedger(db);
|
|
19
|
+
await ledger.seedSystemAccounts();
|
|
19
20
|
settingsRepo = new DrizzleAutoTopupSettingsRepository(db);
|
|
20
21
|
});
|
|
21
22
|
it("does nothing when tenant has no auto-topup settings", async () => {
|
|
@@ -26,7 +27,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
26
27
|
});
|
|
27
28
|
it("does nothing when usage_enabled is false", async () => {
|
|
28
29
|
await settingsRepo.upsert("t1", { usageEnabled: false });
|
|
29
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
30
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
31
|
+
description: "buy",
|
|
32
|
+
referenceId: "ref-1",
|
|
33
|
+
fundingSource: "stripe",
|
|
34
|
+
});
|
|
30
35
|
const mockCharge = vi.fn();
|
|
31
36
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
32
37
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -38,7 +43,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
38
43
|
usageThreshold: Credit.fromCents(100),
|
|
39
44
|
usageTopup: Credit.fromCents(500),
|
|
40
45
|
});
|
|
41
|
-
await ledger.credit("t1", Credit.fromCents(200), "purchase",
|
|
46
|
+
await ledger.credit("t1", Credit.fromCents(200), "purchase", {
|
|
47
|
+
description: "buy",
|
|
48
|
+
referenceId: "ref-1",
|
|
49
|
+
fundingSource: "stripe",
|
|
50
|
+
});
|
|
42
51
|
const mockCharge = vi.fn();
|
|
43
52
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
44
53
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -50,7 +59,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
50
59
|
usageThreshold: Credit.fromCents(100),
|
|
51
60
|
usageTopup: Credit.fromCents(500),
|
|
52
61
|
});
|
|
53
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
62
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
63
|
+
description: "buy",
|
|
64
|
+
referenceId: "ref-1",
|
|
65
|
+
fundingSource: "stripe",
|
|
66
|
+
});
|
|
54
67
|
const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_123" });
|
|
55
68
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
56
69
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -59,7 +72,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
59
72
|
it("skips when charge is already in-flight", async () => {
|
|
60
73
|
await settingsRepo.upsert("t1", { usageEnabled: true, usageThreshold: Credit.fromCents(100) });
|
|
61
74
|
await settingsRepo.setUsageChargeInFlight("t1", true);
|
|
62
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
75
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
76
|
+
description: "buy",
|
|
77
|
+
referenceId: "ref-1",
|
|
78
|
+
fundingSource: "stripe",
|
|
79
|
+
});
|
|
63
80
|
const mockCharge = vi.fn();
|
|
64
81
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
65
82
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -72,7 +89,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
72
89
|
usageThreshold: Credit.fromCents(500),
|
|
73
90
|
usageTopup: Credit.fromCents(2000),
|
|
74
91
|
});
|
|
75
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase",
|
|
92
|
+
await ledger.credit("t1", Credit.fromCents(100), "purchase", {
|
|
93
|
+
description: "buy",
|
|
94
|
+
referenceId: "ref-1",
|
|
95
|
+
fundingSource: "stripe",
|
|
96
|
+
});
|
|
76
97
|
const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_race" });
|
|
77
98
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
78
99
|
// Fire two concurrent calls — both see balance < threshold,
|
|
@@ -91,7 +112,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
91
112
|
usageThreshold: Credit.fromCents(100),
|
|
92
113
|
usageTopup: Credit.fromCents(500),
|
|
93
114
|
});
|
|
94
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
115
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
116
|
+
description: "buy",
|
|
117
|
+
referenceId: "ref-1",
|
|
118
|
+
fundingSource: "stripe",
|
|
119
|
+
});
|
|
95
120
|
const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_123" });
|
|
96
121
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
97
122
|
// First call — triggers charge, flag set then cleared
|
|
@@ -109,7 +134,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
109
134
|
usageThreshold: Credit.fromCents(100),
|
|
110
135
|
usageTopup: Credit.fromCents(500),
|
|
111
136
|
});
|
|
112
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
137
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
138
|
+
description: "buy",
|
|
139
|
+
referenceId: "ref-1",
|
|
140
|
+
fundingSource: "stripe",
|
|
141
|
+
});
|
|
113
142
|
const mockCharge = vi
|
|
114
143
|
.fn()
|
|
115
144
|
.mockRejectedValueOnce(new Error("Stripe network error"))
|
|
@@ -132,7 +161,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
132
161
|
});
|
|
133
162
|
await settingsRepo.incrementUsageFailures("t1");
|
|
134
163
|
await settingsRepo.incrementUsageFailures("t1");
|
|
135
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
164
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
165
|
+
description: "buy",
|
|
166
|
+
referenceId: "ref-1",
|
|
167
|
+
fundingSource: "stripe",
|
|
168
|
+
});
|
|
136
169
|
const mockCharge = vi.fn().mockResolvedValue({ success: true });
|
|
137
170
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
138
171
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -144,7 +177,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
144
177
|
usageThreshold: Credit.fromCents(100),
|
|
145
178
|
usageTopup: Credit.fromCents(500),
|
|
146
179
|
});
|
|
147
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
180
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
181
|
+
description: "buy",
|
|
182
|
+
referenceId: "ref-1",
|
|
183
|
+
fundingSource: "stripe",
|
|
184
|
+
});
|
|
148
185
|
const mockCharge = vi.fn().mockResolvedValue({ success: false, error: "declined" });
|
|
149
186
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
150
187
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -170,7 +207,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
170
207
|
});
|
|
171
208
|
await settingsRepo.incrementUsageFailures("t1");
|
|
172
209
|
await settingsRepo.incrementUsageFailures("t1");
|
|
173
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
210
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
211
|
+
description: "buy",
|
|
212
|
+
referenceId: "ref-1",
|
|
213
|
+
fundingSource: "stripe",
|
|
214
|
+
});
|
|
174
215
|
const mockCharge = vi.fn().mockResolvedValue({ success: false, error: "declined" });
|
|
175
216
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
176
217
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { Credit } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import type { IBotInstanceRepository } from "../../fleet/bot-instance-repository.js";
|
|
4
4
|
import type { INodeCommandBus } from "../../fleet/node-command-bus.js";
|
|
@@ -11,7 +11,7 @@ export interface IBotBilling {
|
|
|
11
11
|
suspendBot(botId: string): Promise<void>;
|
|
12
12
|
suspendAllForTenant(tenantId: string): Promise<string[]>;
|
|
13
13
|
reactivateBot(botId: string): Promise<void>;
|
|
14
|
-
checkReactivation(tenantId: string, ledger:
|
|
14
|
+
checkReactivation(tenantId: string, ledger: ILedger): Promise<string[]>;
|
|
15
15
|
destroyBot(botId: string): Promise<void>;
|
|
16
16
|
destroyExpiredBots(): Promise<string[]>;
|
|
17
17
|
getBotBilling(botId: string): Promise<unknown>;
|
|
@@ -56,7 +56,7 @@ export declare class DrizzleBotBilling implements IBotBilling {
|
|
|
56
56
|
*
|
|
57
57
|
* @returns IDs of reactivated bots.
|
|
58
58
|
*/
|
|
59
|
-
checkReactivation(tenantId: string, ledger:
|
|
59
|
+
checkReactivation(tenantId: string, ledger: ILedger): Promise<string[]>;
|
|
60
60
|
/**
|
|
61
61
|
* Mark a bot as destroyed.
|
|
62
62
|
* Sets billingState='destroyed'. Actual Docker cleanup is handled by the caller.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Credit,
|
|
1
|
+
import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { sql } from "drizzle-orm";
|
|
3
3
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { botInstances } from "../../db/schema/bot-instances.js";
|
|
@@ -61,7 +61,8 @@ describe("BotBilling", () => {
|
|
|
61
61
|
beforeEach(async () => {
|
|
62
62
|
await truncateAllTables(pool);
|
|
63
63
|
billing = new BotBilling(new DrizzleBotInstanceRepository(db));
|
|
64
|
-
ledger = new
|
|
64
|
+
ledger = new DrizzleLedger(db);
|
|
65
|
+
await ledger.seedSystemAccounts();
|
|
65
66
|
});
|
|
66
67
|
describe("registerBot", () => {
|
|
67
68
|
it("registers a bot in active billing state", async () => {
|
|
@@ -178,7 +179,11 @@ describe("BotBilling", () => {
|
|
|
178
179
|
await billing.registerBot("bot-2", "tenant-1", "bot-b");
|
|
179
180
|
await billing.suspendBot("bot-1");
|
|
180
181
|
await billing.suspendBot("bot-2");
|
|
181
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase",
|
|
182
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", {
|
|
183
|
+
description: "test credit",
|
|
184
|
+
referenceId: "ref-1",
|
|
185
|
+
fundingSource: "stripe",
|
|
186
|
+
});
|
|
182
187
|
const reactivated = await billing.checkReactivation("tenant-1", ledger);
|
|
183
188
|
expect(reactivated.sort()).toEqual(["bot-1", "bot-2"]);
|
|
184
189
|
expect(await billing.getActiveBotCount("tenant-1")).toBe(2);
|
|
@@ -193,12 +198,20 @@ describe("BotBilling", () => {
|
|
|
193
198
|
it("does not reactivate destroyed bots", async () => {
|
|
194
199
|
await billing.registerBot("bot-1", "tenant-1", "bot-a");
|
|
195
200
|
await billing.destroyBot("bot-1");
|
|
196
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase",
|
|
201
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", {
|
|
202
|
+
description: "test credit",
|
|
203
|
+
referenceId: "ref-1",
|
|
204
|
+
fundingSource: "stripe",
|
|
205
|
+
});
|
|
197
206
|
const reactivated = await billing.checkReactivation("tenant-1", ledger);
|
|
198
207
|
expect(reactivated).toEqual([]);
|
|
199
208
|
});
|
|
200
209
|
it("returns empty array for tenant with no bots", async () => {
|
|
201
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase",
|
|
210
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", {
|
|
211
|
+
description: "test credit",
|
|
212
|
+
referenceId: "ref-1",
|
|
213
|
+
fundingSource: "stripe",
|
|
214
|
+
});
|
|
202
215
|
const reactivated = await billing.checkReactivation("tenant-1", ledger);
|
|
203
216
|
expect(reactivated).toEqual([]);
|
|
204
217
|
});
|