@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
|
@@ -82,7 +82,10 @@ export class PromotionEngine {
|
|
|
82
82
|
if (!granted)
|
|
83
83
|
return null;
|
|
84
84
|
// Grant credits
|
|
85
|
-
const tx = await ledger.credit(ctx.tenantId, grantAmount, "promo",
|
|
85
|
+
const tx = await ledger.credit(ctx.tenantId, grantAmount, "promo", {
|
|
86
|
+
description: `Promotion: ${promo.name}`,
|
|
87
|
+
referenceId: refId,
|
|
88
|
+
});
|
|
86
89
|
// Record redemption
|
|
87
90
|
await redemptionRepo.create({
|
|
88
91
|
promotionId: promo.id,
|
|
@@ -96,7 +96,9 @@ describe("PromotionEngine", () => {
|
|
|
96
96
|
});
|
|
97
97
|
expect(results).toHaveLength(1);
|
|
98
98
|
expect(ledger.credit).toHaveBeenCalledWith("tenant-1", expect.any(Object), // Credit instance
|
|
99
|
-
"promo", expect.
|
|
99
|
+
"promo", expect.objectContaining({
|
|
100
|
+
referenceId: "promo:promo-1:tenant-1:1",
|
|
101
|
+
}));
|
|
100
102
|
});
|
|
101
103
|
it("skips if already redeemed (idempotency)", async () => {
|
|
102
104
|
vi.mocked(ledger.hasReferenceId).mockResolvedValue(true);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { IPayRamChargeRepository, ITenantCustomerRepository, PayRamChargeRecord, } from "@wopr-network/platform-core/billing";
|
|
2
|
-
export type { IAutoTopupSettingsRepository,
|
|
2
|
+
export type { IAutoTopupSettingsRepository, ILedger } from "@wopr-network/platform-core/credits";
|
|
3
3
|
export type { IMeterAggregator, IMeterEmitter } from "@wopr-network/platform-core/metering";
|
|
4
4
|
export type { FraudEvent, FraudEventInput, IAffiliateFraudRepository } from "./affiliate/affiliate-fraud-repository.js";
|
|
5
5
|
export type { AffiliateCode, AffiliateReferral, AffiliateStats, IAffiliateRepository, } from "./affiliate/drizzle-affiliate-repository.js";
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
import type { MeterEmitter } from "@wopr-network/platform-core/metering";
|
|
13
13
|
import type { AdapterCapability, ProviderAdapter } from "../adapters/types.js";
|
|
14
14
|
import type { ArbitrageRouter } from "../arbitrage/router.js";
|
|
15
|
-
import type {
|
|
15
|
+
import type { IBudgetChecker, SpendLimits } from "../budget/budget-checker.js";
|
|
16
16
|
export interface SocketConfig {
|
|
17
17
|
/** MeterEmitter instance for usage tracking */
|
|
18
18
|
meter: MeterEmitter;
|
|
19
|
-
/**
|
|
20
|
-
budgetChecker?:
|
|
19
|
+
/** IBudgetChecker instance for pre-call budget validation */
|
|
20
|
+
budgetChecker?: IBudgetChecker;
|
|
21
21
|
/** Default margin multiplier (default: 1.3) */
|
|
22
22
|
defaultMargin?: number;
|
|
23
23
|
/** ArbitrageRouter for cost-optimized routing (GPU-first, cheapest, 5xx failover) */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { IWebhookSeenRepository } from "@wopr-network/platform-core/billing";
|
|
2
2
|
import { type ChargeOpts, type ChargeResult, type CheckoutOpts, type CheckoutSession, type CreditPriceMap, type Invoice, type IPaymentProcessor, type ITenantCustomerRepository, type PortalOpts, type SavedPaymentMethod, type SetupResult, type WebhookResult } from "@wopr-network/platform-core/billing";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
4
4
|
import type Stripe from "stripe";
|
|
5
5
|
import type { IAutoTopupEventLogRepository } from "../credits/auto-topup-event-log-repository.js";
|
|
6
6
|
import type { BotBilling } from "../credits/bot-billing.js";
|
|
@@ -9,7 +9,7 @@ export interface StripePaymentProcessorDeps {
|
|
|
9
9
|
tenantRepo: ITenantCustomerRepository;
|
|
10
10
|
webhookSecret: string;
|
|
11
11
|
priceMap?: CreditPriceMap;
|
|
12
|
-
creditLedger:
|
|
12
|
+
creditLedger: ILedger;
|
|
13
13
|
botBilling?: BotBilling;
|
|
14
14
|
replayGuard: IWebhookSeenRepository;
|
|
15
15
|
autoTopupEventLog?: IAutoTopupEventLogRepository;
|
|
@@ -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 replayGuard = {
|
|
60
67
|
isDuplicate: vi.fn(),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CreditPriceMap, IWebhookSeenRepository, TenantCustomerRepository } from "@wopr-network/platform-core/billing";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import { Credit } from "@wopr-network/platform-core/credits";
|
|
4
4
|
import type { NotificationService } from "@wopr-network/platform-core/email";
|
|
5
5
|
import type Stripe from "stripe";
|
|
@@ -34,7 +34,7 @@ export interface WebhookResult {
|
|
|
34
34
|
*/
|
|
35
35
|
export interface WebhookDeps {
|
|
36
36
|
tenantRepo: TenantCustomerRepository;
|
|
37
|
-
creditLedger:
|
|
37
|
+
creditLedger: ILedger;
|
|
38
38
|
/** Map of Stripe Price ID -> CreditPricePoint for bonus calculation. */
|
|
39
39
|
priceMap?: CreditPriceMap;
|
|
40
40
|
/** Bot billing manager for reactivation after credit purchase (WOP-447). */
|
|
@@ -2,6 +2,43 @@ import { Credit } from "@wopr-network/platform-core/credits";
|
|
|
2
2
|
import { logger } from "../../config/logger.js";
|
|
3
3
|
import { processAffiliateCreditMatch } from "../affiliate/credit-match.js";
|
|
4
4
|
import { grantNewUserBonus } from "../affiliate/new-user-bonus.js";
|
|
5
|
+
/**
|
|
6
|
+
* Extract the card fingerprint from a Stripe webhook payload object.
|
|
7
|
+
* Works for PaymentIntent (via latest_charge or charges.data[0]) and
|
|
8
|
+
* Invoice (via charge) objects where the Charge is expanded.
|
|
9
|
+
* Returns undefined when the nested object is only a string ID (not expanded).
|
|
10
|
+
*/
|
|
11
|
+
function extractStripeFingerprint(obj) {
|
|
12
|
+
if (!obj || typeof obj !== "object")
|
|
13
|
+
return undefined;
|
|
14
|
+
const o = obj;
|
|
15
|
+
// PaymentIntent: pi.latest_charge expanded → Charge.payment_method_details.card.fingerprint
|
|
16
|
+
const latestCharge = o.latest_charge;
|
|
17
|
+
if (latestCharge && typeof latestCharge === "object") {
|
|
18
|
+
const pmd = latestCharge.payment_method_details;
|
|
19
|
+
const fp = pmd?.card?.fingerprint;
|
|
20
|
+
if (typeof fp === "string")
|
|
21
|
+
return fp;
|
|
22
|
+
}
|
|
23
|
+
// PaymentIntent (older API): pi.charges.data[0].payment_method_details.card.fingerprint
|
|
24
|
+
const charges = o.charges;
|
|
25
|
+
const charge0 = charges?.data?.[0];
|
|
26
|
+
if (charge0) {
|
|
27
|
+
const pmd = charge0.payment_method_details;
|
|
28
|
+
const fp = pmd?.card?.fingerprint;
|
|
29
|
+
if (typeof fp === "string")
|
|
30
|
+
return fp;
|
|
31
|
+
}
|
|
32
|
+
// Invoice: invoice.charge expanded → Charge.payment_method_details.card.fingerprint
|
|
33
|
+
const invoiceCharge = o.charge;
|
|
34
|
+
if (invoiceCharge && typeof invoiceCharge === "object") {
|
|
35
|
+
const pmd = invoiceCharge.payment_method_details;
|
|
36
|
+
const fp = pmd?.card?.fingerprint;
|
|
37
|
+
if (typeof fp === "string")
|
|
38
|
+
return fp;
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
5
42
|
/**
|
|
6
43
|
* Process a Stripe webhook event.
|
|
7
44
|
*
|
|
@@ -64,7 +101,12 @@ export async function handleWebhookEvent(deps, event) {
|
|
|
64
101
|
creditCents = amountPaid;
|
|
65
102
|
}
|
|
66
103
|
// Credit the ledger with session ID as reference for idempotency.
|
|
67
|
-
await deps.creditLedger.credit(tenant, Credit.fromCents(creditCents), "purchase",
|
|
104
|
+
await deps.creditLedger.credit(tenant, Credit.fromCents(creditCents), "purchase", {
|
|
105
|
+
description: `Stripe credit purchase (session: ${stripeSessionId})`,
|
|
106
|
+
referenceId: stripeSessionId,
|
|
107
|
+
fundingSource: "stripe",
|
|
108
|
+
stripeFingerprint: extractStripeFingerprint(session),
|
|
109
|
+
});
|
|
68
110
|
// New-user first-purchase bonus for referred users (WOP-950).
|
|
69
111
|
// Must run before credit match so markFirstPurchase hasn't been called yet.
|
|
70
112
|
let affiliateBonusCredit;
|
|
@@ -198,7 +240,12 @@ export async function handleWebhookEvent(deps, event) {
|
|
|
198
240
|
}
|
|
199
241
|
// Fallback grant — inline path failed or process crashed before granting.
|
|
200
242
|
const source = pi.metadata?.wopr_source ?? "auto_topup_webhook_fallback";
|
|
201
|
-
await deps.creditLedger.credit(tenant, Credit.fromCents(pi.amount), "purchase",
|
|
243
|
+
await deps.creditLedger.credit(tenant, Credit.fromCents(pi.amount), "purchase", {
|
|
244
|
+
description: `Auto-topup webhook fallback (${source})`,
|
|
245
|
+
referenceId: pi.id,
|
|
246
|
+
fundingSource: "stripe",
|
|
247
|
+
stripeFingerprint: extractStripeFingerprint(pi),
|
|
248
|
+
});
|
|
202
249
|
result = {
|
|
203
250
|
handled: true,
|
|
204
251
|
event_type: event.type,
|
|
@@ -261,7 +308,12 @@ export async function handleWebhookEvent(deps, event) {
|
|
|
261
308
|
result = { handled: true, event_type: event.type, tenant, creditedCents: 0 };
|
|
262
309
|
break;
|
|
263
310
|
}
|
|
264
|
-
await deps.creditLedger.credit(tenant, Credit.fromCents(amountPaid), "purchase",
|
|
311
|
+
await deps.creditLedger.credit(tenant, Credit.fromCents(amountPaid), "purchase", {
|
|
312
|
+
description: `Stripe subscription renewal (invoice: ${invoice.id})`,
|
|
313
|
+
referenceId: invoice.id,
|
|
314
|
+
fundingSource: "stripe",
|
|
315
|
+
stripeFingerprint: extractStripeFingerprint(invoice),
|
|
316
|
+
});
|
|
265
317
|
// Reactivate suspended bots now that balance is positive (WOP-447).
|
|
266
318
|
let reactivatedBots;
|
|
267
319
|
if (deps.botBilling) {
|
|
@@ -304,7 +356,11 @@ export async function handleWebhookEvent(deps, event) {
|
|
|
304
356
|
break;
|
|
305
357
|
}
|
|
306
358
|
// Debit the ledger. Allow negative balance — refund must always succeed.
|
|
307
|
-
await deps.creditLedger.debit(tenant, Credit.fromCents(refundedCents), "refund",
|
|
359
|
+
await deps.creditLedger.debit(tenant, Credit.fromCents(refundedCents), "refund", {
|
|
360
|
+
description: `Stripe refund (charge: ${charge.id})`,
|
|
361
|
+
referenceId: event.id,
|
|
362
|
+
allowNegative: true,
|
|
363
|
+
});
|
|
308
364
|
logger.warn("Charge refunded — credits debited", { tenant, customerId, chargeId: charge.id, refundedCents });
|
|
309
365
|
result = { handled: true, event_type: event.type, tenant, debitedCents: refundedCents };
|
|
310
366
|
break;
|
|
@@ -334,7 +390,11 @@ export async function handleWebhookEvent(deps, event) {
|
|
|
334
390
|
await deps.tenantRepo.setBillingHold(tenant, true);
|
|
335
391
|
// Debit disputed amount (allow negative). Idempotent via disputeId.
|
|
336
392
|
if (disputedCents > 0 && !(await deps.creditLedger.hasReferenceId(disputeId))) {
|
|
337
|
-
await deps.creditLedger.debit(tenant, Credit.fromCents(disputedCents), "correction",
|
|
393
|
+
await deps.creditLedger.debit(tenant, Credit.fromCents(disputedCents), "correction", {
|
|
394
|
+
description: `Stripe dispute (dispute: ${disputeId}, reason: ${dispute.reason})`,
|
|
395
|
+
referenceId: disputeId,
|
|
396
|
+
allowNegative: true,
|
|
397
|
+
});
|
|
338
398
|
}
|
|
339
399
|
// Suspend all bots (non-fatal if botBilling not provided).
|
|
340
400
|
let suspendedBots;
|
|
@@ -388,7 +448,11 @@ export async function handleWebhookEvent(deps, event) {
|
|
|
388
448
|
// Re-credit the disputed amount. Idempotent via reversal referenceId.
|
|
389
449
|
const reversalRef = `${disputeId}:reversal`;
|
|
390
450
|
if (disputedCents > 0 && !(await deps.creditLedger.hasReferenceId(reversalRef))) {
|
|
391
|
-
await deps.creditLedger.credit(tenant, Credit.fromCents(disputedCents), "correction",
|
|
451
|
+
await deps.creditLedger.credit(tenant, Credit.fromCents(disputedCents), "correction", {
|
|
452
|
+
description: `Stripe dispute won — credits restored (dispute: ${disputeId})`,
|
|
453
|
+
referenceId: reversalRef,
|
|
454
|
+
fundingSource: "stripe",
|
|
455
|
+
});
|
|
392
456
|
}
|
|
393
457
|
// Reactivate bots (non-fatal).
|
|
394
458
|
let reactivatedBots;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CREDIT_PRICE_POINTS, DrizzleWebhookSeenRepository, noOpReplayGuard, TenantCustomerRepository, } from "@wopr-network/platform-core/billing";
|
|
2
|
-
import { Credit,
|
|
2
|
+
import { Credit, DrizzleLedger } from "@wopr-network/platform-core/credits";
|
|
3
3
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { createTestDb, truncateAllTables } from "../../test/db.js";
|
|
5
5
|
import { DrizzleAffiliateRepository } from "../affiliate/drizzle-affiliate-repository.js";
|
|
@@ -24,7 +24,8 @@ describe("handleWebhookEvent (credit model)", () => {
|
|
|
24
24
|
beforeEach(async () => {
|
|
25
25
|
await truncateAllTables(pool);
|
|
26
26
|
tenantRepo = new TenantCustomerRepository(db);
|
|
27
|
-
creditLedger = new
|
|
27
|
+
creditLedger = new DrizzleLedger(db);
|
|
28
|
+
await creditLedger.seedSystemAccounts();
|
|
28
29
|
deps = { tenantRepo, creditLedger, replayGuard: noOpReplayGuard };
|
|
29
30
|
});
|
|
30
31
|
// ---------------------------------------------------------------------------
|
|
@@ -170,7 +171,7 @@ describe("handleWebhookEvent (credit model)", () => {
|
|
|
170
171
|
const txns = await creditLedger.history("tenant-123");
|
|
171
172
|
expect(txns).toHaveLength(1);
|
|
172
173
|
expect(txns[0].description).toContain("cs_test_abc");
|
|
173
|
-
expect(txns[0].
|
|
174
|
+
expect(txns[0].entryType).toBe("purchase");
|
|
174
175
|
expect(txns[0].referenceId).toBe("cs_test_abc");
|
|
175
176
|
});
|
|
176
177
|
it("grants affiliate match credits on first purchase for referred tenant (WOP-949)", async () => {
|
|
@@ -656,7 +657,7 @@ describe("handleWebhookEvent (credit model)", () => {
|
|
|
656
657
|
const txns = await creditLedger.history("tenant-renew-ref");
|
|
657
658
|
expect(txns).toHaveLength(1);
|
|
658
659
|
expect(txns[0].referenceId).toBe("in_renewal_abc");
|
|
659
|
-
expect(txns[0].
|
|
660
|
+
expect(txns[0].entryType).toBe("purchase");
|
|
660
661
|
expect(txns[0].description).toContain("in_renewal_abc");
|
|
661
662
|
});
|
|
662
663
|
it("handles missing botBilling gracefully (no reactivation, still credits)", async () => {
|
|
@@ -690,7 +691,7 @@ describe("handleWebhookEvent (credit model)", () => {
|
|
|
690
691
|
}
|
|
691
692
|
it("debits the credit ledger for the refunded amount", async () => {
|
|
692
693
|
await tenantRepo.upsert({ tenant: "tenant-ref-1", processorCustomerId: "cus_ref_abc" });
|
|
693
|
-
await creditLedger.credit("tenant-ref-1", Credit.fromCents(5000), "purchase", "seed");
|
|
694
|
+
await creditLedger.credit("tenant-ref-1", Credit.fromCents(5000), "purchase", { description: "seed" });
|
|
694
695
|
const result = await handleWebhookEvent(deps, makeChargeRefundedEvent());
|
|
695
696
|
expect(result.handled).toBe(true);
|
|
696
697
|
expect(result.event_type).toBe("charge.refunded");
|
|
@@ -707,7 +708,7 @@ describe("handleWebhookEvent (credit model)", () => {
|
|
|
707
708
|
});
|
|
708
709
|
it("is idempotent — skips duplicate refund for same charge ID", async () => {
|
|
709
710
|
await tenantRepo.upsert({ tenant: "tenant-ref-idem", processorCustomerId: "cus_ref_idem" });
|
|
710
|
-
await creditLedger.credit("tenant-ref-idem", Credit.fromCents(5000), "purchase", "seed");
|
|
711
|
+
await creditLedger.credit("tenant-ref-idem", Credit.fromCents(5000), "purchase", { description: "seed" });
|
|
711
712
|
const event = makeChargeRefundedEvent({ customer: "cus_ref_idem" });
|
|
712
713
|
const first = await handleWebhookEvent(deps, event);
|
|
713
714
|
expect(first.debitedCents).toBe(2500);
|
|
@@ -727,19 +728,19 @@ describe("handleWebhookEvent (credit model)", () => {
|
|
|
727
728
|
});
|
|
728
729
|
it("handles customer object instead of string", async () => {
|
|
729
730
|
await tenantRepo.upsert({ tenant: "tenant-ref-obj", processorCustomerId: "cus_ref_obj" });
|
|
730
|
-
await creditLedger.credit("tenant-ref-obj", Credit.fromCents(3000), "purchase", "seed");
|
|
731
|
+
await creditLedger.credit("tenant-ref-obj", Credit.fromCents(3000), "purchase", { description: "seed" });
|
|
731
732
|
const result = await handleWebhookEvent(deps, makeChargeRefundedEvent({ customer: { id: "cus_ref_obj" }, amount_refunded: 1500 }));
|
|
732
733
|
expect(result.handled).toBe(true);
|
|
733
734
|
expect(result.debitedCents).toBe(1500);
|
|
734
735
|
});
|
|
735
736
|
it("records event ID as referenceId in the ledger transaction", async () => {
|
|
736
737
|
await tenantRepo.upsert({ tenant: "tenant-ref-txn", processorCustomerId: "cus_ref_txn" });
|
|
737
|
-
await creditLedger.credit("tenant-ref-txn", Credit.fromCents(5000), "purchase", "seed");
|
|
738
|
+
await creditLedger.credit("tenant-ref-txn", Credit.fromCents(5000), "purchase", { description: "seed" });
|
|
738
739
|
await handleWebhookEvent(deps, makeChargeRefundedEvent({ customer: "cus_ref_txn", id: "ch_ref_txn_123" }));
|
|
739
740
|
const txns = await creditLedger.history("tenant-ref-txn", { type: "refund" });
|
|
740
741
|
expect(txns).toHaveLength(1);
|
|
741
742
|
expect(txns[0].referenceId).toBe("evt_charge_ref_1");
|
|
742
|
-
expect(txns[0].
|
|
743
|
+
expect(txns[0].entryType).toBe("refund");
|
|
743
744
|
expect(txns[0].description).toContain("ch_ref_txn_123");
|
|
744
745
|
});
|
|
745
746
|
});
|
|
@@ -810,7 +811,11 @@ describe("handleWebhookEvent (credit model)", () => {
|
|
|
810
811
|
});
|
|
811
812
|
it("skips credit when referenceId already exists (inline grant ran first)", async () => {
|
|
812
813
|
// Simulate inline grant already happened
|
|
813
|
-
await creditLedger.credit("t1", Credit.fromCents(500), "purchase",
|
|
814
|
+
await creditLedger.credit("t1", Credit.fromCents(500), "purchase", {
|
|
815
|
+
description: "Auto-topup",
|
|
816
|
+
referenceId: "pi_already_granted",
|
|
817
|
+
fundingSource: "stripe",
|
|
818
|
+
});
|
|
814
819
|
const event = {
|
|
815
820
|
id: "evt_pi_success_2",
|
|
816
821
|
type: "payment_intent.succeeded",
|
|
@@ -891,7 +896,7 @@ describe("handleWebhookEvent (credit model)", () => {
|
|
|
891
896
|
}
|
|
892
897
|
it("freezes tenant credits and suspends bots on dispute", async () => {
|
|
893
898
|
await tenantRepo.upsert({ tenant: "tenant-dispute-1", processorCustomerId: "cus_dispute_abc" });
|
|
894
|
-
await creditLedger.credit("tenant-dispute-1", Credit.fromCents(5000), "purchase", "seed");
|
|
899
|
+
await creditLedger.credit("tenant-dispute-1", Credit.fromCents(5000), "purchase", { description: "seed" });
|
|
895
900
|
const botBilling = {
|
|
896
901
|
suspendAllForTenant: vi.fn(async () => ["bot-d1"]),
|
|
897
902
|
};
|
|
@@ -912,7 +917,7 @@ describe("handleWebhookEvent (credit model)", () => {
|
|
|
912
917
|
});
|
|
913
918
|
it("is idempotent — skips duplicate debit for same dispute ID", async () => {
|
|
914
919
|
await tenantRepo.upsert({ tenant: "tenant-dispute-idem", processorCustomerId: "cus_dispute_idem" });
|
|
915
|
-
await creditLedger.credit("tenant-dispute-idem", Credit.fromCents(5000), "purchase", "seed");
|
|
920
|
+
await creditLedger.credit("tenant-dispute-idem", Credit.fromCents(5000), "purchase", { description: "seed" });
|
|
916
921
|
const event = makeDisputeCreatedEvent("cus_dispute_idem");
|
|
917
922
|
await handleWebhookEvent(deps, event);
|
|
918
923
|
await handleWebhookEvent(deps, event);
|
|
@@ -921,7 +926,7 @@ describe("handleWebhookEvent (credit model)", () => {
|
|
|
921
926
|
});
|
|
922
927
|
it("sends admin notification when notificationService is available", async () => {
|
|
923
928
|
await tenantRepo.upsert({ tenant: "tenant-dispute-notify", processorCustomerId: "cus_dispute_notify" });
|
|
924
|
-
await creditLedger.credit("tenant-dispute-notify", Credit.fromCents(5000), "purchase", "seed");
|
|
929
|
+
await creditLedger.credit("tenant-dispute-notify", Credit.fromCents(5000), "purchase", { description: "seed" });
|
|
925
930
|
const notifyFn = vi.fn();
|
|
926
931
|
const notificationService = {
|
|
927
932
|
notifyDisputeCreated: notifyFn,
|
|
@@ -957,14 +962,14 @@ describe("handleWebhookEvent (credit model)", () => {
|
|
|
957
962
|
});
|
|
958
963
|
it("handles customer object (expanded) inside charge", async () => {
|
|
959
964
|
await tenantRepo.upsert({ tenant: "tenant-dispute-obj", processorCustomerId: "cus_dispute_obj" });
|
|
960
|
-
await creditLedger.credit("tenant-dispute-obj", Credit.fromCents(3000), "purchase", "seed");
|
|
965
|
+
await creditLedger.credit("tenant-dispute-obj", Credit.fromCents(3000), "purchase", { description: "seed" });
|
|
961
966
|
const result = await handleWebhookEvent(deps, makeDisputeCreatedEvent({ id: "cus_dispute_obj" }));
|
|
962
967
|
expect(result.handled).toBe(true);
|
|
963
968
|
expect(result.tenant).toBe("tenant-dispute-obj");
|
|
964
969
|
});
|
|
965
970
|
it("works without botBilling (no suspension, still handled)", async () => {
|
|
966
971
|
await tenantRepo.upsert({ tenant: "tenant-dispute-no-bb", processorCustomerId: "cus_dispute_no_bb" });
|
|
967
|
-
await creditLedger.credit("tenant-dispute-no-bb", Credit.fromCents(5000), "purchase", "seed");
|
|
972
|
+
await creditLedger.credit("tenant-dispute-no-bb", Credit.fromCents(5000), "purchase", { description: "seed" });
|
|
968
973
|
const result = await handleWebhookEvent(deps, makeDisputeCreatedEvent("cus_dispute_no_bb"));
|
|
969
974
|
expect(result.handled).toBe(true);
|
|
970
975
|
expect(result.suspendedBots).toBeUndefined();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ILedger } from "@wopr-network/platform-core/credits";
|
|
2
2
|
import type { ISessionUsageRepository } from "../inference/session-usage-repository.js";
|
|
3
3
|
import type { OnboardingConfig } from "./config.js";
|
|
4
4
|
import type { IDaemonManager } from "./daemon-manager.js";
|
|
@@ -16,7 +16,7 @@ export declare class OnboardingService {
|
|
|
16
16
|
private readonly resolveTenantId?;
|
|
17
17
|
private static readonly DEFAULT_PROMPT;
|
|
18
18
|
private static readonly DEFAULT_SCRIPT_REPO;
|
|
19
|
-
constructor(repo: IOnboardingSessionRepository, client: IWoprClient, config: OnboardingConfig, daemon: IDaemonManager, usageRepo?: ISessionUsageRepository | undefined, scriptRepo?: IOnboardingScriptRepository, creditLedger?:
|
|
19
|
+
constructor(repo: IOnboardingSessionRepository, client: IWoprClient, config: OnboardingConfig, daemon: IDaemonManager, usageRepo?: ISessionUsageRepository | undefined, scriptRepo?: IOnboardingScriptRepository, creditLedger?: ILedger | undefined, resolveTenantId?: ((userId: string) => Promise<string | null>) | undefined);
|
|
20
20
|
createSession(opts: {
|
|
21
21
|
userId?: string;
|
|
22
22
|
anonymousId?: string;
|
|
@@ -120,8 +120,12 @@ export class OnboardingService {
|
|
|
120
120
|
if (costCents > 0) {
|
|
121
121
|
const tenantId = await this.resolveTenantId(session.userId);
|
|
122
122
|
if (tenantId) {
|
|
123
|
-
await this.creditLedger.debit(tenantId, Credit.fromCents(costCents), "onboarding_llm",
|
|
124
|
-
|
|
123
|
+
await this.creditLedger.debit(tenantId, Credit.fromCents(costCents), "onboarding_llm", {
|
|
124
|
+
description: `Onboarding session ${sessionId}`,
|
|
125
|
+
referenceId: `onboarding-${sessionId}-${Date.now()}`,
|
|
126
|
+
allowNegative: true,
|
|
127
|
+
attributedUserId: session.userId,
|
|
128
|
+
});
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
131
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
CREATE TABLE "gateway_service_keys" (
|
|
2
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
3
|
+
"key_hash" text NOT NULL,
|
|
4
|
+
"tenant_id" text NOT NULL,
|
|
5
|
+
"instance_id" text NOT NULL,
|
|
6
|
+
"created_at" bigint NOT NULL,
|
|
7
|
+
"revoked_at" bigint
|
|
8
|
+
);
|
|
9
|
+
--> statement-breakpoint
|
|
10
|
+
CREATE UNIQUE INDEX "idx_gateway_service_keys_hash" ON "gateway_service_keys" USING btree ("key_hash");
|
|
11
|
+
--> statement-breakpoint
|
|
12
|
+
CREATE INDEX "idx_gateway_service_keys_tenant" ON "gateway_service_keys" USING btree ("tenant_id");
|
|
13
|
+
--> statement-breakpoint
|
|
14
|
+
CREATE INDEX "idx_gateway_service_keys_instance" ON "gateway_service_keys" USING btree ("instance_id");
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
-- Double-entry ledger: accounts, journal_entries, journal_lines, account_balances
|
|
2
|
+
-- Replaces single-entry credit_transactions + credit_balances
|
|
3
|
+
|
|
4
|
+
DO $$ BEGIN
|
|
5
|
+
CREATE TYPE "public"."account_type" AS ENUM('asset','liability','equity','revenue','expense');
|
|
6
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
7
|
+
END $$;--> statement-breakpoint
|
|
8
|
+
DO $$ BEGIN
|
|
9
|
+
CREATE TYPE "public"."entry_side" AS ENUM('debit','credit');
|
|
10
|
+
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
11
|
+
END $$;--> statement-breakpoint
|
|
12
|
+
CREATE TABLE "accounts" (
|
|
13
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
14
|
+
"code" text NOT NULL,
|
|
15
|
+
"name" text NOT NULL,
|
|
16
|
+
"type" "account_type" NOT NULL,
|
|
17
|
+
"normal_side" "entry_side" NOT NULL,
|
|
18
|
+
"tenant_id" text,
|
|
19
|
+
"created_at" text DEFAULT (now()) NOT NULL
|
|
20
|
+
);--> statement-breakpoint
|
|
21
|
+
CREATE TABLE "journal_entries" (
|
|
22
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
23
|
+
"posted_at" text DEFAULT (now()) NOT NULL,
|
|
24
|
+
"entry_type" text NOT NULL,
|
|
25
|
+
"description" text,
|
|
26
|
+
"reference_id" text,
|
|
27
|
+
"tenant_id" text NOT NULL,
|
|
28
|
+
"metadata" jsonb,
|
|
29
|
+
"created_by" text
|
|
30
|
+
);--> statement-breakpoint
|
|
31
|
+
CREATE TABLE "journal_lines" (
|
|
32
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
33
|
+
"journal_entry_id" text NOT NULL REFERENCES "journal_entries"("id"),
|
|
34
|
+
"account_id" text NOT NULL REFERENCES "accounts"("id"),
|
|
35
|
+
"amount" bigint NOT NULL,
|
|
36
|
+
"side" "entry_side" NOT NULL
|
|
37
|
+
);--> statement-breakpoint
|
|
38
|
+
CREATE TABLE "account_balances" (
|
|
39
|
+
"account_id" text PRIMARY KEY NOT NULL REFERENCES "accounts"("id"),
|
|
40
|
+
"balance" bigint DEFAULT 0 NOT NULL,
|
|
41
|
+
"last_updated" text DEFAULT (now()) NOT NULL
|
|
42
|
+
);--> statement-breakpoint
|
|
43
|
+
CREATE UNIQUE INDEX "idx_accounts_code" ON "accounts" USING btree ("code");--> statement-breakpoint
|
|
44
|
+
CREATE INDEX "idx_accounts_tenant" ON "accounts" USING btree ("tenant_id") WHERE "tenant_id" IS NOT NULL;--> statement-breakpoint
|
|
45
|
+
CREATE INDEX "idx_accounts_type" ON "accounts" USING btree ("type");--> statement-breakpoint
|
|
46
|
+
CREATE UNIQUE INDEX "idx_je_reference" ON "journal_entries" USING btree ("reference_id") WHERE "reference_id" IS NOT NULL;--> statement-breakpoint
|
|
47
|
+
CREATE INDEX "idx_je_tenant" ON "journal_entries" USING btree ("tenant_id");--> statement-breakpoint
|
|
48
|
+
CREATE INDEX "idx_je_type" ON "journal_entries" USING btree ("entry_type");--> statement-breakpoint
|
|
49
|
+
CREATE INDEX "idx_je_posted" ON "journal_entries" USING btree ("posted_at");--> statement-breakpoint
|
|
50
|
+
CREATE INDEX "idx_je_tenant_posted" ON "journal_entries" USING btree ("tenant_id","posted_at");--> statement-breakpoint
|
|
51
|
+
CREATE INDEX "idx_jl_entry" ON "journal_lines" USING btree ("journal_entry_id");--> statement-breakpoint
|
|
52
|
+
CREATE INDEX "idx_jl_account" ON "journal_lines" USING btree ("account_id");--> statement-breakpoint
|
|
53
|
+
CREATE INDEX "idx_jl_account_side" ON "journal_lines" USING btree ("account_id","side");--> statement-breakpoint
|
|
54
|
+
|
|
55
|
+
-- Seed system accounts
|
|
56
|
+
INSERT INTO "accounts" ("id", "code", "name", "type", "normal_side") VALUES
|
|
57
|
+
(gen_random_uuid(), '1000', 'Cash', 'asset', 'debit'),
|
|
58
|
+
(gen_random_uuid(), '1100', 'Stripe Receivable', 'asset', 'debit'),
|
|
59
|
+
(gen_random_uuid(), '3000', 'Retained Earnings', 'equity', 'credit'),
|
|
60
|
+
(gen_random_uuid(), '4000', 'Revenue: Bot Runtime', 'revenue', 'credit'),
|
|
61
|
+
(gen_random_uuid(), '4010', 'Revenue: Adapter Usage', 'revenue', 'credit'),
|
|
62
|
+
(gen_random_uuid(), '4020', 'Revenue: Addon', 'revenue', 'credit'),
|
|
63
|
+
(gen_random_uuid(), '4030', 'Revenue: Storage Upgrade', 'revenue', 'credit'),
|
|
64
|
+
(gen_random_uuid(), '4040', 'Revenue: Resource Upgrade', 'revenue', 'credit'),
|
|
65
|
+
(gen_random_uuid(), '4050', 'Revenue: Onboarding LLM', 'revenue', 'credit'),
|
|
66
|
+
(gen_random_uuid(), '4060', 'Revenue: Expired Credits', 'revenue', 'credit'),
|
|
67
|
+
(gen_random_uuid(), '5000', 'Expense: Signup Grant', 'expense', 'debit'),
|
|
68
|
+
(gen_random_uuid(), '5010', 'Expense: Admin Grant', 'expense', 'debit'),
|
|
69
|
+
(gen_random_uuid(), '5020', 'Expense: Promo', 'expense', 'debit'),
|
|
70
|
+
(gen_random_uuid(), '5030', 'Expense: Referral', 'expense', 'debit'),
|
|
71
|
+
(gen_random_uuid(), '5040', 'Expense: Affiliate', 'expense', 'debit'),
|
|
72
|
+
(gen_random_uuid(), '5050', 'Expense: Bounty', 'expense', 'debit'),
|
|
73
|
+
(gen_random_uuid(), '5060', 'Expense: Dividend', 'expense', 'debit'),
|
|
74
|
+
(gen_random_uuid(), '5070', 'Expense: Correction', 'expense', 'debit');--> statement-breakpoint
|
|
75
|
+
|
|
76
|
+
-- Initialize balance rows for all seeded system accounts
|
|
77
|
+
INSERT INTO "account_balances" ("account_id", "balance")
|
|
78
|
+
SELECT "id", 0 FROM "accounts";--> statement-breakpoint
|
|
79
|
+
|
|
80
|
+
-- Drop old single-entry tables
|
|
81
|
+
DROP TABLE IF EXISTS "credit_transactions" CASCADE;--> statement-breakpoint
|
|
82
|
+
DROP TABLE IF EXISTS "credit_balances" CASCADE;
|
|
@@ -15,6 +15,20 @@
|
|
|
15
15
|
"when": 1773279600000,
|
|
16
16
|
"tag": "0001_infrastructure_extraction",
|
|
17
17
|
"breakpoints": true
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"idx": 2,
|
|
21
|
+
"version": "7",
|
|
22
|
+
"when": 1741795200000,
|
|
23
|
+
"tag": "0002_gateway_service_keys",
|
|
24
|
+
"breakpoints": true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"idx": 3,
|
|
28
|
+
"version": "7",
|
|
29
|
+
"when": 1741881600000,
|
|
30
|
+
"tag": "0003_double_entry_ledger",
|
|
31
|
+
"breakpoints": true
|
|
18
32
|
}
|
|
19
33
|
]
|
|
20
34
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { AuthEnv } from "../../auth/index.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { ILedger } from "../../credits/index.js";
|
|
5
5
|
import { Credit, InsufficientBalanceError } from "../../credits/index.js";
|
|
6
6
|
import type { AdminAuditLogger } from "./admin-audit-helper.js";
|
|
7
7
|
import { safeAuditLog } from "./admin-audit-helper.js";
|
|
@@ -31,7 +31,7 @@ function parseIntParam(value: string | undefined): number | undefined {
|
|
|
31
31
|
* Pass a ledger directly or a factory for lazy init.
|
|
32
32
|
*/
|
|
33
33
|
export function createAdminCreditApiRoutes(
|
|
34
|
-
ledgerOrFactory:
|
|
34
|
+
ledgerOrFactory: ILedger | (() => ILedger),
|
|
35
35
|
auditLogger?: () => AdminAuditLogger,
|
|
36
36
|
): Hono<AuthEnv> {
|
|
37
37
|
const ledgerFactory = typeof ledgerOrFactory === "function" ? ledgerOrFactory : () => ledgerOrFactory;
|
|
@@ -66,15 +66,10 @@ export function createAdminCreditApiRoutes(
|
|
|
66
66
|
const adminUser = user?.id ?? "unknown";
|
|
67
67
|
let result: Awaited<ReturnType<typeof ledger.credit>>;
|
|
68
68
|
try {
|
|
69
|
-
result = await ledger.credit(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
reason,
|
|
74
|
-
undefined,
|
|
75
|
-
undefined,
|
|
76
|
-
adminUser,
|
|
77
|
-
);
|
|
69
|
+
result = await ledger.credit(tenant, Credit.fromCents(amountCents), "admin_grant", {
|
|
70
|
+
description: reason,
|
|
71
|
+
createdBy: adminUser,
|
|
72
|
+
});
|
|
78
73
|
} catch (err) {
|
|
79
74
|
safeAuditLog(auditLogger, {
|
|
80
75
|
adminUser,
|
|
@@ -129,7 +124,7 @@ export function createAdminCreditApiRoutes(
|
|
|
129
124
|
const adminUser = user?.id ?? "unknown";
|
|
130
125
|
let result: Awaited<ReturnType<typeof ledger.credit>>;
|
|
131
126
|
try {
|
|
132
|
-
result = await ledger.credit(tenant, Credit.fromCents(amountCents), "admin_grant", reason);
|
|
127
|
+
result = await ledger.credit(tenant, Credit.fromCents(amountCents), "admin_grant", { description: reason });
|
|
133
128
|
} catch (err) {
|
|
134
129
|
safeAuditLog(auditLogger, {
|
|
135
130
|
adminUser,
|
|
@@ -188,9 +183,11 @@ export function createAdminCreditApiRoutes(
|
|
|
188
183
|
let result: Awaited<ReturnType<typeof ledger.credit>>;
|
|
189
184
|
try {
|
|
190
185
|
if (amountCents >= 0) {
|
|
191
|
-
result = await ledger.credit(tenant, Credit.fromCents(amountCents), "
|
|
186
|
+
result = await ledger.credit(tenant, Credit.fromCents(amountCents), "correction", { description: reason });
|
|
192
187
|
} else {
|
|
193
|
-
result = await ledger.debit(tenant, Credit.fromCents(Math.abs(amountCents)), "correction",
|
|
188
|
+
result = await ledger.debit(tenant, Credit.fromCents(Math.abs(amountCents)), "correction", {
|
|
189
|
+
description: reason,
|
|
190
|
+
});
|
|
194
191
|
}
|
|
195
192
|
} catch (err) {
|
|
196
193
|
safeAuditLog(auditLogger, {
|
package/src/api/routes/quota.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ILedger } from "../../credits/ledger.js";
|
|
3
3
|
import { checkInstanceQuota, DEFAULT_INSTANCE_LIMITS } from "../../monetization/quotas/quota-check.js";
|
|
4
4
|
import { buildResourceLimits, DEFAULT_RESOURCE_CONFIG } from "../../monetization/quotas/resource-limits.js";
|
|
5
5
|
|
|
@@ -8,7 +8,7 @@ import { buildResourceLimits, DEFAULT_RESOURCE_CONFIG } from "../../monetization
|
|
|
8
8
|
*
|
|
9
9
|
* @param ledgerFactory - Factory returning the credit ledger
|
|
10
10
|
*/
|
|
11
|
-
export function createQuotaRoutes(ledgerFactory: () =>
|
|
11
|
+
export function createQuotaRoutes(ledgerFactory: () => ILedger): Hono {
|
|
12
12
|
const routes = new Hono();
|
|
13
13
|
|
|
14
14
|
/**
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import type { Pool } from "pg";
|
|
3
3
|
import { logger } from "../../config/logger.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { ILedger } from "../../credits/index.js";
|
|
5
5
|
import { grantSignupCredits } from "../../credits/index.js";
|
|
6
6
|
import { getEmailClient } from "../../email/client.js";
|
|
7
7
|
import { verifyToken, welcomeTemplate } from "../../email/index.js";
|
|
8
8
|
|
|
9
9
|
export interface VerifyEmailRouteDeps {
|
|
10
10
|
pool: Pool;
|
|
11
|
-
creditLedger:
|
|
11
|
+
creditLedger: ILedger;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface VerifyEmailRouteConfig {
|
|
@@ -32,7 +32,7 @@ export function createVerifyEmailRoutes(deps: VerifyEmailRouteDeps, config?: Ver
|
|
|
32
32
|
*/
|
|
33
33
|
export function createVerifyEmailRoutesLazy(
|
|
34
34
|
poolFactory: () => Pool,
|
|
35
|
-
creditLedgerFactory: () =>
|
|
35
|
+
creditLedgerFactory: () => ILedger,
|
|
36
36
|
config?: VerifyEmailRouteConfig,
|
|
37
37
|
): Hono {
|
|
38
38
|
return buildRoutes(poolFactory, creditLedgerFactory, config);
|
|
@@ -40,7 +40,7 @@ export function createVerifyEmailRoutesLazy(
|
|
|
40
40
|
|
|
41
41
|
function buildRoutes(
|
|
42
42
|
poolFactory: () => Pool,
|
|
43
|
-
creditLedgerFactory: () =>
|
|
43
|
+
creditLedgerFactory: () => ILedger,
|
|
44
44
|
config?: VerifyEmailRouteConfig,
|
|
45
45
|
): Hono {
|
|
46
46
|
const uiOrigin = config?.uiOrigin ?? process.env.UI_ORIGIN ?? "http://localhost:3001";
|