import { AppError, badRequest } from "@conusai-kit/core" import { LEDGER_ERRORS, type RecordTerms } from "../contract/index.js" import type { LedgerReads, LedgerService } from "./ports.js" /** Optional preview only. Never run mutable account reads ahead of service replay. */ export async function validateBalanced(service: LedgerReads, terms: RecordTerms): Promise { const accounts = new Map( await Promise.all( [...new Set(terms.entries.map((entry) => entry.accountId))].map( async (id) => [id, await service.account(id)] as const, ), ), ) const totals = new Map() for (const entry of terms.entries) { const account = accounts.get(entry.accountId) if (account?.status !== "active") throw badRequest("Choose active ledger accounts.") totals.set( account.currency, (totals.get(account.currency) ?? 0n) + BigInt(entry.amountMinor) * (entry.direction === "debit" ? 1n : -1n), ) } if ([...totals.values()].some((amount) => amount !== 0n)) throw badRequest("Balance debits and credits separately in every currency.") } export async function recordBalanced(service: LedgerService, terms: RecordTerms, requestId: string) { await assertProjection(service) // Service replay precedes account status, locking, funds, currency and overflow checks. return service.record(terms, operationKey("record", requestId)) } export async function assertProjection(service: LedgerReads): Promise { const health = await service.verify() if (health.drift.length) throw new AppError({ type: `/problems/${LEDGER_ERRORS.drift}`, code: LEDGER_ERRORS.drift, status: 409, title: "Ledger posting fenced", detail: "Ledger projections require operator recovery before posting.", }) } export async function transition( service: LedgerService, action: "post" | "void" | "reverse", id: string, revision: string, requestId: string, ) { if (action !== "void") await assertProjection(service) return service.transition(action, id, revision, operationKey(action, requestId)) } export const operationKey = (action: string, requestId: string): string => `app-ledger:${action}:${requestId}`