Source code for cadrumo.domain.censo.certificado

"""Typed record for the Certificado de Situación Censal (procedure G313).

The certificate is the operator-downloadable censal artefact the setup
flow's cotejo phase reconciles against. Its target shape is the SIX
officially certified fields (AEAT "¿Qué certifica?"): domicilio fiscal,
condición de residencia, NIF de los representantes, situación tributaria,
actividades y locales, and obligaciones periódicas. The shape is
primary-source grounded; the PDF layout extraction is deliberately
unpinned until a real issued-certificate specimen exists, so the inbound
adapter refuses loudly rather than guessing a layout.

``censo_facts_from_certificado`` projects the certificate onto candidate
:class:`~cadrumo.domain.user_profile.values.UserProfileFact` rows for the cotejo's
compare-select pages. Only axes with an unambiguous profile counterpart
produce a candidate fact; the rest are display-only certificate evidence:

* ``condicion_residencia`` — free-form certificate prose; mapping it onto
  the typed fiscal-residency enum needs the specimen's exact vocabulary.
* ``representantes_nif`` — the certificate certifies legal representatives,
  a DIFFERENT axis from the profile's IRNR ``representante_fiscal_nif``;
  auto-mapping would conflate the two.
* ``situacion_tributaria`` / ``obligaciones_periodicas`` — obligation
  surface, an input to the derived deadline schedule, not a static profile
  fact.

Every candidate fact carries the non-official artefact provenance token —
never an AEAT-verified stamp, so the calendar's ``censo.enrolment_unverified``
posture is unaffected.
"""

from __future__ import annotations

from pydantic import BaseModel, Field, field_validator

from ...core.errors.hierarchy import CadrumoError, pydantic_validation_boundary
from ...core.external_constants import PROVENANCE_SOURCE_CENSO_ARTEFACT
from ...core.models import STRICT_FROZEN_CONFIG
from ..calculations.registry.tax_id_runtime import validate_runtime_spanish_tax_id
from ..user_profile.values import UserProfileFact


[docs] class CertificadoCensalError(CadrumoError): """Base for every Certificado de Situación Censal failure."""
[docs] class CertificadoCensalParseError(CertificadoCensalError): """Raised when bytes cannot be parsed into a :class:`CertificadoSituacionCensal`."""
[docs] class ActividadLocalCertificada(BaseModel): """One certified actividad/local row on the certificate.""" model_config = STRICT_FROZEN_CONFIG descripcion: str = Field(min_length=1) epigrafe_iae: str = "" local: str = ""
[docs] class CertificadoSituacionCensal(BaseModel): """The six axes AEAT states a G313 censal certificate certifies. This is the model's scope, not an inventory of the document. AEAT's "¿Qué certifica?" enumerates what the certificate ATTESTS; whether the issued artefact also carries an issue date, a CSV or header furniture is unestablished, because no issued-certificate specimen exists in this tree. Read the absence of a temporal field as unmeasured, never as evidence the document has none -- and in particular do not derive an obligation start year from that absence. """ model_config = STRICT_FROZEN_CONFIG domicilio_fiscal: str = Field(min_length=1) condicion_residencia: str = Field(min_length=1) representantes_nif: tuple[str, ...] = () situacion_tributaria: tuple[str, ...] = () actividades: tuple[ActividadLocalCertificada, ...] = () obligaciones_periodicas: tuple[str, ...] = () @field_validator("representantes_nif") @classmethod @pydantic_validation_boundary def _validate_representantes(cls, value: tuple[str, ...]) -> tuple[str, ...]: """Every certified representative NIF validates through the core identity authority.""" return tuple(validate_runtime_spanish_tax_id(nif) for nif in value)
[docs] def censo_facts_from_certificado(certificado: CertificadoSituacionCensal) -> tuple[UserProfileFact, ...]: """Project the certificate onto candidate profile facts for the cotejo. Returns :class:`~cadrumo.domain.user_profile.values.UserProfileFact` rows stamped with the artefact provenance token. Axes without an unambiguous profile counterpart are deliberately absent (display-only evidence on the cotejo page); see the module docstring for the per-axis rationale. Every certified ``actividad`` is projected, not only the first. The ``activities`` section is schema-declared ``repeatable``, and :func:`~cadrumo.application.user_profile.completeness.profile_section_rows` already treats a repeatable fact written WITHOUT an index as the implicit first row -- the shape every other single-activity producer in this codebase writes -- so the primary activity keeps that unindexed address for compatibility, and every activity after it is projected at its own numeric index (``activities.1.description``, ``activities.2.description``, ...), which is the addressing scheme the manager and status surfaces already read repeatable rows through. A taxpayer with a second local or a second IAE epígrafe on the certificate used to lose it here silently: ``actividades[0]`` with no loop, no warning, and a cotejo that still reported success. """ facts: list[UserProfileFact] = [ UserProfileFact( path="contact.fiscal_address", value=certificado.domicilio_fiscal, source=PROVENANCE_SOURCE_CENSO_ARTEFACT, ), ] for index, actividad in enumerate(certificado.actividades): prefix = "activities" if index == 0 else f"activities.{index}" facts.append( UserProfileFact( path=f"{prefix}.description", value=actividad.descripcion, source=PROVENANCE_SOURCE_CENSO_ARTEFACT, ), ) if actividad.epigrafe_iae: facts.append( UserProfileFact( path=f"{prefix}.iae_epigraph", value=actividad.epigrafe_iae, source=PROVENANCE_SOURCE_CENSO_ARTEFACT, ), ) return tuple(facts)
__all__ = [ "ActividadLocalCertificada", "CertificadoCensalError", "CertificadoCensalParseError", "CertificadoSituacionCensal", "censo_facts_from_certificado", ]