Source code for aeat.domain.transactions._classification_rule
"""Domain model for a ledger classification rule.A rule binds a regex description-pattern to a target:class:`~aeat.domain.transactions._enums.BusinessClassification`.Rule IDs are content-addressed (SHA-256 of the rule's key fields) socreation is idempotent: adding the same pattern + classification pairtwice produces the same rule_id and the repository overwrites the priorentry rather than duplicating it."""from__future__importannotationsimportrefromdatetimeimportdatetimefrompydanticimportBaseModel,Field,field_validatorfrom...coreimportSTRICT_FROZEN_CONFIGas_STRICT_FROZENfrom...core.hashingimportsha256_hexfrom...core.loggingimportget_loggerfrom...core.timeimportnowfrom._enumsimportBusinessClassificationfrom._errorsimportClassificationRuleError_logger=get_logger(__name__)_RULE_ID_LENGTH:int=64def_compute_rule_id(description_pattern:str,classification:BusinessClassification,category_id:str|None,)->str:"""Return the SHA-256 content-addressed rule id."""raw=f"{description_pattern}|{classification.value}|{category_idor''}"returnsha256_hex(raw.encode())
[docs]classLedgerClassificationRule(BaseModel):"""A persisted ledger classification rule. ``rule_id`` is content-addressed so that persisting the same pattern twice is idempotent. Priority resolution: lower ``priority`` integer wins; ties broken by ``created_at`` ascending (earliest rule wins). """model_config=_STRICT_FROZENrule_id:str=Field(min_length=_RULE_ID_LENGTH,max_length=_RULE_ID_LENGTH)description_pattern:str=Field(min_length=1)classification:BusinessClassificationcategory_id:str|None=Nonepriority:int=Field(default=100,ge=1)created_at:datetimeactor:str=Field(min_length=1)@field_validator("description_pattern")@classmethoddef_validate_regex(cls,value:str)->str:try:re.compile(value)exceptre.errorasexc:raiseClassificationRuleError(f"description_pattern is not a valid regex: {exc}")fromexcreturnvalue
[docs]@classmethoddefcreate(cls,*,description_pattern:str,classification:BusinessClassification,category_id:str|None=None,priority:int=100,actor:str,created_at:datetime|None=None,)->LedgerClassificationRule:"""Construct a :class:`LedgerClassificationRule` with the content-addressed ``rule_id``."""rule_id=_compute_rule_id(description_pattern,classification,category_id)returncls(rule_id=rule_id,description_pattern=description_pattern,classification=classification,category_id=category_id,priority=priority,created_at=created_atornow(),actor=actor,)
[docs]defmatches(self,description:str)->bool:"""Return ``True`` when this rule's pattern matches ``description`` (case-insensitive)."""returnbool(re.search(self.description_pattern,description,re.IGNORECASE))