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,771 @@
|
||||
-- 0003_domain: the core domain. Projects and units, packets and their child tables, change
|
||||
-- orders, documents and ITPs, plus the views v_units and v_open_cos. Runs as ps_migrate; 0001's
|
||||
-- default privileges give ps_app SELECT, INSERT, UPDATE, DELETE on every table here.
|
||||
-- Append-only tables revoke below. Text ID columns (work order base, lot, split, sub, part ids)
|
||||
-- stay text: leading zeros are kept and lot 7A occurs.
|
||||
|
||||
CREATE TYPE packet_status AS ENUM (
|
||||
'draft', 'warehouse', 'ready', 'assigned', 'in_progress', 'review', 'qc', 'complete', 'filed'
|
||||
);
|
||||
CREATE TYPE wh_stage AS ENUM ('incoming', 'pulling', 'hold', 'pulled');
|
||||
CREATE TYPE co_review AS ENUM ('pending', 'approved', 'rejected');
|
||||
CREATE TYPE co_origin AS ENUM ('visual', 'drawing', 'manual');
|
||||
CREATE TYPE source_kind AS ENUM ('visual', 'hand');
|
||||
CREATE TYPE project_status AS ENUM ('open', 'hold', 'closed');
|
||||
CREATE TYPE route_kind AS ENUM ('databook', 'shipping');
|
||||
CREATE TYPE drawing_state AS ENUM (
|
||||
'none', 'needed', 'attached', 'rev_mismatch', 'not_released', 'not_found', 'lookup_failed', 'hand'
|
||||
);
|
||||
CREATE TYPE itp_status AS ENUM ('open', 'signed', 'reopened');
|
||||
|
||||
-- ---------------------------------------------------------------- organisation
|
||||
|
||||
-- Weld departments get the welding checklist and need welds and filler before submit.
|
||||
ALTER TABLE departments ADD COLUMN weld bool NOT NULL DEFAULT false;
|
||||
UPDATE departments SET weld = true WHERE code IN ('03', '04', '05', '06', '07', '10');
|
||||
|
||||
-- A worker with rows here may only work on packets at these stations. No rows = any station.
|
||||
CREATE TABLE user_station_limits (
|
||||
user_id uuid NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
station_id bigint NOT NULL REFERENCES stations (id),
|
||||
PRIMARY KEY (user_id, station_id)
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------- projects and units
|
||||
|
||||
-- *_src says where a value came from: visual, hand, or null when unset. A hand value is never
|
||||
-- overwritten by the feed; *_visual keeps what VISUAL said beside it.
|
||||
CREATE TABLE projects (
|
||||
id bigserial PRIMARY KEY,
|
||||
number text NOT NULL,
|
||||
name text,
|
||||
customer text,
|
||||
po text,
|
||||
due date,
|
||||
status project_status NOT NULL DEFAULT 'open',
|
||||
notes text,
|
||||
source text NOT NULL DEFAULT 'hand' CHECK (source IN ('hand', 'visual')),
|
||||
needs_details bool NOT NULL DEFAULT false,
|
||||
customer_src text CHECK (customer_src IN ('visual', 'hand')),
|
||||
customer_visual text,
|
||||
po_src text CHECK (po_src IN ('visual', 'hand')),
|
||||
po_visual text,
|
||||
order_date date,
|
||||
order_date_src text CHECK (order_date_src IN ('visual', 'hand')),
|
||||
ship_date date,
|
||||
ship_date_src text CHECK (ship_date_src IN ('visual', 'hand')),
|
||||
version int NOT NULL DEFAULT 1,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_by uuid REFERENCES users (id),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
removed_at timestamptz,
|
||||
removed_by uuid REFERENCES users (id),
|
||||
removed_reason text
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX projects_number_live ON projects (upper(number)) WHERE removed_at IS NULL;
|
||||
|
||||
-- Every current and old number of every project and unit, upper-cased, so a lookup by any
|
||||
-- number resolves. unit_id is null for a project number. The unit_id FK is added after units.
|
||||
CREATE TABLE project_aliases (
|
||||
alias text PRIMARY KEY CHECK (alias = upper(alias)),
|
||||
project_id bigint NOT NULL REFERENCES projects (id),
|
||||
unit_id bigint,
|
||||
kind text NOT NULL CHECK (kind IN ('renumber', 'unit_renumber', 'import')),
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX project_aliases_project ON project_aliases (project_id);
|
||||
|
||||
-- lot is text: 1 to 3 characters, upper-cased, zero-padded to 3 when numeric (001, 7A).
|
||||
CREATE TABLE units (
|
||||
id bigserial PRIMARY KEY,
|
||||
project_id bigint NOT NULL REFERENCES projects (id),
|
||||
lot text NOT NULL CHECK (lot ~ '^[A-Z0-9]{1,3}$' AND (lot !~ '^[0-9]+$' OR length(lot) = 3)),
|
||||
name text,
|
||||
due date,
|
||||
status project_status NOT NULL DEFAULT 'open',
|
||||
notes text,
|
||||
customer_src text CHECK (customer_src IN ('visual', 'hand')),
|
||||
po_src text CHECK (po_src IN ('visual', 'hand')),
|
||||
order_date date,
|
||||
order_date_src text CHECK (order_date_src IN ('visual', 'hand')),
|
||||
ship_date date,
|
||||
ship_date_src text CHECK (ship_date_src IN ('visual', 'hand')),
|
||||
version int NOT NULL DEFAULT 1,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_by uuid REFERENCES users (id),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
removed_at timestamptz,
|
||||
removed_by uuid REFERENCES users (id),
|
||||
removed_reason text
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX units_project_lot_live ON units (project_id, lot) WHERE removed_at IS NULL;
|
||||
|
||||
ALTER TABLE project_aliases
|
||||
ADD CONSTRAINT project_aliases_unit_id_fkey FOREIGN KEY (unit_id) REFERENCES units (id);
|
||||
|
||||
CREATE TABLE project_renumbers (
|
||||
id bigserial PRIMARY KEY,
|
||||
at timestamptz NOT NULL DEFAULT now(),
|
||||
by uuid REFERENCES users (id),
|
||||
project_id bigint NOT NULL REFERENCES projects (id),
|
||||
unit_id bigint REFERENCES units (id),
|
||||
old_number text NOT NULL,
|
||||
new_number text NOT NULL,
|
||||
reason text
|
||||
);
|
||||
|
||||
-- The unit number is the project number plus the lot. A unit's own due date wins over the project's.
|
||||
CREATE VIEW v_units AS
|
||||
SELECT u.*,
|
||||
p.number || u.lot AS number,
|
||||
p.number AS project_number,
|
||||
p.name AS project_name,
|
||||
p.status AS project_status,
|
||||
coalesce(u.due, p.due) AS due_effective
|
||||
FROM units u
|
||||
JOIN projects p ON p.id = u.project_id;
|
||||
|
||||
-- ---------------------------------------------------------------- assets
|
||||
|
||||
-- One row per stored file. storage_key is the path under ASSETS_DIR. packet_id is the packet the
|
||||
-- file was uploaded for, if any; the attach rows (packet_pages, packet_docs) are the real links.
|
||||
CREATE TABLE assets (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
sha256 bytea NOT NULL,
|
||||
size bigint NOT NULL,
|
||||
mime text NOT NULL,
|
||||
original_name text,
|
||||
storage_key text NOT NULL UNIQUE,
|
||||
source text NOT NULL DEFAULT 'upload',
|
||||
source_ref jsonb,
|
||||
pages int,
|
||||
uploaded_by uuid REFERENCES users (id),
|
||||
uploaded_at timestamptz NOT NULL DEFAULT now(),
|
||||
packet_id bigint
|
||||
);
|
||||
|
||||
CREATE INDEX assets_sha256 ON assets (sha256);
|
||||
|
||||
-- ---------------------------------------------------------------- packets
|
||||
|
||||
-- version goes up in every write transaction (the pipeline does it, never a trigger).
|
||||
-- next_*_no are the counters for server-assigned child numbers, taken under the FOR UPDATE lock.
|
||||
CREATE TABLE packets (
|
||||
id bigserial PRIMARY KEY,
|
||||
unit_id bigint NOT NULL REFERENCES units (id),
|
||||
dept_code text NOT NULL REFERENCES departments (code),
|
||||
op text,
|
||||
title text,
|
||||
customer text,
|
||||
drawing text,
|
||||
rev text,
|
||||
wps text,
|
||||
status packet_status NOT NULL DEFAULT 'draft',
|
||||
status_at timestamptz NOT NULL DEFAULT now(),
|
||||
station_id bigint REFERENCES stations (id),
|
||||
welder_id uuid REFERENCES users (id),
|
||||
welder_stamp text,
|
||||
for_sup_id uuid REFERENCES users (id),
|
||||
heat_map bool,
|
||||
wh_skipped bool NOT NULL DEFAULT false,
|
||||
wh_sent_at timestamptz,
|
||||
routed_to route_kind,
|
||||
return_note text,
|
||||
vclosed_at timestamptz,
|
||||
vclosed_by uuid REFERENCES users (id),
|
||||
databook_at timestamptz,
|
||||
source_kind source_kind NOT NULL DEFAULT 'hand',
|
||||
source jsonb NOT NULL DEFAULT '{}',
|
||||
visual_key text,
|
||||
visual_rel date,
|
||||
visual_want date,
|
||||
drawing_state drawing_state NOT NULL DEFAULT 'none',
|
||||
flags text[] NOT NULL DEFAULT '{}',
|
||||
visual_fp_applied text,
|
||||
visual_closed_at timestamptz,
|
||||
visual jsonb NOT NULL DEFAULT '{}',
|
||||
next_weld_no int NOT NULL DEFAULT 1,
|
||||
next_heat_no int NOT NULL DEFAULT 1,
|
||||
next_page_no int NOT NULL DEFAULT 1,
|
||||
next_co_no int NOT NULL DEFAULT 1,
|
||||
version int NOT NULL DEFAULT 1,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_by uuid REFERENCES users (id),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
removed_at timestamptz,
|
||||
removed_by uuid REFERENCES users (id),
|
||||
removed_reason text
|
||||
);
|
||||
|
||||
-- One live packet per VISUAL key. A removed packet keeps its key so the feed never recreates it.
|
||||
CREATE UNIQUE INDEX packets_visual_key ON packets (visual_key)
|
||||
WHERE visual_key IS NOT NULL AND removed_at IS NULL;
|
||||
CREATE INDEX packets_status_live ON packets (status) WHERE removed_at IS NULL;
|
||||
CREATE INDEX packets_unit ON packets (unit_id);
|
||||
CREATE INDEX packets_dept_status ON packets (dept_code, status);
|
||||
|
||||
-- Sets updated_at only. A read never bumps version; the write pipeline bumps it explicitly.
|
||||
CREATE FUNCTION ps_packet_touch() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
NEW.updated_at := now();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER packets_touch
|
||||
BEFORE UPDATE ON packets
|
||||
FOR EACH ROW EXECUTE FUNCTION ps_packet_touch();
|
||||
|
||||
-- ---------------------------------------------------------------- change orders
|
||||
|
||||
-- One pending VISUAL change order per packet at a time; the feed re-aims it rather than adding one.
|
||||
CREATE TABLE change_orders (
|
||||
id bigserial PRIMARY KEY,
|
||||
packet_id bigint NOT NULL REFERENCES packets (id),
|
||||
co_no int NOT NULL,
|
||||
origin co_origin NOT NULL,
|
||||
review co_review NOT NULL DEFAULT 'pending',
|
||||
minor bool NOT NULL DEFAULT false,
|
||||
hold bool NOT NULL DEFAULT false,
|
||||
held_by uuid REFERENCES users (id),
|
||||
held_at timestamptz,
|
||||
ecn text,
|
||||
reason text,
|
||||
issued_at timestamptz DEFAULT now(),
|
||||
issued_by uuid REFERENCES users (id),
|
||||
issued_signature_id bigint REFERENCES signatures (id),
|
||||
approved_at timestamptz,
|
||||
approved_by uuid REFERENCES users (id),
|
||||
approved_signature_id bigint REFERENCES signatures (id),
|
||||
rejected_at timestamptz,
|
||||
rejected_by uuid REFERENCES users (id),
|
||||
rejected_reason text,
|
||||
rejected_signature_id bigint REFERENCES signatures (id),
|
||||
vis jsonb,
|
||||
waiting jsonb,
|
||||
staged jsonb,
|
||||
target_fp text,
|
||||
row_version int NOT NULL DEFAULT 1,
|
||||
UNIQUE (packet_id, co_no)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX change_orders_visual_pending ON change_orders (packet_id)
|
||||
WHERE origin = 'visual' AND review = 'pending';
|
||||
|
||||
CREATE TABLE co_items (
|
||||
id bigserial PRIMARY KEY,
|
||||
co_id bigint NOT NULL REFERENCES change_orders (id),
|
||||
old_page int,
|
||||
old_dwg text,
|
||||
old_rev text,
|
||||
new_page int,
|
||||
new_dwg text,
|
||||
new_rev text,
|
||||
added bool NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
CREATE INDEX co_items_co ON co_items (co_id);
|
||||
|
||||
CREATE TABLE co_required_depts (
|
||||
co_id bigint NOT NULL REFERENCES change_orders (id),
|
||||
dept_code text NOT NULL REFERENCES departments (code),
|
||||
PRIMARY KEY (co_id, dept_code)
|
||||
);
|
||||
|
||||
-- Insert-only. A primary key violation means the department already verified.
|
||||
CREATE TABLE co_verifications (
|
||||
co_id bigint NOT NULL REFERENCES change_orders (id),
|
||||
dept_code text NOT NULL REFERENCES departments (code),
|
||||
by uuid NOT NULL REFERENCES users (id),
|
||||
at timestamptz NOT NULL DEFAULT now(),
|
||||
note text,
|
||||
signature_id bigint NOT NULL REFERENCES signatures (id),
|
||||
it_reason text,
|
||||
PRIMARY KEY (co_id, dept_code)
|
||||
);
|
||||
|
||||
-- The old openCOs(): an approved change order that some required department has not verified,
|
||||
-- or a pending one Clerical put on hold. pending_depts lists the unverified departments, sorted.
|
||||
CREATE VIEW v_open_cos AS
|
||||
SELECT x.packet_id, x.co_id, x.co_no, x.review, x.hold, x.pending_depts
|
||||
FROM (
|
||||
SELECT co.packet_id,
|
||||
co.id AS co_id,
|
||||
co.co_no,
|
||||
co.review,
|
||||
co.hold,
|
||||
ARRAY(
|
||||
SELECT r.dept_code
|
||||
FROM co_required_depts r
|
||||
WHERE r.co_id = co.id
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM co_verifications v
|
||||
WHERE v.co_id = r.co_id AND v.dept_code = r.dept_code
|
||||
)
|
||||
ORDER BY r.dept_code
|
||||
) AS pending_depts
|
||||
FROM change_orders co
|
||||
WHERE co.review IN ('approved', 'pending')
|
||||
) x
|
||||
WHERE (x.review = 'approved' AND cardinality(x.pending_depts) > 0)
|
||||
OR (x.review = 'pending' AND x.hold);
|
||||
|
||||
-- ---------------------------------------------------------------- packet children
|
||||
|
||||
-- All key parts are text. lot, split and sub default to '' so the unique key works without nulls.
|
||||
CREATE TABLE packet_work_orders (
|
||||
id bigserial PRIMARY KEY,
|
||||
packet_id bigint NOT NULL REFERENCES packets (id),
|
||||
wo_base text NOT NULL,
|
||||
lot text NOT NULL DEFAULT '',
|
||||
split text NOT NULL DEFAULT '',
|
||||
sub text NOT NULL DEFAULT '',
|
||||
seq int,
|
||||
qty numeric,
|
||||
target_hrs numeric,
|
||||
op text,
|
||||
description text,
|
||||
visual_status text,
|
||||
missing_runs int NOT NULL DEFAULT 0,
|
||||
gone_at timestamptz,
|
||||
sort int NOT NULL DEFAULT 0,
|
||||
row_version int NOT NULL DEFAULT 1,
|
||||
UNIQUE (packet_id, wo_base, lot, split, sub)
|
||||
);
|
||||
|
||||
-- n comes from packets.next_page_no. A page replaced by a change order is voided, never deleted.
|
||||
CREATE TABLE packet_pages (
|
||||
id bigserial PRIMARY KEY,
|
||||
packet_id bigint NOT NULL REFERENCES packets (id),
|
||||
n int NOT NULL,
|
||||
type text NOT NULL CHECK (type IN ('drawing', 'info', 'overlay')),
|
||||
asset_id uuid REFERENCES assets (id),
|
||||
pn int NOT NULL DEFAULT 1,
|
||||
dwg text,
|
||||
rev text,
|
||||
co_id bigint REFERENCES change_orders (id),
|
||||
void_co_id bigint REFERENCES change_orders (id),
|
||||
void_at timestamptz,
|
||||
void_by uuid REFERENCES users (id),
|
||||
void_new_n int,
|
||||
row_version int NOT NULL DEFAULT 1,
|
||||
UNIQUE (packet_id, n)
|
||||
);
|
||||
|
||||
CREATE TABLE bom_lines (
|
||||
id bigserial PRIMARY KEY,
|
||||
packet_id bigint NOT NULL REFERENCES packets (id),
|
||||
item int NOT NULL,
|
||||
qty numeric,
|
||||
part_id text,
|
||||
description text,
|
||||
dwg_ref text,
|
||||
cl_item text,
|
||||
length text,
|
||||
material text,
|
||||
heat text,
|
||||
na bool NOT NULL DEFAULT false,
|
||||
issued numeric,
|
||||
loc text,
|
||||
pulled bool NOT NULL DEFAULT false,
|
||||
pulled_at timestamptz,
|
||||
pulled_by uuid REFERENCES users (id),
|
||||
mtr_asset_id uuid REFERENCES assets (id),
|
||||
src text CHECK (src IN ('visual', 'hand')),
|
||||
vkey text,
|
||||
whs text,
|
||||
comm text,
|
||||
cons text,
|
||||
sub text,
|
||||
grp text,
|
||||
sort int NOT NULL DEFAULT 0,
|
||||
row_version int NOT NULL DEFAULT 1,
|
||||
UNIQUE (packet_id, item)
|
||||
);
|
||||
|
||||
-- At most one open short per BOM line.
|
||||
CREATE TABLE bom_shorts (
|
||||
id bigserial PRIMARY KEY,
|
||||
packet_id bigint NOT NULL REFERENCES packets (id),
|
||||
bom_line_id bigint NOT NULL REFERENCES bom_lines (id) ON DELETE CASCADE,
|
||||
qty numeric NOT NULL,
|
||||
qty_open numeric NOT NULL,
|
||||
reason text,
|
||||
ref text,
|
||||
eta date,
|
||||
note text,
|
||||
opened_at timestamptz NOT NULL DEFAULT now(),
|
||||
opened_by uuid REFERENCES users (id),
|
||||
closed_at timestamptz,
|
||||
row_version int NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX bom_shorts_open ON bom_shorts (bom_line_id) WHERE closed_at IS NULL;
|
||||
CREATE INDEX bom_shorts_packet_open ON bom_shorts (packet_id) WHERE closed_at IS NULL;
|
||||
|
||||
CREATE TABLE short_receipts (
|
||||
id bigserial PRIMARY KEY,
|
||||
short_id bigint NOT NULL REFERENCES bom_shorts (id),
|
||||
packet_id bigint NOT NULL REFERENCES packets (id),
|
||||
qty numeric NOT NULL,
|
||||
heat text,
|
||||
note text,
|
||||
at timestamptz NOT NULL DEFAULT now(),
|
||||
by uuid REFERENCES users (id)
|
||||
);
|
||||
|
||||
CREATE INDEX short_receipts_short ON short_receipts (short_id);
|
||||
|
||||
-- One stage per visit to a shop department. The open stage has closed_at null.
|
||||
-- checklist maps key -> {done, at, by, stamp}.
|
||||
CREATE TABLE packet_stages (
|
||||
id bigserial PRIMARY KEY,
|
||||
packet_id bigint NOT NULL REFERENCES packets (id),
|
||||
dept_code text NOT NULL REFERENCES departments (code),
|
||||
station_id bigint REFERENCES stations (id),
|
||||
welder_id uuid REFERENCES users (id),
|
||||
welder_stamp text,
|
||||
checklist jsonb NOT NULL DEFAULT '{}',
|
||||
filler text,
|
||||
filler_lot text,
|
||||
preheat text,
|
||||
notes text,
|
||||
clock_elapsed_ms bigint NOT NULL DEFAULT 0,
|
||||
clock_running_since timestamptz,
|
||||
clock_started_at timestamptz,
|
||||
qc_snapshot jsonb,
|
||||
opened_at timestamptz NOT NULL DEFAULT now(),
|
||||
closed_at timestamptz,
|
||||
row_version int NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX packet_stages_open ON packet_stages (packet_id) WHERE closed_at IS NULL;
|
||||
CREATE INDEX packet_stages_packet ON packet_stages (packet_id, id);
|
||||
|
||||
-- weld_no comes from packets.next_weld_no. fx and fy are fractions of the page.
|
||||
CREATE TABLE welds (
|
||||
id bigserial PRIMARY KEY,
|
||||
packet_id bigint NOT NULL REFERENCES packets (id),
|
||||
weld_no int NOT NULL,
|
||||
page int,
|
||||
fx double precision,
|
||||
fy double precision,
|
||||
wo_base text,
|
||||
piece int,
|
||||
items int[] NOT NULL DEFAULT '{}',
|
||||
type text,
|
||||
size text,
|
||||
process text,
|
||||
stamp text,
|
||||
welder_id uuid REFERENCES users (id),
|
||||
dept_code text REFERENCES departments (code),
|
||||
stage_id bigint REFERENCES packet_stages (id),
|
||||
carried_from int,
|
||||
at timestamptz NOT NULL DEFAULT now(),
|
||||
by uuid REFERENCES users (id),
|
||||
row_version int NOT NULL DEFAULT 1,
|
||||
removed_at timestamptz,
|
||||
removed_by uuid REFERENCES users (id),
|
||||
removed_reason text
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX welds_packet_no_live ON welds (packet_id, weld_no) WHERE removed_at IS NULL;
|
||||
|
||||
-- tag_no comes from packets.next_heat_no.
|
||||
CREATE TABLE heat_tags (
|
||||
id bigserial PRIMARY KEY,
|
||||
packet_id bigint NOT NULL REFERENCES packets (id),
|
||||
tag_no int NOT NULL,
|
||||
page int,
|
||||
item int,
|
||||
fx double precision,
|
||||
fy double precision,
|
||||
carried_from int,
|
||||
at timestamptz NOT NULL DEFAULT now(),
|
||||
by uuid REFERENCES users (id),
|
||||
row_version int NOT NULL DEFAULT 1,
|
||||
removed_at timestamptz,
|
||||
removed_by uuid REFERENCES users (id),
|
||||
removed_reason text
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX heat_tags_packet_no_live ON heat_tags (packet_id, tag_no) WHERE removed_at IS NULL;
|
||||
|
||||
-- One heat table position per page; written as an idempotent upsert.
|
||||
CREATE TABLE heat_table_placements (
|
||||
id bigserial PRIMARY KEY,
|
||||
packet_id bigint NOT NULL REFERENCES packets (id),
|
||||
page int NOT NULL,
|
||||
x double precision,
|
||||
y double precision,
|
||||
s double precision,
|
||||
by uuid REFERENCES users (id),
|
||||
at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (packet_id, page)
|
||||
);
|
||||
|
||||
-- The warehouse stage history. The current row has closed_at null; a change closes it and inserts.
|
||||
CREATE TABLE packet_wh (
|
||||
id bigserial PRIMARY KEY,
|
||||
packet_id bigint NOT NULL REFERENCES packets (id),
|
||||
stage wh_stage NOT NULL,
|
||||
by uuid REFERENCES users (id),
|
||||
at timestamptz NOT NULL DEFAULT now(),
|
||||
reason text,
|
||||
note text,
|
||||
auto bool NOT NULL DEFAULT false,
|
||||
closed_at timestamptz
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX packet_wh_current ON packet_wh (packet_id) WHERE closed_at IS NULL;
|
||||
|
||||
-- Every status change. Replaces the old hist and sentBack lists.
|
||||
CREATE TABLE packet_transitions (
|
||||
id bigserial PRIMARY KEY,
|
||||
packet_id bigint NOT NULL REFERENCES packets (id),
|
||||
at timestamptz NOT NULL DEFAULT now(),
|
||||
by uuid REFERENCES users (id),
|
||||
action text NOT NULL,
|
||||
from_status packet_status,
|
||||
to_status packet_status,
|
||||
from_dept text,
|
||||
to_dept text,
|
||||
reason text,
|
||||
signature_id bigint REFERENCES signatures (id),
|
||||
it_reason text
|
||||
);
|
||||
|
||||
CREATE INDEX packet_transitions_packet ON packet_transitions (packet_id, id);
|
||||
|
||||
-- One open inspection per packet. weld_exam maps weld_no -> {result, note}.
|
||||
CREATE TABLE qc_inspections (
|
||||
id bigserial PRIMARY KEY,
|
||||
packet_id bigint NOT NULL REFERENCES packets (id),
|
||||
stage_id bigint REFERENCES packet_stages (id),
|
||||
sent_at timestamptz NOT NULL DEFAULT now(),
|
||||
sent_by uuid REFERENCES users (id),
|
||||
book bool,
|
||||
spool bool,
|
||||
location text,
|
||||
sent_note text,
|
||||
checks jsonb NOT NULL DEFAULT '{}',
|
||||
weld_exam jsonb NOT NULL DEFAULT '{}',
|
||||
nde_method text,
|
||||
nde_result text,
|
||||
nde_report text,
|
||||
hardness text,
|
||||
nace bool,
|
||||
inspector text,
|
||||
stamp text,
|
||||
notes text,
|
||||
result text CHECK (result IN ('accepted', 'rejected')),
|
||||
result_at timestamptz,
|
||||
result_by uuid REFERENCES users (id),
|
||||
result_note text,
|
||||
signature_id bigint REFERENCES signatures (id),
|
||||
row_version int NOT NULL DEFAULT 1,
|
||||
closed_at timestamptz
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX qc_inspections_open ON qc_inspections (packet_id) WHERE closed_at IS NULL;
|
||||
|
||||
CREATE TABLE qc_history (
|
||||
id bigserial PRIMARY KEY,
|
||||
packet_id bigint NOT NULL REFERENCES packets (id),
|
||||
at timestamptz NOT NULL DEFAULT now(),
|
||||
result text,
|
||||
inspector text,
|
||||
note text,
|
||||
dept_code text,
|
||||
snapshot jsonb
|
||||
);
|
||||
|
||||
CREATE INDEX qc_history_packet ON qc_history (packet_id, id);
|
||||
|
||||
-- HMAC seal written when a packet is filed. At most one unbroken seal per packet.
|
||||
CREATE TABLE packet_seals (
|
||||
id bigserial PRIMARY KEY,
|
||||
packet_id bigint NOT NULL REFERENCES packets (id),
|
||||
hash bytea NOT NULL,
|
||||
key_id text NOT NULL,
|
||||
sealed_at timestamptz NOT NULL DEFAULT now(),
|
||||
sealed_by uuid REFERENCES users (id),
|
||||
signature_id bigint REFERENCES signatures (id),
|
||||
broken_at timestamptz,
|
||||
broken_by uuid REFERENCES users (id),
|
||||
broken_reason text
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX packet_seals_unbroken ON packet_seals (packet_id) WHERE broken_at IS NULL;
|
||||
|
||||
-- The user-visible history. by_name and role are snapshots at the time of writing.
|
||||
CREATE TABLE packet_log (
|
||||
id bigserial PRIMARY KEY,
|
||||
packet_id bigint NOT NULL REFERENCES packets (id),
|
||||
at timestamptz NOT NULL DEFAULT now(),
|
||||
by uuid REFERENCES users (id),
|
||||
by_name text,
|
||||
role text,
|
||||
action text NOT NULL,
|
||||
note text,
|
||||
it_reason text,
|
||||
signature_id bigint REFERENCES signatures (id),
|
||||
transition_id bigint REFERENCES packet_transitions (id)
|
||||
);
|
||||
|
||||
CREATE INDEX packet_log_packet ON packet_log (packet_id, id);
|
||||
|
||||
-- What the feed last observed per leg. Written by the worker only.
|
||||
CREATE TABLE packet_visual_state (
|
||||
packet_id bigint PRIMARY KEY REFERENCES packets (id),
|
||||
legs jsonb NOT NULL DEFAULT '[]',
|
||||
ticket_hours numeric,
|
||||
order_ref jsonb,
|
||||
observed_at timestamptz
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------- documents and ITPs
|
||||
|
||||
CREATE TABLE packet_docs (
|
||||
id bigserial PRIMARY KEY,
|
||||
packet_id bigint NOT NULL REFERENCES packets (id),
|
||||
asset_id uuid NOT NULL REFERENCES assets (id),
|
||||
kind text NOT NULL,
|
||||
title text,
|
||||
added_at timestamptz NOT NULL DEFAULT now(),
|
||||
added_by uuid REFERENCES users (id),
|
||||
removed_at timestamptz,
|
||||
removed_by uuid REFERENCES users (id),
|
||||
removed_reason text
|
||||
);
|
||||
|
||||
CREATE INDEX packet_docs_packet ON packet_docs (packet_id) WHERE removed_at IS NULL;
|
||||
|
||||
-- Q.C. documents for a project or a unit. A new revision points the old row at the new one.
|
||||
CREATE TABLE qc_docs (
|
||||
id bigserial PRIMARY KEY,
|
||||
project_id bigint REFERENCES projects (id),
|
||||
unit_id bigint REFERENCES units (id),
|
||||
type text NOT NULL,
|
||||
rev text,
|
||||
asset_id uuid NOT NULL REFERENCES assets (id),
|
||||
auto bool NOT NULL DEFAULT false,
|
||||
superseded_by bigint REFERENCES qc_docs (id),
|
||||
added_at timestamptz NOT NULL DEFAULT now(),
|
||||
added_by uuid REFERENCES users (id)
|
||||
);
|
||||
|
||||
-- One ITP per unit. template is frozen when the ITP is made.
|
||||
CREATE TABLE itps (
|
||||
id bigserial PRIMARY KEY,
|
||||
unit_id bigint UNIQUE NOT NULL REFERENCES units (id),
|
||||
template jsonb NOT NULL,
|
||||
status itp_status NOT NULL DEFAULT 'open',
|
||||
signoff_at timestamptz,
|
||||
signoff_by uuid REFERENCES users (id),
|
||||
signoff_signature_id bigint REFERENCES signatures (id),
|
||||
signoff_asset_id uuid REFERENCES assets (id),
|
||||
reopen_at timestamptz,
|
||||
reopen_by uuid REFERENCES users (id),
|
||||
reopen_reason text,
|
||||
version int NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE itp_lines (
|
||||
id bigserial PRIMARY KEY,
|
||||
itp_id bigint NOT NULL REFERENCES itps (id),
|
||||
key text NOT NULL,
|
||||
result text CHECK (result IN ('approve', 'reject', 'na')),
|
||||
by uuid REFERENCES users (id),
|
||||
at timestamptz,
|
||||
ncr text,
|
||||
note text,
|
||||
signature_id bigint REFERENCES signatures (id),
|
||||
row_version int NOT NULL DEFAULT 1,
|
||||
UNIQUE (itp_id, key)
|
||||
);
|
||||
|
||||
-- Every reject keeps its NCR id, even after the line is approved later.
|
||||
CREATE TABLE itp_rejects (
|
||||
id bigserial PRIMARY KEY,
|
||||
itp_id bigint NOT NULL REFERENCES itps (id),
|
||||
line_key text NOT NULL,
|
||||
ncr text NOT NULL,
|
||||
note text,
|
||||
at timestamptz NOT NULL DEFAULT now(),
|
||||
by uuid REFERENCES users (id)
|
||||
);
|
||||
|
||||
CREATE TABLE itp_blocks (
|
||||
id bigserial PRIMARY KEY,
|
||||
itp_id bigint NOT NULL REFERENCES itps (id),
|
||||
block_key text NOT NULL,
|
||||
signed_at timestamptz NOT NULL DEFAULT now(),
|
||||
signed_by uuid REFERENCES users (id),
|
||||
signature_id bigint REFERENCES signatures (id),
|
||||
UNIQUE (itp_id, block_key)
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------- append-only records
|
||||
|
||||
CREATE TRIGGER project_renumbers_append_only
|
||||
BEFORE UPDATE OR DELETE ON project_renumbers
|
||||
FOR EACH ROW EXECUTE FUNCTION ps_append_only();
|
||||
CREATE TRIGGER project_renumbers_no_truncate
|
||||
BEFORE TRUNCATE ON project_renumbers
|
||||
FOR EACH STATEMENT EXECUTE FUNCTION ps_append_only();
|
||||
|
||||
CREATE TRIGGER short_receipts_append_only
|
||||
BEFORE UPDATE OR DELETE ON short_receipts
|
||||
FOR EACH ROW EXECUTE FUNCTION ps_append_only();
|
||||
CREATE TRIGGER short_receipts_no_truncate
|
||||
BEFORE TRUNCATE ON short_receipts
|
||||
FOR EACH STATEMENT EXECUTE FUNCTION ps_append_only();
|
||||
|
||||
CREATE TRIGGER packet_transitions_append_only
|
||||
BEFORE UPDATE OR DELETE ON packet_transitions
|
||||
FOR EACH ROW EXECUTE FUNCTION ps_append_only();
|
||||
CREATE TRIGGER packet_transitions_no_truncate
|
||||
BEFORE TRUNCATE ON packet_transitions
|
||||
FOR EACH STATEMENT EXECUTE FUNCTION ps_append_only();
|
||||
|
||||
CREATE TRIGGER qc_history_append_only
|
||||
BEFORE UPDATE OR DELETE ON qc_history
|
||||
FOR EACH ROW EXECUTE FUNCTION ps_append_only();
|
||||
CREATE TRIGGER qc_history_no_truncate
|
||||
BEFORE TRUNCATE ON qc_history
|
||||
FOR EACH STATEMENT EXECUTE FUNCTION ps_append_only();
|
||||
|
||||
CREATE TRIGGER packet_log_append_only
|
||||
BEFORE UPDATE OR DELETE ON packet_log
|
||||
FOR EACH ROW EXECUTE FUNCTION ps_append_only();
|
||||
CREATE TRIGGER packet_log_no_truncate
|
||||
BEFORE TRUNCATE ON packet_log
|
||||
FOR EACH STATEMENT EXECUTE FUNCTION ps_append_only();
|
||||
|
||||
CREATE TRIGGER itp_rejects_append_only
|
||||
BEFORE UPDATE OR DELETE ON itp_rejects
|
||||
FOR EACH ROW EXECUTE FUNCTION ps_append_only();
|
||||
CREATE TRIGGER itp_rejects_no_truncate
|
||||
BEFORE TRUNCATE ON itp_rejects
|
||||
FOR EACH STATEMENT EXECUTE FUNCTION ps_append_only();
|
||||
|
||||
CREATE TRIGGER co_verifications_append_only
|
||||
BEFORE UPDATE OR DELETE ON co_verifications
|
||||
FOR EACH ROW EXECUTE FUNCTION ps_append_only();
|
||||
CREATE TRIGGER co_verifications_no_truncate
|
||||
BEFORE TRUNCATE ON co_verifications
|
||||
FOR EACH STATEMENT EXECUTE FUNCTION ps_append_only();
|
||||
|
||||
-- Explicit, after the default privileges: the app may only read and append these.
|
||||
REVOKE UPDATE, DELETE, TRUNCATE ON project_renumbers, short_receipts, packet_transitions, qc_history,
|
||||
packet_log, itp_rejects, co_verifications FROM ps_app;
|
||||
GRANT SELECT, INSERT ON project_renumbers, short_receipts, packet_transitions, qc_history,
|
||||
packet_log, itp_rejects, co_verifications TO ps_app;
|
||||
-- Views are read-only to the app.
|
||||
REVOKE INSERT, UPDATE, DELETE, TRUNCATE ON v_units, v_open_cos FROM ps_app;
|
||||
GRANT SELECT ON v_units, v_open_cos TO ps_app;
|
||||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ps_app;
|
||||
Reference in New Issue
Block a user