import { createInboxItem, type InboxItem } from "@siax/contracts/inbox"; import { resolveAction, type ResolvedAction } from "@siax/contracts/authz-ux"; /** * Kanonisk orderstatus (ADR-0002, C3 2026-09-29): kodnivå-union som är den * ENDA sanningskällan för vilka statusar som existerar. DB-kolumnen är fortfarande * `text` (sql/001_init.sql line ~92) men guardas av sql/015_order_status_check.sql * (CHECK ... NOT VALID + VALIDATE i två steg, körs EJ automatiskt mot prod). * 'cancelled' ingår: tests/order-flow.test.ts behandlar den som terminal status. */ export type OrderStatus = | "draft" | "quoted" | "in_production" | "review" | "accepted" | "delivered" | "cancelled"; /** Uttömmande, kanonisk lista — SQL-constrainten i sql/015 MÅSTE spegla denna. */ export const ORDER_STATUSES = [ "draft", "quoted", "in_production", "review", "accepted", "delivered", "cancelled", ] as const satisfies readonly OrderStatus[]; /** Typ-vakt: fri text ur DB/payload klassas mot unionen (fail-closed mot read_only). */ export function isKnownOrderStatus(status: string): status is OrderStatus { return (ORDER_STATUSES as readonly string[]).includes(status); } /** Statusar som kan flyttas fram; delivered/cancelled är terminala. */ type AdvanceableStatus = Exclude; /** Kanoniskt flöde — servern äger resolutionen (ADR-0001), UI renderar. */ export const NEXT_STATUS: Record = { draft: "quoted", quoted: "in_production", in_production: "review", review: "accepted", accepted: "delivered", }; /** Guardad nästa-status: okänd/terminal status → null (aldrig påhittat steg). */ export function nextOrderStatus(status: string): OrderStatus | null { if (!isKnownOrderStatus(status)) return null; return NEXT_STATUS[status as AdvanceableStatus] ?? null; } /** * Server-renderad action-resolution för advance-steget (ADR-0001): * ägarskapenforces redan av requireOrderOwner på GET/PATCH; tillståndet * avgör read_only. Innan Session/roller finns i 0pt är permission alltid * granted för autentiserade ägare — hårdkoda aldrig denna resolve i klienten. * Okänd status (fri text ur DB innan sql/015-valideringen körts) → read_only. */ export function resolveAdvanceAction(status: string): ResolvedAction { if (nextOrderStatus(status)) { return resolveAction({ permission: "granted", entitlement: "active", approval: "not_required", readOnly: false }); } return resolveAction({ permission: "granted", entitlement: "active", approval: "not_required", readOnly: true }); } /** Orderstatus som utgör en inkorgspost (våg 3.3: orders kö = inbox). */ const INBOX_ELIGIBLE: Partial> = { draft: { kind: "task", priority: "normal" }, quoted: { kind: "task", priority: "normal" }, in_production: { kind: "task", priority: "normal" }, review: { kind: "approval", priority: "high" }, }; /** * Server-renderad inkorgspost (ADR-0001-mönstret): en order som väntar på * nästa steg projiceras på universal-inbox-konvolverna. Terminala statusar * (delivered/cancelled) och okända statusar ger ingen post — inboxen är * öppna arbetsobjekt. */ export function buildOrderInboxItem(order: { order_id: string; status: string; flow: string; total_ore?: number | null; }): InboxItem | null { const next = nextOrderStatus(order.status); const eligible = isKnownOrderStatus(order.status) ? INBOX_ELIGIBLE[order.status] : undefined; if (!eligible || !next) return null; return createInboxItem({ id: `order:${order.order_id}`, kind: eligible.kind, tenantId: "0pt", subject: `Order ${order.order_id.slice(0, 8)} — ${order.status} → ${next}`, priority: eligible.priority, createdAt: new Date().toISOString(), source: { system: "0pt", channel: "order-queue" }, entity: { type: "order", id: order.order_id }, actions: [{ id: "advance", label: `→ ${next}` }], }); }