@wopr-network/platform-core 1.39.2 → 1.39.4
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/credits/credit.js +15 -3
- package/dist/credits/credit.test.js +32 -0
- package/dist/monetization/credits/runtime-cron.js +78 -58
- package/dist/monetization/credits/runtime-cron.test.js +106 -0
- package/package.json +1 -1
- package/src/credits/credit.test.ts +39 -0
- package/src/credits/credit.ts +15 -3
- package/src/monetization/credits/runtime-cron.test.ts +126 -0
- package/src/monetization/credits/runtime-cron.ts +81 -65
- package/vitest.config.ts +9 -0
package/dist/credits/credit.js
CHANGED
|
@@ -91,15 +91,27 @@ export class Credit {
|
|
|
91
91
|
}
|
|
92
92
|
/** Add another Credit, returning a new Credit. */
|
|
93
93
|
add(other) {
|
|
94
|
-
|
|
94
|
+
const result = this.raw + other.raw;
|
|
95
|
+
if (!Number.isSafeInteger(result)) {
|
|
96
|
+
throw new RangeError(`Credit.add overflow: ${this.raw} + ${other.raw} = ${result}`);
|
|
97
|
+
}
|
|
98
|
+
return new Credit(result);
|
|
95
99
|
}
|
|
96
100
|
/** Subtract another Credit, returning a new Credit (may be negative). */
|
|
97
101
|
subtract(other) {
|
|
98
|
-
|
|
102
|
+
const result = this.raw - other.raw;
|
|
103
|
+
if (!Number.isSafeInteger(result)) {
|
|
104
|
+
throw new RangeError(`Credit.subtract overflow: ${this.raw} - ${other.raw} = ${result}`);
|
|
105
|
+
}
|
|
106
|
+
return new Credit(result);
|
|
99
107
|
}
|
|
100
108
|
/** Multiply by a factor, rounding to nearest raw unit. */
|
|
101
109
|
multiply(factor) {
|
|
102
|
-
|
|
110
|
+
const result = Math.round(this.raw * factor);
|
|
111
|
+
if (!Number.isSafeInteger(result)) {
|
|
112
|
+
throw new RangeError(`Credit.multiply overflow: ${this.raw} * ${factor} = ${result}`);
|
|
113
|
+
}
|
|
114
|
+
return new Credit(result);
|
|
103
115
|
}
|
|
104
116
|
/** True if this credit is negative. */
|
|
105
117
|
isNegative() {
|
|
@@ -103,6 +103,38 @@ describe("Credit", () => {
|
|
|
103
103
|
it("multiply by zero gives zero", () => {
|
|
104
104
|
expect(Credit.fromDollars(5).multiply(0).isZero()).toBe(true);
|
|
105
105
|
});
|
|
106
|
+
it("add throws RangeError on positive overflow", () => {
|
|
107
|
+
const a = Credit.fromRaw(Number.MAX_SAFE_INTEGER);
|
|
108
|
+
const b = Credit.fromRaw(1);
|
|
109
|
+
expect(() => a.add(b)).toThrow(RangeError);
|
|
110
|
+
});
|
|
111
|
+
it("subtract throws RangeError on negative overflow", () => {
|
|
112
|
+
const a = Credit.fromRaw(-Number.MAX_SAFE_INTEGER);
|
|
113
|
+
const b = Credit.fromRaw(1);
|
|
114
|
+
expect(() => a.subtract(b)).toThrow(RangeError);
|
|
115
|
+
});
|
|
116
|
+
it("multiply throws RangeError on overflow", () => {
|
|
117
|
+
const a = Credit.fromRaw(Number.MAX_SAFE_INTEGER);
|
|
118
|
+
expect(() => a.multiply(2)).toThrow(RangeError);
|
|
119
|
+
});
|
|
120
|
+
it("multiply throws RangeError on Infinity factor", () => {
|
|
121
|
+
const a = Credit.fromRaw(1);
|
|
122
|
+
expect(() => a.multiply(Infinity)).toThrow(RangeError);
|
|
123
|
+
});
|
|
124
|
+
it("multiply throws RangeError on NaN factor", () => {
|
|
125
|
+
const a = Credit.fromRaw(1);
|
|
126
|
+
expect(() => a.multiply(NaN)).toThrow(RangeError);
|
|
127
|
+
});
|
|
128
|
+
it("add does not throw for values within safe range", () => {
|
|
129
|
+
const a = Credit.fromRaw(Number.MAX_SAFE_INTEGER - 1);
|
|
130
|
+
const b = Credit.fromRaw(1);
|
|
131
|
+
expect(a.add(b).toRaw()).toBe(Number.MAX_SAFE_INTEGER);
|
|
132
|
+
});
|
|
133
|
+
it("subtract does not throw for values within safe range", () => {
|
|
134
|
+
const a = Credit.fromRaw(-(Number.MAX_SAFE_INTEGER - 1));
|
|
135
|
+
const b = Credit.fromRaw(1);
|
|
136
|
+
expect(a.subtract(b).toRaw()).toBe(-Number.MAX_SAFE_INTEGER);
|
|
137
|
+
});
|
|
106
138
|
});
|
|
107
139
|
describe("comparison", () => {
|
|
108
140
|
it("isNegative returns true for negative", () => {
|
|
@@ -59,69 +59,95 @@ export async function runRuntimeDeductions(cfg) {
|
|
|
59
59
|
for (const { tenantId, balance } of tenants) {
|
|
60
60
|
try {
|
|
61
61
|
const runtimeRef = `runtime:${cfg.date}:${tenantId}`;
|
|
62
|
-
|
|
63
|
-
result.skipped.push(tenantId);
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
62
|
+
const runtimeAlreadyBilled = await cfg.ledger.hasReferenceId(runtimeRef);
|
|
66
63
|
const botCount = await cfg.getActiveBotCount(tenantId);
|
|
67
|
-
if (botCount <= 0)
|
|
64
|
+
if (botCount <= 0) {
|
|
65
|
+
if (runtimeAlreadyBilled)
|
|
66
|
+
result.skipped.push(tenantId);
|
|
68
67
|
continue;
|
|
68
|
+
}
|
|
69
69
|
const totalCost = DAILY_BOT_COST.multiply(botCount);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
let didBillAnything = false;
|
|
71
|
+
// Bill runtime debit (skipped if already billed on a previous run)
|
|
72
|
+
if (!runtimeAlreadyBilled) {
|
|
73
|
+
if (!balance.lessThan(totalCost)) {
|
|
74
|
+
// Full deduction
|
|
75
|
+
await cfg.ledger.debit(tenantId, totalCost, "bot_runtime", {
|
|
76
|
+
description: `Daily runtime: ${botCount} bot(s) x $${DAILY_BOT_COST.toDollars().toFixed(2)}`,
|
|
77
|
+
referenceId: runtimeRef,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Partial deduction — balance insufficient to cover full cost; debit what's available and suspend
|
|
82
|
+
if (balance.greaterThan(Credit.ZERO)) {
|
|
83
|
+
await cfg.ledger.debit(tenantId, balance, "bot_runtime", {
|
|
84
|
+
description: `Partial daily runtime (balance exhausted): ${botCount} bot(s)`,
|
|
85
|
+
referenceId: runtimeRef,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (!result.suspended.includes(tenantId)) {
|
|
89
|
+
result.suspended.push(tenantId);
|
|
90
|
+
if (cfg.onSuspend)
|
|
91
|
+
await cfg.onSuspend(tenantId);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
didBillAnything = true;
|
|
95
|
+
}
|
|
96
|
+
// Debit resource tier surcharges (if any) — independent idempotency
|
|
97
|
+
if (cfg.getResourceTierCosts) {
|
|
98
|
+
const tierRef = `runtime-tier:${cfg.date}:${tenantId}`;
|
|
99
|
+
if (!(await cfg.ledger.hasReferenceId(tierRef))) {
|
|
78
100
|
const tierCost = await cfg.getResourceTierCosts(tenantId);
|
|
79
101
|
if (!tierCost.isZero()) {
|
|
80
102
|
const balanceAfterRuntime = await cfg.ledger.balance(tenantId);
|
|
81
103
|
if (!balanceAfterRuntime.lessThan(tierCost)) {
|
|
82
104
|
await cfg.ledger.debit(tenantId, tierCost, "resource_upgrade", {
|
|
83
105
|
description: "Daily resource tier surcharge",
|
|
84
|
-
referenceId:
|
|
106
|
+
referenceId: tierRef,
|
|
85
107
|
});
|
|
86
108
|
}
|
|
87
109
|
else if (balanceAfterRuntime.greaterThan(Credit.ZERO)) {
|
|
88
110
|
await cfg.ledger.debit(tenantId, balanceAfterRuntime, "resource_upgrade", {
|
|
89
111
|
description: "Partial resource tier surcharge (balance exhausted)",
|
|
90
|
-
referenceId:
|
|
112
|
+
referenceId: tierRef,
|
|
91
113
|
});
|
|
92
114
|
}
|
|
115
|
+
didBillAnything = true;
|
|
93
116
|
}
|
|
94
117
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
}
|
|
119
|
+
const newBalance = await cfg.ledger.balance(tenantId);
|
|
120
|
+
// Fire onLowBalance if balance crossed below threshold from above
|
|
121
|
+
if (newBalance.greaterThan(Credit.ZERO) &&
|
|
122
|
+
!newBalance.greaterThan(LOW_BALANCE_THRESHOLD) &&
|
|
123
|
+
balance.greaterThan(LOW_BALANCE_THRESHOLD) &&
|
|
124
|
+
cfg.onLowBalance) {
|
|
125
|
+
await cfg.onLowBalance(tenantId, newBalance);
|
|
126
|
+
}
|
|
127
|
+
// Fire onCreditsExhausted if balance just hit 0
|
|
128
|
+
if (!newBalance.greaterThan(Credit.ZERO) && balance.greaterThan(Credit.ZERO) && cfg.onCreditsExhausted) {
|
|
129
|
+
await cfg.onCreditsExhausted(tenantId);
|
|
130
|
+
}
|
|
131
|
+
// Suspend tenant when balance hits zero (zero-crossing guard)
|
|
132
|
+
if (!newBalance.greaterThan(Credit.ZERO) &&
|
|
133
|
+
balance.greaterThan(Credit.ZERO) &&
|
|
134
|
+
!result.suspended.includes(tenantId)) {
|
|
135
|
+
result.suspended.push(tenantId);
|
|
136
|
+
if (cfg.onSuspend) {
|
|
137
|
+
await cfg.onSuspend(tenantId);
|
|
115
138
|
}
|
|
116
|
-
|
|
117
|
-
|
|
139
|
+
}
|
|
140
|
+
// Debit storage tier surcharges (if any) — independent idempotency
|
|
141
|
+
if (cfg.getStorageTierCosts) {
|
|
142
|
+
const storageRef = `runtime-storage:${cfg.date}:${tenantId}`;
|
|
143
|
+
if (!(await cfg.ledger.hasReferenceId(storageRef))) {
|
|
118
144
|
const storageCost = await cfg.getStorageTierCosts(tenantId);
|
|
119
145
|
if (!storageCost.isZero()) {
|
|
120
146
|
const currentBalance = await cfg.ledger.balance(tenantId);
|
|
121
147
|
if (!currentBalance.lessThan(storageCost)) {
|
|
122
148
|
await cfg.ledger.debit(tenantId, storageCost, "storage_upgrade", {
|
|
123
149
|
description: "Daily storage tier surcharge",
|
|
124
|
-
referenceId:
|
|
150
|
+
referenceId: storageRef,
|
|
125
151
|
});
|
|
126
152
|
}
|
|
127
153
|
else {
|
|
@@ -129,7 +155,7 @@ export async function runRuntimeDeductions(cfg) {
|
|
|
129
155
|
if (currentBalance.greaterThan(Credit.ZERO)) {
|
|
130
156
|
await cfg.ledger.debit(tenantId, currentBalance, "storage_upgrade", {
|
|
131
157
|
description: "Partial storage tier surcharge (balance exhausted)",
|
|
132
|
-
referenceId:
|
|
158
|
+
referenceId: storageRef,
|
|
133
159
|
});
|
|
134
160
|
}
|
|
135
161
|
if (!result.suspended.includes(tenantId)) {
|
|
@@ -138,17 +164,21 @@ export async function runRuntimeDeductions(cfg) {
|
|
|
138
164
|
await cfg.onSuspend(tenantId);
|
|
139
165
|
}
|
|
140
166
|
}
|
|
167
|
+
didBillAnything = true;
|
|
141
168
|
}
|
|
142
169
|
}
|
|
143
|
-
|
|
144
|
-
|
|
170
|
+
}
|
|
171
|
+
// Debit infrastructure add-on costs (if any) — independent idempotency
|
|
172
|
+
if (cfg.getAddonCosts) {
|
|
173
|
+
const addonRef = `runtime-addon:${cfg.date}:${tenantId}`;
|
|
174
|
+
if (!(await cfg.ledger.hasReferenceId(addonRef))) {
|
|
145
175
|
const addonCost = await cfg.getAddonCosts(tenantId);
|
|
146
176
|
if (!addonCost.isZero()) {
|
|
147
177
|
const currentBalance = await cfg.ledger.balance(tenantId);
|
|
148
178
|
if (!currentBalance.lessThan(addonCost)) {
|
|
149
179
|
await cfg.ledger.debit(tenantId, addonCost, "addon", {
|
|
150
180
|
description: "Daily infrastructure add-on charges",
|
|
151
|
-
referenceId:
|
|
181
|
+
referenceId: addonRef,
|
|
152
182
|
});
|
|
153
183
|
}
|
|
154
184
|
else {
|
|
@@ -156,7 +186,7 @@ export async function runRuntimeDeductions(cfg) {
|
|
|
156
186
|
if (currentBalance.greaterThan(Credit.ZERO)) {
|
|
157
187
|
await cfg.ledger.debit(tenantId, currentBalance, "addon", {
|
|
158
188
|
description: "Partial add-on charges (balance exhausted)",
|
|
159
|
-
referenceId:
|
|
189
|
+
referenceId: addonRef,
|
|
160
190
|
});
|
|
161
191
|
}
|
|
162
192
|
if (!result.suspended.includes(tenantId)) {
|
|
@@ -165,26 +195,16 @@ export async function runRuntimeDeductions(cfg) {
|
|
|
165
195
|
await cfg.onSuspend(tenantId);
|
|
166
196
|
}
|
|
167
197
|
}
|
|
198
|
+
didBillAnything = true;
|
|
168
199
|
}
|
|
169
200
|
}
|
|
170
201
|
}
|
|
202
|
+
if (didBillAnything) {
|
|
203
|
+
result.processed++;
|
|
204
|
+
}
|
|
171
205
|
else {
|
|
172
|
-
|
|
173
|
-
if (balance.greaterThan(Credit.ZERO)) {
|
|
174
|
-
await cfg.ledger.debit(tenantId, balance, "bot_runtime", {
|
|
175
|
-
description: `Partial daily runtime (balance exhausted): ${botCount} bot(s)`,
|
|
176
|
-
referenceId: runtimeRef,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
if (cfg.onCreditsExhausted) {
|
|
180
|
-
await cfg.onCreditsExhausted(tenantId);
|
|
181
|
-
}
|
|
182
|
-
result.suspended.push(tenantId);
|
|
183
|
-
if (cfg.onSuspend) {
|
|
184
|
-
await cfg.onSuspend(tenantId);
|
|
185
|
-
}
|
|
206
|
+
result.skipped.push(tenantId);
|
|
186
207
|
}
|
|
187
|
-
result.processed++;
|
|
188
208
|
}
|
|
189
209
|
catch (err) {
|
|
190
210
|
if (err instanceof InsufficientBalanceError) {
|
|
@@ -314,4 +314,110 @@ describe("runRuntimeDeductions", () => {
|
|
|
314
314
|
// Balance unchanged after second run
|
|
315
315
|
expect((await ledger.balance("tenant-1")).toCents()).toBe(500 - 17);
|
|
316
316
|
});
|
|
317
|
+
it("bills surcharges on retry when runtime was already billed (crash recovery)", async () => {
|
|
318
|
+
// Setup: tenant with enough balance for runtime + tier + storage + addon
|
|
319
|
+
await ledger.credit("tenant-1", Credit.fromCents(1000), "purchase", { description: "top-up" });
|
|
320
|
+
const cfg = {
|
|
321
|
+
ledger,
|
|
322
|
+
date: "2025-07-01",
|
|
323
|
+
getActiveBotCount: async () => 1,
|
|
324
|
+
getResourceTierCosts: async () => Credit.fromCents(10),
|
|
325
|
+
getStorageTierCosts: async () => Credit.fromCents(8),
|
|
326
|
+
getAddonCosts: async () => Credit.fromCents(5),
|
|
327
|
+
};
|
|
328
|
+
// First run — bills everything
|
|
329
|
+
const first = await runRuntimeDeductions(cfg);
|
|
330
|
+
expect(first.processed).toBe(1);
|
|
331
|
+
// 1000 - 17 (runtime) - 10 (tier) - 8 (storage) - 5 (addon) = 960
|
|
332
|
+
expect((await ledger.balance("tenant-1")).toCents()).toBe(960);
|
|
333
|
+
// Second run — all already billed, should skip
|
|
334
|
+
const second = await runRuntimeDeductions(cfg);
|
|
335
|
+
expect(second.skipped).toContain("tenant-1");
|
|
336
|
+
expect(second.processed).toBe(0);
|
|
337
|
+
expect((await ledger.balance("tenant-1")).toCents()).toBe(960);
|
|
338
|
+
});
|
|
339
|
+
it("bills remaining surcharges when runtime was billed but surcharges were not (simulated crash)", async () => {
|
|
340
|
+
// Setup: tenant with enough balance
|
|
341
|
+
await ledger.credit("tenant-1", Credit.fromCents(1000), "purchase", { description: "top-up" });
|
|
342
|
+
// Simulate crash: manually debit only the runtime charge (as if the cron crashed after this)
|
|
343
|
+
await ledger.debit("tenant-1", DAILY_BOT_COST, "bot_runtime", {
|
|
344
|
+
description: "Daily runtime: 1 bot(s) x $0.17",
|
|
345
|
+
referenceId: `runtime:2025-07-02:tenant-1`,
|
|
346
|
+
});
|
|
347
|
+
expect((await ledger.balance("tenant-1")).toCents()).toBe(983); // 1000 - 17
|
|
348
|
+
// Retry run — runtime already billed, but surcharges should still be billed
|
|
349
|
+
const result = await runRuntimeDeductions({
|
|
350
|
+
ledger,
|
|
351
|
+
date: "2025-07-02",
|
|
352
|
+
getActiveBotCount: async () => 1,
|
|
353
|
+
getResourceTierCosts: async () => Credit.fromCents(10),
|
|
354
|
+
getStorageTierCosts: async () => Credit.fromCents(8),
|
|
355
|
+
getAddonCosts: async () => Credit.fromCents(5),
|
|
356
|
+
});
|
|
357
|
+
expect(result.processed).toBe(1);
|
|
358
|
+
expect(result.skipped).not.toContain("tenant-1");
|
|
359
|
+
// 983 - 10 (tier) - 8 (storage) - 5 (addon) = 960
|
|
360
|
+
expect((await ledger.balance("tenant-1")).toCents()).toBe(960);
|
|
361
|
+
});
|
|
362
|
+
it("bills only missing surcharges when some were already committed (simulated partial crash)", async () => {
|
|
363
|
+
await ledger.credit("tenant-1", Credit.fromCents(1000), "purchase", { description: "top-up" });
|
|
364
|
+
// Simulate: runtime + tier already billed, storage + addon not yet
|
|
365
|
+
await ledger.debit("tenant-1", DAILY_BOT_COST, "bot_runtime", {
|
|
366
|
+
description: "Daily runtime: 1 bot(s) x $0.17",
|
|
367
|
+
referenceId: `runtime:2025-07-03:tenant-1`,
|
|
368
|
+
});
|
|
369
|
+
await ledger.debit("tenant-1", Credit.fromCents(10), "resource_upgrade", {
|
|
370
|
+
description: "Daily resource tier surcharge",
|
|
371
|
+
referenceId: `runtime-tier:2025-07-03:tenant-1`,
|
|
372
|
+
});
|
|
373
|
+
expect((await ledger.balance("tenant-1")).toCents()).toBe(973); // 1000 - 17 - 10
|
|
374
|
+
const result = await runRuntimeDeductions({
|
|
375
|
+
ledger,
|
|
376
|
+
date: "2025-07-03",
|
|
377
|
+
getActiveBotCount: async () => 1,
|
|
378
|
+
getResourceTierCosts: async () => Credit.fromCents(10),
|
|
379
|
+
getStorageTierCosts: async () => Credit.fromCents(8),
|
|
380
|
+
getAddonCosts: async () => Credit.fromCents(5),
|
|
381
|
+
});
|
|
382
|
+
expect(result.processed).toBe(1);
|
|
383
|
+
// 973 - 8 (storage) - 5 (addon) = 960
|
|
384
|
+
expect((await ledger.balance("tenant-1")).toCents()).toBe(960);
|
|
385
|
+
});
|
|
386
|
+
it("does not double-debit runtime on retry after partial deduction + crash", async () => {
|
|
387
|
+
await ledger.credit("tenant-1", Credit.fromCents(10), "purchase", { description: "top-up" });
|
|
388
|
+
// Simulate: partial runtime debit already committed (balance was 10, cost was 17)
|
|
389
|
+
await ledger.debit("tenant-1", Credit.fromCents(10), "bot_runtime", {
|
|
390
|
+
description: "Partial daily runtime (balance exhausted): 1 bot(s)",
|
|
391
|
+
referenceId: `runtime:2025-07-04:tenant-1`,
|
|
392
|
+
allowNegative: true,
|
|
393
|
+
});
|
|
394
|
+
expect((await ledger.balance("tenant-1")).toCents()).toBe(0);
|
|
395
|
+
// Retry — runtime already billed, balance is 0, nothing should happen
|
|
396
|
+
const result = await runRuntimeDeductions({
|
|
397
|
+
ledger,
|
|
398
|
+
date: "2025-07-04",
|
|
399
|
+
getActiveBotCount: async () => 1,
|
|
400
|
+
});
|
|
401
|
+
// Tenant still has 0 balance (tenantsWithBalance returns only positive), so won't be processed
|
|
402
|
+
expect(result.processed).toBe(0);
|
|
403
|
+
expect((await ledger.balance("tenant-1")).toCents()).toBe(0);
|
|
404
|
+
});
|
|
405
|
+
it("trial balance remains balanced after crash-recovery billing", async () => {
|
|
406
|
+
await ledger.credit("tenant-1", Credit.fromCents(1000), "purchase", { description: "top-up" });
|
|
407
|
+
// Simulate crash: only runtime billed
|
|
408
|
+
await ledger.debit("tenant-1", DAILY_BOT_COST, "bot_runtime", {
|
|
409
|
+
description: "Daily runtime: 1 bot(s) x $0.17",
|
|
410
|
+
referenceId: `runtime:2025-07-05:tenant-1`,
|
|
411
|
+
});
|
|
412
|
+
// Retry — surcharges billed
|
|
413
|
+
await runRuntimeDeductions({
|
|
414
|
+
ledger,
|
|
415
|
+
date: "2025-07-05",
|
|
416
|
+
getActiveBotCount: async () => 1,
|
|
417
|
+
getResourceTierCosts: async () => Credit.fromCents(10),
|
|
418
|
+
getStorageTierCosts: async () => Credit.fromCents(8),
|
|
419
|
+
});
|
|
420
|
+
const tb = await ledger.trialBalance();
|
|
421
|
+
expect(tb.balanced).toBe(true);
|
|
422
|
+
});
|
|
317
423
|
});
|
package/package.json
CHANGED
|
@@ -124,6 +124,45 @@ describe("Credit", () => {
|
|
|
124
124
|
it("multiply by zero gives zero", () => {
|
|
125
125
|
expect(Credit.fromDollars(5).multiply(0).isZero()).toBe(true);
|
|
126
126
|
});
|
|
127
|
+
|
|
128
|
+
it("add throws RangeError on positive overflow", () => {
|
|
129
|
+
const a = Credit.fromRaw(Number.MAX_SAFE_INTEGER);
|
|
130
|
+
const b = Credit.fromRaw(1);
|
|
131
|
+
expect(() => a.add(b)).toThrow(RangeError);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("subtract throws RangeError on negative overflow", () => {
|
|
135
|
+
const a = Credit.fromRaw(-Number.MAX_SAFE_INTEGER);
|
|
136
|
+
const b = Credit.fromRaw(1);
|
|
137
|
+
expect(() => a.subtract(b)).toThrow(RangeError);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("multiply throws RangeError on overflow", () => {
|
|
141
|
+
const a = Credit.fromRaw(Number.MAX_SAFE_INTEGER);
|
|
142
|
+
expect(() => a.multiply(2)).toThrow(RangeError);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("multiply throws RangeError on Infinity factor", () => {
|
|
146
|
+
const a = Credit.fromRaw(1);
|
|
147
|
+
expect(() => a.multiply(Infinity)).toThrow(RangeError);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("multiply throws RangeError on NaN factor", () => {
|
|
151
|
+
const a = Credit.fromRaw(1);
|
|
152
|
+
expect(() => a.multiply(NaN)).toThrow(RangeError);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("add does not throw for values within safe range", () => {
|
|
156
|
+
const a = Credit.fromRaw(Number.MAX_SAFE_INTEGER - 1);
|
|
157
|
+
const b = Credit.fromRaw(1);
|
|
158
|
+
expect(a.add(b).toRaw()).toBe(Number.MAX_SAFE_INTEGER);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("subtract does not throw for values within safe range", () => {
|
|
162
|
+
const a = Credit.fromRaw(-(Number.MAX_SAFE_INTEGER - 1));
|
|
163
|
+
const b = Credit.fromRaw(1);
|
|
164
|
+
expect(a.subtract(b).toRaw()).toBe(-Number.MAX_SAFE_INTEGER);
|
|
165
|
+
});
|
|
127
166
|
});
|
|
128
167
|
|
|
129
168
|
describe("comparison", () => {
|
package/src/credits/credit.ts
CHANGED
|
@@ -101,17 +101,29 @@ export class Credit {
|
|
|
101
101
|
|
|
102
102
|
/** Add another Credit, returning a new Credit. */
|
|
103
103
|
add(other: Credit): Credit {
|
|
104
|
-
|
|
104
|
+
const result = this.raw + other.raw;
|
|
105
|
+
if (!Number.isSafeInteger(result)) {
|
|
106
|
+
throw new RangeError(`Credit.add overflow: ${this.raw} + ${other.raw} = ${result}`);
|
|
107
|
+
}
|
|
108
|
+
return new Credit(result);
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
/** Subtract another Credit, returning a new Credit (may be negative). */
|
|
108
112
|
subtract(other: Credit): Credit {
|
|
109
|
-
|
|
113
|
+
const result = this.raw - other.raw;
|
|
114
|
+
if (!Number.isSafeInteger(result)) {
|
|
115
|
+
throw new RangeError(`Credit.subtract overflow: ${this.raw} - ${other.raw} = ${result}`);
|
|
116
|
+
}
|
|
117
|
+
return new Credit(result);
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
/** Multiply by a factor, rounding to nearest raw unit. */
|
|
113
121
|
multiply(factor: number): Credit {
|
|
114
|
-
|
|
122
|
+
const result = Math.round(this.raw * factor);
|
|
123
|
+
if (!Number.isSafeInteger(result)) {
|
|
124
|
+
throw new RangeError(`Credit.multiply overflow: ${this.raw} * ${factor} = ${result}`);
|
|
125
|
+
}
|
|
126
|
+
return new Credit(result);
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
/** True if this credit is negative. */
|
|
@@ -356,4 +356,130 @@ describe("runRuntimeDeductions", () => {
|
|
|
356
356
|
// Balance unchanged after second run
|
|
357
357
|
expect((await ledger.balance("tenant-1")).toCents()).toBe(500 - 17);
|
|
358
358
|
});
|
|
359
|
+
|
|
360
|
+
it("bills surcharges on retry when runtime was already billed (crash recovery)", async () => {
|
|
361
|
+
// Setup: tenant with enough balance for runtime + tier + storage + addon
|
|
362
|
+
await ledger.credit("tenant-1", Credit.fromCents(1000), "purchase", { description: "top-up" });
|
|
363
|
+
|
|
364
|
+
const cfg = {
|
|
365
|
+
ledger,
|
|
366
|
+
date: "2025-07-01",
|
|
367
|
+
getActiveBotCount: async () => 1,
|
|
368
|
+
getResourceTierCosts: async () => Credit.fromCents(10),
|
|
369
|
+
getStorageTierCosts: async () => Credit.fromCents(8),
|
|
370
|
+
getAddonCosts: async () => Credit.fromCents(5),
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// First run — bills everything
|
|
374
|
+
const first = await runRuntimeDeductions(cfg);
|
|
375
|
+
expect(first.processed).toBe(1);
|
|
376
|
+
// 1000 - 17 (runtime) - 10 (tier) - 8 (storage) - 5 (addon) = 960
|
|
377
|
+
expect((await ledger.balance("tenant-1")).toCents()).toBe(960);
|
|
378
|
+
|
|
379
|
+
// Second run — all already billed, should skip
|
|
380
|
+
const second = await runRuntimeDeductions(cfg);
|
|
381
|
+
expect(second.skipped).toContain("tenant-1");
|
|
382
|
+
expect(second.processed).toBe(0);
|
|
383
|
+
expect((await ledger.balance("tenant-1")).toCents()).toBe(960);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("bills remaining surcharges when runtime was billed but surcharges were not (simulated crash)", async () => {
|
|
387
|
+
// Setup: tenant with enough balance
|
|
388
|
+
await ledger.credit("tenant-1", Credit.fromCents(1000), "purchase", { description: "top-up" });
|
|
389
|
+
|
|
390
|
+
// Simulate crash: manually debit only the runtime charge (as if the cron crashed after this)
|
|
391
|
+
await ledger.debit("tenant-1", DAILY_BOT_COST, "bot_runtime", {
|
|
392
|
+
description: "Daily runtime: 1 bot(s) x $0.17",
|
|
393
|
+
referenceId: `runtime:2025-07-02:tenant-1`,
|
|
394
|
+
});
|
|
395
|
+
expect((await ledger.balance("tenant-1")).toCents()).toBe(983); // 1000 - 17
|
|
396
|
+
|
|
397
|
+
// Retry run — runtime already billed, but surcharges should still be billed
|
|
398
|
+
const result = await runRuntimeDeductions({
|
|
399
|
+
ledger,
|
|
400
|
+
date: "2025-07-02",
|
|
401
|
+
getActiveBotCount: async () => 1,
|
|
402
|
+
getResourceTierCosts: async () => Credit.fromCents(10),
|
|
403
|
+
getStorageTierCosts: async () => Credit.fromCents(8),
|
|
404
|
+
getAddonCosts: async () => Credit.fromCents(5),
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
expect(result.processed).toBe(1);
|
|
408
|
+
expect(result.skipped).not.toContain("tenant-1");
|
|
409
|
+
// 983 - 10 (tier) - 8 (storage) - 5 (addon) = 960
|
|
410
|
+
expect((await ledger.balance("tenant-1")).toCents()).toBe(960);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("bills only missing surcharges when some were already committed (simulated partial crash)", async () => {
|
|
414
|
+
await ledger.credit("tenant-1", Credit.fromCents(1000), "purchase", { description: "top-up" });
|
|
415
|
+
|
|
416
|
+
// Simulate: runtime + tier already billed, storage + addon not yet
|
|
417
|
+
await ledger.debit("tenant-1", DAILY_BOT_COST, "bot_runtime", {
|
|
418
|
+
description: "Daily runtime: 1 bot(s) x $0.17",
|
|
419
|
+
referenceId: `runtime:2025-07-03:tenant-1`,
|
|
420
|
+
});
|
|
421
|
+
await ledger.debit("tenant-1", Credit.fromCents(10), "resource_upgrade", {
|
|
422
|
+
description: "Daily resource tier surcharge",
|
|
423
|
+
referenceId: `runtime-tier:2025-07-03:tenant-1`,
|
|
424
|
+
});
|
|
425
|
+
expect((await ledger.balance("tenant-1")).toCents()).toBe(973); // 1000 - 17 - 10
|
|
426
|
+
|
|
427
|
+
const result = await runRuntimeDeductions({
|
|
428
|
+
ledger,
|
|
429
|
+
date: "2025-07-03",
|
|
430
|
+
getActiveBotCount: async () => 1,
|
|
431
|
+
getResourceTierCosts: async () => Credit.fromCents(10),
|
|
432
|
+
getStorageTierCosts: async () => Credit.fromCents(8),
|
|
433
|
+
getAddonCosts: async () => Credit.fromCents(5),
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
expect(result.processed).toBe(1);
|
|
437
|
+
// 973 - 8 (storage) - 5 (addon) = 960
|
|
438
|
+
expect((await ledger.balance("tenant-1")).toCents()).toBe(960);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("does not double-debit runtime on retry after partial deduction + crash", async () => {
|
|
442
|
+
await ledger.credit("tenant-1", Credit.fromCents(10), "purchase", { description: "top-up" });
|
|
443
|
+
|
|
444
|
+
// Simulate: partial runtime debit already committed (balance was 10, cost was 17)
|
|
445
|
+
await ledger.debit("tenant-1", Credit.fromCents(10), "bot_runtime", {
|
|
446
|
+
description: "Partial daily runtime (balance exhausted): 1 bot(s)",
|
|
447
|
+
referenceId: `runtime:2025-07-04:tenant-1`,
|
|
448
|
+
allowNegative: true,
|
|
449
|
+
});
|
|
450
|
+
expect((await ledger.balance("tenant-1")).toCents()).toBe(0);
|
|
451
|
+
|
|
452
|
+
// Retry — runtime already billed, balance is 0, nothing should happen
|
|
453
|
+
const result = await runRuntimeDeductions({
|
|
454
|
+
ledger,
|
|
455
|
+
date: "2025-07-04",
|
|
456
|
+
getActiveBotCount: async () => 1,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Tenant still has 0 balance (tenantsWithBalance returns only positive), so won't be processed
|
|
460
|
+
expect(result.processed).toBe(0);
|
|
461
|
+
expect((await ledger.balance("tenant-1")).toCents()).toBe(0);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("trial balance remains balanced after crash-recovery billing", async () => {
|
|
465
|
+
await ledger.credit("tenant-1", Credit.fromCents(1000), "purchase", { description: "top-up" });
|
|
466
|
+
|
|
467
|
+
// Simulate crash: only runtime billed
|
|
468
|
+
await ledger.debit("tenant-1", DAILY_BOT_COST, "bot_runtime", {
|
|
469
|
+
description: "Daily runtime: 1 bot(s) x $0.17",
|
|
470
|
+
referenceId: `runtime:2025-07-05:tenant-1`,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Retry — surcharges billed
|
|
474
|
+
await runRuntimeDeductions({
|
|
475
|
+
ledger,
|
|
476
|
+
date: "2025-07-05",
|
|
477
|
+
getActiveBotCount: async () => 1,
|
|
478
|
+
getResourceTierCosts: async () => Credit.fromCents(10),
|
|
479
|
+
getStorageTierCosts: async () => Credit.fromCents(8),
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const tb = await ledger.trialBalance();
|
|
483
|
+
expect(tb.balanced).toBe(true);
|
|
484
|
+
});
|
|
359
485
|
});
|
|
@@ -110,87 +110,111 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
|
|
|
110
110
|
for (const { tenantId, balance } of tenants) {
|
|
111
111
|
try {
|
|
112
112
|
const runtimeRef = `runtime:${cfg.date}:${tenantId}`;
|
|
113
|
-
|
|
114
|
-
result.skipped.push(tenantId);
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
113
|
+
const runtimeAlreadyBilled = await cfg.ledger.hasReferenceId(runtimeRef);
|
|
117
114
|
|
|
118
115
|
const botCount = await cfg.getActiveBotCount(tenantId);
|
|
119
|
-
if (botCount <= 0)
|
|
116
|
+
if (botCount <= 0) {
|
|
117
|
+
if (runtimeAlreadyBilled) result.skipped.push(tenantId);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
120
|
|
|
121
121
|
const totalCost = DAILY_BOT_COST.multiply(botCount);
|
|
122
|
+
let didBillAnything = false;
|
|
122
123
|
|
|
123
|
-
if
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
// Bill runtime debit (skipped if already billed on a previous run)
|
|
125
|
+
if (!runtimeAlreadyBilled) {
|
|
126
|
+
if (!balance.lessThan(totalCost)) {
|
|
127
|
+
// Full deduction
|
|
128
|
+
await cfg.ledger.debit(tenantId, totalCost, "bot_runtime", {
|
|
129
|
+
description: `Daily runtime: ${botCount} bot(s) x $${DAILY_BOT_COST.toDollars().toFixed(2)}`,
|
|
130
|
+
referenceId: runtimeRef,
|
|
131
|
+
});
|
|
132
|
+
} else {
|
|
133
|
+
// Partial deduction — balance insufficient to cover full cost; debit what's available and suspend
|
|
134
|
+
if (balance.greaterThan(Credit.ZERO)) {
|
|
135
|
+
await cfg.ledger.debit(tenantId, balance, "bot_runtime", {
|
|
136
|
+
description: `Partial daily runtime (balance exhausted): ${botCount} bot(s)`,
|
|
137
|
+
referenceId: runtimeRef,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (!result.suspended.includes(tenantId)) {
|
|
141
|
+
result.suspended.push(tenantId);
|
|
142
|
+
if (cfg.onSuspend) await cfg.onSuspend(tenantId);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
didBillAnything = true;
|
|
146
|
+
}
|
|
129
147
|
|
|
130
|
-
|
|
131
|
-
|
|
148
|
+
// Debit resource tier surcharges (if any) — independent idempotency
|
|
149
|
+
if (cfg.getResourceTierCosts) {
|
|
150
|
+
const tierRef = `runtime-tier:${cfg.date}:${tenantId}`;
|
|
151
|
+
if (!(await cfg.ledger.hasReferenceId(tierRef))) {
|
|
132
152
|
const tierCost = await cfg.getResourceTierCosts(tenantId);
|
|
133
153
|
if (!tierCost.isZero()) {
|
|
134
154
|
const balanceAfterRuntime = await cfg.ledger.balance(tenantId);
|
|
135
155
|
if (!balanceAfterRuntime.lessThan(tierCost)) {
|
|
136
156
|
await cfg.ledger.debit(tenantId, tierCost, "resource_upgrade", {
|
|
137
157
|
description: "Daily resource tier surcharge",
|
|
138
|
-
referenceId:
|
|
158
|
+
referenceId: tierRef,
|
|
139
159
|
});
|
|
140
160
|
} else if (balanceAfterRuntime.greaterThan(Credit.ZERO)) {
|
|
141
161
|
await cfg.ledger.debit(tenantId, balanceAfterRuntime, "resource_upgrade", {
|
|
142
162
|
description: "Partial resource tier surcharge (balance exhausted)",
|
|
143
|
-
referenceId:
|
|
163
|
+
referenceId: tierRef,
|
|
144
164
|
});
|
|
145
165
|
}
|
|
166
|
+
didBillAnything = true;
|
|
146
167
|
}
|
|
147
168
|
}
|
|
169
|
+
}
|
|
148
170
|
|
|
149
|
-
|
|
171
|
+
const newBalance = await cfg.ledger.balance(tenantId);
|
|
150
172
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
173
|
+
// Fire onLowBalance if balance crossed below threshold from above
|
|
174
|
+
if (
|
|
175
|
+
newBalance.greaterThan(Credit.ZERO) &&
|
|
176
|
+
!newBalance.greaterThan(LOW_BALANCE_THRESHOLD) &&
|
|
177
|
+
balance.greaterThan(LOW_BALANCE_THRESHOLD) &&
|
|
178
|
+
cfg.onLowBalance
|
|
179
|
+
) {
|
|
180
|
+
await cfg.onLowBalance(tenantId, newBalance);
|
|
181
|
+
}
|
|
160
182
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
183
|
+
// Fire onCreditsExhausted if balance just hit 0
|
|
184
|
+
if (!newBalance.greaterThan(Credit.ZERO) && balance.greaterThan(Credit.ZERO) && cfg.onCreditsExhausted) {
|
|
185
|
+
await cfg.onCreditsExhausted(tenantId);
|
|
186
|
+
}
|
|
165
187
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
188
|
+
// Suspend tenant when balance hits zero (zero-crossing guard)
|
|
189
|
+
if (
|
|
190
|
+
!newBalance.greaterThan(Credit.ZERO) &&
|
|
191
|
+
balance.greaterThan(Credit.ZERO) &&
|
|
192
|
+
!result.suspended.includes(tenantId)
|
|
193
|
+
) {
|
|
194
|
+
result.suspended.push(tenantId);
|
|
195
|
+
if (cfg.onSuspend) {
|
|
196
|
+
await cfg.onSuspend(tenantId);
|
|
176
197
|
}
|
|
198
|
+
}
|
|
177
199
|
|
|
178
|
-
|
|
179
|
-
|
|
200
|
+
// Debit storage tier surcharges (if any) — independent idempotency
|
|
201
|
+
if (cfg.getStorageTierCosts) {
|
|
202
|
+
const storageRef = `runtime-storage:${cfg.date}:${tenantId}`;
|
|
203
|
+
if (!(await cfg.ledger.hasReferenceId(storageRef))) {
|
|
180
204
|
const storageCost = await cfg.getStorageTierCosts(tenantId);
|
|
181
205
|
if (!storageCost.isZero()) {
|
|
182
206
|
const currentBalance = await cfg.ledger.balance(tenantId);
|
|
183
207
|
if (!currentBalance.lessThan(storageCost)) {
|
|
184
208
|
await cfg.ledger.debit(tenantId, storageCost, "storage_upgrade", {
|
|
185
209
|
description: "Daily storage tier surcharge",
|
|
186
|
-
referenceId:
|
|
210
|
+
referenceId: storageRef,
|
|
187
211
|
});
|
|
188
212
|
} else {
|
|
189
213
|
// Partial debit — take what's left, then suspend
|
|
190
214
|
if (currentBalance.greaterThan(Credit.ZERO)) {
|
|
191
215
|
await cfg.ledger.debit(tenantId, currentBalance, "storage_upgrade", {
|
|
192
216
|
description: "Partial storage tier surcharge (balance exhausted)",
|
|
193
|
-
referenceId:
|
|
217
|
+
referenceId: storageRef,
|
|
194
218
|
});
|
|
195
219
|
}
|
|
196
220
|
if (!result.suspended.includes(tenantId)) {
|
|
@@ -198,25 +222,29 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
|
|
|
198
222
|
if (cfg.onSuspend) await cfg.onSuspend(tenantId);
|
|
199
223
|
}
|
|
200
224
|
}
|
|
225
|
+
didBillAnything = true;
|
|
201
226
|
}
|
|
202
227
|
}
|
|
228
|
+
}
|
|
203
229
|
|
|
204
|
-
|
|
205
|
-
|
|
230
|
+
// Debit infrastructure add-on costs (if any) — independent idempotency
|
|
231
|
+
if (cfg.getAddonCosts) {
|
|
232
|
+
const addonRef = `runtime-addon:${cfg.date}:${tenantId}`;
|
|
233
|
+
if (!(await cfg.ledger.hasReferenceId(addonRef))) {
|
|
206
234
|
const addonCost = await cfg.getAddonCosts(tenantId);
|
|
207
235
|
if (!addonCost.isZero()) {
|
|
208
236
|
const currentBalance = await cfg.ledger.balance(tenantId);
|
|
209
237
|
if (!currentBalance.lessThan(addonCost)) {
|
|
210
238
|
await cfg.ledger.debit(tenantId, addonCost, "addon", {
|
|
211
239
|
description: "Daily infrastructure add-on charges",
|
|
212
|
-
referenceId:
|
|
240
|
+
referenceId: addonRef,
|
|
213
241
|
});
|
|
214
242
|
} else {
|
|
215
243
|
// Partial debit — take what's left, then suspend
|
|
216
244
|
if (currentBalance.greaterThan(Credit.ZERO)) {
|
|
217
245
|
await cfg.ledger.debit(tenantId, currentBalance, "addon", {
|
|
218
246
|
description: "Partial add-on charges (balance exhausted)",
|
|
219
|
-
referenceId:
|
|
247
|
+
referenceId: addonRef,
|
|
220
248
|
});
|
|
221
249
|
}
|
|
222
250
|
if (!result.suspended.includes(tenantId)) {
|
|
@@ -224,28 +252,16 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
|
|
|
224
252
|
if (cfg.onSuspend) await cfg.onSuspend(tenantId);
|
|
225
253
|
}
|
|
226
254
|
}
|
|
255
|
+
didBillAnything = true;
|
|
227
256
|
}
|
|
228
257
|
}
|
|
229
|
-
} else {
|
|
230
|
-
// Partial deduction — debit remaining balance, then suspend
|
|
231
|
-
if (balance.greaterThan(Credit.ZERO)) {
|
|
232
|
-
await cfg.ledger.debit(tenantId, balance, "bot_runtime", {
|
|
233
|
-
description: `Partial daily runtime (balance exhausted): ${botCount} bot(s)`,
|
|
234
|
-
referenceId: runtimeRef,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (cfg.onCreditsExhausted) {
|
|
239
|
-
await cfg.onCreditsExhausted(tenantId);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
result.suspended.push(tenantId);
|
|
243
|
-
if (cfg.onSuspend) {
|
|
244
|
-
await cfg.onSuspend(tenantId);
|
|
245
|
-
}
|
|
246
258
|
}
|
|
247
259
|
|
|
248
|
-
|
|
260
|
+
if (didBillAnything) {
|
|
261
|
+
result.processed++;
|
|
262
|
+
} else {
|
|
263
|
+
result.skipped.push(tenantId);
|
|
264
|
+
}
|
|
249
265
|
} catch (err) {
|
|
250
266
|
if (err instanceof InsufficientBalanceError) {
|
|
251
267
|
result.suspended.push(tenantId);
|
package/vitest.config.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
1
2
|
import { defineConfig } from "vitest/config";
|
|
2
3
|
|
|
3
4
|
export default defineConfig({
|
|
5
|
+
resolve: {
|
|
6
|
+
alias: {
|
|
7
|
+
"@wopr-network/platform-core/billing": resolve(__dirname, "src/billing/index.ts"),
|
|
8
|
+
"@wopr-network/platform-core/credits": resolve(__dirname, "src/credits/index.ts"),
|
|
9
|
+
"@wopr-network/platform-core/email": resolve(__dirname, "src/email/index.ts"),
|
|
10
|
+
"@wopr-network/platform-core/metering": resolve(__dirname, "src/metering/index.ts"),
|
|
11
|
+
},
|
|
12
|
+
},
|
|
4
13
|
test: {
|
|
5
14
|
testTimeout: 30000,
|
|
6
15
|
hookTimeout: 30000,
|