-- Finance monthly-result backfill and booking-source enrichment support v1.0 -- PostgreSQL 15+ -- Target database: booking_test (connect to that database before running). -- This migration is additive; do not edit applied migration 001 in place. BEGIN; ALTER TABLE finance.daily_versions ADD COLUMN ingestion_mode text NOT NULL DEFAULT 'opera_xml'; ALTER TABLE finance.daily_versions ADD CONSTRAINT daily_versions_ingestion_mode_valid CHECK ( ingestion_mode IN ('opera_xml', 'monthly_backfill') ); 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 ) OR ingestion_mode = 'monthly_backfill' ) ) ); CREATE UNIQUE INDEX daily_versions_source_date_mode_unique ON finance.daily_versions ( source_file_id, business_date, ingestion_mode ); COMMENT ON COLUMN finance.daily_versions.ingestion_mode IS 'opera_xml for normal deterministic processing; monthly_backfill only for an explicitly approved processed-workbook backfill.'; ALTER TABLE finance.daily_records ADD COLUMN source_worksheet text, ADD COLUMN source_row_no integer, ADD COLUMN booking_source_row_id bigint, ADD COLUMN booking_source_match_status text NOT NULL DEFAULT 'not_checked'; ALTER TABLE finance.daily_records ADD CONSTRAINT daily_records_source_worksheet_valid CHECK ( source_worksheet IS NULL OR btrim(source_worksheet) <> '' ), ADD CONSTRAINT daily_records_source_row_no_valid CHECK ( source_row_no IS NULL OR source_row_no > 1 ), ADD CONSTRAINT daily_records_source_coordinate_shape CHECK ( (source_worksheet IS NULL AND source_row_no IS NULL) OR (source_worksheet IS NOT NULL AND source_row_no IS NOT NULL) ), ADD CONSTRAINT daily_records_booking_source_status_valid CHECK ( booking_source_match_status IN ( 'matched', 'unmatched', 'missing_group_code', 'conflict', 'not_checked' ) ), ADD CONSTRAINT daily_records_booking_source_group_fk FOREIGN KEY (booking_source_row_id, group_code_key) REFERENCES booking.booking_source_rows(id, group_code_key), ADD CONSTRAINT daily_records_booking_source_match_shape CHECK ( ( booking_source_match_status = 'matched' AND group_code_key IS NOT NULL AND booking_source_row_id IS NOT NULL ) OR ( booking_source_match_status IN ('unmatched', 'conflict') AND group_code_key IS NOT NULL AND booking_source_row_id IS NULL ) OR ( booking_source_match_status = 'missing_group_code' AND group_code_key IS NULL AND booking_source_row_id IS NULL ) OR ( booking_source_match_status = 'not_checked' AND booking_source_row_id IS NULL ) ); CREATE UNIQUE INDEX daily_records_source_coordinate_unique ON finance.daily_records ( daily_version_id, source_worksheet, source_row_no ) WHERE source_worksheet IS NOT NULL; CREATE INDEX daily_records_booking_source_row_idx ON finance.daily_records (booking_source_row_id) WHERE booking_source_row_id IS NOT NULL; CREATE INDEX daily_records_booking_source_status_idx ON finance.daily_records (booking_source_match_status); COMMENT ON COLUMN finance.daily_records.source_worksheet IS 'Exact source worksheet for processed-workbook backfills; null for direct XML rows.'; COMMENT ON COLUMN finance.daily_records.source_row_no IS 'One-based source worksheet row number; null for direct XML rows.'; COMMENT ON COLUMN finance.daily_records.booking_source_row_id IS 'Accepted booking attachment source row selected by exact normalized RES_COMMENT Group Code.'; COMMENT ON COLUMN finance.daily_records.booking_source_match_status IS 'Source-layer booking lookup status, separate from Agent parse-version matching.'; ALTER TABLE finance.report_versions ADD COLUMN as_of_date date NOT NULL, ADD COLUMN processor_version text NOT NULL, ADD COLUMN rule_set_sha256 character(64), ADD COLUMN result_schema_version text NOT NULL DEFAULT '1.0'; ALTER TABLE finance.report_versions ADD CONSTRAINT report_versions_as_of_date_valid CHECK ( as_of_date BETWEEN period_start AND period_end ), ADD CONSTRAINT report_versions_processor_version_valid CHECK ( btrim(processor_version) <> '' ), ADD CONSTRAINT report_versions_rule_set_sha256_valid CHECK ( rule_set_sha256 IS NULL OR rule_set_sha256 ~ '^[0-9a-f]{64}$' ), ADD CONSTRAINT report_versions_result_schema_version_valid CHECK ( btrim(result_schema_version) <> '' ); COMMENT ON COLUMN finance.report_versions.as_of_date IS 'Latest business date included in this report artifact, distinct from its natural reporting period end.'; COMMENT ON COLUMN finance.report_versions.processor_version IS 'Deterministic report generator or approved backfill adapter version.'; CREATE TABLE finance.report_booking_sources ( report_version_id bigint NOT NULL REFERENCES finance.report_versions(id), group_code_key text NOT NULL CHECK ( group_code_key = upper(btrim(group_code_key)) AND group_code_key <> '' ), booking_source_row_id bigint NOT NULL, PRIMARY KEY (report_version_id, group_code_key), CONSTRAINT report_booking_source_row_fk FOREIGN KEY (booking_source_row_id, group_code_key) REFERENCES booking.booking_source_rows(id, group_code_key) ); COMMENT ON TABLE finance.report_booking_sources IS 'Freezes booking attachment source rows used by a report before Agent parse versions exist or are required.'; CREATE OR REPLACE 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, booking_source.hotel_raw 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 LEFT JOIN booking.booking_source_rows AS booking_source ON booking_source.id = records.booking_source_row_id WHERE records.outcome = 'retained'; CREATE VIEW finance.v_channel_booking_enriched AS SELECT current_version.business_date, records.daily_version_id, records.id AS daily_record_id, records.source_worksheet, records.source_row_no, records.channel_key, records.company_key, records.company_name, records.rate_code, records.normalized_rate_code, records.block_code, records.group_code_key, records.res_comment, records.room_category_label AS opera_room_category_label, booking_source.hotel_raw AS booking_type_of_room_raw, records.booking_source_row_id, records.booking_source_match_status, records.adults, records.children, records.no_of_rooms, records.arrival, records.departure, records.nights, records.effective_rate_amount, records.real_price, records.total_price, records.kb_amount, records.pricing_method FROM finance.current_daily_versions AS current_version JOIN finance.daily_records AS records ON records.daily_version_id = current_version.daily_version_id LEFT JOIN booking.booking_source_rows AS booking_source ON booking_source.id = records.booking_source_row_id WHERE records.outcome = 'retained'; COMMENT ON VIEW finance.v_company_report_source IS 'Privacy-minimized company-report facts; type_of_room is the matched booking attachment source text, never Opera ROOM_CATEGORY_LABEL.'; COMMENT ON VIEW finance.v_channel_booking_enriched IS 'Privacy-minimized active channel facts with Opera room category and booking original type_of_room kept as separate fields.'; COMMIT;