// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // Exact terms are service-owned. Party links are metadata and confer no CRM access. import { procedure } from "@conusai-kit/core" import { Decimal, isCurrencyCode } from "@conusai-kit/money" import { isUnitCode } from "@conusai-kit/money/units" import { z } from "zod" const suffix = "[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}" export const invoiceId = z.string().regex(new RegExp(`^inv_${suffix}$`)) export const seriesId = z.string().regex(new RegExp(`^ser_${suffix}$`)) export const revision = z .string() .regex(/^[1-9][0-9]{0,18}$/) .refine((value) => { try { return BigInt(value) <= 9223372036854775807n } catch { return false } }) // Exactly the service's `DecimalAmount`, `CurrencyCode` and `UnitCode` (business-services/types), checked by the kit, so what // this form accepts the service accepts too. The wire values stay plain strings (`: boolean`, not a type guard). export const decimal = z.string().refine((value): boolean => Decimal.isAmount(value)) const currency = z.string().refine((value): boolean => isCurrencyCode(value)) const date = z.iso.date() const text = z.string().trim().min(1).max(200) export const money = z.object({ amount: decimal, currency }).strict() const identifier = z.object({ scheme: text, value: text }).strict() const address = z .object({ country: z.string().regex(/^[A-Z]{2}$/), street: text.nullable().optional(), additional_street: text.nullable().optional(), address_line_3: text.nullable().optional(), city: text.nullable().optional(), postal_code: text.nullable().optional(), subdivision: text.nullable().optional(), }) .strict() // A CRM link identifies a legal/business party. A scoped member link may identify // an actual person; the service checks that its binding matches the assertion. const reference = z .string() .max(256) .regex( new RegExp( `^(?:conusai://crm/parties/pty_${suffix}|conusai://app-partitions/[0-9a-f-]{36}/members/[0-9a-f-]{36})$`, ), ) export const party = z .object({ reference, name: text, trading_name: text.nullable().optional(), identifiers: z.array(identifier).max(20).optional(), legal_registration: identifier.nullable().optional(), vat_identifier: identifier.nullable().optional(), endpoint: z.object({ scheme: text, value: text }).strict(), address, }) .strict() const quantity = z .string() .regex(/^[1-9][0-9]{0,12}$/) .refine((value) => { try { return BigInt(value) <= 1000000000000n } catch { return false } }) export const line = z .object({ quantity, unit: z.string().refine((value): boolean => isUnitCode(value)), item_name: text, item_identifiers: z.array(identifier).max(20).optional(), net_price: money, vat_category: z.string().min(1).max(8), vat_rate: decimal.nullable().optional(), exemption_reason: text.nullable().optional(), }) .strict() const adjustment = z .object({ amount: money, charge: z.boolean(), reason: text, vat_category: z.string().min(1).max(8), vat_rate: decimal.nullable().optional(), exemption_reason: text.nullable().optional(), }) .strict() export const invoiceTerms = z .object({ type_code: z.string().regex(/^[0-9]{3}$/), series_id: seriesId, issue_date: date, due_date: date.nullable().optional(), currency, seller: party, buyer: party, buyer_reference: text, preceding_invoice: invoiceId.nullable().optional(), lines: z.array(line).min(1).max(200), allowances_charges: z.array(adjustment).max(100).optional(), payment_means: z .object({ code: text, account_id: text.nullable().optional(), account_name: text.nullable().optional() }) .strict() .nullable() .optional(), notes: z.array(z.string().trim().min(1).max(1000)).max(20).optional(), ext: z.record(z.string(), z.unknown()).optional(), }) .strict() export type InvoiceTerms = z.output export const invoiceView = z.object({ invoiceId, revision, status: z.enum(["draft", "issued", "voided"]), number: z.string().nullable(), data: invoiceTerms, payable: money, deleted: z.boolean(), erased: z.boolean(), }) export type InvoiceView = z.output export const seriesView = z.object({ seriesId, revision, code: text, prefix: text, kind: z.enum(["invoice", "credit_note"]), active: z.boolean(), nextNumber: revision, }) export type SeriesView = z.output const org = { org: z.string() } const cursor = z.string().min(1).max(4096).optional() const requestId = z.string().uuid() export const invoices = procedure("invoicing.invoice.list") .kind("read") .scope("org") .permission("invoicing.invoice.read") .input(z.object({ ...org, after: cursor })) .output(z.object({ rows: z.array(invoiceView), after: cursor })) .contract() export const invoice = procedure("invoicing.invoice.get") .kind("read") .scope("org") .permission("invoicing.invoice.read") .input(z.object({ ...org, invoiceId })) .output(invoiceView) .contract() export const series = procedure("invoicing.series.list") .kind("read") .scope("org") .permission("invoicing.series.read") .input(z.object({ ...org, after: cursor })) .output(z.object({ rows: z.array(seriesView), after: cursor })) .contract() export const draft = procedure("invoicing.invoice.draft") .kind("effect") .scope("org") .permission("invoicing.invoice.write") .input(z.object({ ...org, data: invoiceTerms, requestId })) .output(invoiceView) .contract() export const update = procedure("invoicing.invoice.update") .kind("effect") .scope("org") .permission("invoicing.invoice.write") .input(z.object({ ...org, invoiceId, expectedVersion: revision, data: invoiceTerms, requestId })) .output(invoiceView) .contract() const transition = { ...org, invoiceId, expectedVersion: revision, requestId, confirmed: z.literal(true) } export const issue = procedure("invoicing.invoice.issue") .kind("effect") .scope("org") .permission("invoicing.invoice.issue") .input(z.object(transition)) .output(invoiceView) .contract() export const credit = procedure("invoicing.invoice.credit") .kind("effect") .scope("org") .permission("invoicing.invoice.credit") .input(z.object({ ...transition, seriesId, reason: text })) .output(invoiceView) .contract() export const voidInvoice = procedure("invoicing.invoice.void") .kind("effect") .scope("org") .permission("invoicing.invoice.void") .input(z.object({ ...transition, reason: text })) .output(invoiceView) .contract() export const ubl = procedure("invoicing.invoice.ubl") .kind("read") .scope("org") .permission("invoicing.invoice.read") .input(z.object({ ...org, invoiceId })) .output(z.object({ filename: z.string(), xml: z.string().max(8000000) })) .contract() export const createSeries = procedure("invoicing.series.create") .kind("effect") .scope("org") .permission("invoicing.series.manage") .input( z.object({ ...org, code: z.string().regex(/^[A-Za-z0-9._-]{1,32}$/), prefix: z.string().regex(/^[A-Za-z0-9._-]{0,31}[-._]$/), kind: z.enum(["invoice", "credit_note"]), requestId, }), ) .output(seriesView) .contract()