229 lines
7.1 KiB
TypeScript
229 lines
7.1 KiB
TypeScript
// The public API of the ledger module: the zod schemas, procedure contracts, permissions and events
|
|
// another module or a browser may import. Nothing here reaches core/, adapters/ or a driver.
|
|
|
|
import { procedure } from "@conusai-kit/core"
|
|
import { z } from "zod"
|
|
|
|
/**
|
|
* The module's own error: its verification found projection drift, so it fences posting. A refusal of the
|
|
* Ledger service is the kit's (`business_<refusal>`, `@conusai-kit/business`), not a code of this module.
|
|
*/
|
|
export const LEDGER_ERRORS = {
|
|
drift: "ledger_projection_drift",
|
|
} as const
|
|
|
|
const uuid = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
|
|
export const accountId = z.string().regex(new RegExp(`^acc_${uuid}$`))
|
|
export const transactionId = z.string().regex(new RegExp(`^txn_${uuid}$`))
|
|
const entryId = z.string().regex(new RegExp(`^ent_${uuid}$`))
|
|
const maximum = 9223372036854775807n
|
|
/** Integer decimal strings cross JSON; conversion to BigInt happens only in the adapter. */
|
|
export const positiveMinor = z
|
|
.string()
|
|
.regex(/^[1-9][0-9]{0,18}$/)
|
|
.refine((value) => {
|
|
try {
|
|
return BigInt(value) <= maximum
|
|
} catch {
|
|
return false
|
|
}
|
|
})
|
|
export const revision = positiveMinor
|
|
const signedMinor = z
|
|
.string()
|
|
.regex(/^(?:0|-?[1-9][0-9]{0,18})$/)
|
|
.refine((value) => {
|
|
try {
|
|
const amount = BigInt(value)
|
|
return amount >= -9223372036854775808n && amount <= maximum
|
|
} catch {
|
|
return false
|
|
}
|
|
})
|
|
const currency = z.string().regex(/^[A-Z]{3}$/)
|
|
const text = z
|
|
.string()
|
|
.trim()
|
|
.min(1)
|
|
.max(200)
|
|
.refine((value) => ![...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127))
|
|
export const direction = z.enum(["debit", "credit"])
|
|
export const accountType = z.enum(["asset", "liability", "equity", "income", "expense"])
|
|
export const accountView = z.object({
|
|
accountId,
|
|
code: z.string(),
|
|
name: z.string(),
|
|
currency,
|
|
type: accountType,
|
|
normalBalance: direction,
|
|
allowNegative: z.boolean(),
|
|
status: z.enum(["active", "archived"]),
|
|
revision,
|
|
})
|
|
export type AccountView = z.output<typeof accountView>
|
|
export const balanceView = z.object({
|
|
accountId,
|
|
currency,
|
|
posted: signedMinor,
|
|
pending: signedMinor,
|
|
available: signedMinor,
|
|
})
|
|
export type BalanceView = z.output<typeof balanceView>
|
|
export const entryView = z.object({
|
|
entryId,
|
|
transactionId,
|
|
accountId,
|
|
currency,
|
|
direction,
|
|
amountMinor: positiveMinor,
|
|
})
|
|
export type EntryView = z.output<typeof entryView>
|
|
export const transactionView = z.object({
|
|
transactionId,
|
|
revision,
|
|
effectiveAt: z.iso.datetime({ offset: true }),
|
|
description: z.string(),
|
|
reference: z.string().nullable(),
|
|
reverses: transactionId.nullable(),
|
|
status: z.enum(["pending", "posted", "voided"]),
|
|
entries: z.array(entryView).max(200),
|
|
})
|
|
export type TransactionView = z.output<typeof transactionView>
|
|
export const verificationView = z.object({ checkedAccounts: z.number().int().nonnegative(), drift: z.array(accountId) })
|
|
export type VerificationView = z.output<typeof verificationView>
|
|
export const recordTerms = z
|
|
.object({
|
|
effectiveAt: z.iso.datetime({ offset: true }),
|
|
description: text,
|
|
reference: text.optional(),
|
|
status: z.enum(["pending", "posted"]),
|
|
entries: z
|
|
.array(z.object({ accountId, direction, amountMinor: positiveMinor }).strict())
|
|
.min(2)
|
|
.max(200),
|
|
})
|
|
.strict()
|
|
export type RecordTerms = z.output<typeof recordTerms>
|
|
const org = { org: z.string() }
|
|
const cursor = z.string().min(1).max(4096).optional()
|
|
const requestId = z.string().uuid()
|
|
export const accounts = procedure("ledger.accounts.list")
|
|
.kind("read")
|
|
.scope("org")
|
|
.permission("ledger.accounts.read")
|
|
.input(z.object({ ...org, after: cursor }))
|
|
.output(z.object({ rows: z.array(accountView), after: cursor }))
|
|
.contract()
|
|
export const account = procedure("ledger.accounts.get")
|
|
.kind("read")
|
|
.scope("org")
|
|
.permission("ledger.accounts.read")
|
|
.input(z.object({ ...org, accountId }))
|
|
.output(accountView)
|
|
.contract()
|
|
export const balances = procedure("ledger.balances.list")
|
|
.kind("read")
|
|
.scope("org")
|
|
.permission("ledger.balances.read")
|
|
.input(z.object({ ...org, accountId: accountId.optional(), after: cursor }))
|
|
.output(z.object({ rows: z.array(balanceView), after: cursor }))
|
|
.contract()
|
|
export const entries = procedure("ledger.entries.list")
|
|
.kind("read")
|
|
.scope("org")
|
|
.permission("ledger.transactions.read")
|
|
.input(z.object({ ...org, accountId, after: cursor }))
|
|
.output(z.object({ rows: z.array(entryView), after: cursor }))
|
|
.contract()
|
|
export const transactions = procedure("ledger.transactions.list")
|
|
.kind("read")
|
|
.scope("org")
|
|
.permission("ledger.transactions.read")
|
|
.input(z.object({ ...org, after: cursor }))
|
|
.output(z.object({ rows: z.array(transactionView), after: cursor }))
|
|
.contract()
|
|
export const transaction = procedure("ledger.transactions.get")
|
|
.kind("read")
|
|
.scope("org")
|
|
.permission("ledger.transactions.read")
|
|
.input(z.object({ ...org, transactionId }))
|
|
.output(transactionView)
|
|
.contract()
|
|
export const verify = procedure("ledger.verify")
|
|
.kind("read")
|
|
.scope("org")
|
|
.permission("ledger.balances.read")
|
|
.input(z.object(org))
|
|
.output(verificationView)
|
|
.contract()
|
|
export const createAccount = procedure("ledger.accounts.create")
|
|
.kind("effect")
|
|
.scope("org")
|
|
.permission("ledger.accounts.manage")
|
|
.input(
|
|
z
|
|
.object({
|
|
...org,
|
|
requestId,
|
|
code: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/),
|
|
name: text,
|
|
currency,
|
|
type: accountType,
|
|
normalBalance: direction,
|
|
allowNegative: z.boolean(),
|
|
})
|
|
.strict(),
|
|
)
|
|
.output(accountView)
|
|
.contract()
|
|
export const updateAccount = procedure("ledger.accounts.update")
|
|
.kind("effect")
|
|
.scope("org")
|
|
.permission("ledger.accounts.manage")
|
|
.input(
|
|
z
|
|
.object({
|
|
...org,
|
|
accountId,
|
|
requestId,
|
|
expectedVersion: revision,
|
|
name: text,
|
|
status: z.enum(["active", "archived"]),
|
|
allowNegative: z.boolean(),
|
|
})
|
|
.strict(),
|
|
)
|
|
.output(accountView)
|
|
.contract()
|
|
export const record = procedure("ledger.transactions.record")
|
|
.kind("effect")
|
|
.scope("org")
|
|
.permission("ledger.transactions.write")
|
|
.input(z.object({ ...org, requestId, terms: recordTerms, confirmed: z.literal(true) }).strict())
|
|
.output(transactionView)
|
|
.contract()
|
|
const transitionInput = z
|
|
.object({ ...org, transactionId, expectedVersion: revision, requestId, confirmed: z.literal(true) })
|
|
.strict()
|
|
export const post = procedure("ledger.transactions.post")
|
|
.kind("effect")
|
|
.scope("org")
|
|
.permission("ledger.transactions.write")
|
|
.input(transitionInput)
|
|
.output(transactionView)
|
|
.contract()
|
|
export const voidTransaction = procedure("ledger.transactions.void")
|
|
.kind("effect")
|
|
.scope("org")
|
|
.permission("ledger.transactions.write")
|
|
.input(transitionInput)
|
|
.output(transactionView)
|
|
.contract()
|
|
export const reverse = procedure("ledger.transactions.reverse")
|
|
.kind("effect")
|
|
.scope("org")
|
|
.permission("ledger.transactions.reverse")
|
|
.input(transitionInput)
|
|
.output(transactionView)
|
|
.contract()
|