B-43: 双边流水归并与规范事件层——迁移5、匹配引擎、个人过账映射与事件API
This commit is contained in:
@@ -354,6 +354,220 @@ MIGRATIONS: tuple[Migration, ...] = (
|
||||
DROP TABLE IF EXISTS sheet_reviews;
|
||||
""",
|
||||
),
|
||||
Migration(
|
||||
version=5,
|
||||
name="0005_canonical_transfer_matching",
|
||||
# Canonical transfer event layer (B-43). Immutable source rows stay
|
||||
# untouched; every matching decision, its participant/observation
|
||||
# bindings and its candidate evidence are append-only history, and the
|
||||
# current decision + row claims are a rebuildable projection that the
|
||||
# B-44 balance layer reads through the eligible view.
|
||||
up="""
|
||||
ALTER TABLE import_batches ADD COLUMN upload_bank_account_id INTEGER REFERENCES bank_accounts (id);
|
||||
|
||||
CREATE TABLE master_data_changes_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
entity_type TEXT NOT NULL
|
||||
CHECK (entity_type IN ('company', 'user', 'bank_account', 'account_alias', 'personal_transit_mapping')),
|
||||
entity_id INTEGER NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
before_json TEXT,
|
||||
after_json TEXT,
|
||||
reason TEXT,
|
||||
actor_user_id INTEGER REFERENCES users (id),
|
||||
actor_username TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO master_data_changes_new SELECT * FROM master_data_changes;
|
||||
DROP TABLE master_data_changes;
|
||||
ALTER TABLE master_data_changes_new RENAME TO master_data_changes;
|
||||
|
||||
CREATE TABLE personal_transit_mappings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
account_number TEXT NOT NULL UNIQUE,
|
||||
account_name TEXT,
|
||||
represented_company_id INTEGER NOT NULL REFERENCES companies (id),
|
||||
allowed_direction TEXT NOT NULL
|
||||
CHECK (allowed_direction IN ('outgoing', 'incoming', 'both')),
|
||||
status TEXT NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending', 'active', 'returned', 'disabled')),
|
||||
effective_from TEXT,
|
||||
effective_to TEXT,
|
||||
submitted_by INTEGER REFERENCES users (id),
|
||||
reviewed_by INTEGER REFERENCES users (id),
|
||||
reviewed_at TEXT,
|
||||
review_reason TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE canonical_transfer_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
lifecycle TEXT NOT NULL DEFAULT 'active'
|
||||
CHECK (lifecycle IN ('active', 'superseded')),
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE transfer_match_decisions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
event_id INTEGER NOT NULL REFERENCES canonical_transfer_events (id),
|
||||
revision INTEGER NOT NULL,
|
||||
classification TEXT NOT NULL CHECK (classification IN (
|
||||
'unresolved', 'needs_review', 'internal_single',
|
||||
'intercompany', 'same_company', 'external'
|
||||
)),
|
||||
pairing TEXT NOT NULL
|
||||
CHECK (pairing IN ('single', 'paired', 'not_applicable')),
|
||||
amount TEXT,
|
||||
currency TEXT,
|
||||
effective_at TEXT,
|
||||
mode TEXT NOT NULL CHECK (mode IN ('auto', 'manual', 'reversal')),
|
||||
rule_version TEXT,
|
||||
locked INTEGER NOT NULL DEFAULT 0 CHECK (locked IN (0, 1)),
|
||||
reason TEXT,
|
||||
idempotency_key TEXT,
|
||||
actor_user_id INTEGER REFERENCES users (id),
|
||||
actor_username TEXT,
|
||||
supersedes_decision_id INTEGER REFERENCES transfer_match_decisions (id),
|
||||
created_at TEXT NOT NULL,
|
||||
UNIQUE (event_id, revision)
|
||||
);
|
||||
|
||||
CREATE TABLE transfer_decision_observations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
decision_id INTEGER NOT NULL REFERENCES transfer_match_decisions (id),
|
||||
source_row_id INTEGER NOT NULL REFERENCES source_rows (id),
|
||||
role TEXT NOT NULL CHECK (role IN ('outgoing', 'incoming')),
|
||||
created_at TEXT NOT NULL,
|
||||
UNIQUE (decision_id, source_row_id)
|
||||
);
|
||||
|
||||
CREATE TABLE transfer_decision_participants (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
decision_id INTEGER NOT NULL REFERENCES transfer_match_decisions (id),
|
||||
role TEXT NOT NULL CHECK (role IN ('payer', 'payee')),
|
||||
company_id INTEGER REFERENCES companies (id),
|
||||
bank_account_id INTEGER REFERENCES bank_accounts (id),
|
||||
resolve_method TEXT NOT NULL CHECK (resolve_method IN (
|
||||
'own_exact', 'upload_account', 'counterparty_exact',
|
||||
'account_alias', 'personal_mapping', 'manual'
|
||||
)),
|
||||
alias_id INTEGER REFERENCES account_aliases (id),
|
||||
mapping_id INTEGER REFERENCES personal_transit_mappings (id),
|
||||
evidence TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
UNIQUE (decision_id, role)
|
||||
);
|
||||
|
||||
CREATE TABLE transfer_match_candidates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
decision_id INTEGER NOT NULL REFERENCES transfer_match_decisions (id),
|
||||
source_row_id INTEGER NOT NULL REFERENCES source_rows (id),
|
||||
rule_tier TEXT NOT NULL,
|
||||
date_diff_days INTEGER,
|
||||
account_mirror INTEGER NOT NULL DEFAULT 0,
|
||||
reference_match TEXT,
|
||||
summary_match INTEGER NOT NULL DEFAULT 0,
|
||||
accepted INTEGER,
|
||||
reason TEXT,
|
||||
rule_version TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE current_transfer_decisions (
|
||||
event_id INTEGER PRIMARY KEY REFERENCES canonical_transfer_events (id),
|
||||
decision_id INTEGER NOT NULL UNIQUE REFERENCES transfer_match_decisions (id)
|
||||
);
|
||||
|
||||
CREATE TABLE transfer_observation_claims (
|
||||
source_row_id INTEGER PRIMARY KEY REFERENCES source_rows (id),
|
||||
event_id INTEGER NOT NULL REFERENCES canonical_transfer_events (id),
|
||||
decision_id INTEGER NOT NULL REFERENCES transfer_match_decisions (id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_obs_decision ON transfer_decision_observations (decision_id);
|
||||
CREATE INDEX idx_participants_decision ON transfer_decision_participants (decision_id);
|
||||
CREATE INDEX idx_candidates_decision ON transfer_match_candidates (decision_id);
|
||||
CREATE INDEX idx_claims_event ON transfer_observation_claims (event_id);
|
||||
|
||||
CREATE VIEW eligible_intercompany_events AS
|
||||
SELECT e.id AS event_id, d.id AS decision_id, d.revision AS revision,
|
||||
d.effective_at AS effective_at, d.amount AS amount, d.currency AS currency,
|
||||
payer.company_id AS payer_company_id,
|
||||
payer.bank_account_id AS payer_account_id,
|
||||
payee.company_id AS payee_company_id,
|
||||
payee.bank_account_id AS payee_account_id,
|
||||
d.pairing AS pairing, d.rule_version AS rule_version,
|
||||
(SELECT COUNT(*) FROM transfer_decision_observations o
|
||||
WHERE o.decision_id = d.id) AS evidence_count
|
||||
FROM current_transfer_decisions c
|
||||
JOIN canonical_transfer_events e ON e.id = c.event_id
|
||||
JOIN transfer_match_decisions d ON d.id = c.decision_id
|
||||
JOIN transfer_decision_participants payer
|
||||
ON payer.decision_id = d.id AND payer.role = 'payer'
|
||||
JOIN transfer_decision_participants payee
|
||||
ON payee.decision_id = d.id AND payee.role = 'payee'
|
||||
WHERE e.lifecycle = 'active' AND d.classification = 'intercompany';
|
||||
|
||||
CREATE TRIGGER canonical_transfer_events_no_delete BEFORE DELETE ON canonical_transfer_events
|
||||
BEGIN SELECT RAISE (ABORT, 'canonical_transfer_events rows are immutable'); END;
|
||||
CREATE TRIGGER canonical_transfer_events_no_update BEFORE UPDATE ON canonical_transfer_events
|
||||
BEGIN
|
||||
SELECT RAISE (ABORT, 'canonical_transfer_events only allow lifecycle changes')
|
||||
WHERE OLD.lifecycle = NEW.lifecycle
|
||||
OR OLD.id IS NOT NEW.id
|
||||
OR OLD.created_at IS NOT NEW.created_at;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER transfer_match_decisions_no_update BEFORE UPDATE ON transfer_match_decisions
|
||||
BEGIN SELECT RAISE (ABORT, 'transfer_match_decisions rows are immutable'); END;
|
||||
CREATE TRIGGER transfer_match_decisions_no_delete BEFORE DELETE ON transfer_match_decisions
|
||||
BEGIN SELECT RAISE (ABORT, 'transfer_match_decisions rows are immutable'); END;
|
||||
|
||||
CREATE TRIGGER transfer_decision_observations_no_update BEFORE UPDATE ON transfer_decision_observations
|
||||
BEGIN SELECT RAISE (ABORT, 'transfer_decision_observations rows are immutable'); END;
|
||||
CREATE TRIGGER transfer_decision_observations_no_delete BEFORE DELETE ON transfer_decision_observations
|
||||
BEGIN SELECT RAISE (ABORT, 'transfer_decision_observations rows are immutable'); END;
|
||||
|
||||
CREATE TRIGGER transfer_decision_participants_no_update BEFORE UPDATE ON transfer_decision_participants
|
||||
BEGIN SELECT RAISE (ABORT, 'transfer_decision_participants rows are immutable'); END;
|
||||
CREATE TRIGGER transfer_decision_participants_no_delete BEFORE DELETE ON transfer_decision_participants
|
||||
BEGIN SELECT RAISE (ABORT, 'transfer_decision_participants rows are immutable'); END;
|
||||
|
||||
CREATE TRIGGER transfer_match_candidates_no_update BEFORE UPDATE ON transfer_match_candidates
|
||||
BEGIN SELECT RAISE (ABORT, 'transfer_match_candidates rows are immutable'); END;
|
||||
CREATE TRIGGER transfer_match_candidates_no_delete BEFORE DELETE ON transfer_match_candidates
|
||||
BEGIN SELECT RAISE (ABORT, 'transfer_match_candidates rows are immutable'); END;
|
||||
""",
|
||||
down="""
|
||||
DROP VIEW IF EXISTS eligible_intercompany_events;
|
||||
DROP TABLE IF EXISTS transfer_observation_claims;
|
||||
DROP TABLE IF EXISTS current_transfer_decisions;
|
||||
DROP TABLE IF EXISTS transfer_match_candidates;
|
||||
DROP TABLE IF EXISTS transfer_decision_participants;
|
||||
DROP TABLE IF EXISTS transfer_decision_observations;
|
||||
DROP TABLE IF EXISTS transfer_match_decisions;
|
||||
DROP TABLE IF EXISTS canonical_transfer_events;
|
||||
DROP TABLE IF EXISTS personal_transit_mappings;
|
||||
CREATE TABLE master_data_changes_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
entity_type TEXT NOT NULL
|
||||
CHECK (entity_type IN ('company', 'user', 'bank_account', 'account_alias')),
|
||||
entity_id INTEGER NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
before_json TEXT,
|
||||
after_json TEXT,
|
||||
reason TEXT,
|
||||
actor_user_id INTEGER REFERENCES users (id),
|
||||
actor_username TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
INSERT INTO master_data_changes_new SELECT * FROM master_data_changes;
|
||||
DROP TABLE master_data_changes;
|
||||
ALTER TABLE master_data_changes_new RENAME TO master_data_changes;
|
||||
ALTER TABLE import_batches DROP COLUMN upload_bank_account_id;
|
||||
""",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user