// 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 = 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() 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 if (data.kind === "list") return (

{labels.fiscalNotice}

) const invoice = data.invoice return ( {labels.back}

{labels.fiscalNotice}

({ label: line.item_name, value: data.shown.lines[index] ?? "", })), { label: labels.payable, value: data.shown.payable }, ]} /> {invoice.status === "draft" ? ( <> ) : null} {invoice.status === "issued" ? ( <> ) : null} {invoice.status !== "draft" ? ( ) : null}
) }, }) /** 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(" · "), ), } }