@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,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 { describe, expect, it, vi } from "vitest";
|
|
4
4
|
import {
|
|
@@ -48,8 +48,8 @@ function makeManager(overrides: Record<string, unknown> = {}): SnapshotManager {
|
|
|
48
48
|
} as unknown as SnapshotManager;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function makeLedger(balanceCents = 100):
|
|
52
|
-
return { balance: vi.fn().mockResolvedValue(Credit.fromCents(balanceCents)) } as unknown as
|
|
51
|
+
function makeLedger(balanceCents = 100): ILedger {
|
|
52
|
+
return { balance: vi.fn().mockResolvedValue(Credit.fromCents(balanceCents)) } as unknown as ILedger;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
function makeService(managerOverrides: Record<string, unknown> = {}, balanceCents = 100) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import type { SnapshotManager } from "./snapshot-manager.js";
|
|
3
3
|
import type { Snapshot, Tier } from "./types.js";
|
|
4
4
|
import { SNAPSHOT_TIER_POLICIES, STORAGE_CHARGE_PER_GB_MONTH, STORAGE_COST_PER_GB_MONTH } from "./types.js";
|
|
5
5
|
|
|
6
6
|
export interface OnDemandSnapshotServiceConfig {
|
|
7
7
|
manager: SnapshotManager;
|
|
8
|
-
ledger:
|
|
8
|
+
ledger: ILedger;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export interface CreateSnapshotParams {
|
|
@@ -25,7 +25,7 @@ export interface CreateSnapshotResult {
|
|
|
25
25
|
|
|
26
26
|
export class OnDemandSnapshotService {
|
|
27
27
|
private readonly manager: SnapshotManager;
|
|
28
|
-
private readonly ledger:
|
|
28
|
+
private readonly ledger: ILedger;
|
|
29
29
|
|
|
30
30
|
constructor(cfg: OnDemandSnapshotServiceConfig) {
|
|
31
31
|
this.manager = cfg.manager;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { PGlite } from "@electric-sql/pglite";
|
|
9
9
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
|
-
import {
|
|
10
|
+
import { DrizzleLedger } from "../../credits/ledger.js";
|
|
11
11
|
import type { PlatformDb } from "../../db/index.js";
|
|
12
12
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
13
13
|
import { DrizzleWebhookSeenRepository } from "../drizzle-webhook-seen-repository.js";
|
|
@@ -41,13 +41,15 @@ afterAll(async () => {
|
|
|
41
41
|
|
|
42
42
|
describe("handlePayRamWebhook", () => {
|
|
43
43
|
let chargeStore: PayRamChargeRepository;
|
|
44
|
-
let creditLedger:
|
|
44
|
+
let creditLedger: DrizzleLedger;
|
|
45
45
|
let deps: PayRamWebhookDeps;
|
|
46
46
|
|
|
47
47
|
beforeEach(async () => {
|
|
48
48
|
await truncateAllTables(pool);
|
|
49
49
|
chargeStore = new PayRamChargeRepository(db);
|
|
50
|
-
creditLedger = new
|
|
50
|
+
creditLedger = new DrizzleLedger(db);
|
|
51
|
+
|
|
52
|
+
await creditLedger.seedSystemAccounts();
|
|
51
53
|
deps = { chargeStore, creditLedger, replayGuard: noOpReplayGuard };
|
|
52
54
|
|
|
53
55
|
// Create a default test charge
|
|
@@ -77,14 +79,14 @@ describe("handlePayRamWebhook", () => {
|
|
|
77
79
|
const history = await creditLedger.history("tenant-a");
|
|
78
80
|
expect(history).toHaveLength(1);
|
|
79
81
|
expect(history[0].referenceId).toBe("payram:ref-test-001");
|
|
80
|
-
expect(history[0].
|
|
82
|
+
expect(history[0].entryType).toBe("purchase");
|
|
81
83
|
});
|
|
82
84
|
|
|
83
85
|
it("records fundingSource as payram", async () => {
|
|
84
86
|
await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
|
|
85
87
|
|
|
86
88
|
const history = await creditLedger.history("tenant-a");
|
|
87
|
-
expect(history[0].fundingSource).toBe("payram");
|
|
89
|
+
expect(history[0].metadata?.fundingSource).toBe("payram");
|
|
88
90
|
});
|
|
89
91
|
|
|
90
92
|
it("marks the charge as credited after FILLED", async () => {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { Credit } from "../../credits/credit.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ILedger } from "../../credits/ledger.js";
|
|
3
3
|
import type { IWebhookSeenRepository } from "../webhook-seen-repository.js";
|
|
4
4
|
import type { PayRamChargeRepository } from "./charge-store.js";
|
|
5
5
|
import type { PayRamWebhookPayload, PayRamWebhookResult } from "./types.js";
|
|
6
6
|
|
|
7
7
|
export interface PayRamWebhookDeps {
|
|
8
8
|
chargeStore: PayRamChargeRepository;
|
|
9
|
-
creditLedger:
|
|
9
|
+
creditLedger: ILedger;
|
|
10
10
|
replayGuard: IWebhookSeenRepository;
|
|
11
11
|
/** Called after credits are purchased — consumer can reactivate suspended resources. Returns reactivated resource IDs. */
|
|
12
|
-
onCreditsPurchased?: (tenantId: string, ledger:
|
|
12
|
+
onCreditsPurchased?: (tenantId: string, ledger: ILedger) => Promise<string[]>;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -57,14 +57,11 @@ export async function handlePayRamWebhook(
|
|
|
57
57
|
// overpayment stays in the PayRam wallet as a buffer.
|
|
58
58
|
const creditCents = charge.amountUsdCents;
|
|
59
59
|
|
|
60
|
-
await creditLedger.credit(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
`payram:${payload.reference_id}`,
|
|
66
|
-
"payram",
|
|
67
|
-
);
|
|
60
|
+
await creditLedger.credit(charge.tenantId, Credit.fromCents(creditCents), "purchase", {
|
|
61
|
+
description: `Crypto credit purchase via PayRam (ref: ${payload.reference_id}, ${payload.currency ?? "crypto"})`,
|
|
62
|
+
referenceId: `payram:${payload.reference_id}`,
|
|
63
|
+
fundingSource: "payram",
|
|
64
|
+
});
|
|
68
65
|
|
|
69
66
|
await chargeStore.markCredited(payload.reference_id);
|
|
70
67
|
|
|
@@ -2,7 +2,7 @@ import type Stripe from "stripe";
|
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import type { IAutoTopupEventLogRepository } from "../../credits/auto-topup-event-log-repository.js";
|
|
4
4
|
import { Credit } from "../../credits/credit.js";
|
|
5
|
-
import type {
|
|
5
|
+
import type { ILedger, JournalEntry } from "../../credits/ledger.js";
|
|
6
6
|
import { PaymentMethodOwnershipError } from "../payment-processor.js";
|
|
7
7
|
import type { CreditPriceMap } from "./credit-prices.js";
|
|
8
8
|
import { StripePaymentProcessor } from "./stripe-payment-processor.js";
|
|
@@ -58,7 +58,8 @@ function createMocks() {
|
|
|
58
58
|
buildCustomerIdMap: vi.fn(),
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
const creditLedger:
|
|
61
|
+
const creditLedger: ILedger = {
|
|
62
|
+
post: vi.fn(),
|
|
62
63
|
credit: vi.fn(),
|
|
63
64
|
debit: vi.fn(),
|
|
64
65
|
balance: vi.fn(),
|
|
@@ -69,6 +70,12 @@ function createMocks() {
|
|
|
69
70
|
expiredCredits: vi.fn(),
|
|
70
71
|
lifetimeSpend: vi.fn(),
|
|
71
72
|
lifetimeSpendBatch: vi.fn().mockResolvedValue(new Map()),
|
|
73
|
+
trialBalance: vi.fn(),
|
|
74
|
+
accountBalance: vi.fn(),
|
|
75
|
+
seedSystemAccounts: vi.fn(),
|
|
76
|
+
existsByReferenceIdLike: vi.fn(),
|
|
77
|
+
sumPurchasesForPeriod: vi.fn(),
|
|
78
|
+
getActiveTenantIdsInWindow: vi.fn(),
|
|
72
79
|
};
|
|
73
80
|
|
|
74
81
|
const autoTopupEventLog: IAutoTopupEventLogRepository = {
|
|
@@ -330,7 +337,7 @@ describe("StripePaymentProcessor", () => {
|
|
|
330
337
|
status: "succeeded",
|
|
331
338
|
} as unknown as Stripe.Response<Stripe.PaymentIntent>);
|
|
332
339
|
vi.mocked(mocks.creditLedger.hasReferenceId).mockResolvedValue(false);
|
|
333
|
-
vi.mocked(mocks.creditLedger.credit).mockResolvedValue({} as unknown as
|
|
340
|
+
vi.mocked(mocks.creditLedger.credit).mockResolvedValue({} as unknown as JournalEntry);
|
|
334
341
|
vi.mocked(mocks.autoTopupEventLog.writeEvent).mockResolvedValue(undefined);
|
|
335
342
|
|
|
336
343
|
const result = await processor.charge({
|
|
@@ -2,7 +2,7 @@ import type Stripe from "stripe";
|
|
|
2
2
|
import { chargeAutoTopup } from "../../credits/auto-topup-charge.js";
|
|
3
3
|
import type { IAutoTopupEventLogRepository } from "../../credits/auto-topup-event-log-repository.js";
|
|
4
4
|
import { Credit } from "../../credits/credit.js";
|
|
5
|
-
import type {
|
|
5
|
+
import type { ILedger } from "../../credits/ledger.js";
|
|
6
6
|
import {
|
|
7
7
|
type ChargeOpts,
|
|
8
8
|
type ChargeResult,
|
|
@@ -37,7 +37,7 @@ export interface StripePaymentProcessorDeps {
|
|
|
37
37
|
tenantRepo: ITenantCustomerRepository;
|
|
38
38
|
webhookSecret: string;
|
|
39
39
|
priceMap?: CreditPriceMap;
|
|
40
|
-
creditLedger:
|
|
40
|
+
creditLedger: ILedger;
|
|
41
41
|
autoTopupEventLog?: IAutoTopupEventLogRepository;
|
|
42
42
|
/** Consumer-supplied webhook handler (handles domain-specific event processing). */
|
|
43
43
|
webhookHandler?: (event: Stripe.Event) => Promise<StripeWebhookHandlerResult>;
|
|
@@ -50,7 +50,7 @@ export class StripePaymentProcessor implements IPaymentProcessor {
|
|
|
50
50
|
private readonly tenantRepo: ITenantCustomerRepository;
|
|
51
51
|
private readonly webhookSecret: string;
|
|
52
52
|
private readonly priceMap: CreditPriceMap;
|
|
53
|
-
private readonly creditLedger:
|
|
53
|
+
private readonly creditLedger: ILedger;
|
|
54
54
|
private readonly autoTopupEventLog?: IAutoTopupEventLogRepository;
|
|
55
55
|
private readonly webhookHandler?: (event: Stripe.Event) => Promise<StripeWebhookHandlerResult>;
|
|
56
56
|
|
|
@@ -23,7 +23,7 @@ export interface ITenantCustomerRepository {
|
|
|
23
23
|
* All billing operations look up the processor customer via this store.
|
|
24
24
|
*
|
|
25
25
|
* Note: No subscription tracking — WOPR uses credits, not subscriptions.
|
|
26
|
-
* Credit balances are managed by
|
|
26
|
+
* Credit balances are managed by ILedger / DrizzleCreditLedger.
|
|
27
27
|
*/
|
|
28
28
|
export class DrizzleTenantCustomerRepository implements ITenantCustomerRepository {
|
|
29
29
|
constructor(private readonly db: PlatformDb) {}
|
|
@@ -8,7 +8,7 @@ import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
|
8
8
|
import { type AutoTopupChargeDeps, chargeAutoTopup, MAX_CONSECUTIVE_FAILURES } from "./auto-topup-charge.js";
|
|
9
9
|
import { DrizzleAutoTopupEventLogRepository } from "./auto-topup-event-log-repository.js";
|
|
10
10
|
import { Credit } from "./credit.js";
|
|
11
|
-
import {
|
|
11
|
+
import { DrizzleLedger } from "./ledger.js";
|
|
12
12
|
import type { ITenantCustomerRepository } from "./tenant-customer-repository.js";
|
|
13
13
|
|
|
14
14
|
function mockStripe(overrides?: {
|
|
@@ -49,7 +49,7 @@ function mockTenantStore(stripeCustomerId = "cus_123") {
|
|
|
49
49
|
describe("chargeAutoTopup", () => {
|
|
50
50
|
let pool: PGlite;
|
|
51
51
|
let db: PlatformDb;
|
|
52
|
-
let ledger:
|
|
52
|
+
let ledger: DrizzleLedger;
|
|
53
53
|
|
|
54
54
|
beforeAll(async () => {
|
|
55
55
|
({ db, pool } = await createTestDb());
|
|
@@ -61,7 +61,9 @@ describe("chargeAutoTopup", () => {
|
|
|
61
61
|
|
|
62
62
|
beforeEach(async () => {
|
|
63
63
|
await truncateAllTables(pool);
|
|
64
|
-
ledger = new
|
|
64
|
+
ledger = new DrizzleLedger(db);
|
|
65
|
+
|
|
66
|
+
await ledger.seedSystemAccounts();
|
|
65
67
|
});
|
|
66
68
|
|
|
67
69
|
it("charges Stripe and credits ledger on success", async () => {
|
|
@@ -80,8 +82,8 @@ describe("chargeAutoTopup", () => {
|
|
|
80
82
|
expect(result.paymentReference).toEqual(expect.any(String));
|
|
81
83
|
expect((await ledger.balance("t1")).toCents()).toBe(500);
|
|
82
84
|
const history = await ledger.history("t1");
|
|
83
|
-
expect(history[0].
|
|
84
|
-
expect(history[0].fundingSource).toBe("stripe");
|
|
85
|
+
expect(history[0].entryType).toBe("purchase");
|
|
86
|
+
expect(history[0].metadata?.fundingSource).toBe("stripe");
|
|
85
87
|
});
|
|
86
88
|
|
|
87
89
|
it("writes success event to credit_auto_topup log", async () => {
|
|
@@ -2,7 +2,7 @@ import Stripe from "stripe";
|
|
|
2
2
|
import { logger } from "../config/logger.js";
|
|
3
3
|
import type { IAutoTopupEventLogRepository } from "./auto-topup-event-log-repository.js";
|
|
4
4
|
import type { Credit } from "./credit.js";
|
|
5
|
-
import type {
|
|
5
|
+
import type { ILedger } from "./ledger.js";
|
|
6
6
|
import type { ITenantCustomerRepository } from "./tenant-customer-repository.js";
|
|
7
7
|
|
|
8
8
|
/** After this many consecutive Stripe failures, the auto-topup mode is disabled. */
|
|
@@ -11,7 +11,7 @@ export const MAX_CONSECUTIVE_FAILURES = 3;
|
|
|
11
11
|
export interface AutoTopupChargeDeps {
|
|
12
12
|
stripe: Stripe;
|
|
13
13
|
tenantRepo: ITenantCustomerRepository;
|
|
14
|
-
creditLedger:
|
|
14
|
+
creditLedger: ILedger;
|
|
15
15
|
eventLogRepo: IAutoTopupEventLogRepository;
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -137,14 +137,11 @@ export async function chargeAutoTopup(
|
|
|
137
137
|
// 5. Credit the ledger (idempotent via referenceId = PI ID)
|
|
138
138
|
try {
|
|
139
139
|
if (!(await deps.creditLedger.hasReferenceId(paymentIntent.id))) {
|
|
140
|
-
await deps.creditLedger.credit(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
"
|
|
144
|
-
|
|
145
|
-
paymentIntent.id,
|
|
146
|
-
"stripe",
|
|
147
|
-
);
|
|
140
|
+
await deps.creditLedger.credit(tenantId, amount, "purchase", {
|
|
141
|
+
description: `Auto-topup (${source})`,
|
|
142
|
+
referenceId: paymentIntent.id,
|
|
143
|
+
fundingSource: "stripe",
|
|
144
|
+
});
|
|
148
145
|
}
|
|
149
146
|
} catch (err) {
|
|
150
147
|
const message = `Stripe charge ${paymentIntent.id} succeeded but credit grant failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -5,12 +5,12 @@ import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
|
5
5
|
import { DrizzleAutoTopupSettingsRepository } from "./auto-topup-settings-repository.js";
|
|
6
6
|
import { maybeTriggerUsageTopup, type UsageTopupDeps } from "./auto-topup-usage.js";
|
|
7
7
|
import { Credit } from "./credit.js";
|
|
8
|
-
import {
|
|
8
|
+
import { DrizzleLedger } from "./ledger.js";
|
|
9
9
|
|
|
10
10
|
describe("maybeTriggerUsageTopup", () => {
|
|
11
11
|
let pool: PGlite;
|
|
12
12
|
let db: PlatformDb;
|
|
13
|
-
let ledger:
|
|
13
|
+
let ledger: DrizzleLedger;
|
|
14
14
|
let settingsRepo: DrizzleAutoTopupSettingsRepository;
|
|
15
15
|
|
|
16
16
|
beforeAll(async () => {
|
|
@@ -23,7 +23,9 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
23
23
|
|
|
24
24
|
beforeEach(async () => {
|
|
25
25
|
await truncateAllTables(pool);
|
|
26
|
-
ledger = new
|
|
26
|
+
ledger = new DrizzleLedger(db);
|
|
27
|
+
|
|
28
|
+
await ledger.seedSystemAccounts();
|
|
27
29
|
settingsRepo = new DrizzleAutoTopupSettingsRepository(db);
|
|
28
30
|
});
|
|
29
31
|
|
|
@@ -37,7 +39,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
37
39
|
|
|
38
40
|
it("does nothing when usage_enabled is false", async () => {
|
|
39
41
|
await settingsRepo.upsert("t1", { usageEnabled: false });
|
|
40
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
42
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
43
|
+
description: "buy",
|
|
44
|
+
referenceId: "ref-1",
|
|
45
|
+
fundingSource: "stripe",
|
|
46
|
+
});
|
|
41
47
|
const mockCharge = vi.fn();
|
|
42
48
|
const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
43
49
|
|
|
@@ -51,7 +57,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
51
57
|
usageThreshold: Credit.fromCents(100),
|
|
52
58
|
usageTopup: Credit.fromCents(500),
|
|
53
59
|
});
|
|
54
|
-
await ledger.credit("t1", Credit.fromCents(200), "purchase",
|
|
60
|
+
await ledger.credit("t1", Credit.fromCents(200), "purchase", {
|
|
61
|
+
description: "buy",
|
|
62
|
+
referenceId: "ref-1",
|
|
63
|
+
fundingSource: "stripe",
|
|
64
|
+
});
|
|
55
65
|
const mockCharge = vi.fn();
|
|
56
66
|
const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
57
67
|
|
|
@@ -65,7 +75,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
65
75
|
usageThreshold: Credit.fromCents(100),
|
|
66
76
|
usageTopup: Credit.fromCents(500),
|
|
67
77
|
});
|
|
68
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
78
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
79
|
+
description: "buy",
|
|
80
|
+
referenceId: "ref-1",
|
|
81
|
+
fundingSource: "stripe",
|
|
82
|
+
});
|
|
69
83
|
const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_123" });
|
|
70
84
|
const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
71
85
|
|
|
@@ -76,7 +90,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
76
90
|
it("skips when charge is already in-flight", async () => {
|
|
77
91
|
await settingsRepo.upsert("t1", { usageEnabled: true, usageThreshold: Credit.fromCents(100) });
|
|
78
92
|
await settingsRepo.setUsageChargeInFlight("t1", true);
|
|
79
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
93
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
94
|
+
description: "buy",
|
|
95
|
+
referenceId: "ref-1",
|
|
96
|
+
fundingSource: "stripe",
|
|
97
|
+
});
|
|
80
98
|
const mockCharge = vi.fn();
|
|
81
99
|
const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
82
100
|
|
|
@@ -91,7 +109,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
91
109
|
usageThreshold: Credit.fromCents(500),
|
|
92
110
|
usageTopup: Credit.fromCents(2000),
|
|
93
111
|
});
|
|
94
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase",
|
|
112
|
+
await ledger.credit("t1", Credit.fromCents(100), "purchase", {
|
|
113
|
+
description: "buy",
|
|
114
|
+
referenceId: "ref-1",
|
|
115
|
+
fundingSource: "stripe",
|
|
116
|
+
});
|
|
95
117
|
|
|
96
118
|
const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_race" });
|
|
97
119
|
const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
@@ -115,7 +137,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
115
137
|
usageThreshold: Credit.fromCents(100),
|
|
116
138
|
usageTopup: Credit.fromCents(500),
|
|
117
139
|
});
|
|
118
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
140
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
141
|
+
description: "buy",
|
|
142
|
+
referenceId: "ref-1",
|
|
143
|
+
fundingSource: "stripe",
|
|
144
|
+
});
|
|
119
145
|
const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_123" });
|
|
120
146
|
const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
121
147
|
|
|
@@ -137,7 +163,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
137
163
|
usageThreshold: Credit.fromCents(100),
|
|
138
164
|
usageTopup: Credit.fromCents(500),
|
|
139
165
|
});
|
|
140
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
166
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
167
|
+
description: "buy",
|
|
168
|
+
referenceId: "ref-1",
|
|
169
|
+
fundingSource: "stripe",
|
|
170
|
+
});
|
|
141
171
|
const mockCharge = vi
|
|
142
172
|
.fn()
|
|
143
173
|
.mockRejectedValueOnce(new Error("Stripe network error"))
|
|
@@ -164,7 +194,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
164
194
|
});
|
|
165
195
|
await settingsRepo.incrementUsageFailures("t1");
|
|
166
196
|
await settingsRepo.incrementUsageFailures("t1");
|
|
167
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
197
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
198
|
+
description: "buy",
|
|
199
|
+
referenceId: "ref-1",
|
|
200
|
+
fundingSource: "stripe",
|
|
201
|
+
});
|
|
168
202
|
const mockCharge = vi.fn().mockResolvedValue({ success: true });
|
|
169
203
|
const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
170
204
|
|
|
@@ -178,7 +212,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
178
212
|
usageThreshold: Credit.fromCents(100),
|
|
179
213
|
usageTopup: Credit.fromCents(500),
|
|
180
214
|
});
|
|
181
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
215
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
216
|
+
description: "buy",
|
|
217
|
+
referenceId: "ref-1",
|
|
218
|
+
fundingSource: "stripe",
|
|
219
|
+
});
|
|
182
220
|
const mockCharge = vi.fn().mockResolvedValue({ success: false, error: "declined" });
|
|
183
221
|
const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
184
222
|
|
|
@@ -210,7 +248,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
210
248
|
});
|
|
211
249
|
await settingsRepo.incrementUsageFailures("t1");
|
|
212
250
|
await settingsRepo.incrementUsageFailures("t1");
|
|
213
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
251
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
252
|
+
description: "buy",
|
|
253
|
+
referenceId: "ref-1",
|
|
254
|
+
fundingSource: "stripe",
|
|
255
|
+
});
|
|
214
256
|
const mockCharge = vi.fn().mockResolvedValue({ success: false, error: "declined" });
|
|
215
257
|
const deps: UsageTopupDeps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
216
258
|
|
|
@@ -3,11 +3,11 @@ import type { AutoTopupChargeResult } from "./auto-topup-charge.js";
|
|
|
3
3
|
import { MAX_CONSECUTIVE_FAILURES } from "./auto-topup-charge.js";
|
|
4
4
|
import type { IAutoTopupSettingsRepository } from "./auto-topup-settings-repository.js";
|
|
5
5
|
import type { Credit } from "./credit.js";
|
|
6
|
-
import type {
|
|
6
|
+
import type { ILedger } from "./ledger.js";
|
|
7
7
|
|
|
8
8
|
export interface UsageTopupDeps {
|
|
9
9
|
settingsRepo: IAutoTopupSettingsRepository;
|
|
10
|
-
creditLedger:
|
|
10
|
+
creditLedger: ILedger;
|
|
11
11
|
/** Injected charge function (allows mocking in tests). */
|
|
12
12
|
chargeAutoTopup: (tenantId: string, amount: Credit, source: string) => Promise<AutoTopupChargeResult>;
|
|
13
13
|
/** Optional tenant status check. If provided and returns non-null, skip the charge. */
|
|
@@ -3,16 +3,16 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
|
3
3
|
import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
4
4
|
import { Credit } from "./credit.js";
|
|
5
5
|
import { runCreditExpiryCron } from "./credit-expiry-cron.js";
|
|
6
|
-
import {
|
|
6
|
+
import { DrizzleLedger } from "./ledger.js";
|
|
7
7
|
|
|
8
8
|
describe("runCreditExpiryCron", () => {
|
|
9
9
|
let pool: PGlite;
|
|
10
|
-
let ledger:
|
|
10
|
+
let ledger: DrizzleLedger;
|
|
11
11
|
|
|
12
12
|
beforeAll(async () => {
|
|
13
13
|
const { db, pool: p } = await createTestDb();
|
|
14
14
|
pool = p;
|
|
15
|
-
ledger = new
|
|
15
|
+
ledger = new DrizzleLedger(db);
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
afterAll(async () => {
|
|
@@ -21,6 +21,7 @@ describe("runCreditExpiryCron", () => {
|
|
|
21
21
|
|
|
22
22
|
beforeEach(async () => {
|
|
23
23
|
await truncateAllTables(pool);
|
|
24
|
+
await ledger.seedSystemAccounts();
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
// All tests pass an explicit `now` parameter — hardcoded dates are time-independent
|
|
@@ -33,16 +34,11 @@ describe("runCreditExpiryCron", () => {
|
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
it("debits expired promotional credit grant", async () => {
|
|
36
|
-
await ledger.credit(
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
"promo:tenant-1",
|
|
42
|
-
undefined,
|
|
43
|
-
undefined,
|
|
44
|
-
"2026-01-10T00:00:00Z",
|
|
45
|
-
);
|
|
37
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
38
|
+
description: "New user bonus",
|
|
39
|
+
referenceId: "promo:tenant-1",
|
|
40
|
+
expiresAt: "2026-01-10T00:00:00Z",
|
|
41
|
+
});
|
|
46
42
|
|
|
47
43
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
48
44
|
expect(result.processed).toBe(1);
|
|
@@ -53,16 +49,11 @@ describe("runCreditExpiryCron", () => {
|
|
|
53
49
|
});
|
|
54
50
|
|
|
55
51
|
it("does not debit non-expired credits", async () => {
|
|
56
|
-
await ledger.credit(
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
"promo:tenant-1-future",
|
|
62
|
-
undefined,
|
|
63
|
-
undefined,
|
|
64
|
-
"2026-02-01T00:00:00Z",
|
|
65
|
-
);
|
|
52
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
53
|
+
description: "Future bonus",
|
|
54
|
+
referenceId: "promo:tenant-1-future",
|
|
55
|
+
expiresAt: "2026-02-01T00:00:00Z",
|
|
56
|
+
});
|
|
66
57
|
|
|
67
58
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
68
59
|
expect(result.processed).toBe(0);
|
|
@@ -72,7 +63,7 @@ describe("runCreditExpiryCron", () => {
|
|
|
72
63
|
});
|
|
73
64
|
|
|
74
65
|
it("does not debit credits without expires_at", async () => {
|
|
75
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "Top-up");
|
|
66
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "Top-up" });
|
|
76
67
|
|
|
77
68
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
78
69
|
expect(result.processed).toBe(0);
|
|
@@ -82,17 +73,12 @@ describe("runCreditExpiryCron", () => {
|
|
|
82
73
|
});
|
|
83
74
|
|
|
84
75
|
it("only debits up to available balance when partially consumed", async () => {
|
|
85
|
-
await ledger.credit(
|
|
86
|
-
"
|
|
87
|
-
|
|
88
|
-
"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
undefined,
|
|
92
|
-
undefined,
|
|
93
|
-
"2026-01-10T00:00:00Z",
|
|
94
|
-
);
|
|
95
|
-
await ledger.debit("tenant-1", Credit.fromCents(300), "bot_runtime", "Runtime");
|
|
76
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
77
|
+
description: "Promo",
|
|
78
|
+
referenceId: "promo:partial",
|
|
79
|
+
expiresAt: "2026-01-10T00:00:00Z",
|
|
80
|
+
});
|
|
81
|
+
await ledger.debit("tenant-1", Credit.fromCents(300), "bot_runtime", { description: "Runtime" });
|
|
96
82
|
|
|
97
83
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
98
84
|
expect(result.processed).toBe(1);
|
|
@@ -102,16 +88,11 @@ describe("runCreditExpiryCron", () => {
|
|
|
102
88
|
});
|
|
103
89
|
|
|
104
90
|
it("is idempotent -- does not double-debit on second run", async () => {
|
|
105
|
-
await ledger.credit(
|
|
106
|
-
"
|
|
107
|
-
|
|
108
|
-
"
|
|
109
|
-
|
|
110
|
-
"promo:idemp",
|
|
111
|
-
undefined,
|
|
112
|
-
undefined,
|
|
113
|
-
"2026-01-10T00:00:00Z",
|
|
114
|
-
);
|
|
91
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
92
|
+
description: "Promo",
|
|
93
|
+
referenceId: "promo:idemp",
|
|
94
|
+
expiresAt: "2026-01-10T00:00:00Z",
|
|
95
|
+
});
|
|
115
96
|
|
|
116
97
|
await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
117
98
|
const balanceAfterFirst = await ledger.balance("tenant-1");
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { logger } from "../config/logger.js";
|
|
2
|
-
import type {
|
|
3
|
-
import { InsufficientBalanceError } from "./
|
|
2
|
+
import type { ILedger } from "./ledger.js";
|
|
3
|
+
import { InsufficientBalanceError } from "./ledger.js";
|
|
4
4
|
|
|
5
5
|
export interface CreditExpiryCronConfig {
|
|
6
|
-
ledger:
|
|
6
|
+
ledger: ILedger;
|
|
7
7
|
/** Current time as ISO-8601 string. */
|
|
8
8
|
now: string;
|
|
9
9
|
}
|
|
@@ -42,13 +42,10 @@ export async function runCreditExpiryCron(cfg: CreditExpiryCronConfig): Promise<
|
|
|
42
42
|
// Debit the lesser of the original grant amount or current balance
|
|
43
43
|
const debitAmount = balance.lessThan(grant.amount) ? balance : grant.amount;
|
|
44
44
|
|
|
45
|
-
await cfg.ledger.debit(
|
|
46
|
-
grant.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
`Expired credit grant reclaimed: ${grant.id}`,
|
|
50
|
-
`expiry:${grant.id}`,
|
|
51
|
-
);
|
|
45
|
+
await cfg.ledger.debit(grant.tenantId, debitAmount, "credit_expiry", {
|
|
46
|
+
description: `Expired credit grant reclaimed: ${grant.entryId}`,
|
|
47
|
+
referenceId: `expiry:${grant.entryId}`,
|
|
48
|
+
});
|
|
52
49
|
|
|
53
50
|
result.processed++;
|
|
54
51
|
if (!result.expired.includes(grant.tenantId)) {
|
|
@@ -59,8 +56,8 @@ export async function runCreditExpiryCron(cfg: CreditExpiryCronConfig): Promise<
|
|
|
59
56
|
result.skippedZeroBalance++;
|
|
60
57
|
} else {
|
|
61
58
|
const msg = err instanceof Error ? err.message : String(err);
|
|
62
|
-
logger.error("Credit expiry failed", { tenantId: grant.tenantId,
|
|
63
|
-
result.errors.push(`${grant.tenantId}:${grant.
|
|
59
|
+
logger.error("Credit expiry failed", { tenantId: grant.tenantId, entryId: grant.entryId, error: msg });
|
|
60
|
+
result.errors.push(`${grant.tenantId}:${grant.entryId}: ${msg}`);
|
|
64
61
|
}
|
|
65
62
|
}
|
|
66
63
|
}
|
|
@@ -95,7 +95,7 @@ export class InsufficientBalanceError extends Error {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
export interface
|
|
98
|
+
export interface ILedger {
|
|
99
99
|
credit(
|
|
100
100
|
tenantId: string,
|
|
101
101
|
amount: Credit,
|
|
@@ -135,7 +135,7 @@ export interface ICreditLedger {
|
|
|
135
135
|
* creditBalances row is always consistent with the sum of creditTransactions.
|
|
136
136
|
* Zero raw SQL in application code.
|
|
137
137
|
*/
|
|
138
|
-
export class DrizzleCreditLedger implements
|
|
138
|
+
export class DrizzleCreditLedger implements ILedger {
|
|
139
139
|
constructor(private readonly db: PlatformDb) {}
|
|
140
140
|
|
|
141
141
|
/**
|
|
@@ -446,5 +446,5 @@ export class DrizzleCreditLedger implements ICreditLedger {
|
|
|
446
446
|
}
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
-
// Backward-compat alias — callers using 'new
|
|
449
|
+
// Backward-compat alias — callers using 'new DrizzleLedger(db)' continue to work.
|
|
450
450
|
export { DrizzleCreditLedger as CreditLedger };
|