-- ARR XML processing delivery, audit, and deterministic ingestion support
-- Structured-result contract: 3.0; processor allowlist is enforced by ARR code.
-- PostgreSQL 15+
-- Target database: booking_test (connect to that database before running).
-- This migration is additive; do not edit applied migrations 001-004 in place.

BEGIN;

ALTER TABLE booking.file_objects
    DROP CONSTRAINT file_objects_file_kind_check;

ALTER TABLE booking.file_objects
    ADD CONSTRAINT file_objects_file_kind_check CHECK (file_kind IN (
        'booking_excel',
        'opera_xml',
        'daily_xlsx',
        'monthly_xlsx',
        'company_ten_day_xlsx',
        'exception_xlsx',
        'result_json',
        'structured_result_json'
    ));

CREATE TABLE finance.processing_jobs (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    job_key text NOT NULL UNIQUE CHECK (
        job_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
    ),
    source_file_id bigint NOT NULL
        REFERENCES booking.file_objects(id),
    job_status text NOT NULL CHECK (job_status IN (
        'uploaded',
        'queued',
        'running',
        'delivered',
        'validating',
        'succeeded',
        'failed',
        'cancelled'
    )),
    requested_processor_version text NOT NULL CHECK (
        btrim(requested_processor_version) <> ''
    ),
    requested_rule_set_sha256 character(64) NOT NULL CHECK (
        requested_rule_set_sha256 ~ '^[0-9a-f]{64}$'
    ),
    current_attempt_no integer NOT NULL DEFAULT 0 CHECK (
        current_attempt_no >= 0
    ),
    business_date date,
    failure_code text,
    failure_message text,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now(),
    finished_at timestamptz,
    CONSTRAINT processing_jobs_terminal_shape CHECK (
        (
            job_status = 'succeeded'
            AND business_date IS NOT NULL
            AND failure_code IS NULL
            AND finished_at IS NOT NULL
        )
        OR (
            job_status = 'failed'
            AND failure_code IS NOT NULL
            AND finished_at IS NOT NULL
        )
        OR (
            job_status = 'cancelled'
            AND finished_at IS NOT NULL
        )
        OR job_status NOT IN ('succeeded', 'failed', 'cancelled')
    )
);

COMMENT ON TABLE finance.processing_jobs IS
  'ARR-owned XML processing jobs. SuperAgent may execute attempts but never writes this table or Finance facts.';

CREATE TABLE finance.processing_attempts (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    processing_job_id bigint NOT NULL
        REFERENCES finance.processing_jobs(id),
    attempt_no integer NOT NULL CHECK (attempt_no > 0),
    attempt_status text NOT NULL CHECK (attempt_status IN (
        'queued',
        'dispatched',
        'running',
        'delivered',
        'succeeded',
        'failed',
        'cancelled'
    )),
    idempotency_key character(64) NOT NULL UNIQUE CHECK (
        idempotency_key ~ '^[0-9a-f]{64}$'
    ),
    remote_run_id text,
    failure_code text,
    failure_message text,
    created_at timestamptz NOT NULL DEFAULT now(),
    started_at timestamptz,
    finished_at timestamptz,
    CONSTRAINT processing_attempts_job_number_unique
        UNIQUE (processing_job_id, attempt_no),
    CONSTRAINT processing_attempts_id_job_unique
        UNIQUE (id, processing_job_id),
    CONSTRAINT processing_attempts_remote_run_unique
        UNIQUE (remote_run_id),
    CONSTRAINT processing_attempts_terminal_shape CHECK (
        (
            attempt_status = 'succeeded'
            AND failure_code IS NULL
            AND finished_at IS NOT NULL
        )
        OR (
            attempt_status = 'failed'
            AND failure_code IS NOT NULL
            AND finished_at IS NOT NULL
        )
        OR (
            attempt_status = 'cancelled'
            AND finished_at IS NOT NULL
        )
        OR attempt_status NOT IN ('succeeded', 'failed', 'cancelled')
    )
);

COMMENT ON TABLE finance.processing_attempts IS
  'Recoverable dispatch attempts correlated to one ARR processing job and an optional remote SuperAgent run.';

CREATE TABLE finance.processing_callbacks (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    callback_key text NOT NULL UNIQUE CHECK (
        callback_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
    ),
    processing_job_id bigint NOT NULL,
    attempt_id bigint NOT NULL,
    envelope_sha256 character(64) NOT NULL CHECK (
        envelope_sha256 ~ '^[0-9a-f]{64}$'
    ),
    envelope_json jsonb NOT NULL,
    callback_status text NOT NULL CHECK (callback_status IN (
        'received',
        'validating',
        'committed',
        'recorded_failure',
        'rejected'
    )),
    result_status text NOT NULL CHECK (result_status IN (
        'success', 'failed'
    )),
    processor_version text NOT NULL CHECK (btrim(processor_version) <> ''),
    rule_set_sha256 character(64) NOT NULL CHECK (
        rule_set_sha256 ~ '^[0-9a-f]{64}$'
    ),
    result_schema_version text NOT NULL CHECK (
        result_schema_version = '3.0'
    ),
    business_date date,
    source_file_id bigint REFERENCES booking.file_objects(id),
    daily_report_file_id bigint REFERENCES booking.file_objects(id),
    result_json_file_id bigint REFERENCES booking.file_objects(id),
    structured_result_file_id bigint REFERENCES booking.file_objects(id),
    exception_report_file_id bigint REFERENCES booking.file_objects(id),
    daily_version_id bigint REFERENCES finance.daily_versions(id),
    failure_code text,
    failure_message text,
    received_at timestamptz NOT NULL DEFAULT now(),
    validated_at timestamptz,
    committed_at timestamptz,
    CONSTRAINT processing_callbacks_attempt_job_fk
        FOREIGN KEY (attempt_id, processing_job_id)
        REFERENCES finance.processing_attempts(id, processing_job_id),
    CONSTRAINT processing_callbacks_attempt_envelope_unique
        UNIQUE (attempt_id, envelope_sha256),
    CONSTRAINT processing_callbacks_terminal_shape CHECK (
        (
            callback_status = 'committed'
            AND result_status = 'success'
            AND business_date IS NOT NULL
            AND source_file_id IS NOT NULL
            AND daily_report_file_id IS NOT NULL
            AND result_json_file_id IS NOT NULL
            AND structured_result_file_id IS NOT NULL
            AND exception_report_file_id IS NULL
            AND daily_version_id IS NOT NULL
            AND failure_code IS NULL
            AND validated_at IS NOT NULL
            AND committed_at IS NOT NULL
        )
        OR (
            callback_status = 'recorded_failure'
            AND result_status = 'failed'
            AND source_file_id IS NOT NULL
            AND daily_report_file_id IS NULL
            AND result_json_file_id IS NOT NULL
            AND structured_result_file_id IS NOT NULL
            AND exception_report_file_id IS NOT NULL
            AND daily_version_id IS NULL
            AND failure_code IS NOT NULL
            AND validated_at IS NOT NULL
            AND committed_at IS NOT NULL
        )
        OR (
            callback_status = 'rejected'
            AND daily_version_id IS NULL
            AND failure_code IS NOT NULL
            AND committed_at IS NULL
        )
        OR callback_status IN ('received', 'validating')
    )
);

COMMENT ON TABLE finance.processing_callbacks IS
  'Idempotent ARR receipt and validation record for one structured SuperAgent delivery; envelope JSON contains object identities, not XML bytes.';

CREATE TABLE finance.outbox_events (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    event_key text NOT NULL UNIQUE CHECK (btrim(event_key) <> ''),
    aggregate_type text NOT NULL CHECK (aggregate_type IN (
        'processing_job', 'daily_version', 'report_version'
    )),
    aggregate_id bigint NOT NULL CHECK (aggregate_id > 0),
    event_type text NOT NULL CHECK (btrim(event_type) <> ''),
    payload jsonb NOT NULL,
    publish_status text NOT NULL DEFAULT 'pending' CHECK (
        publish_status IN ('pending', 'publishing', 'published', 'dead')
    ),
    publish_attempts integer NOT NULL DEFAULT 0 CHECK (
        publish_attempts >= 0
    ),
    available_at timestamptz NOT NULL DEFAULT now(),
    created_at timestamptz NOT NULL DEFAULT now(),
    published_at timestamptz,
    last_error_code text,
    CONSTRAINT outbox_events_published_shape CHECK (
        (publish_status = 'published' AND published_at IS NOT NULL)
        OR publish_status <> 'published'
    )
);

COMMENT ON TABLE finance.outbox_events IS
  'Privacy-minimized transactional outbox; payloads must contain opaque IDs, dates, counts, and artifact identities only.';

ALTER TABLE finance.daily_versions
    ADD COLUMN structured_result_file_id bigint
        REFERENCES booking.file_objects(id),
    ADD COLUMN processing_job_id bigint
        REFERENCES finance.processing_jobs(id);

DROP INDEX finance.daily_versions_source_date_mode_unique;

CREATE UNIQUE INDEX daily_versions_source_date_rule_unique
    ON finance.daily_versions (
        source_file_id,
        business_date,
        ingestion_mode,
        processor_version,
        COALESCE(rule_set_sha256, repeat('0', 64)::character(64))
    );

CREATE UNIQUE INDEX daily_versions_processing_job_unique
    ON finance.daily_versions (processing_job_id)
    WHERE processing_job_id IS NOT NULL;

ALTER TABLE finance.daily_versions
    DROP CONSTRAINT daily_version_active_shape;

ALTER TABLE finance.daily_versions
    ADD 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 failure_code IS NULL
            AND (
                (
                    ingestion_mode = 'opera_xml'
                    AND daily_report_file_id IS NOT NULL
                    AND result_json_file_id IS NOT NULL
                    AND structured_result_file_id IS NOT NULL
                    AND result_schema_version = '3.0'
                )
                OR ingestion_mode = 'monthly_backfill'
            )
        )
    );

COMMENT ON COLUMN finance.daily_versions.structured_result_file_id IS
  'Original validated structured-result.json v3 object used by ARR for this immutable Opera daily version.';

COMMENT ON COLUMN finance.daily_versions.processing_job_id IS
  'ARR processing job that committed this Opera daily version; null for approved historical backfills.';

-- Successful v3 batches persist every XML source row. Rows excluded before full
-- business validation may legitimately contain otherwise-invalid optional
-- values, so business-value checks apply only to retained facts.
ALTER TABLE finance.daily_records
    DROP CONSTRAINT daily_records_people_valid,
    DROP CONSTRAINT daily_records_room_count_valid,
    DROP CONSTRAINT daily_records_dates_and_nights_valid,
    DROP CONSTRAINT daily_records_total_valid,
    DROP CONSTRAINT daily_records_kb_valid;

ALTER TABLE finance.daily_records
    ADD CONSTRAINT daily_records_people_valid CHECK (
        outcome <> 'retained'
        OR (
            (adults IS NULL OR adults >= 0)
            AND (children IS NULL OR children >= 0)
        )
    ),
    ADD CONSTRAINT daily_records_room_count_valid CHECK (
        outcome <> 'retained'
        OR no_of_rooms IS NULL
        OR no_of_rooms > 0
    ),
    ADD CONSTRAINT daily_records_dates_and_nights_valid CHECK (
        outcome <> 'retained'
        OR arrival IS NULL
        OR departure IS NULL
        OR nights IS NULL
        OR (
            departure >= arrival
            AND nights = departure - arrival
        )
    ),
    ADD CONSTRAINT daily_records_total_valid CHECK (
        outcome <> 'retained'
        OR 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
    ),
    ADD CONSTRAINT daily_records_kb_valid CHECK (
        outcome <> 'retained'
        OR kb_amount IS NULL
        OR no_of_rooms IS NULL
        OR kb_amount = no_of_rooms * 100
    );

COMMENT ON CONSTRAINT daily_records_people_valid
    ON finance.daily_records IS
  'Business-value validity applies to retained facts; pre-validation exclusions remain auditable without becoming active facts.';

ALTER TABLE finance.daily_channel_metrics
    ADD COLUMN worksheet_order integer,
    ADD CONSTRAINT daily_channel_metrics_order_valid CHECK (
        worksheet_order IS NULL OR worksheet_order > 0
    );

CREATE UNIQUE INDEX daily_channel_metrics_order_unique
    ON finance.daily_channel_metrics (daily_version_id, worksheet_order)
    WHERE worksheet_order IS NOT NULL;

COMMENT ON COLUMN finance.daily_channel_metrics.worksheet_order IS
  'One-based deterministic channel order for new Opera daily versions; null on historical rows until a report-level manifest is approved.';

COMMIT;
