@wopr-network/platform-core 1.13.3 → 1.14.1
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/.github/workflows/dependabot-auto-merge.yml +1 -2
- 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/protocol/handlers.test.js +461 -0
- 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/protocol/handlers.test.ts +549 -1
- 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,203 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for CreditLedger — including the allowNegative debit parameter (WOP-821).
|
|
3
|
-
*/
|
|
4
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
6
|
-
import { Credit } from "./credit.js";
|
|
7
|
-
import { CreditLedger, InsufficientBalanceError } from "./credit-ledger.js";
|
|
8
|
-
// TOP OF FILE - shared across ALL describes
|
|
9
|
-
let pool;
|
|
10
|
-
let db;
|
|
11
|
-
beforeAll(async () => {
|
|
12
|
-
({ db, pool } = await createTestDb());
|
|
13
|
-
});
|
|
14
|
-
afterAll(async () => {
|
|
15
|
-
await pool.close();
|
|
16
|
-
});
|
|
17
|
-
describe("CreditLedger core methods", () => {
|
|
18
|
-
let ledger;
|
|
19
|
-
beforeEach(async () => {
|
|
20
|
-
await truncateAllTables(pool);
|
|
21
|
-
ledger = new CreditLedger(db);
|
|
22
|
-
});
|
|
23
|
-
// --- credit() ---
|
|
24
|
-
describe("credit()", () => {
|
|
25
|
-
it("happy path: credits a tenant and returns correct transaction fields", async () => {
|
|
26
|
-
const txn = await ledger.credit("t1", Credit.fromCents(100), "purchase", "Initial deposit", "ref-001", "stripe", "user-abc");
|
|
27
|
-
expect(txn.tenantId).toBe("t1");
|
|
28
|
-
expect(txn.amount.toCents()).toBe(100);
|
|
29
|
-
expect(txn.balanceAfter.toCents()).toBe(100);
|
|
30
|
-
expect(txn.type).toBe("purchase");
|
|
31
|
-
expect(txn.description).toBe("Initial deposit");
|
|
32
|
-
expect(txn.referenceId).toBe("ref-001");
|
|
33
|
-
expect(txn.fundingSource).toBe("stripe");
|
|
34
|
-
expect(txn.attributedUserId).toBe("user-abc");
|
|
35
|
-
expect(txn.id).toEqual(expect.any(String));
|
|
36
|
-
expect(txn.createdAt).toEqual(expect.any(String));
|
|
37
|
-
});
|
|
38
|
-
it("multiple credits accumulate balance correctly", async () => {
|
|
39
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase");
|
|
40
|
-
await ledger.credit("t1", Credit.fromCents(50), "promo");
|
|
41
|
-
const bal = await ledger.balance("t1");
|
|
42
|
-
expect(bal.toCents()).toBe(150);
|
|
43
|
-
});
|
|
44
|
-
it("rejects zero amount", async () => {
|
|
45
|
-
await expect(ledger.credit("t1", Credit.fromCents(0), "purchase")).rejects.toThrow("amount must be positive for credits");
|
|
46
|
-
});
|
|
47
|
-
it("rejects negative amount", async () => {
|
|
48
|
-
await expect(ledger.credit("t1", Credit.fromRaw(-1), "purchase")).rejects.toThrow("amount must be positive for credits");
|
|
49
|
-
});
|
|
50
|
-
it("optional fields default to null", async () => {
|
|
51
|
-
const txn = await ledger.credit("t1", Credit.fromCents(10), "signup_grant");
|
|
52
|
-
expect(txn.description).toBeNull();
|
|
53
|
-
expect(txn.referenceId).toBeNull();
|
|
54
|
-
expect(txn.fundingSource).toBeNull();
|
|
55
|
-
expect(txn.attributedUserId).toBeNull();
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
// --- balance() ---
|
|
59
|
-
describe("balance()", () => {
|
|
60
|
-
it("returns Credit.ZERO for a tenant with no transactions", async () => {
|
|
61
|
-
const bal = await ledger.balance("nonexistent");
|
|
62
|
-
expect(bal.toCents()).toBe(0);
|
|
63
|
-
expect(bal.isZero()).toBe(true);
|
|
64
|
-
});
|
|
65
|
-
it("reflects credits and debits accurately", async () => {
|
|
66
|
-
await ledger.credit("t1", Credit.fromCents(200), "purchase");
|
|
67
|
-
await ledger.debit("t1", Credit.fromCents(50), "bot_runtime");
|
|
68
|
-
const bal = await ledger.balance("t1");
|
|
69
|
-
expect(bal.toCents()).toBe(150);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
// --- history() ---
|
|
73
|
-
describe("history()", () => {
|
|
74
|
-
it("returns transactions in reverse chronological order (newest first)", async () => {
|
|
75
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase", "first");
|
|
76
|
-
await ledger.credit("t1", Credit.fromCents(200), "promo", "second");
|
|
77
|
-
await ledger.debit("t1", Credit.fromCents(50), "bot_runtime", "third");
|
|
78
|
-
const hist = await ledger.history("t1");
|
|
79
|
-
expect(hist).toHaveLength(3);
|
|
80
|
-
// newest first
|
|
81
|
-
expect(hist[0].description).toBe("third");
|
|
82
|
-
expect(hist[1].description).toBe("second");
|
|
83
|
-
expect(hist[2].description).toBe("first");
|
|
84
|
-
});
|
|
85
|
-
it("all CreditTransaction fields are populated", async () => {
|
|
86
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase", "desc", "ref-1", "stripe", "user-1");
|
|
87
|
-
const hist = await ledger.history("t1");
|
|
88
|
-
expect(hist).toHaveLength(1);
|
|
89
|
-
const txn = hist[0];
|
|
90
|
-
expect(txn.id).toEqual(expect.any(String));
|
|
91
|
-
expect(txn.tenantId).toBe("t1");
|
|
92
|
-
expect(txn.amount.toCents()).toBe(100);
|
|
93
|
-
expect(txn.balanceAfter.toCents()).toBe(100);
|
|
94
|
-
expect(txn.type).toBe("purchase");
|
|
95
|
-
expect(txn.description).toBe("desc");
|
|
96
|
-
expect(txn.referenceId).toBe("ref-1");
|
|
97
|
-
expect(txn.fundingSource).toBe("stripe");
|
|
98
|
-
expect(txn.attributedUserId).toBe("user-1");
|
|
99
|
-
expect(txn.createdAt).toEqual(expect.any(String));
|
|
100
|
-
});
|
|
101
|
-
it("respects limit and offset for pagination", async () => {
|
|
102
|
-
// Insert 5 transactions
|
|
103
|
-
for (let i = 1; i <= 5; i++) {
|
|
104
|
-
await ledger.credit("t1", Credit.fromCents(10 * i), "purchase", `txn-${i}`);
|
|
105
|
-
}
|
|
106
|
-
const page1 = await ledger.history("t1", { limit: 2, offset: 0 });
|
|
107
|
-
expect(page1).toHaveLength(2);
|
|
108
|
-
expect(page1[0].description).toBe("txn-5"); // newest first
|
|
109
|
-
expect(page1[1].description).toBe("txn-4");
|
|
110
|
-
const page2 = await ledger.history("t1", { limit: 2, offset: 2 });
|
|
111
|
-
expect(page2).toHaveLength(2);
|
|
112
|
-
expect(page2[0].description).toBe("txn-3");
|
|
113
|
-
expect(page2[1].description).toBe("txn-2");
|
|
114
|
-
});
|
|
115
|
-
it("filters by type when provided", async () => {
|
|
116
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase", "buy");
|
|
117
|
-
await ledger.credit("t1", Credit.fromCents(50), "promo", "free");
|
|
118
|
-
await ledger.debit("t1", Credit.fromCents(10), "bot_runtime", "usage");
|
|
119
|
-
const purchases = await ledger.history("t1", { type: "purchase" });
|
|
120
|
-
expect(purchases).toHaveLength(1);
|
|
121
|
-
expect(purchases[0].description).toBe("buy");
|
|
122
|
-
});
|
|
123
|
-
it("returns empty array for tenant with no transactions", async () => {
|
|
124
|
-
const hist = await ledger.history("nonexistent");
|
|
125
|
-
expect(hist).toEqual([]);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
// --- hasReferenceId() ---
|
|
129
|
-
describe("hasReferenceId()", () => {
|
|
130
|
-
it("returns false for a reference ID that does not exist", async () => {
|
|
131
|
-
expect(await ledger.hasReferenceId("nonexistent-ref")).toBe(false);
|
|
132
|
-
});
|
|
133
|
-
it("returns true for a reference ID used in a credit", async () => {
|
|
134
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase", "desc", "ref-unique");
|
|
135
|
-
expect(await ledger.hasReferenceId("ref-unique")).toBe(true);
|
|
136
|
-
});
|
|
137
|
-
it("returns true for a reference ID used in a debit", async () => {
|
|
138
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase");
|
|
139
|
-
await ledger.debit("t1", Credit.fromCents(10), "bot_runtime", "desc", "debit-ref");
|
|
140
|
-
expect(await ledger.hasReferenceId("debit-ref")).toBe(true);
|
|
141
|
-
});
|
|
142
|
-
it("detects reference IDs across different tenants", async () => {
|
|
143
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase", "desc", "cross-tenant-ref");
|
|
144
|
-
// hasReferenceId is global, not tenant-scoped
|
|
145
|
-
expect(await ledger.hasReferenceId("cross-tenant-ref")).toBe(true);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
// --- tenantsWithBalance() ---
|
|
149
|
-
describe("tenantsWithBalance()", () => {
|
|
150
|
-
it("returns empty array when no tenants exist", async () => {
|
|
151
|
-
const result = await ledger.tenantsWithBalance();
|
|
152
|
-
expect(result).toEqual([]);
|
|
153
|
-
});
|
|
154
|
-
it("returns only tenants with positive balance", async () => {
|
|
155
|
-
// t1: positive balance (100 cents)
|
|
156
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase");
|
|
157
|
-
// t2: zero balance (credit then debit same amount)
|
|
158
|
-
await ledger.credit("t2", Credit.fromCents(50), "purchase");
|
|
159
|
-
await ledger.debit("t2", Credit.fromCents(50), "bot_runtime");
|
|
160
|
-
// t3: negative balance (via allowNegative)
|
|
161
|
-
await ledger.credit("t3", Credit.fromCents(10), "purchase");
|
|
162
|
-
await ledger.debit("t3", Credit.fromCents(20), "bot_runtime", undefined, undefined, true);
|
|
163
|
-
// t4: positive balance (200 cents)
|
|
164
|
-
await ledger.credit("t4", Credit.fromCents(200), "signup_grant");
|
|
165
|
-
const result = await ledger.tenantsWithBalance();
|
|
166
|
-
const tenantIds = result.map((r) => r.tenantId).sort();
|
|
167
|
-
expect(tenantIds).toEqual(["t1", "t4"]);
|
|
168
|
-
const t1 = result.find((r) => r.tenantId === "t1");
|
|
169
|
-
expect(t1?.balance.toCents()).toBe(100);
|
|
170
|
-
const t4 = result.find((r) => r.tenantId === "t4");
|
|
171
|
-
expect(t4?.balance.toCents()).toBe(200);
|
|
172
|
-
});
|
|
173
|
-
it("excludes tenants with exactly zero balance", async () => {
|
|
174
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase");
|
|
175
|
-
await ledger.debit("t1", Credit.fromCents(100), "bot_runtime");
|
|
176
|
-
const result = await ledger.tenantsWithBalance();
|
|
177
|
-
expect(result).toEqual([]);
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
describe("CreditLedger.debit with allowNegative", () => {
|
|
182
|
-
let ledger;
|
|
183
|
-
beforeEach(async () => {
|
|
184
|
-
await truncateAllTables(pool);
|
|
185
|
-
ledger = new CreditLedger(db);
|
|
186
|
-
});
|
|
187
|
-
it("debit with allowNegative=false (default) throws InsufficientBalanceError when balance insufficient", async () => {
|
|
188
|
-
await ledger.credit("t1", Credit.fromCents(5), "purchase", "setup");
|
|
189
|
-
await expect(ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "test")).rejects.toThrow(InsufficientBalanceError);
|
|
190
|
-
});
|
|
191
|
-
it("debit with allowNegative=true allows negative balance", async () => {
|
|
192
|
-
await ledger.credit("t1", Credit.fromCents(5), "purchase", "setup");
|
|
193
|
-
const txn = await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "test", undefined, true);
|
|
194
|
-
expect(txn).not.toBeNull();
|
|
195
|
-
expect((await ledger.balance("t1")).toCents()).toBe(-5);
|
|
196
|
-
});
|
|
197
|
-
it("debit with allowNegative=true records correct transaction with negative amount and negative balanceAfter", async () => {
|
|
198
|
-
await ledger.credit("t1", Credit.fromCents(5), "purchase", "setup");
|
|
199
|
-
const txn = await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "test", undefined, true);
|
|
200
|
-
expect(txn.amount.toCents()).toBe(-10);
|
|
201
|
-
expect(txn.balanceAfter.toCents()).toBe(-5);
|
|
202
|
-
});
|
|
203
|
-
});
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
-
import { creditTransactions } from "../db/schema/credits.js";
|
|
4
|
-
import { beginTestTransaction, createTestDb, endTestTransaction, rollbackTestTransaction } from "../test/db.js";
|
|
5
|
-
import { Credit } from "./credit.js";
|
|
6
|
-
import { DrizzleCreditTransactionRepository } from "./credit-transaction-repository.js";
|
|
7
|
-
let pool;
|
|
8
|
-
let db;
|
|
9
|
-
beforeAll(async () => {
|
|
10
|
-
({ db, pool } = await createTestDb());
|
|
11
|
-
await beginTestTransaction(pool);
|
|
12
|
-
});
|
|
13
|
-
afterAll(async () => {
|
|
14
|
-
await endTestTransaction(pool);
|
|
15
|
-
await pool.close();
|
|
16
|
-
});
|
|
17
|
-
/** Seed a credit_transactions row directly. */
|
|
18
|
-
async function seedTx(opts) {
|
|
19
|
-
await db.insert(creditTransactions).values({
|
|
20
|
-
id: crypto.randomUUID(),
|
|
21
|
-
tenantId: opts.tenantId,
|
|
22
|
-
amount: opts.amount,
|
|
23
|
-
balanceAfter: opts.balanceAfter ?? opts.amount,
|
|
24
|
-
type: opts.type,
|
|
25
|
-
description: "test",
|
|
26
|
-
referenceId: opts.referenceId ?? null,
|
|
27
|
-
createdAt: opts.createdAt ?? new Date().toISOString(),
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
describe("DrizzleCreditTransactionRepository", () => {
|
|
31
|
-
let repo;
|
|
32
|
-
beforeEach(async () => {
|
|
33
|
-
await rollbackTestTransaction(pool);
|
|
34
|
-
repo = new DrizzleCreditTransactionRepository(db);
|
|
35
|
-
});
|
|
36
|
-
describe("existsByReferenceIdLike()", () => {
|
|
37
|
-
it("returns false when no transactions exist", async () => {
|
|
38
|
-
const result = await repo.existsByReferenceIdLike("div-%");
|
|
39
|
-
expect(result).toBe(false);
|
|
40
|
-
});
|
|
41
|
-
it("returns true when a matching referenceId exists", async () => {
|
|
42
|
-
await seedTx({
|
|
43
|
-
tenantId: "t1",
|
|
44
|
-
amount: Credit.fromCents(100),
|
|
45
|
-
type: "purchase",
|
|
46
|
-
referenceId: "div-2026-01-01",
|
|
47
|
-
});
|
|
48
|
-
const result = await repo.existsByReferenceIdLike("div-%");
|
|
49
|
-
expect(result).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
it("returns false when no referenceId matches the pattern", async () => {
|
|
52
|
-
await seedTx({
|
|
53
|
-
tenantId: "t1",
|
|
54
|
-
amount: Credit.fromCents(100),
|
|
55
|
-
type: "purchase",
|
|
56
|
-
referenceId: "purchase-abc",
|
|
57
|
-
});
|
|
58
|
-
const result = await repo.existsByReferenceIdLike("div-%");
|
|
59
|
-
expect(result).toBe(false);
|
|
60
|
-
});
|
|
61
|
-
it("matches partial patterns with wildcards", async () => {
|
|
62
|
-
await seedTx({
|
|
63
|
-
tenantId: "t1",
|
|
64
|
-
amount: Credit.fromCents(50),
|
|
65
|
-
type: "community_dividend",
|
|
66
|
-
referenceId: "div-2026-02-15-t1",
|
|
67
|
-
});
|
|
68
|
-
expect(await repo.existsByReferenceIdLike("div-2026-02-%")).toBe(true);
|
|
69
|
-
expect(await repo.existsByReferenceIdLike("div-2026-03-%")).toBe(false);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
describe("sumPurchasesForPeriod()", () => {
|
|
73
|
-
it("returns Credit.ZERO when no transactions exist", async () => {
|
|
74
|
-
const sum = await repo.sumPurchasesForPeriod("2026-01-01T00:00:00Z", "2026-02-01T00:00:00Z");
|
|
75
|
-
expect(sum.toRaw()).toBe(0);
|
|
76
|
-
});
|
|
77
|
-
it("sums only purchase-type transactions", async () => {
|
|
78
|
-
await seedTx({
|
|
79
|
-
tenantId: "t1",
|
|
80
|
-
amount: Credit.fromCents(100),
|
|
81
|
-
type: "purchase",
|
|
82
|
-
createdAt: "2026-01-15T12:00:00Z",
|
|
83
|
-
});
|
|
84
|
-
await seedTx({
|
|
85
|
-
tenantId: "t1",
|
|
86
|
-
amount: Credit.fromCents(200),
|
|
87
|
-
type: "signup_grant",
|
|
88
|
-
createdAt: "2026-01-15T12:00:00Z",
|
|
89
|
-
});
|
|
90
|
-
const sum = await repo.sumPurchasesForPeriod("2026-01-01T00:00:00Z", "2026-02-01T00:00:00Z");
|
|
91
|
-
expect(sum.toRaw()).toBe(Credit.fromCents(100).toRaw());
|
|
92
|
-
});
|
|
93
|
-
it("respects half-open interval [start, end)", async () => {
|
|
94
|
-
// Exactly at start — included
|
|
95
|
-
await seedTx({
|
|
96
|
-
tenantId: "t1",
|
|
97
|
-
amount: Credit.fromCents(10),
|
|
98
|
-
type: "purchase",
|
|
99
|
-
createdAt: "2026-01-01T00:00:00Z",
|
|
100
|
-
});
|
|
101
|
-
// Inside window
|
|
102
|
-
await seedTx({
|
|
103
|
-
tenantId: "t1",
|
|
104
|
-
amount: Credit.fromCents(20),
|
|
105
|
-
type: "purchase",
|
|
106
|
-
createdAt: "2026-01-15T00:00:00Z",
|
|
107
|
-
});
|
|
108
|
-
// Exactly at end — excluded
|
|
109
|
-
await seedTx({
|
|
110
|
-
tenantId: "t1",
|
|
111
|
-
amount: Credit.fromCents(40),
|
|
112
|
-
type: "purchase",
|
|
113
|
-
createdAt: "2026-02-01T00:00:00Z",
|
|
114
|
-
});
|
|
115
|
-
const sum = await repo.sumPurchasesForPeriod("2026-01-01T00:00:00Z", "2026-02-01T00:00:00Z");
|
|
116
|
-
expect(sum.toRaw()).toBe(Credit.fromCents(30).toRaw()); // 10 + 20, not 40
|
|
117
|
-
});
|
|
118
|
-
it("sums across all tenants (not tenant-scoped)", async () => {
|
|
119
|
-
await seedTx({
|
|
120
|
-
tenantId: "t1",
|
|
121
|
-
amount: Credit.fromCents(50),
|
|
122
|
-
type: "purchase",
|
|
123
|
-
createdAt: "2026-01-10T00:00:00Z",
|
|
124
|
-
});
|
|
125
|
-
await seedTx({
|
|
126
|
-
tenantId: "t2",
|
|
127
|
-
amount: Credit.fromCents(75),
|
|
128
|
-
type: "purchase",
|
|
129
|
-
createdAt: "2026-01-10T00:00:00Z",
|
|
130
|
-
});
|
|
131
|
-
const sum = await repo.sumPurchasesForPeriod("2026-01-01T00:00:00Z", "2026-02-01T00:00:00Z");
|
|
132
|
-
expect(sum.toRaw()).toBe(Credit.fromCents(125).toRaw()); // 50 + 75
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
describe("getActiveTenantIdsInWindow()", () => {
|
|
136
|
-
it("returns empty array when no transactions exist", async () => {
|
|
137
|
-
const ids = await repo.getActiveTenantIdsInWindow("2026-01-01T00:00:00Z", "2026-02-01T00:00:00Z");
|
|
138
|
-
expect(ids).toEqual([]);
|
|
139
|
-
});
|
|
140
|
-
it("returns distinct tenantIds with purchase transactions in window", async () => {
|
|
141
|
-
await seedTx({
|
|
142
|
-
tenantId: "t1",
|
|
143
|
-
amount: Credit.fromCents(10),
|
|
144
|
-
type: "purchase",
|
|
145
|
-
createdAt: "2026-01-10T00:00:00Z",
|
|
146
|
-
});
|
|
147
|
-
// t1 again — should not duplicate
|
|
148
|
-
await seedTx({
|
|
149
|
-
tenantId: "t1",
|
|
150
|
-
amount: Credit.fromCents(20),
|
|
151
|
-
type: "purchase",
|
|
152
|
-
createdAt: "2026-01-11T00:00:00Z",
|
|
153
|
-
});
|
|
154
|
-
await seedTx({
|
|
155
|
-
tenantId: "t2",
|
|
156
|
-
amount: Credit.fromCents(30),
|
|
157
|
-
type: "purchase",
|
|
158
|
-
createdAt: "2026-01-12T00:00:00Z",
|
|
159
|
-
});
|
|
160
|
-
const ids = await repo.getActiveTenantIdsInWindow("2026-01-01T00:00:00Z", "2026-02-01T00:00:00Z");
|
|
161
|
-
expect(ids.sort()).toEqual(["t1", "t2"]);
|
|
162
|
-
});
|
|
163
|
-
it("excludes non-purchase transaction types", async () => {
|
|
164
|
-
await seedTx({
|
|
165
|
-
tenantId: "t1",
|
|
166
|
-
amount: Credit.fromCents(100),
|
|
167
|
-
type: "signup_grant",
|
|
168
|
-
createdAt: "2026-01-10T00:00:00Z",
|
|
169
|
-
});
|
|
170
|
-
const ids = await repo.getActiveTenantIdsInWindow("2026-01-01T00:00:00Z", "2026-02-01T00:00:00Z");
|
|
171
|
-
expect(ids).toEqual([]);
|
|
172
|
-
});
|
|
173
|
-
it("respects half-open interval [start, end)", async () => {
|
|
174
|
-
// Before window
|
|
175
|
-
await seedTx({
|
|
176
|
-
tenantId: "t-before",
|
|
177
|
-
amount: Credit.fromCents(10),
|
|
178
|
-
type: "purchase",
|
|
179
|
-
createdAt: "2025-12-31T23:59:59Z",
|
|
180
|
-
});
|
|
181
|
-
// At start — included
|
|
182
|
-
await seedTx({
|
|
183
|
-
tenantId: "t-start",
|
|
184
|
-
amount: Credit.fromCents(10),
|
|
185
|
-
type: "purchase",
|
|
186
|
-
createdAt: "2026-01-01T00:00:00Z",
|
|
187
|
-
});
|
|
188
|
-
// At end — excluded
|
|
189
|
-
await seedTx({
|
|
190
|
-
tenantId: "t-end",
|
|
191
|
-
amount: Credit.fromCents(10),
|
|
192
|
-
type: "purchase",
|
|
193
|
-
createdAt: "2026-02-01T00:00:00Z",
|
|
194
|
-
});
|
|
195
|
-
const ids = await repo.getActiveTenantIdsInWindow("2026-01-01T00:00:00Z", "2026-02-01T00:00:00Z");
|
|
196
|
-
expect(ids).toEqual(["t-start"]);
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
describe("referenceId uniqueness", () => {
|
|
200
|
-
it("rejects duplicate referenceId (database unique constraint)", async () => {
|
|
201
|
-
await seedTx({
|
|
202
|
-
tenantId: "t1",
|
|
203
|
-
amount: Credit.fromCents(100),
|
|
204
|
-
type: "purchase",
|
|
205
|
-
referenceId: "unique-ref-1",
|
|
206
|
-
});
|
|
207
|
-
await expect(seedTx({
|
|
208
|
-
tenantId: "t1",
|
|
209
|
-
amount: Credit.fromCents(200),
|
|
210
|
-
type: "purchase",
|
|
211
|
-
referenceId: "unique-ref-1",
|
|
212
|
-
})).rejects.toThrow(); // PG unique constraint violation
|
|
213
|
-
});
|
|
214
|
-
it("allows null referenceId on multiple rows", async () => {
|
|
215
|
-
await seedTx({
|
|
216
|
-
tenantId: "t1",
|
|
217
|
-
amount: Credit.fromCents(100),
|
|
218
|
-
type: "purchase",
|
|
219
|
-
referenceId: undefined, // null
|
|
220
|
-
});
|
|
221
|
-
await seedTx({
|
|
222
|
-
tenantId: "t1",
|
|
223
|
-
amount: Credit.fromCents(200),
|
|
224
|
-
type: "purchase",
|
|
225
|
-
referenceId: undefined, // null
|
|
226
|
-
});
|
|
227
|
-
// Both inserted — no constraint violation for nulls
|
|
228
|
-
const result = await repo.existsByReferenceIdLike("%");
|
|
229
|
-
expect(result).toBe(false); // LIKE '%' won't match null referenceIds
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Credit, CreditLedger, InsufficientBalanceError } from "@wopr-network/platform-core/credits";
|
|
2
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
-
import { beginTestTransaction, createTestDb, endTestTransaction, rollbackTestTransaction } from "../../test/db.js";
|
|
4
|
-
let pool;
|
|
5
|
-
let db;
|
|
6
|
-
beforeAll(async () => {
|
|
7
|
-
({ db, pool } = await createTestDb());
|
|
8
|
-
await beginTestTransaction(pool);
|
|
9
|
-
});
|
|
10
|
-
afterAll(async () => {
|
|
11
|
-
await endTestTransaction(pool);
|
|
12
|
-
await pool.close();
|
|
13
|
-
});
|
|
14
|
-
describe("CreditLedger concurrent debit safety", () => {
|
|
15
|
-
let ledger;
|
|
16
|
-
beforeEach(async () => {
|
|
17
|
-
await rollbackTestTransaction(pool);
|
|
18
|
-
ledger = new CreditLedger(db);
|
|
19
|
-
});
|
|
20
|
-
it("concurrent debits do not overdraw — at least one should fail with InsufficientBalanceError", async () => {
|
|
21
|
-
// Fund with exactly 100 cents
|
|
22
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase");
|
|
23
|
-
// Fire two 100-cent debits concurrently — only one can succeed
|
|
24
|
-
const results = await Promise.allSettled([
|
|
25
|
-
ledger.debit("t1", Credit.fromCents(100), "bot_runtime", "debit-1"),
|
|
26
|
-
ledger.debit("t1", Credit.fromCents(100), "bot_runtime", "debit-2"),
|
|
27
|
-
]);
|
|
28
|
-
const fulfilled = results.filter((r) => r.status === "fulfilled");
|
|
29
|
-
const rejected = results.filter((r) => r.status === "rejected");
|
|
30
|
-
// Exactly one succeeds, one fails (PGlite serializes transactions so this is deterministic)
|
|
31
|
-
expect(fulfilled).toHaveLength(1);
|
|
32
|
-
expect(rejected).toHaveLength(1);
|
|
33
|
-
const err = rejected[0].reason;
|
|
34
|
-
expect(err).toBeInstanceOf(InsufficientBalanceError);
|
|
35
|
-
// Final balance must be exactly 0, not negative
|
|
36
|
-
const bal = await ledger.balance("t1");
|
|
37
|
-
expect(bal.toCents()).toBe(0);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { Credit, CreditLedger } from "@wopr-network/platform-core/credits";
|
|
2
|
-
import { afterAll, beforeAll, beforeEach, bench, describe } from "vitest";
|
|
3
|
-
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
4
|
-
let db;
|
|
5
|
-
let pool;
|
|
6
|
-
beforeAll(async () => {
|
|
7
|
-
({ db, pool } = await createTestDb());
|
|
8
|
-
});
|
|
9
|
-
afterAll(async () => {
|
|
10
|
-
await pool.close();
|
|
11
|
-
});
|
|
12
|
-
describe("CreditLedger throughput", () => {
|
|
13
|
-
let ledger;
|
|
14
|
-
beforeEach(async () => {
|
|
15
|
-
await truncateAllTables(pool);
|
|
16
|
-
ledger = new CreditLedger(db);
|
|
17
|
-
});
|
|
18
|
-
let creditIdx = 0;
|
|
19
|
-
let debitIdx = 0;
|
|
20
|
-
bench("credit operation", async () => {
|
|
21
|
-
const tenant = `tenant-${creditIdx++ % 100}`;
|
|
22
|
-
await ledger.credit(tenant, Credit.fromCents(100), "purchase", "bench");
|
|
23
|
-
}, { iterations: 1_000 });
|
|
24
|
-
bench("debit operation", async () => {
|
|
25
|
-
const tenant = `tenant-${debitIdx++ % 100}`;
|
|
26
|
-
await ledger.debit(tenant, Credit.fromCents(1), "adapter_usage", "bench");
|
|
27
|
-
}, { iterations: 1_000 });
|
|
28
|
-
bench("balance query", async () => {
|
|
29
|
-
const tenant = `tenant-${debitIdx++ % 100}`;
|
|
30
|
-
await ledger.balance(tenant);
|
|
31
|
-
}, { iterations: 5_000 });
|
|
32
|
-
});
|