-- ARR MCP direct structured-result ingestion ledger. -- PostgreSQL 15+. Additive only; migrations 008 and 009 remain immutable. BEGIN; DO $$ BEGIN IF current_database() <> 'booking_test' THEN RAISE EXCEPTION 'ARR MCP result ingestion is allowed only in booking_test'; END IF; IF to_regclass('ingestion.processing_runs') IS NULL OR to_regclass('ingestion.processing_attempts') IS NULL OR to_regclass('finance.daily_versions') IS NULL THEN RAISE EXCEPTION 'ARR MVP v1 migration 008 must be applied first'; END IF; IF to_regclass('ingestion.result_submission_grants') IS NOT NULL OR to_regclass('ingestion.result_submissions') IS NOT NULL THEN RAISE EXCEPTION 'ARR MCP result ingestion migration 010 is already applied'; END IF; END; $$; -- Existing rows retain the artifact callback contract. New MCP runs opt in -- explicitly and may finish without Agent-produced result objects in OSS. ALTER TABLE ingestion.processing_runs ADD COLUMN result_delivery_mode text NOT NULL DEFAULT 'artifact_callback'; ALTER TABLE ingestion.processing_runs ADD CONSTRAINT processing_runs_result_delivery_mode_check CHECK ( result_delivery_mode IN ('artifact_callback', 'direct_mcp') ), ADD CONSTRAINT processing_runs_result_delivery_pipeline_check CHECK ( pipeline_type = 'opera_daily' OR result_delivery_mode = 'artifact_callback' ); ALTER TABLE ingestion.processing_runs DROP CONSTRAINT processing_runs_terminal_shape; ALTER TABLE ingestion.processing_runs ADD CONSTRAINT processing_runs_terminal_shape CHECK ( ( run_status = 'accepted' AND failure_code IS NULL AND validated_at IS NOT NULL AND finished_at IS NOT NULL AND ( pipeline_type <> 'opera_daily' OR ( business_date IS NOT NULL AND delivered_processor_version IS NOT NULL AND delivered_rule_set_sha256 IS NOT NULL AND result_schema_version IS NOT NULL AND delivery_sha256 IS NOT NULL AND ( ( result_delivery_mode = 'artifact_callback' AND result_artifact_id IS NOT NULL ) OR ( result_delivery_mode = 'direct_mcp' AND result_artifact_id IS NULL ) ) ) ) ) OR ( run_status IN ('rejected', 'failed') AND failure_code IS NOT NULL AND finished_at IS NOT NULL ) OR ( run_status = 'cancelled' AND finished_at IS NOT NULL ) OR run_status IN ('received', 'queued', 'running', 'validating') ); ALTER TABLE finance.daily_versions ADD COLUMN result_delivery_mode text NOT NULL DEFAULT 'artifact_callback'; ALTER TABLE finance.daily_versions ADD CONSTRAINT daily_versions_result_delivery_mode_check CHECK ( result_delivery_mode IN ('artifact_callback', 'direct_mcp') ), ADD CONSTRAINT daily_versions_result_delivery_shape CHECK ( result_delivery_mode <> 'direct_mcp' OR ( daily_report_artifact_id IS NULL AND result_json_artifact_id IS NULL AND structured_result_artifact_id IS NULL AND exception_report_artifact_id IS NULL ) ); ALTER TABLE finance.daily_versions DROP CONSTRAINT daily_versions_status_shape; ALTER TABLE finance.daily_versions ADD CONSTRAINT daily_versions_status_shape CHECK ( ( version_status IN ('validated', 'active', 'superseded') AND business_date IS NOT NULL AND exception_report_artifact_id IS NULL AND failure_code IS NULL AND validated_at IS NOT NULL AND ( ( result_delivery_mode = 'artifact_callback' AND daily_report_artifact_id IS NOT NULL AND result_json_artifact_id IS NOT NULL AND structured_result_artifact_id IS NOT NULL ) OR ( result_delivery_mode = 'direct_mcp' AND daily_report_artifact_id IS NULL AND result_json_artifact_id IS NULL AND structured_result_artifact_id IS NULL ) ) ) OR ( version_status = 'rejected' AND failure_code IS NOT NULL ) ); CREATE TABLE ingestion.result_submission_grants ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, grant_sha256 character(64) NOT NULL UNIQUE CHECK ( grant_sha256 ~ '^[0-9a-f]{64}$' ), processing_run_id bigint NOT NULL REFERENCES ingestion.processing_runs(id), attempt_id bigint NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), expires_at timestamptz NOT NULL, consumed_at timestamptz, revoked_at timestamptz, CONSTRAINT result_submission_grants_attempt_unique UNIQUE (attempt_id), CONSTRAINT result_submission_grants_id_run_attempt_unique UNIQUE (id, processing_run_id, attempt_id), CONSTRAINT result_submission_grants_attempt_run_fk FOREIGN KEY (attempt_id, processing_run_id) REFERENCES ingestion.processing_attempts(id, processing_run_id), CONSTRAINT result_submission_grants_lifetime CHECK ( expires_at > created_at AND expires_at <= created_at + interval '30 minutes' ), CONSTRAINT result_submission_grants_usage_shape CHECK ( NOT (consumed_at IS NOT NULL AND revoked_at IS NOT NULL) AND ( consumed_at IS NULL OR consumed_at BETWEEN created_at AND expires_at ) AND (revoked_at IS NULL OR revoked_at >= created_at) ) ); CREATE INDEX result_submission_grants_expiry_idx ON ingestion.result_submission_grants (expires_at) WHERE consumed_at IS NULL AND revoked_at IS NULL; CREATE TABLE ingestion.result_submissions ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, submission_key text NOT NULL UNIQUE CHECK ( submission_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' ), grant_id bigint NOT NULL UNIQUE, processing_run_id bigint NOT NULL, attempt_id bigint NOT NULL UNIQUE, submission_status text NOT NULL CHECK (submission_status IN ( 'received', 'validating', 'committed', 'already_committed', 'rejected', 'expired' )), contract_version text NOT NULL CHECK ( contract_version = 'arr-direct-ingestion-1' ), business_date date NOT NULL, processor_version text NOT NULL CHECK ( processor_version ~ '^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$' ), rule_set_sha256 character(64) NOT NULL CHECK ( rule_set_sha256 ~ '^[0-9a-f]{64}$' ), result_schema_version text NOT NULL CHECK ( btrim(result_schema_version) <> '' ), payload_json jsonb CHECK ( payload_json IS NULL OR jsonb_typeof(payload_json) = 'object' ), payload_sha256 character(64) NOT NULL CHECK ( payload_sha256 ~ '^[0-9a-f]{64}$' ), payload_byte_size integer NOT NULL CHECK ( payload_byte_size BETWEEN 2 AND 3145728 ), record_count integer NOT NULL CHECK ( record_count >= 0 ), created_at timestamptz NOT NULL DEFAULT now(), expires_at timestamptz NOT NULL, validation_started_at timestamptz, finished_at timestamptz, payload_purged_at timestamptz, daily_version_id bigint REFERENCES finance.daily_versions(id), failure_code text, failure_message text, receipt_json jsonb NOT NULL DEFAULT '{}'::jsonb CHECK ( jsonb_typeof(receipt_json) = 'object' ), CONSTRAINT result_submissions_grant_run_attempt_fk FOREIGN KEY (grant_id, processing_run_id, attempt_id) REFERENCES ingestion.result_submission_grants( id, processing_run_id, attempt_id ), CONSTRAINT result_submissions_lifetime CHECK ( expires_at > created_at AND expires_at <= created_at + interval '30 minutes' ), CONSTRAINT result_submissions_payload_shape CHECK ( payload_json IS NULL OR ( payload_json ? 'records' AND jsonb_typeof(payload_json -> 'records') = 'array' AND jsonb_array_length(payload_json -> 'records') = record_count ) ), CONSTRAINT result_submissions_timestamps_valid CHECK ( ( validation_started_at IS NULL OR validation_started_at >= created_at ) AND (finished_at IS NULL OR finished_at >= created_at) AND ( payload_purged_at IS NULL OR ( finished_at IS NOT NULL AND payload_purged_at >= finished_at ) ) AND ( submission_status <> 'expired' OR finished_at >= expires_at ) ), CONSTRAINT result_submissions_status_shape CHECK ( ( submission_status = 'received' AND validation_started_at IS NULL AND finished_at IS NULL AND daily_version_id IS NULL AND failure_code IS NULL AND receipt_json = '{}'::jsonb AND payload_json IS NOT NULL AND payload_purged_at IS NULL ) OR ( submission_status = 'validating' AND validation_started_at IS NOT NULL AND finished_at IS NULL AND daily_version_id IS NULL AND failure_code IS NULL AND receipt_json = '{}'::jsonb AND payload_json IS NOT NULL AND payload_purged_at IS NULL ) OR ( submission_status IN ('committed', 'already_committed') AND validation_started_at IS NOT NULL AND finished_at IS NOT NULL AND daily_version_id IS NOT NULL AND failure_code IS NULL AND receipt_json <> '{}'::jsonb AND payload_json IS NULL AND payload_purged_at IS NOT NULL ) OR ( submission_status IN ('rejected', 'expired') AND finished_at IS NOT NULL AND daily_version_id IS NULL AND failure_code IS NOT NULL AND btrim(failure_code) <> '' AND payload_json IS NULL AND payload_purged_at IS NOT NULL ) ) ); CREATE INDEX result_submissions_status_expiry_idx ON ingestion.result_submissions (submission_status, expires_at); -- Deliberately non-unique: the same XML/result may be processed repeatedly by -- different test jobs while final Finance facts remain idempotent. CREATE INDEX result_submissions_payload_sha256_idx ON ingestion.result_submissions (payload_sha256); CREATE INDEX result_submissions_daily_version_idx ON ingestion.result_submissions (daily_version_id) WHERE daily_version_id IS NOT NULL; COMMENT ON COLUMN ingestion.processing_runs.result_delivery_mode IS 'artifact_callback preserves the v3 OSS result contract; direct_mcp commits validated structured records without Agent-produced output objects.'; COMMENT ON COLUMN finance.daily_versions.result_delivery_mode IS 'Identifies whether the validated Finance version arrived through the legacy artifact callback or the direct MCP ingestion path.'; COMMENT ON TABLE ingestion.result_submission_grants IS 'Hashed, attempt-bound grants for starting one direct MCP result submission. Plaintext grants and database credentials are never stored.'; COMMENT ON TABLE ingestion.result_submissions IS 'Durable, idempotent single-call result ledger. Payload hashes are intentionally non-unique so the same XML can be rerun under a new processing attempt; guest-bearing payload JSON is purged at terminal status.'; COMMIT;