@wopr-network/platform-core 1.13.2 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/routes/admin-credits.d.ts +2 -2
- package/dist/api/routes/admin-credits.js +9 -4
- package/dist/api/routes/quota.d.ts +2 -2
- package/dist/api/routes/verify-email.d.ts +3 -3
- package/dist/backup/on-demand-snapshot-service.d.ts +2 -2
- package/dist/billing/payram/webhook.d.ts +3 -3
- package/dist/billing/payram/webhook.js +5 -1
- package/dist/billing/payram/webhook.test.js +5 -4
- package/dist/billing/stripe/stripe-payment-processor.d.ts +2 -2
- package/dist/billing/stripe/stripe-payment-processor.test.js +7 -0
- package/dist/billing/stripe/tenant-store.d.ts +1 -1
- package/dist/billing/stripe/tenant-store.js +1 -1
- package/dist/credits/auto-topup-charge.d.ts +2 -2
- package/dist/credits/auto-topup-charge.js +5 -1
- package/dist/credits/auto-topup-charge.test.js +5 -4
- package/dist/credits/auto-topup-usage.d.ts +2 -2
- package/dist/credits/auto-topup-usage.test.js +53 -12
- package/dist/credits/credit-expiry-cron.d.ts +2 -2
- package/dist/credits/credit-expiry-cron.js +7 -4
- package/dist/credits/credit-expiry-cron.test.js +25 -8
- package/dist/credits/credit-ledger.d.ts +2 -2
- package/dist/credits/credit-ledger.js +1 -1
- package/dist/credits/dividend-cron.d.ts +4 -6
- package/dist/credits/dividend-cron.js +10 -16
- package/dist/credits/dividend-cron.test.js +31 -44
- package/dist/credits/dividend-repository.js +19 -22
- package/dist/credits/dividend-repository.test.js +4 -3
- package/dist/credits/index.d.ts +4 -2
- package/dist/credits/index.js +2 -1
- package/dist/credits/ledger.d.ts +195 -0
- package/dist/credits/ledger.js +561 -0
- package/dist/credits/ledger.test.js +418 -0
- package/dist/credits/signup-grant.d.ts +2 -2
- package/dist/credits/signup-grant.js +4 -4
- package/dist/credits/signup-grant.test.js +5 -3
- package/dist/credits/trial-balance-cron.d.ts +19 -0
- package/dist/credits/trial-balance-cron.js +30 -0
- package/dist/credits/trial-balance-cron.test.js +55 -0
- package/dist/db/schema/gateway-service-keys.d.ts +109 -0
- package/dist/db/schema/gateway-service-keys.js +18 -0
- package/dist/db/schema/index.d.ts +2 -0
- package/dist/db/schema/index.js +2 -0
- package/dist/db/schema/ledger.d.ts +442 -0
- package/dist/db/schema/ledger.js +76 -0
- package/dist/gateway/credit-gate.d.ts +2 -2
- package/dist/gateway/credit-gate.js +5 -1
- package/dist/gateway/credit-gate.test.js +35 -33
- package/dist/gateway/gateway-routes.test.js +1 -1
- package/dist/gateway/index.d.ts +2 -0
- package/dist/gateway/index.js +1 -0
- package/dist/gateway/protocol/anthropic.js +1 -1
- package/dist/gateway/protocol/deps.d.ts +5 -5
- package/dist/gateway/protocol/openai.js +1 -1
- package/dist/gateway/proxy.d.ts +4 -4
- package/dist/gateway/route-mounting.test.js +1 -1
- package/dist/gateway/service-key-auth.d.ts +1 -1
- package/dist/gateway/service-key-auth.js +1 -1
- package/dist/gateway/service-key-repository.d.ts +27 -0
- package/dist/gateway/service-key-repository.js +64 -0
- package/dist/gateway/types.d.ts +5 -5
- package/dist/metering/reconciliation-cron.test.js +9 -8
- package/dist/metering/reconciliation-repository.js +12 -10
- package/dist/metering/reconciliation-repository.test.js +9 -8
- package/dist/monetization/affiliate/affiliate-admin-repository.js +10 -8
- package/dist/monetization/affiliate/affiliate-admin-repository.test.js +32 -13
- package/dist/monetization/affiliate/credit-match.d.ts +2 -2
- package/dist/monetization/affiliate/credit-match.js +4 -1
- package/dist/monetization/affiliate/credit-match.test.js +58 -13
- package/dist/monetization/affiliate/new-user-bonus.d.ts +2 -2
- package/dist/monetization/affiliate/new-user-bonus.js +4 -1
- package/dist/monetization/affiliate/new-user-bonus.test.js +4 -3
- package/dist/monetization/credits/auto-topup-charge.d.ts +2 -2
- package/dist/monetization/credits/auto-topup-charge.js +5 -1
- package/dist/monetization/credits/auto-topup-charge.test.js +5 -4
- package/dist/monetization/credits/auto-topup-usage.d.ts +2 -2
- package/dist/monetization/credits/auto-topup-usage.test.js +53 -12
- package/dist/monetization/credits/bot-billing.d.ts +3 -3
- package/dist/monetization/credits/bot-billing.test.js +18 -5
- package/dist/monetization/credits/credit-expiry-cron.test.js +25 -8
- package/dist/monetization/credits/dividend-cron.d.ts +2 -4
- package/dist/monetization/credits/dividend-cron.js +7 -4
- package/dist/monetization/credits/dividend-cron.test.js +26 -46
- package/dist/monetization/credits/dividend-repository.js +15 -24
- package/dist/monetization/credits/dividend-repository.test.js +4 -3
- package/dist/monetization/credits/index.d.ts +2 -2
- package/dist/monetization/credits/index.js +1 -1
- package/dist/monetization/credits/member-usage.test.js +23 -10
- package/dist/monetization/credits/phone-billing.d.ts +2 -2
- package/dist/monetization/credits/phone-billing.js +5 -1
- package/dist/monetization/credits/phone-billing.test.js +9 -12
- package/dist/monetization/credits/runtime-cron.d.ts +2 -2
- package/dist/monetization/credits/runtime-cron.js +32 -8
- package/dist/monetization/credits/runtime-cron.test.js +28 -27
- package/dist/monetization/credits/runtime-scheduler.d.ts +2 -2
- package/dist/monetization/credits/runtime-scheduler.test.js +1 -1
- package/dist/monetization/credits/signup-grant.test.js +5 -3
- package/dist/monetization/credits/storage-tier-cron.test.js +3 -2
- package/dist/monetization/credits/trial-balance-cron.test.js +42 -0
- package/dist/monetization/feature-gate.d.ts +3 -3
- package/dist/monetization/index.d.ts +3 -3
- package/dist/monetization/index.js +1 -1
- package/dist/monetization/metering/reconciliation-cron.test.js +9 -8
- package/dist/monetization/metering/reconciliation-repository.js +11 -10
- package/dist/monetization/metering/reconciliation-repository.test.js +9 -8
- package/dist/monetization/payram/webhook.d.ts +2 -2
- package/dist/monetization/payram/webhook.js +5 -1
- package/dist/monetization/payram/webhook.test.js +5 -4
- package/dist/monetization/promotions/engine.d.ts +2 -2
- package/dist/monetization/promotions/engine.js +4 -1
- package/dist/monetization/promotions/engine.test.js +3 -1
- package/dist/monetization/repository-types.d.ts +1 -1
- package/dist/monetization/socket/socket.d.ts +3 -3
- package/dist/monetization/stripe/stripe-payment-processor.d.ts +2 -2
- package/dist/monetization/stripe/stripe-payment-processor.test.js +7 -0
- package/dist/monetization/stripe/webhook.d.ts +2 -2
- package/dist/monetization/stripe/webhook.js +70 -6
- package/dist/monetization/stripe/webhook.test.js +20 -15
- package/dist/onboarding/onboarding-service.d.ts +2 -2
- package/dist/onboarding/onboarding-service.js +6 -2
- package/drizzle/migrations/0002_gateway_service_keys.sql +14 -0
- package/drizzle/migrations/0003_double_entry_ledger.sql +82 -0
- package/drizzle/migrations/meta/_journal.json +14 -0
- package/package.json +1 -1
- package/src/api/routes/admin-credits.ts +11 -14
- package/src/api/routes/quota.ts +2 -2
- package/src/api/routes/verify-email.ts +4 -4
- package/src/backup/on-demand-snapshot-service.test.ts +3 -3
- package/src/backup/on-demand-snapshot-service.ts +3 -3
- package/src/billing/payram/webhook.test.ts +7 -5
- package/src/billing/payram/webhook.ts +8 -11
- package/src/billing/stripe/stripe-payment-processor.test.ts +10 -3
- package/src/billing/stripe/stripe-payment-processor.ts +3 -3
- package/src/billing/stripe/tenant-store.ts +1 -1
- package/src/credits/auto-topup-charge.test.ts +7 -5
- package/src/credits/auto-topup-charge.ts +7 -10
- package/src/credits/auto-topup-usage.test.ts +55 -13
- package/src/credits/auto-topup-usage.ts +2 -2
- package/src/credits/credit-expiry-cron.test.ts +26 -45
- package/src/credits/credit-expiry-cron.ts +9 -12
- package/src/credits/credit-ledger.ts +3 -3
- package/src/credits/dividend-cron.test.ts +38 -45
- package/src/credits/dividend-cron.ts +12 -26
- package/src/credits/dividend-repository.test.ts +4 -3
- package/src/credits/dividend-repository.ts +21 -23
- package/src/credits/index.ts +23 -4
- package/src/credits/ledger.test.ts +514 -0
- package/src/credits/ledger.ts +851 -0
- package/src/credits/signup-grant.test.ts +7 -4
- package/src/credits/signup-grant.ts +6 -12
- package/src/credits/trial-balance-cron.test.ts +68 -0
- package/src/credits/trial-balance-cron.ts +46 -0
- package/src/db/schema/gateway-service-keys.ts +23 -0
- package/src/db/schema/index.ts +2 -0
- package/src/db/schema/ledger.ts +94 -0
- package/src/gateway/credit-gate-wiring.test.ts +3 -3
- package/src/gateway/credit-gate.test.ts +35 -33
- package/src/gateway/credit-gate.ts +6 -10
- package/src/gateway/gateway-routes.test.ts +6 -6
- package/src/gateway/index.ts +2 -0
- package/src/gateway/protocol/anthropic.ts +2 -2
- package/src/gateway/protocol/deps.ts +5 -5
- package/src/gateway/protocol/openai.ts +2 -2
- package/src/gateway/proxy.ts +4 -4
- package/src/gateway/route-mounting.test.ts +3 -3
- package/src/gateway/service-key-auth.ts +4 -2
- package/src/gateway/service-key-repository.ts +87 -0
- package/src/gateway/types.ts +5 -5
- package/src/metering/reconciliation-cron.test.ts +10 -9
- package/src/metering/reconciliation-repository.test.ts +10 -9
- package/src/metering/reconciliation-repository.ts +14 -11
- package/src/monetization/affiliate/affiliate-admin-repository.test.ts +32 -19
- package/src/monetization/affiliate/affiliate-admin-repository.ts +16 -8
- package/src/monetization/affiliate/credit-match.test.ts +60 -14
- package/src/monetization/affiliate/credit-match.ts +6 -9
- package/src/monetization/affiliate/new-user-bonus.test.ts +6 -4
- package/src/monetization/affiliate/new-user-bonus.ts +6 -9
- package/src/monetization/credits/auto-topup-charge.test.ts +7 -5
- package/src/monetization/credits/auto-topup-charge.ts +7 -10
- package/src/monetization/credits/auto-topup-usage.test.ts +55 -13
- package/src/monetization/credits/auto-topup-usage.ts +2 -2
- package/src/monetization/credits/bot-billing.test.ts +20 -6
- package/src/monetization/credits/bot-billing.ts +3 -3
- package/src/monetization/credits/credit-expiry-cron.test.ts +26 -45
- package/src/monetization/credits/dividend-cron.test.ts +34 -48
- package/src/monetization/credits/dividend-cron.ts +9 -14
- package/src/monetization/credits/dividend-repository.test.ts +4 -3
- package/src/monetization/credits/dividend-repository.ts +19 -25
- package/src/monetization/credits/index.ts +4 -4
- package/src/monetization/credits/member-usage.test.ts +25 -11
- package/src/monetization/credits/phone-billing.test.ts +18 -26
- package/src/monetization/credits/phone-billing.ts +7 -10
- package/src/monetization/credits/runtime-cron.test.ts +29 -28
- package/src/monetization/credits/runtime-cron.ts +34 -58
- package/src/monetization/credits/runtime-scheduler.test.ts +1 -1
- package/src/monetization/credits/runtime-scheduler.ts +2 -2
- package/src/monetization/credits/signup-grant.test.ts +7 -4
- package/src/monetization/credits/storage-tier-cron.test.ts +5 -3
- package/src/monetization/credits/trial-balance-cron.test.ts +52 -0
- package/src/monetization/feature-gate.ts +3 -3
- package/src/monetization/index.ts +4 -4
- package/src/monetization/metering/reconciliation-cron.test.ts +10 -9
- package/src/monetization/metering/reconciliation-repository.test.ts +11 -9
- package/src/monetization/metering/reconciliation-repository.ts +13 -11
- package/src/monetization/payram/webhook.test.ts +7 -5
- package/src/monetization/payram/webhook.ts +7 -10
- package/src/monetization/promotions/engine.test.ts +6 -5
- package/src/monetization/promotions/engine.ts +6 -3
- package/src/monetization/repository-types.ts +1 -1
- package/src/monetization/socket/socket.ts +4 -4
- package/src/monetization/stripe/stripe-payment-processor.test.ts +10 -3
- package/src/monetization/stripe/stripe-payment-processor.ts +3 -3
- package/src/monetization/stripe/webhook.test.ts +22 -16
- package/src/monetization/stripe/webhook.ts +75 -50
- package/src/onboarding/onboarding-service.ts +8 -11
- package/dist/credits/credit-ledger-extra.test.js +0 -40
- package/dist/credits/credit-ledger.bench.js +0 -33
- package/dist/credits/credit-ledger.test.d.ts +0 -4
- package/dist/credits/credit-ledger.test.js +0 -203
- package/dist/credits/credit-transaction-repository.test.js +0 -232
- package/dist/monetization/credits/credit-ledger-extra.test.d.ts +0 -1
- package/dist/monetization/credits/credit-ledger-extra.test.js +0 -39
- package/dist/monetization/credits/credit-ledger.bench.d.ts +0 -1
- package/dist/monetization/credits/credit-ledger.bench.js +0 -32
- package/dist/monetization/credits/credit-ledger.test.d.ts +0 -4
- package/dist/monetization/credits/credit-ledger.test.js +0 -202
- package/dist/monetization/credits/credit-transaction-repository.test.d.ts +0 -1
- package/dist/monetization/credits/credit-transaction-repository.test.js +0 -232
- package/src/credits/credit-ledger-extra.test.ts +0 -57
- package/src/credits/credit-ledger.bench.ts +0 -56
- package/src/credits/credit-ledger.test.ts +0 -276
- package/src/credits/credit-transaction-repository.test.ts +0 -274
- package/src/monetization/credits/credit-ledger-extra.test.ts +0 -56
- package/src/monetization/credits/credit-ledger.bench.ts +0 -55
- package/src/monetization/credits/credit-ledger.test.ts +0 -275
- package/src/monetization/credits/credit-transaction-repository.test.ts +0 -274
- /package/dist/credits/{credit-ledger-extra.test.d.ts → ledger.test.d.ts} +0 -0
- /package/dist/credits/{credit-ledger.bench.d.ts → trial-balance-cron.test.d.ts} +0 -0
- /package/dist/{credits/credit-transaction-repository.test.d.ts → monetization/credits/trial-balance-cron.test.d.ts} +0 -0
|
@@ -1,28 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Credit,
|
|
3
|
-
type CreditTransaction,
|
|
4
|
-
type ICreditLedger,
|
|
5
|
-
InsufficientBalanceError,
|
|
6
|
-
} from "@wopr-network/platform-core/credits";
|
|
1
|
+
import { Credit, type ILedger, InsufficientBalanceError, type JournalEntry } from "@wopr-network/platform-core/credits";
|
|
7
2
|
import type { IMeterEmitter } from "@wopr-network/platform-core/metering";
|
|
8
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
9
4
|
import type { IPhoneNumberRepository } from "./drizzle-phone-number-repository.js";
|
|
10
5
|
import { PHONE_NUMBER_MONTHLY_COST, runMonthlyPhoneBilling } from "./phone-billing.js";
|
|
11
6
|
import type { ProvisionedPhoneNumber } from "./repository-types.js";
|
|
12
7
|
|
|
13
|
-
function makeTx(tenantId: string):
|
|
8
|
+
function makeTx(tenantId: string): JournalEntry {
|
|
14
9
|
return {
|
|
15
10
|
id: "tx-1",
|
|
11
|
+
postedAt: new Date().toISOString(),
|
|
12
|
+
entryType: "addon",
|
|
16
13
|
tenantId,
|
|
17
|
-
amount: Credit.fromDollars(1),
|
|
18
|
-
balanceAfter: Credit.fromDollars(100),
|
|
19
|
-
type: "addon",
|
|
20
14
|
description: "Monthly phone number fee",
|
|
21
15
|
referenceId: null,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
createdAt: new Date().toISOString(),
|
|
25
|
-
expiresAt: null,
|
|
16
|
+
metadata: null,
|
|
17
|
+
lines: [],
|
|
26
18
|
};
|
|
27
19
|
}
|
|
28
20
|
|
|
@@ -108,7 +100,7 @@ describe("runMonthlyPhoneBilling", () => {
|
|
|
108
100
|
|
|
109
101
|
const result = await runMonthlyPhoneBilling(
|
|
110
102
|
phoneRepo as unknown as IPhoneNumberRepository,
|
|
111
|
-
ledger as unknown as
|
|
103
|
+
ledger as unknown as ILedger,
|
|
112
104
|
meter as unknown as IMeterEmitter,
|
|
113
105
|
);
|
|
114
106
|
|
|
@@ -128,7 +120,7 @@ describe("runMonthlyPhoneBilling", () => {
|
|
|
128
120
|
|
|
129
121
|
const result = await runMonthlyPhoneBilling(
|
|
130
122
|
phoneRepo as unknown as IPhoneNumberRepository,
|
|
131
|
-
ledger as unknown as
|
|
123
|
+
ledger as unknown as ILedger,
|
|
132
124
|
meter as unknown as IMeterEmitter,
|
|
133
125
|
);
|
|
134
126
|
|
|
@@ -138,16 +130,16 @@ describe("runMonthlyPhoneBilling", () => {
|
|
|
138
130
|
|
|
139
131
|
// Verify debit was called with the margined charge amount
|
|
140
132
|
expect(ledger.debit).toHaveBeenCalledOnce();
|
|
141
|
-
const [tenantId, chargeAmount, type,
|
|
133
|
+
const [tenantId, chargeAmount, type, opts] = ledger.debit.mock.calls[0];
|
|
142
134
|
expect(tenantId).toBe("tenant-1");
|
|
143
135
|
// chargeCredit = Credit.fromDollars(1.15).multiply(2.6)
|
|
144
136
|
const expectedCharge = Credit.fromDollars(1.15).multiply(2.6);
|
|
145
137
|
expect(chargeAmount.toRaw()).toBe(expectedCharge.toRaw());
|
|
146
138
|
expect(type).toBe("addon");
|
|
147
|
-
expect(description).toBe("Monthly phone number fee");
|
|
139
|
+
expect(opts.description).toBe("Monthly phone number fee");
|
|
148
140
|
const expectedMonth = `${NOW.getFullYear()}-${String(NOW.getMonth() + 1).padStart(2, "0")}`;
|
|
149
|
-
expect(referenceId).toMatch(new RegExp(`^phone-billing:PN-abc123:${expectedMonth}$`));
|
|
150
|
-
expect(allowNegative).toBe(true);
|
|
141
|
+
expect(opts.referenceId).toMatch(new RegExp(`^phone-billing:PN-abc123:${expectedMonth}$`));
|
|
142
|
+
expect(opts.allowNegative).toBe(true);
|
|
151
143
|
|
|
152
144
|
// Verify meter emission
|
|
153
145
|
expect(meter.emit).toHaveBeenCalledOnce();
|
|
@@ -170,7 +162,7 @@ describe("runMonthlyPhoneBilling", () => {
|
|
|
170
162
|
|
|
171
163
|
const result = await runMonthlyPhoneBilling(
|
|
172
164
|
phoneRepo as unknown as IPhoneNumberRepository,
|
|
173
|
-
ledger as unknown as
|
|
165
|
+
ledger as unknown as ILedger,
|
|
174
166
|
meter as unknown as IMeterEmitter,
|
|
175
167
|
);
|
|
176
168
|
|
|
@@ -191,7 +183,7 @@ describe("runMonthlyPhoneBilling", () => {
|
|
|
191
183
|
|
|
192
184
|
const result = await runMonthlyPhoneBilling(
|
|
193
185
|
phoneRepo as unknown as IPhoneNumberRepository,
|
|
194
|
-
ledger as unknown as
|
|
186
|
+
ledger as unknown as ILedger,
|
|
195
187
|
meter as unknown as IMeterEmitter,
|
|
196
188
|
);
|
|
197
189
|
|
|
@@ -208,7 +200,7 @@ describe("runMonthlyPhoneBilling", () => {
|
|
|
208
200
|
|
|
209
201
|
const result = await runMonthlyPhoneBilling(
|
|
210
202
|
phoneRepo as unknown as IPhoneNumberRepository,
|
|
211
|
-
ledger as unknown as
|
|
203
|
+
ledger as unknown as ILedger,
|
|
212
204
|
meter as unknown as IMeterEmitter,
|
|
213
205
|
);
|
|
214
206
|
|
|
@@ -229,7 +221,7 @@ describe("runMonthlyPhoneBilling", () => {
|
|
|
229
221
|
|
|
230
222
|
const result = await runMonthlyPhoneBilling(
|
|
231
223
|
phoneRepo as unknown as IPhoneNumberRepository,
|
|
232
|
-
ledger as unknown as
|
|
224
|
+
ledger as unknown as ILedger,
|
|
233
225
|
meter as unknown as IMeterEmitter,
|
|
234
226
|
);
|
|
235
227
|
|
|
@@ -252,7 +244,7 @@ describe("runMonthlyPhoneBilling", () => {
|
|
|
252
244
|
|
|
253
245
|
const result = await runMonthlyPhoneBilling(
|
|
254
246
|
phoneRepo as unknown as IPhoneNumberRepository,
|
|
255
|
-
ledger as unknown as
|
|
247
|
+
ledger as unknown as ILedger,
|
|
256
248
|
meter as unknown as IMeterEmitter,
|
|
257
249
|
);
|
|
258
250
|
|
|
@@ -271,7 +263,7 @@ describe("runMonthlyPhoneBilling", () => {
|
|
|
271
263
|
|
|
272
264
|
const result = await runMonthlyPhoneBilling(
|
|
273
265
|
phoneRepo as unknown as IPhoneNumberRepository,
|
|
274
|
-
ledger as unknown as
|
|
266
|
+
ledger as unknown as ILedger,
|
|
275
267
|
meter as unknown as IMeterEmitter,
|
|
276
268
|
);
|
|
277
269
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { Credit, InsufficientBalanceError } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import type { IMeterEmitter } from "@wopr-network/platform-core/metering";
|
|
4
4
|
import { logger } from "../../config/logger.js";
|
|
@@ -15,7 +15,7 @@ const PHONE_NUMBER_MARGIN = 2.6;
|
|
|
15
15
|
|
|
16
16
|
export async function runMonthlyPhoneBilling(
|
|
17
17
|
phoneRepo: IPhoneNumberRepository,
|
|
18
|
-
ledger:
|
|
18
|
+
ledger: ILedger,
|
|
19
19
|
meter: IMeterEmitter,
|
|
20
20
|
): Promise<{
|
|
21
21
|
processed: number;
|
|
@@ -45,14 +45,11 @@ export async function runMonthlyPhoneBilling(
|
|
|
45
45
|
const costCredit = Credit.fromDollars(PHONE_NUMBER_MONTHLY_COST);
|
|
46
46
|
const chargeCredit = withMargin(costCredit, PHONE_NUMBER_MARGIN);
|
|
47
47
|
|
|
48
|
-
await ledger.debit(
|
|
49
|
-
number
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
`phone-billing:${number.sid}:${now.toISOString().slice(0, 7)}`,
|
|
54
|
-
true,
|
|
55
|
-
);
|
|
48
|
+
await ledger.debit(number.tenantId, chargeCredit, "addon", {
|
|
49
|
+
description: "Monthly phone number fee",
|
|
50
|
+
referenceId: `phone-billing:${number.sid}:${now.toISOString().slice(0, 7)}`,
|
|
51
|
+
allowNegative: true,
|
|
52
|
+
});
|
|
56
53
|
|
|
57
54
|
meter.emit({
|
|
58
55
|
tenant: number.tenantId,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PGlite } from "@electric-sql/pglite";
|
|
2
|
-
import { Credit,
|
|
2
|
+
import { Credit, DrizzleLedger, InsufficientBalanceError } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { RESOURCE_TIERS } from "../../fleet/resource-tiers.js";
|
|
5
5
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
@@ -8,12 +8,12 @@ import { buildResourceTierCosts, DAILY_BOT_COST, runRuntimeDeductions } from "./
|
|
|
8
8
|
describe("runRuntimeDeductions", () => {
|
|
9
9
|
const TODAY = "2025-01-01";
|
|
10
10
|
let pool: PGlite;
|
|
11
|
-
let ledger:
|
|
11
|
+
let ledger: DrizzleLedger;
|
|
12
12
|
|
|
13
13
|
beforeAll(async () => {
|
|
14
14
|
const { db, pool: p } = await createTestDb();
|
|
15
15
|
pool = p;
|
|
16
|
-
ledger = new
|
|
16
|
+
ledger = new DrizzleLedger(db);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
afterAll(async () => {
|
|
@@ -22,6 +22,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
22
22
|
|
|
23
23
|
beforeEach(async () => {
|
|
24
24
|
await truncateAllTables(pool);
|
|
25
|
+
await ledger.seedSystemAccounts();
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
it("DAILY_BOT_COST equals 17 cents", () => {
|
|
@@ -40,7 +41,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
40
41
|
});
|
|
41
42
|
|
|
42
43
|
it("skips tenants with zero active bots", async () => {
|
|
43
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
|
|
44
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
|
|
44
45
|
const result = await runRuntimeDeductions({
|
|
45
46
|
ledger,
|
|
46
47
|
date: TODAY,
|
|
@@ -51,7 +52,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
it("deducts full amount when balance is sufficient", async () => {
|
|
54
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
|
|
55
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
|
|
55
56
|
const result = await runRuntimeDeductions({
|
|
56
57
|
ledger,
|
|
57
58
|
date: TODAY,
|
|
@@ -63,7 +64,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
63
64
|
});
|
|
64
65
|
|
|
65
66
|
it("partial deduction and suspension when balance is insufficient", async () => {
|
|
66
|
-
await ledger.credit("tenant-1", Credit.fromCents(10), "purchase", "top-up");
|
|
67
|
+
await ledger.credit("tenant-1", Credit.fromCents(10), "purchase", { description: "top-up" });
|
|
67
68
|
const onSuspend = vi.fn();
|
|
68
69
|
const result = await runRuntimeDeductions({
|
|
69
70
|
ledger,
|
|
@@ -78,9 +79,9 @@ describe("runRuntimeDeductions", () => {
|
|
|
78
79
|
});
|
|
79
80
|
|
|
80
81
|
it("suspends with zero partial when balance exactly zero", async () => {
|
|
81
|
-
await ledger.credit("tenant-1", Credit.fromCents(100), "purchase", "top-up");
|
|
82
|
-
await ledger.debit("tenant-1", Credit.fromCents(100), "bot_runtime", "drain");
|
|
83
|
-
await ledger.credit("tenant-1", Credit.fromCents(1), "purchase", "tiny");
|
|
82
|
+
await ledger.credit("tenant-1", Credit.fromCents(100), "purchase", { description: "top-up" });
|
|
83
|
+
await ledger.debit("tenant-1", Credit.fromCents(100), "bot_runtime", { description: "drain" });
|
|
84
|
+
await ledger.credit("tenant-1", Credit.fromCents(1), "purchase", { description: "tiny" });
|
|
84
85
|
|
|
85
86
|
const onSuspend = vi.fn();
|
|
86
87
|
const result = await runRuntimeDeductions({
|
|
@@ -95,7 +96,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
95
96
|
});
|
|
96
97
|
|
|
97
98
|
it("suspends without onSuspend callback", async () => {
|
|
98
|
-
await ledger.credit("tenant-1", Credit.fromCents(5), "purchase", "top-up");
|
|
99
|
+
await ledger.credit("tenant-1", Credit.fromCents(5), "purchase", { description: "top-up" });
|
|
99
100
|
const result = await runRuntimeDeductions({
|
|
100
101
|
ledger,
|
|
101
102
|
date: TODAY,
|
|
@@ -106,7 +107,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
106
107
|
});
|
|
107
108
|
|
|
108
109
|
it("handles errors from getActiveBotCount gracefully", async () => {
|
|
109
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
|
|
110
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
|
|
110
111
|
const result = await runRuntimeDeductions({
|
|
111
112
|
ledger,
|
|
112
113
|
date: TODAY,
|
|
@@ -120,8 +121,8 @@ describe("runRuntimeDeductions", () => {
|
|
|
120
121
|
});
|
|
121
122
|
|
|
122
123
|
it("handles InsufficientBalanceError from ledger.debit", async () => {
|
|
123
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
|
|
124
|
-
await ledger.debit("tenant-1", Credit.fromCents(499), "bot_runtime", "drain");
|
|
124
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
|
|
125
|
+
await ledger.debit("tenant-1", Credit.fromCents(499), "bot_runtime", { description: "drain" });
|
|
125
126
|
const onSuspend = vi.fn();
|
|
126
127
|
const result = await runRuntimeDeductions({
|
|
127
128
|
ledger,
|
|
@@ -134,7 +135,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
134
135
|
});
|
|
135
136
|
|
|
136
137
|
it("catches InsufficientBalanceError from debit and suspends", async () => {
|
|
137
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
|
|
138
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
|
|
138
139
|
vi.spyOn(ledger, "debit").mockRejectedValue(
|
|
139
140
|
new InsufficientBalanceError(Credit.fromCents(0), Credit.fromCents(17)),
|
|
140
141
|
);
|
|
@@ -152,7 +153,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
152
153
|
});
|
|
153
154
|
|
|
154
155
|
it("catches InsufficientBalanceError without onSuspend callback", async () => {
|
|
155
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
|
|
156
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
|
|
156
157
|
vi.spyOn(ledger, "debit").mockRejectedValue(
|
|
157
158
|
new InsufficientBalanceError(Credit.fromCents(0), Credit.fromCents(17)),
|
|
158
159
|
);
|
|
@@ -167,8 +168,8 @@ describe("runRuntimeDeductions", () => {
|
|
|
167
168
|
});
|
|
168
169
|
|
|
169
170
|
it("processes multiple tenants", async () => {
|
|
170
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
|
|
171
|
-
await ledger.credit("tenant-2", Credit.fromCents(10), "purchase", "top-up");
|
|
171
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
|
|
172
|
+
await ledger.credit("tenant-2", Credit.fromCents(10), "purchase", { description: "top-up" });
|
|
172
173
|
const onSuspend = vi.fn();
|
|
173
174
|
const result = await runRuntimeDeductions({
|
|
174
175
|
ledger,
|
|
@@ -182,7 +183,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
182
183
|
});
|
|
183
184
|
|
|
184
185
|
it("fires onLowBalance when balance drops below 100 cents threshold", async () => {
|
|
185
|
-
await ledger.credit("tenant-1", Credit.fromCents(110), "purchase", "top-up");
|
|
186
|
+
await ledger.credit("tenant-1", Credit.fromCents(110), "purchase", { description: "top-up" });
|
|
186
187
|
const onLowBalance = vi.fn();
|
|
187
188
|
await runRuntimeDeductions({
|
|
188
189
|
ledger,
|
|
@@ -197,7 +198,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
197
198
|
});
|
|
198
199
|
|
|
199
200
|
it("does NOT fire onLowBalance when balance was already below threshold before deduction", async () => {
|
|
200
|
-
await ledger.credit("tenant-1", Credit.fromCents(90), "purchase", "top-up");
|
|
201
|
+
await ledger.credit("tenant-1", Credit.fromCents(90), "purchase", { description: "top-up" });
|
|
201
202
|
const onLowBalance = vi.fn();
|
|
202
203
|
await runRuntimeDeductions({
|
|
203
204
|
ledger,
|
|
@@ -209,7 +210,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
209
210
|
});
|
|
210
211
|
|
|
211
212
|
it("fires onCreditsExhausted when full deduction causes balance to drop to 0", async () => {
|
|
212
|
-
await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", "top-up");
|
|
213
|
+
await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", { description: "top-up" });
|
|
213
214
|
const onCreditsExhausted = vi.fn();
|
|
214
215
|
await runRuntimeDeductions({
|
|
215
216
|
ledger,
|
|
@@ -223,7 +224,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
223
224
|
|
|
224
225
|
it("suspends tenant when full deduction causes balance to drop to exactly 0", async () => {
|
|
225
226
|
// Balance = exactly 1 bot * DAILY_BOT_COST = 17 cents → full deduction → 0
|
|
226
|
-
await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", "top-up");
|
|
227
|
+
await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", { description: "top-up" });
|
|
227
228
|
const onSuspend = vi.fn();
|
|
228
229
|
const onCreditsExhausted = vi.fn();
|
|
229
230
|
const result = await runRuntimeDeductions({
|
|
@@ -240,7 +241,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
240
241
|
});
|
|
241
242
|
|
|
242
243
|
it("fires onCreditsExhausted on partial deduction when balance hits 0", async () => {
|
|
243
|
-
await ledger.credit("tenant-1", Credit.fromCents(10), "purchase", "top-up");
|
|
244
|
+
await ledger.credit("tenant-1", Credit.fromCents(10), "purchase", { description: "top-up" });
|
|
244
245
|
const onCreditsExhausted = vi.fn();
|
|
245
246
|
await runRuntimeDeductions({
|
|
246
247
|
ledger,
|
|
@@ -253,7 +254,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
253
254
|
});
|
|
254
255
|
|
|
255
256
|
it("partially debits resource tier surcharge when balance is positive but insufficient", async () => {
|
|
256
|
-
await ledger.credit("tenant-1", Credit.fromCents(30), "purchase", "top-up");
|
|
257
|
+
await ledger.credit("tenant-1", Credit.fromCents(30), "purchase", { description: "top-up" });
|
|
257
258
|
const result = await runRuntimeDeductions({
|
|
258
259
|
ledger,
|
|
259
260
|
date: TODAY,
|
|
@@ -265,7 +266,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
265
266
|
});
|
|
266
267
|
|
|
267
268
|
it("skips resource tier partial debit when balance is exactly 0 after runtime", async () => {
|
|
268
|
-
await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", "top-up");
|
|
269
|
+
await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", { description: "top-up" });
|
|
269
270
|
const onCreditsExhausted = vi.fn();
|
|
270
271
|
const result = await runRuntimeDeductions({
|
|
271
272
|
ledger,
|
|
@@ -284,7 +285,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
284
285
|
// triggering the zero-crossing suspend in the runtime block.
|
|
285
286
|
// Storage cost (5 cents) then tries to suspend again via its else-branch (balance 0 < 5).
|
|
286
287
|
// The !result.suspended.includes(tenantId) guard must prevent onSuspend being called twice.
|
|
287
|
-
await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", "top-up");
|
|
288
|
+
await ledger.credit("tenant-1", Credit.fromCents(17), "purchase", { description: "top-up" });
|
|
288
289
|
const onSuspend = vi.fn();
|
|
289
290
|
const result = await runRuntimeDeductions({
|
|
290
291
|
ledger,
|
|
@@ -301,7 +302,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
301
302
|
it("buildResourceTierCosts: deducts pro tier surcharge via getResourceTierCosts", async () => {
|
|
302
303
|
const proTierCost = RESOURCE_TIERS.pro.dailyCost.toCents();
|
|
303
304
|
const startBalance = 17 + proTierCost + 10;
|
|
304
|
-
await ledger.credit("tenant-1", Credit.fromCents(startBalance), "purchase", "top-up");
|
|
305
|
+
await ledger.credit("tenant-1", Credit.fromCents(startBalance), "purchase", { description: "top-up" });
|
|
305
306
|
|
|
306
307
|
const mockRepo = {
|
|
307
308
|
getResourceTier: async (_botId: string): Promise<string | null> => "pro",
|
|
@@ -324,7 +325,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
324
325
|
});
|
|
325
326
|
|
|
326
327
|
it("treats unique constraint violation from concurrent debit as already-billed (skip, not error)", async () => {
|
|
327
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
|
|
328
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
|
|
328
329
|
const uniqueErr = Object.assign(new Error("duplicate key value violates unique constraint"), { code: "23505" });
|
|
329
330
|
vi.spyOn(ledger, "debit").mockRejectedValueOnce(uniqueErr);
|
|
330
331
|
const result = await runRuntimeDeductions({
|
|
@@ -338,7 +339,7 @@ describe("runRuntimeDeductions", () => {
|
|
|
338
339
|
});
|
|
339
340
|
|
|
340
341
|
it("is idempotent — second run on same date does not double-deduct", async () => {
|
|
341
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "top-up");
|
|
342
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "top-up" });
|
|
342
343
|
const cfg = {
|
|
343
344
|
ledger,
|
|
344
345
|
getActiveBotCount: async () => 1,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { Credit, InsufficientBalanceError } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import { logger } from "../../config/logger.js";
|
|
4
4
|
import type { IBotInstanceRepository } from "../../fleet/bot-instance-repository.js";
|
|
@@ -20,7 +20,7 @@ export type GetActiveBotCount = (tenantId: string) => number | Promise<number>;
|
|
|
20
20
|
export const LOW_BALANCE_THRESHOLD = Credit.fromCents(100);
|
|
21
21
|
|
|
22
22
|
export interface RuntimeCronConfig {
|
|
23
|
-
ledger:
|
|
23
|
+
ledger: ILedger;
|
|
24
24
|
getActiveBotCount: GetActiveBotCount;
|
|
25
25
|
/** The date being billed, as YYYY-MM-DD. Used for idempotency. */
|
|
26
26
|
date: string;
|
|
@@ -122,13 +122,10 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
|
|
|
122
122
|
|
|
123
123
|
if (!balance.lessThan(totalCost)) {
|
|
124
124
|
// Full deduction
|
|
125
|
-
await cfg.ledger.debit(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
`Daily runtime: ${botCount} bot(s) x $${DAILY_BOT_COST.toDollars().toFixed(2)}`,
|
|
130
|
-
runtimeRef,
|
|
131
|
-
);
|
|
125
|
+
await cfg.ledger.debit(tenantId, totalCost, "bot_runtime", {
|
|
126
|
+
description: `Daily runtime: ${botCount} bot(s) x $${DAILY_BOT_COST.toDollars().toFixed(2)}`,
|
|
127
|
+
referenceId: runtimeRef,
|
|
128
|
+
});
|
|
132
129
|
|
|
133
130
|
// Debit resource tier surcharges (if any)
|
|
134
131
|
if (cfg.getResourceTierCosts) {
|
|
@@ -136,21 +133,15 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
|
|
|
136
133
|
if (!tierCost.isZero()) {
|
|
137
134
|
const balanceAfterRuntime = await cfg.ledger.balance(tenantId);
|
|
138
135
|
if (!balanceAfterRuntime.lessThan(tierCost)) {
|
|
139
|
-
await cfg.ledger.debit(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
"Daily resource tier surcharge",
|
|
144
|
-
`runtime-tier:${cfg.date}:${tenantId}`,
|
|
145
|
-
);
|
|
136
|
+
await cfg.ledger.debit(tenantId, tierCost, "resource_upgrade", {
|
|
137
|
+
description: "Daily resource tier surcharge",
|
|
138
|
+
referenceId: `runtime-tier:${cfg.date}:${tenantId}`,
|
|
139
|
+
});
|
|
146
140
|
} else if (balanceAfterRuntime.greaterThan(Credit.ZERO)) {
|
|
147
|
-
await cfg.ledger.debit(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
"Partial resource tier surcharge (balance exhausted)",
|
|
152
|
-
`runtime-tier:${cfg.date}:${tenantId}`,
|
|
153
|
-
);
|
|
141
|
+
await cfg.ledger.debit(tenantId, balanceAfterRuntime, "resource_upgrade", {
|
|
142
|
+
description: "Partial resource tier surcharge (balance exhausted)",
|
|
143
|
+
referenceId: `runtime-tier:${cfg.date}:${tenantId}`,
|
|
144
|
+
});
|
|
154
145
|
}
|
|
155
146
|
}
|
|
156
147
|
}
|
|
@@ -190,23 +181,17 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
|
|
|
190
181
|
if (!storageCost.isZero()) {
|
|
191
182
|
const currentBalance = await cfg.ledger.balance(tenantId);
|
|
192
183
|
if (!currentBalance.lessThan(storageCost)) {
|
|
193
|
-
await cfg.ledger.debit(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
"Daily storage tier surcharge",
|
|
198
|
-
`runtime-storage:${cfg.date}:${tenantId}`,
|
|
199
|
-
);
|
|
184
|
+
await cfg.ledger.debit(tenantId, storageCost, "storage_upgrade", {
|
|
185
|
+
description: "Daily storage tier surcharge",
|
|
186
|
+
referenceId: `runtime-storage:${cfg.date}:${tenantId}`,
|
|
187
|
+
});
|
|
200
188
|
} else {
|
|
201
189
|
// Partial debit — take what's left, then suspend
|
|
202
190
|
if (currentBalance.greaterThan(Credit.ZERO)) {
|
|
203
|
-
await cfg.ledger.debit(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
"Partial storage tier surcharge (balance exhausted)",
|
|
208
|
-
`runtime-storage:${cfg.date}:${tenantId}`,
|
|
209
|
-
);
|
|
191
|
+
await cfg.ledger.debit(tenantId, currentBalance, "storage_upgrade", {
|
|
192
|
+
description: "Partial storage tier surcharge (balance exhausted)",
|
|
193
|
+
referenceId: `runtime-storage:${cfg.date}:${tenantId}`,
|
|
194
|
+
});
|
|
210
195
|
}
|
|
211
196
|
if (!result.suspended.includes(tenantId)) {
|
|
212
197
|
result.suspended.push(tenantId);
|
|
@@ -222,23 +207,17 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
|
|
|
222
207
|
if (!addonCost.isZero()) {
|
|
223
208
|
const currentBalance = await cfg.ledger.balance(tenantId);
|
|
224
209
|
if (!currentBalance.lessThan(addonCost)) {
|
|
225
|
-
await cfg.ledger.debit(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
"Daily infrastructure add-on charges",
|
|
230
|
-
`runtime-addon:${cfg.date}:${tenantId}`,
|
|
231
|
-
);
|
|
210
|
+
await cfg.ledger.debit(tenantId, addonCost, "addon", {
|
|
211
|
+
description: "Daily infrastructure add-on charges",
|
|
212
|
+
referenceId: `runtime-addon:${cfg.date}:${tenantId}`,
|
|
213
|
+
});
|
|
232
214
|
} else {
|
|
233
215
|
// Partial debit — take what's left, then suspend
|
|
234
216
|
if (currentBalance.greaterThan(Credit.ZERO)) {
|
|
235
|
-
await cfg.ledger.debit(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
"Partial add-on charges (balance exhausted)",
|
|
240
|
-
`runtime-addon:${cfg.date}:${tenantId}`,
|
|
241
|
-
);
|
|
217
|
+
await cfg.ledger.debit(tenantId, currentBalance, "addon", {
|
|
218
|
+
description: "Partial add-on charges (balance exhausted)",
|
|
219
|
+
referenceId: `runtime-addon:${cfg.date}:${tenantId}`,
|
|
220
|
+
});
|
|
242
221
|
}
|
|
243
222
|
if (!result.suspended.includes(tenantId)) {
|
|
244
223
|
result.suspended.push(tenantId);
|
|
@@ -250,13 +229,10 @@ export async function runRuntimeDeductions(cfg: RuntimeCronConfig): Promise<Runt
|
|
|
250
229
|
} else {
|
|
251
230
|
// Partial deduction — debit remaining balance, then suspend
|
|
252
231
|
if (balance.greaterThan(Credit.ZERO)) {
|
|
253
|
-
await cfg.ledger.debit(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
`Partial daily runtime (balance exhausted): ${botCount} bot(s)`,
|
|
258
|
-
runtimeRef,
|
|
259
|
-
);
|
|
232
|
+
await cfg.ledger.debit(tenantId, balance, "bot_runtime", {
|
|
233
|
+
description: `Partial daily runtime (balance exhausted): ${botCount} bot(s)`,
|
|
234
|
+
referenceId: runtimeRef,
|
|
235
|
+
});
|
|
260
236
|
}
|
|
261
237
|
|
|
262
238
|
if (cfg.onCreditsExhausted) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { RUNTIME_INTERVAL_MS, startRuntimeScheduler } from "./runtime-scheduler.js";
|
|
3
3
|
|
|
4
|
-
// Minimal
|
|
4
|
+
// Minimal ILedger stub — only the methods runRuntimeDeductions calls.
|
|
5
5
|
function makeLedger() {
|
|
6
6
|
return {
|
|
7
7
|
tenantsWithBalance: vi.fn().mockResolvedValue([]),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { logger } from "../../config/logger.js";
|
|
3
3
|
import type { IBotInstanceRepository } from "../../fleet/bot-instance-repository.js";
|
|
4
4
|
import { buildAddonCosts } from "../addons/addon-cron.js";
|
|
@@ -6,7 +6,7 @@ import type { ITenantAddonRepository } from "../addons/addon-repository.js";
|
|
|
6
6
|
import { buildResourceTierCosts, runRuntimeDeductions } from "./runtime-cron.js";
|
|
7
7
|
|
|
8
8
|
export interface RuntimeSchedulerDeps {
|
|
9
|
-
ledger:
|
|
9
|
+
ledger: ILedger;
|
|
10
10
|
botInstanceRepo: IBotInstanceRepository;
|
|
11
11
|
tenantAddonRepo: ITenantAddonRepository;
|
|
12
12
|
onSuspend?: (tenantId: string) => void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PGlite } from "@electric-sql/pglite";
|
|
2
|
-
import {
|
|
2
|
+
import { DrizzleLedger, grantSignupCredits, SIGNUP_GRANT } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import type { DrizzleDb } from "../../db/index.js";
|
|
5
5
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
@@ -7,7 +7,7 @@ import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
|
7
7
|
describe("grantSignupCredits", () => {
|
|
8
8
|
let pool: PGlite;
|
|
9
9
|
let db: DrizzleDb;
|
|
10
|
-
let ledger:
|
|
10
|
+
let ledger: DrizzleLedger;
|
|
11
11
|
|
|
12
12
|
beforeAll(async () => {
|
|
13
13
|
({ db, pool } = await createTestDb());
|
|
@@ -19,7 +19,9 @@ describe("grantSignupCredits", () => {
|
|
|
19
19
|
|
|
20
20
|
beforeEach(async () => {
|
|
21
21
|
await truncateAllTables(pool);
|
|
22
|
-
ledger = new
|
|
22
|
+
ledger = new DrizzleLedger(db);
|
|
23
|
+
|
|
24
|
+
await ledger.seedSystemAccounts();
|
|
23
25
|
});
|
|
24
26
|
|
|
25
27
|
it("grants credits to a new tenant and returns true", async () => {
|
|
@@ -52,7 +54,8 @@ describe("grantSignupCredits", () => {
|
|
|
52
54
|
const uniqueErr = Object.assign(new Error("duplicate key value violates unique constraint"), {
|
|
53
55
|
code: "23505",
|
|
54
56
|
});
|
|
55
|
-
const racingLedger = new
|
|
57
|
+
const racingLedger = new DrizzleLedger(db);
|
|
58
|
+
await racingLedger.seedSystemAccounts();
|
|
56
59
|
vi.spyOn(racingLedger, "hasReferenceId").mockResolvedValue(false);
|
|
57
60
|
vi.spyOn(racingLedger, "credit").mockRejectedValue(uniqueErr);
|
|
58
61
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PGlite } from "@electric-sql/pglite";
|
|
2
|
-
import { Credit,
|
|
2
|
+
import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
4
4
|
import type { DrizzleDb } from "../../db/index.js";
|
|
5
5
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
@@ -9,7 +9,7 @@ describe("runtime cron with storage tiers", () => {
|
|
|
9
9
|
const TODAY = "2025-01-01";
|
|
10
10
|
let pool: PGlite;
|
|
11
11
|
let db: DrizzleDb;
|
|
12
|
-
let ledger:
|
|
12
|
+
let ledger: DrizzleLedger;
|
|
13
13
|
|
|
14
14
|
beforeAll(async () => {
|
|
15
15
|
({ db, pool } = await createTestDb());
|
|
@@ -21,7 +21,9 @@ describe("runtime cron with storage tiers", () => {
|
|
|
21
21
|
|
|
22
22
|
beforeEach(async () => {
|
|
23
23
|
await truncateAllTables(pool);
|
|
24
|
-
ledger = new
|
|
24
|
+
ledger = new DrizzleLedger(db);
|
|
25
|
+
|
|
26
|
+
await ledger.seedSystemAccounts();
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
it("debits base cost plus storage surcharge for pro tier", async () => {
|