// The inventory procedures end to end: the caller, the tenancy resolver, the module and the real // generated client against the business-services fake -- what a member sees, what the service gets, // and each refusal as the kit's app error (`businessAppError`) a page or a form shows. import { businessClient } from "@conusai-kit/business" import type { AppError } from "@conusai-kit/core" import { entitiesOf } from "@conusai-kit/data" import { TENANCY_ALSO_CHECK, tenancyIsolationExempt, tenancyIsolationFixtures } from "@conusai-kit/tenancy/testing" import { type IsolationTenant, runIsolation, snapshotOf } from "@conusai-kit/testing" import { inventoryCreateLocation, inventoryRecordMovement } from "@conusai-sdk/inventory" import { createClient } from "@conusai-sdk/inventory/client" import { describe, expect, it } from "vitest" import { projectReference } from "../core/inventory.js" import { BINDING_ID, inventoryApp } from "./app.js" import { inventoryIsolationFixtures } from "./isolation.js" const refusal = async (promise: Promise): Promise => { try { await promise } catch (error) { return error as AppError } throw new Error("the call was not refused") } const bolt = { sku: "BOLT-8", name: "Bolt M8", unit: "pcs" } /** Stock as the service receives it: a new location, then a receipt into it; answers the location's id. */ const receive = async (app: ReturnType, itemId: string, quantity: number): Promise => { const client = businessClient( { apiUrl: app.fake.apiUrl, token: app.fake.token, orgSlug: app.fake.orgSlug }, createClient, ) client.setConfig({ fetch: app.fake.fetch as typeof fetch }) const path = { org_slug: app.fake.orgSlug } const code = `STORE-${app.fake.inventory.locations.size + 1}` const location = await inventoryCreateLocation({ client, path, body: { code, name: "Store" } }) const locationId = location.data?.id as string const receipt = await inventoryRecordMovement({ client, path, body: { kind: "receipt", item_id: itemId, quantity: BigInt(quantity), to_location_id: locationId, idempotency_key: `receipt:${locationId}`, }, }) expect(receipt.response?.status).toBe(201) return locationId } describe("the inventory procedures", () => { it("create an item, list it, and read its stock", async () => { const app = inventoryApp() const t = await app.tenant("alpha") const created = (await t.call.callByName("inventory.item.create", { ...t.slugs, ...bolt })) as { itemId: string } expect(created).toMatchObject({ sku: "BOLT-8", name: "Bolt M8", unit: "pcs", active: true, description: null }) const listed = (await t.call.callByName("inventory.item.list", t.slugs)) as { rows: { itemId: string }[] after?: string } expect(listed.rows.map((row) => row.itemId)).toEqual([created.itemId]) expect(listed.after).toBeUndefined() await receive(app, created.itemId, 12) const stock = (await t.call.callByName("inventory.item.availability", { ...t.slugs, itemId: created.itemId })) as { availability: { onHand: number; available: number; locations: unknown[] } } expect(stock.availability).toMatchObject({ onHand: 12, available: 12 }) expect(stock.availability.locations).toHaveLength(1) }) it("reserve against the project's reference, list the project's reservations, and release them", async () => { const app = inventoryApp() const t = await app.tenant("alpha") const { itemId } = (await t.call.callByName("inventory.item.create", { ...t.slugs, ...bolt })) as { itemId: string } const locationId = await receive(app, itemId, 10) const requestId = crypto.randomUUID() const reserve = { ...t.slugs, itemId, locationId, quantity: 3, requestId } const reserved = (await t.call.callByName("inventory.stock.reserve", reserve)) as { reservationId: string } expect(reserved).toMatchObject({ quantity: 3, outstanding: 3, status: "open" }) // A retried request reserves once. expect(await t.call.callByName("inventory.stock.reserve", reserve)).toEqual(reserved) expect(app.fake.inventory.reservations.size).toBe(1) const held = [...app.fake.inventory.reservations.values()][0] expect(held?.reference).toBe(projectReference(BINDING_ID, t.projectId)) expect(held?.reference).toBe(`conusai://app-partitions/${BINDING_ID}/projects/${t.projectId}`) const listed = (await t.call.callByName("inventory.reservation.list", { ...t.slugs, status: "open" })) as { rows: unknown[] } expect(listed.rows).toEqual([reserved]) const released = await t.call.callByName("inventory.stock.release", { ...t.slugs, reservationId: reserved.reservationId, requestId: crypto.randomUUID(), }) expect(released).toMatchObject({ outstanding: 0, released: 3, status: "closed" }) }) it("keep one project's reservations from another project of the same organization", async () => { const app = inventoryApp() const t = await app.tenant("alpha") const other = (await t.call.callByName("tenancy.project.create", { org: t.orgSlug, workspace: t.workspaceSlug, slug: "other", name: "Other", moduleKey: "inventory", })) as { slug: string } const otherSlugs = { ...t.slugs, project: other.slug } const { itemId } = (await t.call.callByName("inventory.item.create", { ...t.slugs, ...bolt })) as { itemId: string } const locationId = await receive(app, itemId, 10) const mine = (await t.call.callByName("inventory.stock.reserve", { ...t.slugs, itemId, locationId, quantity: 1, requestId: crypto.randomUUID(), })) as { reservationId: string } expect(((await t.call.callByName("inventory.reservation.list", otherSlugs)) as { rows: unknown[] }).rows).toEqual( [], ) const refused = await refusal( t.call.callByName("inventory.stock.release", { ...otherSlugs, reservationId: mine.reservationId, requestId: crypto.randomUUID(), }), ) expect(refused.problem).toMatchObject({ status: 404, code: "not_found" }) expect(app.fake.inventory.reservations.get(mine.reservationId)?.outstanding).toBe(1) }) it("answer the service's refusals with the kit's codes, carrying none of the service's detail", async () => { const app = inventoryApp() const t = await app.tenant("alpha") await t.call.callByName("inventory.item.create", { ...t.slugs, ...bolt }) const taken = await refusal(t.call.callByName("inventory.item.create", { ...t.slugs, ...bolt })) expect(taken.problem).toMatchObject({ status: 409, code: "business_duplicate", title: "This already exists." }) expect(taken.problem.detail).toBeUndefined() const { itemId } = (await t.call.callByName("inventory.item.create", { ...t.slugs, ...bolt, sku: "NUT-8" })) as { itemId: string } const locationId = await receive(app, itemId, 1) const short = await refusal( t.call.callByName("inventory.stock.reserve", { ...t.slugs, itemId, locationId, quantity: 2, requestId: crypto.randomUUID(), }), ) expect(short.problem).toMatchObject({ status: 409, code: "business_insufficient_stock" }) }) it("answer a disabled service, a missing scope and a silent service with the kit's codes", async () => { const app = inventoryApp({ inventory: false }) const t = await app.tenant("alpha") const disabled = await refusal(t.call.callByName("inventory.item.list", t.slugs)) expect(disabled.problem).toMatchObject({ status: 404, code: "service_not_enabled" }) app.fake.enable("inventory") app.fake.revoke("inventory.items:write") const scoped = await refusal(t.call.callByName("inventory.item.create", { ...t.slugs, ...bolt })) expect(scoped.problem).toMatchObject({ status: 403, code: "service_permission_missing" }) expect(scoped.problem.detail).not.toContain("inventory.items:write") app.fake.refuseNext("network") const silent = await refusal(t.call.callByName("inventory.item.list", t.slugs)) expect(silent.problem).toMatchObject({ status: 503, code: "business_unavailable" }) expect(JSON.stringify([disabled.problem, scoped.problem, silent.problem])).not.toContain(app.fake.token) }) it("let a viewer read and refuse their write before the service is called", async () => { const app = inventoryApp() const t = await app.tenant("alpha") const viewer = await app.member(t, "viewer@alpha.test", "viewer") expect(await viewer.callByName("inventory.item.list", t.slugs)).toEqual({ rows: [] }) const before = app.fake.calls.length // A member without the permission is answered as a non-member is (kit-core's caller, spec ยง3.3). const refused = await refusal(viewer.callByName("inventory.item.create", { ...t.slugs, ...bolt })) expect(refused.problem.status).toBe(404) const reserve = await refusal( viewer.callByName("inventory.stock.reserve", { ...t.slugs, itemId: crypto.randomUUID(), locationId: crypto.randomUUID(), quantity: 1, requestId: crypto.randomUUID(), }), ) expect(reserve.problem.status).toBe(404) expect(app.fake.calls.length).toBe(before) }) it("refuse a body outside the contract before the service is called", async () => { const app = inventoryApp() const t = await app.tenant("alpha") const refused = await refusal(t.call.callByName("inventory.item.create", { ...t.slugs, ...bolt, unit: "Pieces" })) expect(refused.problem.status).toBe(422) expect(app.fake.calls).toEqual([]) }) }) describe("the inventory module's tenant isolation", () => { it("refuses every procedure called with another organization's slugs, and never reaches the service", async () => { const app = inventoryApp() const tenant = async (slug: string): Promise => { const t = await app.tenant(slug) return { orgSlug: t.orgSlug, workspaceSlug: t.workspaceSlug, projectSlug: t.projectSlug, orgId: t.orgId, workspaceId: t.workspaceId, projectId: t.projectId, userId: t.owner.userId, email: `owner@${slug}.test`, extra: {}, } } const a = await tenant("alpha") const b = await tenant("bravo") const result = await runIsolation({ registry: app.registry, seed: async () => ({ a, b }), caller: (actor) => app.caller(actor), snapshot: () => snapshotOf(app.data, entitiesOf(app.registry, "postgres"), { outbox: app.data.outbox }), fixtures: { ...tenancyIsolationFixtures(), ...inventoryIsolationFixtures }, alsoCheck: [...TENANCY_ALSO_CHECK], exempt: tenancyIsolationExempt, }) expect(result.failures).toEqual([]) for (const name of Object.keys(inventoryIsolationFixtures)) expect(result.passed, name).toContain(name) expect(app.fake.calls).toEqual([]) }) })