@@ -0,0 +1,50 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
import { badRequest, conflict } from "@conusai-kit/core"
|
||||
import type { InvoiceTerms } from "../contract/index.js"
|
||||
import type { InvoicingService } from "./ports.js"
|
||||
export const operationKey = (operation: string, requestId: string) => `invoicing:${operation}:${requestId}`
|
||||
export function validateTerms(data: InvoiceTerms): InvoiceTerms {
|
||||
if (
|
||||
data.lines.some((line) => line.net_price.currency !== data.currency) ||
|
||||
data.allowances_charges?.some((charge) => charge.amount.currency !== data.currency)
|
||||
)
|
||||
throw badRequest("Invoice currencies must match.")
|
||||
return data
|
||||
}
|
||||
export async function updateDraft(
|
||||
service: InvoicingService,
|
||||
input: { invoiceId: string; data: InvoiceTerms; expectedVersion: string; requestId: string },
|
||||
) {
|
||||
const current = await service.get(input.invoiceId)
|
||||
// Prior revisions reach the service's idempotency ledger on response-loss retries.
|
||||
if (BigInt(input.expectedVersion) > BigInt(current.revision)) throw conflict("Reload the invoice before editing.")
|
||||
return service.update(
|
||||
input.invoiceId,
|
||||
validateTerms(input.data),
|
||||
input.expectedVersion,
|
||||
operationKey("update", input.requestId),
|
||||
)
|
||||
}
|
||||
export async function transitionInvoice(
|
||||
service: InvoicingService,
|
||||
operation: "issue" | "credit" | "void",
|
||||
input: {
|
||||
invoiceId: string
|
||||
expectedVersion: string
|
||||
requestId: string
|
||||
confirmed: true
|
||||
reason?: string
|
||||
seriesId?: string
|
||||
},
|
||||
) {
|
||||
if (input.confirmed !== true) throw badRequest("Explicit confirmation is required.")
|
||||
// The service owns state/revision, retries, immutable numbering and fiscal retention.
|
||||
// No local allocation or predicted revision may substitute for that transaction.
|
||||
return service.transition(operation, {
|
||||
invoiceId: input.invoiceId,
|
||||
expectedVersion: input.expectedVersion,
|
||||
key: operationKey(operation, input.requestId),
|
||||
...(input.reason ? { reason: input.reason } : {}),
|
||||
...(input.seriesId ? { seriesId: input.seriesId } : {}),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
import { token } from "@conusai-kit/core"
|
||||
import type { InvoiceTerms, InvoiceView, SeriesView } from "../contract/index.js"
|
||||
export interface Page<T> {
|
||||
rows: T[]
|
||||
after?: string
|
||||
}
|
||||
export interface InvoicingReads {
|
||||
list(after?: string): Promise<Page<InvoiceView>>
|
||||
get(id: string): Promise<InvoiceView>
|
||||
series(after?: string): Promise<Page<SeriesView>>
|
||||
ubl(id: string): Promise<{ filename: string; xml: string }>
|
||||
}
|
||||
export interface InvoicingService extends InvoicingReads {
|
||||
draft(data: InvoiceTerms, key: string): Promise<InvoiceView>
|
||||
update(id: string, data: InvoiceTerms, revision: string, key: string): Promise<InvoiceView>
|
||||
transition(
|
||||
operation: "issue" | "credit" | "void",
|
||||
input: { invoiceId: string; expectedVersion: string; key: string; seriesId?: string; reason?: string },
|
||||
): Promise<InvoiceView>
|
||||
createSeries(input: {
|
||||
code: string
|
||||
prefix: string
|
||||
kind: "invoice" | "credit_note"
|
||||
key: string
|
||||
}): Promise<SeriesView>
|
||||
}
|
||||
export const readsOf = (service: InvoicingReads): InvoicingReads => ({
|
||||
list: (after) => service.list(after),
|
||||
get: (id) => service.get(id),
|
||||
series: (after) => service.series(after),
|
||||
ubl: (id) => service.ubl(id),
|
||||
})
|
||||
export const INVOICING_SERVICE = token<InvoicingService>("invoicing.service", {
|
||||
views: { read: readsOf, write: readsOf },
|
||||
})
|
||||
Reference in New Issue
Block a user