dodil/erp-brick-consolidation

DODIL ERP brick: consolidation

src/brick_consolidation/domain/structure_change.py

"""The StructureChange aggregate — an acquisition, an ownership change or a method change, with what it computed.

The figures (net assets, goodwill, NCI, the rate) are computed in application/structure.py from gl's answers and stored here as they
were shown; approving writes the dated structure row. No database, no HTTP, no identity.
"""
from dataclasses import dataclass, field
from datetime import date
from decimal import Decimal
from typing import Optional

from brick_consolidation.domain.errors import RuleBroken
from brick_consolidation.domain.events import StructureChangeEffective, StructureChangeProposed, StructureChangeRejected
from brick_consolidation.domain.values import StructureChangeState


@dataclass
class StructureChange:
    """an acquisition, an ownership change, a divestiture or a method change, with what it computed"""
    change_id: str
    kind: str = ""
    company_code: str = ""
    investor_company_code: str = ""
    ownership_pct: Optional[Decimal] = None
    voting_pct: Optional[Decimal] = None
    method: str = ""
    currency: str = ""
    effective_date: Optional[date] = None
    consideration: Optional[Decimal] = None
    proceeds: Optional[Decimal] = None
    previously_held_fair_value: Optional[Decimal] = None
    net_assets_json: str = ""
    fair_value_adjustments_json: str = ""
    net_assets: Optional[Decimal] = None
    goodwill: Optional[Decimal] = None
    nci: Optional[Decimal] = None
    gain_loss: Optional[Decimal] = None
    rate: Optional[Decimal] = None
    goodwill_group: Optional[Decimal] = None
    nci_group: Optional[Decimal] = None
    bargain_purchase: str = ""
    proposed_by: str = ""
    proposed_sub: str = ""
    approved_by: str = ""
    approved_sub: str = ""
    status: str = StructureChangeState.PROPOSED.value
    figures_json: str = ""
    source: str = "gl"                           # v3: gl (default) | upload
    name: str = ""
    events: list = field(default_factory=list, repr=False, compare=False)

    def _record(self, event):
        self.events.append(event)
        return event

    def proposed(self, by: str = ""):
        self.status = StructureChangeState.PROPOSED.value
        return self._record(StructureChangeProposed(change_id=self.change_id, by=by))

    def approved(self, by: str = ""):
        self.status = StructureChangeState.EFFECTIVE.value
        return self._record(StructureChangeEffective(change_id=self.change_id, by=by))

    def reject_structure_change(self, reason: str, *, by: str = "") -> Optional[StructureChangeRejected]:
        """Reject the structure change: proposed -> rejected."""
        it = self
        if it.status == StructureChangeState.REJECTED:
            return None                 # already rejected: a repeat changes nothing
        if it.status not in (StructureChangeState.PROPOSED,):
            raise RuleBroken(f"a {it.status} structure change cannot move to rejected")
        if not reason:
            raise RuleBroken("reason is required")
        it.status = StructureChangeState.REJECTED.value
        return self._record(StructureChangeRejected(change_id=it.change_id, by=by))