@@ -0,0 +1,33 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
import { implement } from "@conusai-kit/core"
|
||||
import * as c from "../contract/index.js"
|
||||
import { operationKey, transitionInvoice, updateDraft, validateTerms } from "../core/invoices.js"
|
||||
import { invoicingServiceOf } from "./service.js"
|
||||
export const INVOICING_PROCEDURES = [
|
||||
implement(c.invoices, ({ ctx, input }) => invoicingServiceOf(ctx.container, ctx.kind).list(input.after)),
|
||||
implement(c.invoice, ({ ctx, input }) => invoicingServiceOf(ctx.container, ctx.kind).get(input.invoiceId)),
|
||||
implement(c.series, ({ ctx, input }) => invoicingServiceOf(ctx.container, ctx.kind).series(input.after)),
|
||||
implement(c.ubl, ({ ctx, input }) => invoicingServiceOf(ctx.container, ctx.kind).ubl(input.invoiceId)),
|
||||
implement(c.draft, ({ ctx, input }) =>
|
||||
invoicingServiceOf(ctx.container, "effect").draft(
|
||||
validateTerms(input.data),
|
||||
operationKey("draft", input.requestId),
|
||||
),
|
||||
),
|
||||
implement(c.update, ({ ctx, input }) => updateDraft(invoicingServiceOf(ctx.container, "effect"), input)),
|
||||
implement(c.issue, ({ ctx, input }) =>
|
||||
transitionInvoice(invoicingServiceOf(ctx.container, "effect"), "issue", input),
|
||||
),
|
||||
implement(c.credit, ({ ctx, input }) =>
|
||||
transitionInvoice(invoicingServiceOf(ctx.container, "effect"), "credit", input),
|
||||
),
|
||||
implement(c.voidInvoice, ({ ctx, input }) =>
|
||||
transitionInvoice(invoicingServiceOf(ctx.container, "effect"), "void", input),
|
||||
),
|
||||
implement(c.createSeries, ({ ctx, input }) =>
|
||||
invoicingServiceOf(ctx.container, "effect").createSeries({
|
||||
...input,
|
||||
key: operationKey("series", input.requestId),
|
||||
}),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,223 @@
|
||||
// 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<T>(
|
||||
operation: Promise<{ data?: T; error?: unknown; response?: Response }>,
|
||||
): Promise<Exclude<T, undefined>> {
|
||||
let result: Awaited<typeof operation>
|
||||
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<T, undefined>
|
||||
}
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user