Files
suaveplan/packages/api/api-plugin-banking/src/schemas/schemas.md
2026-05-27 23:29:53 +01:00

4.4 KiB

Banking Zod schemas

Purpose

This module owns every Zod schema for the public boundary of @suaveplan/api-plugin-banking: bank records, bank-account records, the request bodies for create / update / status / transfer / fund-op routes, and the literal-union status/type enums. The schemas are the single source of truth for both runtime validation at the route boundary and the TypeScript types consumed by the service and repository layers.

Features

  • 11 Zod schemas covering the full public surface (BankSchema, CreateBankSchema, UpdateBankSchema, BankAccountSchema, CreateBankAccountSchema, UpdateBankAccountSchema, TransferFundsSchema, FundsOperationSchema, UpdateAccountStatusSchema, plus SortCodeSchema and the three literal-union enums).
  • Default values applied via .default(...) so omitted fields land at their canonical state at parse time (e.g. status defaults to 4 active, balance defaults to 0).
  • Numeric guards: account_number and sort_code are positive integers; amount on every fund movement is z.number().positive() so zero-/negative-amount requests reject at the boundary before reaching the service.

Usage: Basic

import { CreateBankSchema } from "@suaveplan/api-plugin-banking";

const parsed = CreateBankSchema.parse({
    name: "Bank of Genesis",
    sort_code: 123456,
});

Usage: Advanced

The route layer wires each schema to @hono/zod-openapi so the OpenAPI document is generated from the Zod types directly:

import { createRoute, OpenAPIHono } from "@hono/zod-openapi";
import {
    CreateBankAccountSchema,
    BankAccountSchema,
    TransferFundsSchema,
} from "@suaveplan/api-plugin-banking";

const createAccountRoute = createRoute({
    method: "post",
    path: "/",
    request: {
        body: {
            content: {
                "application/json": { schema: CreateBankAccountSchema },
            },
        },
    },
    responses: {
        201: {
            description: "Created",
            content: {
                "application/json": { schema: BankAccountSchema },
            },
        },
    },
});

For typed service-layer parsing (without re-parsing in routes):

import { TransferFundsSchema, type TransferFunds } from "@suaveplan/api-plugin-banking";

const cmd: TransferFunds = TransferFundsSchema.parse(raw);
await service.transfer(cmd);

API Reference

  • BankStatusSchema / BankAccountStatusSchema / BankAccountTypeSchema — literal unions over the integer status / type codes.
  • BankSchema / BankAccountSchema — full normalised record shape returned by repositories (RecordIds flattened to table:id strings).
  • CreateBankSchema / CreateBankAccountSchema — required + optional input fields, with status/balance/overdraft defaulted.
  • UpdateBankSchema / UpdateBankAccountSchema — .partial() over a whitelisted subset of writable fields. Any field outside the whitelist is rejected by the repository's validateFieldName guard rather than silently dropped.
  • TransferFundsSchema, FundsOperationSchema, UpdateAccountStatusSchema — fund-movement and status-change request bodies.
  • SortCodeSchema — XX-XX-XX UK-style sort-code string used by GET /banks/sort-code/:sortCode.

Every schema exports a matching z.infer<> alias under the same name prefixed with the bare type (e.g. Bank, CreateBank, TransferFunds).

Implementation Notes

Banking schemas are deliberately local to this package rather than re-exported from @suaveplan/api-schemas: the upstream monorepo kept them in the universal schemas package, but the genesis convention (rule 4 in CLAUDE.md) is "NEVER re-export external deps". Owning them here also lets the package evolve the schemas (e.g. multi-currency amount, ledger entries) without forcing a schemas-package version bump on every consumer.

The RecordIdSchema and TimestampSchema placeholders are kept intentionally narrow (non-empty strings) because the SurrealDB layer normalises both formats before they cross this boundary. Stricter validation would reject legitimate records.

balance is z.number().default(0), not z.number().nonnegative() — the overdraft mechanism intentionally allows negative balances up to the per-account overdraft ceiling. INSUFFICIENT_FUNDS is enforced in the service layer, not at the schema boundary, because the threshold depends on account.overdraft which is not visible to the schema.