@@ -0,0 +1,52 @@
|
||||
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}`
|
||||
@@ -0,0 +1,43 @@
|
||||
import { token } from "@conusai-kit/core"
|
||||
import type {
|
||||
AccountView,
|
||||
BalanceView,
|
||||
EntryView,
|
||||
RecordTerms,
|
||||
TransactionView,
|
||||
VerificationView,
|
||||
} from "../contract/index.js"
|
||||
export interface Page<T> {
|
||||
rows: T[]
|
||||
after?: string
|
||||
}
|
||||
export interface LedgerReads {
|
||||
accounts(after?: string): Promise<Page<AccountView>>
|
||||
account(id: string): Promise<AccountView>
|
||||
balances(accountId?: string, after?: string): Promise<Page<BalanceView>>
|
||||
entries(accountId: string, after?: string): Promise<Page<EntryView>>
|
||||
transactions(after?: string): Promise<Page<TransactionView>>
|
||||
transaction(id: string): Promise<TransactionView>
|
||||
verify(): Promise<VerificationView>
|
||||
}
|
||||
export interface LedgerService extends LedgerReads {
|
||||
createAccount(input: Omit<AccountView, "accountId" | "revision" | "status">, key: string): Promise<AccountView>
|
||||
updateAccount(
|
||||
id: string,
|
||||
input: Pick<AccountView, "name" | "status" | "allowNegative">,
|
||||
revision: string,
|
||||
key: string,
|
||||
): Promise<AccountView>
|
||||
record(terms: RecordTerms, key: string): Promise<TransactionView>
|
||||
transition(action: "post" | "void" | "reverse", id: string, revision: string, key: string): Promise<TransactionView>
|
||||
}
|
||||
export const readsOf = (service: LedgerReads): LedgerReads => ({
|
||||
accounts: (after) => service.accounts(after),
|
||||
account: (id) => service.account(id),
|
||||
balances: (id, after) => service.balances(id, after),
|
||||
entries: (id, after) => service.entries(id, after),
|
||||
transactions: (after) => service.transactions(after),
|
||||
transaction: (id) => service.transaction(id),
|
||||
verify: () => service.verify(),
|
||||
})
|
||||
export const LEDGER_SERVICE = token<LedgerService>("ledger.service", { views: { read: readsOf, write: readsOf } })
|
||||
Reference in New Issue
Block a user