Phase 2: core domain, policy engine, packet API, Clerical screen
Migration 0003: projects, aliases, renumbers, units, packets and every child table, change orders, assets, documents, ITPs, station limits, department weld flag. Policy package: Principal, PacketState, Action rules, custody, preconditions, CO hold, order of checks, can/blocked/holder on reads. docs/POLICY.md with the tightenings table for Seth. Write pipeline: FOR UPDATE, CAS on status and dept, If-Match row versions, server-assigned numbers, field CAS, version bump, packet log, audit, NOTIFY, signatures, IT reasons, filed lock and HMAC seal. API: /api/packets (header, transitions, work orders, pages, docs, move, remove, BOM, shorts, welds, heat tags, heat table, stage, checklist, clock, warehouse stage, Q.C., change orders, log, seal), /api/projects, /api/units, /api/assets, /_blob. Frontend: Clerical home, projects and units, packet page with pdf.js and QR codes, hand draft, heat map, send to warehouse, department or Q.C. Tests: policy matrix, API, concurrency, schema, projects; Playwright Clerical draft scenario. Verified on docker-1 with the CI stack and the Samba DC. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""The policy's view of a person (Principal) and of a packet (PacketState)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable, Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from types import MappingProxyType
|
||||
|
||||
from ..auth.groups import SHOP_LEVELS, parse_role
|
||||
|
||||
SUPPORT = ("CL", "WH", "QC")
|
||||
LEVELS: dict[str, int] = {level: i + 1 for i, level in enumerate(SHOP_LEVELS)} # worker 1, lead 2, supervisor 3
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Principal:
|
||||
user_id: str | None # uuid as text; None for system
|
||||
admin: bool = False
|
||||
support: frozenset[str] = frozenset() # codes among CL, WH, QC the user is a member of (any level)
|
||||
support_sup: frozenset[str] = frozenset() # codes among CL, WH, QC where the user is Supervisor
|
||||
shop: Mapping[str, int] = field(default_factory=lambda: MappingProxyType({})) # code -> 1 worker, 2 lead, 3 sup
|
||||
station_limits: frozenset[int] = frozenset() # station ids a worker is limited to; empty = any
|
||||
pm: bool = False
|
||||
session_kind: str = "password" # password | pin
|
||||
system: str | None = None # "visual-feed" for the worker; never from a session
|
||||
|
||||
def level(self, dept: str) -> int:
|
||||
"""Shop level in a department: 0 none, 1 worker, 2 lead, 3 supervisor."""
|
||||
return int(self.shop.get(dept, 0))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PacketState:
|
||||
id: int
|
||||
status: str # packet_status value
|
||||
dept: str # dept_code
|
||||
station_id: int | None
|
||||
welder_id: str | None # uuid text of the assigned welder
|
||||
wh_skipped: bool
|
||||
routed_to: str | None # databook | shipping | None
|
||||
vclosed: bool
|
||||
removed: bool
|
||||
source_kind: str # visual | hand
|
||||
weld_dept: bool # departments.weld
|
||||
co_hold: bool # an approved CO with pending verifications, or a pending CO with hold=true
|
||||
co_hold_why: str = "" # text for the error
|
||||
open_shorts: int = 0
|
||||
drawing_attached: bool = False
|
||||
heat_map: bool | None = None
|
||||
bom_total: int = 0 # lines not marked n/a
|
||||
bom_ready: int = 0 # lines pulled, or with an open short
|
||||
welds: int = 0
|
||||
checklist_complete: bool = False # every key of the department checklist is done (empty list is complete)
|
||||
filler_set: bool = False
|
||||
qc_missing: tuple[str, ...] = () # the old qcMissing() list, computed by the read model
|
||||
has_qc_sent: bool = False # qc_inspections row exists for the current stage
|
||||
|
||||
|
||||
SYSTEM_FEED = Principal(user_id=None, system="visual-feed")
|
||||
|
||||
|
||||
def principal_from_roles(
|
||||
user_id: str | None,
|
||||
roles: Iterable[str],
|
||||
station_limits: Iterable[int] = (),
|
||||
session_kind: str = "password",
|
||||
) -> Principal:
|
||||
"""Build the policy Principal from phase 1 role strings. A role that does not parse is ignored."""
|
||||
admin = False
|
||||
pm = False
|
||||
support: set[str] = set()
|
||||
support_sup: set[str] = set()
|
||||
shop: dict[str, int] = {}
|
||||
for text in roles:
|
||||
try:
|
||||
role = parse_role(text)
|
||||
except ValueError:
|
||||
continue
|
||||
if role.kind == "admin":
|
||||
admin = True
|
||||
elif role.kind == "pm":
|
||||
pm = True
|
||||
elif role.kind.startswith("shop_") and role.dept_code:
|
||||
level = LEVELS[role.kind.removeprefix("shop_")]
|
||||
shop[role.dept_code] = max(level, shop.get(role.dept_code, 0))
|
||||
elif role.dept_code in SUPPORT:
|
||||
support.add(role.dept_code)
|
||||
if role.kind.endswith("_supervisor"):
|
||||
support_sup.add(role.dept_code)
|
||||
return Principal(
|
||||
user_id=user_id,
|
||||
admin=admin,
|
||||
support=frozenset(support),
|
||||
support_sup=frozenset(support_sup),
|
||||
shop=MappingProxyType(shop),
|
||||
station_limits=frozenset(int(s) for s in station_limits),
|
||||
pm=pm,
|
||||
session_kind=session_kind,
|
||||
)
|
||||
Reference in New Issue
Block a user