Files
2026-11-14 09:56:45 +00:00

220 lines
7.2 KiB
TypeScript

// 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<typeof invoiceTerms>
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<typeof invoiceView>
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<typeof seriesView>
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()