feat: add daily manual price review workflow
This commit is contained in:
1 parent
aae3d8e1db
commit
ca3e8e18fa
77 files changed
+7750
-602
No files matched your search
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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": {
|
||||
|
||||
Reference in new issue
Block a user