// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // Simulated ports cover contract/app rules; real service fiscal transactions are separate acceptance. import { describe, expect, it, vi } from "vitest" import { decimal, draft, invoiceTerms, issue, revision } from "../contract/index.js" import { transitionInvoice, updateDraft, validateTerms } from "../core/invoices.js" import { type InvoicingService, readsOf } from "../core/ports.js" import { en } from "../i18n/en.js" import { lt } from "../i18n/lt.js" import { draftData } from "../ui/form-data.js" import { id, invoiceId, row, terms } from "./fixture.js" function service(): InvoicingService { return { list: vi.fn(async () => ({ rows: [row] })), get: vi.fn(async () => row), series: vi.fn(async () => ({ rows: [] })), ubl: vi.fn(async () => ({ filename: "fixture.xml", xml: "" })), draft: vi.fn(async () => row), update: vi.fn(async () => row), transition: vi.fn(async () => row), createSeries: vi.fn(async () => { throw new Error("not in this fixture") }), } } describe("Invoicing contracts and safe app operations", () => { it("keeps amounts exact and rejects number/exponent and malformed integer inputs without throwing", () => { expect(invoiceTerms.parse(terms).lines[0]?.net_price.amount).toBe("9007199254740993.123456789012345678") for (const bad of [1, NaN, Infinity, "1e3", "1.2.3", ""]) expect(decimal.safeParse(bad).success).toBe(false) for (const bad of ["oops", "1.5", "9223372036854775808", ""]) expect(revision.safeParse(bad).success).toBe(false) expect( draft.input.safeParse({ org: "fixture", data: { ...terms, lines: [{ ...terms.lines[0], quantity: "oops" }] }, requestId: id, }).success, ).toBe(false) }) it("refuses cross-currency terms before an external mutation", () => { expect(() => validateTerms({ ...terms, lines: [ { ...terms.lines[0], quantity: "2", item_name: "Synthetic", unit: "C62", vat_category: "S", net_price: { currency: "USD", amount: "1" }, }, ], }), ).toThrow() }) it("requires an explicit confirmation and grants no write methods to a read view", async () => { expect( issue.input.safeParse({ org: "fixture", invoiceId, expectedVersion: "1", requestId: id, confirmed: false }) .success, ).toBe(false) const port = service() expect("draft" in readsOf(port)).toBe(false) await expect( transitionInvoice(port, "issue", { invoiceId, expectedVersion: "1", requestId: id, confirmed: false as never }), ).rejects.toMatchObject({ code: "bad_request" }) expect(port.transition).not.toHaveBeenCalled() }) it("forwards the same exact revision and idempotency key on response-loss retries", async () => { const port = service() const input = { invoiceId, expectedVersion: "1", requestId: id, confirmed: true as const } await transitionInvoice(port, "issue", input) await transitionInvoice(port, "issue", input) expect(vi.mocked(port.transition).mock.calls[0]).toEqual(vi.mocked(port.transition).mock.calls[1]) expect(port.transition).toHaveBeenCalledWith( "issue", expect.objectContaining({ expectedVersion: "1", key: `invoicing:issue:${id}` }), ) }) it("does not predict a future revision, and delegates stale receipt resolution to the service", async () => { const port = service() await expect( updateDraft(port, { invoiceId, data: terms, expectedVersion: "9007199254740994", requestId: id }), ).rejects.toMatchObject({ code: "conflict" }) expect(port.update).not.toHaveBeenCalled() await updateDraft(port, { invoiceId, data: terms, expectedVersion: "1", requestId: id }) expect(port.update).toHaveBeenCalledWith(invoiceId, terms, "1", `invoicing:update:${id}`) }) it("retains unedited invoice terms and exact digits while editing an existing draft", () => { const prior = { ...terms, notes: ["Synthetic note"], ext: { fixture: "retain" }, payment_means: { code: "30", account_id: "SYNTHETIC" }, seller: { ...terms.seller, trading_name: "Synthetic trading" }, } const values: Record = { series: terms.series_id, currency: terms.currency, issueDate: terms.issue_date, buyerReference: terms.buyer_reference, } for (const side of ["seller", "buyer"] as const) { const party = terms[side] Object.assign(values, { [`${side}.reference`]: party.reference, [`${side}.name`]: party.name, [`${side}.country`]: party.address.country, [`${side}.scheme`]: party.endpoint.scheme, [`${side}.endpoint`]: party.endpoint.value, [`${side}.vatScheme`]: party.vat_identifier?.scheme ?? "vat", [`${side}.vatValue`]: party.vat_identifier?.value ?? "", }) } const line = terms.lines[0] if (!line) throw new Error("fixture line absent") Object.assign(values, { "line.0.name": line.item_name, "line.0.quantity": line.quantity, "line.0.unit": line.unit, "line.0.price": line.net_price.amount, "line.0.category": line.vat_category, "line.0.rate": line.vat_rate ?? "", }) const next = draftData(values, 1, "en", prior) expect(next.ext).toEqual(prior.ext) expect(next.payment_means).toEqual(prior.payment_means) expect(next.seller.trading_name).toBe(prior.seller.trading_name) expect(next.notes).toEqual(prior.notes) expect(next.lines[0]?.net_price.amount).toBe(line.net_price.amount) }) it("reads a price and a VAT rate as a Lithuanian reader types them, exactly", () => { const values: Record = { series: terms.series_id, currency: "EUR", issueDate: terms.issue_date, buyerReference: terms.buyer_reference, "line.0.name": "Varžtas", "line.0.quantity": "3", "line.0.unit": "H87", "line.0.price": "1\u00a0234,50", "line.0.category": "S", "line.0.rate": "21", } for (const side of ["seller", "buyer"] as const) { const party = terms[side] Object.assign(values, { [`${side}.reference`]: party.reference, [`${side}.name`]: party.name, [`${side}.scheme`]: party.endpoint.scheme, [`${side}.endpoint`]: party.endpoint.value, [`${side}.country`]: party.address.country, }) } const next = draftData(values, 1, "lt", terms) expect(next.lines[0]?.net_price).toEqual({ currency: "EUR", amount: "1234.50" }) expect(next.lines[0]?.vat_rate).toBe("21") expect(() => draftData({ ...values, "line.0.price": "12,5,0" }, 1, "lt", terms)).toThrow() }) it("ships the same translated labels in both languages", () => expect(Object.keys(en).sort()).toEqual(Object.keys(lt).sort())) })