Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
226 commits
Select commit Hold shift + click to select a range
6e26295
test: prove hard-close trial balance evidence is immutable
seonghobae Sep 4, 2026
7f295a0
fix: enforce hard-close trial balance immutability
seonghobae Sep 4, 2026
2813d2e
fix: install trial balance immutability migration
seonghobae Sep 4, 2026
c47db00
refactor: keep installer diagnostics stable
seonghobae Sep 4, 2026
d6097a6
test: cover hard-close snapshot deletions
seonghobae Sep 4, 2026
3abb16b
security: harden trial balance trigger search path
seonghobae Sep 4, 2026
3ac9f8c
test: pin trial balance immutability install contract
seonghobae Sep 4, 2026
12cf040
security: restrict trial balance trigger search path
seonghobae Sep 4, 2026
5cdcbd1
test: pin trigger privilege boundary
seonghobae Sep 4, 2026
85447c5
test(close): reject post-close snapshot header insertion
seonghobae Sep 4, 2026
550cb6b
fix(close): block post-close snapshot header creation
seonghobae Sep 4, 2026
be76191
test(close): reject pre-close snapshot authority poisoning
seonghobae Sep 4, 2026
aa4281b
fix(close): fail closed on preexisting snapshot population
seonghobae Sep 4, 2026
c36f7f6
test(close): pin one snapshot population per book-period
seonghobae Sep 4, 2026
c29e978
docs(close): record single hard-close snapshot authority
seonghobae Sep 4, 2026
4358160
docs(close): record hard-close evidence immutability
seonghobae Sep 4, 2026
e7bf6ae
docs(close): trace immutable period-close evidence
seonghobae Sep 4, 2026
76877c5
test(close): bind poisoning probe to exact book-period scope
seonghobae Sep 4, 2026
1019072
test(close): reproduce stale snapshot population race
seonghobae Sep 4, 2026
1cc85b7
fix(close): enforce one snapshot population physically
seonghobae Sep 4, 2026
88bca20
test(close): pin physical one-population constraint
seonghobae Sep 4, 2026
e89dba6
docs(close): record repeatable-read uniqueness boundary
seonghobae Sep 4, 2026
25af584
test(close): require concurrent snapshot uniqueness build
seonghobae Sep 4, 2026
30f6f24
refactor(close): split concurrent snapshot index migration
seonghobae Sep 4, 2026
a6b7fb1
fix(close): build snapshot uniqueness concurrently
seonghobae Sep 4, 2026
001f780
fix(close): attach concurrent snapshot index safely
seonghobae Sep 4, 2026
f586b05
fix(close): install split snapshot migrations in order
seonghobae Sep 4, 2026
0623914
docs(close): record low-blocking snapshot migration design
seonghobae Sep 4, 2026
84365fd
test(close): require snapshot admission authority
seonghobae Sep 4, 2026
279f75b
fix(close): require purpose-limited snapshot writer
seonghobae Sep 4, 2026
0e64f95
docs(close): trace snapshot writer authority
seonghobae Sep 4, 2026
f878cf2
test(close): make snapshot system time database-owned
seonghobae Sep 4, 2026
11fafa2
fix(close): make snapshot chronology database-owned
seonghobae Sep 4, 2026
88529b0
test(close): ratchet snapshot authority and system time
seonghobae Sep 4, 2026
879577d
docs(close): make snapshot system time explicit
seonghobae Sep 4, 2026
ab2b46b
test(close): preserve authorized stale-snapshot race coverage
seonghobae Sep 4, 2026
d956d0c
test(close): keep conflict probes inside closing authority
seonghobae Sep 4, 2026
45cb632
test(close): cover hard close without closing journal
seonghobae Sep 4, 2026
c6e0043
fix(close): decouple snapshot authority from optional journal
seonghobae Sep 4, 2026
97578db
test(close): ratchet canonical close command context
seonghobae Sep 4, 2026
6ab1bff
docs(close): trace journal-independent command authority
seonghobae Sep 4, 2026
eff6184
test(close): match snapshot authority lock to command key
seonghobae Sep 4, 2026
07516da
fix(close): use canonical accounting book lock identity
seonghobae Sep 4, 2026
6028faf
docs(close): identify canonical advisory lock key
seonghobae Sep 4, 2026
613167f
test(close): reject cross-scope trial balance evidence
seonghobae Sep 4, 2026
5396c4f
fix(close): bind snapshot evidence to accounting-book scope
seonghobae Sep 4, 2026
3d59ab2
docs(close): record trial-balance book-scope invariant
seonghobae Sep 4, 2026
03eb711
test(close): require trial balance line arithmetic conservation
seonghobae Sep 4, 2026
06d365b
fix(close): enforce trial balance line arithmetic conservation
seonghobae Sep 4, 2026
8901124
docs(close): record trial balance arithmetic invariant
seonghobae Sep 4, 2026
9600c6a
docs(close): trace trial balance conservation control
seonghobae Sep 4, 2026
d6b75eb
test(close): separate trial balance validation lock phase
seonghobae Sep 4, 2026
605ffe0
fix(close): release add-constraint lock before validation
seonghobae Sep 4, 2026
e6a3ec1
fix(close): validate retained line conservation separately
seonghobae Sep 4, 2026
abdd2df
fix(close): install conservation validation migration
seonghobae Sep 4, 2026
82a6d1f
docs(close): correct conservation validation lock semantics
seonghobae Sep 4, 2026
9237000
docs(close): trace separate conservation validation phase
seonghobae Sep 4, 2026
8d748c7
docs(close): trace retained line conservation authority
seonghobae Sep 4, 2026
46c877c
docs(close): preserve unrelated traceability wording
seonghobae Sep 4, 2026
715d3a6
test(close): reject trial-balance snapshot currency drift
seonghobae Sep 4, 2026
914aee7
fix(close): bind trial-balance snapshot currency to book
seonghobae Sep 4, 2026
9c75abd
docs(close): bind snapshot currency to book authority
seonghobae Sep 4, 2026
d3ea0b9
test(close): pin resolved book lock identity
seonghobae Sep 4, 2026
be97a48
fix(close): lock resolved accounting book identity
seonghobae Sep 4, 2026
cbbbed8
fix(close): remove accidental unrelated edit
seonghobae Sep 4, 2026
306f4c1
test(close): expose pre-lock journal snapshot race
seonghobae Sep 4, 2026
e0c707a
fix(close): fence journal population before hard close
seonghobae Sep 5, 2026
0babb3d
fix(close): install journal population fence
seonghobae Sep 5, 2026
cd8f3af
test(close): require safe retry after stale snapshot conflict
seonghobae Sep 5, 2026
a50011b
test(close): pin journal population serialization fence
seonghobae Sep 5, 2026
ff06eed
docs(close): record journal population serialization fence
seonghobae Sep 5, 2026
c8f9088
fix(close): harden journal population fence function
seonghobae Sep 5, 2026
84f5666
test(close): pin fence function hardening
seonghobae Sep 5, 2026
6fe1fde
test(close): expose open-period journal fence hotspot
seonghobae Sep 5, 2026
7a97984
fix(close): avoid open-period control-row write hotspot
seonghobae Sep 5, 2026
37d4f51
test(close): ratchet journal fence lock profile
seonghobae Sep 5, 2026
815f705
docs(close): record split journal fence lock profile
seonghobae Sep 5, 2026
2bdb09a
test(close): prove shared fence preserves open posting concurrency
seonghobae Sep 5, 2026
5f1da1f
docs(close): trace journal fence authority and lock tradeoff
seonghobae Sep 5, 2026
70a9b19
test(close): expose stale open-period hard-close race
seonghobae Sep 5, 2026
ac3a2a7
fix(close): stripe open-period journal population fence
seonghobae Sep 5, 2026
7cafddd
fix(close): install open-period population fence
seonghobae Sep 5, 2026
7279c9a
test(close): ratchet striped open-period freshness fence
seonghobae Sep 5, 2026
a6d32fc
fix(close): seed population fences before forcing RLS
seonghobae Sep 5, 2026
e5a95de
docs(close): trace direct open-period freshness fence
seonghobae Sep 5, 2026
73f1204
docs(close): align ADR with striped freshness witness
seonghobae Sep 5, 2026
cda5bb4
test(close): require canonical 0033 installer boundary
seonghobae Sep 5, 2026
35c76f4
test(close): cover open snapshot and fence completeness
seonghobae Sep 5, 2026
1683fd5
test(posting): expose period advisory serialization
seonghobae Sep 5, 2026
839e930
test(posting): ratchet open-period advisory hotspot
seonghobae Sep 5, 2026
e32e498
docs(posting): expose remaining advisory serialization
seonghobae Sep 5, 2026
e588289
docs(posting): record advisory-lock repair prerequisite
seonghobae Sep 5, 2026
e5f40ca
test(close): require control seed for every book period
seonghobae Sep 5, 2026
e22c2a6
fix(close): seed book-period control for future master data
seonghobae Sep 5, 2026
0d619c2
fix(close): install book-period control seed migration
seonghobae Sep 5, 2026
3d80b4d
docs(close): trace post-install book-period authority seeding
seonghobae Sep 5, 2026
e7378d8
test(close): ratchet future book-period authority seeding
seonghobae Sep 5, 2026
800716a
test(close): require owner-safe book-period backfill
seonghobae Sep 5, 2026
9278912
fix(close): seed book-period authority before forced RLS
seonghobae Sep 5, 2026
a19be19
fix(close): preserve forced RLS across cross-tenant seed repair
seonghobae Sep 5, 2026
dc06c5c
docs(close): record owner-safe forced-RLS migration boundary
seonghobae Sep 5, 2026
e063258
docs(close): trace forced-RLS migration-owner repair
seonghobae Sep 5, 2026
e040f34
docs(security): distinguish migration owner from runtime tenant
seonghobae Sep 5, 2026
29808a7
test(close): require owner visibility on forced-RLS seed sources
seonghobae Sep 5, 2026
f5a28af
fix(close): expose forced-RLS seed sources to migration owner
seonghobae Sep 5, 2026
027fae4
fix(close): include forced-RLS seed sources in owner repair
seonghobae Sep 5, 2026
c5e950a
docs(close): trace forced-RLS source visibility repair
seonghobae Sep 5, 2026
4a87f35
docs(close): include forced-RLS seed sources in owner decision
seonghobae Sep 5, 2026
cca0f5a
test(close): verify owner repair restores forced RLS
seonghobae Sep 5, 2026
895a1d1
docs(close): add real PostgreSQL forced-RLS runtime acceptance
seonghobae Sep 5, 2026
430f4dd
fix(posting): remove open-period close mutex
seonghobae Sep 5, 2026
70c07ab
test(posting): ratchet open-period lock removal
seonghobae Sep 5, 2026
a5eee53
docs(architecture): align period-close fence ownership
seonghobae Sep 5, 2026
9cbac18
docs(trace): close resolved posting mutex finding
seonghobae Sep 5, 2026
f109228
docs(adr): record open-post mutex repair
seonghobae Sep 5, 2026
7bde5a8
chore(docs): restore canonical documentation ownership
seonghobae Sep 5, 2026
6cbcf0e
test(close): expose concurrent book-period seed race
seonghobae Sep 5, 2026
a038f87
fix(close): serialize opposite-side authority seeding
seonghobae Sep 5, 2026
1d286cd
test(close): ratchet seed concurrency lock
seonghobae Sep 5, 2026
58b5f57
test(close): require repeatable-read seed retry
seonghobae Sep 5, 2026
5211045
fix(close): version tenant seed fence for fixed snapshots
seonghobae Sep 5, 2026
aa97d22
test(close): ratchet tenant MVCC seed witness
seonghobae Sep 5, 2026
29f871c
docs(trace): record book-period seed concurrency fence
seonghobae Sep 5, 2026
7592a31
test(close): reject tenant-period projection as new-book close authority
seonghobae Sep 5, 2026
083488b
test(close): reject new-book inheritance from shared period status
seonghobae Sep 5, 2026
ac38a09
test(close): pin open-only control seeding authority
seonghobae Sep 5, 2026
797cf55
fix(close): keep shared period projection out of book authority
seonghobae Sep 5, 2026
1efd6e7
docs(close): keep compatibility status out of book close authority
seonghobae Sep 5, 2026
6c2fa9f
docs(close): trace open-only book-period authority seeding
seonghobae Sep 5, 2026
44795d2
style(close): keep authority contract formatter-safe
seonghobae Sep 5, 2026
614d116
test(close): reject shared-status book authority projection
seonghobae Sep 5, 2026
610d770
fix(close): keep book-period authority on canonical seed path
seonghobae Sep 5, 2026
ee233b5
test(close): pin canonical book-period insert authority
seonghobae Sep 5, 2026
fa117a4
docs(close): trace canonical book-period insert authority
seonghobae Sep 5, 2026
8a6c283
test(close): reject shared period authority fallback
seonghobae Sep 5, 2026
8221e0a
fix(close): fail closed on missing book-period authority
seonghobae Sep 5, 2026
d61a10d
fix(close): restore complete persistence adapter after partial write
seonghobae Sep 5, 2026
795b3ef
test(close): require open-period seed on book activation
seonghobae Sep 5, 2026
ca98183
fix(close): seed book-period authority on activation
seonghobae Sep 5, 2026
ce73c8c
test(close): ratchet book activation seeding
seonghobae Sep 5, 2026
b1aa2e5
docs(close): trace book activation period authority
seonghobae Sep 5, 2026
ef2f9dc
test(close): reject GUC-only snapshot authority
seonghobae Sep 5, 2026
e2a01fc
fix(close): require canonical lock for retained snapshots
seonghobae Sep 5, 2026
b790b5d
fix(close): remove transient GUC snapshot authority
seonghobae Sep 5, 2026
a677226
test(close): pin non-spoofable snapshot authority
seonghobae Sep 5, 2026
b6310d8
docs(close): trace retained snapshot authority repair
seonghobae Sep 5, 2026
ad3347a
test(close): prove forged snapshot has closing capability
seonghobae Sep 5, 2026
952bb1b
fix(close): remove application period authority fallback
seonghobae Sep 5, 2026
a8c5abe
fix(close): restore complete persistence adapter after partial update
seonghobae Sep 5, 2026
6a0fd16
docs(close): record application authority repair contract
seonghobae Sep 5, 2026
9086422
test(close): preserve unrelated persistence surfaces
seonghobae Sep 5, 2026
048671f
fix: make book-period authority application-read-only
seonghobae Sep 5, 2026
4092ce2
docs: trace application book-period authority cleanup
seonghobae Sep 5, 2026
3ff7ac3
test(reporting): require book-scoped trial-balance close state
seonghobae Sep 5, 2026
a26e35a
test(reporting): reproduce sibling-open hard-close read drift
seonghobae Sep 5, 2026
d6acfec
docs(close): trace book-scoped snapshot read authority
seonghobae Sep 5, 2026
9af6fe8
fix(reporting): use book-period close authority for trial balance
seonghobae Sep 5, 2026
1c1360e
test(close): require snapshot and hard-close commit pairing
seonghobae Sep 5, 2026
9c810ea
fix(close): pair retained snapshot with hard-close commit
seonghobae Sep 5, 2026
1a9f28c
build(close): install snapshot hard-close pairing guard
seonghobae Sep 5, 2026
9c17dab
test(close): ratchet snapshot hard-close pairing guard
seonghobae Sep 5, 2026
58175fe
docs(close): trace deferred snapshot hard-close pairing
seonghobae Sep 5, 2026
c89d710
test(close): remove unused commit-pair import
seonghobae Sep 5, 2026
bea21ed
test(close): require hard-close snapshot pair
seonghobae Sep 5, 2026
26e71eb
fix(close): require retained snapshot on hard close
seonghobae Sep 5, 2026
a3c2b25
fix(close): install hard-close snapshot pair guard
seonghobae Sep 5, 2026
b7056cd
test(close): ratchet bidirectional commit pair
seonghobae Sep 5, 2026
80a719f
docs(close): record bidirectional commit pair
seonghobae Sep 5, 2026
780b66c
docs(close): trace hard-close snapshot pairing
seonghobae Sep 5, 2026
615d253
docs(close): align architecture with snapshot pair
seonghobae Sep 5, 2026
f1935c8
test(close): reject legacy one-sided hard close
seonghobae Sep 5, 2026
08c7b0f
fix(close): refuse legacy one-sided hard close
seonghobae Sep 5, 2026
4536a84
test(close): ratchet pair upgrade preflight
seonghobae Sep 5, 2026
6cc80d7
docs(close): trace pair upgrade preflight
seonghobae Sep 5, 2026
9923941
docs(close): bring operability through migration 0036
seonghobae Sep 5, 2026
eec9f4c
test(docs): keep operability on canonical migration chain
seonghobae Sep 5, 2026
344180c
test(close): reject silent book-period authority writes
seonghobae Sep 5, 2026
b33b187
fix(close): reject direct book-period authority writes
seonghobae Sep 5, 2026
5dd76f1
docs(close): trace explicit authority-write rejection
seonghobae Sep 5, 2026
ba6be58
test(close): preserve posted role at hard close
seonghobae Sep 5, 2026
8dabaee
test(close): ratchet posted role classification
seonghobae Sep 5, 2026
3dd278e
docs(close): trace posted role authority
seonghobae Sep 5, 2026
2851c6a
fix(close): classify historical P&L from posted roles
seonghobae Sep 5, 2026
858d299
docs(close): trace posted-role production repair
seonghobae Sep 5, 2026
0281743
docs(close): preserve posted role history at hard close
seonghobae Sep 5, 2026
6faea7d
test(close): preserve posted chart-account identity
seonghobae Sep 5, 2026
a612539
test(close): ratchet historical account identity boundary
seonghobae Sep 5, 2026
909c2c6
docs(close): trace posted account identity gap
seonghobae Sep 5, 2026
6c1063c
docs(close): extend temporal identity amendment
seonghobae Sep 5, 2026
dc50064
docs(close): note IFRS 18 transition boundary
seonghobae Sep 5, 2026
0d3caba
test: separate hard close from reporting projection authority
seonghobae Sep 5, 2026
0634205
docs: trace hard close reporting-authority separation
seonghobae Sep 5, 2026
c5266ce
test(close): reject account-code reuse redirection
seonghobae Sep 5, 2026
f8d199b
docs(close): trace runner red and account-code reuse
seonghobae Sep 5, 2026
ae2aaa1
test(close): bind snapshot hash to account entity
seonghobae Sep 5, 2026
f9f9265
docs(close): bind retained evidence hash to entity
seonghobae Sep 5, 2026
c5adb00
test(close): acquire snapshot authority lock in concurrency RED
seonghobae Sep 5, 2026
a1d96d9
test(close): use real authority lock for currency scope RED
seonghobae Sep 5, 2026
e91f783
test(close): bind snapshot scope REDs to real close lock
seonghobae Sep 5, 2026
91ae032
test(close): use real authority lock for pre-close snapshot RED
seonghobae Sep 5, 2026
9aa4e02
test(close): expose weak-isolation period transition
seonghobae Sep 5, 2026
ab5cd11
fix(close): require snapshot isolation for period transitions
seonghobae Sep 5, 2026
23e6441
test(close): expose unbound runtime fence seeding
seonghobae Sep 5, 2026
d1a294a
fix(close): fail closed on unbound runtime fence seeding
seonghobae Sep 5, 2026
4f146ad
test(close): reject read-uncommitted close transitions
seonghobae Sep 5, 2026
0d0077b
fix(close): allow only strong transition isolation
seonghobae Sep 5, 2026
bd1772b
test(close): preserve PostgreSQL RLS-bypass seeding authority
seonghobae Sep 5, 2026
66867a8
test(close): align RLS bypass check with effective role
seonghobae Sep 5, 2026
ec4d2c3
fix(close): preserve effective RLS bypass during fence seeding
seonghobae Sep 5, 2026
0602fc5
docs(close): trace transition and fence authority repairs
seonghobae Sep 5, 2026
5cb9cfb
test(close): release paused open-post thread on failure
seonghobae Sep 5, 2026
8cc0066
test(close): release paused close thread on failure
seonghobae Sep 5, 2026
61d8289
test(close): release paused adjusting thread on failure
seonghobae Sep 5, 2026
efc1916
test(close): require soft-close command evidence pair
seonghobae Sep 5, 2026
7279f05
fix(close): bind soft-close state to command evidence
seonghobae Sep 5, 2026
ce91cc1
fix(close): install soft-close evidence pair migration
seonghobae Sep 5, 2026
c594c78
test(close): ratchet soft-close evidence authority
seonghobae Sep 5, 2026
38076ba
docs(close): trace soft-close evidence pair
seonghobae Sep 5, 2026
2b24fe3
docs(ops): add soft-close evidence-pair recovery
seonghobae Sep 5, 2026
8268687
fix(close): preserve base migration errors
seonghobae Sep 6, 2026
e15b7b8
merge: restack #53 on prerequisite repair
seonghobae Sep 6, 2026
3090bbb
fix(close): preserve migration preflight atomicity
seonghobae Sep 6, 2026
66c91e4
merge: restack #53 on atomic migration preflight
seonghobae Sep 6, 2026
3832cf7
fix(close): preserve posted chart-account identity
seonghobae Sep 6, 2026
7c75e05
docs(close): record posted-account repair candidate
seonghobae Sep 6, 2026
9c3dba6
test(close): bind closing evidence to posted account identity
seonghobae Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions database/migrations/0009_accounting_book_period_control.sql
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ CREATE INDEX accounting_book_period_scope_index
tenant_account_id, accounting_book_id, fiscal_period_id, period_status_code
);

ALTER TABLE accounting_core.accounting_book_period_control ENABLE ROW LEVEL SECURITY;
ALTER TABLE accounting_core.accounting_book_period_control FORCE ROW LEVEL SECURITY;
CREATE POLICY accounting_book_period_isolation
ON accounting_core.accounting_book_period_control
USING (tenant_account_id = accounting_core.current_tenant_account_id())
WITH CHECK (tenant_account_id = accounting_core.current_tenant_account_id());
-- Migrations 0005+ force the table owner through tenant RLS on both source
-- relations. An unbound NOSUPERUSER/NOBYPASSRLS migration owner therefore
-- needs its normal owner visibility restored on the sources before this
-- all-tenant backfill. RLS stays enabled for non-owner roles, and FORCE is
-- restored before the migration transaction commits.
ALTER TABLE accounting_core.accounting_book NO FORCE ROW LEVEL SECURITY;
ALTER TABLE accounting_core.fiscal_period NO FORCE ROW LEVEL SECURITY;

INSERT INTO accounting_core.accounting_book_period_control (
tenant_account_id, accounting_book_id, fiscal_period_id,
Expand All @@ -44,6 +45,16 @@ JOIN accounting_core.fiscal_period
WHERE accounting_book.valid_to IS NULL
ON CONFLICT (tenant_account_id, accounting_book_id, fiscal_period_id) DO NOTHING;

ALTER TABLE accounting_core.fiscal_period FORCE ROW LEVEL SECURITY;
ALTER TABLE accounting_core.accounting_book FORCE ROW LEVEL SECURITY;

ALTER TABLE accounting_core.accounting_book_period_control ENABLE ROW LEVEL SECURITY;
ALTER TABLE accounting_core.accounting_book_period_control FORCE ROW LEVEL SECURITY;
CREATE POLICY accounting_book_period_isolation
ON accounting_core.accounting_book_period_control
USING (tenant_account_id = accounting_core.current_tenant_account_id())
WITH CHECK (tenant_account_id = accounting_core.current_tenant_account_id());

CREATE OR REPLACE FUNCTION accounting_core.guard_period_insert()
RETURNS trigger
LANGUAGE plpgsql
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- PostgreSQL requires CREATE INDEX CONCURRENTLY to run outside a transaction block.
-- Keep this migration to the single concurrent build so the canonical installer can
-- apply it as one autocommit statement without blocking ordinary table writes.
CREATE UNIQUE INDEX CONCURRENTLY trial_balance_snapshot_one_population_per_book_period
ON accounting_reporting.trial_balance_snapshot
(tenant_account_id, accounting_book_id, fiscal_period_id);
245 changes: 245 additions & 0 deletions database/migrations/0030_trial_balance_snapshot_immutability.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
BEGIN;

-- The unique index was built concurrently in migration 0029. Attaching it as a
-- constraint is a short metadata operation and makes the physical invariant part
-- of the table contract without rebuilding the index under a long write-blocking lock.
ALTER TABLE accounting_reporting.trial_balance_snapshot
ADD CONSTRAINT trial_balance_snapshot_one_population_per_book_period
UNIQUE USING INDEX trial_balance_snapshot_one_population_per_book_period;

-- Retained trial-balance values are accounting evidence, not three independently
-- writable amounts. Add the row-local invariant without scanning inherited rows
-- under this transaction's ADD-CONSTRAINT lock. Migration 0031 validates history
-- in a separate autocommit statement after this transaction releases its locks.
ALTER TABLE accounting_reporting.trial_balance_line
ADD CONSTRAINT trial_balance_line_net_balance_conservation
CHECK (net_balance_amount = debit_total_amount - credit_total_amount)
NOT VALID;

CREATE OR REPLACE FUNCTION accounting_reporting.reject_trial_balance_snapshot_mutation()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
RAISE EXCEPTION
'hard-close trial balance evidence is immutable (trial_balance_snapshot_immutable)'
USING ERRCODE = 'check_violation';
END;
$$;

CREATE TRIGGER trial_balance_snapshot_immutable_guard
BEFORE UPDATE OR DELETE
ON accounting_reporting.trial_balance_snapshot
FOR EACH ROW
EXECUTE FUNCTION accounting_reporting.reject_trial_balance_snapshot_mutation();

CREATE OR REPLACE FUNCTION accounting_reporting.reject_trial_balance_line_mutation()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
RAISE EXCEPTION
'hard-close trial balance evidence is immutable (trial_balance_snapshot_immutable)'
USING ERRCODE = 'check_violation';
END;
$$;

CREATE TRIGGER trial_balance_line_immutable_guard
BEFORE UPDATE OR DELETE
ON accounting_reporting.trial_balance_line
FOR EACH ROW
EXECUTE FUNCTION accounting_reporting.reject_trial_balance_line_mutation();

CREATE OR REPLACE FUNCTION accounting_reporting.guard_trial_balance_snapshot_insert()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_catalog, pg_temp
AS $$
DECLARE
period_status_value text;
book_legal_entity_id uuid;
book_reporting_currency_code text;
close_command_lock_held boolean;
BEGIN
SELECT accounting_book_period_control.period_status_code
INTO period_status_value
FROM accounting_core.accounting_book_period_control
WHERE accounting_book_period_control.tenant_account_id = NEW.tenant_account_id
AND accounting_book_period_control.accounting_book_id = NEW.accounting_book_id
AND accounting_book_period_control.fiscal_period_id = NEW.fiscal_period_id
FOR UPDATE;

IF period_status_value IS NULL THEN
RAISE EXCEPTION
'trial balance snapshot has no matching book-period authority (trial_balance_snapshot_scope_missing)'
USING ERRCODE = 'check_violation';
END IF;

SELECT accounting_book.legal_entity_id,
accounting_book.reporting_currency_code
INTO book_legal_entity_id,
book_reporting_currency_code
FROM accounting_core.accounting_book
WHERE accounting_book.tenant_account_id = NEW.tenant_account_id
AND accounting_book.accounting_book_id = NEW.accounting_book_id;

IF book_legal_entity_id IS NOT NULL
AND book_legal_entity_id IS DISTINCT FROM NEW.legal_entity_id THEN
RAISE EXCEPTION
'trial balance snapshot legal entity must own the accounting book (trial_balance_snapshot_book_entity_mismatch)'
USING ERRCODE = 'check_violation';
END IF;

IF book_reporting_currency_code IS NOT NULL
AND book_reporting_currency_code IS DISTINCT FROM NEW.snapshot_currency_code THEN
RAISE EXCEPTION
'trial balance snapshot currency must match the accounting book reporting currency (trial_balance_snapshot_currency_mismatch)'
USING ERRCODE = 'check_violation';
END IF;

IF period_status_value = 'hard_closed' THEN
RAISE EXCEPTION
'hard-close trial balance evidence is immutable (trial_balance_snapshot_immutable)'
USING ERRCODE = 'check_violation';
END IF;

-- The hard-close command always acquires this tenant/book/period advisory
-- lock before assembling close evidence. The lock remains present even when
-- zero net revenue/expense means no period-closing journal is emitted. A
-- caller-set journal_write_role GUC is therefore never snapshot authority.
SELECT EXISTS (
SELECT 1
FROM pg_catalog.pg_locks AS held_lock
JOIN accounting_core.tenant_account
ON tenant_account.tenant_account_id = NEW.tenant_account_id
JOIN accounting_core.accounting_book
ON accounting_book.tenant_account_id = NEW.tenant_account_id
AND accounting_book.accounting_book_id = NEW.accounting_book_id
JOIN accounting_core.fiscal_period
ON fiscal_period.tenant_account_id = NEW.tenant_account_id
AND fiscal_period.fiscal_period_id = NEW.fiscal_period_id
WHERE held_lock.locktype = 'advisory'
AND held_lock.pid = pg_backend_pid()
AND held_lock.database = (
SELECT pg_database.oid
FROM pg_catalog.pg_database
WHERE pg_database.datname = current_database()
)
AND held_lock.mode = 'ExclusiveLock'
AND held_lock.granted
AND held_lock.objsubid = 2
AND held_lock.classid::bigint = (
hashtext(tenant_account.tenant_account_code)::bigint & 4294967295
)
AND held_lock.objid::bigint = (
hashtext(
'period:' || accounting_book.accounting_book_id::text || ':' || fiscal_period.period_code
)::bigint & 4294967295
)
) INTO close_command_lock_held;

IF period_status_value <> 'soft_closed'
OR NOT pg_has_role(session_user, 'accounting_closing_writer', 'MEMBER')
OR NOT close_command_lock_held
THEN
RAISE EXCEPTION
'trial balance snapshot creation requires the purpose-limited hard-close writer (trial_balance_snapshot_authority_required)'
USING ERRCODE = 'check_violation';
END IF;

-- Snapshot chronology is an AIS system-time fact. A purpose-limited closing
-- writer may supply accounting evidence but cannot select the recording clock.
NEW.snapshot_generated_at := clock_timestamp();

IF EXISTS (
SELECT 1
FROM accounting_reporting.trial_balance_snapshot
WHERE trial_balance_snapshot.tenant_account_id = NEW.tenant_account_id
AND trial_balance_snapshot.accounting_book_id = NEW.accounting_book_id
AND trial_balance_snapshot.fiscal_period_id = NEW.fiscal_period_id
) THEN
RAISE EXCEPTION
'trial balance snapshot population already occupies this book-period (trial_balance_snapshot_population_conflict)'
USING ERRCODE = 'check_violation';
END IF;

RETURN NEW;
END;
$$;

REVOKE ALL ON FUNCTION accounting_reporting.guard_trial_balance_snapshot_insert()
FROM PUBLIC;

CREATE TRIGGER trial_balance_snapshot_population_guard
BEFORE INSERT
ON accounting_reporting.trial_balance_snapshot
FOR EACH ROW
EXECUTE FUNCTION accounting_reporting.guard_trial_balance_snapshot_insert();

CREATE OR REPLACE FUNCTION accounting_reporting.guard_trial_balance_line_insert()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_catalog, pg_temp
AS $$
DECLARE
period_status_value text;
snapshot_book_id uuid;
chart_account_book_id uuid;
BEGIN
SELECT accounting_book_period_control.period_status_code,
trial_balance_snapshot.accounting_book_id
INTO period_status_value, snapshot_book_id
FROM accounting_reporting.trial_balance_snapshot
JOIN accounting_core.accounting_book_period_control
ON accounting_book_period_control.tenant_account_id
= trial_balance_snapshot.tenant_account_id
AND accounting_book_period_control.accounting_book_id
= trial_balance_snapshot.accounting_book_id
AND accounting_book_period_control.fiscal_period_id
= trial_balance_snapshot.fiscal_period_id
WHERE trial_balance_snapshot.tenant_account_id = NEW.tenant_account_id
AND trial_balance_snapshot.trial_balance_snapshot_id
= NEW.trial_balance_snapshot_id
FOR UPDATE OF accounting_book_period_control;

IF period_status_value IS NULL THEN
RAISE EXCEPTION
'trial balance snapshot has no matching book-period authority (trial_balance_snapshot_scope_missing)'
USING ERRCODE = 'check_violation';
END IF;

SELECT chart_account.accounting_book_id
INTO chart_account_book_id
FROM accounting_core.chart_account
WHERE chart_account.tenant_account_id = NEW.tenant_account_id
AND chart_account.chart_account_id = NEW.chart_account_id;

IF chart_account_book_id IS NOT NULL
AND snapshot_book_id IS DISTINCT FROM chart_account_book_id THEN
RAISE EXCEPTION
'trial balance line chart account must belong to the snapshot accounting book (trial_balance_line_book_scope_mismatch)'
USING ERRCODE = 'check_violation';
END IF;

IF period_status_value = 'hard_closed' THEN
RAISE EXCEPTION
'hard-close trial balance evidence is immutable (trial_balance_snapshot_immutable)'
USING ERRCODE = 'check_violation';
END IF;

RETURN NEW;
END;
$$;

REVOKE ALL ON FUNCTION accounting_reporting.guard_trial_balance_line_insert()
FROM PUBLIC;

CREATE TRIGGER trial_balance_line_population_guard
BEFORE INSERT
ON accounting_reporting.trial_balance_line
FOR EACH ROW
EXECUTE FUNCTION accounting_reporting.guard_trial_balance_line_insert();

COMMIT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE accounting_reporting.trial_balance_line
VALIDATE CONSTRAINT trial_balance_line_net_balance_conservation;
100 changes: 100 additions & 0 deletions database/migrations/0032_period_close_journal_population_fence.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
BEGIN;

-- Journals admitted while a period is soft-closed change the authoritative
-- population that hard close is about to freeze. Version those close-window
-- writes on the same control row that hard close later locks. Ordinary open-
-- period posting must not UPDATE that row on every journal: concurrent open
-- posting instead holds a shared row lock so a period-state transition cannot
-- overtake a journal that was admitted under the open-state contract.
ALTER TABLE accounting_core.accounting_book_period_control
ADD COLUMN journal_population_revision bigint NOT NULL DEFAULT 0
CHECK (journal_population_revision >= 0);

CREATE OR REPLACE FUNCTION accounting_core.guard_period_insert()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_catalog, pg_temp
AS $$
DECLARE
period_status_value text;
locked_period_status_value text;
journal_write_role_value text;
BEGIN
SELECT accounting_book_period_control.period_status_code
INTO period_status_value
FROM accounting_core.accounting_book_period_control
WHERE accounting_book_period_control.tenant_account_id = NEW.tenant_account_id
AND accounting_book_period_control.accounting_book_id = NEW.accounting_book_id
AND accounting_book_period_control.fiscal_period_id = NEW.fiscal_period_id;

IF period_status_value IS NULL THEN
RAISE EXCEPTION
'accounting book fiscal period control is missing for this journal insert (period_control_missing)'
USING ERRCODE = 'check_violation';
END IF;

IF period_status_value = 'open' THEN
-- Many ordinary postings may share this row lock concurrently. A soft-close
-- UPDATE must wait for all journals admitted as open-period work to commit.
-- If the period changed while this statement waited, retry from a fresh
-- transaction rather than silently applying open-period authority to a
-- soft-closed period.
SELECT accounting_book_period_control.period_status_code
INTO locked_period_status_value
FROM accounting_core.accounting_book_period_control
WHERE accounting_book_period_control.tenant_account_id = NEW.tenant_account_id
AND accounting_book_period_control.accounting_book_id = NEW.accounting_book_id
AND accounting_book_period_control.fiscal_period_id = NEW.fiscal_period_id
FOR SHARE;

IF locked_period_status_value = 'open' THEN
RETURN NEW;
END IF;

RAISE EXCEPTION
'accounting book fiscal period changed during journal admission (period_state_changed_retry)'
USING ERRCODE = 'serialization_failure';
END IF;

journal_write_role_value := nullif(
current_setting('accounting_core.journal_write_role', true),
''
);

IF period_status_value = 'soft_closed'
AND journal_write_role_value IN ('period_closing', 'adjusting', 'reversal')
AND pg_has_role(session_user, 'accounting_closing_writer', 'MEMBER')
THEN
-- Close-window journals are intentionally serialized on this revision.
-- If hard close already won the row, PostgreSQL either raises a serialization
-- failure under REPEATABLE READ or this predicate stops matching after wait.
locked_period_status_value := NULL;
UPDATE accounting_core.accounting_book_period_control
SET journal_population_revision = journal_population_revision + 1
WHERE tenant_account_id = NEW.tenant_account_id
AND accounting_book_id = NEW.accounting_book_id
AND fiscal_period_id = NEW.fiscal_period_id
AND period_status_code = 'soft_closed'
RETURNING period_status_code
INTO locked_period_status_value;

IF locked_period_status_value = 'soft_closed' THEN
RETURN NEW;
END IF;

RAISE EXCEPTION
'accounting book fiscal period changed during close-window journal admission (period_state_changed_retry)'
USING ERRCODE = 'serialization_failure';
END IF;

RAISE EXCEPTION
'Accounting book fiscal period is % (period_closed). Ordinary journals cannot be inserted after close. Post the AIS closing journal before hard-close; do not insert a later ordinary or reversal journal into a locked book period.',
period_status_value
USING ERRCODE = 'check_violation';
END;
$$;

REVOKE ALL ON FUNCTION accounting_core.guard_period_insert() FROM PUBLIC;

COMMIT;
Loading