// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 import { businessAppError, type RequestBusinessBinding, requestBusinessBinding, toBusinessError, } from "@conusai-kit/business" import { badRequest, type Container, notFound, type ProcedureKind, serviceRefusal } from "@conusai-kit/core" import { type Invoice, type InvoiceData, invoicingCreateSeries, invoicingCreditInvoice, invoicingDraftInvoice, invoicingGetInvoice, invoicingIssueInvoice, invoicingListInvoices, invoicingListSeries, invoicingRenderUbl, invoicingUpdateInvoice, invoicingVoidInvoice, type Series, } from "@conusai-sdk/invoicing" import { createClient } from "@conusai-sdk/invoicing/client" import { type InvoiceTerms, type InvoiceView, invoiceView, type SeriesView, seriesView } from "../contract/index.js" import { INVOICING_SERVICE, type InvoicingReads, type InvoicingService, readsOf } from "../core/ports.js" async function answer( operation: Promise<{ data?: T; error?: unknown; response?: Response }>, ): Promise> { let result: Awaited try { result = await operation } catch (error) { throw businessAppError(toBusinessError(error, undefined)) } if (result.error !== undefined || result.data === undefined) throw businessAppError(toBusinessError(result.error, result.response)) return result.data as Exclude } const partyOf = (party: InvoiceTerms["seller"]): InvoiceData["seller"] => ({ reference: party.reference, name: party.name, trading_name: party.trading_name ?? null, identifiers: party.identifiers ?? [], legal_registration: party.legal_registration ?? null, vat_identifier: party.vat_identifier ?? null, endpoint: party.endpoint, address: { country: party.address.country, street: party.address.street ?? null, additional_street: party.address.additional_street ?? null, address_line_3: party.address.address_line_3 ?? null, city: party.address.city ?? null, postal_code: party.address.postal_code ?? null, subdivision: party.address.subdivision ?? null, }, }) const terms = (data: InvoiceTerms): InvoiceData => ({ type_code: data.type_code, series_id: data.series_id, issue_date: data.issue_date, due_date: data.due_date ?? null, currency: data.currency, seller: partyOf(data.seller), buyer: partyOf(data.buyer), buyer_reference: data.buyer_reference, preceding_invoice: data.preceding_invoice ?? null, lines: data.lines.map((line) => ({ item_name: line.item_name, quantity: BigInt(line.quantity), unit: line.unit, item_identifiers: line.item_identifiers ?? [], net_price: line.net_price, vat_category: line.vat_category, vat_rate: line.vat_rate ?? null, exemption_reason: line.exemption_reason ?? null, })), allowances_charges: (data.allowances_charges ?? []).map((entry) => ({ amount: entry.amount, charge: entry.charge, reason: entry.reason, vat_category: entry.vat_category, vat_rate: entry.vat_rate ?? null, exemption_reason: entry.exemption_reason ?? null, })), notes: data.notes ?? [], ext: data.ext ?? {}, payment_means: data.payment_means ? { code: data.payment_means.code, account_id: data.payment_means.account_id ?? null, account_name: data.payment_means.account_name ?? null, } : null, }) const view = (row: Invoice): InvoiceView => { if (row.deleted || row.erased) throw notFound() return invoiceView.parse({ invoiceId: row.id, revision: row.revision.toString(), status: row.status, number: row.number ?? null, data: { ...row.data, lines: row.data.lines.map((line) => ({ ...line, quantity: line.quantity.toString() })) }, payable: row.calculated.totals.payable, deleted: row.deleted, erased: row.erased, }) } const series = (row: Series): SeriesView => seriesView.parse({ seriesId: row.id, revision: row.revision.toString(), code: row.code, prefix: row.prefix, active: row.active, kind: row.kind, nextNumber: row.next_number.toString(), }) export function sdkInvoicingService(binding: RequestBusinessBinding): InvoicingService { const client = binding.client(createClient) const org = { org_slug: binding.orgSlug } const scopedTerms = (data: InvoiceTerms): InvoiceData => { for (const party of [data.seller, data.buyer]) if ( party.reference.startsWith("conusai://app-partitions/") && !party.reference.startsWith(`conusai://app-partitions/${binding.bindingId}/members/`) ) throw badRequest("The party subject belongs to another app connection.") return terms(data) } return { async list(after) { const result = await answer( invoicingListInvoices({ client, path: org, query: { limit: 50n, ...(after ? { after } : {}) } }), ) return { rows: result.data.filter((row) => !row.deleted && !row.erased).map(view), ...(result.next_after ? { after: result.next_after } : {}), } }, async get(id) { return view(await answer(invoicingGetInvoice({ client, path: { ...org, invoice_id: id } }))) }, async series(after) { const result = await answer( invoicingListSeries({ client, path: org, query: { limit: 50n, ...(after ? { after } : {}) } }), ) return { rows: result.data.map(series), ...(result.next_after ? { after: result.next_after } : {}) } }, async draft(data, key) { return view( await answer( invoicingDraftInvoice({ client, path: org, body: { data: scopedTerms(data), idempotency_key: key } }), ), ) }, async update(id, data, revision, key) { return view( await answer( invoicingUpdateInvoice({ client, path: { ...org, invoice_id: id }, body: { data: scopedTerms(data), revision: BigInt(revision), idempotency_key: key }, }), ), ) }, async transition(operation, input) { const method = operation === "issue" ? invoicingIssueInvoice : operation === "credit" ? invoicingCreditInvoice : invoicingVoidInvoice return view( await answer( method({ client, path: { ...org, invoice_id: input.invoiceId }, body: { revision: BigInt(input.expectedVersion), idempotency_key: input.key, ...(input.seriesId ? { series_id: input.seriesId } : {}), ...(input.reason ? { reason: input.reason } : {}), }, }), ), ) }, async createSeries(input) { return series( await answer( invoicingCreateSeries({ client, path: org, body: { code: input.code, prefix: input.prefix, kind: input.kind, active: true, idempotency_key: input.key, }, }), ), ) }, async ubl(id) { // Force the generated wire client to preserve XML bytes as a Blob. const blob = await answer(invoicingRenderUbl({ client, path: { ...org, invoice_id: id }, parseAs: "blob" })) if (!(blob instanceof Blob) || blob.size > 8000000) throw serviceRefusal("degraded") return { filename: `invoice-${id}.xml`, xml: await blob.text() } }, } } export function invoicingServiceOf(container: Container, kind: "effect"): InvoicingService export function invoicingServiceOf(container: Container, kind: ProcedureKind): InvoicingReads export function invoicingServiceOf(container: Container, kind: ProcedureKind): InvoicingReads { const service = container.has(INVOICING_SERVICE) ? container.resolve(INVOICING_SERVICE) : sdkInvoicingService(requestBusinessBinding(container)) return kind === "effect" ? service : readsOf(service) }