Files
wyndham-ARR/database/002_finance_monthly_backfill_support.sql
2026-07-29 16:38:05 +08:00

240 lines
8.6 KiB
PL/PgSQL

-- 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;