@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,7 +1,7 @@
|
|
|
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
|
-
describe("
|
|
4
|
+
describe("DrizzleDrizzleLedger.memberUsage", () => {
|
|
5
5
|
let pool;
|
|
6
6
|
let db;
|
|
7
7
|
let ledger;
|
|
@@ -13,13 +13,23 @@ describe("DrizzleCreditLedger.memberUsage", () => {
|
|
|
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("should aggregate debit totals per attributed user", async () => {
|
|
19
|
-
await ledger.credit("org-1", Credit.fromCents(10000), "purchase", "Seed");
|
|
20
|
-
await ledger.debit("org-1", Credit.fromCents(100), "adapter_usage",
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
await ledger.credit("org-1", Credit.fromCents(10000), "purchase", { description: "Seed" });
|
|
21
|
+
await ledger.debit("org-1", Credit.fromCents(100), "adapter_usage", {
|
|
22
|
+
description: "Chat",
|
|
23
|
+
attributedUserId: "user-a",
|
|
24
|
+
});
|
|
25
|
+
await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage", {
|
|
26
|
+
description: "Chat",
|
|
27
|
+
attributedUserId: "user-a",
|
|
28
|
+
});
|
|
29
|
+
await ledger.debit("org-1", Credit.fromCents(300), "adapter_usage", {
|
|
30
|
+
description: "Chat",
|
|
31
|
+
attributedUserId: "user-b",
|
|
32
|
+
});
|
|
23
33
|
const result = await ledger.memberUsage("org-1");
|
|
24
34
|
expect(result).toHaveLength(2);
|
|
25
35
|
const userA = result.find((r) => r.userId === "user-a");
|
|
@@ -30,9 +40,12 @@ describe("DrizzleCreditLedger.memberUsage", () => {
|
|
|
30
40
|
expect(userB?.transactionCount).toBe(1);
|
|
31
41
|
});
|
|
32
42
|
it("should exclude transactions with null attributedUserId", async () => {
|
|
33
|
-
await ledger.credit("org-1", Credit.fromCents(10000), "purchase", "Seed");
|
|
34
|
-
await ledger.debit("org-1", Credit.fromCents(100), "bot_runtime", "Cron"); // no attributedUserId
|
|
35
|
-
await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage",
|
|
43
|
+
await ledger.credit("org-1", Credit.fromCents(10000), "purchase", { description: "Seed" });
|
|
44
|
+
await ledger.debit("org-1", Credit.fromCents(100), "bot_runtime", { description: "Cron" }); // no attributedUserId
|
|
45
|
+
await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage", {
|
|
46
|
+
description: "Chat",
|
|
47
|
+
attributedUserId: "user-a",
|
|
48
|
+
});
|
|
36
49
|
const result = await ledger.memberUsage("org-1");
|
|
37
50
|
expect(result).toHaveLength(1);
|
|
38
51
|
expect(result[0]?.userId).toBe("user-a");
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import type { IMeterEmitter } from "@wopr-network/platform-core/metering";
|
|
3
3
|
import type { IPhoneNumberRepository } from "./drizzle-phone-number-repository.js";
|
|
4
4
|
export type { IPhoneNumberRepository } from "./drizzle-phone-number-repository.js";
|
|
5
5
|
/** Phone number monthly wholesale cost in USD. Exported for proxy.ts to import. */
|
|
6
6
|
export declare const PHONE_NUMBER_MONTHLY_COST = 1.15;
|
|
7
|
-
export declare function runMonthlyPhoneBilling(phoneRepo: IPhoneNumberRepository, ledger:
|
|
7
|
+
export declare function runMonthlyPhoneBilling(phoneRepo: IPhoneNumberRepository, ledger: ILedger, meter: IMeterEmitter): Promise<{
|
|
8
8
|
processed: number;
|
|
9
9
|
billed: {
|
|
10
10
|
tenantId: string;
|
|
@@ -23,7 +23,11 @@ export async function runMonthlyPhoneBilling(phoneRepo, ledger, meter) {
|
|
|
23
23
|
try {
|
|
24
24
|
const costCredit = Credit.fromDollars(PHONE_NUMBER_MONTHLY_COST);
|
|
25
25
|
const chargeCredit = withMargin(costCredit, PHONE_NUMBER_MARGIN);
|
|
26
|
-
await ledger.debit(number.tenantId, chargeCredit, "addon",
|
|
26
|
+
await ledger.debit(number.tenantId, chargeCredit, "addon", {
|
|
27
|
+
description: "Monthly phone number fee",
|
|
28
|
+
referenceId: `phone-billing:${number.sid}:${now.toISOString().slice(0, 7)}`,
|
|
29
|
+
allowNegative: true,
|
|
30
|
+
});
|
|
27
31
|
meter.emit({
|
|
28
32
|
tenant: number.tenantId,
|
|
29
33
|
cost: costCredit,
|
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
import { Credit, InsufficientBalanceError
|
|
1
|
+
import { Credit, InsufficientBalanceError } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { PHONE_NUMBER_MONTHLY_COST, runMonthlyPhoneBilling } from "./phone-billing.js";
|
|
4
4
|
function makeTx(tenantId) {
|
|
5
5
|
return {
|
|
6
6
|
id: "tx-1",
|
|
7
|
+
postedAt: new Date().toISOString(),
|
|
8
|
+
entryType: "addon",
|
|
7
9
|
tenantId,
|
|
8
|
-
amount: Credit.fromDollars(1),
|
|
9
|
-
balanceAfter: Credit.fromDollars(100),
|
|
10
|
-
type: "addon",
|
|
11
10
|
description: "Monthly phone number fee",
|
|
12
11
|
referenceId: null,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
createdAt: new Date().toISOString(),
|
|
16
|
-
expiresAt: null,
|
|
12
|
+
metadata: null,
|
|
13
|
+
lines: [],
|
|
17
14
|
};
|
|
18
15
|
}
|
|
19
16
|
function makeNumber(overrides = {}) {
|
|
@@ -85,16 +82,16 @@ describe("runMonthlyPhoneBilling", () => {
|
|
|
85
82
|
expect(result.failed).toEqual([]);
|
|
86
83
|
// Verify debit was called with the margined charge amount
|
|
87
84
|
expect(ledger.debit).toHaveBeenCalledOnce();
|
|
88
|
-
const [tenantId, chargeAmount, type,
|
|
85
|
+
const [tenantId, chargeAmount, type, opts] = ledger.debit.mock.calls[0];
|
|
89
86
|
expect(tenantId).toBe("tenant-1");
|
|
90
87
|
// chargeCredit = Credit.fromDollars(1.15).multiply(2.6)
|
|
91
88
|
const expectedCharge = Credit.fromDollars(1.15).multiply(2.6);
|
|
92
89
|
expect(chargeAmount.toRaw()).toBe(expectedCharge.toRaw());
|
|
93
90
|
expect(type).toBe("addon");
|
|
94
|
-
expect(description).toBe("Monthly phone number fee");
|
|
91
|
+
expect(opts.description).toBe("Monthly phone number fee");
|
|
95
92
|
const expectedMonth = `${NOW.getFullYear()}-${String(NOW.getMonth() + 1).padStart(2, "0")}`;
|
|
96
|
-
expect(referenceId).toMatch(new RegExp(`^phone-billing:PN-abc123:${expectedMonth}$`));
|
|
97
|
-
expect(allowNegative).toBe(true);
|
|
93
|
+
expect(opts.referenceId).toMatch(new RegExp(`^phone-billing:PN-abc123:${expectedMonth}$`));
|
|
94
|
+
expect(opts.allowNegative).toBe(true);
|
|
98
95
|
// Verify meter emission
|
|
99
96
|
expect(meter.emit).toHaveBeenCalledOnce();
|
|
100
97
|
const event = meter.emit.mock.calls[0][0];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { Credit } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import type { IBotInstanceRepository } from "../../fleet/bot-instance-repository.js";
|
|
4
4
|
/**
|
|
@@ -13,7 +13,7 @@ export type GetActiveBotCount = (tenantId: string) => number | Promise<number>;
|
|
|
13
13
|
/** Low balance threshold ($1.00 = 20% of signup grant). */
|
|
14
14
|
export declare const LOW_BALANCE_THRESHOLD: Credit;
|
|
15
15
|
export interface RuntimeCronConfig {
|
|
16
|
-
ledger:
|
|
16
|
+
ledger: ILedger;
|
|
17
17
|
getActiveBotCount: GetActiveBotCount;
|
|
18
18
|
/** The date being billed, as YYYY-MM-DD. Used for idempotency. */
|
|
19
19
|
date: string;
|
|
@@ -69,17 +69,26 @@ export async function runRuntimeDeductions(cfg) {
|
|
|
69
69
|
const totalCost = DAILY_BOT_COST.multiply(botCount);
|
|
70
70
|
if (!balance.lessThan(totalCost)) {
|
|
71
71
|
// Full deduction
|
|
72
|
-
await cfg.ledger.debit(tenantId, totalCost, "bot_runtime",
|
|
72
|
+
await cfg.ledger.debit(tenantId, totalCost, "bot_runtime", {
|
|
73
|
+
description: `Daily runtime: ${botCount} bot(s) x $${DAILY_BOT_COST.toDollars().toFixed(2)}`,
|
|
74
|
+
referenceId: runtimeRef,
|
|
75
|
+
});
|
|
73
76
|
// Debit resource tier surcharges (if any)
|
|
74
77
|
if (cfg.getResourceTierCosts) {
|
|
75
78
|
const tierCost = await cfg.getResourceTierCosts(tenantId);
|
|
76
79
|
if (!tierCost.isZero()) {
|
|
77
80
|
const balanceAfterRuntime = await cfg.ledger.balance(tenantId);
|
|
78
81
|
if (!balanceAfterRuntime.lessThan(tierCost)) {
|
|
79
|
-
await cfg.ledger.debit(tenantId, tierCost, "resource_upgrade",
|
|
82
|
+
await cfg.ledger.debit(tenantId, tierCost, "resource_upgrade", {
|
|
83
|
+
description: "Daily resource tier surcharge",
|
|
84
|
+
referenceId: `runtime-tier:${cfg.date}:${tenantId}`,
|
|
85
|
+
});
|
|
80
86
|
}
|
|
81
87
|
else if (balanceAfterRuntime.greaterThan(Credit.ZERO)) {
|
|
82
|
-
await cfg.ledger.debit(tenantId, balanceAfterRuntime, "resource_upgrade",
|
|
88
|
+
await cfg.ledger.debit(tenantId, balanceAfterRuntime, "resource_upgrade", {
|
|
89
|
+
description: "Partial resource tier surcharge (balance exhausted)",
|
|
90
|
+
referenceId: `runtime-tier:${cfg.date}:${tenantId}`,
|
|
91
|
+
});
|
|
83
92
|
}
|
|
84
93
|
}
|
|
85
94
|
}
|
|
@@ -110,12 +119,18 @@ export async function runRuntimeDeductions(cfg) {
|
|
|
110
119
|
if (!storageCost.isZero()) {
|
|
111
120
|
const currentBalance = await cfg.ledger.balance(tenantId);
|
|
112
121
|
if (!currentBalance.lessThan(storageCost)) {
|
|
113
|
-
await cfg.ledger.debit(tenantId, storageCost, "storage_upgrade",
|
|
122
|
+
await cfg.ledger.debit(tenantId, storageCost, "storage_upgrade", {
|
|
123
|
+
description: "Daily storage tier surcharge",
|
|
124
|
+
referenceId: `runtime-storage:${cfg.date}:${tenantId}`,
|
|
125
|
+
});
|
|
114
126
|
}
|
|
115
127
|
else {
|
|
116
128
|
// Partial debit — take what's left, then suspend
|
|
117
129
|
if (currentBalance.greaterThan(Credit.ZERO)) {
|
|
118
|
-
await cfg.ledger.debit(tenantId, currentBalance, "storage_upgrade",
|
|
130
|
+
await cfg.ledger.debit(tenantId, currentBalance, "storage_upgrade", {
|
|
131
|
+
description: "Partial storage tier surcharge (balance exhausted)",
|
|
132
|
+
referenceId: `runtime-storage:${cfg.date}:${tenantId}`,
|
|
133
|
+
});
|
|
119
134
|
}
|
|
120
135
|
if (!result.suspended.includes(tenantId)) {
|
|
121
136
|
result.suspended.push(tenantId);
|
|
@@ -131,12 +146,18 @@ export async function runRuntimeDeductions(cfg) {
|
|
|
131
146
|
if (!addonCost.isZero()) {
|
|
132
147
|
const currentBalance = await cfg.ledger.balance(tenantId);
|
|
133
148
|
if (!currentBalance.lessThan(addonCost)) {
|
|
134
|
-
await cfg.ledger.debit(tenantId, addonCost, "addon",
|
|
149
|
+
await cfg.ledger.debit(tenantId, addonCost, "addon", {
|
|
150
|
+
description: "Daily infrastructure add-on charges",
|
|
151
|
+
referenceId: `runtime-addon:${cfg.date}:${tenantId}`,
|
|
152
|
+
});
|
|
135
153
|
}
|
|
136
154
|
else {
|
|
137
155
|
// Partial debit — take what's left, then suspend
|
|
138
156
|
if (currentBalance.greaterThan(Credit.ZERO)) {
|
|
139
|
-
await cfg.ledger.debit(tenantId, currentBalance, "addon",
|
|
157
|
+
await cfg.ledger.debit(tenantId, currentBalance, "addon", {
|
|
158
|
+
description: "Partial add-on charges (balance exhausted)",
|
|
159
|
+
referenceId: `runtime-addon:${cfg.date}:${tenantId}`,
|
|
160
|
+
});
|
|
140
161
|
}
|
|
141
162
|
if (!result.suspended.includes(tenantId)) {
|
|
142
163
|
result.suspended.push(tenantId);
|
|
@@ -150,7 +171,10 @@ export async function runRuntimeDeductions(cfg) {
|
|
|
150
171
|
else {
|
|
151
172
|
// Partial deduction — debit remaining balance, then suspend
|
|
152
173
|
if (balance.greaterThan(Credit.ZERO)) {
|
|
153
|
-
await cfg.ledger.debit(tenantId, balance, "bot_runtime",
|
|
174
|
+
await cfg.ledger.debit(tenantId, balance, "bot_runtime", {
|
|
175
|
+
description: `Partial daily runtime (balance exhausted): ${botCount} bot(s)`,
|
|
176
|
+
referenceId: runtimeRef,
|
|
177
|
+
});
|
|
154
178
|
}
|
|
155
179
|
if (cfg.onCreditsExhausted) {
|
|
156
180
|
await cfg.onCreditsExhausted(tenantId);
|
|
@@ -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";
|