GOTT · Sibyla · approved 2026-08-11

Master Data Consolidation & UI Plan

Database structure first, then the human interface over it. Companion to docs/V6.md and docs/project-todo-v6.md; the party collapse landed as S1.5 between S1 and S2.

This reconciled HTML twin is current; master-data-and-ui-plan.md is authoritative. From rev. 17 this twin carries the .md's revision number rather than a counter of its own, which had reached rev. 12 and made the two look out of step when they were not.

2026-08-11 · rev. 17, 2026-08-21 · Approved by Miguel. M-D1…M-D8 are decisions of record; S1.5 is complete through S1.5-8. M-D8 (2026-08-21, with the Stage 11 source alignment) states that a source retirement is evidence, not a target mutation — a tombstoned row keeps its code reserved and is never seeded, while an already-seeded row under that code is left to the target and reported as skipped — and that an identity pin binds to a natural key, not to a code: the two governed related-party kinds are pinned by their (Class, Subclass) pair, because Stage 11 re-issued the ITMCLS codes underneath them.

Current state: S1 closed 2026-08-20 on the demonstrated “CTT” exit — Miguel walked all nine Entity 360 tabs against the real EC000007 row at 14:45, the session’s two findings (the Roles-tab mojibake !121, the empty Tax Identifiers tab !122 + !124) were fixed the same day, and he validated the exit with screenshots as the record. P2-2 stays closed without consolidation; ITMCLS, MNGACC and COCACC remain separate. Every buildable S1-9 piece is delivered: the module shell (!63) and <ReferenceGrid> (!64); all six read-only c descriptor screens — ITMCLS (!65), MNGACC (!66), COCACC (!69), then EXCRAT with the missing-rates panel (!100), ITMALS (!101) and ENTALS (!102) on 2026-08-20, that ENTALS descriptor screen being distinct from the Entity 360 Aliases tab; all eleven Entity 360 slices — Identity shell (!74), governed Identity (!79), Tax identifiers (!82), Roles (!84), read-only Aliases (!86), governed Aliases/ENTALS (!89), Banking (!91), Items (!93) and the read surfaces Documents (!96), Balances (!97) and Audit (!98); the ITMMST item master (!109), the Entity × Item matrix with the classification queue (!110), the entity-banking catalogue (!112) and the SNCACC tree (!113); the Companies entity half repointed onto the governed identity write (!107), its connection half untouched S4 code; the DOCEFL read-only flag-rules catalogue (!104); and the DOCTYP governed presentation write (!105, LabelPt/LabelEn/SortOrder only) with the governed offer toggle (!117) — g1 and g2. Two remainders were carried out of S1: g3, the routing columns, blocked on O-1 (Luís), and the DOCEFL editor, formally deferred to S2 (O-4, Miguel). S2 is open — registry-bridge slices 1–5 merged 2026-08-20/21, and the reference-data source moved to Stage 11 on 2026-08-21 (!135/!137), which is what M-D8 records.

0 · What changed between drafting and approval

M-D1's cost argument was that the P11aSchemaSql ENTMST is empty and has no writerSTATE.md said so explicitly. That stopped being true the same day this plan was written. !43 (6b42d79) and 7a93a59 merged the Layer 2 reference seeder, which writes ENTMST, ENTALS, ENTBNK and ENTITM.

Seeded nowRows
ENTMST · ENTALS · ENTBNK119 · 7 · 43
ENTITM — FK'd on CodeName327
What does not change. The 26 inbound foreign keys on BusinessEntity still do not move. Collapsing upward is still far cheaper than the downward alternative, and every argument below about the rename carrying constraints by OID stands untouched.

What changed — three things. (1) Phase B became a destructive migration against populated data, so S1.5 carried two stop-list items rather than one. (2) The safety argument changed from emptiness to reproducibility: S1.5-3 demonstrated the idempotent, read-only-source recovery path before phase B ran. (3) The seeder was repointed in S1.5-8; upsert by natural key now resolves CodeName through ExternalIdentity to EntityId.

And one argument became stronger. Every further row seeded against a CodeName foreign key would have been another row to migrate at S1.5-4. ENTITM already had 327. The cost of this phase grew with delay, which was the practical reason to run it before S1-9.

1 · What is actually duplicated

The premise was that thirteen tables are repeated. Three are. The rest are capability the July spine never had — and separately, seven July tables look like duplicates but are not, and deleting them would remove working behaviour.

3
Repeated — replace, but merge missing fields first
ENTMST · ITMMST · DOCTYP
10
New — no July-spine counterpart at all
ENTALS ENTBNK ITMALS ENTITM ITMCLS MNGACC SNCACC COCACC EXCRAT DOCEFL
3
July tables retired
BusinessEntity · DocumentTypeSetting · IntegrationProductPlKeyMapping
7
Look duplicated, are not — keep and repoint
EntityTaxIdentifier · EntityRoleAssignment · ExtractionProfile · ProductBinding · PartyBinding · BankAccount · CashAccount
Replace + merge New Retired Keep & repoint

2 · The overlap matrix

Every table named in the brief, plus the July-spine counterpart it collides with.
FDR tableJuly counterpartVerdictStructural resolution
ENTMSTBusinessEntityMerge upward — one table The two became one physically: BusinessEntity was renamed to ENTMST and gained the FDR business columns; the seeded, reproducible P11a ENTMST was dropped. It also gained five columns the FDR spec never had — inactive, origin, merged into, a searchable normalised name, blocked from new captures.
ITMMSTIntegrationProductPlKeyMappingReplace + merge 2 ProductCode → PlKey is the same relationship ITMMST states as ItemCode → PLMKEY, done per-provider. ITMMST now subsumes it and carries NormalizedItemDesc and IsActive.
DOCTYPDocumentTypeSetting
+ FinancialDocumentType enum
Replace + merge 4 DocumentTypeSetting was presentation only — a PT/EN label on a hard-coded enum. DOCTYP is behavioural and now carries LabelPt, LabelEn, SortOrder and IsActive.
ENTALSNewThe July spine had only an implicit NormalizedLegalName match; ENTALS adds governed aliases.
ENTBNK +2 childrenNewNot a duplicate of BankAccount — see §3.
ITMALSNewThe July spine's nearest equivalent was free text in DocumentLine.Description.
ENTITMNewThe per-entity item classification that makes the learning path possible; the July spine had no equivalent.
ITMCLSNew
MNGACCNew
SNCACCNewThe July spine had no chart of accounts. DocumentLine.Category was an unconstrained string.
COCACCNewOne placeholder row today. Real cost centres arrive through the UI, not the seeder.
EXCRATNewThe July spine had no rate table; ExtractionRevision.Currency records a currency but does not convert it.
DOCEFLNewThe July equivalents — ValidationGatesJson, WarningsJson and AttentionReason — were ad hoc and hard-coded rather than a catalogue.

3 · The four “duplicates” that are not

PairVerdictWhy they are different things
ENTBNK ↔ BankAccount / CashAccountKeep both BankAccount holds our own companies’ accounts — the source side of PurchasePaymentInstruction. ENTBNK holds counterparty routing — how we pay a supplier. Opposite sides of the transaction; both read at step 8. The real duplicate of BankAccount is BNKACC (Layer 6), deferred to Phase 2 — do not touch it in Phase 1.
ENTMST.FiscalNo ↔ EntityTaxIdentifierKeep child ENTMST carries one FiscalNo. EntityTaxIdentifier is 1:N with type, primary flag, normalised and original value — a VAT number and a national ID and an EORI. Capability ENTMST lacks. Repoint to ENTMST; FiscalNo becomes the denormalised primary.
ENTMST.Role ↔ EntityRoleAssignmentKeep child ENTMST.Role is a scalar. An entity is routinely supplier and customer, and an internal company is an entity with a Company role. A scalar cannot express that. Keep the child as authoritative; keep the scalar as denormalised primary role for display and export parity.
ENTALS ↔ IntegrationProduct/PartyBindingKeep both Bindings cache the provider’s external id, keyed by (Company, ConnectionKey, Provider). An alias is a name a document uses. Different lifetimes, different owners. One cleanup: drop IntegrationPartyBinding.TaxIdentifier, a stored copy of what ENTMST already holds.

4 · Target structure

One party table — counterparties and internal companies alike. The authoritative EntityRoleAssignments child identifies InternalCompany; scalar ENTMST Role='Company' remains a display/export projection. ENTMST is the physical BusinessEntity table renamed, so all 26 inbound foreign keys followed it untouched. CompanyRegistry keeps its shipped job.

TENANT / IDENTITY ENTITY MASTER REGISTRY — written at step 5, not master CONFIGURATION ITEM & ACCOUNTING RENAME TO— same table Company role CompanyRegistryCompanyCode · active tenant BusinessEntityrenamed to ENTMST —26 FKs follow it DOCTYPTreatment · ProcessingRoute · step 2 DOCEFLflag rule catalogue — step 6 ENTMST ★entity master — the hubrenamed · uuid Id is the only join key Role assignmentsauthoritative multi-role ENTALSaliases ENTBNKpayment routing Account /Method EntityTaxId1:N identifiers DOCLOGcapture register FDCHDR / FDCDTLfiscal entries DOCFLGflag instances ENTITMentity × item ITMMST+ ITMALS ITMCLSclass / subclass MNGACCPLMKEY / PLMKO SNCACCchart of accounts COCACC — cost centres EXCRAT — FX rates
Solid borders mark the verdict: replace + merge new keep & repoint. ENTMST is the BusinessEntity table renamed. PostgreSQL carries inbound FK constraints through the rename, so there is no foreign-key DDL at all.

Why collapsing upward costs no foreign-key DDL at all

There are two directions to collapse in, and they are not symmetric. Downward — create ENTMST properly and retarget every FK onto it — is 26 constraint changes across 21 tables, and 26 chances to get a live reference wrong. Upward — keep the table the FKs already point at, and rename it — costs zero. PostgreSQL carries inbound foreign-key constraints through ALTER TABLE "BusinessEntity" RENAME TO "ENTMST"; they follow the table by OID, not by name.

Untouched by the renameWhat the FDR side cost
All 26 uuid FK columns · Document(Id, BuyerEntityId) — the FDCHDR composite anchor · CompanyRegistry(CompanyId, CompanyCode) · Membership · Department · both ERP connections · all six IntegrationSales* tables · every index Drop the P11a ENTMST and its seeded rows · add the FDR business columns · re-establish five child FKs — now on ENTMST.Id, not CodeName (ENTALS, ENTITM, ENTBNK, ENTBNKMethod, ENTBNKAccount) · create ExternalIdentity and backfill both code schemes · re-point the CHECK constraints · fold the DDL into EF migrations and retire P11aSchemaSql · rewrite the 61-test suite once
The original cost premise moved, but the direction held. The P11aSchemaSql ENTMST was empty when the decision was drafted, then the Layer 2 seeder populated it. Phase B therefore dropped reproducible seeded data under the demonstrated recovery path. The collapse still completed without retargeting the 26 live constraints. The source-semantics work was integrated with the merged-table sequence, and the post-collapse seeder realization completed in cumulative MR !59.

The migration — pin, prepare, rename, contract

PhaseWhatReversible?
0 — pinFroze a schema contract covering the 26 FK join paths, the FDCHDR composite anchor, tenancy joins, ERP binding reads and archive-path resolution.n/a — it is the evidence
A — prepareAdded the FDR business columns and ExternalIdentity, backfilled parties and recorded issued codes.Additive only
B — renameDropped the seeded P11a ENTMST under stop-list approval, renamed BusinessEntity to ENTMST, established child FKs and constraints, created ENTMST_Export, and made EF migrations authoritative.Recovery was the demonstrated seeder path
B′ — verifyReplayed the unchanged phase-0 query set with an empty diff; repointed the seeder (S1.5-8) and demonstrated governed byte-equal replay.Complete
C — contractDrop LegacyCode and transitional shims once nothing reads them.Phase 2 plan, P2-6
The drop was reversible by regeneration, not by emptiness (§0). Dropping the P11a ENTMST destroyed seeded reference rows; the read-only source and idempotent seeder were the restore point, and S1.5-3 demonstrated that path before phase B ran. The phase-0 schema contract was the evidence specific to the rename and proved the existing join paths survived. Two stop-list items were involved: CodeLedger issuance in phase A, and the phase-B drop.

What ENTMST absorbs

ColumnTypeWhy ENTMST cannot do without it
Iduuid PKAlready there — the table is BusinessEntity renamed. Stays the primary key, so all 26 inbound FKs remain valid and the five FDR children now FK here too. The only key anything joins on.
Statusvarchar(24)ENTMST has IsProvisional only — it cannot express Inactive. Every FK is RESTRICT, so retiring is the only available operation and there is no column for it.
MergedIntoIduuid NULL FK ENTMST(Id)Duplicate-identity merge is an existing, used operator function. Self-referencing, RESTRICT; a merged row is never deleted.
Originvarchar(24)Extracted · Manual · Seeded · Integration. Answers “did a human assert this, or did an agent guess it?” — the first question asked of a suspicious counterparty.
NormalizedEntityNametextResolution is NIF → alias → name. The name leg needs a deterministic search key; CodeName is a permanent human code and cannot serve.
IsActiveForCaptureboolAn entity may be Active for history while blocked from new captures. Cheap now, expensive once documents reference it.
LegacyCodevarchar(64) NULLTransitional — preserves the July Code so nothing quoting it breaks mid-migration. Dropped in Phase 2.

Business codes leave the master table realized in S1.5

EntityCode and CodeName do not arrive as columns at all. They become rows in a mapping table, one per entity per scheme — fdr.entity_code, fdr.code_name, and later cegid.party / moloni.customer.

ExternalIdentity
  Id         uuid PK
  EntityId   uuid NOT NULL FK ENTMST(Id) RESTRICT
  Scheme     varchar(32) NOT NULL
  Code       varchar(64) NOT NULL
  IsPrimary  bool NOT NULL DEFAULT false
  IssuedAt   timestamptz NOT NULL
  UNIQUE (EntityId, Scheme)   -- one code per entity per scheme
  UNIQUE (Scheme, Code)       -- unique inside its own scheme, not globally
Why this is the one item that cannot wait. S1.5-2 is the moment codes are minted; the window is one migration wide. A minted code on a master column is permanent and load-bearing the instant it exists — the same code as a mapping row is a fact that can be superseded.

The rename cascade disappears. CodeName is currently the FK target of five tables, which is why the FDR prototype needed rename_codename.py and validate_codename_immutability.py. With the children FK'd on ENTMST.Id, a rename is one UPDATE of one row and both scripts stop being needed. That is prototype evidence, not prediction.

Two costs, stated plainly. Every export path gains a join — mitigated by an ENTMST_Export view that renders the flat FDR shape, so the exporter's SQL does not change. And the step-9 archive path resolves through that view rather than a column read. What does not change: codes are still issued once, still read-only in the UI, a rename is still a governed command with an audit event — it is merely no longer a cascade.

5 · Why the flow decides this, not the row counts

Every one of the thirteen tables is read at step 4 or step 8, and four of them are written at step 7. That last row is the argument for the maintenance UI: step 7 is the difference between a system that learns and a tool people abandon.

#StepMaster tables readWritten
1A document arrivesCompanyRegistry · ENTALS
2It is recognisedDOCTYP · EntityRoleAssignments
3It is readExtractionProfile
4It is classifiedENTMST → ENTALS → name · EntityRoleAssignments gate · ITMALS → ITMMST → ENTITM → ITMCLS → MNGACC → SNCACC → COCACC · EXCRAT if not EURENTMST (provisional)
5It is persistedCodeLedgerDOCLOG · FDCHDR · FDCDTL
6Uncertainty is surfacedDOCEFLDOCFLG
7The human’s answer is learnedENTALS · ENTITM · ITMALS · ENTMST
8It is pushed to CegidENTMST.FiscalNo · ENTBNK · PartyBinding · MNGACC / SNCACC · BankAccountPartyBinding
9It is filedExternalIdentity via ENTMST_Export (archive path)
The July equivalents fail the same walk. DocumentTypeSetting is read at no step — it only labels. IntegrationProductPlKeyMapping is read at step 8 only, for one provider. BusinessEntity is read at steps 1, 2, 4 and 8 — which is exactly why it has to merge rather than simply be replaced.

6 · Screens

One reusable <ReferenceGrid> driven by a per-table metadata descriptor covers the flat tables; seven get purpose-built screens. Every master write — from the grid, from the review queue’s learning path, or from the seeder — goes through the same application service.

ScreenRouteTableBuildThe thing that makes it non-trivial
Entity 360/master/entitiesENTMSThandNine tabs over one entity. Replaces Suppliers.razor and is the S1 exit criterion. Delivered: read-only Identity shell !74, governed Identity !79, Tax identifiers !82, Roles !84, read-only Aliases !86, governed Aliases/ENTALS writes !89, governed Banking !91, governed Items !93, read-only Documents !96, Balances !97 and Audit !98. All nine tabs built, and the exit was demonstrated 2026-08-20 — Miguel walked them against the real EC000007 row and validated it with screenshots; S1 closed on it.
Item master/master/itemsITMMSThandReverse view: which entities use this item.
Entity × Item/master/entity-itemsENTITMhand“Unclassified lines seen in the last 30 days” turns a reference table into a work queue.
Entity banking/master/entity-bankingENTBNKhandEdit the children, regenerate the parent render-parity strings — never the reverse. Unscoped rows disable PayAccount structurally.
Chart of accounts/master/accounts/sncSNCACChand3,509 rows since the Stage 11 source alignment, self-FK tree (518 at the Aug-6 pin; the 3,509 figure was withdrawn as unmeasured under v6 §5 and was re-measured by !135). Shows how many ITMMST/ENTITM rows point at each account — that is what makes a change safe.
Document types/admin/document-typesDOCTYPrewriteA routing preview in words next to each row — that sentence is the behaviour of the table. Read-only catalogue + PT/EN preview shipped with !72; g1 (governed presentation write, !105) and g2 (governed offer toggle, !117) shipped 2026-08-20; g3, the routing columns, is blocked on O-1.
Flag rules/admin/flag-rulesDOCEFLhandHighest consequence in the plan — changes what blocks production without a deploy. Needs a pattern tester against recent documents and an impact count before save. Read half shipped 2026-08-20 (!104, read-only catalogue); the editor is formally deferred to S2 and waits on O-4.
Aliases · classes · accounts · FX/master/…ENTALS ITMALS ITMCLS MNGACC COCACC EXCRATgenericDescriptor-driven. EXCRAT must show which rates are missing for the period, not just list what exists.
Companies/admin/companiesENTMST + RegistryrepointRepoint the entity half only. The connection half is working S4 code — do not touch it.

Grid capabilities — all mandatory, because a half-built grid is worse than none

CapabilityRequirement
DeleteThere is no delete. Every FK is RESTRICT. The action is deactivate / retire, and the UI says so.
ConcurrencyOptimistic on PostgreSQL xmin; a conflict shows both values and asks — never silently overwrites.
ValidationThe CHECK constraints are the contract. The UI reproduces their messages in PT and EN rather than merely failing.
Company scopeA persistent banner naming the active company. The shared Gott/Itoorer inbox is a documented hazard; a row edited under the wrong company is a correctness bug.
Permanent codesEntityCode, CodeName, ItemCode, EICode render read-only with an explanatory tooltip.
AlsoServer-side paging · per-column filter row · inline edit with per-row dirty state · audit drawer over AuditEvent · CSV/XLSX export of the filtered view · bulk column set with a row-count confirmation · per-table permissions.

8 · Sequencing

S1.5 is complete through S1.5-8. Cumulative MR !59 completed the party collapse on 2026-08-15. The table records outcomes, not prospective work.

ItemWorkNote
S1.5-0Layer 2 source-semantics work integrated with the merged-table sequenceComplete
S1.5-1Phase 0 pinned the schema contract with its fixture and query setComplete
S1.5-2Phase A added the business columns and ExternalIdentity, backfilled parties and recorded issued codesCodeLedger issuance was stop-list work
S1.5-3Reconciliation and the seeder-based recovery path were demonstratedPhase B prerequisite satisfied
S1.5-4Phase B dropped the seeded P11a ENTMST, renamed the live party table to ENTMST, moved child FKs to Id, and made EF migrations authoritativeApproved destructive migration
S1.5-5Phase B′ replayed the unchanged phase-0 contract with an empty diffExit criterion satisfied
S1.5-6The 61-test P11aDatabase suite now runs against migrationsComplete
S1.5-7Compatibility realization retained authoritative EntityRoleAssignments with InternalCompany, scalar ENTMST Role='Company', and party reads on merged ENTMSTNo physical ENTROL; S2-7 remains open
S1.5-8The reference-data seeder was repointed to the post-collapse shape with governed byte-equal replayComplete
Phase 2Seven deferred structural items are in master-data-phase2-plan.md. P2-2 is closed without consolidation; the tables stay separate and the decision does not reopen at S1-9c. Only P2-3 remains a Phase 1 decision point, at S2-6.see companion

9 · Open questions

Five remain. Each changes what gets built; none blocks continuing the work above.

#QuestionWhy it mattersOwner
O-1DOCTYP.DocClass — is Payables/Receivable a typo?Open since 2026-08-05. The DOCTYP screen fixes it at source or preserves it verbatim forever.Luís
O-2Who owns the cost-centre list?COCACC has one placeholder row. The screen is inert without an owner.Business
O-3Is SNCACC global or per company?Cheap now, expensive once entries reference rows. Tied to the open LMD cross-check.Accounting
O-4Who may edit DOCEFL rules?Editing them changes production blocking behaviour without a deploy.Miguel
O-5Does an entity need Inactive while carrying posted documents?Decides whether Status and IsActiveForCapture are genuinely two columns.Business