feat: add daily manual price review workflow

This commit is contained in:
Wyndham ARR committed 2026-08-06 22:40:18 +08:00
1 parent aae3d8e1db
commit ca3e8e18fa
77 files changed
+7750 -602

No files matched your search

+172
View File
@@ -0,0 +1,172 @@
-- Protected rollback for migration 017. It deliberately refuses to discard any
-- review/audit/manual-price fact; use a forward corrective migration instead.
BEGIN;
DO $$
BEGIN
IF current_database() <> 'booking_test' THEN
RAISE EXCEPTION 'ARR daily price review rollback is allowed only in booking_test';
END IF;
IF to_regclass('ingestion.daily_review_cases') IS NULL THEN
RAISE EXCEPTION 'ARR daily price review migration is not applied';
END IF;
IF EXISTS (SELECT 1 FROM ingestion.daily_review_cases)
OR EXISTS (
SELECT 1
FROM finance.daily_versions
WHERE review_case_id IS NOT NULL
OR manual_override_sha256 IS NOT NULL
OR manually_priced_rows <> 0
)
OR EXISTS (
SELECT 1 FROM ingestion.processing_runs WHERE run_status = 'awaiting_review'
)
OR EXISTS (
SELECT 1 FROM ingestion.processing_attempts WHERE attempt_status = 'review_required'
)
OR EXISTS (
SELECT 1 FROM ingestion.processing_deliveries
WHERE delivery_status = 'recorded_review' OR result_status = 'review_required'
) THEN
RAISE EXCEPTION
'refusing destructive rollback: daily review or manual-pricing facts exist';
END IF;
END;
$$;
DO $$
BEGIN
IF to_regrole('arr_app') IS NOT NULL THEN
EXECUTE 'REVOKE SELECT, INSERT, UPDATE ON ingestion.daily_review_cases, ingestion.daily_review_items, ingestion.daily_review_events FROM arr_app';
EXECUTE 'REVOKE USAGE, SELECT ON SEQUENCE ingestion.daily_review_cases_id_seq, ingestion.daily_review_items_id_seq, ingestion.daily_review_events_id_seq FROM arr_app';
END IF;
END;
$$;
ALTER TABLE finance.daily_versions
DROP CONSTRAINT IF EXISTS daily_versions_review_lineage_shape;
DROP INDEX IF EXISTS finance.daily_versions_review_case_unique;
DROP INDEX IF EXISTS finance.daily_versions_source_rule_override_unique;
ALTER TABLE finance.daily_versions
DROP COLUMN manually_priced_rows,
DROP COLUMN manual_override_sha256,
DROP COLUMN review_case_id;
CREATE UNIQUE INDEX daily_versions_source_rule_unique
ON finance.daily_versions (
source_artifact_id,
business_date,
processor_version,
rule_set_sha256
)
WHERE business_date IS NOT NULL;
ALTER TABLE finance.daily_records
DROP CONSTRAINT IF EXISTS daily_records_retained_values;
ALTER TABLE finance.daily_records
ADD CONSTRAINT daily_records_retained_values CHECK (
outcome <> 'retained'
OR (
block_code IS NOT NULL
AND adults IS NOT NULL AND adults >= 0
AND children IS NOT NULL AND children >= 0
AND company_name IS NOT NULL AND btrim(company_name) <> ''
AND company_key IS NOT NULL AND btrim(company_key) <> ''
AND confirmation_no IS NOT NULL AND btrim(confirmation_no) <> ''
AND disp_room_no IS NOT NULL AND btrim(disp_room_no) <> ''
AND effective_rate_amount IS NOT NULL AND effective_rate_amount >= 0
AND full_name IS NOT NULL
AND no_of_rooms IS NOT NULL AND no_of_rooms > 0
AND rate_code IS NOT NULL
AND normalized_rate_code IS NOT NULL
AND arrival IS NOT NULL
AND departure IS NOT NULL
AND nights IS NOT NULL AND nights >= 0
AND real_price IS NOT NULL AND real_price >= 0
AND total_price IS NOT NULL AND total_price >= 0
AND channel_key IS NOT NULL AND btrim(channel_key) <> ''
AND pricing_method IN ('price_reference_exact', 'zero_price_exception')
)
);
ALTER TABLE ingestion.processing_deliveries
DROP CONSTRAINT IF EXISTS processing_deliveries_review_case_fk,
DROP COLUMN review_case_id,
DROP COLUMN manual_override_artifact_id;
DROP TRIGGER IF EXISTS daily_review_items_mutability_guard ON ingestion.daily_review_items;
DROP TRIGGER IF EXISTS daily_review_cases_immutability_guard ON ingestion.daily_review_cases;
DROP TRIGGER IF EXISTS daily_review_events_immutability_guard ON ingestion.daily_review_events;
DROP FUNCTION IF EXISTS ingestion.enforce_daily_review_item_mutability();
DROP FUNCTION IF EXISTS ingestion.enforce_daily_review_case_immutability();
DROP FUNCTION IF EXISTS ingestion.enforce_daily_review_event_immutability();
DROP TABLE ingestion.daily_review_events;
DROP TABLE ingestion.daily_review_items;
DROP TABLE ingestion.daily_review_cases;
ALTER TABLE ingestion.processing_deliveries
DROP CONSTRAINT IF EXISTS processing_deliveries_delivery_status_check,
DROP CONSTRAINT IF EXISTS processing_deliveries_result_status_check;
ALTER TABLE ingestion.processing_deliveries
ADD CONSTRAINT processing_deliveries_delivery_status_check CHECK (delivery_status IN (
'received', 'validating', 'committed', 'recorded_failure', 'rejected'
)),
ADD CONSTRAINT processing_deliveries_result_status_check CHECK (result_status IN (
'success', 'failed'
));
ALTER TABLE ingestion.processing_attempts
DROP CONSTRAINT IF EXISTS processing_attempts_attempt_status_check,
DROP CONSTRAINT IF EXISTS processing_attempts_terminal_shape;
ALTER TABLE ingestion.processing_attempts
ADD CONSTRAINT processing_attempts_attempt_status_check CHECK (attempt_status IN (
'queued', 'dispatched', 'running', 'delivered', 'succeeded', 'failed', 'cancelled'
)),
ADD 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 IN ('queued', 'dispatched', 'running', 'delivered')
);
ALTER TABLE ingestion.processing_runs
DROP CONSTRAINT IF EXISTS processing_runs_run_status_check,
DROP CONSTRAINT IF EXISTS processing_runs_terminal_shape;
ALTER TABLE ingestion.processing_runs
ADD CONSTRAINT processing_runs_run_status_check CHECK (run_status IN (
'received', 'queued', 'running', 'validating', 'accepted', 'rejected', 'failed', 'cancelled'
)),
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')
);
COMMIT;
+401
View File
@@ -0,0 +1,401 @@
-- ARR daily PRICE_UNMATCHED manual-price review.
-- Apply only after the operator has confirmed/applied migration 016 in the same
-- release window. This migration is additive and records no guest information
-- in review tables beyond the normalized pricing key and aggregate impact.
BEGIN;
DO $$
BEGIN
IF current_database() <> 'booking_test' THEN
RAISE EXCEPTION 'ARR daily price review migration 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('ingestion.processing_deliveries') IS NULL
OR to_regclass('finance.daily_versions') IS NULL
OR to_regclass('finance.daily_records') IS NULL
OR to_regprocedure('reporting.validate_monthly_run_publication()') IS NULL THEN
RAISE EXCEPTION 'ARR migrations 008 through 016 must be applied before 017';
END IF;
IF to_regclass('ingestion.daily_review_cases') IS NOT NULL THEN
RAISE EXCEPTION 'ARR daily price review migration is already applied';
END IF;
END;
$$;
-- New lifecycle values. Historical constraints are replaced verbatim rather
-- than modifying migrations 008/010.
ALTER TABLE ingestion.processing_runs
DROP CONSTRAINT IF EXISTS processing_runs_run_status_check,
DROP CONSTRAINT IF EXISTS processing_runs_terminal_shape;
ALTER TABLE ingestion.processing_runs
ADD CONSTRAINT processing_runs_run_status_check CHECK (run_status IN (
'received', 'queued', 'running', 'validating', 'awaiting_review',
'accepted', 'rejected', 'failed', 'cancelled'
)),
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 = 'awaiting_review'
AND pipeline_type = 'opera_daily'
AND result_delivery_mode = 'artifact_callback'
AND result_artifact_id IS NOT NULL
AND business_date IS NOT NULL
AND delivered_processor_version IS NOT NULL
AND delivered_rule_set_sha256 IS NOT NULL
AND result_schema_version = '4.0'
AND delivery_sha256 IS NOT NULL
AND validated_at IS NOT NULL
AND finished_at IS NULL
)
OR run_status IN ('received', 'queued', 'running', 'validating')
);
ALTER TABLE ingestion.processing_attempts
DROP CONSTRAINT IF EXISTS processing_attempts_attempt_status_check,
DROP CONSTRAINT IF EXISTS processing_attempts_terminal_shape;
ALTER TABLE ingestion.processing_attempts
ADD CONSTRAINT processing_attempts_attempt_status_check CHECK (attempt_status IN (
'queued', 'dispatched', 'running', 'delivered', 'review_required',
'succeeded', 'failed', 'cancelled'
)),
ADD 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 = 'review_required'
AND failure_code IS NULL
AND finished_at IS NOT NULL
)
OR (attempt_status = 'cancelled' AND finished_at IS NOT NULL)
OR attempt_status IN ('queued', 'dispatched', 'running', 'delivered')
);
ALTER TABLE ingestion.processing_deliveries
ADD COLUMN manual_override_artifact_id bigint REFERENCES ingestion.artifacts(id),
ADD COLUMN review_case_id bigint,
DROP CONSTRAINT IF EXISTS processing_deliveries_delivery_status_check,
DROP CONSTRAINT IF EXISTS processing_deliveries_result_status_check;
ALTER TABLE ingestion.processing_deliveries
ADD CONSTRAINT processing_deliveries_delivery_status_check CHECK (delivery_status IN (
'received', 'validating', 'committed', 'recorded_failure',
'recorded_review', 'rejected'
)),
ADD CONSTRAINT processing_deliveries_result_status_check CHECK (result_status IN (
'success', 'review_required', 'failed'
));
CREATE TABLE ingestion.daily_review_cases (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
case_key text NOT NULL UNIQUE CHECK (
case_key ~ '^dailyreview-[0-9a-f]{32}$'
),
processing_run_id bigint NOT NULL UNIQUE
REFERENCES ingestion.processing_runs(id),
initial_delivery_id bigint NOT NULL UNIQUE
REFERENCES ingestion.processing_deliveries(id),
business_date date NOT NULL,
source_sha256 character(64) NOT NULL CHECK (
source_sha256 ~ '^[0-9a-f]{64}$'
),
processor_version text NOT NULL CHECK (btrim(processor_version) <> ''),
rule_set_sha256 character(64) NOT NULL CHECK (
rule_set_sha256 ~ '^[0-9a-f]{64}$'
),
review_version text NOT NULL CHECK (review_version = '1.0'),
case_status text NOT NULL CHECK (case_status IN (
'open', 'processing', 'completed', 'generation_failed', 'failed', 'cancelled'
)),
revision integer NOT NULL DEFAULT 0 CHECK (revision >= 0),
manual_override_json jsonb CHECK (
manual_override_json IS NULL OR jsonb_typeof(manual_override_json) = 'object'
),
manual_override_sha256 character(64) CHECK (
manual_override_sha256 IS NULL OR manual_override_sha256 ~ '^[0-9a-f]{64}$'
),
manual_override_artifact_id bigint REFERENCES ingestion.artifacts(id),
frozen_at timestamptz,
failure_code text,
failure_message text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
completed_at timestamptz,
cancelled_at timestamptz,
CONSTRAINT daily_review_cases_frozen_shape CHECK (
(frozen_at IS NULL AND manual_override_json IS NULL
AND manual_override_sha256 IS NULL AND manual_override_artifact_id IS NULL)
OR (frozen_at IS NOT NULL AND manual_override_json IS NOT NULL
AND manual_override_sha256 IS NOT NULL)
),
CONSTRAINT daily_review_cases_terminal_shape CHECK (
(case_status = 'completed' AND completed_at IS NOT NULL AND frozen_at IS NOT NULL)
OR (case_status = 'cancelled' AND cancelled_at IS NOT NULL)
OR case_status IN ('open', 'processing', 'generation_failed', 'failed')
)
);
CREATE INDEX daily_review_cases_open_idx
ON ingestion.daily_review_cases (case_status, updated_at)
WHERE case_status IN ('open', 'generation_failed', 'processing');
CREATE TABLE ingestion.daily_review_items (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
review_case_id bigint NOT NULL
REFERENCES ingestion.daily_review_cases(id),
company_key text NOT NULL CHECK (btrim(company_key) <> ''),
rate_code text NOT NULL CHECK (
rate_code = upper(btrim(rate_code))
AND rate_code ~ '^[A-Z0-9]+$'
),
effective_rate_amount numeric(18,2) NOT NULL CHECK (effective_rate_amount >= 0),
candidate_prices jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (
jsonb_typeof(candidate_prices) = 'array'
),
affected_records integer NOT NULL CHECK (affected_records > 0),
affected_rooms integer NOT NULL CHECK (affected_rooms > 0),
affected_room_nights integer NOT NULL CHECK (affected_room_nights >= 0),
real_price numeric(18,2) CHECK (real_price >= 0),
revision integer NOT NULL DEFAULT 0 CHECK (revision >= 0),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT daily_review_items_key_unique
UNIQUE (review_case_id, company_key, rate_code, effective_rate_amount)
);
CREATE INDEX daily_review_items_case_idx
ON ingestion.daily_review_items (review_case_id, id);
ALTER TABLE ingestion.daily_review_items
ADD CONSTRAINT daily_review_items_id_case_unique UNIQUE (id, review_case_id);
CREATE TABLE ingestion.daily_review_events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
review_case_id bigint NOT NULL
REFERENCES ingestion.daily_review_cases(id),
review_item_id bigint REFERENCES ingestion.daily_review_items(id),
event_type text NOT NULL CHECK (event_type IN (
'PRICE_REVIEW_REQUIRED', 'PRICE_REVIEW_UPDATED', 'PRICE_REVIEW_FINALIZED',
'PRICE_REVIEW_CANCELLED', 'PRICE_REVIEW_GENERATION_FAILED', 'PRICE_REVIEW_FAILED'
)),
actor_username text NOT NULL CHECK (btrim(actor_username) <> ''),
previous_real_price numeric(18,2) CHECK (previous_real_price >= 0),
new_real_price numeric(18,2) CHECK (new_real_price >= 0),
revision integer NOT NULL CHECK (revision >= 0),
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT daily_review_events_item_case_fk
FOREIGN KEY (review_item_id, review_case_id)
REFERENCES ingestion.daily_review_items(id, review_case_id)
);
ALTER TABLE ingestion.processing_deliveries
ADD CONSTRAINT processing_deliveries_review_case_fk
FOREIGN KEY (review_case_id) REFERENCES ingestion.daily_review_cases(id);
CREATE OR REPLACE FUNCTION ingestion.enforce_daily_review_item_mutability()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
locked_case_status text;
locked_case_id bigint;
BEGIN
locked_case_id := COALESCE(NEW.review_case_id, OLD.review_case_id);
SELECT case_status INTO locked_case_status
FROM ingestion.daily_review_cases
WHERE id = locked_case_id
FOR KEY SHARE;
IF locked_case_status IS NULL THEN
RAISE EXCEPTION 'daily review case is unavailable';
END IF;
IF locked_case_status <> 'open' THEN
RAISE EXCEPTION 'daily review items are immutable after final confirmation';
END IF;
IF TG_OP = 'UPDATE' AND (
NEW.review_case_id <> OLD.review_case_id
OR NEW.company_key <> OLD.company_key
OR NEW.rate_code <> OLD.rate_code
OR NEW.effective_rate_amount <> OLD.effective_rate_amount
OR NEW.candidate_prices <> OLD.candidate_prices
OR NEW.affected_records <> OLD.affected_records
OR NEW.affected_rooms <> OLD.affected_rooms
OR NEW.affected_room_nights <> OLD.affected_room_nights
) THEN
RAISE EXCEPTION 'daily review pricing key and impact are immutable';
END IF;
IF TG_OP = 'DELETE' THEN
RETURN OLD;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER daily_review_items_mutability_guard
BEFORE UPDATE OR DELETE ON ingestion.daily_review_items
FOR EACH ROW EXECUTE FUNCTION ingestion.enforce_daily_review_item_mutability();
CREATE OR REPLACE FUNCTION ingestion.enforce_daily_review_case_immutability()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF TG_OP = 'DELETE' THEN
RAISE EXCEPTION 'daily review cases are permanent audit records';
END IF;
IF OLD.frozen_at IS NOT NULL AND (
NEW.manual_override_json IS DISTINCT FROM OLD.manual_override_json
OR NEW.manual_override_sha256 IS DISTINCT FROM OLD.manual_override_sha256
OR (
OLD.manual_override_artifact_id IS NOT NULL
AND NEW.manual_override_artifact_id IS DISTINCT FROM OLD.manual_override_artifact_id
)
OR NEW.source_sha256 IS DISTINCT FROM OLD.source_sha256
OR NEW.business_date IS DISTINCT FROM OLD.business_date
OR NEW.processor_version IS DISTINCT FROM OLD.processor_version
OR NEW.rule_set_sha256 IS DISTINCT FROM OLD.rule_set_sha256
OR NEW.review_version IS DISTINCT FROM OLD.review_version
) THEN
RAISE EXCEPTION 'frozen manual override provenance is immutable';
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER daily_review_cases_immutability_guard
BEFORE UPDATE OR DELETE ON ingestion.daily_review_cases
FOR EACH ROW EXECUTE FUNCTION ingestion.enforce_daily_review_case_immutability();
CREATE OR REPLACE FUNCTION ingestion.enforce_daily_review_event_immutability()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
RAISE EXCEPTION 'daily review events are append-only audit records';
END;
$$;
CREATE TRIGGER daily_review_events_immutability_guard
BEFORE UPDATE OR DELETE ON ingestion.daily_review_events
FOR EACH ROW EXECUTE FUNCTION ingestion.enforce_daily_review_event_immutability();
ALTER TABLE finance.daily_versions
ADD COLUMN review_case_id bigint REFERENCES ingestion.daily_review_cases(id),
ADD COLUMN manual_override_sha256 character(64) CHECK (
manual_override_sha256 IS NULL OR manual_override_sha256 ~ '^[0-9a-f]{64}$'
),
ADD COLUMN manually_priced_rows integer NOT NULL DEFAULT 0 CHECK (manually_priced_rows >= 0),
ADD CONSTRAINT daily_versions_review_lineage_shape CHECK (
(review_case_id IS NULL AND manual_override_sha256 IS NULL AND manually_priced_rows = 0)
OR (review_case_id IS NOT NULL AND manual_override_sha256 IS NOT NULL AND manually_priced_rows > 0)
);
CREATE UNIQUE INDEX daily_versions_review_case_unique
ON finance.daily_versions (review_case_id)
WHERE review_case_id IS NOT NULL;
DROP INDEX IF EXISTS finance.daily_versions_source_rule_unique;
CREATE UNIQUE INDEX daily_versions_source_rule_override_unique
ON finance.daily_versions (
source_artifact_id,
business_date,
processor_version,
rule_set_sha256,
COALESCE(manual_override_sha256, '')
)
WHERE business_date IS NOT NULL;
ALTER TABLE finance.daily_records
DROP CONSTRAINT IF EXISTS daily_records_retained_values;
ALTER TABLE finance.daily_records
ADD CONSTRAINT daily_records_retained_values CHECK (
outcome <> 'retained'
OR (
block_code IS NOT NULL
AND adults IS NOT NULL AND adults >= 0
AND children IS NOT NULL AND children >= 0
AND company_name IS NOT NULL AND btrim(company_name) <> ''
AND company_key IS NOT NULL AND btrim(company_key) <> ''
AND confirmation_no IS NOT NULL AND btrim(confirmation_no) <> ''
AND disp_room_no IS NOT NULL AND btrim(disp_room_no) <> ''
AND effective_rate_amount IS NOT NULL AND effective_rate_amount >= 0
AND full_name IS NOT NULL
AND no_of_rooms IS NOT NULL AND no_of_rooms > 0
AND rate_code IS NOT NULL
AND normalized_rate_code IS NOT NULL
AND arrival IS NOT NULL
AND departure IS NOT NULL
AND nights IS NOT NULL AND nights >= 0
AND real_price IS NOT NULL AND real_price >= 0
AND total_price IS NOT NULL AND total_price >= 0
AND channel_key IS NOT NULL AND btrim(channel_key) <> ''
AND pricing_method IN (
'price_reference_exact', 'zero_price_exception', 'manual_review'
)
)
);
COMMENT ON TABLE ingestion.daily_review_cases IS
'One immutable-audit manual price review case per XML processing run. No guest names, comments, traces or raw rows are stored here.';
COMMENT ON TABLE ingestion.daily_review_items IS
'One staff-editable non-negative price per normalized missing fixed-price key; mutable only while its case is open.';
COMMENT ON TABLE ingestion.daily_review_events IS
'Append-only actor, revision and before/after-price audit history. Intentionally has no notes field.';
COMMENT ON COLUMN finance.daily_versions.manual_override_sha256 IS
'SHA-256 of the canonical frozen review manifest used for independently validated final replay.';
-- The application needs no DELETE privilege over review facts. Deployments that
-- use a differently named login role keep their existing grant model unchanged.
DO $$
BEGIN
IF to_regrole('arr_app') IS NOT NULL THEN
EXECUTE 'GRANT SELECT, INSERT, UPDATE ON ingestion.daily_review_cases, ingestion.daily_review_items TO arr_app';
EXECUTE 'GRANT SELECT, INSERT ON ingestion.daily_review_events TO arr_app';
EXECUTE 'GRANT USAGE, SELECT ON SEQUENCE ingestion.daily_review_cases_id_seq, ingestion.daily_review_items_id_seq, ingestion.daily_review_events_id_seq TO arr_app';
END IF;
END;
$$;
COMMIT;
@@ -0,0 +1,59 @@
-- Restore the pre-018 artifact-kind check only while no frozen manual manifest
-- has been registered. Once such an immutable fact exists, use a forward fix.
BEGIN;
DO $$
DECLARE
kind_check text;
BEGIN
IF current_database() <> 'booking_test' THEN
RAISE EXCEPTION
'ARR manual-override artifact rollback is allowed only in booking_test';
END IF;
IF to_regclass('ingestion.artifacts') IS NULL
OR to_regclass('ingestion.daily_review_cases') IS NULL THEN
RAISE EXCEPTION 'ARR migration 018 prerequisite is unavailable';
END IF;
SELECT pg_get_constraintdef(oid)
INTO kind_check
FROM pg_constraint
WHERE conrelid = 'ingestion.artifacts'::regclass
AND conname = 'artifacts_artifact_kind_check';
IF kind_check IS NULL
OR position('manual_override_json' IN kind_check) = 0 THEN
RAISE EXCEPTION 'ARR migration 018 is not applied';
END IF;
IF EXISTS (
SELECT 1
FROM ingestion.artifacts
WHERE artifact_kind = 'manual_override_json'
) THEN
RAISE EXCEPTION
'refusing rollback: immutable manual-override artifacts exist';
END IF;
END;
$$;
ALTER TABLE ingestion.artifacts
DROP CONSTRAINT artifacts_artifact_kind_check;
ALTER TABLE ingestion.artifacts
ADD CONSTRAINT artifacts_artifact_kind_check CHECK (artifact_kind IN (
'booking_source_md',
'booking_excel',
'opera_xml',
'daily_xlsx',
'monthly_xlsx',
'channel_detail_xlsx',
'company_ten_day_xlsx',
'exception_xlsx',
'result_json',
'structured_result_json'
));
COMMENT ON CONSTRAINT artifacts_artifact_kind_check ON ingestion.artifacts IS NULL;
COMMIT;
@@ -0,0 +1,75 @@
-- Allow the frozen manual-price manifest to be registered as an immutable
-- ingestion artifact. Migration 017 added the reference columns but did not
-- extend the pre-existing artifact-kind check.
BEGIN;
DO $$
DECLARE
kind_check text;
BEGIN
IF current_database() <> 'booking_test' THEN
RAISE EXCEPTION
'ARR manual-override artifact migration is allowed only in booking_test';
END IF;
IF to_regclass('ingestion.artifacts') IS NULL
OR to_regclass('ingestion.daily_review_cases') IS NULL THEN
RAISE EXCEPTION 'ARR migration 017 must be applied first';
END IF;
SELECT pg_get_constraintdef(oid)
INTO kind_check
FROM pg_constraint
WHERE conrelid = 'ingestion.artifacts'::regclass
AND conname = 'artifacts_artifact_kind_check';
IF kind_check IS NULL THEN
RAISE EXCEPTION 'ingestion artifact-kind constraint is missing';
END IF;
IF position('manual_override_json' IN kind_check) > 0 THEN
RAISE EXCEPTION 'ARR migration 018 is already applied';
END IF;
IF EXISTS (
SELECT 1
FROM ingestion.artifacts
WHERE artifact_kind NOT IN (
'booking_source_md',
'booking_excel',
'opera_xml',
'daily_xlsx',
'monthly_xlsx',
'channel_detail_xlsx',
'company_ten_day_xlsx',
'exception_xlsx',
'result_json',
'structured_result_json'
)
) THEN
RAISE EXCEPTION
'unexpected artifact kind exists; refusing to replace its constraint';
END IF;
END;
$$;
ALTER TABLE ingestion.artifacts
DROP CONSTRAINT artifacts_artifact_kind_check;
ALTER TABLE ingestion.artifacts
ADD CONSTRAINT artifacts_artifact_kind_check CHECK (artifact_kind IN (
'booking_source_md',
'booking_excel',
'opera_xml',
'daily_xlsx',
'monthly_xlsx',
'channel_detail_xlsx',
'company_ten_day_xlsx',
'exception_xlsx',
'result_json',
'structured_result_json',
'manual_override_json'
));
COMMENT ON CONSTRAINT artifacts_artifact_kind_check ON ingestion.artifacts IS
'Immutable ARR artifact roles, including frozen Daily manual-price manifests.';
COMMIT;
+48 -4
View File
@@ -1,7 +1,7 @@
# ARR 测试数据库已执行记录
更新时间:2026-08-04
状态:008–015 已提交;014/015 已通过迁移回滚探针、正式应用、结构/数据复核及真实 PostgreSQL 草稿激活外层回滚验收。016 已提交代码、待在测试机 booking_test 应用并完成 OSS 月报验收。首次真实业务工作簿激活仍待操作员授权。
更新时间:2026-08-06
状态:008–018 已提交。016 的实时函数体和注释已于 2026-08-06 与代码语义核对一致,但早期应用时间未留在本台账,本次没有重跑 016。017 已完成隐私最小化备份、修正后的 up/down 外层事务回滚探针、正式应用、结构/数据复核和真实 `0805.XML` 初始待复核验收。018 已通过独立备份、精确 up/down 回滚探针和最终化外层事务竖切后正式应用。最新冻结 case 的员工重试与最终 Finance/月报验收仍待显式操作;首次真实 Booking 业务工作簿激活也仍待操作员授权。
## 目标与隔离
@@ -29,7 +29,51 @@
| `013_daily_upload_filename.sql` | `f7ea18d6b844d9bd90fa757a4cf8428d1dd5833c088ae23444ac81fbe204fb0a` | 已提交并验收 |
| `014_booking_current_source_batch.sql` | `23bf0fcc880225ca276d4d7d871057be4f7950e761ddbeed1df2a3c6e25463b5` | 2026-07-31 已通过同事务回滚探针后由受控连接正式应用;数据复核通过 |
| `015_booking_excel_review_drafts.sql` | `a80689c4ecc3b6b3502e8f094a8c175af38df8c09f9d2c64412a9aec55bae12a` | 2026-07-31 已由受控连接正式应用;真实 PostgreSQL 草稿编辑/激活外层回滚通过 |
| `016_monthly_report_oss_artifacts.sql` | `70ca052f71da9f88e83fe8bccf0a6682cdd3138c3525981c80eebc868e401626` | 代码已提交;尚未在远程 `booking_test` 应用 |
| `016_monthly_report_oss_artifacts.sql` | `70ca052f71da9f88e83fe8bccf0a6682cdd3138c3525981c80eebc868e401626` | 实时函数体/注释于 2026-08-06 核对为已生效;历史应用时间未记录,本次未重跑 |
| `017_daily_price_review.sql` | `22a0578e8748573d29ff2c15dbc687aaea14d7dc0c34cf8fe1faa2ef56067b5b` | 2026-08-06 通过修正后的 up/down 回滚探针后由受控连接正式应用;初始真实复核验收通过 |
| `018_daily_review_manual_override_artifact.sql` | `5600d82597304d66ffb4933df4b375406080484dfbc7717c360b48a9e28ccd00` | 2026-08-06 通过精确 up/down 与最终化外层回滚探针后由受控连接正式应用;既有业务事实不变 |
## 017 日报价格人工复核(已部署,最终人工定价待验收)
017 为 `artifact_callback/fixed_processor` 的 v4 `PRICE_UNMATCHED` 专项能力增加:
- `awaiting_review` run、`review_required` attempt/delivery 与 `recorded_review` receipt;初始复核不创建
`finance.daily_versions`、日报下载、`arr.processing_failed` 或月报 outbox;
- `ingestion.daily_review_cases`、items、append-only events,含服务端 actor、revision、前后价格和冻结清单
provenance,但不保存姓名、备注或 raw trace;
- 最终成功才在现有原子事务内写入 Finance review lineage、`manual_review` retained facts 和一次
`arr.daily_version_committed`;可重试基础设施错误保留同一清单,确定性最终错误关闭 case/run;
- 最小权限授予无 DELETE;down 文件一旦存在任意 review/manual 事实即拒绝破坏性回滚。
`017_daily_price_review.down.sql` SHA-256:`9e85af0cedb03acafed84623c12c2719ee36ddcbb5012c27c1f2379e0ee29101`。
正式应用使用 checkpoint `runtime/backups/booking_test_pre_017_20260806T192026+0800/manifest.json`(SHA-256
`d66cc4341db42420b9382fa78c90214f5ecb3c200062c0ac4bb8d275a7123b17`)。探针仅在内存中移除 up/down 最外层
`BEGIN`/`COMMIT`,将两份正文放进同一外层事务,验证 up 中间态、down 精确恢复和第二连接零残留后回滚;正式
应用才执行原始 up 文件的自带事务。首个探针发现并回滚了对 010 `direct_mcp` 终态约束的漂移,修正后才正式
应用。现已存在真实 review case,down 会按设计拒绝破坏性回滚,后续只能使用前向修复迁移。
运行态验收:runs 61–63 已通过正常 repository 入口关闭为 `REVIEW_SCHEMA_MIGRATION_MISSING`;run 64 安全记录
了随后发现并修复的 delivery 占位符错误;run 65 为 `awaiting_review`,含两个未填写价格项和一个
`PRICE_REVIEW_REQUIRED` 事件。它没有日报、Finance 版本、run outbox 或新增月报事件,历史版本 28 保持 rejected。
## 018 人工清单工件约束修复(已部署,员工重试待验收)
017 增加了人工清单引用列,但遗漏了 `manual_override_json` 工件类型。018 只重建
`ingestion.artifacts_artifact_kind_check`,在原十类工件基础上增加这一类;它不新增表、列、业务行或权限。
up 文件拒绝非 `booking_test`、缺失 017、未知既有类型和重复应用。down 文件 SHA-256 为
`514ba908059bd8eadedd9a38da6efae85efb378ccfca0395028eae1fed22b92f`,一旦存在不可变人工清单即拒绝回滚。
正式应用使用 checkpoint
`runtime/backups/booking_test_pre_018_20260806T210250+0800/manifest.json`(SHA-256
`fcc24362d43c5982590a6566276e6ac7d1d172da2f2f040e8e54a063ca7e2b16`)。up/down 外层事务探针恢复精确前态
且零残留;正式应用证据 SHA-256 为 `651dc998d471734ebfc1ebd5ba81f5a5da57942879676aa1747fe5bcb2d24d84`。
应用后,一个真实 PostgreSQL 外层事务竖切完成复核登记、整数 `0` 输入、`0.00` 清单冻结、Finance 激活和单一
commit event,然后整体回滚为零残留;证据 SHA-256 为
`857cee393fa232ea4cbd85913f5069948246acdace6add39c019bd91787711dd`。
当前 live 状态未自动重试:run 66 冻结 GRP1/LBLT `2300/0`,最新 run 67 冻结 `200/0`,两者均为
`generation_failed` 且没有 Finance 版本或 run outbox;run 65 仍未填写。员工必须刷新登录后核对并只重试
预期的最新 case,历史版本 28 保持 rejected。
## 014/015 最新核查状态
@@ -53,7 +97,7 @@
- 整个重建在一个事务内完成;
- 不触碰 `public` 或其他数据库。
当前对象:23 张基础表(`ingestion=8`、`booking=8`、`finance=4`、`reporting=3`)和 8 个视图;012 另增加月报发布/子表不可变保护触发器。
当前对象:26 张基础表(`ingestion=11`、`booking=8`、`finance=4`、`reporting=3`)和 8 个视图;012 增加月报发布/子表不可变保护触发器,017 增加三张价格复核表及其不可变保护触发器。
## 009/010 增量迁移
@@ -0,0 +1,56 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://arr.local/schemas/arr-direct-legacy-structured-result-v3.json",
"title": "Retired ARR direct-MCP structured result v3",
"description": "Read-only compatibility contract for the retired direct-MCP success route. It intentionally has no manual-review fields or review-required status.",
"type": "object",
"additionalProperties": false,
"required": [
"result_schema_version",
"status",
"activation_eligible",
"ingestion_mode",
"business_date",
"processor_version",
"rule_set_sha256",
"source_rows",
"removed_by_rate_code",
"removed_as_duplicates",
"output_rows",
"outcome_counts",
"channels",
"artifacts",
"records",
"errors"
],
"properties": {
"result_schema_version": { "type": "string", "const": "3.0" },
"status": { "type": "string", "const": "success" },
"activation_eligible": { "type": "boolean", "const": true },
"ingestion_mode": { "type": "string", "const": "opera_xml" },
"business_date": { "type": "string", "format": "date" },
"processor_version": { "type": "string", "const": "3.0.0" },
"rule_set_sha256": { "$ref": "#/$defs/sha256" },
"source_rows": { "type": "integer", "minimum": 0 },
"removed_by_rate_code": { "type": "integer", "minimum": 0 },
"removed_as_duplicates": { "type": "integer", "minimum": 0 },
"output_rows": { "type": "integer", "minimum": 0 },
"outcome_counts": { "type": "object" },
"channels": { "type": "array", "items": { "$ref": "#/$defs/channel" } },
"artifacts": { "type": "object" },
"records": { "type": "array", "items": { "type": "object" } },
"errors": { "type": "array", "maxItems": 0 }
},
"$defs": {
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
"channel": {
"type": "object",
"additionalProperties": false,
"required": ["worksheet", "rows"],
"properties": {
"worksheet": { "type": "string", "minLength": 1 },
"rows": { "type": "integer", "minimum": 0 }
}
}
}
}
@@ -28,7 +28,7 @@
"payload": {
"allOf": [
{
"$ref": "../../arr-opera-daily-ingest/references/structured-result.schema.json"
"$ref": "arr-direct-legacy-structured-result-v3.schema.json"
},
{
"properties": {