@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
|
@@ -2,7 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import type { PGlite } from "@electric-sql/pglite";
|
|
3
3
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { Credit } from "../credits/credit.js";
|
|
5
|
-
import {
|
|
5
|
+
import { DrizzleLedger } from "../credits/ledger.js";
|
|
6
6
|
import type { PlatformDb } from "../db/index.js";
|
|
7
7
|
import { usageSummaries } from "../db/schema/meter-events.js";
|
|
8
8
|
import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
@@ -18,7 +18,7 @@ const DAY_END = DAY_START + 24 * 60 * 60 * 1000;
|
|
|
18
18
|
describe("runReconciliation", () => {
|
|
19
19
|
let pool: PGlite;
|
|
20
20
|
let db: PlatformDb;
|
|
21
|
-
let ledger:
|
|
21
|
+
let ledger: DrizzleLedger;
|
|
22
22
|
let usageSummaryRepo: DrizzleUsageSummaryRepository;
|
|
23
23
|
let adapterUsageRepo: DrizzleAdapterUsageRepository;
|
|
24
24
|
|
|
@@ -26,7 +26,7 @@ describe("runReconciliation", () => {
|
|
|
26
26
|
const t = await createTestDb();
|
|
27
27
|
pool = t.pool;
|
|
28
28
|
db = t.db;
|
|
29
|
-
ledger = new
|
|
29
|
+
ledger = new DrizzleLedger(db);
|
|
30
30
|
usageSummaryRepo = new DrizzleUsageSummaryRepository(db);
|
|
31
31
|
adapterUsageRepo = new DrizzleAdapterUsageRepository(db);
|
|
32
32
|
});
|
|
@@ -37,6 +37,7 @@ describe("runReconciliation", () => {
|
|
|
37
37
|
|
|
38
38
|
beforeEach(async () => {
|
|
39
39
|
await truncateAllTables(pool);
|
|
40
|
+
await ledger.seedSystemAccounts();
|
|
40
41
|
});
|
|
41
42
|
|
|
42
43
|
/** Insert a usage_summaries row directly. */
|
|
@@ -75,7 +76,7 @@ describe("runReconciliation", () => {
|
|
|
75
76
|
await insertSummary({ tenant: "t1", totalCharge: charge.toRaw() });
|
|
76
77
|
|
|
77
78
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
78
|
-
await ledger.debit("t1", charge, "adapter_usage", "chat usage");
|
|
79
|
+
await ledger.debit("t1", charge, "adapter_usage", { description: "chat usage" });
|
|
79
80
|
|
|
80
81
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
81
82
|
expect(result.tenantsChecked).toBe(1);
|
|
@@ -86,7 +87,7 @@ describe("runReconciliation", () => {
|
|
|
86
87
|
await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(100).toRaw() });
|
|
87
88
|
|
|
88
89
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
89
|
-
await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", "chat usage");
|
|
90
|
+
await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", { description: "chat usage" });
|
|
90
91
|
|
|
91
92
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
92
93
|
expect(result.tenantsChecked).toBe(1);
|
|
@@ -118,7 +119,7 @@ describe("runReconciliation", () => {
|
|
|
118
119
|
|
|
119
120
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
120
121
|
// Debit as bot_runtime — should NOT count toward reconciliation
|
|
121
|
-
await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", "daily runtime");
|
|
122
|
+
await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", { description: "daily runtime" });
|
|
122
123
|
|
|
123
124
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
124
125
|
// Metered 20c, ledger adapter_usage = 0 => drift = 20c
|
|
@@ -150,12 +151,12 @@ describe("runReconciliation", () => {
|
|
|
150
151
|
// t1: balanced
|
|
151
152
|
await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(50).toRaw() });
|
|
152
153
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
153
|
-
await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", "chat");
|
|
154
|
+
await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", { description: "chat" });
|
|
154
155
|
|
|
155
156
|
// t2: drifted
|
|
156
157
|
await insertSummary({ tenant: "t2", totalCharge: Credit.fromCents(100).toRaw() });
|
|
157
158
|
await ledger.credit("t2", Credit.fromCents(500), "purchase");
|
|
158
|
-
await ledger.debit("t2", Credit.fromCents(60), "adapter_usage", "chat");
|
|
159
|
+
await ledger.debit("t2", Credit.fromCents(60), "adapter_usage", { description: "chat" });
|
|
159
160
|
|
|
160
161
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
161
162
|
expect(result.tenantsChecked).toBe(2);
|
|
@@ -193,7 +194,7 @@ describe("runReconciliation", () => {
|
|
|
193
194
|
await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(50).toRaw() });
|
|
194
195
|
|
|
195
196
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
196
|
-
await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", "chat usage");
|
|
197
|
+
await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", { description: "chat usage" });
|
|
197
198
|
|
|
198
199
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
199
200
|
expect(result.discrepancies).toHaveLength(1);
|
|
@@ -2,7 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import type { PGlite } from "@electric-sql/pglite";
|
|
3
3
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
4
4
|
import { Credit } from "../credits/credit.js";
|
|
5
|
-
import {
|
|
5
|
+
import { DrizzleLedger } from "../credits/ledger.js";
|
|
6
6
|
import type { PlatformDb } from "../db/index.js";
|
|
7
7
|
import { createTestDb, seedUsageSummary, truncateAllTables } from "../test/db.js";
|
|
8
8
|
import { DrizzleAdapterUsageRepository, DrizzleUsageSummaryRepository } from "./reconciliation-repository.js";
|
|
@@ -126,12 +126,13 @@ describe("DrizzleUsageSummaryRepository", () => {
|
|
|
126
126
|
|
|
127
127
|
describe("DrizzleAdapterUsageRepository", () => {
|
|
128
128
|
let repo: DrizzleAdapterUsageRepository;
|
|
129
|
-
let ledger:
|
|
129
|
+
let ledger: DrizzleLedger;
|
|
130
130
|
|
|
131
131
|
beforeEach(async () => {
|
|
132
132
|
await truncateAllTables(pool);
|
|
133
133
|
repo = new DrizzleAdapterUsageRepository(db);
|
|
134
|
-
ledger = new
|
|
134
|
+
ledger = new DrizzleLedger(db);
|
|
135
|
+
await ledger.seedSystemAccounts();
|
|
135
136
|
});
|
|
136
137
|
|
|
137
138
|
it("returns empty array when no adapter_usage debits exist", async () => {
|
|
@@ -147,9 +148,9 @@ describe("DrizzleAdapterUsageRepository", () => {
|
|
|
147
148
|
await ledger.credit("t1", Credit.fromCents(1000), "purchase");
|
|
148
149
|
await ledger.credit("t2", Credit.fromCents(1000), "purchase");
|
|
149
150
|
|
|
150
|
-
await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", "t1-debit-1");
|
|
151
|
-
await ledger.debit("t1", Credit.fromCents(20), "adapter_usage", "t1-debit-2");
|
|
152
|
-
await ledger.debit("t2", Credit.fromCents(50), "adapter_usage", "t2-debit-1");
|
|
151
|
+
await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", { description: "t1-debit-1" });
|
|
152
|
+
await ledger.debit("t1", Credit.fromCents(20), "adapter_usage", { description: "t1-debit-2" });
|
|
153
|
+
await ledger.debit("t2", Credit.fromCents(50), "adapter_usage", { description: "t2-debit-1" });
|
|
153
154
|
|
|
154
155
|
// Query window covering today
|
|
155
156
|
const today = new Date().toISOString().slice(0, 10);
|
|
@@ -168,8 +169,8 @@ describe("DrizzleAdapterUsageRepository", () => {
|
|
|
168
169
|
|
|
169
170
|
it("excludes non-adapter_usage debit types", async () => {
|
|
170
171
|
await ledger.credit("t1", Credit.fromCents(1000), "purchase");
|
|
171
|
-
await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", "adapter debit");
|
|
172
|
-
await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", "runtime debit");
|
|
172
|
+
await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", { description: "adapter debit" });
|
|
173
|
+
await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", { description: "runtime debit" });
|
|
173
174
|
|
|
174
175
|
const today = new Date().toISOString().slice(0, 10);
|
|
175
176
|
const startIso = `${today}T00:00:00Z`;
|
|
@@ -182,7 +183,7 @@ describe("DrizzleAdapterUsageRepository", () => {
|
|
|
182
183
|
|
|
183
184
|
it("excludes credit transactions (positive amounts are not debits)", async () => {
|
|
184
185
|
await ledger.credit("t1", Credit.fromCents(1000), "purchase");
|
|
185
|
-
await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "real debit");
|
|
186
|
+
await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", { description: "real debit" });
|
|
186
187
|
|
|
187
188
|
const today = new Date().toISOString().slice(0, 10);
|
|
188
189
|
const startIso = `${today}T00:00:00Z`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { and, eq, gte, lt, ne, sql } from "drizzle-orm";
|
|
2
2
|
import type { PlatformDb } from "../db/index.js";
|
|
3
|
-
import {
|
|
3
|
+
import { journalEntries, journalLines } from "../db/schema/ledger.js";
|
|
4
4
|
import { usageSummaries } from "../db/schema/meter-events.js";
|
|
5
5
|
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
@@ -59,24 +59,27 @@ export class DrizzleAdapterUsageRepository implements IAdapterUsageRepository {
|
|
|
59
59
|
constructor(private readonly db: PlatformDb) {}
|
|
60
60
|
|
|
61
61
|
async getAggregatedAdapterUsageDebits(startIso: string, endIso: string): Promise<AggregatedDebit[]> {
|
|
62
|
+
// Sum the debit-side journal line amounts for adapter_usage entries.
|
|
63
|
+
// In double-entry: DR tenant liability (2000:<tenantId>), CR revenue:adapter_usage (4010).
|
|
64
|
+
// The debit line on the tenant account represents the charge amount.
|
|
62
65
|
const rows = await this.db
|
|
63
66
|
.select({
|
|
64
|
-
tenantId:
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
// raw SQL: Drizzle cannot express ABS with COALESCE and SUM
|
|
68
|
-
totalDebitRaw: sql<number>`COALESCE(SUM(ABS(amount_credits)), 0)`,
|
|
67
|
+
tenantId: journalEntries.tenantId,
|
|
68
|
+
// raw SQL: Drizzle cannot express COALESCE with SUM aggregation
|
|
69
|
+
totalDebitRaw: sql<number>`COALESCE(SUM(${journalLines.amount}), 0)`,
|
|
69
70
|
})
|
|
70
|
-
.from(
|
|
71
|
+
.from(journalLines)
|
|
72
|
+
.innerJoin(journalEntries, eq(journalEntries.id, journalLines.journalEntryId))
|
|
71
73
|
.where(
|
|
72
74
|
and(
|
|
73
|
-
eq(
|
|
75
|
+
eq(journalEntries.entryType, "adapter_usage"),
|
|
76
|
+
eq(journalLines.side, "debit"),
|
|
74
77
|
// raw SQL: Drizzle cannot express timestamptz cast for text column date comparison
|
|
75
|
-
sql`${
|
|
76
|
-
sql`${
|
|
78
|
+
sql`${journalEntries.postedAt}::timestamptz >= ${startIso}::timestamptz`,
|
|
79
|
+
sql`${journalEntries.postedAt}::timestamptz < ${endIso}::timestamptz`,
|
|
77
80
|
),
|
|
78
81
|
)
|
|
79
|
-
.groupBy(
|
|
82
|
+
.groupBy(journalEntries.tenantId);
|
|
80
83
|
|
|
81
84
|
return rows.map((r) => ({ tenantId: r.tenantId, totalDebitRaw: Number(r.totalDebitRaw) }));
|
|
82
85
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { PGlite } from "@electric-sql/pglite";
|
|
2
|
-
import {
|
|
2
|
+
import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
4
4
|
import type { DrizzleDb } from "../../db/index.js";
|
|
5
5
|
import { affiliateReferrals } from "../../db/schema/affiliate.js";
|
|
6
6
|
import { affiliateFraudEvents } from "../../db/schema/affiliate-fraud.js";
|
|
7
|
-
import {
|
|
7
|
+
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
8
8
|
import { ADMIN_BLOCK_SENTINEL, DrizzleAffiliateFraudAdminRepository } from "./affiliate-admin-repository.js";
|
|
9
9
|
|
|
10
10
|
describe("DrizzleAffiliateFraudAdminRepository", () => {
|
|
@@ -14,16 +14,15 @@ describe("DrizzleAffiliateFraudAdminRepository", () => {
|
|
|
14
14
|
|
|
15
15
|
beforeAll(async () => {
|
|
16
16
|
({ db, pool } = await createTestDb());
|
|
17
|
-
await beginTestTransaction(pool);
|
|
18
17
|
});
|
|
19
18
|
|
|
20
19
|
afterAll(async () => {
|
|
21
|
-
await endTestTransaction(pool);
|
|
22
20
|
await pool.close();
|
|
23
21
|
});
|
|
24
22
|
|
|
25
23
|
beforeEach(async () => {
|
|
26
|
-
await
|
|
24
|
+
await truncateAllTables(pool);
|
|
25
|
+
await new DrizzleLedger(db).seedSystemAccounts();
|
|
27
26
|
repo = new DrizzleAffiliateFraudAdminRepository(db);
|
|
28
27
|
});
|
|
29
28
|
|
|
@@ -162,11 +161,17 @@ describe("DrizzleAffiliateFraudAdminRepository", () => {
|
|
|
162
161
|
|
|
163
162
|
describe("blockFingerprint", () => {
|
|
164
163
|
it("should insert fraud events with ADMIN_BLOCK as referredTenantId", async () => {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
164
|
+
const ledger = new DrizzleLedger(db);
|
|
165
|
+
await ledger.credit("t-alice", Credit.fromCents(1), "purchase", {
|
|
166
|
+
description: "test purchase",
|
|
167
|
+
referenceId: "ref-alice-fp_abc123",
|
|
168
|
+
stripeFingerprint: "fp_abc123",
|
|
169
|
+
});
|
|
170
|
+
await ledger.credit("t-bob", Credit.fromCents(1), "purchase", {
|
|
171
|
+
description: "test purchase",
|
|
172
|
+
referenceId: "ref-bob-fp_abc123",
|
|
173
|
+
stripeFingerprint: "fp_abc123",
|
|
174
|
+
});
|
|
170
175
|
|
|
171
176
|
await repo.blockFingerprint("fp_abc123", "admin-user-1");
|
|
172
177
|
|
|
@@ -185,11 +190,17 @@ describe("DrizzleAffiliateFraudAdminRepository", () => {
|
|
|
185
190
|
});
|
|
186
191
|
|
|
187
192
|
it("should use unique referralId per tenant to avoid unique constraint conflicts", async () => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
+
const ledger = new DrizzleLedger(db);
|
|
194
|
+
await ledger.credit("t-carol", Credit.fromCents(1), "purchase", {
|
|
195
|
+
description: "test purchase",
|
|
196
|
+
referenceId: "ref-carol-fp_def456",
|
|
197
|
+
stripeFingerprint: "fp_def456",
|
|
198
|
+
});
|
|
199
|
+
await ledger.credit("t-dave", Credit.fromCents(1), "purchase", {
|
|
200
|
+
description: "test purchase",
|
|
201
|
+
referenceId: "ref-dave-fp_def456",
|
|
202
|
+
stripeFingerprint: "fp_def456",
|
|
203
|
+
});
|
|
193
204
|
|
|
194
205
|
await repo.blockFingerprint("fp_def456", "admin-user-2");
|
|
195
206
|
|
|
@@ -204,10 +215,12 @@ describe("DrizzleAffiliateFraudAdminRepository", () => {
|
|
|
204
215
|
});
|
|
205
216
|
|
|
206
217
|
it("should be idempotent via onConflictDoNothing", async () => {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
218
|
+
const ledger = new DrizzleLedger(db);
|
|
219
|
+
await ledger.credit("t-eve", Credit.fromCents(1), "purchase", {
|
|
220
|
+
description: "test purchase",
|
|
221
|
+
referenceId: "ref-eve-fp_ghi789",
|
|
222
|
+
stripeFingerprint: "fp_ghi789",
|
|
223
|
+
});
|
|
211
224
|
|
|
212
225
|
await repo.blockFingerprint("fp_ghi789", "admin-user-3");
|
|
213
226
|
await repo.blockFingerprint("fp_ghi789", "admin-user-3");
|
|
@@ -4,7 +4,7 @@ import { logger } from "../../config/logger.js";
|
|
|
4
4
|
import type { DrizzleDb } from "../../db/index.js";
|
|
5
5
|
import { affiliateReferrals } from "../../db/schema/affiliate.js";
|
|
6
6
|
import { affiliateFraudEvents } from "../../db/schema/affiliate-fraud.js";
|
|
7
|
-
import {
|
|
7
|
+
import { journalEntries } from "../../db/schema/ledger.js";
|
|
8
8
|
|
|
9
9
|
function parseSignals(raw: string): string[] {
|
|
10
10
|
try {
|
|
@@ -119,13 +119,16 @@ export class DrizzleAffiliateFraudAdminRepository implements IAffiliateFraudAdmi
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
async listFingerprintClusters(): Promise<FingerprintCluster[]> {
|
|
122
|
+
// Query journal_entries.metadata->>'stripeFingerprint' for purchase entries
|
|
122
123
|
// raw SQL: Drizzle cannot express HAVING COUNT(DISTINCT ...) with array_agg in a single query
|
|
123
124
|
type ClusterRow = { stripe_fingerprint: string; tenant_ids: string[] };
|
|
124
125
|
const rows = (await this.db.execute(sql`
|
|
125
|
-
SELECT
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
SELECT metadata->>'stripeFingerprint' AS stripe_fingerprint,
|
|
127
|
+
array_agg(DISTINCT tenant_id ORDER BY tenant_id) AS tenant_ids
|
|
128
|
+
FROM journal_entries
|
|
129
|
+
WHERE metadata->>'stripeFingerprint' IS NOT NULL
|
|
130
|
+
AND entry_type = 'purchase'
|
|
131
|
+
GROUP BY metadata->>'stripeFingerprint'
|
|
129
132
|
HAVING COUNT(DISTINCT tenant_id) > 1
|
|
130
133
|
ORDER BY COUNT(DISTINCT tenant_id) DESC
|
|
131
134
|
`)) as unknown as { rows: ClusterRow[] };
|
|
@@ -138,9 +141,14 @@ export class DrizzleAffiliateFraudAdminRepository implements IAffiliateFraudAdmi
|
|
|
138
141
|
|
|
139
142
|
async blockFingerprint(fingerprint: string, adminUserId: string): Promise<void> {
|
|
140
143
|
const rows = await this.db
|
|
141
|
-
.selectDistinct({ tenantId:
|
|
142
|
-
.from(
|
|
143
|
-
.where(
|
|
144
|
+
.selectDistinct({ tenantId: journalEntries.tenantId })
|
|
145
|
+
.from(journalEntries)
|
|
146
|
+
.where(
|
|
147
|
+
and(
|
|
148
|
+
eq(journalEntries.entryType, "purchase"),
|
|
149
|
+
sql`${journalEntries.metadata}->>'stripeFingerprint' = ${fingerprint}`,
|
|
150
|
+
),
|
|
151
|
+
);
|
|
144
152
|
const tenantIds = rows.map((r) => r.tenantId);
|
|
145
153
|
|
|
146
154
|
const now = new Date().toISOString();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PGlite } from "@electric-sql/pglite";
|
|
2
|
-
import { Credit,
|
|
2
|
+
import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
4
4
|
import type { DrizzleDb } from "../../db/index.js";
|
|
5
5
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
@@ -10,7 +10,7 @@ import { DrizzleAffiliateRepository } from "./drizzle-affiliate-repository.js";
|
|
|
10
10
|
describe("processAffiliateCreditMatch", () => {
|
|
11
11
|
let pool: PGlite;
|
|
12
12
|
let db: DrizzleDb;
|
|
13
|
-
let ledger:
|
|
13
|
+
let ledger: DrizzleLedger;
|
|
14
14
|
let affiliateRepo: DrizzleAffiliateRepository;
|
|
15
15
|
let fraudRepo: DrizzleAffiliateFraudRepository;
|
|
16
16
|
|
|
@@ -24,13 +24,19 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
24
24
|
|
|
25
25
|
beforeEach(async () => {
|
|
26
26
|
await truncateAllTables(pool);
|
|
27
|
-
ledger = new
|
|
27
|
+
ledger = new DrizzleLedger(db);
|
|
28
|
+
|
|
29
|
+
await ledger.seedSystemAccounts();
|
|
28
30
|
affiliateRepo = new DrizzleAffiliateRepository(db);
|
|
29
31
|
fraudRepo = new DrizzleAffiliateFraudRepository(db);
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
it("does nothing when tenant has no referral", async () => {
|
|
33
|
-
await ledger.credit("buyer", Credit.fromCents(1000), "purchase",
|
|
35
|
+
await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
|
|
36
|
+
description: "first buy",
|
|
37
|
+
referenceId: "session-1",
|
|
38
|
+
fundingSource: "stripe",
|
|
39
|
+
});
|
|
34
40
|
|
|
35
41
|
const result = await processAffiliateCreditMatch({
|
|
36
42
|
tenantId: "buyer",
|
|
@@ -45,8 +51,16 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
45
51
|
it("does nothing when tenant already has prior purchases", async () => {
|
|
46
52
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
47
53
|
|
|
48
|
-
await ledger.credit("buyer", Credit.fromCents(500), "purchase",
|
|
49
|
-
|
|
54
|
+
await ledger.credit("buyer", Credit.fromCents(500), "purchase", {
|
|
55
|
+
description: "old buy",
|
|
56
|
+
referenceId: "session-0",
|
|
57
|
+
fundingSource: "stripe",
|
|
58
|
+
});
|
|
59
|
+
await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
|
|
60
|
+
description: "new buy",
|
|
61
|
+
referenceId: "session-1",
|
|
62
|
+
fundingSource: "stripe",
|
|
63
|
+
});
|
|
50
64
|
|
|
51
65
|
const result = await processAffiliateCreditMatch({
|
|
52
66
|
tenantId: "buyer",
|
|
@@ -60,7 +74,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
60
74
|
|
|
61
75
|
it("credits referrer on first purchase with 100% match", async () => {
|
|
62
76
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
63
|
-
await ledger.credit("buyer", Credit.fromCents(2000), "purchase",
|
|
77
|
+
await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
|
|
78
|
+
description: "first buy",
|
|
79
|
+
referenceId: "session-1",
|
|
80
|
+
fundingSource: "stripe",
|
|
81
|
+
});
|
|
64
82
|
|
|
65
83
|
const result = await processAffiliateCreditMatch({
|
|
66
84
|
tenantId: "buyer",
|
|
@@ -83,7 +101,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
83
101
|
|
|
84
102
|
it("respects custom match rate", async () => {
|
|
85
103
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
86
|
-
await ledger.credit("buyer", Credit.fromCents(2000), "purchase",
|
|
104
|
+
await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
|
|
105
|
+
description: "first buy",
|
|
106
|
+
referenceId: "session-1",
|
|
107
|
+
fundingSource: "stripe",
|
|
108
|
+
});
|
|
87
109
|
|
|
88
110
|
const result = await processAffiliateCreditMatch({
|
|
89
111
|
tenantId: "buyer",
|
|
@@ -99,7 +121,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
99
121
|
|
|
100
122
|
it("is idempotent — second call returns null", async () => {
|
|
101
123
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
102
|
-
await ledger.credit("buyer", Credit.fromCents(1000), "purchase",
|
|
124
|
+
await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
|
|
125
|
+
description: "first buy",
|
|
126
|
+
referenceId: "session-1",
|
|
127
|
+
fundingSource: "stripe",
|
|
128
|
+
});
|
|
103
129
|
|
|
104
130
|
const first = await processAffiliateCreditMatch({
|
|
105
131
|
tenantId: "buyer",
|
|
@@ -123,7 +149,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
123
149
|
signupIp: "1.2.3.4",
|
|
124
150
|
signupEmail: "alice+ref@gmail.com",
|
|
125
151
|
});
|
|
126
|
-
await ledger.credit("buyer", Credit.fromCents(2000), "purchase",
|
|
152
|
+
await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
|
|
153
|
+
description: "first buy",
|
|
154
|
+
referenceId: "session-1",
|
|
155
|
+
fundingSource: "stripe",
|
|
156
|
+
});
|
|
127
157
|
|
|
128
158
|
const result = await processAffiliateCreditMatch({
|
|
129
159
|
tenantId: "buyer",
|
|
@@ -156,7 +186,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
156
186
|
|
|
157
187
|
// New referral
|
|
158
188
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
159
|
-
await ledger.credit("buyer", Credit.fromCents(1000), "purchase",
|
|
189
|
+
await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
|
|
190
|
+
description: "first buy",
|
|
191
|
+
referenceId: "session-1",
|
|
192
|
+
fundingSource: "stripe",
|
|
193
|
+
});
|
|
160
194
|
|
|
161
195
|
const result = await processAffiliateCreditMatch({
|
|
162
196
|
tenantId: "buyer",
|
|
@@ -184,7 +218,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
184
218
|
|
|
185
219
|
// New referral
|
|
186
220
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
187
|
-
await ledger.credit("buyer", Credit.fromCents(1000), "purchase",
|
|
221
|
+
await ledger.credit("buyer", Credit.fromCents(1000), "purchase", {
|
|
222
|
+
description: "first buy",
|
|
223
|
+
referenceId: "session-1",
|
|
224
|
+
fundingSource: "stripe",
|
|
225
|
+
});
|
|
188
226
|
|
|
189
227
|
const result = await processAffiliateCreditMatch({
|
|
190
228
|
tenantId: "buyer",
|
|
@@ -203,7 +241,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
203
241
|
|
|
204
242
|
it("allows payout when under both caps", async () => {
|
|
205
243
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123");
|
|
206
|
-
await ledger.credit("buyer", Credit.fromCents(2000), "purchase",
|
|
244
|
+
await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
|
|
245
|
+
description: "first buy",
|
|
246
|
+
referenceId: "session-1",
|
|
247
|
+
fundingSource: "stripe",
|
|
248
|
+
});
|
|
207
249
|
|
|
208
250
|
const result = await processAffiliateCreditMatch({
|
|
209
251
|
tenantId: "buyer",
|
|
@@ -223,7 +265,11 @@ describe("processAffiliateCreditMatch", () => {
|
|
|
223
265
|
await affiliateRepo.recordReferral("referrer", "buyer", "abc123", {
|
|
224
266
|
signupIp: "1.2.3.4",
|
|
225
267
|
});
|
|
226
|
-
await ledger.credit("buyer", Credit.fromCents(2000), "purchase",
|
|
268
|
+
await ledger.credit("buyer", Credit.fromCents(2000), "purchase", {
|
|
269
|
+
description: "first buy",
|
|
270
|
+
referenceId: "session-1",
|
|
271
|
+
fundingSource: "stripe",
|
|
272
|
+
});
|
|
227
273
|
|
|
228
274
|
const result = await processAffiliateCreditMatch({
|
|
229
275
|
tenantId: "buyer",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Credit,
|
|
1
|
+
import type { Credit, ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { config } from "../../config/index.js";
|
|
3
3
|
import type { IAffiliateFraudRepository } from "./affiliate-fraud-repository.js";
|
|
4
4
|
import type { IAffiliateRepository } from "./drizzle-affiliate-repository.js";
|
|
@@ -11,7 +11,7 @@ const DEFAULT_MAX_MATCH_CREDITS_30D = config.billing.affiliateMaxMatchCredits30d
|
|
|
11
11
|
export interface AffiliateCreditMatchDeps {
|
|
12
12
|
tenantId: string;
|
|
13
13
|
purchaseAmount: Credit;
|
|
14
|
-
ledger:
|
|
14
|
+
ledger: ILedger;
|
|
15
15
|
affiliateRepo: IAffiliateRepository;
|
|
16
16
|
matchRate?: number;
|
|
17
17
|
fraudRepo?: IAffiliateFraudRepository;
|
|
@@ -122,13 +122,10 @@ export async function processAffiliateCreditMatch(
|
|
|
122
122
|
if (matchAmount.isZero() || matchAmount.isNegative()) return null;
|
|
123
123
|
|
|
124
124
|
// 6. Credit the referrer
|
|
125
|
-
await ledger.credit(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
`Affiliate match for referred tenant ${tenantId}`,
|
|
130
|
-
refId,
|
|
131
|
-
);
|
|
125
|
+
await ledger.credit(referral.referrerTenantId, matchAmount, "affiliate_match", {
|
|
126
|
+
description: `Affiliate match for referred tenant ${tenantId}`,
|
|
127
|
+
referenceId: refId,
|
|
128
|
+
});
|
|
132
129
|
|
|
133
130
|
// 7. Update referral record
|
|
134
131
|
await affiliateRepo.markFirstPurchase(tenantId);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PGlite } from "@electric-sql/pglite";
|
|
2
|
-
import { Credit,
|
|
2
|
+
import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
4
4
|
import type { DrizzleDb } from "../../db/index.js";
|
|
5
5
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
@@ -9,7 +9,7 @@ import { DEFAULT_BONUS_RATE, grantNewUserBonus } from "./new-user-bonus.js";
|
|
|
9
9
|
describe("grantNewUserBonus", () => {
|
|
10
10
|
let pool: PGlite;
|
|
11
11
|
let db: DrizzleDb;
|
|
12
|
-
let ledger:
|
|
12
|
+
let ledger: DrizzleLedger;
|
|
13
13
|
let affiliateRepo: DrizzleAffiliateRepository;
|
|
14
14
|
|
|
15
15
|
beforeAll(async () => {
|
|
@@ -22,7 +22,9 @@ describe("grantNewUserBonus", () => {
|
|
|
22
22
|
|
|
23
23
|
beforeEach(async () => {
|
|
24
24
|
await truncateAllTables(pool);
|
|
25
|
-
ledger = new
|
|
25
|
+
ledger = new DrizzleLedger(db);
|
|
26
|
+
|
|
27
|
+
await ledger.seedSystemAccounts();
|
|
26
28
|
affiliateRepo = new DrizzleAffiliateRepository(db);
|
|
27
29
|
});
|
|
28
30
|
|
|
@@ -48,7 +50,7 @@ describe("grantNewUserBonus", () => {
|
|
|
48
50
|
|
|
49
51
|
const txns = await ledger.history("referred-1");
|
|
50
52
|
expect(txns).toHaveLength(1);
|
|
51
|
-
expect(txns[0].
|
|
53
|
+
expect(txns[0].entryType).toBe("affiliate_bonus");
|
|
52
54
|
expect(txns[0].referenceId).toBe("affiliate-bonus:referred-1");
|
|
53
55
|
expect(txns[0].description).toContain("first-purchase bonus");
|
|
54
56
|
});
|
|
@@ -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 { config } from "../../config/index.js";
|
|
4
4
|
import type { IAffiliateRepository } from "./drizzle-affiliate-repository.js";
|
|
@@ -7,7 +7,7 @@ import type { IAffiliateRepository } from "./drizzle-affiliate-repository.js";
|
|
|
7
7
|
export const DEFAULT_BONUS_RATE = config.billing.affiliateNewUserBonusRate;
|
|
8
8
|
|
|
9
9
|
export interface NewUserBonusParams {
|
|
10
|
-
ledger:
|
|
10
|
+
ledger: ILedger;
|
|
11
11
|
affiliateRepo: IAffiliateRepository;
|
|
12
12
|
referredTenantId: string;
|
|
13
13
|
purchaseAmount: Credit;
|
|
@@ -57,13 +57,10 @@ export async function grantNewUserBonus(params: NewUserBonusParams): Promise<New
|
|
|
57
57
|
await affiliateRepo.markFirstPurchase(referredTenantId);
|
|
58
58
|
|
|
59
59
|
// 6. Credit the bonus
|
|
60
|
-
await ledger.credit(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`New user first-purchase bonus (${Math.round(rate * 100)}%)`,
|
|
65
|
-
refId,
|
|
66
|
-
);
|
|
60
|
+
await ledger.credit(referredTenantId, bonus, "affiliate_bonus", {
|
|
61
|
+
description: `New user first-purchase bonus (${Math.round(rate * 100)}%)`,
|
|
62
|
+
referenceId: refId,
|
|
63
|
+
});
|
|
67
64
|
|
|
68
65
|
return { granted: true, bonus };
|
|
69
66
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import type { PGlite } from "@electric-sql/pglite";
|
|
3
3
|
import type { ITenantCustomerRepository } from "@wopr-network/platform-core/billing";
|
|
4
|
-
import { Credit,
|
|
4
|
+
import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
5
5
|
import Stripe from "stripe";
|
|
6
6
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
7
|
import type { DrizzleDb } from "../../db/index.js";
|
|
@@ -48,7 +48,7 @@ function mockTenantStore(stripeCustomerId = "cus_123") {
|
|
|
48
48
|
describe("chargeAutoTopup", () => {
|
|
49
49
|
let pool: PGlite;
|
|
50
50
|
let db: DrizzleDb;
|
|
51
|
-
let ledger:
|
|
51
|
+
let ledger: DrizzleLedger;
|
|
52
52
|
|
|
53
53
|
beforeAll(async () => {
|
|
54
54
|
({ db, pool } = await createTestDb());
|
|
@@ -60,7 +60,9 @@ describe("chargeAutoTopup", () => {
|
|
|
60
60
|
|
|
61
61
|
beforeEach(async () => {
|
|
62
62
|
await truncateAllTables(pool);
|
|
63
|
-
ledger = new
|
|
63
|
+
ledger = new DrizzleLedger(db);
|
|
64
|
+
|
|
65
|
+
await ledger.seedSystemAccounts();
|
|
64
66
|
});
|
|
65
67
|
|
|
66
68
|
it("charges Stripe and credits ledger on success", async () => {
|
|
@@ -79,8 +81,8 @@ describe("chargeAutoTopup", () => {
|
|
|
79
81
|
expect(result.paymentReference).toEqual(expect.any(String));
|
|
80
82
|
expect((await ledger.balance("t1")).toCents()).toBe(500);
|
|
81
83
|
const history = await ledger.history("t1");
|
|
82
|
-
expect(history[0].
|
|
83
|
-
expect(history[0].fundingSource).toBe("stripe");
|
|
84
|
+
expect(history[0].entryType).toBe("purchase");
|
|
85
|
+
expect(history[0].metadata?.fundingSource).toBe("stripe");
|
|
84
86
|
});
|
|
85
87
|
|
|
86
88
|
it("writes success event to credit_auto_topup log", async () => {
|