CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
masterplan-lock / check (push) Successful in 1m3s
CI (SIAX Cloud) / security (push) Successful in 1m4s
Security Scan (SIAX Cloud) / security (push) Successful in 1m7s
CI (SIAX Cloud) / quality (push) Successful in 1m52s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 6s
CI (SIAX Cloud) / security (pull_request) Successful in 18s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 19s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m20s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m58s
103 lines
4.0 KiB
TypeScript
103 lines
4.0 KiB
TypeScript
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<OrderStatus, "delivered" | "cancelled">;
|
|
|
|
/** Kanoniskt flöde — servern äger resolutionen (ADR-0001), UI renderar. */
|
|
export const NEXT_STATUS: Record<AdvanceableStatus, OrderStatus> = {
|
|
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<Record<OrderStatus, { kind: "task" | "approval"; priority: "normal" | "high" }>> = {
|
|
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}` }],
|
|
});
|
|
}
|