feat: sync latest ARR implementation
This commit is contained in:
34
database/011_artifact_object_identity.down.sql
Normal file
34
database/011_artifact_object_identity.down.sql
Normal file
@@ -0,0 +1,34 @@
|
||||
-- Manual rollback for migration 011.
|
||||
-- Refuses once equal bytes have been registered under multiple object identities.
|
||||
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF current_database() <> 'booking_test' THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR artifact identity rollback is allowed only in booking_test';
|
||||
END IF;
|
||||
IF to_regclass('ingestion.artifacts_kind_hash_idx') IS NULL THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR artifact identity migration is not applied';
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM ingestion.artifacts
|
||||
GROUP BY artifact_kind, sha256
|
||||
HAVING count(*) > 1
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'refusing rollback: repeated artifact content exists across storage identities';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP INDEX ingestion.artifacts_kind_hash_idx;
|
||||
|
||||
ALTER TABLE ingestion.artifacts
|
||||
ADD CONSTRAINT artifacts_kind_hash_unique
|
||||
UNIQUE (artifact_kind, sha256);
|
||||
|
||||
COMMIT;
|
||||
42
database/011_artifact_object_identity.sql
Normal file
42
database/011_artifact_object_identity.sql
Normal file
@@ -0,0 +1,42 @@
|
||||
-- Preserve one artifact row per immutable storage object identity.
|
||||
-- PostgreSQL 15+. Migrations 008-010 remain immutable.
|
||||
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF current_database() <> 'booking_test' THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR artifact identity migration is allowed only in booking_test';
|
||||
END IF;
|
||||
IF to_regclass('ingestion.artifacts') IS NULL
|
||||
OR to_regclass('ingestion.result_submissions') IS NULL THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR migrations 008 through 010 must be applied first';
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'ingestion.artifacts'::regclass
|
||||
AND conname = 'artifacts_kind_hash_unique'
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR artifact identity migration is already applied or the source schema is invalid';
|
||||
END IF;
|
||||
IF to_regclass('ingestion.artifacts_kind_hash_idx') IS NOT NULL THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR artifact identity lookup index already exists';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
ALTER TABLE ingestion.artifacts
|
||||
DROP CONSTRAINT artifacts_kind_hash_unique;
|
||||
|
||||
CREATE INDEX artifacts_kind_hash_idx
|
||||
ON ingestion.artifacts (artifact_kind, sha256);
|
||||
|
||||
COMMENT ON INDEX ingestion.artifacts_kind_hash_idx IS
|
||||
'Non-unique content lookup. Equal bytes may belong to different immutable OSS object identities and processing runs.';
|
||||
|
||||
COMMIT;
|
||||
37
database/012_monthly_report_publication.down.sql
Normal file
37
database/012_monthly_report_publication.down.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
-- Roll back monthly publication metadata only before it contains report or local-artifact data.
|
||||
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF current_database() <> 'booking_test' THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR monthly publication rollback is allowed only in booking_test';
|
||||
END IF;
|
||||
IF to_regnamespace('reporting') IS NULL THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR monthly publication migration is not applied';
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM reporting.monthly_runs)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM ingestion.artifacts
|
||||
WHERE storage_provider = 'local'
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'refusing rollback: monthly publication or controlled local artifact data exists';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP SCHEMA reporting CASCADE;
|
||||
|
||||
ALTER TABLE ingestion.artifacts
|
||||
DROP CONSTRAINT artifacts_storage_provider_check;
|
||||
|
||||
ALTER TABLE ingestion.artifacts
|
||||
ADD CONSTRAINT artifacts_storage_provider_check CHECK (
|
||||
storage_provider IN ('oss', 's3', 'local_fixture')
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
281
database/012_monthly_report_publication.sql
Normal file
281
database/012_monthly_report_publication.sql
Normal file
@@ -0,0 +1,281 @@
|
||||
-- Durable metadata-only monthly report publication for ARR2.
|
||||
-- PostgreSQL 15+. Migrations 008-011 remain immutable.
|
||||
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF current_database() <> 'booking_test' THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR monthly publication migration is allowed only in booking_test';
|
||||
END IF;
|
||||
IF to_regclass('ingestion.artifacts') IS NULL
|
||||
OR to_regclass('ingestion.outbox_events') IS NULL
|
||||
OR to_regclass('finance.daily_versions') IS NULL THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR migrations 008 through 011 must be applied first';
|
||||
END IF;
|
||||
IF to_regnamespace('reporting') IS NOT NULL THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR monthly publication migration is already applied';
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conrelid = 'ingestion.artifacts'::regclass
|
||||
AND conname = 'artifacts_storage_provider_check'
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'ingestion.artifacts storage-provider constraint is missing';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
ALTER TABLE ingestion.artifacts
|
||||
DROP CONSTRAINT artifacts_storage_provider_check;
|
||||
|
||||
ALTER TABLE ingestion.artifacts
|
||||
ADD CONSTRAINT artifacts_storage_provider_check CHECK (
|
||||
storage_provider IN ('oss', 's3', 'local_fixture', 'local')
|
||||
);
|
||||
|
||||
CREATE SCHEMA reporting;
|
||||
|
||||
COMMENT ON SCHEMA reporting IS
|
||||
'Metadata-only publication lifecycle and lineage for derived ARR reports; business rows remain in Finance facts.';
|
||||
|
||||
CREATE TABLE reporting.monthly_runs (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
period_start date NOT NULL,
|
||||
as_of_date date NOT NULL,
|
||||
version_no integer NOT NULL CHECK (version_no > 0),
|
||||
source_snapshot_sha256 character(64) NOT NULL CHECK (
|
||||
source_snapshot_sha256 ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
report_status text NOT NULL CHECK (report_status IN (
|
||||
'reserved', 'active', 'superseded', '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 (
|
||||
btrim(result_schema_version) <> ''
|
||||
),
|
||||
row_count integer NOT NULL CHECK (row_count >= 0),
|
||||
channel_count integer NOT NULL CHECK (channel_count >= 5),
|
||||
workbook_artifact_id bigint REFERENCES ingestion.artifacts(id),
|
||||
result_artifact_id bigint REFERENCES ingestion.artifacts(id),
|
||||
semantic_sha256 character(64) CHECK (
|
||||
semantic_sha256 IS NULL OR semantic_sha256 ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
failure_code text,
|
||||
failure_message text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
published_at timestamptz,
|
||||
superseded_at timestamptz,
|
||||
failed_at timestamptz,
|
||||
CONSTRAINT monthly_runs_period_shape CHECK (
|
||||
period_start = date_trunc('month', period_start)::date
|
||||
AND as_of_date >= period_start
|
||||
AND as_of_date < (period_start + interval '1 month')::date
|
||||
),
|
||||
CONSTRAINT monthly_runs_period_version_unique
|
||||
UNIQUE (period_start, version_no),
|
||||
CONSTRAINT monthly_runs_snapshot_unique
|
||||
UNIQUE (period_start, source_snapshot_sha256),
|
||||
CONSTRAINT monthly_runs_status_shape CHECK (
|
||||
(
|
||||
report_status = 'reserved'
|
||||
AND workbook_artifact_id IS NULL
|
||||
AND result_artifact_id IS NULL
|
||||
AND semantic_sha256 IS NULL
|
||||
AND failure_code IS NULL
|
||||
AND failure_message IS NULL
|
||||
AND published_at IS NULL
|
||||
AND superseded_at IS NULL
|
||||
AND failed_at IS NULL
|
||||
)
|
||||
OR (
|
||||
report_status = 'active'
|
||||
AND workbook_artifact_id IS NOT NULL
|
||||
AND result_artifact_id IS NOT NULL
|
||||
AND semantic_sha256 IS NOT NULL
|
||||
AND failure_code IS NULL
|
||||
AND failure_message IS NULL
|
||||
AND published_at IS NOT NULL
|
||||
AND superseded_at IS NULL
|
||||
AND failed_at IS NULL
|
||||
)
|
||||
OR (
|
||||
report_status = 'superseded'
|
||||
AND workbook_artifact_id IS NOT NULL
|
||||
AND result_artifact_id IS NOT NULL
|
||||
AND semantic_sha256 IS NOT NULL
|
||||
AND failure_code IS NULL
|
||||
AND failure_message IS NULL
|
||||
AND published_at IS NOT NULL
|
||||
AND superseded_at IS NOT NULL
|
||||
AND failed_at IS NULL
|
||||
)
|
||||
OR (
|
||||
report_status = 'failed'
|
||||
AND workbook_artifact_id IS NULL
|
||||
AND result_artifact_id IS NULL
|
||||
AND semantic_sha256 IS NULL
|
||||
AND failure_code IS NOT NULL
|
||||
AND btrim(failure_code) <> ''
|
||||
AND published_at IS NULL
|
||||
AND superseded_at IS NULL
|
||||
AND failed_at IS NOT NULL
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX monthly_runs_one_active_period_idx
|
||||
ON reporting.monthly_runs (period_start)
|
||||
WHERE report_status = 'active';
|
||||
|
||||
CREATE INDEX monthly_runs_period_history_idx
|
||||
ON reporting.monthly_runs (period_start, version_no DESC);
|
||||
|
||||
CREATE TABLE reporting.monthly_run_daily_versions (
|
||||
report_id bigint NOT NULL REFERENCES reporting.monthly_runs(id),
|
||||
business_date date NOT NULL,
|
||||
daily_version_id bigint NOT NULL,
|
||||
PRIMARY KEY (report_id, business_date),
|
||||
CONSTRAINT monthly_run_daily_versions_version_unique
|
||||
UNIQUE (report_id, daily_version_id),
|
||||
CONSTRAINT monthly_run_daily_versions_version_fk
|
||||
FOREIGN KEY (daily_version_id, business_date)
|
||||
REFERENCES finance.daily_versions(id, business_date)
|
||||
);
|
||||
|
||||
CREATE TABLE reporting.monthly_channel_manifest (
|
||||
report_id bigint NOT NULL REFERENCES reporting.monthly_runs(id),
|
||||
worksheet text NOT NULL CHECK (
|
||||
btrim(worksheet) <> ''
|
||||
AND char_length(worksheet) <= 31
|
||||
AND worksheet !~ '[:\\/?*\[\]]'
|
||||
),
|
||||
worksheet_order integer NOT NULL CHECK (worksheet_order > 0),
|
||||
row_count integer NOT NULL CHECK (row_count >= 0),
|
||||
PRIMARY KEY (report_id, worksheet),
|
||||
CONSTRAINT monthly_channel_manifest_order_unique
|
||||
UNIQUE (report_id, worksheet_order)
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION reporting.validate_monthly_run_publication()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
workbook_kind text;
|
||||
workbook_provider text;
|
||||
result_kind text;
|
||||
result_provider text;
|
||||
manifest_count integer;
|
||||
manifest_rows bigint;
|
||||
manifest_min integer;
|
||||
manifest_max integer;
|
||||
lineage_count integer;
|
||||
BEGIN
|
||||
IF NEW.report_status NOT IN ('active', 'superseded') THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
SELECT artifact_kind, storage_provider
|
||||
INTO workbook_kind, workbook_provider
|
||||
FROM ingestion.artifacts
|
||||
WHERE id = NEW.workbook_artifact_id;
|
||||
|
||||
SELECT artifact_kind, storage_provider
|
||||
INTO result_kind, result_provider
|
||||
FROM ingestion.artifacts
|
||||
WHERE id = NEW.result_artifact_id;
|
||||
|
||||
IF workbook_kind IS DISTINCT FROM 'monthly_xlsx'
|
||||
OR workbook_provider IS DISTINCT FROM 'local'
|
||||
OR result_kind IS DISTINCT FROM 'result_json'
|
||||
OR result_provider IS DISTINCT FROM 'local' THEN
|
||||
RAISE EXCEPTION
|
||||
'published monthly run must reference controlled local monthly_xlsx and result_json artifacts';
|
||||
END IF;
|
||||
|
||||
SELECT count(*), COALESCE(sum(row_count), 0), min(worksheet_order), max(worksheet_order)
|
||||
INTO manifest_count, manifest_rows, manifest_min, manifest_max
|
||||
FROM reporting.monthly_channel_manifest
|
||||
WHERE report_id = NEW.id;
|
||||
|
||||
IF manifest_count <> NEW.channel_count
|
||||
OR manifest_rows <> NEW.row_count
|
||||
OR manifest_min <> 1
|
||||
OR manifest_max <> manifest_count THEN
|
||||
RAISE EXCEPTION
|
||||
'published monthly run requires a continuous reconciled channel manifest';
|
||||
END IF;
|
||||
|
||||
SELECT count(*)
|
||||
INTO lineage_count
|
||||
FROM reporting.monthly_run_daily_versions
|
||||
WHERE report_id = NEW.id;
|
||||
|
||||
IF lineage_count < 1 THEN
|
||||
RAISE EXCEPTION
|
||||
'published monthly run requires daily-version lineage';
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER monthly_runs_publication_guard
|
||||
BEFORE INSERT OR UPDATE ON reporting.monthly_runs
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION reporting.validate_monthly_run_publication();
|
||||
|
||||
CREATE OR REPLACE FUNCTION reporting.protect_published_monthly_child()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
parent_id bigint;
|
||||
BEGIN
|
||||
parent_id := COALESCE(OLD.report_id, NEW.report_id);
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM reporting.monthly_runs AS run
|
||||
WHERE run.id = parent_id
|
||||
AND run.report_status IN ('active', 'superseded')
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'published monthly report lineage and manifest are immutable';
|
||||
END IF;
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER monthly_run_daily_versions_immutable
|
||||
BEFORE UPDATE OR DELETE ON reporting.monthly_run_daily_versions
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION reporting.protect_published_monthly_child();
|
||||
|
||||
CREATE TRIGGER monthly_channel_manifest_immutable
|
||||
BEFORE UPDATE OR DELETE ON reporting.monthly_channel_manifest
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION reporting.protect_published_monthly_child();
|
||||
|
||||
COMMENT ON TABLE reporting.monthly_runs IS
|
||||
'Durable publication versions and artifact identities only; no guest-bearing monthly business rows are duplicated.';
|
||||
|
||||
COMMENT ON COLUMN reporting.monthly_runs.as_of_date IS
|
||||
'Greatest ARRIVAL included in this publication; never derived from an upload filename or wall-clock date.';
|
||||
|
||||
COMMENT ON TABLE reporting.monthly_run_daily_versions IS
|
||||
'Immutable current Finance daily-version pins used to build one monthly publication.';
|
||||
|
||||
COMMENT ON TABLE reporting.monthly_channel_manifest IS
|
||||
'Privacy-minimized worksheet order and row-count manifest for one monthly publication.';
|
||||
|
||||
COMMIT;
|
||||
37
database/013_daily_upload_filename.down.sql
Normal file
37
database/013_daily_upload_filename.down.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
-- Manual rollback for migration 013.
|
||||
-- Refuses to discard user-facing filename provenance once new uploads exist.
|
||||
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF current_database() <> 'booking_test' THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR daily upload filename rollback is allowed only in booking_test';
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'ingestion'
|
||||
AND table_name = 'processing_runs'
|
||||
AND column_name = 'uploaded_filename'
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR daily upload filename migration is not applied';
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM ingestion.processing_runs
|
||||
WHERE uploaded_filename IS NOT NULL
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'refusing rollback: uploaded filename provenance exists';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
ALTER TABLE ingestion.processing_runs
|
||||
DROP CONSTRAINT processing_runs_uploaded_filename_check,
|
||||
DROP COLUMN uploaded_filename;
|
||||
|
||||
COMMIT;
|
||||
47
database/013_daily_upload_filename.sql
Normal file
47
database/013_daily_upload_filename.sql
Normal file
@@ -0,0 +1,47 @@
|
||||
-- Preserve the browser-supplied XML basename separately from the canonical source artifact name.
|
||||
-- PostgreSQL 15+. Migrations 008-012 remain immutable.
|
||||
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF current_database() <> 'booking_test' THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR daily upload filename migration is allowed only in booking_test';
|
||||
END IF;
|
||||
IF to_regclass('ingestion.processing_runs') IS NULL
|
||||
OR to_regclass('reporting.monthly_runs') IS NULL THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR migrations 008 through 012 must be applied first';
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'ingestion'
|
||||
AND table_name = 'processing_runs'
|
||||
AND column_name = 'uploaded_filename'
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR daily upload filename migration is already applied';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
ALTER TABLE ingestion.processing_runs
|
||||
ADD COLUMN uploaded_filename text;
|
||||
|
||||
ALTER TABLE ingestion.processing_runs
|
||||
ADD CONSTRAINT processing_runs_uploaded_filename_check CHECK (
|
||||
uploaded_filename IS NULL
|
||||
OR (
|
||||
char_length(uploaded_filename) BETWEEN 1 AND 255
|
||||
AND uploaded_filename NOT IN ('.', '..')
|
||||
AND uploaded_filename !~ '[[:cntrl:]/\\]'
|
||||
AND right(lower(uploaded_filename), 4) = '.xml'
|
||||
)
|
||||
);
|
||||
|
||||
COMMENT ON COLUMN ingestion.processing_runs.uploaded_filename IS
|
||||
'Validated browser-supplied XML basename for user-facing provenance; NULL on historical runs. Internal source artifacts remain source.xml.';
|
||||
|
||||
COMMIT;
|
||||
72
database/014_booking_current_source_batch.down.sql
Normal file
72
database/014_booking_current_source_batch.down.sql
Normal file
@@ -0,0 +1,72 @@
|
||||
-- Manual rollback for migration 014.
|
||||
-- Refuses once real Booking Excel data exists because removing the pointer would aggregate full batches together.
|
||||
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF current_database() <> 'booking_test' THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR Booking current-source rollback is allowed only in booking_test';
|
||||
END IF;
|
||||
IF to_regclass('booking.current_source_batch') IS NULL THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR Booking current-source migration is not applied';
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM booking.source_batches
|
||||
WHERE source_kind = 'booking_excel'
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'refusing rollback: Booking Excel source history exists';
|
||||
END IF;
|
||||
IF (
|
||||
SELECT count(*)
|
||||
FROM booking.source_batches
|
||||
WHERE batch_status = 'accepted'
|
||||
) > 1 THEN
|
||||
RAISE EXCEPTION
|
||||
'refusing rollback: multiple accepted Booking source batches exist';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE VIEW booking.v_current_room_items AS
|
||||
SELECT
|
||||
source.id AS source_row_id,
|
||||
source.source_batch_id,
|
||||
source.source_worksheet,
|
||||
source.source_row_no,
|
||||
source.group_code_raw,
|
||||
source.group_code_key,
|
||||
source.type_of_room_raw,
|
||||
source.no_of_rooms AS source_no_of_rooms,
|
||||
parsed.id AS parse_version_id,
|
||||
parsed.version_no AS parse_version_no,
|
||||
item.item_no,
|
||||
item.room_type_raw,
|
||||
item.room_type_code,
|
||||
item.quantity,
|
||||
item.unit_price,
|
||||
item.currency_code,
|
||||
item.price_token_raw,
|
||||
item.source_fragment
|
||||
FROM booking.current_row_parses AS current_parse
|
||||
JOIN booking.parse_versions AS parsed
|
||||
ON parsed.id = current_parse.parse_version_id
|
||||
JOIN booking.source_rows AS source
|
||||
ON source.id = current_parse.source_row_id
|
||||
JOIN booking.source_batches AS batch
|
||||
ON batch.id = source.source_batch_id
|
||||
JOIN booking.room_items AS item
|
||||
ON item.parse_version_id = parsed.id
|
||||
WHERE batch.batch_status = 'accepted'
|
||||
AND parsed.parse_status = 'accepted';
|
||||
|
||||
DROP TRIGGER current_source_batch_acceptance_guard
|
||||
ON booking.current_source_batch;
|
||||
DROP FUNCTION booking.validate_current_source_batch();
|
||||
DROP TABLE booking.current_source_batch;
|
||||
|
||||
COMMIT;
|
||||
118
database/014_booking_current_source_batch.sql
Normal file
118
database/014_booking_current_source_batch.sql
Normal file
@@ -0,0 +1,118 @@
|
||||
-- Add one explicit active Booking source batch for full-workbook replacement imports.
|
||||
-- PostgreSQL 15+. Migrations 008-013 remain immutable.
|
||||
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF current_database() <> 'booking_test' THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR Booking current-source migration is allowed only in booking_test';
|
||||
END IF;
|
||||
IF to_regclass('booking.source_batches') IS NULL
|
||||
OR to_regclass('booking.current_row_parses') IS NULL
|
||||
OR NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'ingestion'
|
||||
AND table_name = 'processing_runs'
|
||||
AND column_name = 'uploaded_filename'
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR migrations 008 through 013 must be applied first';
|
||||
END IF;
|
||||
IF to_regclass('booking.current_source_batch') IS NOT NULL THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR Booking current-source migration is already applied';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TABLE booking.current_source_batch (
|
||||
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
||||
source_batch_id bigint NOT NULL UNIQUE
|
||||
REFERENCES booking.source_batches(id),
|
||||
activated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE booking.current_source_batch IS
|
||||
'Singleton pointer to the one accepted full Booking source batch used by Finance matching and company reports.';
|
||||
|
||||
CREATE OR REPLACE FUNCTION booking.validate_current_source_batch()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM booking.source_batches AS batch
|
||||
WHERE batch.id = NEW.source_batch_id
|
||||
AND batch.batch_status = 'accepted'
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'current Booking source must reference an accepted source batch';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER current_source_batch_acceptance_guard
|
||||
BEFORE INSERT OR UPDATE ON booking.current_source_batch
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION booking.validate_current_source_batch();
|
||||
|
||||
INSERT INTO booking.current_source_batch (
|
||||
singleton,
|
||||
source_batch_id,
|
||||
activated_at
|
||||
)
|
||||
SELECT
|
||||
true,
|
||||
accepted.id,
|
||||
COALESCE(accepted.finished_at, accepted.validated_at, accepted.created_at)
|
||||
FROM booking.source_batches AS accepted
|
||||
WHERE accepted.batch_status = 'accepted'
|
||||
ORDER BY
|
||||
accepted.finished_at DESC NULLS LAST,
|
||||
accepted.id DESC
|
||||
LIMIT 1;
|
||||
|
||||
CREATE OR REPLACE VIEW booking.v_current_room_items AS
|
||||
SELECT
|
||||
source.id AS source_row_id,
|
||||
source.source_batch_id,
|
||||
source.source_worksheet,
|
||||
source.source_row_no,
|
||||
source.group_code_raw,
|
||||
source.group_code_key,
|
||||
source.type_of_room_raw,
|
||||
source.no_of_rooms AS source_no_of_rooms,
|
||||
parsed.id AS parse_version_id,
|
||||
parsed.version_no AS parse_version_no,
|
||||
item.item_no,
|
||||
item.room_type_raw,
|
||||
item.room_type_code,
|
||||
item.quantity,
|
||||
item.unit_price,
|
||||
item.currency_code,
|
||||
item.price_token_raw,
|
||||
item.source_fragment
|
||||
FROM booking.current_source_batch AS active_source
|
||||
JOIN booking.source_rows AS source
|
||||
ON source.source_batch_id = active_source.source_batch_id
|
||||
JOIN booking.current_row_parses AS current_parse
|
||||
ON current_parse.source_row_id = source.id
|
||||
JOIN booking.parse_versions AS parsed
|
||||
ON parsed.id = current_parse.parse_version_id
|
||||
JOIN booking.source_batches AS batch
|
||||
ON batch.id = source.source_batch_id
|
||||
JOIN booking.room_items AS item
|
||||
ON item.parse_version_id = parsed.id
|
||||
WHERE active_source.singleton
|
||||
AND batch.batch_status = 'accepted'
|
||||
AND parsed.parse_status = 'accepted';
|
||||
|
||||
COMMENT ON VIEW booking.v_current_room_items IS
|
||||
'Accepted room items from the one active full Booking source batch; historical batches remain immutable but inactive.';
|
||||
|
||||
COMMIT;
|
||||
30
database/015_booking_excel_review_drafts.down.sql
Normal file
30
database/015_booking_excel_review_drafts.down.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
-- Roll back durable Booking Excel extraction drafts.
|
||||
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF current_database() <> 'booking_test' THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR Booking review rollback is allowed only in booking_test';
|
||||
END IF;
|
||||
IF to_regclass('booking.extraction_drafts') IS NULL
|
||||
OR to_regclass('booking.extraction_draft_items') IS NULL THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR Booking review migration is not applied';
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM booking.extraction_drafts
|
||||
WHERE draft_status = 'reviewing'
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR Booking review rollback refused while a review draft is open';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TABLE booking.extraction_draft_items;
|
||||
DROP TABLE booking.extraction_drafts;
|
||||
|
||||
COMMIT;
|
||||
123
database/015_booking_excel_review_drafts.sql
Normal file
123
database/015_booking_excel_review_drafts.sql
Normal file
@@ -0,0 +1,123 @@
|
||||
-- Durable Booking Excel extraction drafts and item-level human review.
|
||||
-- PostgreSQL 15+. Migrations 008-014 remain immutable.
|
||||
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF current_database() <> 'booking_test' THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR Booking review migration is allowed only in booking_test';
|
||||
END IF;
|
||||
IF to_regclass('booking.current_source_batch') IS NULL
|
||||
OR to_regclass('booking.room_items') IS NULL
|
||||
OR to_regclass('ingestion.artifacts') IS NULL THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR migrations 008 through 014 must be applied first';
|
||||
END IF;
|
||||
IF to_regclass('booking.extraction_drafts') IS NOT NULL
|
||||
OR to_regclass('booking.extraction_draft_items') IS NOT NULL THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR Booking review migration is already applied';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TABLE booking.extraction_drafts (
|
||||
draft_id text PRIMARY KEY CHECK (
|
||||
draft_id ~ '^bookingdraft-[0-9a-f]{32}$'
|
||||
),
|
||||
source_artifact_id bigint NOT NULL UNIQUE
|
||||
REFERENCES ingestion.artifacts(id),
|
||||
draft_status text NOT NULL CHECK (
|
||||
draft_status IN ('reviewing', 'activated', 'superseded')
|
||||
),
|
||||
processor_version text NOT NULL CHECK (btrim(processor_version) <> ''),
|
||||
rule_set_sha256 character(64) NOT NULL CHECK (
|
||||
rule_set_sha256 ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
source_sha256 character(64) NOT NULL CHECK (
|
||||
source_sha256 ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
source_byte_size bigint NOT NULL CHECK (source_byte_size > 0),
|
||||
source_rows integer NOT NULL CHECK (source_rows > 0),
|
||||
worksheet_count integer NOT NULL CHECK (worksheet_count > 0),
|
||||
distinct_group_codes integer NOT NULL CHECK (distinct_group_codes > 0),
|
||||
extracted_items integer NOT NULL CHECK (extracted_items > 0),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
activated_source_batch_id bigint UNIQUE
|
||||
REFERENCES booking.source_batches(id),
|
||||
activated_at timestamptz,
|
||||
CONSTRAINT extraction_drafts_status_shape CHECK (
|
||||
(
|
||||
draft_status = 'reviewing'
|
||||
AND activated_source_batch_id IS NULL
|
||||
AND activated_at IS NULL
|
||||
)
|
||||
OR (
|
||||
draft_status = 'activated'
|
||||
AND activated_source_batch_id IS NOT NULL
|
||||
AND activated_at IS NOT NULL
|
||||
)
|
||||
OR (
|
||||
draft_status = 'superseded'
|
||||
AND activated_source_batch_id IS NULL
|
||||
AND activated_at IS NULL
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX extraction_drafts_status_created_idx
|
||||
ON booking.extraction_drafts (draft_status, created_at DESC);
|
||||
|
||||
CREATE TABLE booking.extraction_draft_items (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
draft_id text NOT NULL
|
||||
REFERENCES booking.extraction_drafts(draft_id),
|
||||
source_worksheet text NOT NULL CHECK (btrim(source_worksheet) <> ''),
|
||||
source_row_no integer NOT NULL CHECK (source_row_no > 0),
|
||||
item_no integer NOT NULL CHECK (item_no > 0),
|
||||
group_code_raw text NOT NULL CHECK (btrim(group_code_raw) <> ''),
|
||||
group_code_key text GENERATED ALWAYS AS (
|
||||
upper(btrim(group_code_raw))
|
||||
) STORED,
|
||||
hotel_raw text NOT NULL CHECK (btrim(hotel_raw) <> ''),
|
||||
room_type_raw text NOT NULL CHECK (btrim(room_type_raw) <> ''),
|
||||
room_type_code text CHECK (
|
||||
room_type_code IS NULL OR btrim(room_type_code) <> ''
|
||||
),
|
||||
quantity integer NOT NULL CHECK (quantity BETWEEN 1 AND 9999),
|
||||
review_status text NOT NULL CHECK (
|
||||
review_status IN ('confirmed', 'pending', 'deleted')
|
||||
),
|
||||
automatic boolean NOT NULL,
|
||||
source_fragment text NOT NULL DEFAULT '',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT extraction_draft_items_position_unique UNIQUE (
|
||||
draft_id,
|
||||
source_worksheet,
|
||||
source_row_no,
|
||||
item_no
|
||||
),
|
||||
CONSTRAINT extraction_draft_items_review_shape CHECK (
|
||||
(review_status = 'confirmed' AND room_type_code IS NOT NULL)
|
||||
OR (review_status = 'pending' AND room_type_code IS NULL)
|
||||
OR review_status = 'deleted'
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX extraction_draft_items_draft_status_idx
|
||||
ON booking.extraction_draft_items (draft_id, review_status, id);
|
||||
|
||||
CREATE INDEX extraction_draft_items_group_idx
|
||||
ON booking.extraction_draft_items (draft_id, group_code_key);
|
||||
|
||||
COMMENT ON TABLE booking.extraction_drafts IS
|
||||
'Private Booking Excel extraction drafts. A reviewing draft never changes the active accepted Booking source.';
|
||||
|
||||
COMMENT ON TABLE booking.extraction_draft_items IS
|
||||
'Editable extracted room items. Pending items are excluded until confirmed; deleted items remain audit-visible.';
|
||||
|
||||
COMMIT;
|
||||
@@ -1,14 +1,14 @@
|
||||
# ARR 测试数据库已执行记录
|
||||
|
||||
更新时间:2026-07-29
|
||||
状态:ARR 数据落库 MVP v1、OSS 取件授权与 MCP 直接结构化结果提交基础已迁移到远程测试库并验收。
|
||||
更新时间:2026-07-31
|
||||
状态:008–015 已提交;014/015 已通过迁移回滚探针、正式应用、结构/数据复核及真实 PostgreSQL 草稿激活外层回滚验收。首次真实业务工作簿激活仍待操作员授权。
|
||||
|
||||
## 目标与隔离
|
||||
|
||||
- PostgreSQL:`<ARR_DB_HOST>:5432`
|
||||
- 数据库:`booking_test`
|
||||
- 当前测试用户:`<TEST_DB_OWNER>`(数据库 owner、非超级用户;尚非生产最小权限账号)
|
||||
- schema:`ingestion`、`booking`、`finance`
|
||||
- schema:`ingestion`、`booking`、`finance`、`reporting`
|
||||
- `public` 用户对象:0
|
||||
- 共享维护数据库 `postgres`:未修改
|
||||
- 当前配置:`/path/to/private/booking-test-db.env`,权限 `0600`
|
||||
@@ -24,6 +24,24 @@
|
||||
| `008_arr_mvp_v1_rebuild.sql` | `a29df928a2bcfca262216a9d53260528374b830266b3285f3687199cb802a530` | 已提交并验收 |
|
||||
| `009_source_read_grants.sql` | `69c1dcea29b5148f4bf4aab4bd7e606109547849f6722d59bffd1ac756ecd8f8` | 已提交并验收 |
|
||||
| `010_mcp_result_ingestion.sql` | `480a9d1c039d1b45aaa86e90470ef3f7057972be056731f0513a372588bffcf1` | 已提交并验收 |
|
||||
| `011_artifact_object_identity.sql` | `dab8b2eb155161ab7c1400bd95655fed5b5a9c395174df97e10a5e8f333a08e7` | 已提交并验收 |
|
||||
| `012_monthly_report_publication.sql` | `5f6da06031f9bc8ad1a7e37bf5c58bd7f777eb7b6f71cbd03c693b884960849f` | 已提交并验收 |
|
||||
| `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 草稿编辑/激活外层回滚通过 |
|
||||
|
||||
## 014/015 最新核查状态
|
||||
|
||||
- 正式应用前,014/015 在一个事务内完成应用与回滚探针;回滚后两个草稿关系不存在,历史 Booking 数据未变。
|
||||
- 正式应用后,`current_source_batch` 选择历史 accepted batch 1;当前房间明细 867 行、348 个 Group Code、
|
||||
数量合计 867,两张 extraction draft 表均为 0 行。
|
||||
- 新表字段、外键、CHECK/UNIQUE 约束与当前 014/015 源文件结构一致,所有相关约束均已验证。
|
||||
- 真实 PostgreSQL 事务探针创建 2 个提取项、保留 1 个待确认项,编辑为 `EXTRA BED` 后激活为 1 条来源行、
|
||||
数量 3;外层回滚后 active batch 仍为 1,合成草稿、来源和工件残留均为 0。
|
||||
- 当前 8766 进程已加载新 composition,认证健康检查的数据库、处理、月报、下载、公司报表和公司来源上传
|
||||
readiness 全为 true,草稿接口返回未打开。尚未用真实用户工作簿替换当前业务来源。
|
||||
- 015 是基础单操作员 latest-state 草稿模型,没有 reviewer/reason/revision/history;若要求可审计确认,仍需
|
||||
新增迁移。
|
||||
|
||||
`008` 在 `booking_test` 内仅重建 `ingestion`、`booking`、`finance` 三个 schema。它取代 001–007 所形成的旧 schema 状态;001–007 文件只保留为历史记录,不得再对当前库补跑。
|
||||
|
||||
@@ -34,7 +52,7 @@
|
||||
- 整个重建在一个事务内完成;
|
||||
- 不触碰 `public` 或其他数据库。
|
||||
|
||||
当前对象:17 张基础表(`ingestion=8`、`booking=5`、`finance=4`)、8 个视图、2 个约束触发器,未验证约束为 0。
|
||||
当前对象:23 张基础表(`ingestion=8`、`booking=8`、`finance=4`、`reporting=3`)和 8 个视图;012 另增加月报发布/子表不可变保护触发器。
|
||||
|
||||
## 009/010 增量迁移
|
||||
|
||||
@@ -55,6 +73,45 @@
|
||||
|
||||
`010_mcp_result_ingestion.down.sql` SHA-256:`ec937b7c046eaaaccc1e95466ef3e9367cab622cddceaecbab0e9570bbbc5c79`。回滚脚本只允许在新表无任何记录且无 `direct_mcp` 业务数据时使用。
|
||||
|
||||
## 011 工件对象身份迁移
|
||||
|
||||
`011` 修正了同一份 XML 跨任务重复上传时的工件身份冲突:
|
||||
|
||||
- 删除 `ingestion.artifacts (artifact_kind, sha256)` 的唯一约束,保留同名普通查询索引;
|
||||
- 同内容哈希可以属于不同 run 的不同私有 OSS 对象;
|
||||
- `artifacts_storage_identity_unique` 继续保证同一存储地址只有一条工件记录;
|
||||
- 应用层以 storage provider、bucket alias、object key 和版本身份查找工件,仍会核对类型、文件名、哈希、字节数和 MIME。
|
||||
|
||||
迁移正式提交前已在单一事务内完成 up/down 回滚演练。正式提交后 `artifacts` 仍为 12 行,未改动业务数据。`011_artifact_object_identity.down.sql` SHA-256:`c6accbb8c91887fabc5290ad758916c66211479bdcb2a3d9503579d648ba2fa7`;当已出现同类型、同哈希的多 OSS 对象时,回滚会拒绝恢复错误的唯一约束。
|
||||
|
||||
## 012 月报发布元数据迁移
|
||||
|
||||
`012` 在不复制月报业务/住客行的前提下增加:
|
||||
|
||||
- `reporting.monthly_runs`:月份内顺序版本、状态、最大 `ARRIVAL` 水位、规则/快照身份与两个工件引用;
|
||||
- `reporting.monthly_run_daily_versions`:生成时使用的 Finance 日版本 lineage;
|
||||
- `reporting.monthly_channel_manifest`:工作表顺序和隐私最小化行数清单;
|
||||
- `ingestion.artifacts.storage_provider` 的受控 `local` 值,用于当前单机共享输出卷。
|
||||
|
||||
激活约束要求 workbook/result 工件、lineage 和连续渠道清单同时存在;同月只能有一个 active 版本,已发布子记录不可变。`as_of_date` 的数据库语义明确为月报实际纳入数据中的最大 `ARRIVAL`,不得来自 XML 文件名或当前时间。
|
||||
|
||||
正式提交前已完成完整 up/down 事务回滚探针和合成 active 发布探针。由于环境没有 `pg_dump`,使用不含业务行和凭据的元数据 checkpoint:`runtime/backups/booking_test_pre_012_20260730T151922+0800/manifest.json`,SHA-256 `230206eb074fd5877132744eb592b3dc6cd6638d12a3dd4d71a8101a9bd2998e`。
|
||||
|
||||
`012_monthly_report_publication.down.sql` SHA-256:`e696888ffa9525fa345210123894c1b13acbd930fefbdc7effdd6b349bcb63d6`。一旦已有 reporting/local 工件数据,回滚脚本会拒绝破坏性删除。
|
||||
|
||||
## 013 日报上传文件名来源字段
|
||||
|
||||
`013` 在不改变源工件身份和处理器输入契约的前提下增加:
|
||||
|
||||
- `ingestion.processing_runs.uploaded_filename`:保存浏览器提交并通过校验的 XML basename;
|
||||
- 历史任务保持 `NULL`,不使用内部 `source.xml` 进行错误回填;
|
||||
- `ingestion.artifacts.original_filename` 继续保持规范内部名 `source.xml`;
|
||||
- 日报历史与任务日志只读取任务上的上传文件名,缺失时由页面显示 `—`。
|
||||
|
||||
正式提交后 `processing_runs=25`、`uploaded_filename` 非空行数为 0,新约束已验证。一个完整事务内的 Unicode 文件名探针证明日报列表与任务日志读取上传名、源工件仍为 `source.xml`,随后已整体回滚且未留下测试数据。
|
||||
|
||||
`013_daily_upload_filename.down.sql` SHA-256:`663d287e36bb99533d28918d8ead7a2b03e8bbf7e7d682a527400d11f0ee3fbf`。一旦存在非空上传文件名,回滚脚本会拒绝丢弃该来源信息。
|
||||
|
||||
## 远程完整迁移
|
||||
|
||||
2026-07-28 已把完整 MVP v1 结构、数据、identity sequence 状态和处理审计从旧 LAN 测试库迁移到当前远程测试库。
|
||||
@@ -92,7 +149,7 @@
|
||||
|
||||
867 条来源行逐行保存,没有按 Group Code 合并;每条都保留工作表、行号、原文、数量和行哈希。重复导入返回 `already_applied_and_verified`,没有新增重复记录。
|
||||
|
||||
当前 MD 是用于跑通流程的预期结果样本,每行已经给出一个可查询房型,因此当前每行对应一个 room item。未来真实预订 Excel 仍由 Agent 输出行级 JSON 和一个或多个 room items。
|
||||
当前 MD 是用于跑通流程的预期结果样本,每行已经给出一个可查询房型,因此当前每行对应一个 room item。真实预订 Excel 现在由确定性 parser 2.0 从 `Tour Code`/`โรงแรม` 提取一个或多个 room items,未知项先进入人工复核草稿,不经过 Agent。
|
||||
|
||||
## 已导入 Finance 合成日报
|
||||
|
||||
@@ -125,7 +182,7 @@
|
||||
- 未验证约束:0;
|
||||
- `public` 用户对象:0。
|
||||
|
||||
应用只读验收结果:
|
||||
应用只读验收结果(最初合成基线):
|
||||
|
||||
- 渠道 BI:1 个渠道、1 条预订;
|
||||
- 渠道明细:1 行;
|
||||
@@ -134,11 +191,36 @@
|
||||
- Web 日报列表:1 条;
|
||||
- Web 月度数据源投影:1 条。
|
||||
|
||||
完整自动化测试共 194 项:192 项通过,2 项仅因默认环境未配置可选 XLSX 渲染工具而跳过。最终代码同步后又执行了 65 项相关回归并全部通过,其中 Web 新 schema 契约与 Web 回归为 14 项。
|
||||
该段为 008–011 初始验收快照;2026-07-30 的 012/月报验收见下节。
|
||||
|
||||
## 2026-07-30 月报发布验收
|
||||
|
||||
- 首次验收时为 2 个 daily version、120 条 active daily facts;两个事件均由独立 worker 在首次尝试后标记为 `published`;
|
||||
- 首次两个事件幂等汇聚到 report ID 1/version 1:`as_of_date=2026-07-27`、120 行、5 个渠道;
|
||||
- 首次 workbook 重新打开后为 120 个授权 `TOTAL PRICE` 公式、0 个缺失/错位/越权公式,20,741 字节及 SHA-256 均与登记值一致;
|
||||
- 同日后续两个真实上传安全生成 V02/V03。最新只读状态为 4 个已发布事件、V03 active、V02/V01 superseded;
|
||||
- V03 为 308 行、6 个渠道,`as_of_date=max(ARRIVAL)=2026-07-27`;其 308 个公式全部正确,40,635 字节及 SHA-256 `494fb78283f68cb7bffce2501b3531613c494d148a56a7ccc8d2aabe46b1e29c` 与登记值一致;
|
||||
- Web 列表显示 V03/V02/V01 与下载链接;页面无月报刷新按钮,可见时约每 4 秒自动读取新版本。
|
||||
- 最终完整发现运行 291 项测试,全部通过,7 项仅因环境/未打包私有 fixture 而跳过。
|
||||
|
||||
## 2026-07-31 Booking 当前源与复核结构
|
||||
|
||||
受控连接先完成 014/015 同事务应用/回滚探针,再正式应用权威 SQL。`booking.current_source_batch` 指向
|
||||
accepted source batch 1,`booking.extraction_drafts` 与 `booking.extraction_draft_items` 可读且为空;正式应用前后
|
||||
867 条房间明细、348 个 Group Code 和数量合计 867 均未变化。
|
||||
|
||||
- `014_booking_current_source_batch.sql` SHA-256:`23bf0fcc880225ca276d4d7d871057be4f7950e761ddbeed1df2a3c6e25463b5`
|
||||
- `014_booking_current_source_batch.down.sql` SHA-256:`fa64a251e1a018e536c8b361bb81b18daffdbcbef8abc839fe0c95ccfa7d045b`
|
||||
- `015_booking_excel_review_drafts.sql` SHA-256:`a80689c4ecc3b6b3502e8f094a8c175af38df8c09f9d2c64412a9aec55bae12a`
|
||||
- `015_booking_excel_review_drafts.down.sql` SHA-256:`7643f0bda8b8c15cf40afa76d89d396bdf2a7654ba8eb73df872c7a838fc809d`
|
||||
|
||||
真实 PostgreSQL 事务探针完成 draft create/edit/activate 全路径后由外层回滚,恢复 batch 1 且无合成残留。
|
||||
Port 8766 于 11:30:46 +08 加载当前复核程序;认证 readiness/source-draft 检查通过,PATCH/DELETE 已到达
|
||||
应用层。首个真实用户工作簿上传/人工复核/激活仍未执行,因此当前业务来源未被替换。
|
||||
|
||||
## 当前模型的重要含义
|
||||
|
||||
- PostgreSQL 保存已验收的工件身份、预订事实、全部日报 outcome、current 指针和渠道指标。
|
||||
- 月报行、渠道明细行、公司 10 日报表行由普通程序查询当前事实生成,不再重复存储 `report_versions` 或 report 行表。
|
||||
- 生成的 XLSX/JSON 可以保存在私有 OSS 或受控输出目录,但不是重复的数据库业务事实。
|
||||
- 后续变更必须新增迁移,不得编辑已执行的 `008`。
|
||||
- 月报行、渠道明细行、公司 10 日报表行由普通程序查询当前事实生成;`reporting` 只保存版本、lineage、清单和工件身份。
|
||||
- 月报 XLSX/JSON 当前保存在受控共享输出目录并登记为 `local` 工件,但不是重复的数据库业务事实。
|
||||
- 后续变更必须新增迁移,不得编辑已执行的 `008`–`015`。
|
||||
|
||||
Reference in New Issue
Block a user