-- Durable Booking Excel extraction drafts and item-level human review. -- PostgreSQL 15+. Migrations 008-014 remain immutable. BEGIN; DO $$ BEGIN IF current_database() <> 'booking_test' THEN RAISE EXCEPTION 'ARR Booking review migration is allowed only in booking_test'; END IF; IF to_regclass('booking.current_source_batch') IS NULL OR to_regclass('booking.room_items') IS NULL OR to_regclass('ingestion.artifacts') IS NULL THEN RAISE EXCEPTION 'ARR migrations 008 through 014 must be applied first'; END IF; IF to_regclass('booking.extraction_drafts') IS NOT NULL OR to_regclass('booking.extraction_draft_items') IS NOT NULL THEN RAISE EXCEPTION 'ARR Booking review migration is already applied'; END IF; END; $$; CREATE TABLE booking.extraction_drafts ( draft_id text PRIMARY KEY CHECK ( draft_id ~ '^bookingdraft-[0-9a-f]{32}$' ), source_artifact_id bigint NOT NULL UNIQUE REFERENCES ingestion.artifacts(id), draft_status text NOT NULL CHECK ( draft_status IN ('reviewing', 'activated', 'superseded') ), processor_version text NOT NULL CHECK (btrim(processor_version) <> ''), rule_set_sha256 character(64) NOT NULL CHECK ( rule_set_sha256 ~ '^[0-9a-f]{64}$' ), source_sha256 character(64) NOT NULL CHECK ( source_sha256 ~ '^[0-9a-f]{64}$' ), source_byte_size bigint NOT NULL CHECK (source_byte_size > 0), source_rows integer NOT NULL CHECK (source_rows > 0), worksheet_count integer NOT NULL CHECK (worksheet_count > 0), distinct_group_codes integer NOT NULL CHECK (distinct_group_codes > 0), extracted_items integer NOT NULL CHECK (extracted_items > 0), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), activated_source_batch_id bigint UNIQUE REFERENCES booking.source_batches(id), activated_at timestamptz, CONSTRAINT extraction_drafts_status_shape CHECK ( ( draft_status = 'reviewing' AND activated_source_batch_id IS NULL AND activated_at IS NULL ) OR ( draft_status = 'activated' AND activated_source_batch_id IS NOT NULL AND activated_at IS NOT NULL ) OR ( draft_status = 'superseded' AND activated_source_batch_id IS NULL AND activated_at IS NULL ) ) ); CREATE INDEX extraction_drafts_status_created_idx ON booking.extraction_drafts (draft_status, created_at DESC); CREATE TABLE booking.extraction_draft_items ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, draft_id text NOT NULL REFERENCES booking.extraction_drafts(draft_id), source_worksheet text NOT NULL CHECK (btrim(source_worksheet) <> ''), source_row_no integer NOT NULL CHECK (source_row_no > 0), item_no integer NOT NULL CHECK (item_no > 0), group_code_raw text NOT NULL CHECK (btrim(group_code_raw) <> ''), group_code_key text GENERATED ALWAYS AS ( upper(btrim(group_code_raw)) ) STORED, hotel_raw text NOT NULL CHECK (btrim(hotel_raw) <> ''), room_type_raw text NOT NULL CHECK (btrim(room_type_raw) <> ''), room_type_code text CHECK ( room_type_code IS NULL OR btrim(room_type_code) <> '' ), quantity integer NOT NULL CHECK (quantity BETWEEN 1 AND 9999), review_status text NOT NULL CHECK ( review_status IN ('confirmed', 'pending', 'deleted') ), automatic boolean NOT NULL, source_fragment text NOT NULL DEFAULT '', created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT extraction_draft_items_position_unique UNIQUE ( draft_id, source_worksheet, source_row_no, item_no ), CONSTRAINT extraction_draft_items_review_shape CHECK ( (review_status = 'confirmed' AND room_type_code IS NOT NULL) OR (review_status = 'pending' AND room_type_code IS NULL) OR review_status = 'deleted' ) ); CREATE INDEX extraction_draft_items_draft_status_idx ON booking.extraction_draft_items (draft_id, review_status, id); CREATE INDEX extraction_draft_items_group_idx ON booking.extraction_draft_items (draft_id, group_code_key); COMMENT ON TABLE booking.extraction_drafts IS 'Private Booking Excel extraction drafts. A reviewing draft never changes the active accepted Booking source.'; COMMENT ON TABLE booking.extraction_draft_items IS 'Editable extracted room items. Pending items are excluded until confirmed; deleted items remain audit-visible.'; COMMIT;