@wopr-network/platform-core 1.13.3 → 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/index.d.ts +1 -0
- package/dist/db/schema/index.js +1 -0
- package/dist/db/schema/ledger.d.ts +442 -0
- package/dist/db/schema/ledger.js +76 -0
- package/dist/gateway/credit-gate.d.ts +2 -2
- package/dist/gateway/credit-gate.js +5 -1
- package/dist/gateway/credit-gate.test.js +35 -33
- package/dist/gateway/protocol/deps.d.ts +2 -2
- package/dist/gateway/proxy.d.ts +2 -2
- package/dist/gateway/types.d.ts +2 -2
- package/dist/metering/reconciliation-cron.test.js +9 -8
- package/dist/metering/reconciliation-repository.js +12 -10
- package/dist/metering/reconciliation-repository.test.js +9 -8
- package/dist/monetization/affiliate/affiliate-admin-repository.js +10 -8
- package/dist/monetization/affiliate/affiliate-admin-repository.test.js +32 -13
- package/dist/monetization/affiliate/credit-match.d.ts +2 -2
- package/dist/monetization/affiliate/credit-match.js +4 -1
- package/dist/monetization/affiliate/credit-match.test.js +58 -13
- package/dist/monetization/affiliate/new-user-bonus.d.ts +2 -2
- package/dist/monetization/affiliate/new-user-bonus.js +4 -1
- package/dist/monetization/affiliate/new-user-bonus.test.js +4 -3
- package/dist/monetization/credits/auto-topup-charge.d.ts +2 -2
- package/dist/monetization/credits/auto-topup-charge.js +5 -1
- package/dist/monetization/credits/auto-topup-charge.test.js +5 -4
- package/dist/monetization/credits/auto-topup-usage.d.ts +2 -2
- package/dist/monetization/credits/auto-topup-usage.test.js +53 -12
- package/dist/monetization/credits/bot-billing.d.ts +3 -3
- package/dist/monetization/credits/bot-billing.test.js +18 -5
- package/dist/monetization/credits/credit-expiry-cron.test.js +25 -8
- package/dist/monetization/credits/dividend-cron.d.ts +2 -4
- package/dist/monetization/credits/dividend-cron.js +7 -4
- package/dist/monetization/credits/dividend-cron.test.js +26 -46
- package/dist/monetization/credits/dividend-repository.js +15 -24
- package/dist/monetization/credits/dividend-repository.test.js +4 -3
- package/dist/monetization/credits/index.d.ts +2 -2
- package/dist/monetization/credits/index.js +1 -1
- package/dist/monetization/credits/member-usage.test.js +23 -10
- package/dist/monetization/credits/phone-billing.d.ts +2 -2
- package/dist/monetization/credits/phone-billing.js +5 -1
- package/dist/monetization/credits/phone-billing.test.js +9 -12
- package/dist/monetization/credits/runtime-cron.d.ts +2 -2
- package/dist/monetization/credits/runtime-cron.js +32 -8
- package/dist/monetization/credits/runtime-cron.test.js +28 -27
- package/dist/monetization/credits/runtime-scheduler.d.ts +2 -2
- package/dist/monetization/credits/runtime-scheduler.test.js +1 -1
- package/dist/monetization/credits/signup-grant.test.js +5 -3
- package/dist/monetization/credits/storage-tier-cron.test.js +3 -2
- package/dist/monetization/credits/trial-balance-cron.test.js +42 -0
- package/dist/monetization/feature-gate.d.ts +3 -3
- package/dist/monetization/index.d.ts +3 -3
- package/dist/monetization/index.js +1 -1
- package/dist/monetization/metering/reconciliation-cron.test.js +9 -8
- package/dist/monetization/metering/reconciliation-repository.js +11 -10
- package/dist/monetization/metering/reconciliation-repository.test.js +9 -8
- package/dist/monetization/payram/webhook.d.ts +2 -2
- package/dist/monetization/payram/webhook.js +5 -1
- package/dist/monetization/payram/webhook.test.js +5 -4
- package/dist/monetization/promotions/engine.d.ts +2 -2
- package/dist/monetization/promotions/engine.js +4 -1
- package/dist/monetization/promotions/engine.test.js +3 -1
- package/dist/monetization/repository-types.d.ts +1 -1
- package/dist/monetization/stripe/stripe-payment-processor.d.ts +2 -2
- package/dist/monetization/stripe/stripe-payment-processor.test.js +7 -0
- package/dist/monetization/stripe/webhook.d.ts +2 -2
- package/dist/monetization/stripe/webhook.js +70 -6
- package/dist/monetization/stripe/webhook.test.js +20 -15
- package/dist/onboarding/onboarding-service.d.ts +2 -2
- package/dist/onboarding/onboarding-service.js +6 -2
- package/drizzle/migrations/0003_double_entry_ledger.sql +82 -0
- package/drizzle/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/src/api/routes/admin-credits.ts +11 -14
- package/src/api/routes/quota.ts +2 -2
- package/src/api/routes/verify-email.ts +4 -4
- package/src/backup/on-demand-snapshot-service.test.ts +3 -3
- package/src/backup/on-demand-snapshot-service.ts +3 -3
- package/src/billing/payram/webhook.test.ts +7 -5
- package/src/billing/payram/webhook.ts +8 -11
- package/src/billing/stripe/stripe-payment-processor.test.ts +10 -3
- package/src/billing/stripe/stripe-payment-processor.ts +3 -3
- package/src/billing/stripe/tenant-store.ts +1 -1
- package/src/credits/auto-topup-charge.test.ts +7 -5
- package/src/credits/auto-topup-charge.ts +7 -10
- package/src/credits/auto-topup-usage.test.ts +55 -13
- package/src/credits/auto-topup-usage.ts +2 -2
- package/src/credits/credit-expiry-cron.test.ts +26 -45
- package/src/credits/credit-expiry-cron.ts +9 -12
- package/src/credits/credit-ledger.ts +3 -3
- package/src/credits/dividend-cron.test.ts +38 -45
- package/src/credits/dividend-cron.ts +12 -26
- package/src/credits/dividend-repository.test.ts +4 -3
- package/src/credits/dividend-repository.ts +21 -23
- package/src/credits/index.ts +23 -4
- package/src/credits/ledger.test.ts +514 -0
- package/src/credits/ledger.ts +851 -0
- package/src/credits/signup-grant.test.ts +7 -4
- package/src/credits/signup-grant.ts +6 -12
- package/src/credits/trial-balance-cron.test.ts +68 -0
- package/src/credits/trial-balance-cron.ts +46 -0
- package/src/db/schema/index.ts +1 -0
- package/src/db/schema/ledger.ts +94 -0
- package/src/gateway/credit-gate-wiring.test.ts +3 -3
- package/src/gateway/credit-gate.test.ts +35 -33
- package/src/gateway/credit-gate.ts +6 -10
- package/src/gateway/gateway-routes.test.ts +5 -5
- package/src/gateway/protocol/deps.ts +2 -2
- package/src/gateway/proxy.ts +2 -2
- package/src/gateway/route-mounting.test.ts +2 -2
- package/src/gateway/types.ts +2 -2
- package/src/metering/reconciliation-cron.test.ts +10 -9
- package/src/metering/reconciliation-repository.test.ts +10 -9
- package/src/metering/reconciliation-repository.ts +14 -11
- package/src/monetization/affiliate/affiliate-admin-repository.test.ts +32 -19
- package/src/monetization/affiliate/affiliate-admin-repository.ts +16 -8
- package/src/monetization/affiliate/credit-match.test.ts +60 -14
- package/src/monetization/affiliate/credit-match.ts +6 -9
- package/src/monetization/affiliate/new-user-bonus.test.ts +6 -4
- package/src/monetization/affiliate/new-user-bonus.ts +6 -9
- package/src/monetization/credits/auto-topup-charge.test.ts +7 -5
- package/src/monetization/credits/auto-topup-charge.ts +7 -10
- package/src/monetization/credits/auto-topup-usage.test.ts +55 -13
- package/src/monetization/credits/auto-topup-usage.ts +2 -2
- package/src/monetization/credits/bot-billing.test.ts +20 -6
- package/src/monetization/credits/bot-billing.ts +3 -3
- package/src/monetization/credits/credit-expiry-cron.test.ts +26 -45
- package/src/monetization/credits/dividend-cron.test.ts +34 -48
- package/src/monetization/credits/dividend-cron.ts +9 -14
- package/src/monetization/credits/dividend-repository.test.ts +4 -3
- package/src/monetization/credits/dividend-repository.ts +19 -25
- package/src/monetization/credits/index.ts +4 -4
- package/src/monetization/credits/member-usage.test.ts +25 -11
- package/src/monetization/credits/phone-billing.test.ts +18 -26
- package/src/monetization/credits/phone-billing.ts +7 -10
- package/src/monetization/credits/runtime-cron.test.ts +29 -28
- package/src/monetization/credits/runtime-cron.ts +34 -58
- package/src/monetization/credits/runtime-scheduler.test.ts +1 -1
- package/src/monetization/credits/runtime-scheduler.ts +2 -2
- package/src/monetization/credits/signup-grant.test.ts +7 -4
- package/src/monetization/credits/storage-tier-cron.test.ts +5 -3
- package/src/monetization/credits/trial-balance-cron.test.ts +52 -0
- package/src/monetization/feature-gate.ts +3 -3
- package/src/monetization/index.ts +4 -4
- package/src/monetization/metering/reconciliation-cron.test.ts +10 -9
- package/src/monetization/metering/reconciliation-repository.test.ts +11 -9
- package/src/monetization/metering/reconciliation-repository.ts +13 -11
- package/src/monetization/payram/webhook.test.ts +7 -5
- package/src/monetization/payram/webhook.ts +7 -10
- package/src/monetization/promotions/engine.test.ts +6 -5
- package/src/monetization/promotions/engine.ts +6 -3
- package/src/monetization/repository-types.ts +1 -1
- package/src/monetization/stripe/stripe-payment-processor.test.ts +10 -3
- package/src/monetization/stripe/stripe-payment-processor.ts +3 -3
- package/src/monetization/stripe/webhook.test.ts +22 -16
- package/src/monetization/stripe/webhook.ts +75 -50
- package/src/onboarding/onboarding-service.ts +8 -11
- package/dist/credits/credit-ledger-extra.test.js +0 -40
- package/dist/credits/credit-ledger.bench.js +0 -33
- package/dist/credits/credit-ledger.test.d.ts +0 -4
- package/dist/credits/credit-ledger.test.js +0 -203
- package/dist/credits/credit-transaction-repository.test.js +0 -232
- package/dist/monetization/credits/credit-ledger-extra.test.d.ts +0 -1
- package/dist/monetization/credits/credit-ledger-extra.test.js +0 -39
- package/dist/monetization/credits/credit-ledger.bench.d.ts +0 -1
- package/dist/monetization/credits/credit-ledger.bench.js +0 -32
- package/dist/monetization/credits/credit-ledger.test.d.ts +0 -4
- package/dist/monetization/credits/credit-ledger.test.js +0 -202
- package/dist/monetization/credits/credit-transaction-repository.test.d.ts +0 -1
- package/dist/monetization/credits/credit-transaction-repository.test.js +0 -232
- package/src/credits/credit-ledger-extra.test.ts +0 -57
- package/src/credits/credit-ledger.bench.ts +0 -56
- package/src/credits/credit-ledger.test.ts +0 -276
- package/src/credits/credit-transaction-repository.test.ts +0 -274
- package/src/monetization/credits/credit-ledger-extra.test.ts +0 -56
- package/src/monetization/credits/credit-ledger.bench.ts +0 -55
- package/src/monetization/credits/credit-ledger.test.ts +0 -275
- package/src/monetization/credits/credit-transaction-repository.test.ts +0 -274
- /package/dist/credits/{credit-ledger-extra.test.d.ts → ledger.test.d.ts} +0 -0
- /package/dist/credits/{credit-ledger.bench.d.ts → trial-balance-cron.test.d.ts} +0 -0
- /package/dist/{credits/credit-transaction-repository.test.d.ts → monetization/credits/trial-balance-cron.test.d.ts} +0 -0
|
@@ -1,40 +1,25 @@
|
|
|
1
|
-
import { Credit,
|
|
1
|
+
import { CREDIT_TYPE_ACCOUNT, Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
-
import { creditBalances, creditTransactions } from "../../db/schema/credits.js";
|
|
4
3
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
5
|
-
import { DrizzleCreditTransactionRepository } from "./credit-transaction-repository.js";
|
|
6
4
|
import { runDividendCron } from "./dividend-cron.js";
|
|
7
|
-
async function insertPurchase(
|
|
8
|
-
const id = `test-${tenantId}-${Date.now()}-${Math.random()}`;
|
|
5
|
+
async function insertPurchase(ledger, tenantId, amountCents, postedAt) {
|
|
9
6
|
const amount = Credit.fromCents(amountCents);
|
|
10
|
-
await
|
|
11
|
-
|
|
7
|
+
await ledger.post({
|
|
8
|
+
entryType: "purchase",
|
|
12
9
|
tenantId,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
description: `Test purchase ${amountCents}¢`,
|
|
11
|
+
referenceId: `test-purchase:${tenantId}:${postedAt}:${Math.random()}`,
|
|
12
|
+
postedAt,
|
|
13
|
+
lines: [
|
|
14
|
+
{ accountCode: CREDIT_TYPE_ACCOUNT.purchase, amount, side: "debit" },
|
|
15
|
+
{ accountCode: `2000:${tenantId}`, amount, side: "credit" },
|
|
16
|
+
],
|
|
17
17
|
});
|
|
18
|
-
// Upsert credit_balances
|
|
19
|
-
const existing = await db
|
|
20
|
-
.select()
|
|
21
|
-
.from(creditBalances)
|
|
22
|
-
.where((await import("drizzle-orm")).eq(creditBalances.tenantId, tenantId));
|
|
23
|
-
if (existing.length > 0) {
|
|
24
|
-
await db
|
|
25
|
-
.update(creditBalances)
|
|
26
|
-
.set({ balance: existing[0].balance.add(amount) })
|
|
27
|
-
.where((await import("drizzle-orm")).eq(creditBalances.tenantId, tenantId));
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
await db.insert(creditBalances).values({ tenantId, balance: amount });
|
|
31
|
-
}
|
|
32
18
|
}
|
|
33
19
|
describe("runDividendCron", () => {
|
|
34
20
|
let pool;
|
|
35
21
|
let db;
|
|
36
22
|
let ledger;
|
|
37
|
-
let creditTransactionRepo;
|
|
38
23
|
beforeAll(async () => {
|
|
39
24
|
({ db, pool } = await createTestDb());
|
|
40
25
|
});
|
|
@@ -43,12 +28,11 @@ describe("runDividendCron", () => {
|
|
|
43
28
|
});
|
|
44
29
|
beforeEach(async () => {
|
|
45
30
|
await truncateAllTables(pool);
|
|
46
|
-
ledger = new
|
|
47
|
-
|
|
31
|
+
ledger = new DrizzleLedger(db);
|
|
32
|
+
await ledger.seedSystemAccounts();
|
|
48
33
|
});
|
|
49
34
|
function makeConfig(overrides) {
|
|
50
35
|
return {
|
|
51
|
-
creditTransactionRepo,
|
|
52
36
|
ledger,
|
|
53
37
|
matchRate: 1.0,
|
|
54
38
|
targetDate: "2026-02-20",
|
|
@@ -56,7 +40,7 @@ describe("runDividendCron", () => {
|
|
|
56
40
|
};
|
|
57
41
|
}
|
|
58
42
|
it("distributes dividend to eligible tenants", async () => {
|
|
59
|
-
await insertPurchase(
|
|
43
|
+
await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
|
|
60
44
|
const result = await runDividendCron(makeConfig());
|
|
61
45
|
expect(result.distributed).toBe(1);
|
|
62
46
|
expect(result.pool.toCents()).toBe(1000);
|
|
@@ -64,7 +48,7 @@ describe("runDividendCron", () => {
|
|
|
64
48
|
expect(result.activeCount).toBe(1);
|
|
65
49
|
});
|
|
66
50
|
it("is idempotent — skips if already ran for the date", async () => {
|
|
67
|
-
await insertPurchase(
|
|
51
|
+
await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
|
|
68
52
|
const result1 = await runDividendCron(makeConfig());
|
|
69
53
|
expect(result1.distributed).toBe(1);
|
|
70
54
|
expect(result1.skippedAlreadyRun).toBe(false);
|
|
@@ -75,20 +59,19 @@ describe("runDividendCron", () => {
|
|
|
75
59
|
expect((await ledger.balance("t1")).equals(balanceAfterFirst)).toBe(true);
|
|
76
60
|
});
|
|
77
61
|
it("handles floor rounding — remainder is not distributed", async () => {
|
|
78
|
-
await insertPurchase(
|
|
79
|
-
await insertPurchase(
|
|
80
|
-
await insertPurchase(
|
|
62
|
+
await insertPurchase(ledger, "t1", 50, "2026-02-20 12:00:00");
|
|
63
|
+
await insertPurchase(ledger, "t2", 30, "2026-02-20 12:00:00");
|
|
64
|
+
await insertPurchase(ledger, "t3", 20, "2026-02-20 12:00:00");
|
|
81
65
|
const result = await runDividendCron(makeConfig());
|
|
82
66
|
expect(result.pool.toCents()).toBe(100);
|
|
83
67
|
expect(result.activeCount).toBe(3);
|
|
84
68
|
// Nanodollar precision: floor(1_000_000_000 raw / 3) = 333_333_333 raw each
|
|
85
|
-
// Remainder = 1 nanodollar (not 1 cent — far less wasted with higher scale)
|
|
86
69
|
expect(result.perUser.toRaw()).toBe(333_333_333);
|
|
87
70
|
expect(result.distributed).toBe(3);
|
|
88
71
|
});
|
|
89
72
|
it("skips distribution when pool is zero", async () => {
|
|
90
73
|
// Tenant purchased within 7 days but NOT on target date -> pool = 0
|
|
91
|
-
await insertPurchase(
|
|
74
|
+
await insertPurchase(ledger, "t1", 500, "2026-02-18 12:00:00");
|
|
92
75
|
const result = await runDividendCron(makeConfig());
|
|
93
76
|
expect(result.pool.toCents()).toBe(0);
|
|
94
77
|
expect(result.activeCount).toBe(1);
|
|
@@ -96,11 +79,9 @@ describe("runDividendCron", () => {
|
|
|
96
79
|
expect(result.distributed).toBe(0);
|
|
97
80
|
});
|
|
98
81
|
it("distributes sub-cent amounts at nanodollar precision", async () => {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
await insertPurchase(
|
|
102
|
-
await insertPurchase(db, "t2", 500, "2026-02-18 12:00:00");
|
|
103
|
-
await insertPurchase(db, "t3", 500, "2026-02-17 12:00:00");
|
|
82
|
+
await insertPurchase(ledger, "t1", 1, "2026-02-20 12:00:00");
|
|
83
|
+
await insertPurchase(ledger, "t2", 500, "2026-02-18 12:00:00");
|
|
84
|
+
await insertPurchase(ledger, "t3", 500, "2026-02-17 12:00:00");
|
|
104
85
|
const result = await runDividendCron(makeConfig({ matchRate: 1.0 }));
|
|
105
86
|
expect(result.pool.toCents()).toBe(1);
|
|
106
87
|
expect(result.activeCount).toBe(3);
|
|
@@ -108,18 +89,17 @@ describe("runDividendCron", () => {
|
|
|
108
89
|
expect(result.distributed).toBe(3);
|
|
109
90
|
});
|
|
110
91
|
it("records transactions with correct type and referenceId", async () => {
|
|
111
|
-
await insertPurchase(
|
|
92
|
+
await insertPurchase(ledger, "t1", 1000, "2026-02-20 12:00:00");
|
|
112
93
|
await runDividendCron(makeConfig());
|
|
113
94
|
const history = await ledger.history("t1", { type: "community_dividend" });
|
|
114
95
|
expect(history).toHaveLength(1);
|
|
115
|
-
expect(history[0].
|
|
96
|
+
expect(history[0].entryType).toBe("community_dividend");
|
|
116
97
|
expect(history[0].referenceId).toBe("dividend:2026-02-20:t1");
|
|
117
|
-
expect(history[0].amount.toCents()).toBe(1000);
|
|
118
98
|
expect(history[0].description).toContain("Community dividend");
|
|
119
99
|
});
|
|
120
100
|
it("collects errors without stopping distribution to other tenants", async () => {
|
|
121
|
-
await insertPurchase(
|
|
122
|
-
await insertPurchase(
|
|
101
|
+
await insertPurchase(ledger, "t1", 500, "2026-02-20 12:00:00");
|
|
102
|
+
await insertPurchase(ledger, "t2", 500, "2026-02-20 12:00:00");
|
|
123
103
|
const result = await runDividendCron(makeConfig());
|
|
124
104
|
expect(result.distributed).toBe(2);
|
|
125
105
|
expect(result.errors).toEqual([]);
|
|
@@ -1,32 +1,26 @@
|
|
|
1
1
|
import { Credit } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { and, desc, eq, gte, lt, sql } from "drizzle-orm";
|
|
3
3
|
import { adminUsers } from "../../db/schema/admin-users.js";
|
|
4
|
-
import { creditTransactions } from "../../db/schema/credits.js";
|
|
5
4
|
import { dividendDistributions } from "../../db/schema/dividend-distributions.js";
|
|
5
|
+
import { journalEntries, journalLines } from "../../db/schema/ledger.js";
|
|
6
6
|
export class DrizzleDividendRepository {
|
|
7
7
|
db;
|
|
8
8
|
constructor(db) {
|
|
9
9
|
this.db = db;
|
|
10
10
|
}
|
|
11
11
|
async getStats(tenantId) {
|
|
12
|
-
// 1. Pool = sum of purchase amounts from yesterday UTC
|
|
12
|
+
// 1. Pool = sum of purchase credit amounts from yesterday UTC
|
|
13
13
|
const poolRow = (await this.db
|
|
14
|
-
|
|
15
|
-
.
|
|
16
|
-
.
|
|
17
|
-
.where(and(eq(
|
|
18
|
-
|
|
19
|
-
sql `${creditTransactions.createdAt}::timestamp >= date_trunc('day', timezone('UTC', now())) - INTERVAL '1 day'`, sql `${creditTransactions.createdAt}::timestamp < date_trunc('day', timezone('UTC', now()))`)))[0];
|
|
20
|
-
const poolCents = poolRow?.total ?? 0;
|
|
21
|
-
const pool = Credit.fromCents(poolCents);
|
|
14
|
+
.select({ total: sql `COALESCE(SUM(${journalLines.amount}), 0)` })
|
|
15
|
+
.from(journalLines)
|
|
16
|
+
.innerJoin(journalEntries, eq(journalEntries.id, journalLines.journalEntryId))
|
|
17
|
+
.where(and(eq(journalEntries.entryType, "purchase"), eq(journalLines.side, "credit"), sql `${journalEntries.postedAt}::timestamp >= date_trunc('day', timezone('UTC', now())) - INTERVAL '1 day'`, sql `${journalEntries.postedAt}::timestamp < date_trunc('day', timezone('UTC', now()))`)))[0];
|
|
18
|
+
const pool = Credit.fromRaw(Number(poolRow?.total ?? 0));
|
|
22
19
|
// 2. Active users = distinct tenants with a purchase in the last 7 days
|
|
23
20
|
const activeRow = (await this.db
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
.
|
|
27
|
-
.where(and(eq(creditTransactions.type, "purchase"),
|
|
28
|
-
// raw SQL: Drizzle cannot express timestamp comparison with interval arithmetic
|
|
29
|
-
sql `${creditTransactions.createdAt}::timestamp >= timezone('UTC', now()) - INTERVAL '7 days'`)))[0];
|
|
21
|
+
.select({ count: sql `COUNT(DISTINCT ${journalEntries.tenantId})` })
|
|
22
|
+
.from(journalEntries)
|
|
23
|
+
.where(and(eq(journalEntries.entryType, "purchase"), sql `${journalEntries.postedAt}::timestamp >= timezone('UTC', now()) - INTERVAL '7 days'`)))[0];
|
|
30
24
|
const activeUsers = activeRow?.count ?? 0;
|
|
31
25
|
// 3. Per-user projection (avoid division by zero)
|
|
32
26
|
const perUser = activeUsers > 0 ? Credit.fromRaw(Math.floor(pool.toRaw() / activeUsers)) : Credit.ZERO;
|
|
@@ -36,19 +30,16 @@ export class DrizzleDividendRepository {
|
|
|
36
30
|
const nextDistributionAt = nextMidnight.toISOString();
|
|
37
31
|
// 5. User eligibility — last purchase within 7 days
|
|
38
32
|
const userPurchaseRow = (await this.db
|
|
39
|
-
.select({
|
|
40
|
-
.from(
|
|
41
|
-
.where(and(eq(
|
|
42
|
-
.orderBy(desc(
|
|
33
|
+
.select({ postedAt: journalEntries.postedAt })
|
|
34
|
+
.from(journalEntries)
|
|
35
|
+
.where(and(eq(journalEntries.tenantId, tenantId), eq(journalEntries.entryType, "purchase")))
|
|
36
|
+
.orderBy(desc(journalEntries.postedAt))
|
|
43
37
|
.limit(1))[0];
|
|
44
38
|
let userEligible = false;
|
|
45
39
|
let userLastPurchaseAt = null;
|
|
46
40
|
let userWindowExpiresAt = null;
|
|
47
41
|
if (userPurchaseRow) {
|
|
48
|
-
const
|
|
49
|
-
// Parse the timestamp directly. PGlite may return ISO strings with or without
|
|
50
|
-
// timezone suffix. JavaScript's Date constructor handles ISO 8601 strings natively.
|
|
51
|
-
const lastPurchase = new Date(rawTs);
|
|
42
|
+
const lastPurchase = new Date(userPurchaseRow.postedAt);
|
|
52
43
|
userLastPurchaseAt = lastPurchase.toISOString();
|
|
53
44
|
const windowExpiry = new Date(lastPurchase.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
54
45
|
userWindowExpiresAt = windowExpiry.toISOString();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
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 { adminUsers } from "../../db/schema/admin-users.js";
|
|
5
5
|
import { dividendDistributions } from "../../db/schema/dividend-distributions.js";
|
|
@@ -39,6 +39,7 @@ describe("DrizzleDividendRepository", () => {
|
|
|
39
39
|
let repo;
|
|
40
40
|
beforeEach(async () => {
|
|
41
41
|
await truncateAllTables(pool);
|
|
42
|
+
await new DrizzleLedger(db).seedSystemAccounts();
|
|
42
43
|
repo = new DrizzleDividendRepository(db);
|
|
43
44
|
});
|
|
44
45
|
// --- getHistory() ---
|
|
@@ -152,8 +153,8 @@ describe("DrizzleDividendRepository", () => {
|
|
|
152
153
|
expect(stats.nextDistributionAt).toEqual(expect.any(String));
|
|
153
154
|
});
|
|
154
155
|
it("marks user as eligible when they have a recent purchase", async () => {
|
|
155
|
-
const ledger = new
|
|
156
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase", "recent buy");
|
|
156
|
+
const ledger = new DrizzleLedger(db);
|
|
157
|
+
await ledger.credit("t1", Credit.fromCents(100), "purchase", { description: "recent buy" });
|
|
157
158
|
const stats = await repo.getStats("t1");
|
|
158
159
|
expect(stats.userEligible).toBe(true);
|
|
159
160
|
expect(stats.userLastPurchaseAt).toEqual(expect.any(String));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export type { AutoTopupSettings, CreditExpiryCronConfig, CreditExpiryCronResult,
|
|
2
|
-
export { ALLOWED_SCHEDULE_INTERVALS, ALLOWED_THRESHOLDS, ALLOWED_TOPUP_AMOUNTS,
|
|
1
|
+
export type { AutoTopupSettings, CreditExpiryCronConfig, CreditExpiryCronResult, CreditType, DebitType, HistoryOptions, IAutoTopupSettingsRepository, ILedger, JournalEntry, TransactionType, } from "@wopr-network/platform-core/credits";
|
|
2
|
+
export { ALLOWED_SCHEDULE_INTERVALS, ALLOWED_THRESHOLDS, ALLOWED_TOPUP_AMOUNTS, computeNextScheduleAt, DrizzleAutoTopupSettingsRepository, DrizzleLedger, grantSignupCredits, InsufficientBalanceError, Ledger, runCreditExpiryCron, SIGNUP_GRANT, } from "@wopr-network/platform-core/credits";
|
|
3
3
|
export type { BillingState, IBotBilling } from "./bot-billing.js";
|
|
4
4
|
export { BotBilling, DrizzleBotBilling, SUSPENSION_GRACE_DAYS } from "./bot-billing.js";
|
|
5
5
|
export type { DividendDigestConfig, DividendDigestResult } from "./dividend-digest-cron.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ALLOWED_SCHEDULE_INTERVALS, ALLOWED_THRESHOLDS, ALLOWED_TOPUP_AMOUNTS,
|
|
1
|
+
export { ALLOWED_SCHEDULE_INTERVALS, ALLOWED_THRESHOLDS, ALLOWED_TOPUP_AMOUNTS, computeNextScheduleAt, DrizzleAutoTopupSettingsRepository, DrizzleLedger, grantSignupCredits, InsufficientBalanceError, Ledger, runCreditExpiryCron, SIGNUP_GRANT, } from "@wopr-network/platform-core/credits";
|
|
2
2
|
export { BotBilling, DrizzleBotBilling, SUSPENSION_GRACE_DAYS } from "./bot-billing.js";
|
|
3
3
|
export { runDividendDigestCron } from "./dividend-digest-cron.js";
|
|
4
4
|
export { buildResourceTierCosts, DAILY_BOT_COST, runRuntimeDeductions } from "./runtime-cron.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Credit,
|
|
1
|
+
import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
3
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
4
|
-
describe("
|
|
4
|
+
describe("DrizzleDrizzleLedger.memberUsage", () => {
|
|
5
5
|
let pool;
|
|
6
6
|
let db;
|
|
7
7
|
let ledger;
|
|
@@ -13,13 +13,23 @@ describe("DrizzleCreditLedger.memberUsage", () => {
|
|
|
13
13
|
});
|
|
14
14
|
beforeEach(async () => {
|
|
15
15
|
await truncateAllTables(pool);
|
|
16
|
-
ledger = new
|
|
16
|
+
ledger = new DrizzleLedger(db);
|
|
17
|
+
await ledger.seedSystemAccounts();
|
|
17
18
|
});
|
|
18
19
|
it("should aggregate debit totals per attributed user", async () => {
|
|
19
|
-
await ledger.credit("org-1", Credit.fromCents(10000), "purchase", "Seed");
|
|
20
|
-
await ledger.debit("org-1", Credit.fromCents(100), "adapter_usage",
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
await ledger.credit("org-1", Credit.fromCents(10000), "purchase", { description: "Seed" });
|
|
21
|
+
await ledger.debit("org-1", Credit.fromCents(100), "adapter_usage", {
|
|
22
|
+
description: "Chat",
|
|
23
|
+
attributedUserId: "user-a",
|
|
24
|
+
});
|
|
25
|
+
await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage", {
|
|
26
|
+
description: "Chat",
|
|
27
|
+
attributedUserId: "user-a",
|
|
28
|
+
});
|
|
29
|
+
await ledger.debit("org-1", Credit.fromCents(300), "adapter_usage", {
|
|
30
|
+
description: "Chat",
|
|
31
|
+
attributedUserId: "user-b",
|
|
32
|
+
});
|
|
23
33
|
const result = await ledger.memberUsage("org-1");
|
|
24
34
|
expect(result).toHaveLength(2);
|
|
25
35
|
const userA = result.find((r) => r.userId === "user-a");
|
|
@@ -30,9 +40,12 @@ describe("DrizzleCreditLedger.memberUsage", () => {
|
|
|
30
40
|
expect(userB?.transactionCount).toBe(1);
|
|
31
41
|
});
|
|
32
42
|
it("should exclude transactions with null attributedUserId", async () => {
|
|
33
|
-
await ledger.credit("org-1", Credit.fromCents(10000), "purchase", "Seed");
|
|
34
|
-
await ledger.debit("org-1", Credit.fromCents(100), "bot_runtime", "Cron"); // no attributedUserId
|
|
35
|
-
await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage",
|
|
43
|
+
await ledger.credit("org-1", Credit.fromCents(10000), "purchase", { description: "Seed" });
|
|
44
|
+
await ledger.debit("org-1", Credit.fromCents(100), "bot_runtime", { description: "Cron" }); // no attributedUserId
|
|
45
|
+
await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage", {
|
|
46
|
+
description: "Chat",
|
|
47
|
+
attributedUserId: "user-a",
|
|
48
|
+
});
|
|
36
49
|
const result = await ledger.memberUsage("org-1");
|
|
37
50
|
expect(result).toHaveLength(1);
|
|
38
51
|
expect(result[0]?.userId).toBe("user-a");
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import type { IMeterEmitter } from "@wopr-network/platform-core/metering";
|
|
3
3
|
import type { IPhoneNumberRepository } from "./drizzle-phone-number-repository.js";
|
|
4
4
|
export type { IPhoneNumberRepository } from "./drizzle-phone-number-repository.js";
|
|
5
5
|
/** Phone number monthly wholesale cost in USD. Exported for proxy.ts to import. */
|
|
6
6
|
export declare const PHONE_NUMBER_MONTHLY_COST = 1.15;
|
|
7
|
-
export declare function runMonthlyPhoneBilling(phoneRepo: IPhoneNumberRepository, ledger:
|
|
7
|
+
export declare function runMonthlyPhoneBilling(phoneRepo: IPhoneNumberRepository, ledger: ILedger, meter: IMeterEmitter): Promise<{
|
|
8
8
|
processed: number;
|
|
9
9
|
billed: {
|
|
10
10
|
tenantId: string;
|
|
@@ -23,7 +23,11 @@ export async function runMonthlyPhoneBilling(phoneRepo, ledger, meter) {
|
|
|
23
23
|
try {
|
|
24
24
|
const costCredit = Credit.fromDollars(PHONE_NUMBER_MONTHLY_COST);
|
|
25
25
|
const chargeCredit = withMargin(costCredit, PHONE_NUMBER_MARGIN);
|
|
26
|
-
await ledger.debit(number.tenantId, chargeCredit, "addon",
|
|
26
|
+
await ledger.debit(number.tenantId, chargeCredit, "addon", {
|
|
27
|
+
description: "Monthly phone number fee",
|
|
28
|
+
referenceId: `phone-billing:${number.sid}:${now.toISOString().slice(0, 7)}`,
|
|
29
|
+
allowNegative: true,
|
|
30
|
+
});
|
|
27
31
|
meter.emit({
|
|
28
32
|
tenant: number.tenantId,
|
|
29
33
|
cost: costCredit,
|
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
import { Credit, InsufficientBalanceError
|
|
1
|
+
import { Credit, InsufficientBalanceError } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { PHONE_NUMBER_MONTHLY_COST, runMonthlyPhoneBilling } from "./phone-billing.js";
|
|
4
4
|
function makeTx(tenantId) {
|
|
5
5
|
return {
|
|
6
6
|
id: "tx-1",
|
|
7
|
+
postedAt: new Date().toISOString(),
|
|
8
|
+
entryType: "addon",
|
|
7
9
|
tenantId,
|
|
8
|
-
amount: Credit.fromDollars(1),
|
|
9
|
-
balanceAfter: Credit.fromDollars(100),
|
|
10
|
-
type: "addon",
|
|
11
10
|
description: "Monthly phone number fee",
|
|
12
11
|
referenceId: null,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
createdAt: new Date().toISOString(),
|
|
16
|
-
expiresAt: null,
|
|
12
|
+
metadata: null,
|
|
13
|
+
lines: [],
|
|
17
14
|
};
|
|
18
15
|
}
|
|
19
16
|
function makeNumber(overrides = {}) {
|
|
@@ -85,16 +82,16 @@ describe("runMonthlyPhoneBilling", () => {
|
|
|
85
82
|
expect(result.failed).toEqual([]);
|
|
86
83
|
// Verify debit was called with the margined charge amount
|
|
87
84
|
expect(ledger.debit).toHaveBeenCalledOnce();
|
|
88
|
-
const [tenantId, chargeAmount, type,
|
|
85
|
+
const [tenantId, chargeAmount, type, opts] = ledger.debit.mock.calls[0];
|
|
89
86
|
expect(tenantId).toBe("tenant-1");
|
|
90
87
|
// chargeCredit = Credit.fromDollars(1.15).multiply(2.6)
|
|
91
88
|
const expectedCharge = Credit.fromDollars(1.15).multiply(2.6);
|
|
92
89
|
expect(chargeAmount.toRaw()).toBe(expectedCharge.toRaw());
|
|
93
90
|
expect(type).toBe("addon");
|
|
94
|
-
expect(description).toBe("Monthly phone number fee");
|
|
91
|
+
expect(opts.description).toBe("Monthly phone number fee");
|
|
95
92
|
const expectedMonth = `${NOW.getFullYear()}-${String(NOW.getMonth() + 1).padStart(2, "0")}`;
|
|
96
|
-
expect(referenceId).toMatch(new RegExp(`^phone-billing:PN-abc123:${expectedMonth}$`));
|
|
97
|
-
expect(allowNegative).toBe(true);
|
|
93
|
+
expect(opts.referenceId).toMatch(new RegExp(`^phone-billing:PN-abc123:${expectedMonth}$`));
|
|
94
|
+
expect(opts.allowNegative).toBe(true);
|
|
98
95
|
// Verify meter emission
|
|
99
96
|
expect(meter.emit).toHaveBeenCalledOnce();
|
|
100
97
|
const event = meter.emit.mock.calls[0][0];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import { Credit } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import type { IBotInstanceRepository } from "../../fleet/bot-instance-repository.js";
|
|
4
4
|
/**
|
|
@@ -13,7 +13,7 @@ export type GetActiveBotCount = (tenantId: string) => number | Promise<number>;
|
|
|
13
13
|
/** Low balance threshold ($1.00 = 20% of signup grant). */
|
|
14
14
|
export declare const LOW_BALANCE_THRESHOLD: Credit;
|
|
15
15
|
export interface RuntimeCronConfig {
|
|
16
|
-
ledger:
|
|
16
|
+
ledger: ILedger;
|
|
17
17
|
getActiveBotCount: GetActiveBotCount;
|
|
18
18
|
/** The date being billed, as YYYY-MM-DD. Used for idempotency. */
|
|
19
19
|
date: string;
|
|
@@ -69,17 +69,26 @@ export async function runRuntimeDeductions(cfg) {
|
|
|
69
69
|
const totalCost = DAILY_BOT_COST.multiply(botCount);
|
|
70
70
|
if (!balance.lessThan(totalCost)) {
|
|
71
71
|
// Full deduction
|
|
72
|
-
await cfg.ledger.debit(tenantId, totalCost, "bot_runtime",
|
|
72
|
+
await cfg.ledger.debit(tenantId, totalCost, "bot_runtime", {
|
|
73
|
+
description: `Daily runtime: ${botCount} bot(s) x $${DAILY_BOT_COST.toDollars().toFixed(2)}`,
|
|
74
|
+
referenceId: runtimeRef,
|
|
75
|
+
});
|
|
73
76
|
// Debit resource tier surcharges (if any)
|
|
74
77
|
if (cfg.getResourceTierCosts) {
|
|
75
78
|
const tierCost = await cfg.getResourceTierCosts(tenantId);
|
|
76
79
|
if (!tierCost.isZero()) {
|
|
77
80
|
const balanceAfterRuntime = await cfg.ledger.balance(tenantId);
|
|
78
81
|
if (!balanceAfterRuntime.lessThan(tierCost)) {
|
|
79
|
-
await cfg.ledger.debit(tenantId, tierCost, "resource_upgrade",
|
|
82
|
+
await cfg.ledger.debit(tenantId, tierCost, "resource_upgrade", {
|
|
83
|
+
description: "Daily resource tier surcharge",
|
|
84
|
+
referenceId: `runtime-tier:${cfg.date}:${tenantId}`,
|
|
85
|
+
});
|
|
80
86
|
}
|
|
81
87
|
else if (balanceAfterRuntime.greaterThan(Credit.ZERO)) {
|
|
82
|
-
await cfg.ledger.debit(tenantId, balanceAfterRuntime, "resource_upgrade",
|
|
88
|
+
await cfg.ledger.debit(tenantId, balanceAfterRuntime, "resource_upgrade", {
|
|
89
|
+
description: "Partial resource tier surcharge (balance exhausted)",
|
|
90
|
+
referenceId: `runtime-tier:${cfg.date}:${tenantId}`,
|
|
91
|
+
});
|
|
83
92
|
}
|
|
84
93
|
}
|
|
85
94
|
}
|
|
@@ -110,12 +119,18 @@ export async function runRuntimeDeductions(cfg) {
|
|
|
110
119
|
if (!storageCost.isZero()) {
|
|
111
120
|
const currentBalance = await cfg.ledger.balance(tenantId);
|
|
112
121
|
if (!currentBalance.lessThan(storageCost)) {
|
|
113
|
-
await cfg.ledger.debit(tenantId, storageCost, "storage_upgrade",
|
|
122
|
+
await cfg.ledger.debit(tenantId, storageCost, "storage_upgrade", {
|
|
123
|
+
description: "Daily storage tier surcharge",
|
|
124
|
+
referenceId: `runtime-storage:${cfg.date}:${tenantId}`,
|
|
125
|
+
});
|
|
114
126
|
}
|
|
115
127
|
else {
|
|
116
128
|
// Partial debit — take what's left, then suspend
|
|
117
129
|
if (currentBalance.greaterThan(Credit.ZERO)) {
|
|
118
|
-
await cfg.ledger.debit(tenantId, currentBalance, "storage_upgrade",
|
|
130
|
+
await cfg.ledger.debit(tenantId, currentBalance, "storage_upgrade", {
|
|
131
|
+
description: "Partial storage tier surcharge (balance exhausted)",
|
|
132
|
+
referenceId: `runtime-storage:${cfg.date}:${tenantId}`,
|
|
133
|
+
});
|
|
119
134
|
}
|
|
120
135
|
if (!result.suspended.includes(tenantId)) {
|
|
121
136
|
result.suspended.push(tenantId);
|
|
@@ -131,12 +146,18 @@ export async function runRuntimeDeductions(cfg) {
|
|
|
131
146
|
if (!addonCost.isZero()) {
|
|
132
147
|
const currentBalance = await cfg.ledger.balance(tenantId);
|
|
133
148
|
if (!currentBalance.lessThan(addonCost)) {
|
|
134
|
-
await cfg.ledger.debit(tenantId, addonCost, "addon",
|
|
149
|
+
await cfg.ledger.debit(tenantId, addonCost, "addon", {
|
|
150
|
+
description: "Daily infrastructure add-on charges",
|
|
151
|
+
referenceId: `runtime-addon:${cfg.date}:${tenantId}`,
|
|
152
|
+
});
|
|
135
153
|
}
|
|
136
154
|
else {
|
|
137
155
|
// Partial debit — take what's left, then suspend
|
|
138
156
|
if (currentBalance.greaterThan(Credit.ZERO)) {
|
|
139
|
-
await cfg.ledger.debit(tenantId, currentBalance, "addon",
|
|
157
|
+
await cfg.ledger.debit(tenantId, currentBalance, "addon", {
|
|
158
|
+
description: "Partial add-on charges (balance exhausted)",
|
|
159
|
+
referenceId: `runtime-addon:${cfg.date}:${tenantId}`,
|
|
160
|
+
});
|
|
140
161
|
}
|
|
141
162
|
if (!result.suspended.includes(tenantId)) {
|
|
142
163
|
result.suspended.push(tenantId);
|
|
@@ -150,7 +171,10 @@ export async function runRuntimeDeductions(cfg) {
|
|
|
150
171
|
else {
|
|
151
172
|
// Partial deduction — debit remaining balance, then suspend
|
|
152
173
|
if (balance.greaterThan(Credit.ZERO)) {
|
|
153
|
-
await cfg.ledger.debit(tenantId, balance, "bot_runtime",
|
|
174
|
+
await cfg.ledger.debit(tenantId, balance, "bot_runtime", {
|
|
175
|
+
description: `Partial daily runtime (balance exhausted): ${botCount} bot(s)`,
|
|
176
|
+
referenceId: runtimeRef,
|
|
177
|
+
});
|
|
154
178
|
}
|
|
155
179
|
if (cfg.onCreditsExhausted) {
|
|
156
180
|
await cfg.onCreditsExhausted(tenantId);
|