220 lines
7.2 KiB
TypeScript
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()
|