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