feat: prepare ARR for controlled public deployment

This commit is contained in:
Wyndham ARR
2026-07-29 16:38:05 +08:00
commit a701de9f0e
271 changed files with 48472 additions and 0 deletions

View File

@@ -0,0 +1,360 @@
-- 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;