-
Notifications
You must be signed in to change notification settings - Fork 0
feat: persist proposed reconciliation matches #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
seonghobae
merged 30 commits into
feat/reconciliation-run-api
from
feat/reconciliation-match-command
Aug 30, 2026
Merged
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
cf501df
feat: persist proposed reconciliation matches
seonghobae 75ca8ab
feat: add reconciliation match command schema
seonghobae e38d0d1
test: scope psycopg wheel hash contract
seonghobae 75732b8
fix: validate psycopg Python 3.14 hash
seonghobae 17340f9
fix: validate reconciliation match sources
seonghobae b73f57d
fix: classify reconciliation match conflicts
seonghobae 06c02cc
fix: distinguish absent reconciliation journals
seonghobae 08e88d9
test: reject cross-paired reconciliation command evidence
seonghobae 3a42b1c
fix: bind match command evidence to candidate chain
seonghobae a418d45
fix: bind match command to its candidate chain
seonghobae 0f7a9d4
docs: record database-owned match command chain
seonghobae 24e6367
Merge remote candidate-chain hardening
seonghobae c77cf13
Merge remote match-chain documentation
seonghobae 8602b55
Merge current reconciliation run API base
seonghobae 26e92ef
Merge remote-tracking branch 'origin/feat/reconciliation-run-api' int…
seonghobae 56381ee
fix(reconciliation): enforce run evidence provenance
seonghobae 55dc4d6
Merge remote-tracking branch 'origin/feat/reconciliation-run-command'…
seonghobae cb309f5
fix(reconciliation): bind matches to source cash evidence
seonghobae 10ead71
fix(reconciliation): close historical evidence gaps
seonghobae e7ef2e4
fix(reconciliation): close concurrency and upgrade gaps
seonghobae 270f0c7
fix(reconciliation): validate legacy provenance on upgrade
seonghobae d4581bf
docs: refresh product gap evidence date
seonghobae 20cb6b6
fix(reconciliation): align amount precision domain
seonghobae b740bcc
fix(reconciliation): rebuild dependent amount trigger
seonghobae 134ecb5
docs: update migration chain guidance
seonghobae 98ba7df
fix(reconciliation): close command evidence races
seonghobae 39d134d
fix(reconciliation): persist command freeze marker
seonghobae 37c7aef
docs(reconciliation): describe durable freeze marker
seonghobae 03dc547
fix(reconciliation): protect command freeze marker
seonghobae e813c8f
fix(http): advertise reconciliation match lookup
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
183 changes: 183 additions & 0 deletions
183
database/migrations/0020_reconciliation_match_command_evidence.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| BEGIN; | ||
|
|
||
| -- Immutable application command identity for a proposed reconciliation match. | ||
| -- This records reviewable candidate evidence only; it cannot approve, close, or | ||
| -- post a journal. | ||
|
|
||
| -- Command provenance must name the candidate actually referenced by the match. | ||
| -- The existing match primary key proves row identity, while this tenant/run | ||
| -- composite key gives downstream evidence a database-owned same-chain target. | ||
| ALTER TABLE accounting_core.reconciliation_match | ||
| ADD CONSTRAINT reconciliation_match_candidate_chain_unique | ||
| UNIQUE ( | ||
| tenant_account_id, | ||
| reconciliation_run_id, | ||
| reconciliation_match_id, | ||
| reconciliation_candidate_id | ||
| ); | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
|
||
|
|
||
| CREATE TABLE accounting_core.reconciliation_match_command ( | ||
| reconciliation_match_command_id uuid PRIMARY KEY DEFAULT uuidv7(), | ||
| tenant_account_id uuid NOT NULL, | ||
| reconciliation_run_id uuid NOT NULL, | ||
| reconciliation_candidate_id uuid NOT NULL, | ||
| reconciliation_match_id uuid NOT NULL, | ||
| candidate_idempotency_key text NOT NULL | ||
| CHECK (btrim(candidate_idempotency_key) <> ''), | ||
| candidate_command_hash text NOT NULL | ||
| CHECK (candidate_command_hash ~ '^sha256:[0-9a-f]{64}$'), | ||
| source_payload_hash text NOT NULL | ||
| CHECK (source_payload_hash ~ '^sha256:[0-9a-f]{64}$'), | ||
| source_payload_reference text NOT NULL | ||
| CHECK (btrim(source_payload_reference) <> ''), | ||
| recorded_at timestamptz NOT NULL DEFAULT clock_timestamp(), | ||
| FOREIGN KEY ( | ||
| tenant_account_id, | ||
| reconciliation_run_id, | ||
| reconciliation_candidate_id | ||
| ) REFERENCES accounting_core.reconciliation_candidate ( | ||
| tenant_account_id, | ||
| reconciliation_run_id, | ||
| reconciliation_candidate_id | ||
| ), | ||
| FOREIGN KEY ( | ||
| tenant_account_id, | ||
| reconciliation_run_id, | ||
| reconciliation_match_id, | ||
| reconciliation_candidate_id | ||
| ) REFERENCES accounting_core.reconciliation_match ( | ||
| tenant_account_id, | ||
| reconciliation_run_id, | ||
| reconciliation_match_id, | ||
| reconciliation_candidate_id | ||
| ), | ||
| UNIQUE (tenant_account_id, candidate_idempotency_key), | ||
| UNIQUE (tenant_account_id, reconciliation_run_id, reconciliation_match_id) | ||
| ); | ||
|
|
||
| CREATE INDEX reconciliation_match_command_run_index | ||
| ON accounting_core.reconciliation_match_command ( | ||
| tenant_account_id, | ||
| reconciliation_run_id, | ||
| recorded_at, | ||
| reconciliation_match_command_id | ||
| ); | ||
|
|
||
| ALTER TABLE accounting_core.reconciliation_match_command ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE accounting_core.reconciliation_match_command FORCE ROW LEVEL SECURITY; | ||
|
|
||
| CREATE POLICY reconciliation_match_command_isolation | ||
| ON accounting_core.reconciliation_match_command | ||
| USING (tenant_account_id = accounting_core.current_tenant_account_id()) | ||
| WITH CHECK (tenant_account_id = accounting_core.current_tenant_account_id()); | ||
|
|
||
| REVOKE ALL ON accounting_core.reconciliation_match_command FROM PUBLIC; | ||
|
|
||
| CREATE OR REPLACE FUNCTION accounting_core.reject_reconciliation_match_command_mutation() | ||
| RETURNS trigger | ||
| LANGUAGE plpgsql | ||
| AS $$ | ||
| BEGIN | ||
| RAISE EXCEPTION | ||
| 'recorded reconciliation match command evidence is immutable; create a new proposed match instead (reconciliation_match_command_immutable)' | ||
| USING ERRCODE = '23514'; | ||
| END; | ||
| $$; | ||
|
|
||
| CREATE TRIGGER reconciliation_match_command_immutability_guard | ||
| BEFORE UPDATE OR DELETE | ||
| ON accounting_core.reconciliation_match_command | ||
| FOR EACH ROW EXECUTE FUNCTION accounting_core.reject_reconciliation_match_command_mutation(); | ||
|
|
||
| CREATE OR REPLACE FUNCTION accounting_core.enforce_reconciliation_match_command_allocations() | ||
| RETURNS trigger | ||
| LANGUAGE plpgsql | ||
| AS $$ | ||
| DECLARE | ||
| statement_allocation_count bigint; | ||
| journal_allocation_count bigint; | ||
| statement_allocation_total numeric(30, 6); | ||
| journal_allocation_total numeric(30, 6); | ||
| candidate_statement_amount numeric(30, 6); | ||
| candidate_journal_amount numeric(30, 6); | ||
| BEGIN | ||
| -- Share the parent match lock with the allocation conservation trigger so | ||
| -- command insertion and a concurrent allocation cannot both commit. | ||
| PERFORM 1 | ||
| FROM accounting_core.reconciliation_match AS match | ||
| WHERE match.tenant_account_id = NEW.tenant_account_id | ||
| AND match.reconciliation_run_id = NEW.reconciliation_run_id | ||
| AND match.reconciliation_match_id = NEW.reconciliation_match_id | ||
| FOR UPDATE; | ||
|
seonghobae marked this conversation as resolved.
devin-ai-integration[bot] marked this conversation as resolved.
|
||
|
|
||
| SELECT candidate.statement_amount, candidate.journal_amount | ||
| INTO candidate_statement_amount, candidate_journal_amount | ||
| FROM accounting_core.reconciliation_candidate AS candidate | ||
| WHERE candidate.tenant_account_id = NEW.tenant_account_id | ||
| AND candidate.reconciliation_run_id = NEW.reconciliation_run_id | ||
| AND candidate.reconciliation_candidate_id = NEW.reconciliation_candidate_id; | ||
|
|
||
| SELECT COUNT(*), COALESCE(SUM(allocation.allocated_amount), 0) | ||
| INTO statement_allocation_count, statement_allocation_total | ||
| FROM accounting_core.statement_match_allocation AS allocation | ||
| WHERE allocation.tenant_account_id = NEW.tenant_account_id | ||
| AND allocation.reconciliation_run_id = NEW.reconciliation_run_id | ||
| AND allocation.reconciliation_match_id = NEW.reconciliation_match_id; | ||
|
|
||
| SELECT COUNT(*), COALESCE(SUM(allocation.allocated_amount), 0) | ||
| INTO journal_allocation_count, journal_allocation_total | ||
| FROM accounting_core.journal_match_allocation AS allocation | ||
| WHERE allocation.tenant_account_id = NEW.tenant_account_id | ||
| AND allocation.reconciliation_run_id = NEW.reconciliation_run_id | ||
| AND allocation.reconciliation_match_id = NEW.reconciliation_match_id; | ||
|
|
||
| IF statement_allocation_count <> 1 | ||
| OR journal_allocation_count <> 1 | ||
| OR statement_allocation_total <> journal_allocation_total | ||
| OR statement_allocation_total <> candidate_statement_amount | ||
| OR journal_allocation_total <> candidate_journal_amount THEN | ||
| RAISE EXCEPTION | ||
| 'reconciliation match command requires exactly one statement and one journal allocation matching candidate amounts (reconciliation_match_command_allocation)' | ||
| USING ERRCODE = '23514'; | ||
| END IF; | ||
|
|
||
| RETURN NEW; | ||
| END; | ||
| $$; | ||
|
|
||
| CREATE TRIGGER z_reconciliation_match_command_allocation_guard | ||
| AFTER INSERT | ||
| ON accounting_core.reconciliation_match_command | ||
| FOR EACH ROW EXECUTE FUNCTION accounting_core.enforce_reconciliation_match_command_allocations(); | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
devin-ai-integration[bot] marked this conversation as resolved.
|
||
|
|
||
| CREATE OR REPLACE FUNCTION accounting_core.reject_reconciliation_match_command_allocation() | ||
| RETURNS trigger | ||
| LANGUAGE plpgsql | ||
| AS $$ | ||
| BEGIN | ||
| IF EXISTS ( | ||
| SELECT 1 | ||
| FROM accounting_core.reconciliation_match_command AS command | ||
| WHERE command.tenant_account_id = NEW.tenant_account_id | ||
| AND command.reconciliation_run_id = NEW.reconciliation_run_id | ||
| AND command.reconciliation_match_id = NEW.reconciliation_match_id | ||
| ) THEN | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
|
||
| RAISE EXCEPTION | ||
| 'reconciliation match command evidence freezes its allocation population; create a new proposed match instead (reconciliation_match_command_allocation_frozen)' | ||
| USING ERRCODE = '23514'; | ||
| END IF; | ||
| RETURN NEW; | ||
| END; | ||
| $$; | ||
|
|
||
| CREATE TRIGGER z_reconciliation_match_command_allocation_frozen_guard | ||
| BEFORE INSERT | ||
| ON accounting_core.statement_match_allocation | ||
| FOR EACH ROW EXECUTE FUNCTION accounting_core.reject_reconciliation_match_command_allocation(); | ||
|
|
||
| CREATE TRIGGER z_reconciliation_match_command_allocation_frozen_guard | ||
| BEFORE INSERT | ||
| ON accounting_core.journal_match_allocation | ||
| FOR EACH ROW EXECUTE FUNCTION accounting_core.reject_reconciliation_match_command_allocation(); | ||
|
|
||
| COMMIT; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.