ConusAI vendor v0.1.0-beta.114
ci / gate (push) Waiting to run

This commit is contained in:
ConusAI vendor v0.1.0-beta.114
2026-11-14 09:56:45 +00:00
commit 1ce8d23c35
867 changed files with 122104 additions and 0 deletions
+240
View File
@@ -0,0 +1,240 @@
"use client"
import { formatMoney } from "@conusai-kit/i18n/values"
import { Money } from "@conusai-kit/money"
import type { ProjectSlugs } from "@conusai-kit/ui"
import { useDataClient, useWrite } from "@conusai-kit/ui/data"
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import {
CheckField,
FormLayout,
type FormValues,
PageSection,
ResourceList,
SelectField,
TextField,
usePageNavigation,
useResourceList,
} from "@conusai-kit/ui/patterns"
import { Button } from "@conusai-kit/ui/primitives"
import { useRef, useState } from "react"
import type { InvoiceView, SeriesView } from "../contract/index.js"
import { DraftFields } from "./fields.js"
import { draftData } from "./form-data.js"
import { describeFailure, type Labels } from "./labels.js"
// A lost response retries the same input/key. Editing terms starts a different operation.
function useSubmissionId() {
const current = useRef<{ value: string; key: string } | null>(null)
return (input: unknown) => {
const value = JSON.stringify(input)
if (current.current?.value !== value) current.current = { value, key: crypto.randomUUID() }
return current.current.key
}
}
export function Invoices({
slugs,
seed,
href,
labels,
}: {
slugs: ProjectSlugs
seed: { rows: InvoiceView[]; after?: string | undefined }
href: string
labels: Labels
}) {
const client = useDataClient()
const { list } = useResourceList<InvoiceView>({
seed,
rowId: (row) => row.invoiceId,
read: async ({ after }) =>
(
await client.call<{ rows: InvoiceView[]; after?: string }>("invoicing.invoice.list", {
org: slugs.org,
...(after ? { after } : {}),
})
).value,
})
return (
<ResourceList
{...list}
label={labels.title}
columns={[
{ id: "number", header: labels.number, cell: (row) => row.number ?? labels.draft },
{ id: "status", header: labels.status, cell: (row) => labels[row.status] },
{ id: "payable", header: labels.payable, cell: (row) => formatMoney(Money.parse(row.payable), labels.locale) },
]}
rowHref={(row) => `${href}/${row.invoiceId}`}
rowLabel={(row) => row.number ?? labels.draft}
empty={{ title: labels.empty }}
/>
)
}
export function InvoiceForm({
slugs,
invoice,
series,
labels,
href,
}: {
slugs: ProjectSlugs
invoice?: InvoiceView
series: SeriesView[]
labels: Labels
href: string
}) {
const navigation = usePageNavigation()
const [count, setCount] = useState(invoice?.data.lines.length ?? 1)
const key = useSubmissionId()
const write = useWrite<Record<string, unknown>, InvoiceView>(
invoice ? "invoicing.invoice.update" : "invoicing.invoice.draft",
)
const submit = async (values: FormValues) => {
const data = draftData(values, count, labels.locale, invoice?.data)
const input = {
org: slugs.org,
data,
...(invoice ? { invoiceId: invoice.invoiceId, expectedVersion: invoice.revision } : {}),
}
const saved = await write.mutate({ ...input, requestId: key(input) })
navigation.navigate(`${href}/${saved.invoiceId}`)
}
if (!series.some((row) => row.active && row.kind === (invoice?.data.type_code === "381" ? "credit_note" : "invoice")))
return <p>{labels.missingSeries}</p>
return (
<PageSection title={invoice ? labels.save : labels.create} variant="card">
<FormLayout
describeError={(error) => describeFailure(error, labels)}
submitLabel={invoice ? labels.save : labels.create}
busyLabel={labels.busy}
onSubmit={submit}
>
<DraftFields labels={labels} {...(invoice ? { data: invoice.data } : {})} series={series} count={count} />
<Button type="button" onClick={() => setCount((old) => Math.min(old + 1, 200))}>
{labels.addLine}
</Button>
</FormLayout>
</PageSection>
)
}
export function FiscalAction({
slugs,
invoice,
operation,
series,
labels,
}: {
slugs: ProjectSlugs
invoice: InvoiceView
operation: "issue" | "credit" | "void"
series: SeriesView[]
labels: Labels
}) {
const navigation = usePageNavigation()
const key = useSubmissionId()
const action = useWrite<Record<string, unknown>, InvoiceView>(`invoicing.invoice.${operation}`)
const submit = async (values: FormValues) => {
const input = {
org: slugs.org,
invoiceId: invoice.invoiceId,
expectedVersion: invoice.revision,
confirmed: values.confirmed === "on",
...(operation === "credit" ? { seriesId: values.series } : {}),
...(operation !== "issue" ? { reason: values.reason } : {}),
}
const result = await action.mutate({ ...input, requestId: key(input) })
if (operation === "credit")
navigation.navigate(
`/${slugs.org}/${slugs.workspace}/${slugs.project}/invoicing/${encodeURIComponent(result.invoiceId)}`,
)
else await navigation.refresh()
}
return (
<PageSection title={labels[operation]}>
<FormLayout
describeError={(error) => describeFailure(error, labels)}
submitLabel={labels[operation]}
busyLabel={labels.busy}
onSubmit={submit}
>
{operation === "credit" ? (
<SelectField
name="series"
label={labels.series}
required
options={series
.filter((row) => row.active && row.kind === "credit_note")
.map((row) => ({ value: row.seriesId, label: row.code }))}
/>
) : null}
{operation !== "issue" ? <TextField name="reason" label={labels.reason} required maxLength={500} /> : null}
<CheckField name="confirmed" label={labels.confirm} required />
</FormLayout>
</PageSection>
)
}
export function SeriesForm({ slugs, labels }: { slugs: ProjectSlugs; labels: Labels }) {
const navigation = usePageNavigation()
const key = useSubmissionId()
const write = useWrite<Record<string, unknown>, SeriesView>("invoicing.series.create")
const submit = async (values: FormValues) => {
const input = { org: slugs.org, code: values.code, prefix: values.prefix, kind: values.kind }
await write.mutate({ ...input, requestId: key(input) })
await navigation.refresh()
}
return (
<PageSection title={labels.createSeries}>
<FormLayout
describeError={(error) => describeFailure(error, labels)}
submitLabel={labels.createSeries}
busyLabel={labels.busy}
onSubmit={submit}
>
<TextField name="code" label={labels.seriesCode} required maxLength={32} />
<TextField name="prefix" label={labels.seriesPrefix} required maxLength={32} />
<SelectField
name="kind"
label={labels.kind}
required
options={[
{ value: "invoice", label: labels.invoiceKind },
{ value: "credit_note", label: labels.creditKind },
]}
/>
</FormLayout>
</PageSection>
)
}
export function UblDownload({ slugs, invoice, labels }: { slugs: ProjectSlugs; invoice: InvoiceView; labels: Labels }) {
const client = useDataClient()
const [problem, setProblem] = useState<string>()
const download = async () => {
try {
const { value } = await client.call<{ xml: string; filename: string }>("invoicing.invoice.ubl", {
org: slugs.org,
invoiceId: invoice.invoiceId,
})
const url = URL.createObjectURL(new Blob([value.xml], { type: "application/xml" }))
const link = document.createElement("a")
link.href = url
link.download = value.filename
link.click()
setTimeout(() => URL.revokeObjectURL(url), 1000)
setProblem(undefined)
} catch (error) {
setProblem(describeFailure(error, labels))
}
}
return (
<>
<Button
type="button"
onClick={() => {
void download()
}}
>
{labels.download}
</Button>
{problem ? <p role="alert">{problem}</p> : null}
</>
)
}
+127
View File
@@ -0,0 +1,127 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import { SelectField, TextField } from "@conusai-kit/ui/patterns"
import { FieldLegend, FieldSet } from "@conusai-kit/ui/primitives"
import type { InvoiceTerms, SeriesView } from "../contract/index.js"
import type { Labels } from "./labels.js"
export function DraftFields({
labels,
data,
series,
count,
}: {
labels: Labels
data?: InvoiceTerms
series: SeriesView[]
count: number
}) {
return (
<>
<SelectField
name="series"
label={labels.series}
required
defaultValue={data?.series_id}
options={series
.filter((row) => row.active && row.kind === (data?.type_code === "381" ? "credit_note" : "invoice"))
.map((row) => ({ value: row.seriesId, label: row.code }))}
/>
<TextField name="currency" label={labels.currency} required maxLength={3} defaultValue={data?.currency} />
<TextField name="issueDate" type="date" label={labels.issueDate} required defaultValue={data?.issue_date} />
<TextField name="dueDate" type="date" label={labels.dueDate} defaultValue={data?.due_date ?? undefined} />
<TextField name="buyerReference" label={labels.buyerReference} required defaultValue={data?.buyer_reference} />
{(["seller", "buyer"] as const).map((side) => (
<FieldSet key={side}>
<FieldLegend>{labels[side]}</FieldLegend>
<TextField
name={`${side}.reference`}
label={labels.partyRef}
required
defaultValue={data?.[side].reference}
/>
<TextField name={`${side}.name`} label={labels.name} required defaultValue={data?.[side].name} />
<SelectField
name={`${side}.vatScheme`}
label={labels.vatScheme}
defaultValue={data?.[side].vat_identifier?.scheme ?? "vat"}
options={[
{ value: "vat", label: labels.vatDefault },
{ value: "9937", label: labels.vatLithuanian },
]}
/>
<TextField
name={`${side}.vatValue`}
label={labels.vatIdentifier}
defaultValue={data?.[side].vat_identifier?.value ?? undefined}
/>
<TextField
name={`${side}.country`}
label={labels.country}
required
maxLength={2}
defaultValue={data?.[side].address.country}
/>
<TextField
name={`${side}.street`}
label={labels.street}
defaultValue={data?.[side].address.street ?? undefined}
/>
<TextField name={`${side}.city`} label={labels.city} defaultValue={data?.[side].address.city ?? undefined} />
<TextField
name={`${side}.postcode`}
label={labels.postcode}
defaultValue={data?.[side].address.postal_code ?? undefined}
/>
<TextField
name={`${side}.scheme`}
label={labels.endpointScheme}
required
defaultValue={data?.[side].endpoint.scheme}
/>
<TextField
name={`${side}.endpoint`}
label={labels.endpoint}
required
defaultValue={data?.[side].endpoint.value}
/>
</FieldSet>
))}
{Array.from({ length: count }, (_, i) => (
<FieldSet key={`line-${i}`}>
<FieldLegend>
{labels.item} {i + 1}
</FieldLegend>
<TextField name={`line.${i}.name`} label={labels.item} required defaultValue={data?.lines[i]?.item_name} />
<TextField
name={`line.${i}.quantity`}
label={labels.quantity}
required
defaultValue={data?.lines[i]?.quantity}
/>
<TextField name={`line.${i}.unit`} label={labels.unit} required defaultValue={data?.lines[i]?.unit} />
<TextField
name={`line.${i}.price`}
label={labels.price}
required
defaultValue={data?.lines[i]?.net_price.amount}
/>
<TextField
name={`line.${i}.category`}
label={labels.vatCategory}
required
defaultValue={data?.lines[i]?.vat_category}
/>
<TextField
name={`line.${i}.rate`}
label={labels.vatRate}
defaultValue={data?.lines[i]?.vat_rate ?? undefined}
/>
<TextField
name={`line.${i}.exemption`}
label={labels.exemption}
defaultValue={data?.lines[i]?.exemption_reason ?? undefined}
/>
</FieldSet>
))}
</>
)
}
+54
View File
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import type { Locale } from "@conusai-kit/i18n"
import { parseDecimalInput } from "@conusai-kit/i18n/values"
import { type InvoiceTerms, invoiceTerms } from "../contract/index.js"
/**
* Merge edited fields, retaining all unedited terms/identifiers/notes/extensions. No float coercion: a price or a
* rate is read as the person typed it in `locale` (`12,50` in LT) into the service's exact text; text that is not a
* number is passed on as typed, so the contract refuses it by name.
*/
export function draftData(
values: Record<string, string>,
count: number,
locale: Locale,
prior?: InvoiceTerms,
): InvoiceTerms {
const exact = (text: string | undefined) => (text ? (parseDecimalInput(text, locale)?.toString() ?? text) : text)
const party = (side: "seller" | "buyer") => ({
...prior?.[side],
reference: values[`${side}.reference`],
name: values[`${side}.name`],
vat_identifier: values[`${side}.vatValue`]
? { scheme: values[`${side}.vatScheme`], value: values[`${side}.vatValue`] }
: null,
endpoint: { scheme: values[`${side}.scheme`], value: values[`${side}.endpoint`] },
address: {
...prior?.[side].address,
country: values[`${side}.country`],
street: values[`${side}.street`] || null,
city: values[`${side}.city`] || null,
postal_code: values[`${side}.postcode`] || null,
},
})
return invoiceTerms.parse({
...prior,
type_code: prior?.type_code ?? "380",
series_id: values.series,
currency: values.currency,
issue_date: values.issueDate,
due_date: values.dueDate || null,
buyer_reference: values.buyerReference,
seller: party("seller"),
buyer: party("buyer"),
lines: Array.from({ length: count }, (_, i) => ({
...prior?.lines[i],
item_name: values[`line.${i}.name`],
quantity: values[`line.${i}.quantity`],
unit: values[`line.${i}.unit`],
net_price: { currency: values.currency, amount: exact(values[`line.${i}.price`]) },
vat_category: values[`line.${i}.category`],
vat_rate: exact(values[`line.${i}.rate`]) || null,
exemption_reason: values[`line.${i}.exemption`] || null,
})),
})
}
+16
View File
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import type { Locale } from "@conusai-kit/i18n"
import { serviceNotice } from "@conusai-kit/ui/patterns"
import { ZodError } from "zod"
import type { en } from "../i18n/en.js"
export type Messages = { [K in keyof typeof en]: string }
/**
* What the loader hands the views: the module's messages, the reader's locale (money is formatted with it) and
* the kit's refusal sentences by problem code (`businessRefusalNotices` of `@conusai-kit/business/refusals`).
*/
export type Labels = Messages & { readonly locale: Locale; readonly refusals: Readonly<Record<string, string>> }
export function describeFailure(error: unknown, labels: Labels): string {
if (error instanceof ZodError) return labels.invalid
return serviceNotice(error, labels.refusals)?.text ?? labels.unavailable
}
+171
View File
@@ -0,0 +1,171 @@
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
import { businessRefusalNotices, businessServiceNotices } from "@conusai-kit/business/refusals"
import { type Container, notFound, serviceRefusal, type TransportCaller } from "@conusai-kit/core"
import type { I18n } from "@conusai-kit/i18n"
import { Decimal, Money } from "@conusai-kit/money"
import { Quantity } from "@conusai-kit/money/units"
import { type ProjectPage, projectPage } from "@conusai-kit/ui"
import { Can } from "@conusai-kit/ui/data"
import {
FactList,
Page,
PageHeader,
PageSection,
type ServiceNotice,
ServiceStatePage,
serviceNotice,
} from "@conusai-kit/ui/patterns"
import * as contracts from "../contract/index.js"
import { en } from "../i18n/en.js"
import { FiscalAction, InvoiceForm, Invoices, SeriesForm, UblDownload } from "./controls.js"
import type { Labels, Messages } from "./labels.js"
type Data = { labels: Labels } & (
| {
kind: "list"
invoices: { rows: contracts.InvoiceView[]; after?: string | undefined }
series: contracts.SeriesView[]
}
| {
kind: "invoice"
invoice: contracts.InvoiceView
series: contracts.SeriesView[]
/** The amounts, quantities and rates as the reader writes them, formatted on the server. */
shown: { payable: string; lines: string[] }
}
| { kind: "state"; notice: ServiceNotice }
)
export const invoicingPage: ProjectPage<TransportCaller, I18n, Container, Data> = projectPage({
async load({ slugs, segments, caller, i18n }) {
const messages = Object.fromEntries(
Object.keys(en).map((key) => [key, i18n.message(`invoicing.${key}`)]),
) as Messages
const labels: Labels = {
...messages,
locale: i18n.locale,
refusals: businessRefusalNotices(i18n.locale, {
conflict: messages.conflict,
revision_conflict: messages.conflict,
invalid: messages.invalid,
}),
}
const [id, ...rest] = segments
if (rest.length || (id !== undefined && !contracts.invoiceId.safeParse(id).success)) throw notFound()
try {
const allSeries: contracts.SeriesView[] = []
let after: string | undefined
const seen = new Set<string>()
let pages = 0
// Numbering choices are bounded; refusal is preferable to silently missing a series.
do {
if (pages++ >= 20) throw serviceRefusal("degraded")
const page = await caller.call(contracts.series, { org: slugs.org, ...(after ? { after } : {}) })
allSeries.push(...page.rows)
after = page.after
if (allSeries.length > 1000 || (after && seen.has(after))) throw serviceRefusal("degraded")
if (after) seen.add(after)
} while (after)
if (id === undefined)
return {
kind: "list",
labels,
invoices: await caller.call(contracts.invoices, { org: slugs.org }),
series: allSeries,
}
const invoice = await caller.call(contracts.invoice, { org: slugs.org, invoiceId: id })
return { kind: "invoice", labels, invoice, series: allSeries, shown: shownOf(invoice, labels, i18n) }
} catch (error) {
const notice = serviceNotice(error, businessServiceNotices(i18n.locale))
if (!notice) throw error
return { kind: "state", labels, notice }
}
},
View({ slugs, data }) {
const labels = data.labels
const href = `/${slugs.org}/${slugs.workspace}/${slugs.project}/invoicing`
if (data.kind === "state") return <ServiceStatePage title={labels.title} notice={data.notice} />
if (data.kind === "list")
return (
<Page>
<PageHeader title={labels.title} description={labels.intro} />
<p>{labels.fiscalNotice}</p>
<Invoices slugs={slugs} seed={data.invoices} href={href} labels={labels} />
<Can permission="invoicing.invoice.write">
<InvoiceForm slugs={slugs} labels={labels} series={data.series} href={href} />
</Can>
<Can permission="invoicing.series.manage">
<SeriesForm slugs={slugs} labels={labels} />
</Can>
</Page>
)
const invoice = data.invoice
return (
<Page>
<PageHeader
title={invoice.number ?? labels.draft}
description={`${labels[invoice.status]} · ${data.shown.payable}`}
/>
<a href={href}>{labels.back}</a>
<p>{labels.fiscalNotice}</p>
<PageSection title={labels.saved}>
<FactList
facts={[
{ label: labels.revision, value: invoice.revision },
{ label: labels.seller, value: invoice.data.seller.name },
{ label: labels.buyer, value: invoice.data.buyer.name },
{ label: labels.issueDate, value: invoice.data.issue_date },
{ label: labels.dueDate, value: invoice.data.due_date ?? "—" },
{ label: labels.currency, value: invoice.data.currency },
{ label: labels.buyerReference, value: invoice.data.buyer_reference },
...invoice.data.lines.map((line, index) => ({
label: line.item_name,
value: data.shown.lines[index] ?? "",
})),
{ label: labels.payable, value: data.shown.payable },
]}
/>
</PageSection>
{invoice.status === "draft" ? (
<>
<Can permission="invoicing.invoice.write">
<InvoiceForm slugs={slugs} invoice={invoice} labels={labels} series={data.series} href={href} />
</Can>
<Can permission="invoicing.invoice.issue">
<FiscalAction slugs={slugs} invoice={invoice} operation="issue" series={data.series} labels={labels} />
</Can>
</>
) : null}
{invoice.status === "issued" ? (
<>
<Can permission="invoicing.invoice.credit">
<FiscalAction slugs={slugs} invoice={invoice} operation="credit" series={data.series} labels={labels} />
</Can>
<Can permission="invoicing.invoice.void">
<FiscalAction slugs={slugs} invoice={invoice} operation="void" series={data.series} labels={labels} />
</Can>
</>
) : null}
{invoice.status !== "draft" ? (
<PageSection title={labels.download}>
<UblDownload slugs={slugs} invoice={invoice} labels={labels} />
</PageSection>
) : null}
</Page>
)
},
})
/** Exact values in the reader's form (`12,50 €`, `3 vnt.`, `21 %`), never through a float. */
function shownOf(invoice: contracts.InvoiceView, labels: Labels, i18n: I18n): { payable: string; lines: string[] } {
return {
payable: i18n.money(Money.parse(invoice.payable)),
lines: invoice.data.lines.map((line) =>
[
`${i18n.quantity(Quantity.of(line.quantity, line.unit))} × ${i18n.money(Money.parse(line.net_price))}`,
`${labels.vatCategory}: ${line.vat_category}`,
`${labels.vatRate}: ${line.vat_rate ? i18n.percent(Decimal.parse(line.vat_rate)) : "—"}`,
...(line.exemption_reason ? [line.exemption_reason] : []),
].join(" · "),
),
}
}