@wopr-network/platform-core 1.74.0 → 1.74.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/dist/fleet/drizzle-bot-instance-repository.d.ts +3 -1
- package/dist/fleet/drizzle-bot-instance-repository.js +8 -2
- package/dist/fleet/instance.d.ts +8 -0
- package/dist/fleet/instance.js +20 -0
- package/dist/fleet/repository-types.d.ts +1 -1
- package/dist/monetization/credits/bot-billing.test.js +35 -1
- package/dist/monetization/credits/runtime-cron.d.ts +10 -2
- package/dist/monetization/credits/runtime-cron.js +19 -4
- package/dist/monetization/credits/runtime-cron.test.js +49 -34
- package/dist/monetization/credits/storage-tier-billing.test.js +9 -1
- package/dist/monetization/credits/storage-tier-cron.test.js +13 -7
- package/package.json +1 -1
- package/src/fleet/drizzle-bot-instance-repository.ts +15 -2
- package/src/fleet/instance.ts +21 -0
- package/src/fleet/repository-types.ts +1 -1
- package/src/monetization/credits/bot-billing.test.ts +35 -1
- package/src/monetization/credits/runtime-cron.test.ts +51 -38
- package/src/monetization/credits/runtime-cron.ts +21 -4
- package/src/monetization/credits/storage-tier-billing.test.ts +9 -1
- package/src/monetization/credits/storage-tier-cron.test.ts +13 -7
|
@@ -4,9 +4,25 @@ import { logger } from "../../config/logger.js";
|
|
|
4
4
|
import type { IBotInstanceRepository } from "../../fleet/bot-instance-repository.js";
|
|
5
5
|
import { RESOURCE_TIERS } from "../../fleet/resource-tiers.js";
|
|
6
6
|
|
|
7
|
+
/** Monthly bot cost in dollars. */
|
|
8
|
+
export const MONTHLY_BOT_COST_DOLLARS = 5;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Compute the daily bot cost for a given date, prorated by the actual
|
|
12
|
+
* number of days in that month. Uses nano-dollar precision so totals
|
|
13
|
+
* sum to exactly $5.00/month (no over/under-billing).
|
|
14
|
+
*/
|
|
15
|
+
export function dailyBotCost(date: string): Credit {
|
|
16
|
+
const d = new Date(date);
|
|
17
|
+
const year = d.getFullYear();
|
|
18
|
+
const month = d.getMonth();
|
|
19
|
+
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
20
|
+
return Credit.fromDollars(MONTHLY_BOT_COST_DOLLARS / daysInMonth);
|
|
21
|
+
}
|
|
22
|
+
|
|
7
23
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
24
|
+
* @deprecated Use dailyBotCost(date) for accurate per-month proration.
|
|
25
|
+
* Kept for backwards compat in tests.
|
|
10
26
|
*/
|
|
11
27
|
export const DAILY_BOT_COST = Credit.fromCents(17);
|
|
12
28
|
|
|
@@ -118,7 +134,8 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
|
|
|
118
134
|
continue;
|
|
119
135
|
}
|
|
120
136
|
|
|
121
|
-
const
|
|
137
|
+
const dailyCost = dailyBotCost(cfg.date);
|
|
138
|
+
const totalCost = dailyCost.multiply(botCount);
|
|
122
139
|
let didBillAnything = false;
|
|
123
140
|
|
|
124
141
|
// Bill runtime debit (skipped if already billed on a previous run)
|
|
@@ -126,7 +143,7 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
|
|
|
126
143
|
if (!balance.lessThan(totalCost)) {
|
|
127
144
|
// Full deduction
|
|
128
145
|
await cfg.ledger.debit(tenantId, totalCost, "bot_runtime", {
|
|
129
|
-
description: `Daily runtime: ${botCount} bot(s) x $${
|
|
146
|
+
description: `Daily runtime: ${botCount} bot(s) x $${dailyCost.toDollars().toFixed(4)}`,
|
|
130
147
|
referenceId: runtimeRef,
|
|
131
148
|
});
|
|
132
149
|
} else {
|
|
@@ -8,6 +8,7 @@ import { DrizzleBotBilling } from "./bot-billing.js";
|
|
|
8
8
|
describe("bot-billing storage tier", () => {
|
|
9
9
|
let pool: PGlite;
|
|
10
10
|
let db: DrizzleDb;
|
|
11
|
+
let repo: DrizzleBotInstanceRepository;
|
|
11
12
|
let billing: DrizzleBotBilling;
|
|
12
13
|
|
|
13
14
|
beforeAll(async () => {
|
|
@@ -22,16 +23,19 @@ describe("bot-billing storage tier", () => {
|
|
|
22
23
|
|
|
23
24
|
beforeEach(async () => {
|
|
24
25
|
await rollbackTestTransaction(pool);
|
|
25
|
-
|
|
26
|
+
repo = new DrizzleBotInstanceRepository(db);
|
|
27
|
+
billing = new DrizzleBotBilling(repo);
|
|
26
28
|
});
|
|
27
29
|
|
|
28
30
|
it("new bot defaults to standard storage tier", async () => {
|
|
29
31
|
await billing.registerBot("bot-1", "tenant-1", "TestBot");
|
|
32
|
+
await repo.startBilling("bot-1");
|
|
30
33
|
expect(await billing.getStorageTier("bot-1")).toBe("standard");
|
|
31
34
|
});
|
|
32
35
|
|
|
33
36
|
it("setStorageTier updates tier", async () => {
|
|
34
37
|
await billing.registerBot("bot-1", "tenant-1", "TestBot");
|
|
38
|
+
await repo.startBilling("bot-1");
|
|
35
39
|
await billing.setStorageTier("bot-1", "pro");
|
|
36
40
|
expect(await billing.getStorageTier("bot-1")).toBe("pro");
|
|
37
41
|
});
|
|
@@ -42,8 +46,11 @@ describe("bot-billing storage tier", () => {
|
|
|
42
46
|
|
|
43
47
|
it("getStorageTierCostsForTenant sums active bot storage costs", async () => {
|
|
44
48
|
await billing.registerBot("bot-1", "tenant-1", "Bot1");
|
|
49
|
+
await repo.startBilling("bot-1");
|
|
45
50
|
await billing.registerBot("bot-2", "tenant-1", "Bot2");
|
|
51
|
+
await repo.startBilling("bot-2");
|
|
46
52
|
await billing.registerBot("bot-3", "tenant-1", "Bot3");
|
|
53
|
+
await repo.startBilling("bot-3");
|
|
47
54
|
await billing.setStorageTier("bot-1", "plus"); // 3 credits/day
|
|
48
55
|
await billing.setStorageTier("bot-2", "max"); // 15 credits/day
|
|
49
56
|
// bot-3 stays standard // 0 credits/day
|
|
@@ -52,6 +59,7 @@ describe("bot-billing storage tier", () => {
|
|
|
52
59
|
|
|
53
60
|
it("getStorageTierCostsForTenant excludes suspended bots", async () => {
|
|
54
61
|
await billing.registerBot("bot-1", "tenant-1", "Bot1");
|
|
62
|
+
await repo.startBilling("bot-1");
|
|
55
63
|
await billing.setStorageTier("bot-1", "pro"); // 8 credits/day
|
|
56
64
|
await billing.suspendBot("bot-1");
|
|
57
65
|
expect((await billing.getStorageTierCostsForTenant("tenant-1")).toCents()).toBe(0);
|
|
@@ -3,10 +3,11 @@ import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
|
3
3
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
4
4
|
import type { DrizzleDb } from "../../db/index.js";
|
|
5
5
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
6
|
-
import { runRuntimeDeductions } from "./runtime-cron.js";
|
|
6
|
+
import { dailyBotCost, runRuntimeDeductions } from "./runtime-cron.js";
|
|
7
7
|
|
|
8
8
|
describe("runtime cron with storage tiers", () => {
|
|
9
9
|
const TODAY = "2025-01-01";
|
|
10
|
+
const BASE_COST_CREDIT = dailyBotCost(TODAY);
|
|
10
11
|
let pool: PGlite;
|
|
11
12
|
let db: DrizzleDb;
|
|
12
13
|
let ledger: DrizzleLedger;
|
|
@@ -36,8 +37,9 @@ describe("runtime cron with storage tiers", () => {
|
|
|
36
37
|
});
|
|
37
38
|
expect(result.processed).toBe(1);
|
|
38
39
|
const balance = await ledger.balance("t1");
|
|
39
|
-
// 1000 -
|
|
40
|
-
|
|
40
|
+
// 1000 - dailyBotCost (base) - 8 (pro storage surcharge)
|
|
41
|
+
const expected = Credit.fromCents(1000).subtract(BASE_COST_CREDIT).subtract(Credit.fromCents(8));
|
|
42
|
+
expect(balance.toCents()).toBe(expected.toCents());
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
it("debits only base cost for standard storage tier (zero surcharge)", async () => {
|
|
@@ -49,7 +51,8 @@ describe("runtime cron with storage tiers", () => {
|
|
|
49
51
|
getStorageTierCosts: async () => Credit.ZERO,
|
|
50
52
|
});
|
|
51
53
|
expect(result.processed).toBe(1);
|
|
52
|
-
|
|
54
|
+
const expectedStd = Credit.fromCents(1000).subtract(BASE_COST_CREDIT);
|
|
55
|
+
expect((await ledger.balance("t1")).toCents()).toBe(expectedStd.toCents());
|
|
53
56
|
});
|
|
54
57
|
|
|
55
58
|
it("skips storage surcharge when callback not provided (backward compat)", async () => {
|
|
@@ -60,11 +63,14 @@ describe("runtime cron with storage tiers", () => {
|
|
|
60
63
|
getActiveBotCount: async () => 1,
|
|
61
64
|
});
|
|
62
65
|
expect(result.processed).toBe(1);
|
|
63
|
-
|
|
66
|
+
const expectedBackcompat = Credit.fromCents(1000).subtract(BASE_COST_CREDIT);
|
|
67
|
+
expect((await ledger.balance("t1")).toCents()).toBe(expectedBackcompat.toCents());
|
|
64
68
|
});
|
|
65
69
|
|
|
66
70
|
it("suspends tenant when storage surcharge exhausts remaining balance", async () => {
|
|
67
|
-
|
|
71
|
+
// Seed just enough for base cost + 3 cents, so storage surcharge (8) exceeds remainder
|
|
72
|
+
const seed = BASE_COST_CREDIT.add(Credit.fromCents(3));
|
|
73
|
+
await ledger.credit("t1", seed, "purchase");
|
|
68
74
|
const suspended: string[] = [];
|
|
69
75
|
const result = await runRuntimeDeductions({
|
|
70
76
|
ledger,
|
|
@@ -75,7 +81,7 @@ describe("runtime cron with storage tiers", () => {
|
|
|
75
81
|
suspended.push(tenantId);
|
|
76
82
|
},
|
|
77
83
|
});
|
|
78
|
-
//
|
|
84
|
+
// seed - BASE_COST = 3 remaining, then 8 surcharge > 3, so partial debit + suspend
|
|
79
85
|
expect(result.processed).toBe(1);
|
|
80
86
|
expect(result.suspended).toContain("t1");
|
|
81
87
|
expect((await ledger.balance("t1")).toCents()).toBe(0);
|