@@ -0,0 +1,228 @@
|
||||
// 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()
|
||||
Reference in New Issue
Block a user