53 lines
2.2 KiB
TypeScript
53 lines
2.2 KiB
TypeScript
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<void> {
|
|
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<string, bigint>()
|
|
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<void> {
|
|
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}`
|