@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.
@@ -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
- * Bot runtime cost: $5/bot/month prorated daily.
9
- * $5.00 / 30 $0.1667/day, rounded to $0.17.
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 totalCost = DAILY_BOT_COST.multiply(botCount);
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 $${DAILY_BOT_COST.toDollars().toFixed(2)}`,
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
- billing = new DrizzleBotBilling(new DrizzleBotInstanceRepository(db));
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 - 17 (base) - 8 (pro storage surcharge) = 975
40
- expect(balance.toCents()).toBe(975);
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
- expect((await ledger.balance("t1")).toCents()).toBe(983); // 1000 - 17
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
- expect((await ledger.balance("t1")).toCents()).toBe(983); // 1000 - 17
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
- await ledger.credit("t1", Credit.fromCents(20), "purchase"); // Only 20 cents
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
- // 20 - 17 = 3 remaining, then 8 surcharge > 3, so partial debit + suspend
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);