@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,9 +1,9 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import type { AuthEnv } from "../../auth/index.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ILedger } from "../../credits/index.js";
|
|
4
4
|
import type { AdminAuditLogger } from "./admin-audit-helper.js";
|
|
5
5
|
/**
|
|
6
6
|
* Create admin credit API routes.
|
|
7
7
|
* Pass a ledger directly or a factory for lazy init.
|
|
8
8
|
*/
|
|
9
|
-
export declare function createAdminCreditApiRoutes(ledgerOrFactory:
|
|
9
|
+
export declare function createAdminCreditApiRoutes(ledgerOrFactory: ILedger | (() => ILedger), auditLogger?: () => AdminAuditLogger): Hono<AuthEnv>;
|
|
@@ -53,7 +53,10 @@ export function createAdminCreditApiRoutes(ledgerOrFactory, auditLogger) {
|
|
|
53
53
|
const adminUser = user?.id ?? "unknown";
|
|
54
54
|
let result;
|
|
55
55
|
try {
|
|
56
|
-
result = await ledger.credit(tenant, Credit.fromCents(amountCents), "admin_grant",
|
|
56
|
+
result = await ledger.credit(tenant, Credit.fromCents(amountCents), "admin_grant", {
|
|
57
|
+
description: reason,
|
|
58
|
+
createdBy: adminUser,
|
|
59
|
+
});
|
|
57
60
|
}
|
|
58
61
|
catch (err) {
|
|
59
62
|
safeAuditLog(auditLogger, {
|
|
@@ -106,7 +109,7 @@ export function createAdminCreditApiRoutes(ledgerOrFactory, auditLogger) {
|
|
|
106
109
|
const adminUser = user?.id ?? "unknown";
|
|
107
110
|
let result;
|
|
108
111
|
try {
|
|
109
|
-
result = await ledger.credit(tenant, Credit.fromCents(amountCents), "admin_grant", reason);
|
|
112
|
+
result = await ledger.credit(tenant, Credit.fromCents(amountCents), "admin_grant", { description: reason });
|
|
110
113
|
}
|
|
111
114
|
catch (err) {
|
|
112
115
|
safeAuditLog(auditLogger, {
|
|
@@ -163,10 +166,12 @@ export function createAdminCreditApiRoutes(ledgerOrFactory, auditLogger) {
|
|
|
163
166
|
let result;
|
|
164
167
|
try {
|
|
165
168
|
if (amountCents >= 0) {
|
|
166
|
-
result = await ledger.credit(tenant, Credit.fromCents(amountCents), "
|
|
169
|
+
result = await ledger.credit(tenant, Credit.fromCents(amountCents), "correction", { description: reason });
|
|
167
170
|
}
|
|
168
171
|
else {
|
|
169
|
-
result = await ledger.debit(tenant, Credit.fromCents(Math.abs(amountCents)), "correction",
|
|
172
|
+
result = await ledger.debit(tenant, Credit.fromCents(Math.abs(amountCents)), "correction", {
|
|
173
|
+
description: reason,
|
|
174
|
+
});
|
|
170
175
|
}
|
|
171
176
|
}
|
|
172
177
|
catch (err) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ILedger } from "../../credits/ledger.js";
|
|
3
3
|
/**
|
|
4
4
|
* Create quota routes.
|
|
5
5
|
*
|
|
6
6
|
* @param ledgerFactory - Factory returning the credit ledger
|
|
7
7
|
*/
|
|
8
|
-
export declare function createQuotaRoutes(ledgerFactory: () =>
|
|
8
|
+
export declare function createQuotaRoutes(ledgerFactory: () => ILedger): Hono;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import type { Pool } from "pg";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ILedger } from "../../credits/index.js";
|
|
4
4
|
export interface VerifyEmailRouteDeps {
|
|
5
5
|
pool: Pool;
|
|
6
|
-
creditLedger:
|
|
6
|
+
creditLedger: ILedger;
|
|
7
7
|
}
|
|
8
8
|
export interface VerifyEmailRouteConfig {
|
|
9
9
|
/** UI origin for redirect URLs (default: http://localhost:3001) */
|
|
@@ -16,4 +16,4 @@ export declare function createVerifyEmailRoutes(deps: VerifyEmailRouteDeps, conf
|
|
|
16
16
|
/**
|
|
17
17
|
* Create verify-email routes with factory functions (for lazy init).
|
|
18
18
|
*/
|
|
19
|
-
export declare function createVerifyEmailRoutesLazy(poolFactory: () => Pool, creditLedgerFactory: () =>
|
|
19
|
+
export declare function createVerifyEmailRoutesLazy(poolFactory: () => Pool, creditLedgerFactory: () => ILedger, config?: VerifyEmailRouteConfig): Hono;
|
|
@@ -1,9 +1,9 @@
|
|
|
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
|
export interface OnDemandSnapshotServiceConfig {
|
|
5
5
|
manager: SnapshotManager;
|
|
6
|
-
ledger:
|
|
6
|
+
ledger: ILedger;
|
|
7
7
|
}
|
|
8
8
|
export interface CreateSnapshotParams {
|
|
9
9
|
tenant: string;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ILedger } from "../../credits/ledger.js";
|
|
2
2
|
import type { IWebhookSeenRepository } from "../webhook-seen-repository.js";
|
|
3
3
|
import type { PayRamChargeRepository } from "./charge-store.js";
|
|
4
4
|
import type { PayRamWebhookPayload, PayRamWebhookResult } from "./types.js";
|
|
5
5
|
export interface PayRamWebhookDeps {
|
|
6
6
|
chargeStore: PayRamChargeRepository;
|
|
7
|
-
creditLedger:
|
|
7
|
+
creditLedger: ILedger;
|
|
8
8
|
replayGuard: IWebhookSeenRepository;
|
|
9
9
|
/** Called after credits are purchased — consumer can reactivate suspended resources. Returns reactivated resource IDs. */
|
|
10
|
-
onCreditsPurchased?: (tenantId: string, ledger:
|
|
10
|
+
onCreditsPurchased?: (tenantId: string, ledger: ILedger) => Promise<string[]>;
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
13
|
* Process a PayRam webhook event.
|
|
@@ -36,7 +36,11 @@ export async function handlePayRamWebhook(deps, payload) {
|
|
|
36
36
|
// For OVER_FILLED, we still credit the requested amount — the
|
|
37
37
|
// overpayment stays in the PayRam wallet as a buffer.
|
|
38
38
|
const creditCents = charge.amountUsdCents;
|
|
39
|
-
await creditLedger.credit(charge.tenantId, Credit.fromCents(creditCents), "purchase",
|
|
39
|
+
await creditLedger.credit(charge.tenantId, Credit.fromCents(creditCents), "purchase", {
|
|
40
|
+
description: `Crypto credit purchase via PayRam (ref: ${payload.reference_id}, ${payload.currency ?? "crypto"})`,
|
|
41
|
+
referenceId: `payram:${payload.reference_id}`,
|
|
42
|
+
fundingSource: "payram",
|
|
43
|
+
});
|
|
40
44
|
await chargeStore.markCredited(payload.reference_id);
|
|
41
45
|
// Reactivate suspended resources after credit purchase.
|
|
42
46
|
let reactivatedBots;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* no-op status, idempotency, replay guard, and bot reactivation.
|
|
6
6
|
*/
|
|
7
7
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
|
-
import {
|
|
8
|
+
import { DrizzleLedger } from "../../credits/ledger.js";
|
|
9
9
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
10
10
|
import { DrizzleWebhookSeenRepository } from "../drizzle-webhook-seen-repository.js";
|
|
11
11
|
import { noOpReplayGuard } from "../webhook-seen-repository.js";
|
|
@@ -37,7 +37,8 @@ describe("handlePayRamWebhook", () => {
|
|
|
37
37
|
beforeEach(async () => {
|
|
38
38
|
await truncateAllTables(pool);
|
|
39
39
|
chargeStore = new PayRamChargeRepository(db);
|
|
40
|
-
creditLedger = new
|
|
40
|
+
creditLedger = new DrizzleLedger(db);
|
|
41
|
+
await creditLedger.seedSystemAccounts();
|
|
41
42
|
deps = { chargeStore, creditLedger, replayGuard: noOpReplayGuard };
|
|
42
43
|
// Create a default test charge
|
|
43
44
|
await chargeStore.create("ref-test-001", "tenant-a", 2500);
|
|
@@ -60,12 +61,12 @@ describe("handlePayRamWebhook", () => {
|
|
|
60
61
|
const history = await creditLedger.history("tenant-a");
|
|
61
62
|
expect(history).toHaveLength(1);
|
|
62
63
|
expect(history[0].referenceId).toBe("payram:ref-test-001");
|
|
63
|
-
expect(history[0].
|
|
64
|
+
expect(history[0].entryType).toBe("purchase");
|
|
64
65
|
});
|
|
65
66
|
it("records fundingSource as payram", async () => {
|
|
66
67
|
await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
|
|
67
68
|
const history = await creditLedger.history("tenant-a");
|
|
68
|
-
expect(history[0].fundingSource).toBe("payram");
|
|
69
|
+
expect(history[0].metadata?.fundingSource).toBe("payram");
|
|
69
70
|
});
|
|
70
71
|
it("marks the charge as credited after FILLED", async () => {
|
|
71
72
|
await handlePayRamWebhook(deps, makePayload({ status: "FILLED" }));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type Stripe from "stripe";
|
|
2
2
|
import type { IAutoTopupEventLogRepository } from "../../credits/auto-topup-event-log-repository.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ILedger } from "../../credits/ledger.js";
|
|
4
4
|
import { type ChargeOpts, type ChargeResult, type CheckoutOpts, type CheckoutSession, type Invoice, type IPaymentProcessor, type PortalOpts, type SavedPaymentMethod, type SetupResult, type WebhookResult } from "../payment-processor.js";
|
|
5
5
|
import type { CreditPriceMap } from "./credit-prices.js";
|
|
6
6
|
import type { ITenantCustomerRepository } from "./tenant-store.js";
|
|
@@ -18,7 +18,7 @@ export interface StripePaymentProcessorDeps {
|
|
|
18
18
|
tenantRepo: ITenantCustomerRepository;
|
|
19
19
|
webhookSecret: string;
|
|
20
20
|
priceMap?: CreditPriceMap;
|
|
21
|
-
creditLedger:
|
|
21
|
+
creditLedger: ILedger;
|
|
22
22
|
autoTopupEventLog?: IAutoTopupEventLogRepository;
|
|
23
23
|
/** Consumer-supplied webhook handler (handles domain-specific event processing). */
|
|
24
24
|
webhookHandler?: (event: Stripe.Event) => Promise<StripeWebhookHandlerResult>;
|
|
@@ -45,6 +45,7 @@ function createMocks() {
|
|
|
45
45
|
buildCustomerIdMap: vi.fn(),
|
|
46
46
|
};
|
|
47
47
|
const creditLedger = {
|
|
48
|
+
post: vi.fn(),
|
|
48
49
|
credit: vi.fn(),
|
|
49
50
|
debit: vi.fn(),
|
|
50
51
|
balance: vi.fn(),
|
|
@@ -55,6 +56,12 @@ function createMocks() {
|
|
|
55
56
|
expiredCredits: vi.fn(),
|
|
56
57
|
lifetimeSpend: vi.fn(),
|
|
57
58
|
lifetimeSpendBatch: vi.fn().mockResolvedValue(new Map()),
|
|
59
|
+
trialBalance: vi.fn(),
|
|
60
|
+
accountBalance: vi.fn(),
|
|
61
|
+
seedSystemAccounts: vi.fn(),
|
|
62
|
+
existsByReferenceIdLike: vi.fn(),
|
|
63
|
+
sumPurchasesForPeriod: vi.fn(),
|
|
64
|
+
getActiveTenantIdsInWindow: vi.fn(),
|
|
58
65
|
};
|
|
59
66
|
const autoTopupEventLog = {
|
|
60
67
|
writeEvent: vi.fn(),
|
|
@@ -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 declare class DrizzleTenantCustomerRepository implements ITenantCustomerRepository {
|
|
29
29
|
private readonly db;
|
|
@@ -7,7 +7,7 @@ import { tenantCustomers } from "../../db/schema/tenant-customers.js";
|
|
|
7
7
|
* All billing operations look up the processor customer via this store.
|
|
8
8
|
*
|
|
9
9
|
* Note: No subscription tracking — WOPR uses credits, not subscriptions.
|
|
10
|
-
* Credit balances are managed by
|
|
10
|
+
* Credit balances are managed by ILedger / DrizzleCreditLedger.
|
|
11
11
|
*/
|
|
12
12
|
export class DrizzleTenantCustomerRepository {
|
|
13
13
|
db;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import Stripe from "stripe";
|
|
2
2
|
import type { IAutoTopupEventLogRepository } from "./auto-topup-event-log-repository.js";
|
|
3
3
|
import type { Credit } from "./credit.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { ILedger } from "./ledger.js";
|
|
5
5
|
import type { ITenantCustomerRepository } from "./tenant-customer-repository.js";
|
|
6
6
|
/** After this many consecutive Stripe failures, the auto-topup mode is disabled. */
|
|
7
7
|
export declare const MAX_CONSECUTIVE_FAILURES = 3;
|
|
8
8
|
export interface AutoTopupChargeDeps {
|
|
9
9
|
stripe: Stripe;
|
|
10
10
|
tenantRepo: ITenantCustomerRepository;
|
|
11
|
-
creditLedger:
|
|
11
|
+
creditLedger: ILedger;
|
|
12
12
|
eventLogRepo: IAutoTopupEventLogRepository;
|
|
13
13
|
}
|
|
14
14
|
export interface AutoTopupChargeResult {
|
|
@@ -113,7 +113,11 @@ export async function chargeAutoTopup(deps, tenantId, amount, source) {
|
|
|
113
113
|
// 5. Credit the ledger (idempotent via referenceId = PI ID)
|
|
114
114
|
try {
|
|
115
115
|
if (!(await deps.creditLedger.hasReferenceId(paymentIntent.id))) {
|
|
116
|
-
await deps.creditLedger.credit(tenantId, amount, "purchase",
|
|
116
|
+
await deps.creditLedger.credit(tenantId, amount, "purchase", {
|
|
117
|
+
description: `Auto-topup (${source})`,
|
|
118
|
+
referenceId: paymentIntent.id,
|
|
119
|
+
fundingSource: "stripe",
|
|
120
|
+
});
|
|
117
121
|
}
|
|
118
122
|
}
|
|
119
123
|
catch (err) {
|
|
@@ -6,7 +6,7 @@ import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
|
6
6
|
import { chargeAutoTopup, MAX_CONSECUTIVE_FAILURES } from "./auto-topup-charge.js";
|
|
7
7
|
import { DrizzleAutoTopupEventLogRepository } from "./auto-topup-event-log-repository.js";
|
|
8
8
|
import { Credit } from "./credit.js";
|
|
9
|
-
import {
|
|
9
|
+
import { DrizzleLedger } from "./ledger.js";
|
|
10
10
|
function mockStripe(overrides) {
|
|
11
11
|
const piId = overrides?.paymentIntentId ?? `pi_${crypto.randomUUID()}`;
|
|
12
12
|
return {
|
|
@@ -47,7 +47,8 @@ describe("chargeAutoTopup", () => {
|
|
|
47
47
|
});
|
|
48
48
|
beforeEach(async () => {
|
|
49
49
|
await truncateAllTables(pool);
|
|
50
|
-
ledger = new
|
|
50
|
+
ledger = new DrizzleLedger(db);
|
|
51
|
+
await ledger.seedSystemAccounts();
|
|
51
52
|
});
|
|
52
53
|
it("charges Stripe and credits ledger on success", async () => {
|
|
53
54
|
const stripe = mockStripe();
|
|
@@ -63,8 +64,8 @@ describe("chargeAutoTopup", () => {
|
|
|
63
64
|
expect(result.paymentReference).toEqual(expect.any(String));
|
|
64
65
|
expect((await ledger.balance("t1")).toCents()).toBe(500);
|
|
65
66
|
const history = await ledger.history("t1");
|
|
66
|
-
expect(history[0].
|
|
67
|
-
expect(history[0].fundingSource).toBe("stripe");
|
|
67
|
+
expect(history[0].entryType).toBe("purchase");
|
|
68
|
+
expect(history[0].metadata?.fundingSource).toBe("stripe");
|
|
68
69
|
});
|
|
69
70
|
it("writes success event to credit_auto_topup log", async () => {
|
|
70
71
|
const stripe = mockStripe();
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { AutoTopupChargeResult } from "./auto-topup-charge.js";
|
|
2
2
|
import type { IAutoTopupSettingsRepository } from "./auto-topup-settings-repository.js";
|
|
3
3
|
import type { Credit } from "./credit.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { ILedger } from "./ledger.js";
|
|
5
5
|
export interface UsageTopupDeps {
|
|
6
6
|
settingsRepo: IAutoTopupSettingsRepository;
|
|
7
|
-
creditLedger:
|
|
7
|
+
creditLedger: ILedger;
|
|
8
8
|
/** Injected charge function (allows mocking in tests). */
|
|
9
9
|
chargeAutoTopup: (tenantId: string, amount: Credit, source: string) => Promise<AutoTopupChargeResult>;
|
|
10
10
|
/** Optional tenant status check. If provided and returns non-null, skip the charge. */
|
|
@@ -3,7 +3,7 @@ import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
|
3
3
|
import { DrizzleAutoTopupSettingsRepository } from "./auto-topup-settings-repository.js";
|
|
4
4
|
import { maybeTriggerUsageTopup } from "./auto-topup-usage.js";
|
|
5
5
|
import { Credit } from "./credit.js";
|
|
6
|
-
import {
|
|
6
|
+
import { DrizzleLedger } from "./ledger.js";
|
|
7
7
|
describe("maybeTriggerUsageTopup", () => {
|
|
8
8
|
let pool;
|
|
9
9
|
let db;
|
|
@@ -17,7 +17,8 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
17
17
|
});
|
|
18
18
|
beforeEach(async () => {
|
|
19
19
|
await truncateAllTables(pool);
|
|
20
|
-
ledger = new
|
|
20
|
+
ledger = new DrizzleLedger(db);
|
|
21
|
+
await ledger.seedSystemAccounts();
|
|
21
22
|
settingsRepo = new DrizzleAutoTopupSettingsRepository(db);
|
|
22
23
|
});
|
|
23
24
|
it("does nothing when tenant has no auto-topup settings", async () => {
|
|
@@ -28,7 +29,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
28
29
|
});
|
|
29
30
|
it("does nothing when usage_enabled is false", async () => {
|
|
30
31
|
await settingsRepo.upsert("t1", { usageEnabled: false });
|
|
31
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
32
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
33
|
+
description: "buy",
|
|
34
|
+
referenceId: "ref-1",
|
|
35
|
+
fundingSource: "stripe",
|
|
36
|
+
});
|
|
32
37
|
const mockCharge = vi.fn();
|
|
33
38
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
34
39
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -40,7 +45,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
40
45
|
usageThreshold: Credit.fromCents(100),
|
|
41
46
|
usageTopup: Credit.fromCents(500),
|
|
42
47
|
});
|
|
43
|
-
await ledger.credit("t1", Credit.fromCents(200), "purchase",
|
|
48
|
+
await ledger.credit("t1", Credit.fromCents(200), "purchase", {
|
|
49
|
+
description: "buy",
|
|
50
|
+
referenceId: "ref-1",
|
|
51
|
+
fundingSource: "stripe",
|
|
52
|
+
});
|
|
44
53
|
const mockCharge = vi.fn();
|
|
45
54
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
46
55
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -52,7 +61,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
52
61
|
usageThreshold: Credit.fromCents(100),
|
|
53
62
|
usageTopup: Credit.fromCents(500),
|
|
54
63
|
});
|
|
55
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
64
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
65
|
+
description: "buy",
|
|
66
|
+
referenceId: "ref-1",
|
|
67
|
+
fundingSource: "stripe",
|
|
68
|
+
});
|
|
56
69
|
const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_123" });
|
|
57
70
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
58
71
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -61,7 +74,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
61
74
|
it("skips when charge is already in-flight", async () => {
|
|
62
75
|
await settingsRepo.upsert("t1", { usageEnabled: true, usageThreshold: Credit.fromCents(100) });
|
|
63
76
|
await settingsRepo.setUsageChargeInFlight("t1", true);
|
|
64
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
77
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
78
|
+
description: "buy",
|
|
79
|
+
referenceId: "ref-1",
|
|
80
|
+
fundingSource: "stripe",
|
|
81
|
+
});
|
|
65
82
|
const mockCharge = vi.fn();
|
|
66
83
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
67
84
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -74,7 +91,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
74
91
|
usageThreshold: Credit.fromCents(500),
|
|
75
92
|
usageTopup: Credit.fromCents(2000),
|
|
76
93
|
});
|
|
77
|
-
await ledger.credit("t1", Credit.fromCents(100), "purchase",
|
|
94
|
+
await ledger.credit("t1", Credit.fromCents(100), "purchase", {
|
|
95
|
+
description: "buy",
|
|
96
|
+
referenceId: "ref-1",
|
|
97
|
+
fundingSource: "stripe",
|
|
98
|
+
});
|
|
78
99
|
const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_race" });
|
|
79
100
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
80
101
|
// Fire two concurrent calls — both see balance < threshold,
|
|
@@ -93,7 +114,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
93
114
|
usageThreshold: Credit.fromCents(100),
|
|
94
115
|
usageTopup: Credit.fromCents(500),
|
|
95
116
|
});
|
|
96
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
117
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
118
|
+
description: "buy",
|
|
119
|
+
referenceId: "ref-1",
|
|
120
|
+
fundingSource: "stripe",
|
|
121
|
+
});
|
|
97
122
|
const mockCharge = vi.fn().mockResolvedValue({ success: true, paymentReference: "pi_123" });
|
|
98
123
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
99
124
|
// First call — triggers charge, flag set then cleared
|
|
@@ -111,7 +136,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
111
136
|
usageThreshold: Credit.fromCents(100),
|
|
112
137
|
usageTopup: Credit.fromCents(500),
|
|
113
138
|
});
|
|
114
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
139
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
140
|
+
description: "buy",
|
|
141
|
+
referenceId: "ref-1",
|
|
142
|
+
fundingSource: "stripe",
|
|
143
|
+
});
|
|
115
144
|
const mockCharge = vi
|
|
116
145
|
.fn()
|
|
117
146
|
.mockRejectedValueOnce(new Error("Stripe network error"))
|
|
@@ -134,7 +163,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
134
163
|
});
|
|
135
164
|
await settingsRepo.incrementUsageFailures("t1");
|
|
136
165
|
await settingsRepo.incrementUsageFailures("t1");
|
|
137
|
-
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
|
+
});
|
|
138
171
|
const mockCharge = vi.fn().mockResolvedValue({ success: true });
|
|
139
172
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
140
173
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -146,7 +179,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
146
179
|
usageThreshold: Credit.fromCents(100),
|
|
147
180
|
usageTopup: Credit.fromCents(500),
|
|
148
181
|
});
|
|
149
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
182
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
183
|
+
description: "buy",
|
|
184
|
+
referenceId: "ref-1",
|
|
185
|
+
fundingSource: "stripe",
|
|
186
|
+
});
|
|
150
187
|
const mockCharge = vi.fn().mockResolvedValue({ success: false, error: "declined" });
|
|
151
188
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
152
189
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -172,7 +209,11 @@ describe("maybeTriggerUsageTopup", () => {
|
|
|
172
209
|
});
|
|
173
210
|
await settingsRepo.incrementUsageFailures("t1");
|
|
174
211
|
await settingsRepo.incrementUsageFailures("t1");
|
|
175
|
-
await ledger.credit("t1", Credit.fromCents(50), "purchase",
|
|
212
|
+
await ledger.credit("t1", Credit.fromCents(50), "purchase", {
|
|
213
|
+
description: "buy",
|
|
214
|
+
referenceId: "ref-1",
|
|
215
|
+
fundingSource: "stripe",
|
|
216
|
+
});
|
|
176
217
|
const mockCharge = vi.fn().mockResolvedValue({ success: false, error: "declined" });
|
|
177
218
|
const deps = { settingsRepo, creditLedger: ledger, chargeAutoTopup: mockCharge };
|
|
178
219
|
await maybeTriggerUsageTopup(deps, "t1");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { logger } from "../config/logger.js";
|
|
2
|
-
import { InsufficientBalanceError } from "./
|
|
2
|
+
import { InsufficientBalanceError } from "./ledger.js";
|
|
3
3
|
/**
|
|
4
4
|
* Sweep expired credit grants and debit the original grant amount
|
|
5
5
|
* (or remaining balance if partially consumed).
|
|
@@ -23,7 +23,10 @@ export async function runCreditExpiryCron(cfg) {
|
|
|
23
23
|
}
|
|
24
24
|
// Debit the lesser of the original grant amount or current balance
|
|
25
25
|
const debitAmount = balance.lessThan(grant.amount) ? balance : grant.amount;
|
|
26
|
-
await cfg.ledger.debit(grant.tenantId, debitAmount, "credit_expiry",
|
|
26
|
+
await cfg.ledger.debit(grant.tenantId, debitAmount, "credit_expiry", {
|
|
27
|
+
description: `Expired credit grant reclaimed: ${grant.entryId}`,
|
|
28
|
+
referenceId: `expiry:${grant.entryId}`,
|
|
29
|
+
});
|
|
27
30
|
result.processed++;
|
|
28
31
|
if (!result.expired.includes(grant.tenantId)) {
|
|
29
32
|
result.expired.push(grant.tenantId);
|
|
@@ -35,8 +38,8 @@ export async function runCreditExpiryCron(cfg) {
|
|
|
35
38
|
}
|
|
36
39
|
else {
|
|
37
40
|
const msg = err instanceof Error ? err.message : String(err);
|
|
38
|
-
logger.error("Credit expiry failed", { tenantId: grant.tenantId,
|
|
39
|
-
result.errors.push(`${grant.tenantId}:${grant.
|
|
41
|
+
logger.error("Credit expiry failed", { tenantId: grant.tenantId, entryId: grant.entryId, error: msg });
|
|
42
|
+
result.errors.push(`${grant.tenantId}:${grant.entryId}: ${msg}`);
|
|
40
43
|
}
|
|
41
44
|
}
|
|
42
45
|
}
|
|
@@ -2,20 +2,21 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
|
2
2
|
import { createTestDb, truncateAllTables } from "../test/db.js";
|
|
3
3
|
import { Credit } from "./credit.js";
|
|
4
4
|
import { runCreditExpiryCron } from "./credit-expiry-cron.js";
|
|
5
|
-
import {
|
|
5
|
+
import { DrizzleLedger } from "./ledger.js";
|
|
6
6
|
describe("runCreditExpiryCron", () => {
|
|
7
7
|
let pool;
|
|
8
8
|
let ledger;
|
|
9
9
|
beforeAll(async () => {
|
|
10
10
|
const { db, pool: p } = await createTestDb();
|
|
11
11
|
pool = p;
|
|
12
|
-
ledger = new
|
|
12
|
+
ledger = new DrizzleLedger(db);
|
|
13
13
|
});
|
|
14
14
|
afterAll(async () => {
|
|
15
15
|
await pool.close();
|
|
16
16
|
});
|
|
17
17
|
beforeEach(async () => {
|
|
18
18
|
await truncateAllTables(pool);
|
|
19
|
+
await ledger.seedSystemAccounts();
|
|
19
20
|
});
|
|
20
21
|
// All tests pass an explicit `now` parameter — hardcoded dates are time-independent
|
|
21
22
|
// because runCreditExpiryCron never reads the system clock.
|
|
@@ -26,7 +27,11 @@ describe("runCreditExpiryCron", () => {
|
|
|
26
27
|
expect(result.errors).toEqual([]);
|
|
27
28
|
});
|
|
28
29
|
it("debits expired promotional credit grant", async () => {
|
|
29
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "promo",
|
|
30
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
31
|
+
description: "New user bonus",
|
|
32
|
+
referenceId: "promo:tenant-1",
|
|
33
|
+
expiresAt: "2026-01-10T00:00:00Z",
|
|
34
|
+
});
|
|
30
35
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
31
36
|
expect(result.processed).toBe(1);
|
|
32
37
|
expect(result.expired).toContain("tenant-1");
|
|
@@ -34,29 +39,41 @@ describe("runCreditExpiryCron", () => {
|
|
|
34
39
|
expect(balance.toCents()).toBe(0);
|
|
35
40
|
});
|
|
36
41
|
it("does not debit non-expired credits", async () => {
|
|
37
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "promo",
|
|
42
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
43
|
+
description: "Future bonus",
|
|
44
|
+
referenceId: "promo:tenant-1-future",
|
|
45
|
+
expiresAt: "2026-02-01T00:00:00Z",
|
|
46
|
+
});
|
|
38
47
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
39
48
|
expect(result.processed).toBe(0);
|
|
40
49
|
const balance = await ledger.balance("tenant-1");
|
|
41
50
|
expect(balance.toCents()).toBe(500);
|
|
42
51
|
});
|
|
43
52
|
it("does not debit credits without expires_at", async () => {
|
|
44
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", "Top-up");
|
|
53
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "purchase", { description: "Top-up" });
|
|
45
54
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
46
55
|
expect(result.processed).toBe(0);
|
|
47
56
|
const balance = await ledger.balance("tenant-1");
|
|
48
57
|
expect(balance.toCents()).toBe(500);
|
|
49
58
|
});
|
|
50
59
|
it("only debits up to available balance when partially consumed", async () => {
|
|
51
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "promo",
|
|
52
|
-
|
|
60
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
61
|
+
description: "Promo",
|
|
62
|
+
referenceId: "promo:partial",
|
|
63
|
+
expiresAt: "2026-01-10T00:00:00Z",
|
|
64
|
+
});
|
|
65
|
+
await ledger.debit("tenant-1", Credit.fromCents(300), "bot_runtime", { description: "Runtime" });
|
|
53
66
|
const result = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
54
67
|
expect(result.processed).toBe(1);
|
|
55
68
|
const balance = await ledger.balance("tenant-1");
|
|
56
69
|
expect(balance.toCents()).toBe(0);
|
|
57
70
|
});
|
|
58
71
|
it("is idempotent -- does not double-debit on second run", async () => {
|
|
59
|
-
await ledger.credit("tenant-1", Credit.fromCents(500), "promo",
|
|
72
|
+
await ledger.credit("tenant-1", Credit.fromCents(500), "promo", {
|
|
73
|
+
description: "Promo",
|
|
74
|
+
referenceId: "promo:idemp",
|
|
75
|
+
expiresAt: "2026-01-10T00:00:00Z",
|
|
76
|
+
});
|
|
60
77
|
await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
61
78
|
const balanceAfterFirst = await ledger.balance("tenant-1");
|
|
62
79
|
const result2 = await runCreditExpiryCron({ ledger, now: "2026-01-15T00:00:00Z" });
|
|
@@ -56,7 +56,7 @@ export declare class InsufficientBalanceError extends Error {
|
|
|
56
56
|
requestedAmount: Credit;
|
|
57
57
|
constructor(currentBalance: Credit, requestedAmount: Credit);
|
|
58
58
|
}
|
|
59
|
-
export interface
|
|
59
|
+
export interface ILedger {
|
|
60
60
|
credit(tenantId: string, amount: Credit, type: CreditType, description?: string, referenceId?: string, fundingSource?: string, attributedUserId?: string, expiresAt?: string): Promise<CreditTransaction>;
|
|
61
61
|
expiredCredits(now: string): Promise<Array<{
|
|
62
62
|
id: string;
|
|
@@ -82,7 +82,7 @@ export interface ICreditLedger {
|
|
|
82
82
|
* creditBalances row is always consistent with the sum of creditTransactions.
|
|
83
83
|
* Zero raw SQL in application code.
|
|
84
84
|
*/
|
|
85
|
-
export declare class DrizzleCreditLedger implements
|
|
85
|
+
export declare class DrizzleCreditLedger implements ILedger {
|
|
86
86
|
private readonly db;
|
|
87
87
|
constructor(db: PlatformDb);
|
|
88
88
|
/**
|
|
@@ -289,5 +289,5 @@ export class DrizzleCreditLedger {
|
|
|
289
289
|
.where(sql `${creditBalances.balance} > 0`);
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
|
-
// Backward-compat alias — callers using 'new
|
|
292
|
+
// Backward-compat alias — callers using 'new DrizzleLedger(db)' continue to work.
|
|
293
293
|
export { DrizzleCreditLedger as CreditLedger };
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { Credit } from "./credit.js";
|
|
2
|
-
import type {
|
|
3
|
-
import type { ICreditTransactionRepository } from "./credit-transaction-repository.js";
|
|
2
|
+
import type { ILedger } from "./ledger.js";
|
|
4
3
|
export interface DividendCronConfig {
|
|
5
|
-
|
|
6
|
-
ledger: ICreditLedger;
|
|
4
|
+
ledger: ILedger;
|
|
7
5
|
/** Fraction of daily purchases matched as dividend pool. Default 1.0 (100%). */
|
|
8
6
|
matchRate: number;
|
|
9
7
|
/** The date to compute dividend for, as YYYY-MM-DD string. Typically yesterday. */
|
|
@@ -21,8 +19,8 @@ export interface DividendCronResult {
|
|
|
21
19
|
* Compute and distribute the community dividend for a given day.
|
|
22
20
|
*
|
|
23
21
|
* 1. Check idempotency — skip if already run for this date.
|
|
24
|
-
* 2. Sum all 'purchase'
|
|
25
|
-
* 3. Find all tenants with a 'purchase'
|
|
22
|
+
* 2. Sum all 'purchase' entries for the target date.
|
|
23
|
+
* 3. Find all tenants with a 'purchase' entry in the last 7 days.
|
|
26
24
|
* 4. Compute pool = sum × matchRate, per-user share = floor(pool / activeCount).
|
|
27
25
|
* 5. Credit each active tenant with their share.
|
|
28
26
|
*/
|