@@ -0,0 +1,161 @@
|
||||
// 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: "<Invoice/>" })),
|
||||
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<string, string> = {
|
||||
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<string, string> = {
|
||||
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()))
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
import type { InvoiceTerms, InvoiceView } from "../contract/index.js"
|
||||
export const id = "01900000-0000-7000-8000-000000000001"
|
||||
export const invoiceId = `inv_${id}`
|
||||
export const seriesId = `ser_${id}`
|
||||
export const terms: InvoiceTerms = {
|
||||
type_code: "380",
|
||||
series_id: seriesId,
|
||||
issue_date: "2026-11-03",
|
||||
currency: "EUR",
|
||||
buyer_reference: "Synthetic-order",
|
||||
seller: {
|
||||
reference: `conusai://crm/parties/pty_${id}`,
|
||||
name: "Synthetic Seller",
|
||||
vat_identifier: { scheme: "vat", value: "LT100000001" },
|
||||
endpoint: { scheme: "0088", value: "1234567890128" },
|
||||
address: { country: "LT", city: "Synthetic city" },
|
||||
},
|
||||
buyer: {
|
||||
reference: `conusai://crm/parties/pty_${id}`,
|
||||
name: "Synthetic Buyer",
|
||||
endpoint: { scheme: "0088", value: "1234567890128" },
|
||||
address: { country: "LT" },
|
||||
},
|
||||
lines: [
|
||||
{
|
||||
item_name: "Synthetic item",
|
||||
quantity: "2",
|
||||
unit: "C62",
|
||||
net_price: { currency: "EUR", amount: "9007199254740993.123456789012345678" },
|
||||
vat_category: "S",
|
||||
vat_rate: "21",
|
||||
},
|
||||
],
|
||||
}
|
||||
export const row: InvoiceView = {
|
||||
invoiceId,
|
||||
revision: "9007199254740993",
|
||||
status: "draft",
|
||||
number: null,
|
||||
data: terms,
|
||||
payable: { currency: "EUR", amount: "18014398509481986.246913578024691356" },
|
||||
deleted: false,
|
||||
erased: false,
|
||||
}
|
||||
export const raw = () => ({
|
||||
id: invoiceId,
|
||||
revision: "RAW_REVISION",
|
||||
status: "draft",
|
||||
number: null,
|
||||
data: { ...terms, lines: terms.lines.map((line) => ({ ...line, quantity: 2 })) },
|
||||
calculated: {
|
||||
line_nets: [],
|
||||
totals: Object.fromEntries(
|
||||
[
|
||||
"allowance",
|
||||
"charge",
|
||||
"line_extension",
|
||||
"payable",
|
||||
"prepaid",
|
||||
"rounding",
|
||||
"tax",
|
||||
"tax_exclusive",
|
||||
"tax_inclusive",
|
||||
].map((key) => [key, row.payable]),
|
||||
),
|
||||
vat_breakdown: [],
|
||||
},
|
||||
deleted: false,
|
||||
erased: false,
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
import type { TransportCaller } from "@conusai-kit/core"
|
||||
import type { IsolationFixture, IsolationTenant } from "@conusai-kit/testing"
|
||||
import { businessModulesOn } from "../../index.js"
|
||||
import { id, invoiceId, seriesId, terms } from "./fixture.js"
|
||||
|
||||
const org = (t: IsolationTenant) => ({ org: t.orgSlug })
|
||||
const get = (t: IsolationTenant) => ({ ...org(t), invoiceId })
|
||||
const action = (t: IsolationTenant) => ({ ...get(t), expectedVersion: "1", requestId: id, confirmed: true })
|
||||
export const invoicingIsolationFixtures: Record<string, IsolationFixture> = {
|
||||
"invoicing.invoice.list": { input: org },
|
||||
"invoicing.series.list": { input: org },
|
||||
"invoicing.invoice.get": { input: get },
|
||||
"invoicing.invoice.ubl": { input: get },
|
||||
"invoicing.invoice.draft": { input: (t) => ({ ...org(t), data: terms, requestId: id }) },
|
||||
"invoicing.invoice.update": { input: (t) => ({ ...action(t), data: terms }) },
|
||||
"invoicing.invoice.issue": { input: action },
|
||||
"invoicing.invoice.credit": { input: (t) => ({ ...action(t), seriesId, reason: "Synthetic correction" }) },
|
||||
"invoicing.invoice.void": { input: (t) => ({ ...action(t), reason: "Synthetic correction" }) },
|
||||
"invoicing.series.create": {
|
||||
input: (t) => ({ ...org(t), code: "SYNTH", prefix: "SYNTH-", kind: "invoice", requestId: id }),
|
||||
},
|
||||
}
|
||||
export const isolation: {
|
||||
seed(
|
||||
call: TransportCaller,
|
||||
inProject: { org: string; workspace: string; project: string },
|
||||
): Promise<Record<string, string>>
|
||||
fixtures: Record<string, IsolationFixture>
|
||||
} = { seed: async () => ({}), fixtures: businessModulesOn.includes("invoicing") ? invoicingIsolationFixtures : {} }
|
||||
@@ -0,0 +1,36 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// The invoice terms accept exactly what the Invoicing service accepts: its `DecimalAmount` grammar and ISO 4217
|
||||
// codes (business-services/types). The module's own regex let `007.50` and `XYZ` through, which the service then
|
||||
// refused; a person found out only after submitting.
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { decimal, line, money } from "../contract/index.js"
|
||||
|
||||
describe("the invoice's exact money", () => {
|
||||
it("refuses an amount with a leading zero and a currency the service does not know", () => {
|
||||
for (const amount of ["007.50", "00", "-01", "1.", ".5", "+1", "1e3", "1,50"]) {
|
||||
expect(decimal.safeParse(amount).success, amount).toBe(false)
|
||||
}
|
||||
expect(money.safeParse({ amount: "7.50", currency: "XYZ" }).success).toBe(false)
|
||||
expect(money.safeParse({ amount: "7.50", currency: "LTL" }).success).toBe(false)
|
||||
})
|
||||
|
||||
it("takes a line's unit only as a UN/CEFACT Rec 20 code the service accepts", () => {
|
||||
const base = {
|
||||
quantity: "3",
|
||||
item_name: "Bolt",
|
||||
net_price: { amount: "1.20", currency: "EUR" },
|
||||
vat_category: "S",
|
||||
vat_rate: "21",
|
||||
}
|
||||
expect(line.safeParse({ ...base, unit: "C62" }).success).toBe(true)
|
||||
for (const unit of ["pcs", "PCS", "XBX", "m2"]) expect(line.safeParse({ ...base, unit }).success, unit).toBe(false)
|
||||
})
|
||||
|
||||
it("accepts every amount the service accepts, trailing zeros and its bounds included", () => {
|
||||
for (const amount of ["0", "-0.00", "7.50", "9999999999999999999999999999.999999999999999999"]) {
|
||||
expect(decimal.safeParse(amount).success, amount).toBe(true)
|
||||
}
|
||||
expect(decimal.safeParse("10000000000000000000000000000").success).toBe(false)
|
||||
expect(money.safeParse({ amount: "7.50", currency: "EUR" }).success).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,97 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// Memory app + injected service port. Proves app membership/roles, never external partition isolation.
|
||||
import { type Actor, buildRegistry, Container, createCaller } from "@conusai-kit/core"
|
||||
import { createMemoryDataSource, DATA_SOURCE, entitiesOf, SYSTEM_DATA } from "@conusai-kit/data"
|
||||
import { createTenancyHooks, TENANCY, tenancyModule, users } from "@conusai-kit/tenancy"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { simulatedBusinessAdmission } from "../../../test/helpers/business-admission.js"
|
||||
import { INVOICING_SERVICE, type InvoicingService } from "../core/ports.js"
|
||||
import { invoicingModule } from "../module.js"
|
||||
import { id, invoiceId, row, terms } from "./fixture.js"
|
||||
import { invoicingIsolationFixtures } from "./isolation.js"
|
||||
|
||||
async function fixture() {
|
||||
const registry = buildRegistry([tenancyModule, invoicingModule])
|
||||
const data = createMemoryDataSource({ entities: entitiesOf(registry, "postgres") })
|
||||
const root = new Container()
|
||||
root.value(DATA_SOURCE, data)
|
||||
root.value(SYSTEM_DATA, data)
|
||||
const port: InvoicingService = {
|
||||
list: vi.fn(async () => ({ rows: [row] })),
|
||||
get: vi.fn(async () => row),
|
||||
series: vi.fn(async () => ({ rows: [] })),
|
||||
ubl: vi.fn(async () => ({ filename: "fixture.xml", xml: "<Invoice/>" })),
|
||||
draft: vi.fn(async () => row),
|
||||
update: vi.fn(async () => row),
|
||||
transition: vi.fn(async () => row),
|
||||
createSeries: vi.fn(async () => {
|
||||
throw new Error("not used")
|
||||
}),
|
||||
}
|
||||
root.value(INVOICING_SERVICE, port)
|
||||
const caller = (actor: Actor) => {
|
||||
const tenancy = createTenancyHooks({ data, registry })
|
||||
const container = root.child()
|
||||
container.value(TENANCY, tenancy)
|
||||
return createCaller({ registry, container, tenancy, serviceReadiness: simulatedBusinessAdmission }, { actor })
|
||||
}
|
||||
const user = async (name: string) => {
|
||||
const row = await data.global(users).insert({
|
||||
issuer: "https://id.example.test",
|
||||
subject: name,
|
||||
email: `${name}@example.test`,
|
||||
displayName: name,
|
||||
locale: "en",
|
||||
})
|
||||
return { userId: row.id }
|
||||
}
|
||||
const owner = caller(await user("owner"))
|
||||
await owner.callByName("tenancy.org.create", { slug: "alpha", name: "Synthetic Alpha" })
|
||||
const foreign = caller(await user("foreign"))
|
||||
await foreign.callByName("tenancy.org.create", { slug: "beta", name: "Synthetic Beta" })
|
||||
const invite = (await owner.callByName("tenancy.member.invite", {
|
||||
org: "alpha",
|
||||
email: "viewer@example.test",
|
||||
role: "viewer",
|
||||
})) as { token: string }
|
||||
const viewer = caller(await user("viewer"))
|
||||
await viewer.callByName("tenancy.invitation.accept", { org: "alpha", token: invite.token })
|
||||
return { owner, foreign, viewer, port }
|
||||
}
|
||||
describe("Invoicing app dispatch (simulated service)", () => {
|
||||
it("denies every foreign-organization operation before reaching a service port", async () => {
|
||||
const { owner, port } = await fixture()
|
||||
for (const [name, fixture] of Object.entries(invoicingIsolationFixtures)) {
|
||||
const input = fixture.input({ orgSlug: "beta" } as never)
|
||||
await expect(owner.callByName(name, input)).rejects.toMatchObject({ code: "not_found" })
|
||||
}
|
||||
for (const method of Object.values(port)) expect(method).not.toHaveBeenCalled()
|
||||
})
|
||||
it("allows viewer reads but refuses fiscal writes and series administration", async () => {
|
||||
const { viewer, port } = await fixture()
|
||||
expect(await viewer.callByName("invoicing.invoice.list", { org: "alpha" })).toMatchObject({ rows: [{ invoiceId }] })
|
||||
for (const name of [
|
||||
"invoicing.invoice.draft",
|
||||
"invoicing.invoice.update",
|
||||
"invoicing.invoice.issue",
|
||||
"invoicing.invoice.credit",
|
||||
"invoicing.invoice.void",
|
||||
"invoicing.series.create",
|
||||
]) {
|
||||
const fixture = invoicingIsolationFixtures[name]
|
||||
if (!fixture) throw new Error("fixture missing")
|
||||
await expect(viewer.callByName(name, fixture.input({ orgSlug: "alpha" } as never))).rejects.toMatchObject({
|
||||
code: "not_found",
|
||||
})
|
||||
}
|
||||
expect(port.draft).not.toHaveBeenCalled()
|
||||
expect(port.transition).not.toHaveBeenCalled()
|
||||
expect(port.createSeries).not.toHaveBeenCalled()
|
||||
})
|
||||
it("routes an authorized draft with exact strings and a stable operation key", async () => {
|
||||
const { owner, port } = await fixture()
|
||||
const result = await owner.callByName("invoicing.invoice.draft", { org: "alpha", data: terms, requestId: id })
|
||||
expect(result).toMatchObject({ invoiceId, revision: row.revision })
|
||||
expect(port.draft).toHaveBeenCalledWith(terms, `invoicing:draft:${id}`)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,114 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// Actual generated SDK over synthetic HTTP; this is not live fiscal/provider acceptance.
|
||||
import { requestBusinessBinding } from "@conusai-kit/business"
|
||||
import { businessServiceHooks } from "@conusai-kit/business/server"
|
||||
import { Container, type ServiceAdmissionContext } from "@conusai-kit/core"
|
||||
import { afterEach, describe, expect, it, vi } from "vitest"
|
||||
import { sdkInvoicingService } from "../adapters/service.js"
|
||||
import { invoicingModule } from "../module.js"
|
||||
import { id, invoiceId, raw, terms } from "./fixture.js"
|
||||
|
||||
const bindingId = id
|
||||
function json(value: unknown) {
|
||||
return new Response(JSON.stringify(value).replaceAll('"RAW_REVISION"', "9007199254740993"), {
|
||||
headers: { "content-type": "application/json" },
|
||||
})
|
||||
}
|
||||
async function fixture(reply: (request: Request) => Response | Promise<Response>) {
|
||||
const requests: Request[] = []
|
||||
vi.stubGlobal("fetch", async (input: Request | string | URL) => {
|
||||
const request = new Request(input)
|
||||
requests.push(request.clone())
|
||||
return reply(request)
|
||||
})
|
||||
const container = new Container()
|
||||
const hooks = businessServiceHooks({
|
||||
env: { apiUrl: "https://platform.example.test/api", token: "synthetic-token", orgSlug: "fixture" },
|
||||
platformOrganizationId: 7,
|
||||
platformProjectId: 9,
|
||||
platformEnvironmentId: 11,
|
||||
profile: "postgres",
|
||||
controlPlane: {
|
||||
lookup: async (appTenantId) => ({
|
||||
partition_major: 1,
|
||||
binding_id: bindingId,
|
||||
organization_id: 7,
|
||||
project_id: 9,
|
||||
environment_id: 11,
|
||||
environment_uuid: id,
|
||||
app_tenant_id: appTenantId,
|
||||
}),
|
||||
observe: async () => {
|
||||
throw new Error("This fixture does not certify readiness")
|
||||
},
|
||||
},
|
||||
})
|
||||
const context: ServiceAdmissionContext = {
|
||||
container,
|
||||
module: "invoicing",
|
||||
procedure: "invoicing.invoice.list",
|
||||
kind: "read",
|
||||
scope: { level: "org", orgId: id, actor: { userId: id } },
|
||||
}
|
||||
await hooks.prepareServices?.(container, context, invoicingModule.serviceRequirements ?? [])
|
||||
return { service: sdkInvoicingService(requestBusinessBinding(container)), requests }
|
||||
}
|
||||
afterEach(() => vi.unstubAllGlobals())
|
||||
describe("Invoicing generated wire adapter", () => {
|
||||
it("preserves decimal and int64 digits through a real generated client", async () => {
|
||||
const { service, requests } = await fixture(() => json(raw()))
|
||||
const result = await service.get(invoiceId)
|
||||
expect(result.revision).toBe("9007199254740993")
|
||||
expect(result.data.lines[0]?.net_price.amount).toBe(terms.lines[0]?.net_price.amount)
|
||||
await service.transition("issue", { invoiceId, expectedVersion: result.revision, key: `fixture:${id}` })
|
||||
const body = await requests[1]?.text()
|
||||
expect(body).toContain('"revision":9007199254740993')
|
||||
expect(body).toContain(`fixture:${id}`)
|
||||
})
|
||||
it("denies a foreign app subject before issuing any network request", async () => {
|
||||
const { service, requests } = await fixture(() => json(raw()))
|
||||
await expect(
|
||||
service.draft(
|
||||
{
|
||||
...terms,
|
||||
buyer: {
|
||||
...terms.buyer,
|
||||
reference:
|
||||
"conusai://app-partitions/01900000-0000-7000-8000-000000000099/members/01900000-0000-7000-8000-000000000001",
|
||||
},
|
||||
},
|
||||
`fixture:${id}`,
|
||||
),
|
||||
).rejects.toMatchObject({ code: "bad_request" })
|
||||
expect(requests).toHaveLength(0)
|
||||
})
|
||||
it("returns bounded XML for explicit download instead of parsing it as JSON", async () => {
|
||||
const { service } = await fixture(
|
||||
() => new Response("<Invoice>synthetic</Invoice>", { headers: { "content-type": "application/xml" } }),
|
||||
)
|
||||
expect(await service.ubl(invoiceId)).toEqual({
|
||||
filename: `invoice-${invoiceId}.xml`,
|
||||
xml: "<Invoice>synthetic</Invoice>",
|
||||
})
|
||||
})
|
||||
it("maps service failures without returning arbitrary private remote details", async () => {
|
||||
const { service } = await fixture(
|
||||
() =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
type: "https://conusai.com/problems/conflict",
|
||||
title: "Conflict",
|
||||
status: 409,
|
||||
detail: "private fixture value",
|
||||
}),
|
||||
{ status: 409, headers: { "content-type": "application/problem+json" } },
|
||||
),
|
||||
)
|
||||
const refused = await service.get(invoiceId).then(
|
||||
() => undefined,
|
||||
(error: unknown) => error,
|
||||
)
|
||||
expect(refused).toMatchObject({ code: "business_conflict", status: 409 })
|
||||
expect(JSON.stringify((refused as { problem: unknown }).problem)).not.toContain("private fixture value")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user