"""Strict pydantic v2 records for the filing submission engine.Every type that crosses a public boundary is a strict+frozen:class:`pydantic.BaseModel` or a closed :class:`enum.StrEnum`.No dataclasses; no bare ``dict[str, Any]``.The records describe a local or imported filing audit trail. They do notauthorize a live AEAT write; live-write refusal stays with the core access gateand application facades compose these records into draft/import flows.See Also: :class:`aeat.domain.submission.SubmissionEngine` Runs preflight and reads these records from the repository. :func:`aeat.application.filing.import_filing_from_justificante` Builds a companion :class:`ModeloPresentado` when an offline justificante PDF is imported. :class:`aeat.domain.modelos.ModeloRecord` Work-unit filing record used by the modelo application facade. :mod:`aeat.application.live` Read-only live-capture surface that may attach AEAT evidence to existing local filing records."""from__future__importannotationsimporthashlibfromdatetimeimportdatetimefromenumimportStrEnumfrompathlibimportPathfrompydanticimportBaseModel,Field,model_validatorfrom...coreimportSTRICT_FROZEN_CONFIGas_STRICT_FROZENfrom...coreimportPeriodfrom...core.identityimportSubjectTaxIdfrom._errorsimportSubmissionValidationError
[docs]classSubmissionStatus(StrEnum):"""Lifecycle status of a :class:`ModeloPresentado`. Values are retained for historical records imported from AEAT, even though live AEAT submission is now permanently forbidden. Member names and values mirror the AEAT Sede labels per ADR A7.2. Attributes: PENDIENTE_DE_PRESENTAR: Filing recorded but no attempt has run. EN_TRAMITACION: An attempt is currently underway. PRESENTADA: Attempt completed; awaiting AEAT acknowledgement. ACEPTADA: AEAT issued a justificante CSV and PDF. RECHAZADA: AEAT explicitly rejected the filing. FALLIDA: Attempt could not complete (transport / browser). """PENDIENTE_DE_PRESENTAR="PENDIENTE_DE_PRESENTAR"EN_TRAMITACION="EN_TRAMITACION"PRESENTADA="PRESENTADA"ACEPTADA="ACEPTADA"RECHAZADA="RECHAZADA"FALLIDA="FALLIDA"
[docs]classSubmissionAttempt(BaseModel):"""A historical attempt record imported or retained locally. Attributes: attempt_id: Stable identifier for this attempt (``<submission_id>.<ordinal>``). started_at: UTC timestamp when the attempt began. ended_at: UTC timestamp when the attempt ended (success or failure). status: Terminal :class:`SubmissionStatus` for the attempt. error_code: Optional machine-readable error code. error_message: Optional multilingual error message. browser_trace_path: Optional path to a Playwright trace file written for this attempt. """model_config=_STRICT_FROZENattempt_id:str=Field(min_length=1)started_at:datetimeended_at:datetimestatus:SubmissionStatuserror_code:str|None=Noneerror_message:str|None=Nonebrowser_trace_path:Path|None=None@model_validator(mode="after")def_check_time_ordering(self)->SubmissionAttempt:"""Reject attempts whose ``ended_at`` predates ``started_at``."""ifself.ended_atandself.ended_at<self.started_at:raiseSubmissionValidationError(f"ended_at ({self.ended_at}) is before started_at ({self.started_at})")returnself
[docs]classModeloPresentado(BaseModel):"""The typed audit record for one historical filing. Attributes: submission_id: Stable SHA-256-derived hex digest of ``f"{draft_id}:{attempt_ordinal}"``. See :func:`make_submission_id`. draft_id: The upstream draft identifier. modelo: The AEAT modelo identifier. period: The :class:`~aeat.core.Period` covered, serialised as ``{"filing_year": int, "code": str}`` across the persistence boundary. profile_tax_id: The validated taxpayer identity value carried by the upstream draft or imported receipt. status: The overall :class:`SubmissionStatus` for the filing. justificante_csv: The AEAT-issued CSV, when present. justificante_pdf_path: Local path to the justificante PDF, when present. submitted_at: UTC timestamp of the first attempt start. acknowledged_at: UTC timestamp the user acknowledged the filing, when set. attempts: Non-empty tuple of :class:`SubmissionAttempt` records in chronological order. """model_config=_STRICT_FROZENsubmission_id:str=Field(min_length=1)draft_id:str=Field(min_length=1)modelo:str=Field(min_length=1)period:Periodprofile_tax_id:SubjectTaxId=Field(min_length=1)status:SubmissionStatusjustificante_csv:str|None=Nonejustificante_pdf_path:Path|None=Nonesubmitted_at:datetimeacknowledged_at:datetime|None=Noneattempts:tuple[SubmissionAttempt,...]=Field(min_length=1)@model_validator(mode="after")def_check_ack_consistency(self)->ModeloPresentado:"""Enforce ``ACEPTADA`` ↔ justificante-present invariants."""ifself.statusisSubmissionStatus.ACEPTADA:ifnotself.justificante_csvornotself.justificante_pdf_path:raiseSubmissionValidationError("status ACEPTADA requires both justificante_csv and justificante_pdf_path",)ifnotself.acknowledged_at:raiseSubmissionValidationError("status ACEPTADA requires acknowledged_at")ifself.acknowledged_atandself.acknowledged_at<self.submitted_at:raiseSubmissionValidationError(f"acknowledged_at ({self.acknowledged_at}) is before submitted_at ({self.submitted_at})",)returnself
[docs]defmake_submission_id(draft_id:str,attempt_ordinal:int)->str:"""Return a stable 16-hex-char SHA-256 prefix for a submission. The output is deterministic: identical ``(draft_id, attempt_ordinal)`` pairs always produce identical ids across runs and processes. Args: draft_id: The upstream draft identifier. attempt_ordinal: A strictly positive ordinal (``>= 1``) distinguishing multiple submission attempts against the same draft. Returns: A 16-character lowercase hex string. Raises: SubmissionValidationError: If ``draft_id`` is empty or ``attempt_ordinal`` is not a positive integer. """ifnotdraft_id:raiseSubmissionValidationError("draft_id must be non-empty")ifattempt_ordinal<1:raiseSubmissionValidationError(f"attempt_ordinal must be >= 1, got {attempt_ordinal}")payload=f"{draft_id}:{attempt_ordinal}".encode()returnhashlib.sha256(payload).hexdigest()[:16]