@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,6 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import type { PGlite } from "@electric-sql/pglite";
|
|
3
|
-
import { Credit,
|
|
3
|
+
import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
4
4
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
5
5
|
import type { DrizzleDb } from "../../db/index.js";
|
|
6
6
|
import { adminUsers } from "../../db/schema/admin-users.js";
|
|
@@ -54,6 +54,7 @@ describe("DrizzleDividendRepository", () => {
|
|
|
54
54
|
|
|
55
55
|
beforeEach(async () => {
|
|
56
56
|
await truncateAllTables(pool);
|
|
57
|
+
await new DrizzleLedger(db).seedSystemAccounts();
|
|
57
58
|
repo = new DrizzleDividendRepository(db);
|
|
58
59
|
});
|
|
59
60
|
|
|
@@ -194,8 +195,8 @@ describe("DrizzleDividendRepository", () => {
|
|
|
194
195
|
});
|
|
195
196
|
|
|
196
197
|
it("marks user as eligible when they have a recent purchase", async () => {
|
|
197
|
-
const ledger = new
|
|
198
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase", "recent buy");
|
|
198
|
+
const ledger = new DrizzleLedger(db);
|
|
199
|
+
await ledger.credit("t1", Credit.fromCents(100), "purchase", { description: "recent buy" });
|
|
199
200
|
|
|
200
201
|
const stats = await repo.getStats("t1");
|
|
201
202
|
expect(stats.userEligible).toBe(true);
|
|
@@ -2,8 +2,8 @@ import { Credit } from "@wopr-network/platform-core/credits";
|
|
|
2
2
|
import { and, desc, eq, gte, lt, sql } from "drizzle-orm";
|
|
3
3
|
import type { DrizzleDb } from "../../db/index.js";
|
|
4
4
|
import { adminUsers } from "../../db/schema/admin-users.js";
|
|
5
|
-
import { creditTransactions } from "../../db/schema/credits.js";
|
|
6
5
|
import { dividendDistributions } from "../../db/schema/dividend-distributions.js";
|
|
6
|
+
import { journalEntries, journalLines } from "../../db/schema/ledger.js";
|
|
7
7
|
import type { DividendHistoryEntry, DividendStats } from "../repository-types.js";
|
|
8
8
|
|
|
9
9
|
export type { DividendHistoryEntry, DividendStats };
|
|
@@ -30,35 +30,32 @@ export class DrizzleDividendRepository implements IDividendRepository {
|
|
|
30
30
|
constructor(private readonly db: DrizzleDb) {}
|
|
31
31
|
|
|
32
32
|
async getStats(tenantId: string): Promise<DividendStats> {
|
|
33
|
-
// 1. Pool = sum of purchase amounts from yesterday UTC
|
|
33
|
+
// 1. Pool = sum of purchase credit amounts from yesterday UTC
|
|
34
34
|
const poolRow = (
|
|
35
35
|
await this.db
|
|
36
|
-
|
|
37
|
-
.
|
|
38
|
-
.
|
|
36
|
+
.select({ total: sql<string>`COALESCE(SUM(${journalLines.amount}), 0)` })
|
|
37
|
+
.from(journalLines)
|
|
38
|
+
.innerJoin(journalEntries, eq(journalEntries.id, journalLines.journalEntryId))
|
|
39
39
|
.where(
|
|
40
40
|
and(
|
|
41
|
-
eq(
|
|
42
|
-
|
|
43
|
-
sql`${
|
|
44
|
-
sql`${
|
|
41
|
+
eq(journalEntries.entryType, "purchase"),
|
|
42
|
+
eq(journalLines.side, "credit"),
|
|
43
|
+
sql`${journalEntries.postedAt}::timestamp >= date_trunc('day', timezone('UTC', now())) - INTERVAL '1 day'`,
|
|
44
|
+
sql`${journalEntries.postedAt}::timestamp < date_trunc('day', timezone('UTC', now()))`,
|
|
45
45
|
),
|
|
46
46
|
)
|
|
47
47
|
)[0];
|
|
48
|
-
const
|
|
49
|
-
const pool = Credit.fromCents(poolCents);
|
|
48
|
+
const pool = Credit.fromRaw(Number(poolRow?.total ?? 0));
|
|
50
49
|
|
|
51
50
|
// 2. Active users = distinct tenants with a purchase in the last 7 days
|
|
52
51
|
const activeRow = (
|
|
53
52
|
await this.db
|
|
54
|
-
|
|
55
|
-
.
|
|
56
|
-
.from(creditTransactions)
|
|
53
|
+
.select({ count: sql<number>`COUNT(DISTINCT ${journalEntries.tenantId})` })
|
|
54
|
+
.from(journalEntries)
|
|
57
55
|
.where(
|
|
58
56
|
and(
|
|
59
|
-
eq(
|
|
60
|
-
|
|
61
|
-
sql`${creditTransactions.createdAt}::timestamp >= timezone('UTC', now()) - INTERVAL '7 days'`,
|
|
57
|
+
eq(journalEntries.entryType, "purchase"),
|
|
58
|
+
sql`${journalEntries.postedAt}::timestamp >= timezone('UTC', now()) - INTERVAL '7 days'`,
|
|
62
59
|
),
|
|
63
60
|
)
|
|
64
61
|
)[0];
|
|
@@ -75,10 +72,10 @@ export class DrizzleDividendRepository implements IDividendRepository {
|
|
|
75
72
|
// 5. User eligibility — last purchase within 7 days
|
|
76
73
|
const userPurchaseRow = (
|
|
77
74
|
await this.db
|
|
78
|
-
.select({
|
|
79
|
-
.from(
|
|
80
|
-
.where(and(eq(
|
|
81
|
-
.orderBy(desc(
|
|
75
|
+
.select({ postedAt: journalEntries.postedAt })
|
|
76
|
+
.from(journalEntries)
|
|
77
|
+
.where(and(eq(journalEntries.tenantId, tenantId), eq(journalEntries.entryType, "purchase")))
|
|
78
|
+
.orderBy(desc(journalEntries.postedAt))
|
|
82
79
|
.limit(1)
|
|
83
80
|
)[0];
|
|
84
81
|
|
|
@@ -87,10 +84,7 @@ export class DrizzleDividendRepository implements IDividendRepository {
|
|
|
87
84
|
let userWindowExpiresAt: string | null = null;
|
|
88
85
|
|
|
89
86
|
if (userPurchaseRow) {
|
|
90
|
-
const
|
|
91
|
-
// Parse the timestamp directly. PGlite may return ISO strings with or without
|
|
92
|
-
// timezone suffix. JavaScript's Date constructor handles ISO 8601 strings natively.
|
|
93
|
-
const lastPurchase = new Date(rawTs);
|
|
87
|
+
const lastPurchase = new Date(userPurchaseRow.postedAt);
|
|
94
88
|
userLastPurchaseAt = lastPurchase.toISOString();
|
|
95
89
|
|
|
96
90
|
const windowExpiry = new Date(lastPurchase.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
@@ -2,24 +2,24 @@ export type {
|
|
|
2
2
|
AutoTopupSettings,
|
|
3
3
|
CreditExpiryCronConfig,
|
|
4
4
|
CreditExpiryCronResult,
|
|
5
|
-
CreditTransaction,
|
|
6
5
|
CreditType,
|
|
7
6
|
DebitType,
|
|
8
7
|
HistoryOptions,
|
|
9
8
|
IAutoTopupSettingsRepository,
|
|
10
|
-
|
|
9
|
+
ILedger,
|
|
10
|
+
JournalEntry,
|
|
11
11
|
TransactionType,
|
|
12
12
|
} from "@wopr-network/platform-core/credits";
|
|
13
13
|
export {
|
|
14
14
|
ALLOWED_SCHEDULE_INTERVALS,
|
|
15
15
|
ALLOWED_THRESHOLDS,
|
|
16
16
|
ALLOWED_TOPUP_AMOUNTS,
|
|
17
|
-
CreditLedger,
|
|
18
17
|
computeNextScheduleAt,
|
|
19
18
|
DrizzleAutoTopupSettingsRepository,
|
|
20
|
-
|
|
19
|
+
DrizzleLedger,
|
|
21
20
|
grantSignupCredits,
|
|
22
21
|
InsufficientBalanceError,
|
|
22
|
+
Ledger,
|
|
23
23
|
runCreditExpiryCron,
|
|
24
24
|
SIGNUP_GRANT,
|
|
25
25
|
} from "@wopr-network/platform-core/credits";
|
|
@@ -1,13 +1,13 @@
|
|
|
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";
|
|
6
6
|
|
|
7
|
-
describe("
|
|
7
|
+
describe("DrizzleDrizzleLedger.memberUsage", () => {
|
|
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,14 +19,25 @@ describe("DrizzleCreditLedger.memberUsage", () => {
|
|
|
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("should aggregate debit totals per attributed user", async () => {
|
|
26
|
-
await ledger.credit("org-1", Credit.fromCents(10000), "purchase", "Seed");
|
|
27
|
-
await ledger.debit("org-1", Credit.fromCents(100), "adapter_usage",
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
await ledger.credit("org-1", Credit.fromCents(10000), "purchase", { description: "Seed" });
|
|
29
|
+
await ledger.debit("org-1", Credit.fromCents(100), "adapter_usage", {
|
|
30
|
+
description: "Chat",
|
|
31
|
+
attributedUserId: "user-a",
|
|
32
|
+
});
|
|
33
|
+
await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage", {
|
|
34
|
+
description: "Chat",
|
|
35
|
+
attributedUserId: "user-a",
|
|
36
|
+
});
|
|
37
|
+
await ledger.debit("org-1", Credit.fromCents(300), "adapter_usage", {
|
|
38
|
+
description: "Chat",
|
|
39
|
+
attributedUserId: "user-b",
|
|
40
|
+
});
|
|
30
41
|
|
|
31
42
|
const result = await ledger.memberUsage("org-1");
|
|
32
43
|
expect(result).toHaveLength(2);
|
|
@@ -41,9 +52,12 @@ describe("DrizzleCreditLedger.memberUsage", () => {
|
|
|
41
52
|
});
|
|
42
53
|
|
|
43
54
|
it("should exclude transactions with null attributedUserId", async () => {
|
|
44
|
-
await ledger.credit("org-1", Credit.fromCents(10000), "purchase", "Seed");
|
|
45
|
-
await ledger.debit("org-1", Credit.fromCents(100), "bot_runtime", "Cron"); // no attributedUserId
|
|
46
|
-
await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage",
|
|
55
|
+
await ledger.credit("org-1", Credit.fromCents(10000), "purchase", { description: "Seed" });
|
|
56
|
+
await ledger.debit("org-1", Credit.fromCents(100), "bot_runtime", { description: "Cron" }); // no attributedUserId
|
|
57
|
+
await ledger.debit("org-1", Credit.fromCents(200), "adapter_usage", {
|
|
58
|
+
description: "Chat",
|
|
59
|
+
attributedUserId: "user-a",
|
|
60
|
+
});
|
|
47
61
|
|
|
48
62
|
const result = await ledger.memberUsage("org-1");
|
|
49
63
|
expect(result).toHaveLength(1);
|
|
@@ -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,
|