Skip to content

Group register — design (Phase 6, stage 1 of consolidation)

Status: DRAFT for owner sign-off. Nothing is built. Roadmap item: Phase 6, “Multi-entity consolidation and inter-company eliminations”, stage one. Architecture decision it rests on: section 18.2 item 5, one org = one legal entity; consolidation is solved above the ledger.

Reference: korre-pm-be prisma/migrations/20261119000000_company_groups (six tables: groups, entities, shareholdings, relationship assessments, beneficial owners, access grants). This design follows its shape and closes two gaps it has: the grantee rule is a comment, not a constraint, and the register has no permissions of its own.

In: a register, kept by one organization (the group’s owner), of the entities in a group, who holds what share of whom and when, who the beneficial owners are, and which member organizations have consented to the owner reading their books.

Out, each its own later spec: consolidation runs, inter-company eliminations, currency translation, consolidated statements. The register only decides who is in the group and how they relate, and whether the owner may read a member.

2. Where the register lives (the central decision)

Section titled “2. Where the register lives (the central decision)”

Every business table carries org_id with FORCE ROW LEVEL SECURITY. A group spans organizations, so the question is whose org_id a group row carries.

Decision: the owner organization’s. company_group, group_entity, shareholding, relationship_assessment and beneficial_owner all carry org_id = the owner, with the standard org_isolation policy. No RLS change, no new tenancy mode, and the existing tenant-isolation drift test covers them unchanged. A member organization cannot see the register; it only sees the consent grants it has made.

group_entity is any legal person in the group. It may link to an organization on this platform (member_org_id, nullable) or stand for a company or individual that is not on the platform. Consolidation later needs the linked ones; the register needs all of them, because ownership chains run through outside holding companies.

Section titled “3. Consent: the grantee rule in the schema”

A member organization lets the owner read its books by making a grant. Grants are made by the member, so the row lives in the member’s org (org_id = member), and only the member’s administrators can create or revoke it.

group_access_grant
org_id -- the member (grantor); RLS isolates it
group_id -- the group
grantee_org_id -- must be the group's owner
scope -- 'ledger_read' | 'balances_read' (vocabulary strings)
granted_by, granted_at, revoked_by, revoked_at
FOREIGN KEY (group_id, grantee_org_id) REFERENCES company_group (id, org_id)

The composite foreign key is the grantee rule: a grant can only name the organization that actually owns that group. A foreign-key check is not subject to RLS, so the member org need not be able to see the group row. The reference only comments the rule.

Revocation is a new dated row state, never a delete (revoked_at); a grant cannot be re-enabled, a new one is made. The ledger-append-only habit applies.

Cross-org reads. The owner reading a member’s figures is the one place isolation is crossed. It goes through allowlisted SECURITY DEFINER functions (the exception the architecture spec records as D1, section 3.3), each of which:

  1. takes the group and member as arguments, never an organization from a request header;
  2. checks an unrevoked grant of sufficient scope exists, in the same call;
  3. returns only that scope’s data, and writes an audit row in the member’s chain (“the group owner read your balances for period X”).

Stage one ships the grant and the check function (group_read_allowed) and the audit entry. The data-returning functions arrive with consolidation runs. Each will need the drift-test allowlist entry and a test that shows it refuses without a grant, after one that shows it answers with one.

All dated rows use effective_from / effective_to (exclusive end). Percentages are NUMERIC(9,6) and travel as strings. Share counts are NUMERIC(20,2) and also strings. A float never touches either.

Table Holds Schema guards
company_group name, presentation currency, basis (mfrs10) unique (id, org_id) for the grant FK
group_entity legal name, kind (company / individual), registration or id number, country, member_org_id? unique (group, member_org); an individual never links to an org
shareholding holder, subject, share class, shares, ownership %, voting %, period, source document 0–100 ranges; no self-holding; period order; no overlapping period for the same holder, subject and class (exclusion constraint); ownership of one subject and class never exceeds 100% on any day (trigger, with the lock the other register triggers use)
relationship_assessment a dated, approved assessment of one holder and subject: the legal outcome and the accounting outcomes below, with basis and rationale. Replaces the reference’s relationship_assessment rationale required; asserter and approver differ; one current row per holder, subject and test
beneficial_owner an individual, a subject, declared % and nature of control, SSM lodgement date period order; individual only

Relationships are derived, never stored. They are computed for a date from the shareholdings and the assertions by a read function. Storing a label would let it disagree with the shares. Malaysian practice (checked 2026-10-03) uses two frameworks that do not give the same answer, plus a registration duty. The register keeps them apart and never reduces any to ownership >= 50%. Each is a derived presumption that a relationship_assessment can confirm or override.

  1. Legal test (Companies Act 2016, section 4). A corporation is a holding company of another when it controls the composition of the board (can appoint or remove all or a majority of the directors), controls more than half of the voting power, or holds more than half of the issued shares (preference shares excluded for this test); indirect relationships count. Outcome: subsidiary or not_subsidiary. The inputs are ownership_percent, voting_percent and a board-control fact (board_control, board_appointment_rights) recorded on the holding or the assessment, since the board test cannot be derived from shares at all. Wording taken from a note the owner supplied; not read by me from the Act, so the adviser confirms it (below).

  2. Accounting control (MFRS 10). Control is judgment: power over the investee, exposure to variable returns, and the link between them. More than half the votes is the usual presumption, not the rule; control can exist below it (de facto control, board appointment rights) and not exist above it (protective or restricted voting). So the derived result is “presumed subsidiary” (above 50% voting, direct or through controlled entities). The authoritative record is the relationship_assessment, which needs a rationale and a second person’s approval, and which overrides the presumption either way. Outcome: controlled, not_controlled or requires_judgement.

  3. Significant influence (MFRS 128). Holding 20% or more of the voting power is presumed to give it, and under 20% presumed not to, each rebuttable. Derived as “presumed associate” for 20% up to the control test; an assessment overrides. Outcome: significant_influence, no_significant_influence or requires_judgement. A 20%-plus holding judged not influential, or under 20% judged influential, is a significant judgment that MFRS asks to be disclosed, so the register lists these overrides.

  4. Beneficial ownership (Companies Act 2016, section 60A, SSM guideline). A natural person with an interest in not less than 20% of the shares, or not less than 20% of the voting shares, directly or indirectly, or who exercises ultimate effective control. This is a registration duty, separate from group accounting. The register derives the candidates (individuals at or over 20% through any chain) so a missing declaration is visible, and keeps the declared beneficial_owner rows as the record.

Indirect holdings follow chains: ownership multiplies along a chain (60% of a company that holds 50% of another is 30% of that one), and voting passes through an entity only where it is controlled. The derivation is a read function with a depth limit and cycle detection (circular holdings are legal in some structures but never silently looped).

An assessment carries: holder, subject, test (legal, mfrs_control, mfrs_influence), outcome, assessment_basis, assessment_rationale, asserter, approved_by, approved_at and effective_from / effective_to. The legal outcome can differ from the accounting one (for example a subsidiary under the Act that is not consolidated, or the reverse), and the register shows both side by side.

Not verified by me in this session: the exact wording of Companies Act 2016 section 4 (taken from a note the owner pasted; my own search returned only its title) and the retention period for accounting records. Both go to the owner’s adviser before the derived labels are shown as anything stronger than “presumed”.

Changing a shareholding is a new dated row that ends the old one, not an edit, so the history a consolidation relied on can always be reproduced.

New keys, not borrowed from accounting (the reference’s second gap), all Owner-only by default:

  • group.register.read: read the register.
  • group.register.manage: create the group, entities, shareholdings and assertions.
  • group.register.sensitive: see and change beneficial-owner identity numbers (personal data, like hr.employee.sensitive).
  • group.grant.manage: on the member side, make and revoke a consent grant.

Each key gets a team-screen label in en, ms and zh, covered by the existing label drift test. Every route is behind httpx.Authorize; reads of beneficial owners are audited.

Endpoints are declared in api/openapi.yaml first, as everywhere. Every register write audits in the owner’s chain. Shareholding and beneficial-owner rows are retention-bearing: the application role is not granted the privilege that would let one be erased. A beneficial owner who ceases to be one is ended by date, not removed.

7. Drift tests this adds (each to be watched failing once)

Section titled “7. Drift tests this adds (each to be watched failing once)”
  • every register table is tenant-isolated (existing test, must see the new tables: assert a non-zero count of candidates first);
  • the grant FK refuses a grantee that is not the group’s owner (presence: it accepts the owner);
  • overlapping shareholdings and an over-100% subject are refused (presence: the adjacent non-overlapping row is accepted);
  • a SECURITY DEFINER function refuses without a grant, and answers with one;
  • a revoked grant stops the read;
  • the new migrations fall in a declared range (proposed: 920–939).
  1. Migration 00920: tables, constraints, RLS, retention grants; range declared.
  2. platform/group: register service, derived relationships, grant and check function.
  3. Permissions, labels, routes and openapi.yaml.
  4. Web: register screens (group, entities, shareholdings with a history view, beneficial owners) and the member-side consent screen.
  5. Drift tests above, then the full suite with -p 2.
  1. Who may own a group? Any organization that creates one (recommended), or only organizations flagged by an operator.
  2. May one organization be a member of more than one group? Recommended yes, with a separate grant per group, since the grant is per group.
  3. Beneficial-owner data in the ERP at all? It is personal data (PDPA). Recommended yes, behind group.register.sensitive with audited reads and the same masking as employee identity numbers; otherwise drop the table from stage one.
  4. Control thresholds. Recommended wording: use the Malaysian statutory and MFRS presumptions, not a bare 50% and 20%. Companies Act 2016 section 4 tests (board control, more than half the voting power, more than half the issued shares excluding preference); MFRS 10 control assessment; 20% of voting power as the MFRS 128 significant-influence presumption; beneficial ownership tracked separately at the 20% line of section 60A. Documented, approved overrides are allowed where the standard permits judgement. Confirm, and have the adviser confirm section 4.
  5. Revocation and history. If a member revokes consent, may past consolidation runs the owner already produced stay readable? Recommended yes (they are the owner’s own records), with no further reads of the member.