@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Credit,
|
|
1
|
+
import { Credit, DrizzleLedger, InsufficientBalanceError } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { RESOURCE_TIERS } from "../../fleet/resource-tiers.js";
|
|
4
4
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
@@ -10,13 +10,14 @@ describe("runRuntimeDeductions", () => {
|
|
|
10
10
|
beforeAll(async () => {
|
|
11
11
|
const { db, pool: p } = await createTestDb();
|
|
12
12
|
pool = p;
|
|
13
|
-
ledger = new
|
|
13
|
+
ledger = new DrizzleLedger(db);
|
|
14
14
|
});
|
|
15
15
|
afterAll(async () => {
|
|
16
16
|
await pool.close();
|
|
17
17
|
});
|
|
18
18
|
beforeEach(async () => {
|
|
19
19
|
await truncateAllTables(pool);
|
|
20
|
+
await ledger.seedSystemAccounts();
|
|
20
21
|
});
|
|
21
22
|
it("DAILY_BOT_COST equals 17 cents", () => {
|
|
22
23
|
expect(DAILY_BOT_COST.toCents()).toBe(17);
|
|
@@ -32,7 +33,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
32
33
|
expect(result.errors).toEqual([]);
|
|
33
34
|
});
|
|
34
35
|
it("skips tenants with zero active bots", async () => {
|
|
35
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
|
|
36
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
|
|
36
37
|
const result = await runRuntimeDeductions({
|
|
37
38
|
ledger,
|
|
38
39
|
date: TODAY,
|
|
@@ -42,7 +43,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
42
43
|
expect((await ledger.balance("tenant-1")).toCents()).toBe(500);
|
|
43
44
|
});
|
|
44
45
|
it("deducts full amount when balance is sufficient", async () => {
|
|
45
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
|
|
46
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
|
|
46
47
|
const result = await runRuntimeDeductions({
|
|
47
48
|
ledger,
|
|
48
49
|
date: TODAY,
|
|
@@ -53,7 +54,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
53
54
|
expect((await ledger.balance("tenant-1")).toCents()).toBe(500 - 2 * 17);
|
|
54
55
|
});
|
|
55
56
|
it("partial deduction and suspension when balance is insufficient", async () => {
|
|
56
|
-
await ledger.credit("tenant-1", Credit.fromCents(10), "purchase", "top-up");
|
|
57
|
+
await ledger.credit("tenant-1", Credit.fromCents(10), "purchase", { description: "top-up" });
|
|
57
58
|
const onSuspend = vi.fn();
|
|
58
59
|
const result = await runRuntimeDeductions({
|
|
59
60
|
ledger,
|
|
@@ -67,9 +68,9 @@ describe("runRuntimeDeductions", () => {
|
|
|
67
68
|
expect((await ledger.balance("tenant-1")).toCents()).toBe(0);
|
|
68
69
|
});
|
|
69
70
|
it("suspends with zero partial when balance exactly zero", async () => {
|
|
70
|
-
await ledger.credit("tenant-1", Credit.fromCents(100), "purchase", "top-up");
|
|
71
|
-
await ledger.debit("tenant-1", Credit.fromCents(100), "bot_runtime", "drain");
|
|
72
|
-
await ledger.credit("tenant-1", Credit.fromCents(1), "purchase", "tiny");
|
|
71
|
+
await ledger.credit("tenant-1", Credit.fromCents(100), "purchase", { description: "top-up" });
|
|
72
|
+
await ledger.debit("tenant-1", Credit.fromCents(100), "bot_runtime", { description: "drain" });
|
|
73
|
+
await ledger.credit("tenant-1", Credit.fromCents(1), "purchase", { description: "tiny" });
|
|
73
74
|
const onSuspend = vi.fn();
|
|
74
75
|
const result = await runRuntimeDeductions({
|
|
75
76
|
ledger,
|
|
@@ -82,7 +83,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
82
83
|
expect((await ledger.balance("tenant-1")).toCents()).toBe(0);
|
|
83
84
|
});
|
|
84
85
|
it("suspends without onSuspend callback", async () => {
|
|
85
|
-
await ledger.credit("tenant-1", Credit.fromCents(5), "purchase", "top-up");
|
|
86
|
+
await ledger.credit("tenant-1", Credit.fromCents(5), "purchase", { description: "top-up" });
|
|
86
87
|
const result = await runRuntimeDeductions({
|
|
87
88
|
ledger,
|
|
88
89
|
date: TODAY,
|
|
@@ -92,7 +93,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
92
93
|
expect(result.processed).toBe(1);
|
|
93
94
|
});
|
|
94
95
|
it("handles errors from getActiveBotCount gracefully", async () => {
|
|
95
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
|
|
96
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
|
|
96
97
|
const result = await runRuntimeDeductions({
|
|
97
98
|
ledger,
|
|
98
99
|
date: TODAY,
|
|
@@ -105,8 +106,8 @@ describe("runRuntimeDeductions", () => {
|
|
|
105
106
|
expect(result.errors[0]).toContain("db connection failed");
|
|
106
107
|
});
|
|
107
108
|
it("handles InsufficientBalanceError from ledger.debit", async () => {
|
|
108
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
|
|
109
|
-
await ledger.debit("tenant-1", Credit.fromCents(499), "bot_runtime", "drain");
|
|
109
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
|
|
110
|
+
await ledger.debit("tenant-1", Credit.fromCents(499), "bot_runtime", { description: "drain" });
|
|
110
111
|
const onSuspend = vi.fn();
|
|
111
112
|
const result = await runRuntimeDeductions({
|
|
112
113
|
ledger,
|
|
@@ -118,7 +119,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
118
119
|
expect(onSuspend).toHaveBeenCalledWith("tenant-1");
|
|
119
120
|
});
|
|
120
121
|
it("catches InsufficientBalanceError from debit and suspends", async () => {
|
|
121
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
|
|
122
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
|
|
122
123
|
vi.spyOn(ledger, "debit").mockRejectedValue(new InsufficientBalanceError(Credit.fromCents(0), Credit.fromCents(17)));
|
|
123
124
|
const onSuspend = vi.fn();
|
|
124
125
|
const result = await runRuntimeDeductions({
|
|
@@ -133,7 +134,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
133
134
|
vi.restoreAllMocks();
|
|
134
135
|
});
|
|
135
136
|
it("catches InsufficientBalanceError without onSuspend callback", async () => {
|
|
136
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
|
|
137
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
|
|
137
138
|
vi.spyOn(ledger, "debit").mockRejectedValue(new InsufficientBalanceError(Credit.fromCents(0), Credit.fromCents(17)));
|
|
138
139
|
const result = await runRuntimeDeductions({
|
|
139
140
|
ledger,
|
|
@@ -145,8 +146,8 @@ describe("runRuntimeDeductions", () => {
|
|
|
145
146
|
vi.restoreAllMocks();
|
|
146
147
|
});
|
|
147
148
|
it("processes multiple tenants", async () => {
|
|
148
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
|
|
149
|
-
await ledger.credit("tenant-2", Credit.fromCents(10), "purchase", "top-up");
|
|
149
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
|
|
150
|
+
await ledger.credit("tenant-2", Credit.fromCents(10), "purchase", { description: "top-up" });
|
|
150
151
|
const onSuspend = vi.fn();
|
|
151
152
|
const result = await runRuntimeDeductions({
|
|
152
153
|
ledger,
|
|
@@ -159,7 +160,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
159
160
|
expect(result.suspended).not.toContain("tenant-1");
|
|
160
161
|
});
|
|
161
162
|
it("fires onLowBalance when balance drops below 100 cents threshold", async () => {
|
|
162
|
-
await ledger.credit("tenant-1", Credit.fromCents(110), "purchase", "top-up");
|
|
163
|
+
await ledger.credit("tenant-1", Credit.fromCents(110), "purchase", { description: "top-up" });
|
|
163
164
|
const onLowBalance = vi.fn();
|
|
164
165
|
await runRuntimeDeductions({
|
|
165
166
|
ledger,
|
|
@@ -173,7 +174,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
173
174
|
expect(calledBalance.toCents()).toBe(93);
|
|
174
175
|
});
|
|
175
176
|
it("does NOT fire onLowBalance when balance was already below threshold before deduction", async () => {
|
|
176
|
-
await ledger.credit("tenant-1", Credit.fromCents(90), "purchase", "top-up");
|
|
177
|
+
await ledger.credit("tenant-1", Credit.fromCents(90), "purchase", { description: "top-up" });
|
|
177
178
|
const onLowBalance = vi.fn();
|
|
178
179
|
await runRuntimeDeductions({
|
|
179
180
|
ledger,
|
|
@@ -184,7 +185,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
184
185
|
expect(onLowBalance).not.toHaveBeenCalled();
|
|
185
186
|
});
|
|
186
187
|
it("fires onCreditsExhausted when full deduction causes balance to drop to 0", async () => {
|
|
187
|
-
await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", "top-up");
|
|
188
|
+
await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", { description: "top-up" });
|
|
188
189
|
const onCreditsExhausted = vi.fn();
|
|
189
190
|
await runRuntimeDeductions({
|
|
190
191
|
ledger,
|
|
@@ -197,7 +198,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
197
198
|
});
|
|
198
199
|
it("suspends tenant when full deduction causes balance to drop to exactly 0", async () => {
|
|
199
200
|
// Balance = exactly 1 bot * DAILY_BOT_COST = 17 cents → full deduction → 0
|
|
200
|
-
await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", "top-up");
|
|
201
|
+
await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", { description: "top-up" });
|
|
201
202
|
const onSuspend = vi.fn();
|
|
202
203
|
const onCreditsExhausted = vi.fn();
|
|
203
204
|
const result = await runRuntimeDeductions({
|
|
@@ -213,7 +214,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
213
214
|
expect((await ledger.balance("tenant-1")).toCents()).toBe(0);
|
|
214
215
|
});
|
|
215
216
|
it("fires onCreditsExhausted on partial deduction when balance hits 0", async () => {
|
|
216
|
-
await ledger.credit("tenant-1", Credit.fromCents(10), "purchase", "top-up");
|
|
217
|
+
await ledger.credit("tenant-1", Credit.fromCents(10), "purchase", { description: "top-up" });
|
|
217
218
|
const onCreditsExhausted = vi.fn();
|
|
218
219
|
await runRuntimeDeductions({
|
|
219
220
|
ledger,
|
|
@@ -225,7 +226,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
225
226
|
expect((await ledger.balance("tenant-1")).toCents()).toBe(0);
|
|
226
227
|
});
|
|
227
228
|
it("partially debits resource tier surcharge when balance is positive but insufficient", async () => {
|
|
228
|
-
await ledger.credit("tenant-1", Credit.fromCents(30), "purchase", "top-up");
|
|
229
|
+
await ledger.credit("tenant-1", Credit.fromCents(30), "purchase", { description: "top-up" });
|
|
229
230
|
const result = await runRuntimeDeductions({
|
|
230
231
|
ledger,
|
|
231
232
|
date: TODAY,
|
|
@@ -236,7 +237,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
236
237
|
expect((await ledger.balance("tenant-1")).toCents()).toBe(0);
|
|
237
238
|
});
|
|
238
239
|
it("skips resource tier partial debit when balance is exactly 0 after runtime", async () => {
|
|
239
|
-
await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", "top-up");
|
|
240
|
+
await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", { description: "top-up" });
|
|
240
241
|
const onCreditsExhausted = vi.fn();
|
|
241
242
|
const result = await runRuntimeDeductions({
|
|
242
243
|
ledger,
|
|
@@ -254,7 +255,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
254
255
|
// triggering the zero-crossing suspend in the runtime block.
|
|
255
256
|
// Storage cost (5 cents) then tries to suspend again via its else-branch (balance 0 < 5).
|
|
256
257
|
// The !result.suspended.includes(tenantId) guard must prevent onSuspend being called twice.
|
|
257
|
-
await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", "top-up");
|
|
258
|
+
await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", { description: "top-up" });
|
|
258
259
|
const onSuspend = vi.fn();
|
|
259
260
|
const result = await runRuntimeDeductions({
|
|
260
261
|
ledger,
|
|
@@ -270,7 +271,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
270
271
|
it("buildResourceTierCosts: deducts pro tier surcharge via getResourceTierCosts", async () => {
|
|
271
272
|
const proTierCost = RESOURCE_TIERS.pro.dailyCost.toCents();
|
|
272
273
|
const startBalance = 17 + proTierCost + 10;
|
|
273
|
-
await ledger.credit("tenant-1", Credit.fromCents(startBalance), "purchase", "top-up");
|
|
274
|
+
await ledger.credit("tenant-1", Credit.fromCents(startBalance), "purchase", { description: "top-up" });
|
|
274
275
|
const mockRepo = {
|
|
275
276
|
getResourceTier: async (_botId) => "pro",
|
|
276
277
|
};
|
|
@@ -285,7 +286,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
285
286
|
expect((await ledger.balance("tenant-1")).toCents()).toBe(expected);
|
|
286
287
|
});
|
|
287
288
|
it("treats unique constraint violation from concurrent debit as already-billed (skip, not error)", async () => {
|
|
288
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
|
|
289
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
|
|
289
290
|
const uniqueErr = Object.assign(new Error("duplicate key value violates unique constraint"), { code: "23505" });
|
|
290
291
|
vi.spyOn(ledger, "debit").mockRejectedValueOnce(uniqueErr);
|
|
291
292
|
const result = await runRuntimeDeductions({
|
|
@@ -298,7 +299,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
298
299
|
vi.restoreAllMocks();
|
|
299
300
|
});
|
|
300
301
|
it("is idempotent — second run on same date does not double-deduct", async () => {
|
|
301
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
|
|
302
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
|
|
302
303
|
const cfg = {
|
|
303
304
|
ledger,
|
|
304
305
|
getActiveBotCount: async () => 1,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import type { IBotInstanceRepository } from "../../fleet/bot-instance-repository.js";
|
|
3
3
|
import type { ITenantAddonRepository } from "../addons/addon-repository.js";
|
|
4
4
|
export interface RuntimeSchedulerDeps {
|
|
5
|
-
ledger:
|
|
5
|
+
ledger: ILedger;
|
|
6
6
|
botInstanceRepo: IBotInstanceRepository;
|
|
7
7
|
tenantAddonRepo: ITenantAddonRepository;
|
|
8
8
|
onSuspend?: (tenantId: string) => void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { RUNTIME_INTERVAL_MS, startRuntimeScheduler } from "./runtime-scheduler.js";
|
|
3
|
-
// Minimal
|
|
3
|
+
// Minimal ILedger stub — only the methods runRuntimeDeductions calls.
|
|
4
4
|
function makeLedger() {
|
|
5
5
|
return {
|
|
6
6
|
tenantsWithBalance: vi.fn().mockResolvedValue([]),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DrizzleLedger, grantSignupCredits, SIGNUP_GRANT } 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
|
describe("grantSignupCredits", () => {
|
|
@@ -13,7 +13,8 @@ describe("grantSignupCredits", () => {
|
|
|
13
13
|
});
|
|
14
14
|
beforeEach(async () => {
|
|
15
15
|
await truncateAllTables(pool);
|
|
16
|
-
ledger = new
|
|
16
|
+
ledger = new DrizzleLedger(db);
|
|
17
|
+
await ledger.seedSystemAccounts();
|
|
17
18
|
});
|
|
18
19
|
it("grants credits to a new tenant and returns true", async () => {
|
|
19
20
|
const result = await grantSignupCredits(ledger, "tenant-1");
|
|
@@ -41,7 +42,8 @@ describe("grantSignupCredits", () => {
|
|
|
41
42
|
const uniqueErr = Object.assign(new Error("duplicate key value violates unique constraint"), {
|
|
42
43
|
code: "23505",
|
|
43
44
|
});
|
|
44
|
-
const racingLedger = new
|
|
45
|
+
const racingLedger = new DrizzleLedger(db);
|
|
46
|
+
await racingLedger.seedSystemAccounts();
|
|
45
47
|
vi.spyOn(racingLedger, "hasReferenceId").mockResolvedValue(false);
|
|
46
48
|
vi.spyOn(racingLedger, "credit").mockRejectedValue(uniqueErr);
|
|
47
49
|
const result = await grantSignupCredits(racingLedger, "tenant-race");
|
|
@@ -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 { runRuntimeDeductions } from "./runtime-cron.js";
|
|
@@ -15,7 +15,8 @@ describe("runtime cron with storage tiers", () => {
|
|
|
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
|
});
|
|
20
21
|
it("debits base cost plus storage surcharge for pro tier", async () => {
|
|
21
22
|
await ledger.credit("t1", Credit.fromCents(1000), "purchase");
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Credit, DrizzleLedger, runTrialBalanceCron } from "@wopr-network/platform-core/credits";
|
|
2
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
4
|
+
describe("runTrialBalanceCron", () => {
|
|
5
|
+
let pool;
|
|
6
|
+
let ledger;
|
|
7
|
+
beforeAll(async () => {
|
|
8
|
+
const { db, pool: p } = await createTestDb();
|
|
9
|
+
pool = p;
|
|
10
|
+
ledger = new DrizzleLedger(db);
|
|
11
|
+
});
|
|
12
|
+
afterAll(async () => {
|
|
13
|
+
await pool.close();
|
|
14
|
+
});
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
await truncateAllTables(pool);
|
|
17
|
+
await ledger.seedSystemAccounts();
|
|
18
|
+
});
|
|
19
|
+
it("returns balanced when no entries exist", async () => {
|
|
20
|
+
const result = await runTrialBalanceCron({ ledger });
|
|
21
|
+
expect(result.balanced).toBe(true);
|
|
22
|
+
expect(result.differenceRaw).toBe(0);
|
|
23
|
+
});
|
|
24
|
+
it("returns balanced after normal credit and debit", async () => {
|
|
25
|
+
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
26
|
+
await ledger.debit("t1", Credit.fromCents(200), "bot_runtime");
|
|
27
|
+
const result = await runTrialBalanceCron({ ledger });
|
|
28
|
+
expect(result.balanced).toBe(true);
|
|
29
|
+
expect(result.differenceRaw).toBe(0);
|
|
30
|
+
});
|
|
31
|
+
it("logs an error on imbalance without throwing", async () => {
|
|
32
|
+
vi.spyOn(ledger, "trialBalance").mockResolvedValueOnce({
|
|
33
|
+
totalDebits: Credit.fromCents(1000),
|
|
34
|
+
totalCredits: Credit.fromCents(900),
|
|
35
|
+
balanced: false,
|
|
36
|
+
difference: Credit.fromCents(100),
|
|
37
|
+
});
|
|
38
|
+
const result = await runTrialBalanceCron({ ledger });
|
|
39
|
+
expect(result.balanced).toBe(false);
|
|
40
|
+
expect(result.differenceRaw).toBe(Credit.fromCents(100).toRaw());
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -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 { Context, Next } from "hono";
|
|
4
4
|
/**
|
|
@@ -37,7 +37,7 @@ export declare function createFeatureGate(cfg: FeatureGateConfig): {
|
|
|
37
37
|
/**
|
|
38
38
|
* Convenience factory that creates a requireBalance middleware from a CreditLedger instance.
|
|
39
39
|
*/
|
|
40
|
-
export declare function createBalanceGate(ledger:
|
|
40
|
+
export declare function createBalanceGate(ledger: ILedger, userKey?: string, userIdField?: string): {
|
|
41
41
|
requireBalance: (minBalance?: Credit) => (c: Context, next: Next) => Promise<void | (Response & import("hono").TypedResponse<{
|
|
42
42
|
error: string;
|
|
43
43
|
}, 401, "json">) | (Response & import("hono").TypedResponse<{
|
|
@@ -54,7 +54,7 @@ export declare function createBalanceGate(ledger: CreditLedger, userKey?: string
|
|
|
54
54
|
export type ResolveTenantId = (c: Context) => string | undefined | Promise<string | undefined>;
|
|
55
55
|
export interface CreditGateConfig {
|
|
56
56
|
/** CreditLedger instance used to check balance. */
|
|
57
|
-
ledger:
|
|
57
|
+
ledger: ILedger;
|
|
58
58
|
/** Resolve the tenant ID from the request context. */
|
|
59
59
|
resolveTenantId: ResolveTenantId;
|
|
60
60
|
}
|
|
@@ -39,8 +39,8 @@ export { type AdapterCapability, type AdapterResult, type EmbeddingsInput, type
|
|
|
39
39
|
export { type ArbitrageRequest, ArbitrageRouter, type ArbitrageRouterConfig, type MarginRecord, type ModelProviderEntry, NoProviderAvailableError, ProviderRegistry, type ProviderRegistryConfig, type RoutingDecision, } from "./arbitrage/index.js";
|
|
40
40
|
export type { BudgetCheckerConfig, BudgetCheckResult, SpendLimits } from "./budget/index.js";
|
|
41
41
|
export { BudgetChecker, DrizzleBudgetChecker } from "./budget/index.js";
|
|
42
|
-
export type { BillingState,
|
|
43
|
-
export { BotBilling, buildResourceTierCosts,
|
|
42
|
+
export type { BillingState, CreditType, DebitType, GetActiveBotCount, HistoryOptions, ILedger, JournalEntry, OnSuspend, RuntimeCronConfig, RuntimeCronResult, TransactionType, } from "./credits/index.js";
|
|
43
|
+
export { BotBilling, buildResourceTierCosts, DAILY_BOT_COST, DrizzleBotBilling, DrizzleLedger, grantSignupCredits, InsufficientBalanceError, Ledger, runRuntimeDeductions, SIGNUP_GRANT, SUSPENSION_GRACE_DAYS, } from "./credits/index.js";
|
|
44
44
|
export { type CreditGateConfig, createBalanceGate, createCreditGate, createFeatureGate, type FeatureGateConfig, type GetUserBalance, type ResolveTenantId, } from "./feature-gate.js";
|
|
45
45
|
export type { BillingPeriod, BillingPeriodSummary, MeterEventRow, UsageSummary, } from "./metering/index.js";
|
|
46
46
|
export { DrizzleMeterAggregator, DrizzleMeterEmitter, MeterAggregator, MeterEmitter, } from "./metering/index.js";
|
|
@@ -48,7 +48,7 @@ export type { PayRamBillingConfig, PayRamCheckoutOpts, PayRamConfig, PayRamPayme
|
|
|
48
48
|
export { createPayRamCheckout, createPayRamClient, DrizzlePayRamChargeRepository, handlePayRamWebhook, loadPayRamConfig, MIN_PAYMENT_USD, PayRamChargeRepository, } from "./payram/index.js";
|
|
49
49
|
export { checkInstanceQuota, DEFAULT_INSTANCE_LIMITS, type InstanceLimits, type QuotaCheckResult, } from "./quotas/quota-check.js";
|
|
50
50
|
export { buildResourceLimits, type ContainerResourceLimits, DEFAULT_RESOURCE_CONFIG, type ResourceConfig, } from "./quotas/resource-limits.js";
|
|
51
|
-
export type { IBotBilling, IBudgetChecker,
|
|
51
|
+
export type { IBotBilling, IBudgetChecker, IMeterAggregator, IMeterEmitter, IPayRamChargeRepository, ITenantCustomerRepository, PayRamChargeRecord, } from "./repository-types.js";
|
|
52
52
|
export { AdapterSocket, type SocketConfig, type SocketRequest } from "./socket/socket.js";
|
|
53
53
|
export type { CreditCheckoutOpts, CreditPriceMap, CreditPricePoint, PortalSessionOpts, StripeBillingConfig, TenantCustomerRow, WebhookDeps, WebhookResult, } from "./stripe/index.js";
|
|
54
54
|
export { CREDIT_PRICE_POINTS, createCreditCheckoutSession, createPortalSession, createStripeClient, DrizzleTenantCustomerRepository, getConfiguredPriceIds, getCreditAmountForPurchase, handleWebhookEvent, loadCreditPriceMap, loadStripeConfig, lookupCreditPrice, TenantCustomerRepository, } from "./stripe/index.js";
|
|
@@ -49,7 +49,7 @@ export { withMargin, } from "./adapters/types.js";
|
|
|
49
49
|
// Arbitrage router — multi-provider routing for maximum margin (WOP-463)
|
|
50
50
|
export { ArbitrageRouter, NoProviderAvailableError, ProviderRegistry, } from "./arbitrage/index.js";
|
|
51
51
|
export { BudgetChecker, DrizzleBudgetChecker } from "./budget/index.js";
|
|
52
|
-
export { BotBilling, buildResourceTierCosts,
|
|
52
|
+
export { BotBilling, buildResourceTierCosts, DAILY_BOT_COST, DrizzleBotBilling, DrizzleLedger, grantSignupCredits, InsufficientBalanceError, Ledger, runRuntimeDeductions, SIGNUP_GRANT, SUSPENSION_GRACE_DAYS, } from "./credits/index.js";
|
|
53
53
|
// Feature gating middleware (WOP-384 — replaced tier gates with balance gates)
|
|
54
54
|
export { createBalanceGate, createCreditGate, createFeatureGate, } from "./feature-gate.js";
|
|
55
55
|
export { DrizzleMeterAggregator, DrizzleMeterEmitter, MeterAggregator, MeterEmitter, } from "./metering/index.js";
|
|
@@ -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 { runReconciliation } from "@wopr-network/platform-core/metering";
|
|
4
4
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { usageSummaries } from "../../db/schema/meter-events.js";
|
|
@@ -20,7 +20,7 @@ describe("runReconciliation", () => {
|
|
|
20
20
|
const t = await createTestDb();
|
|
21
21
|
pool = t.pool;
|
|
22
22
|
db = t.db;
|
|
23
|
-
ledger = new
|
|
23
|
+
ledger = new DrizzleLedger(db);
|
|
24
24
|
usageSummaryRepo = new DrizzleUsageSummaryRepository(db);
|
|
25
25
|
adapterUsageRepo = new DrizzleAdapterUsageRepository(db);
|
|
26
26
|
});
|
|
@@ -29,6 +29,7 @@ describe("runReconciliation", () => {
|
|
|
29
29
|
});
|
|
30
30
|
beforeEach(async () => {
|
|
31
31
|
await truncateAllTables(pool);
|
|
32
|
+
await ledger.seedSystemAccounts();
|
|
32
33
|
});
|
|
33
34
|
/** Insert a usage_summaries row directly. */
|
|
34
35
|
async function insertSummary(opts) {
|
|
@@ -56,7 +57,7 @@ describe("runReconciliation", () => {
|
|
|
56
57
|
const charge = Credit.fromCents(50);
|
|
57
58
|
await insertSummary({ tenant: "t1", totalCharge: charge.toRaw() });
|
|
58
59
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
59
|
-
await ledger.debit("t1", charge, "adapter_usage", "chat usage");
|
|
60
|
+
await ledger.debit("t1", charge, "adapter_usage", { description: "chat usage" });
|
|
60
61
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
61
62
|
expect(result.tenantsChecked).toBe(1);
|
|
62
63
|
expect(result.discrepancies).toEqual([]);
|
|
@@ -64,7 +65,7 @@ describe("runReconciliation", () => {
|
|
|
64
65
|
it("detects drift when metered charge exceeds ledger debit", async () => {
|
|
65
66
|
await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(100).toRaw() });
|
|
66
67
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
67
|
-
await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", "chat usage");
|
|
68
|
+
await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", { description: "chat usage" });
|
|
68
69
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
69
70
|
expect(result.tenantsChecked).toBe(1);
|
|
70
71
|
expect(result.discrepancies).toHaveLength(1);
|
|
@@ -90,7 +91,7 @@ describe("runReconciliation", () => {
|
|
|
90
91
|
await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(20).toRaw() });
|
|
91
92
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
92
93
|
// Debit as bot_runtime — should NOT count toward reconciliation
|
|
93
|
-
await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", "daily runtime");
|
|
94
|
+
await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", { description: "daily runtime" });
|
|
94
95
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
95
96
|
// Metered 20c, ledger adapter_usage = 0 => drift = 20c
|
|
96
97
|
expect(result.discrepancies).toHaveLength(1);
|
|
@@ -118,11 +119,11 @@ describe("runReconciliation", () => {
|
|
|
118
119
|
// t1: balanced
|
|
119
120
|
await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(50).toRaw() });
|
|
120
121
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
121
|
-
await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", "chat");
|
|
122
|
+
await ledger.debit("t1", Credit.fromCents(50), "adapter_usage", { description: "chat" });
|
|
122
123
|
// t2: drifted
|
|
123
124
|
await insertSummary({ tenant: "t2", totalCharge: Credit.fromCents(100).toRaw() });
|
|
124
125
|
await ledger.credit("t2", Credit.fromCents(500), "purchase");
|
|
125
|
-
await ledger.debit("t2", Credit.fromCents(60), "adapter_usage", "chat");
|
|
126
|
+
await ledger.debit("t2", Credit.fromCents(60), "adapter_usage", { description: "chat" });
|
|
126
127
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
127
128
|
expect(result.tenantsChecked).toBe(2);
|
|
128
129
|
expect(result.discrepancies).toHaveLength(1);
|
|
@@ -153,7 +154,7 @@ describe("runReconciliation", () => {
|
|
|
153
154
|
// Metered 50c but debited 80c (over-billed)
|
|
154
155
|
await insertSummary({ tenant: "t1", totalCharge: Credit.fromCents(50).toRaw() });
|
|
155
156
|
await ledger.credit("t1", Credit.fromCents(500), "purchase");
|
|
156
|
-
await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", "chat usage");
|
|
157
|
+
await ledger.debit("t1", Credit.fromCents(80), "adapter_usage", { description: "chat usage" });
|
|
157
158
|
const result = await runReconciliation({ usageSummaryRepo, adapterUsageRepo, targetDate: TODAY });
|
|
158
159
|
expect(result.discrepancies).toHaveLength(1);
|
|
159
160
|
expect(result.discrepancies[0].driftRaw).toBe(Credit.fromCents(-30).toRaw());
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { and, eq, gte, lt, ne, sql } from "drizzle-orm";
|
|
2
|
-
import {
|
|
2
|
+
import { journalEntries, journalLines } from "../../db/schema/ledger.js";
|
|
3
3
|
import { usageSummaries } from "../../db/schema/meter-events.js";
|
|
4
4
|
export class DrizzleUsageSummaryRepository {
|
|
5
5
|
db;
|
|
@@ -25,19 +25,20 @@ export class DrizzleAdapterUsageRepository {
|
|
|
25
25
|
this.db = db;
|
|
26
26
|
}
|
|
27
27
|
async getAggregatedAdapterUsageDebits(startIso, endIso) {
|
|
28
|
+
// Sum the debit-side journal line amounts for adapter_usage entries.
|
|
29
|
+
// In double-entry: DR tenant liability (2000:<tenantId>), CR revenue:adapter_usage (4010).
|
|
28
30
|
const rows = await this.db
|
|
29
31
|
.select({
|
|
30
|
-
tenantId:
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
// raw SQL: Drizzle cannot express ABS with COALESCE and SUM
|
|
34
|
-
totalDebitRaw: sql `COALESCE(SUM(ABS(amount_credits)), 0)`,
|
|
32
|
+
tenantId: journalEntries.tenantId,
|
|
33
|
+
// raw SQL: Drizzle cannot express COALESCE with SUM aggregation
|
|
34
|
+
totalDebitRaw: sql `COALESCE(SUM(${journalLines.amount}), 0)`,
|
|
35
35
|
})
|
|
36
|
-
.from(
|
|
37
|
-
.
|
|
36
|
+
.from(journalLines)
|
|
37
|
+
.innerJoin(journalEntries, eq(journalEntries.id, journalLines.journalEntryId))
|
|
38
|
+
.where(and(eq(journalEntries.entryType, "adapter_usage"), eq(journalLines.side, "debit"),
|
|
38
39
|
// raw SQL: Drizzle cannot express timestamptz cast for text column date comparison
|
|
39
|
-
sql `${
|
|
40
|
-
.groupBy(
|
|
40
|
+
sql `${journalEntries.postedAt}::timestamptz >= ${startIso}::timestamptz`, sql `${journalEntries.postedAt}::timestamptz < ${endIso}::timestamptz`))
|
|
41
|
+
.groupBy(journalEntries.tenantId);
|
|
41
42
|
return rows.map((r) => ({ tenantId: r.tenantId, totalDebitRaw: Number(r.totalDebitRaw) }));
|
|
42
43
|
}
|
|
43
44
|
}
|
|
@@ -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 { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
4
4
|
import { createTestDb, seedUsageSummary, truncateAllTables } from "../../test/db.js";
|
|
5
5
|
import { DrizzleAdapterUsageRepository, DrizzleUsageSummaryRepository } from "./reconciliation-repository.js";
|
|
@@ -107,7 +107,8 @@ describe("DrizzleAdapterUsageRepository", () => {
|
|
|
107
107
|
beforeEach(async () => {
|
|
108
108
|
await truncateAllTables(pool);
|
|
109
109
|
repo = new DrizzleAdapterUsageRepository(db);
|
|
110
|
-
ledger = new
|
|
110
|
+
ledger = new DrizzleLedger(db);
|
|
111
|
+
await ledger.seedSystemAccounts();
|
|
111
112
|
});
|
|
112
113
|
it("returns empty array when no adapter_usage debits exist", async () => {
|
|
113
114
|
const today = new Date().toISOString().slice(0, 10);
|
|
@@ -120,9 +121,9 @@ describe("DrizzleAdapterUsageRepository", () => {
|
|
|
120
121
|
// Fund tenants
|
|
121
122
|
await ledger.credit("t1", Credit.fromCents(1000), "purchase");
|
|
122
123
|
await ledger.credit("t2", Credit.fromCents(1000), "purchase");
|
|
123
|
-
await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", "t1-debit-1");
|
|
124
|
-
await ledger.debit("t1", Credit.fromCents(20), "adapter_usage", "t1-debit-2");
|
|
125
|
-
await ledger.debit("t2", Credit.fromCents(50), "adapter_usage", "t2-debit-1");
|
|
124
|
+
await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", { description: "t1-debit-1" });
|
|
125
|
+
await ledger.debit("t1", Credit.fromCents(20), "adapter_usage", { description: "t1-debit-2" });
|
|
126
|
+
await ledger.debit("t2", Credit.fromCents(50), "adapter_usage", { description: "t2-debit-1" });
|
|
126
127
|
// Query window covering today
|
|
127
128
|
const today = new Date().toISOString().slice(0, 10);
|
|
128
129
|
const startIso = `${today}T00:00:00Z`;
|
|
@@ -136,8 +137,8 @@ describe("DrizzleAdapterUsageRepository", () => {
|
|
|
136
137
|
});
|
|
137
138
|
it("excludes non-adapter_usage debit types", async () => {
|
|
138
139
|
await ledger.credit("t1", Credit.fromCents(1000), "purchase");
|
|
139
|
-
await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", "adapter debit");
|
|
140
|
-
await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", "runtime debit");
|
|
140
|
+
await ledger.debit("t1", Credit.fromCents(30), "adapter_usage", { description: "adapter debit" });
|
|
141
|
+
await ledger.debit("t1", Credit.fromCents(20), "bot_runtime", { description: "runtime debit" });
|
|
141
142
|
const today = new Date().toISOString().slice(0, 10);
|
|
142
143
|
const startIso = `${today}T00:00:00Z`;
|
|
143
144
|
const endIso = new Date(new Date(startIso).getTime() + 86400000).toISOString();
|
|
@@ -147,7 +148,7 @@ describe("DrizzleAdapterUsageRepository", () => {
|
|
|
147
148
|
});
|
|
148
149
|
it("excludes credit transactions (positive amounts are not debits)", async () => {
|
|
149
150
|
await ledger.credit("t1", Credit.fromCents(1000), "purchase");
|
|
150
|
-
await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", "real debit");
|
|
151
|
+
await ledger.debit("t1", Credit.fromCents(10), "adapter_usage", { description: "real debit" });
|
|
151
152
|
const today = new Date().toISOString().slice(0, 10);
|
|
152
153
|
const startIso = `${today}T00:00:00Z`;
|
|
153
154
|
const endIso = new Date(new Date(startIso).getTime() + 86400000).toISOString();
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { IWebhookSeenRepository, PayRamChargeRepository, PayRamWebhookPayload, PayRamWebhookResult } from "@wopr-network/platform-core/billing";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import type { BotBilling } from "../credits/bot-billing.js";
|
|
4
4
|
export interface PayRamWebhookDeps {
|
|
5
5
|
chargeStore: PayRamChargeRepository;
|
|
6
|
-
creditLedger:
|
|
6
|
+
creditLedger: ILedger;
|
|
7
7
|
botBilling?: BotBilling;
|
|
8
8
|
replayGuard: IWebhookSeenRepository;
|
|
9
9
|
}
|
|
@@ -36,7 +36,11 @@ export async function handlePayRamWebhook(deps, payload) {
|
|
|
36
36
|
// For OVER_FILLED, we still credit the requested amount — the
|
|
37
37
|
// overpayment stays in the PayRam wallet as a buffer.
|
|
38
38
|
const creditCents = charge.amountUsdCents;
|
|
39
|
-
await creditLedger.credit(charge.tenantId, Credit.fromCents(creditCents), "purchase",
|
|
39
|
+
await creditLedger.credit(charge.tenantId, Credit.fromCents(creditCents), "purchase", {
|
|
40
|
+
description: `Crypto credit purchase via PayRam (ref: ${payload.reference_id}, ${payload.currency ?? "crypto"})`,
|
|
41
|
+
referenceId: `payram:${payload.reference_id}`,
|
|
42
|
+
fundingSource: "payram",
|
|
43
|
+
});
|
|
40
44
|
await chargeStore.markCredited(payload.reference_id);
|
|
41
45
|
// Reactivate suspended bots (same as Stripe webhook, WOP-447).
|
|
42
46
|
let reactivatedBots;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* no-op status, idempotency, replay guard, and bot reactivation.
|
|
6
6
|
*/
|
|
7
7
|
import { DrizzleWebhookSeenRepository, noOpReplayGuard, PayRamChargeRepository, } from "@wopr-network/platform-core/billing";
|
|
8
|
-
import {
|
|
8
|
+
import { DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
9
9
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
10
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
11
11
|
import { handlePayRamWebhook } from "./webhook.js";
|
|
@@ -35,7 +35,8 @@ describe("handlePayRamWebhook", () => {
|
|
|
35
35
|
beforeEach(async () => {
|
|
36
36
|
await truncateAllTables(pool);
|
|
37
37
|
chargeStore = new PayRamChargeRepository(db);
|
|
38
|
-
creditLedger = new
|
|
38
|
+
creditLedger = new DrizzleLedger(db);
|
|
39
|
+
await creditLedger.seedSystemAccounts();
|
|
39
40
|
deps = { chargeStore, creditLedger, replayGuard: noOpReplayGuard };
|
|
40
41
|
// Create a default test charge
|
|
41
42
|
await chargeStore.create("ref-test-001", "tenant-a", 2500);
|
|
@@ -58,12 +59,12 @@ describe("handlePayRamWebhook", () => {
|
|
|
58
59
|
const history = await creditLedger.history("tenant-a");
|
|
59
60
|
expect(history).toHaveLength(1);
|
|
60
61
|
expect(history[0].referenceId).toBe("payram:ref-test-001");
|
|
61
|
-
expect(history[0].
|
|
62
|
+
expect(history[0].entryType).toBe("purchase");
|
|
62
63
|
});
|
|
63
64
|
it("records fundingSource as payram", async () => {
|
|
64
65
|
await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
|
|
65
66
|
const history = await creditLedger.history("tenant-a");
|
|
66
|
-
expect(history[0].fundingSource).toBe("payram");
|
|
67
|
+
expect(history[0].metadata?.fundingSource).toBe("payram");
|
|
67
68
|
});
|
|
68
69
|
it("marks the charge as credited after FILLED", async () => {
|
|
69
70
|
await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
|
|
@@ -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 { ICouponRepository } from "./coupon-repository.js";
|
|
4
4
|
import type { IPromotionRepository } from "./promotion-repository.js";
|
|
@@ -19,7 +19,7 @@ interface PromotionEngineDeps {
|
|
|
19
19
|
promotionRepo: IPromotionRepository;
|
|
20
20
|
couponRepo: ICouponRepository;
|
|
21
21
|
redemptionRepo: IRedemptionRepository;
|
|
22
|
-
ledger:
|
|
22
|
+
ledger: ILedger;
|
|
23
23
|
}
|
|
24
24
|
export declare class PromotionEngine {
|
|
25
25
|
#private;
|