-- Shared booking facts + Finance processing persistence schema v1.1
-- PostgreSQL 15+
-- Target database: booking_test (connect to that database before running).
-- REVIEW DRAFT ONLY: this file has not been executed against any database.

BEGIN;

CREATE SCHEMA booking;
CREATE SCHEMA finance;

COMMENT ON SCHEMA booking IS
  'Shared simulated booking attachment inputs, Agent parse versions, and current booking facts.';

COMMENT ON SCHEMA finance IS
  'Finance-owned Opera daily facts and report-version lineage derived from shared booking facts.';

CREATE TABLE booking.file_objects (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    file_kind text NOT NULL CHECK (file_kind IN (
        'booking_excel',
        'opera_xml',
        'daily_xlsx',
        'monthly_xlsx',
        'company_ten_day_xlsx',
        'exception_xlsx',
        'result_json'
    )),
    original_filename text NOT NULL CHECK (btrim(original_filename) <> ''),
    storage_key text NOT NULL UNIQUE,
    sha256 character(64) NOT NULL CHECK (sha256 ~ '^[0-9a-f]{64}$'),
    byte_size bigint NOT NULL CHECK (byte_size >= 0),
    mime_type text,
    created_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT file_objects_storage_key_is_relative CHECK (
        storage_key !~ '^/'
        AND storage_key !~ '(^|/)\.\.(/|$)'
    ),
    CONSTRAINT file_objects_kind_hash_unique UNIQUE (file_kind, sha256)
);

CREATE TABLE booking.booking_imports (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    source_file_id bigint NOT NULL UNIQUE
        REFERENCES booking.file_objects(id),
    source_sheet_name text NOT NULL CHECK (btrim(source_sheet_name) <> ''),
    import_status text NOT NULL CHECK (import_status IN (
        'processing', 'accepted', 'failed'
    )),
    source_rows integer NOT NULL DEFAULT 0 CHECK (source_rows >= 0),
    accepted_rows integer NOT NULL DEFAULT 0 CHECK (accepted_rows >= 0),
    failed_rows integer NOT NULL DEFAULT 0 CHECK (failed_rows >= 0),
    failure_code text,
    failure_message text,
    created_at timestamptz NOT NULL DEFAULT now(),
    finished_at timestamptz,
    CONSTRAINT booking_import_counts_valid CHECK (
        accepted_rows + failed_rows <= source_rows
    ),
    CONSTRAINT booking_import_failure_shape CHECK (
        (import_status = 'failed' AND failure_code IS NOT NULL)
        OR (import_status <> 'failed')
    )
);

CREATE TABLE booking.booking_source_rows (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    import_id bigint NOT NULL
        REFERENCES booking.booking_imports(id),
    source_row_no integer NOT NULL CHECK (source_row_no > 0),
    tour_code_raw text NOT NULL CHECK (btrim(tour_code_raw) <> ''),
    group_code_key text GENERATED ALWAYS AS (upper(btrim(tour_code_raw))) STORED,
    hotel_raw text NOT NULL CHECK (btrim(hotel_raw) <> ''),
    row_sha256 character(64) NOT NULL CHECK (row_sha256 ~ '^[0-9a-f]{64}$'),
    created_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT booking_source_rows_position_unique
        UNIQUE (import_id, source_row_no),
    CONSTRAINT booking_source_rows_group_unique
        UNIQUE (import_id, group_code_key),
    CONSTRAINT booking_source_rows_hash_unique
        UNIQUE (import_id, row_sha256),
    CONSTRAINT booking_source_rows_id_group_unique
        UNIQUE (id, group_code_key)
);

CREATE INDEX booking_source_rows_group_idx
    ON booking.booking_source_rows (group_code_key);

CREATE TABLE booking.booking_parse_versions (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    source_row_id bigint NOT NULL,
    group_code_key text NOT NULL CHECK (
        group_code_key = upper(btrim(group_code_key))
        AND group_code_key <> ''
    ),
    version_no integer NOT NULL CHECK (version_no > 0),
    parse_status text NOT NULL CHECK (parse_status IN (
        'accepted', 'needs_review', 'failed'
    )),
    booking_state text NOT NULL DEFAULT 'active' CHECK (booking_state IN (
        'active', 'cancelled'
    )),
    agent_name text NOT NULL CHECK (btrim(agent_name) <> ''),
    agent_version text NOT NULL CHECK (btrim(agent_version) <> ''),
    result_sha256 character(64) CHECK (
        result_sha256 IS NULL OR result_sha256 ~ '^[0-9a-f]{64}$'
    ),
    result_json jsonb NOT NULL DEFAULT '{}'::jsonb,
    failure_code text,
    failure_message text,
    created_at timestamptz NOT NULL DEFAULT now(),
    validated_at timestamptz,
    CONSTRAINT booking_parse_version_unique
        UNIQUE (group_code_key, version_no),
    CONSTRAINT booking_parse_id_group_unique
        UNIQUE (id, group_code_key),
    CONSTRAINT booking_parse_source_group_fk
        FOREIGN KEY (source_row_id, group_code_key)
        REFERENCES booking.booking_source_rows(id, group_code_key),
    CONSTRAINT booking_parse_status_shape CHECK (
        (parse_status = 'accepted'
            AND result_sha256 IS NOT NULL
            AND validated_at IS NOT NULL
            AND failure_code IS NULL)
        OR (parse_status = 'needs_review')
        OR (parse_status = 'failed' AND failure_code IS NOT NULL)
    )
);

CREATE INDEX booking_parse_versions_source_row_idx
    ON booking.booking_parse_versions (source_row_id);

CREATE TABLE booking.booking_stays (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    parse_version_id bigint NOT NULL
        REFERENCES booking.booking_parse_versions(id),
    segment_no integer NOT NULL CHECK (segment_no > 0),
    arrival_date date NOT NULL,
    departure_date date NOT NULL,
    nights integer GENERATED ALWAYS AS (departure_date - arrival_date) STORED,
    source_fragment text,
    CONSTRAINT booking_stays_dates_valid CHECK (departure_date > arrival_date),
    CONSTRAINT booking_stays_segment_unique
        UNIQUE (parse_version_id, segment_no)
);

CREATE INDEX booking_stays_dates_idx
    ON booking.booking_stays (arrival_date, departure_date);

CREATE TABLE booking.booking_room_items (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    stay_id bigint NOT NULL
        REFERENCES booking.booking_stays(id),
    item_no integer NOT NULL CHECK (item_no > 0),
    room_type_raw text NOT NULL CHECK (btrim(room_type_raw) <> ''),
    room_type_code text NOT NULL CHECK (btrim(room_type_code) <> ''),
    quantity integer NOT NULL CHECK (quantity > 0),
    unit_price numeric(18,2) CHECK (unit_price IS NULL OR unit_price >= 0),
    price_token_raw text,
    source_fragment text,
    CONSTRAINT booking_room_items_position_unique
        UNIQUE (stay_id, item_no)
);

CREATE INDEX booking_room_items_type_idx
    ON booking.booking_room_items (room_type_code);

CREATE TABLE booking.current_group_bookings (
    group_code_key text PRIMARY KEY CHECK (
        group_code_key = upper(btrim(group_code_key))
        AND group_code_key <> ''
    ),
    parse_version_id bigint NOT NULL,
    activated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT current_group_booking_parse_fk
        FOREIGN KEY (parse_version_id, group_code_key)
        REFERENCES booking.booking_parse_versions(id, group_code_key)
);

CREATE OR REPLACE FUNCTION booking.validate_current_group_booking()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
    IF NOT EXISTS (
        SELECT 1
        FROM booking.booking_parse_versions AS parsed
        WHERE parsed.id = NEW.parse_version_id
          AND parsed.group_code_key = NEW.group_code_key
          AND parsed.parse_status = 'accepted'
    ) THEN
        RAISE EXCEPTION
            'current Group Code must reference an accepted parse version';
    END IF;
    RETURN NEW;
END;
$$;

CREATE TRIGGER current_group_booking_acceptance_guard
BEFORE INSERT OR UPDATE ON booking.current_group_bookings
FOR EACH ROW
EXECUTE FUNCTION booking.validate_current_group_booking();

CREATE TABLE finance.daily_versions (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    business_date date,
    version_no integer,
    source_file_id bigint NOT NULL
        REFERENCES booking.file_objects(id),
    version_status text NOT NULL CHECK (version_status IN (
        'processing', 'failed', 'validated', 'active', 'superseded'
    )),
    processor_version text NOT NULL CHECK (btrim(processor_version) <> ''),
    rule_set_sha256 character(64) CHECK (
        rule_set_sha256 IS NULL OR rule_set_sha256 ~ '^[0-9a-f]{64}$'
    ),
    result_schema_version text NOT NULL DEFAULT '1.0',
    source_rows integer NOT NULL DEFAULT 0 CHECK (source_rows >= 0),
    removed_by_rate_code integer NOT NULL DEFAULT 0
        CHECK (removed_by_rate_code >= 0),
    removed_as_duplicates integer NOT NULL DEFAULT 0
        CHECK (removed_as_duplicates >= 0),
    output_rows integer NOT NULL DEFAULT 0 CHECK (output_rows >= 0),
    daily_report_file_id bigint REFERENCES booking.file_objects(id),
    result_json_file_id bigint REFERENCES booking.file_objects(id),
    exception_report_file_id bigint REFERENCES booking.file_objects(id),
    failure_code text,
    failure_message text,
    created_at timestamptz NOT NULL DEFAULT now(),
    validated_at timestamptz,
    activated_at timestamptz,
    superseded_at timestamptz,
    CONSTRAINT daily_version_date_number_shape CHECK (
        (business_date IS NULL AND version_no IS NULL)
        OR (business_date IS NOT NULL AND version_no > 0)
    ),
    CONSTRAINT daily_version_business_version_unique
        UNIQUE (business_date, version_no),
    CONSTRAINT daily_version_id_date_unique
        UNIQUE (id, business_date),
    CONSTRAINT daily_version_success_counts CHECK (
        version_status NOT IN ('validated', 'active', 'superseded')
        OR source_rows = removed_by_rate_code + removed_as_duplicates + output_rows
    ),
    CONSTRAINT daily_version_active_shape CHECK (
        version_status <> 'active'
        OR (
            business_date IS NOT NULL
            AND validated_at IS NOT NULL
            AND activated_at IS NOT NULL
            AND daily_report_file_id IS NOT NULL
            AND result_json_file_id IS NOT NULL
            AND failure_code IS NULL
        )
    ),
    CONSTRAINT daily_version_failed_shape CHECK (
        version_status <> 'failed' OR failure_code IS NOT NULL
    )
);

CREATE INDEX daily_versions_business_date_idx
    ON finance.daily_versions (business_date, version_no DESC);

CREATE TABLE finance.daily_records (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    daily_version_id bigint NOT NULL
        REFERENCES finance.daily_versions(id),
    source_sequence integer NOT NULL CHECK (source_sequence > 0),
    source_location text,
    outcome text NOT NULL CHECK (outcome IN (
        'retained',
        'excluded_rate_code',
        'duplicate',
        'validation_failed',
        'price_unmatched'
    )),
    decision_codes text[] NOT NULL DEFAULT ARRAY[]::text[],
    duplicate_of_record_id bigint,

    block_code text,
    group_code_key text CHECK (
        group_code_key IS NULL
        OR (
            group_code_key = upper(btrim(group_code_key))
            AND group_code_key <> ''
        )
    ),
    adults integer,
    children integer,
    company_name text,
    company_key text,
    confirmation_no text,
    disp_room_no text,
    effective_rate_amount numeric(18,2),
    full_name text,
    res_comment text,
    trace_text text,
    no_of_rooms integer,
    products text,
    rate_code text,
    normalized_rate_code text,
    room_category_label text,
    arrival date,
    departure date,
    nights integer,
    real_price numeric(18,2),
    total_price numeric(18,2),
    kb_amount numeric(18,2),
    channel_key text,
    pricing_method text,

    booking_parse_version_id bigint,
    booking_match_status text NOT NULL DEFAULT 'not_required'
        CHECK (booking_match_status IN (
            'matched',
            'unmatched',
            'parse_failed',
            'missing_group_code',
            'not_required'
        )),
    created_at timestamptz NOT NULL DEFAULT now(),

    CONSTRAINT daily_records_source_unique
        UNIQUE (daily_version_id, source_sequence),
    CONSTRAINT daily_records_id_version_unique
        UNIQUE (id, daily_version_id),
    CONSTRAINT daily_records_duplicate_version_fk
        FOREIGN KEY (duplicate_of_record_id, daily_version_id)
        REFERENCES finance.daily_records(id, daily_version_id),
    CONSTRAINT daily_records_booking_group_fk
        FOREIGN KEY (booking_parse_version_id, group_code_key)
        REFERENCES booking.booking_parse_versions(id, group_code_key),
    CONSTRAINT daily_records_people_valid CHECK (
        (adults IS NULL OR adults >= 0)
        AND (children IS NULL OR children >= 0)
    ),
    CONSTRAINT daily_records_room_count_valid CHECK (
        no_of_rooms IS NULL OR no_of_rooms > 0
    ),
    CONSTRAINT daily_records_retained_shape CHECK (
        outcome <> 'retained'
        OR (
            rate_code IS NOT NULL
            AND btrim(rate_code) <> ''
            AND normalized_rate_code IS NOT NULL
            AND btrim(normalized_rate_code) <> ''
            AND company_name IS NOT NULL
            AND btrim(company_name) <> ''
            AND company_key IS NOT NULL
            AND btrim(company_key) <> ''
            AND confirmation_no IS NOT NULL
            AND btrim(confirmation_no) <> ''
            AND disp_room_no IS NOT NULL
            AND btrim(disp_room_no) <> ''
            AND effective_rate_amount IS NOT NULL
            AND effective_rate_amount >= 0
            AND full_name IS NOT NULL
            AND btrim(full_name) <> ''
            AND adults IS NOT NULL
            AND children IS NOT NULL
            AND no_of_rooms IS NOT NULL
            AND arrival IS NOT NULL
            AND departure IS NOT NULL
            AND nights IS NOT NULL
            AND real_price IS NOT NULL
            AND real_price >= 0
            AND total_price IS NOT NULL
            AND total_price >= 0
            AND channel_key IS NOT NULL
            AND btrim(channel_key) <> ''
            AND pricing_method IS NOT NULL
            AND btrim(pricing_method) <> ''
        )
    ),
    CONSTRAINT daily_records_dates_and_nights_valid CHECK (
        arrival IS NULL
        OR departure IS NULL
        OR nights IS NULL
        OR (
            departure > arrival
            AND nights = departure - arrival
        )
    ),
    CONSTRAINT daily_records_total_valid CHECK (
        real_price IS NULL
        OR no_of_rooms IS NULL
        OR nights IS NULL
        OR total_price IS NULL
        OR total_price = real_price * no_of_rooms * nights
    ),
    CONSTRAINT daily_records_kb_valid CHECK (
        kb_amount IS NULL
        OR no_of_rooms IS NULL
        OR kb_amount = no_of_rooms * 100
    ),
    CONSTRAINT daily_records_duplicate_shape CHECK (
        (outcome = 'duplicate' AND duplicate_of_record_id IS NOT NULL)
        OR (outcome <> 'duplicate' AND duplicate_of_record_id IS NULL)
    ),
    CONSTRAINT daily_records_booking_match_shape CHECK (
        (booking_match_status = 'matched'
            AND group_code_key IS NOT NULL
            AND booking_parse_version_id IS NOT NULL)
        OR (booking_match_status = 'unmatched'
            AND group_code_key IS NOT NULL
            AND booking_parse_version_id IS NULL)
        OR (booking_match_status = 'parse_failed'
            AND group_code_key IS NOT NULL)
        OR (booking_match_status = 'missing_group_code'
            AND group_code_key IS NULL
            AND booking_parse_version_id IS NULL)
        OR (booking_match_status = 'not_required'
            AND booking_parse_version_id IS NULL)
    )
);

CREATE UNIQUE INDEX daily_records_retained_dedupe_key
    ON finance.daily_records (daily_version_id, disp_room_no, arrival)
    WHERE outcome = 'retained';

CREATE INDEX daily_records_group_code_idx
    ON finance.daily_records (group_code_key);

CREATE INDEX daily_records_company_date_idx
    ON finance.daily_records (company_key, arrival);

CREATE INDEX daily_records_version_outcome_idx
    ON finance.daily_records (daily_version_id, outcome);

CREATE TABLE finance.daily_channel_metrics (
    daily_version_id bigint NOT NULL
        REFERENCES finance.daily_versions(id),
    worksheet text NOT NULL CHECK (btrim(worksheet) <> ''),
    row_count integer NOT NULL CHECK (row_count >= 0),
    PRIMARY KEY (daily_version_id, worksheet)
);

CREATE TABLE finance.current_daily_versions (
    business_date date PRIMARY KEY,
    daily_version_id bigint NOT NULL,
    activated_at timestamptz NOT NULL DEFAULT now(),
    CONSTRAINT current_daily_version_fk
        FOREIGN KEY (daily_version_id, business_date)
        REFERENCES finance.daily_versions(id, business_date)
);

CREATE OR REPLACE FUNCTION finance.validate_current_daily_version()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
    IF NOT EXISTS (
        SELECT 1
        FROM finance.daily_versions AS version
        WHERE version.id = NEW.daily_version_id
          AND version.business_date = NEW.business_date
          AND version.version_status = 'active'
    ) THEN
        RAISE EXCEPTION
            'current business date must reference an active daily version';
    END IF;
    RETURN NEW;
END;
$$;

CREATE TRIGGER current_daily_version_active_guard
BEFORE INSERT OR UPDATE ON finance.current_daily_versions
FOR EACH ROW
EXECUTE FUNCTION finance.validate_current_daily_version();

CREATE TABLE finance.report_versions (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    report_kind text NOT NULL CHECK (report_kind IN (
        'monthly_channel', 'company_ten_day'
    )),
    period_start date NOT NULL,
    period_end date NOT NULL,
    company_scope text NOT NULL DEFAULT '*',
    version_no integer NOT NULL CHECK (version_no > 0),
    report_status text NOT NULL CHECK (report_status IN (
        'generating', 'failed', 'validated', 'active', 'superseded'
    )),
    artifact_file_id bigint REFERENCES booking.file_objects(id),
    result_json_file_id bigint REFERENCES booking.file_objects(id),
    is_current boolean NOT NULL DEFAULT false,
    failure_code text,
    failure_message text,
    created_at timestamptz NOT NULL DEFAULT now(),
    validated_at timestamptz,
    activated_at timestamptz,
    superseded_at timestamptz,
    CONSTRAINT report_period_valid CHECK (period_end >= period_start),
    CONSTRAINT report_version_scope_unique UNIQUE (
        report_kind, period_start, period_end, company_scope, version_no
    ),
    CONSTRAINT report_current_shape CHECK (
        NOT is_current
        OR (
            report_status = 'active'
            AND artifact_file_id IS NOT NULL
            AND validated_at IS NOT NULL
            AND activated_at IS NOT NULL
            AND failure_code IS NULL
        )
    ),
    CONSTRAINT report_failed_shape CHECK (
        report_status <> 'failed' OR failure_code IS NOT NULL
    )
);

CREATE UNIQUE INDEX report_versions_one_current_scope
    ON finance.report_versions (
        report_kind, period_start, period_end, company_scope
    )
    WHERE is_current;

CREATE TABLE finance.report_daily_versions (
    report_version_id bigint NOT NULL
        REFERENCES finance.report_versions(id),
    business_date date NOT NULL,
    daily_version_id bigint NOT NULL,
    PRIMARY KEY (report_version_id, business_date),
    CONSTRAINT report_daily_version_fk
        FOREIGN KEY (daily_version_id, business_date)
        REFERENCES finance.daily_versions(id, business_date)
);

CREATE TABLE finance.report_booking_versions (
    report_version_id bigint NOT NULL
        REFERENCES finance.report_versions(id),
    group_code_key text NOT NULL,
    booking_parse_version_id bigint NOT NULL,
    PRIMARY KEY (report_version_id, group_code_key),
    CONSTRAINT report_booking_version_fk
        FOREIGN KEY (booking_parse_version_id, group_code_key)
        REFERENCES booking.booking_parse_versions(id, group_code_key)
);

CREATE VIEW booking.v_current_group_room_items AS
SELECT
    current_booking.group_code_key,
    source_row.tour_code_raw,
    parsed.id AS parse_version_id,
    parsed.version_no AS parse_version_no,
    parsed.booking_state,
    source_row.import_id,
    source_row.source_row_no,
    stay.segment_no,
    stay.arrival_date,
    stay.departure_date,
    stay.nights,
    room.item_no,
    room.room_type_raw,
    room.room_type_code,
    room.quantity,
    room.unit_price,
    room.price_token_raw
FROM booking.current_group_bookings AS current_booking
JOIN booking.booking_parse_versions AS parsed
  ON parsed.id = current_booking.parse_version_id
JOIN booking.booking_source_rows AS source_row
  ON source_row.id = parsed.source_row_id
JOIN booking.booking_stays AS stay
  ON stay.parse_version_id = parsed.id
JOIN booking.booking_room_items AS room
  ON room.stay_id = stay.id;

CREATE VIEW finance.v_active_daily_facts AS
SELECT records.*
FROM finance.current_daily_versions AS current_version
JOIN finance.daily_records AS records
  ON records.daily_version_id = current_version.daily_version_id
WHERE records.outcome = 'retained';

CREATE VIEW finance.v_company_report_source AS
SELECT
    current_version.business_date,
    records.id AS daily_record_id,
    records.channel_key,
    records.company_key,
    records.company_name,
    records.block_code,
    records.group_code_key,
    records.res_comment,
    records.room_category_label AS type_of_room,
    records.no_of_rooms AS actual_room_quantity,
    records.arrival,
    records.departure,
    records.nights,
    records.real_price,
    records.total_price,
    records.booking_parse_version_id,
    records.booking_match_status
FROM finance.current_daily_versions AS current_version
JOIN finance.daily_records AS records
  ON records.daily_version_id = current_version.daily_version_id
WHERE records.outcome = 'retained';

COMMENT ON VIEW booking.v_current_group_room_items IS
  'Exact Group Code lookup source for Agent-formatted original room information.';

COMMENT ON VIEW finance.v_active_daily_facts IS
  'Only retained rows from each business date current active daily version.';

COMMENT ON VIEW finance.v_company_report_source IS
  'Privacy-minimized daily facts used to build company ten-day room reports.';

COMMIT;
