# Generado desde el documento OpenAPI publico de NovaFE. No lo edites a mano: regeneralo con `bun run models`.
from __future__ import annotations

from dataclasses import dataclass
from decimal import Decimal
from typing import Any

from .common import _ser, _strip


@dataclass
class DgiiMessage:
    code: int
    value: str

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "code": _ser(self.code),
            "value": _ser(self.value),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "DgiiMessage":
        return cls(
            code=data["code"],
            value=data["value"],
        )


@dataclass
class DgiiTrackIdEntry:
    """Una entrada de `/{amb}/consultatrackids/api/trackids/consulta`. Puede haber más de una para el mismo e-NCF si se remitió varias veces."""

    track_id: str
    estado: str
    fecha_recepcion: str | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "trackId": _ser(self.track_id),
            "estado": _ser(self.estado),
            "fechaRecepcion": _ser(self.fecha_recepcion),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "DgiiTrackIdEntry":
        return cls(
            track_id=data["trackId"],
            estado=data["estado"],
            fecha_recepcion=(data["fechaRecepcion"] if data.get("fechaRecepcion") is not None else None),
        )


@dataclass
class EcfAdditionalInfoPayload:
    """Texto libre para la Representación Impresa."""

    issuer: str | None = None
    buyer: str | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "issuer": _ser(self.issuer),
            "buyer": _ser(self.buyer),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfAdditionalInfoPayload":
        return cls(
            issuer=(data["issuer"] if data.get("issuer") is not None else None),
            buyer=(data["buyer"] if data.get("buyer") is not None else None),
        )


@dataclass
class EcfAdditionalTaxPayload:
    """Desglose de impuestos adicionales por código de la Tabla I (001–039). Todos los montos los trae el cliente ya calculados. `iscEspecifico` (monto fijo por volumen: alcoholes 006-018, cigarrillos 019-022) integra la base imponible del ITBIS de esa línea; `iscAdvalorem` y `otros` (Propina, CDT, ISC de servicios, Primera Placa…) van "por encima", sin tocar la base."""

    code: str
    rate: Decimal | None = None
    isc_especifico: Decimal | None = None
    isc_advalorem: Decimal | None = None
    otros: Decimal | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "code": _ser(self.code),
            "rate": _ser(self.rate),
            "iscEspecifico": _ser(self.isc_especifico),
            "iscAdvalorem": _ser(self.isc_advalorem),
            "otros": _ser(self.otros),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfAdditionalTaxPayload":
        return cls(
            code=data["code"],
            rate=(Decimal(str(data["rate"])) if data.get("rate") is not None else None),
            isc_especifico=(Decimal(str(data["iscEspecifico"])) if data.get("iscEspecifico") is not None else None),
            isc_advalorem=(Decimal(str(data["iscAdvalorem"])) if data.get("iscAdvalorem") is not None else None),
            otros=(Decimal(str(data["otros"])) if data.get("otros") is not None else None),
        )


@dataclass
class EcfAttemptDto:
    """Un renglón del historial de envíos de un comprobante: cómo terminó un ciclo contra la DGII. Lo que emite `GET /api/v1/ecf/{id}/attempts`."""

    attempt: int
    """1, 2, 3… dentro del comprobante."""
    outcome: str
    """En qué estado dejó el ciclo al comprobante: `accepted`, `accepted_conditional`, `rejected`, `review` o `failed`."""
    messages: list[DgiiMessage]
    """Observaciones o motivo de rechazo de la DGII; o el motivo del fallo si no hubo veredicto."""
    xml_sha256: str
    """SHA-256 (hex) del XML enviado en este ciclo."""
    recorded_at: str
    """Cuándo se registró el resultado."""
    track_id: str | None = None
    """`TrackId` de la DGII de este ciclo; ausente si no llegó a haber uno."""
    status_code: int | None = None
    """Código de estado de la DGII (1 aceptado, 2 rechazado, 4 aceptado condicional); ausente si no hubo veredicto."""
    status_text: str | None = None
    """El `estado` textual de la DGII."""
    sequence_used: bool | None = None
    """`secuenciaUtilizada` de la DGII: `false` = el e-NCF no se consumió; `true` o ausente = consumido."""
    retry_decision: str | None = None
    """Qué se decidió con un rechazo: `auto:<código>` (la DGII falló de forma transitoria y NovaFE reenvía el mismo XML solo, p. ej. `auto:4`) o `exhausted` (era reintentable pero se agotó el ladder y el rechazo quedó firme). Ausente en el resto de resultados."""

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "attempt": _ser(self.attempt),
            "outcome": _ser(self.outcome),
            "trackId": _ser(self.track_id),
            "statusCode": _ser(self.status_code),
            "statusText": _ser(self.status_text),
            "sequenceUsed": _ser(self.sequence_used),
            "messages": _ser(self.messages),
            "retryDecision": _ser(self.retry_decision),
            "xmlSha256": _ser(self.xml_sha256),
            "recordedAt": _ser(self.recorded_at),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfAttemptDto":
        return cls(
            attempt=data["attempt"],
            outcome=data["outcome"],
            track_id=(data["trackId"] if data.get("trackId") is not None else None),
            status_code=(data["statusCode"] if data.get("statusCode") is not None else None),
            status_text=(data["statusText"] if data.get("statusText") is not None else None),
            sequence_used=(data["sequenceUsed"] if data.get("sequenceUsed") is not None else None),
            messages=[DgiiMessage.from_dict(i) for i in data["messages"]],
            retry_decision=(data["retryDecision"] if data.get("retryDecision") is not None else None),
            xml_sha256=data["xmlSha256"],
            recorded_at=data["recordedAt"],
        )


@dataclass
class EcfBuyerPayload:
    """Bloque `<Comprador>`. `Rnc` y `ForeignId` son excluyentes."""

    name: str | None = None
    rnc: str | None = None
    foreign_id: str | None = None
    email: str | None = None
    contact: str | None = None
    address: str | None = None
    municipality: str | None = None
    province: str | None = None
    additional_info: str | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "name": _ser(self.name),
            "rnc": _ser(self.rnc),
            "foreignId": _ser(self.foreign_id),
            "email": _ser(self.email),
            "contact": _ser(self.contact),
            "address": _ser(self.address),
            "municipality": _ser(self.municipality),
            "province": _ser(self.province),
            "additionalInfo": _ser(self.additional_info),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfBuyerPayload":
        return cls(
            name=(data["name"] if data.get("name") is not None else None),
            rnc=(data["rnc"] if data.get("rnc") is not None else None),
            foreign_id=(data["foreignId"] if data.get("foreignId") is not None else None),
            email=(data["email"] if data.get("email") is not None else None),
            contact=(data["contact"] if data.get("contact") is not None else None),
            address=(data["address"] if data.get("address") is not None else None),
            municipality=(data["municipality"] if data.get("municipality") is not None else None),
            province=(data["province"] if data.get("province") is not None else None),
            additional_info=(data["additionalInfo"] if data.get("additionalInfo") is not None else None),
        )


@dataclass
class EcfDeclaredTotalsPayload:
    """Totales que calculó el cliente (en DOP). Se comparan con el cálculo de NovaFE (tolerancia RF-06.6); el valor de NovaFE es el que va al XML. Todo opcional."""

    monto_gravado_total: Decimal | None = None
    monto_exento: Decimal | None = None
    total_itbis: Decimal | None = None
    monto_impuesto_adicional: Decimal | None = None
    monto_total: Decimal | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "montoGravadoTotal": _ser(self.monto_gravado_total),
            "montoExento": _ser(self.monto_exento),
            "totalItbis": _ser(self.total_itbis),
            "montoImpuestoAdicional": _ser(self.monto_impuesto_adicional),
            "montoTotal": _ser(self.monto_total),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfDeclaredTotalsPayload":
        return cls(
            monto_gravado_total=(Decimal(str(data["montoGravadoTotal"])) if data.get("montoGravadoTotal") is not None else None),
            monto_exento=(Decimal(str(data["montoExento"])) if data.get("montoExento") is not None else None),
            total_itbis=(Decimal(str(data["totalItbis"])) if data.get("totalItbis") is not None else None),
            monto_impuesto_adicional=(Decimal(str(data["montoImpuestoAdicional"])) if data.get("montoImpuestoAdicional") is not None else None),
            monto_total=(Decimal(str(data["montoTotal"])) if data.get("montoTotal") is not None else None),
        )


@dataclass
class EcfDgiiExchange:
    """El intercambio con la DGII de un comprobante: lo que ella respondió más los instantes en que NovaFE registró cada paso."""

    messages: list[DgiiMessage]
    """Observaciones o motivo de rechazo que devolvió la DGII."""
    track_id: str | None = None
    """Identificador que la DGII asigna al recibir el comprobante."""
    status: str | None = None
    """El `estado` textual de la DGII ("Aceptado", "Rechazado", "En Proceso"…); en el idioma en que lo manda ella."""
    status_code: int | None = None
    """Código de estado de la DGII: 1 aceptado · 2 rechazado · 3 en proceso · 4 aceptado condicional."""
    sequence_used: bool | None = None
    """`secuenciaUtilizada` de la DGII: `false` = el e-NCF no se consumió (firma/XML inválidos); `true` o ausente = consumido."""
    submitted_at: str | None = None
    """Instante en que la DGII confirmó la recepción (hay string? EcfDgiiExchange.TrackId)."""
    received_at: str | None = None
    """La `fechaRecepcion` que informó la DGII; ausente si no la dio (p. ej. RFCE)."""
    processed_at: str | None = None
    """Instante en que la DGII dio un resultado definitivo."""

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "trackId": _ser(self.track_id),
            "status": _ser(self.status),
            "statusCode": _ser(self.status_code),
            "sequenceUsed": _ser(self.sequence_used),
            "messages": _ser(self.messages),
            "submittedAt": _ser(self.submitted_at),
            "receivedAt": _ser(self.received_at),
            "processedAt": _ser(self.processed_at),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfDgiiExchange":
        return cls(
            track_id=(data["trackId"] if data.get("trackId") is not None else None),
            status=(data["status"] if data.get("status") is not None else None),
            status_code=(data["statusCode"] if data.get("statusCode") is not None else None),
            sequence_used=(data["sequenceUsed"] if data.get("sequenceUsed") is not None else None),
            messages=[DgiiMessage.from_dict(i) for i in data["messages"]],
            submitted_at=(data["submittedAt"] if data.get("submittedAt") is not None else None),
            received_at=(data["receivedAt"] if data.get("receivedAt") is not None else None),
            processed_at=(data["processedAt"] if data.get("processedAt") is not None else None),
        )


@dataclass
class EcfDto:
    """Vista de un comprobante emitido (respuesta de `POST /ecf` y `GET /ecf/{id}`). Trae la identidad fiscal, el resumen comercial (comprador, monto total — igual que EcfSummaryDto) y el resultado del intercambio con la DGII (EcfDgiiExchange? EcfDto.Dgii). El desglose por línea y el resto del detalle comercial siguen viviendo solo en el XML firmado (`GET /ecf/{id}/xml`)."""

    id: str
    status: str
    encf: str
    type: int
    environment: str
    issue_date: str
    issued_at: str
    signed_at: str
    security_code: str
    qr_url: str
    submits_rfce: bool
    signed_during_contingency: bool
    monto_total: Decimal
    document_hash: str
    sequence_expires_on: str | None = None
    internal_number: str | None = None
    tolerance_warning: str | None = None
    buyer_rnc: str | None = None
    buyer_name: str | None = None
    dgii: EcfDgiiExchange | None = None
    """Lo que pasó con la DGII: `trackId`, estado (texto y código), mensajes, `secuenciaUtilizada` y los instantes de envío, recepción y resolución. `null` mientras el comprobante no se haya enviado."""
    retry: EcfRetry | None = None
    """El reintento automático en curso o ya hecho: cuántas veces NovaFE reenvió el mismo comprobante tras un rechazo transitorio de la DGII, el tope, y cuándo es el próximo intento. `null` mientras no haya habido ningún reintento."""
    links: EcfLinks | None = None
    """Enlaces a los recursos relacionados del comprobante."""

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "id": _ser(self.id),
            "status": _ser(self.status),
            "encf": _ser(self.encf),
            "type": _ser(self.type),
            "environment": _ser(self.environment),
            "sequenceExpiresOn": _ser(self.sequence_expires_on),
            "issueDate": _ser(self.issue_date),
            "issuedAt": _ser(self.issued_at),
            "signedAt": _ser(self.signed_at),
            "securityCode": _ser(self.security_code),
            "qrUrl": _ser(self.qr_url),
            "submitsRfce": _ser(self.submits_rfce),
            "internalNumber": _ser(self.internal_number),
            "toleranceWarning": _ser(self.tolerance_warning),
            "signedDuringContingency": _ser(self.signed_during_contingency),
            "montoTotal": _ser(self.monto_total),
            "buyerRnc": _ser(self.buyer_rnc),
            "buyerName": _ser(self.buyer_name),
            "documentHash": _ser(self.document_hash),
            "dgii": _ser(self.dgii),
            "retry": _ser(self.retry),
            "links": _ser(self.links),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfDto":
        return cls(
            id=data["id"],
            status=data["status"],
            encf=data["encf"],
            type=data["type"],
            environment=data["environment"],
            sequence_expires_on=(data["sequenceExpiresOn"] if data.get("sequenceExpiresOn") is not None else None),
            issue_date=data["issueDate"],
            issued_at=data["issuedAt"],
            signed_at=data["signedAt"],
            security_code=data["securityCode"],
            qr_url=data["qrUrl"],
            submits_rfce=data["submitsRfce"],
            internal_number=(data["internalNumber"] if data.get("internalNumber") is not None else None),
            tolerance_warning=(data["toleranceWarning"] if data.get("toleranceWarning") is not None else None),
            signed_during_contingency=data["signedDuringContingency"],
            monto_total=Decimal(str(data["montoTotal"])),
            buyer_rnc=(data["buyerRnc"] if data.get("buyerRnc") is not None else None),
            buyer_name=(data["buyerName"] if data.get("buyerName") is not None else None),
            document_hash=data["documentHash"],
            dgii=(EcfDgiiExchange.from_dict(data["dgii"]) if data.get("dgii") is not None else None),
            retry=(EcfRetry.from_dict(data["retry"]) if data.get("retry") is not None else None),
            links=(EcfLinks.from_dict(data["links"]) if data.get("links") is not None else None),
        )


@dataclass
class EcfExportPayload:
    """Datos de exportación (solo tipo 46)."""

    loading_port_name: str | None = None
    delivery_terms: str | None = None
    total_fob: Decimal | None = None
    insurance: Decimal | None = None
    freight: Decimal | None = None
    other_charges: Decimal | None = None
    total_cif: Decimal | None = None
    customs_regime: str | None = None
    departure_port_name: str | None = None
    unloading_port_name: str | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "loadingPortName": _ser(self.loading_port_name),
            "deliveryTerms": _ser(self.delivery_terms),
            "totalFob": _ser(self.total_fob),
            "insurance": _ser(self.insurance),
            "freight": _ser(self.freight),
            "otherCharges": _ser(self.other_charges),
            "totalCif": _ser(self.total_cif),
            "customsRegime": _ser(self.customs_regime),
            "departurePortName": _ser(self.departure_port_name),
            "unloadingPortName": _ser(self.unloading_port_name),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfExportPayload":
        return cls(
            loading_port_name=(data["loadingPortName"] if data.get("loadingPortName") is not None else None),
            delivery_terms=(data["deliveryTerms"] if data.get("deliveryTerms") is not None else None),
            total_fob=(Decimal(str(data["totalFob"])) if data.get("totalFob") is not None else None),
            insurance=(Decimal(str(data["insurance"])) if data.get("insurance") is not None else None),
            freight=(Decimal(str(data["freight"])) if data.get("freight") is not None else None),
            other_charges=(Decimal(str(data["otherCharges"])) if data.get("otherCharges") is not None else None),
            total_cif=(Decimal(str(data["totalCif"])) if data.get("totalCif") is not None else None),
            customs_regime=(data["customsRegime"] if data.get("customsRegime") is not None else None),
            departure_port_name=(data["departurePortName"] if data.get("departurePortName") is not None else None),
            unloading_port_name=(data["unloadingPortName"] if data.get("unloadingPortName") is not None else None),
        )


@dataclass
class EcfForeignCurrencyPayload:
    """Facturación en divisa (`<OtraMoneda>`). El cliente trae los montos ya convertidos."""

    currency: str
    exchange_rate: Decimal
    totals: EcfForeignCurrencyTotalsPayload

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "currency": _ser(self.currency),
            "exchangeRate": _ser(self.exchange_rate),
            "totals": _ser(self.totals),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfForeignCurrencyPayload":
        return cls(
            currency=data["currency"],
            exchange_rate=Decimal(str(data["exchangeRate"])),
            totals=EcfForeignCurrencyTotalsPayload.from_dict(data["totals"]),
        )


@dataclass
class EcfForeignCurrencyTotalsPayload:
    monto_gravado_total: Decimal | None = None
    monto_gravado_i1: Decimal | None = None
    monto_gravado_i2: Decimal | None = None
    monto_gravado_i3: Decimal | None = None
    monto_exento: Decimal | None = None
    total_itbis: Decimal | None = None
    total_itbis1: Decimal | None = None
    total_itbis2: Decimal | None = None
    total_itbis3: Decimal | None = None
    monto_total: Decimal | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "montoGravadoTotal": _ser(self.monto_gravado_total),
            "montoGravadoI1": _ser(self.monto_gravado_i1),
            "montoGravadoI2": _ser(self.monto_gravado_i2),
            "montoGravadoI3": _ser(self.monto_gravado_i3),
            "montoExento": _ser(self.monto_exento),
            "totalItbis": _ser(self.total_itbis),
            "totalItbis1": _ser(self.total_itbis1),
            "totalItbis2": _ser(self.total_itbis2),
            "totalItbis3": _ser(self.total_itbis3),
            "montoTotal": _ser(self.monto_total),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfForeignCurrencyTotalsPayload":
        return cls(
            monto_gravado_total=(Decimal(str(data["montoGravadoTotal"])) if data.get("montoGravadoTotal") is not None else None),
            monto_gravado_i1=(Decimal(str(data["montoGravadoI1"])) if data.get("montoGravadoI1") is not None else None),
            monto_gravado_i2=(Decimal(str(data["montoGravadoI2"])) if data.get("montoGravadoI2") is not None else None),
            monto_gravado_i3=(Decimal(str(data["montoGravadoI3"])) if data.get("montoGravadoI3") is not None else None),
            monto_exento=(Decimal(str(data["montoExento"])) if data.get("montoExento") is not None else None),
            total_itbis=(Decimal(str(data["totalItbis"])) if data.get("totalItbis") is not None else None),
            total_itbis1=(Decimal(str(data["totalItbis1"])) if data.get("totalItbis1") is not None else None),
            total_itbis2=(Decimal(str(data["totalItbis2"])) if data.get("totalItbis2") is not None else None),
            total_itbis3=(Decimal(str(data["totalItbis3"])) if data.get("totalItbis3") is not None else None),
            monto_total=(Decimal(str(data["montoTotal"])) if data.get("montoTotal") is not None else None),
        )


@dataclass
class EcfGlobalAdjustmentPayload:
    """Un descuento o recargo global (Sección D)."""

    line: int
    kind: str | None = None
    affects_itbis_rate: int | None = None
    amount: Decimal | None = None
    norm1007: bool | None = None
    description: str | None = None
    percentage: Decimal | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "line": _ser(self.line),
            "kind": _ser(self.kind),
            "affectsItbisRate": _ser(self.affects_itbis_rate),
            "amount": _ser(self.amount),
            "norm1007": _ser(self.norm1007),
            "description": _ser(self.description),
            "percentage": _ser(self.percentage),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfGlobalAdjustmentPayload":
        return cls(
            line=data["line"],
            kind=(data["kind"] if data.get("kind") is not None else None),
            affects_itbis_rate=(data["affectsItbisRate"] if data.get("affectsItbisRate") is not None else None),
            amount=(Decimal(str(data["amount"])) if data.get("amount") is not None else None),
            norm1007=(data["norm1007"] if data.get("norm1007") is not None else None),
            description=(data["description"] if data.get("description") is not None else None),
            percentage=(Decimal(str(data["percentage"])) if data.get("percentage") is not None else None),
        )


@dataclass
class EcfItemCodePayload:
    type: str
    value: str

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "type": _ser(self.type),
            "value": _ser(self.value),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfItemCodePayload":
        return cls(
            type=data["type"],
            value=data["value"],
        )


@dataclass
class EcfLineDetailsPayload:
    """Campos opcionales del `<Item>` — passthrough. La derivación del ISC desde `alcoholDegrees`/`referenceQuantity` es un slice posterior; hoy el cliente trae el monto final del ISC en `additionalTaxes` (el ISC específico ahí sí integra la base del ITBIS — ver EcfAdditionalTaxPayload)."""

    reference_quantity: Decimal | None = None
    reference_unit: str | None = None
    subquantities: list[EcfSubquantityPayload] | None = None
    alcohol_degrees: Decimal | None = None
    reference_unit_price: Decimal | None = None
    manufacture_date: str | None = None
    expiry_date: str | None = None
    mining: EcfMiningPayload | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "referenceQuantity": _ser(self.reference_quantity),
            "referenceUnit": _ser(self.reference_unit),
            "subquantities": _ser(self.subquantities),
            "alcoholDegrees": _ser(self.alcohol_degrees),
            "referenceUnitPrice": _ser(self.reference_unit_price),
            "manufactureDate": _ser(self.manufacture_date),
            "expiryDate": _ser(self.expiry_date),
            "mining": _ser(self.mining),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfLineDetailsPayload":
        return cls(
            reference_quantity=(Decimal(str(data["referenceQuantity"])) if data.get("referenceQuantity") is not None else None),
            reference_unit=(data["referenceUnit"] if data.get("referenceUnit") is not None else None),
            subquantities=([EcfSubquantityPayload.from_dict(i) for i in data["subquantities"]] if data.get("subquantities") is not None else None),
            alcohol_degrees=(Decimal(str(data["alcoholDegrees"])) if data.get("alcoholDegrees") is not None else None),
            reference_unit_price=(Decimal(str(data["referenceUnitPrice"])) if data.get("referenceUnitPrice") is not None else None),
            manufacture_date=(data["manufactureDate"] if data.get("manufactureDate") is not None else None),
            expiry_date=(data["expiryDate"] if data.get("expiryDate") is not None else None),
            mining=(EcfMiningPayload.from_dict(data["mining"]) if data.get("mining") is not None else None),
        )


@dataclass
class EcfLineForeignCurrencyPayload:
    """`<OtraMonedaDetalle>` — precio y montos de la línea en divisa (passthrough)."""

    unit_price: Decimal | None = None
    discount: Decimal | None = None
    surcharge: Decimal | None = None
    line_amount: Decimal | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "unitPrice": _ser(self.unit_price),
            "discount": _ser(self.discount),
            "surcharge": _ser(self.surcharge),
            "lineAmount": _ser(self.line_amount),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfLineForeignCurrencyPayload":
        return cls(
            unit_price=(Decimal(str(data["unitPrice"])) if data.get("unitPrice") is not None else None),
            discount=(Decimal(str(data["discount"])) if data.get("discount") is not None else None),
            surcharge=(Decimal(str(data["surcharge"])) if data.get("surcharge") is not None else None),
            line_amount=(Decimal(str(data["lineAmount"])) if data.get("lineAmount") is not None else None),
        )


@dataclass
class EcfLinePayload:
    """Una línea de `<DetallesItems>`."""

    name: str | None = None
    kind: str | None = None
    quantity: Decimal | None = None
    unit_price: Decimal | None = None
    itbis_rate: int | None = None
    unit_of_measure: str | None = None
    description: str | None = None
    discount: Decimal | None = None
    surcharge: Decimal | None = None
    price_includes_tax: bool | None = None
    declared_amount: Decimal | None = None
    codes: list[EcfItemCodePayload] | None = None
    retention: EcfLineRetentionPayload | None = None
    additional_taxes: list[EcfAdditionalTaxPayload] | None = None
    foreign_currency: EcfLineForeignCurrencyPayload | None = None
    details: EcfLineDetailsPayload | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "name": _ser(self.name),
            "kind": _ser(self.kind),
            "quantity": _ser(self.quantity),
            "unitPrice": _ser(self.unit_price),
            "itbisRate": _ser(self.itbis_rate),
            "unitOfMeasure": _ser(self.unit_of_measure),
            "description": _ser(self.description),
            "discount": _ser(self.discount),
            "surcharge": _ser(self.surcharge),
            "priceIncludesTax": _ser(self.price_includes_tax),
            "declaredAmount": _ser(self.declared_amount),
            "codes": _ser(self.codes),
            "retention": _ser(self.retention),
            "additionalTaxes": _ser(self.additional_taxes),
            "foreignCurrency": _ser(self.foreign_currency),
            "details": _ser(self.details),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfLinePayload":
        return cls(
            name=(data["name"] if data.get("name") is not None else None),
            kind=(data["kind"] if data.get("kind") is not None else None),
            quantity=(Decimal(str(data["quantity"])) if data.get("quantity") is not None else None),
            unit_price=(Decimal(str(data["unitPrice"])) if data.get("unitPrice") is not None else None),
            itbis_rate=(data["itbisRate"] if data.get("itbisRate") is not None else None),
            unit_of_measure=(data["unitOfMeasure"] if data.get("unitOfMeasure") is not None else None),
            description=(data["description"] if data.get("description") is not None else None),
            discount=(Decimal(str(data["discount"])) if data.get("discount") is not None else None),
            surcharge=(Decimal(str(data["surcharge"])) if data.get("surcharge") is not None else None),
            price_includes_tax=(data["priceIncludesTax"] if data.get("priceIncludesTax") is not None else None),
            declared_amount=(Decimal(str(data["declaredAmount"])) if data.get("declaredAmount") is not None else None),
            codes=([EcfItemCodePayload.from_dict(i) for i in data["codes"]] if data.get("codes") is not None else None),
            retention=(EcfLineRetentionPayload.from_dict(data["retention"]) if data.get("retention") is not None else None),
            additional_taxes=([EcfAdditionalTaxPayload.from_dict(i) for i in data["additionalTaxes"]] if data.get("additionalTaxes") is not None else None),
            foreign_currency=(EcfLineForeignCurrencyPayload.from_dict(data["foreignCurrency"]) if data.get("foreignCurrency") is not None else None),
            details=(EcfLineDetailsPayload.from_dict(data["details"]) if data.get("details") is not None else None),
        )


@dataclass
class EcfLineRetentionPayload:
    """Área `<Retencion>` de la línea (tipos 41 y 47). Los montos los calcula el cliente."""

    agent: str | None = None
    itbis_withheld: Decimal | None = None
    isr_withheld: Decimal | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "agent": _ser(self.agent),
            "itbisWithheld": _ser(self.itbis_withheld),
            "isrWithheld": _ser(self.isr_withheld),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfLineRetentionPayload":
        return cls(
            agent=(data["agent"] if data.get("agent") is not None else None),
            itbis_withheld=(Decimal(str(data["itbisWithheld"])) if data.get("itbisWithheld") is not None else None),
            isr_withheld=(Decimal(str(data["isrWithheld"])) if data.get("isrWithheld") is not None else None),
        )


@dataclass
class EcfLinks:
    """Enlaces (relativos) a los recursos del comprobante."""

    self: str
    """El comprobante y su estado."""
    xml: str
    """El XML firmado (`<ECF>`)."""
    representation: str
    """La Representación Impresa en PDF."""
    rfce_xml: str | None = None
    """El resumen firmado (`<RFCE>`); solo cuando `submitsRfce`."""

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "self": _ser(self.self),
            "xml": _ser(self.xml),
            "rfceXml": _ser(self.rfce_xml),
            "representation": _ser(self.representation),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfLinks":
        return cls(
            self=data["self"],
            xml=data["xml"],
            rfce_xml=(data["rfceXml"] if data.get("rfceXml") is not None else None),
            representation=data["representation"],
        )


@dataclass
class EcfMiningPayload:
    """`<Mineria>` — datos de liquidación minera (solo tipos 32/33/34/46)."""

    net_weight_kilogram: Decimal | None = None
    net_weight_mining: Decimal | None = None
    affiliation_type: int | None = None
    settlement: int | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "netWeightKilogram": _ser(self.net_weight_kilogram),
            "netWeightMining": _ser(self.net_weight_mining),
            "affiliationType": _ser(self.affiliation_type),
            "settlement": _ser(self.settlement),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfMiningPayload":
        return cls(
            net_weight_kilogram=(Decimal(str(data["netWeightKilogram"])) if data.get("netWeightKilogram") is not None else None),
            net_weight_mining=(Decimal(str(data["netWeightMining"])) if data.get("netWeightMining") is not None else None),
            affiliation_type=(data["affiliationType"] if data.get("affiliationType") is not None else None),
            settlement=(data["settlement"] if data.get("settlement") is not None else None),
        )


@dataclass
class EcfPagePayload:
    """Página de la RI (`<Paginacion>`)."""

    number: int | None = None
    line_from: int | None = None
    line_to: int | None = None
    monto_gravado_total: Decimal | None = None
    monto_gravado_i1: Decimal | None = None
    monto_gravado_i2: Decimal | None = None
    monto_gravado_i3: Decimal | None = None
    monto_exento: Decimal | None = None
    total_itbis: Decimal | None = None
    itbis1: Decimal | None = None
    itbis2: Decimal | None = None
    itbis3: Decimal | None = None
    monto_impuesto_adicional: Decimal | None = None
    isc_especifico: Decimal | None = None
    otros_impuestos: Decimal | None = None
    amount: Decimal | None = None
    non_invoiceable_amount: Decimal | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "number": _ser(self.number),
            "lineFrom": _ser(self.line_from),
            "lineTo": _ser(self.line_to),
            "montoGravadoTotal": _ser(self.monto_gravado_total),
            "montoGravadoI1": _ser(self.monto_gravado_i1),
            "montoGravadoI2": _ser(self.monto_gravado_i2),
            "montoGravadoI3": _ser(self.monto_gravado_i3),
            "montoExento": _ser(self.monto_exento),
            "totalItbis": _ser(self.total_itbis),
            "itbis1": _ser(self.itbis1),
            "itbis2": _ser(self.itbis2),
            "itbis3": _ser(self.itbis3),
            "montoImpuestoAdicional": _ser(self.monto_impuesto_adicional),
            "iscEspecifico": _ser(self.isc_especifico),
            "otrosImpuestos": _ser(self.otros_impuestos),
            "amount": _ser(self.amount),
            "nonInvoiceableAmount": _ser(self.non_invoiceable_amount),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfPagePayload":
        return cls(
            number=(data["number"] if data.get("number") is not None else None),
            line_from=(data["lineFrom"] if data.get("lineFrom") is not None else None),
            line_to=(data["lineTo"] if data.get("lineTo") is not None else None),
            monto_gravado_total=(Decimal(str(data["montoGravadoTotal"])) if data.get("montoGravadoTotal") is not None else None),
            monto_gravado_i1=(Decimal(str(data["montoGravadoI1"])) if data.get("montoGravadoI1") is not None else None),
            monto_gravado_i2=(Decimal(str(data["montoGravadoI2"])) if data.get("montoGravadoI2") is not None else None),
            monto_gravado_i3=(Decimal(str(data["montoGravadoI3"])) if data.get("montoGravadoI3") is not None else None),
            monto_exento=(Decimal(str(data["montoExento"])) if data.get("montoExento") is not None else None),
            total_itbis=(Decimal(str(data["totalItbis"])) if data.get("totalItbis") is not None else None),
            itbis1=(Decimal(str(data["itbis1"])) if data.get("itbis1") is not None else None),
            itbis2=(Decimal(str(data["itbis2"])) if data.get("itbis2") is not None else None),
            itbis3=(Decimal(str(data["itbis3"])) if data.get("itbis3") is not None else None),
            monto_impuesto_adicional=(Decimal(str(data["montoImpuestoAdicional"])) if data.get("montoImpuestoAdicional") is not None else None),
            isc_especifico=(Decimal(str(data["iscEspecifico"])) if data.get("iscEspecifico") is not None else None),
            otros_impuestos=(Decimal(str(data["otrosImpuestos"])) if data.get("otrosImpuestos") is not None else None),
            amount=(Decimal(str(data["amount"])) if data.get("amount") is not None else None),
            non_invoiceable_amount=(Decimal(str(data["nonInvoiceableAmount"])) if data.get("nonInvoiceableAmount") is not None else None),
        )


@dataclass
class EcfPaymentMethodPayload:
    """Una forma de pago. `Type` y `Amount` son nullable para distinguir "no vino" de un valor: el validador exige ambos y la API nunca rellena uno por su cuenta."""

    type: str | None = None
    amount: Decimal | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "type": _ser(self.type),
            "amount": _ser(self.amount),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfPaymentMethodPayload":
        return cls(
            type=(data["type"] if data.get("type") is not None else None),
            amount=(Decimal(str(data["amount"])) if data.get("amount") is not None else None),
        )


@dataclass
class EcfPaymentPayload:
    """Bloque de pago del encabezado."""

    condition: str | None = None
    due_date: str | None = None
    methods: list[EcfPaymentMethodPayload] | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "condition": _ser(self.condition),
            "dueDate": _ser(self.due_date),
            "methods": _ser(self.methods),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfPaymentPayload":
        return cls(
            condition=(data["condition"] if data.get("condition") is not None else None),
            due_date=(data["dueDate"] if data.get("dueDate") is not None else None),
            methods=([EcfPaymentMethodPayload.from_dict(i) for i in data["methods"]] if data.get("methods") is not None else None),
        )


@dataclass
class EcfReferencePayload:
    """Sección `<InformacionReferencia>` — Notas de Crédito/Débito y reemplazos."""

    modified_ncf: str
    modified_ncf_date: str
    modification_code: str | None = None
    other_issuer_rnc: str | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "modifiedNcf": _ser(self.modified_ncf),
            "modifiedNcfDate": _ser(self.modified_ncf_date),
            "modificationCode": _ser(self.modification_code),
            "otherIssuerRnc": _ser(self.other_issuer_rnc),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfReferencePayload":
        return cls(
            modified_ncf=data["modifiedNcf"],
            modified_ncf_date=data["modifiedNcfDate"],
            modification_code=(data["modificationCode"] if data.get("modificationCode") is not None else None),
            other_issuer_rnc=(data["otherIssuerRnc"] if data.get("otherIssuerRnc") is not None else None),
        )


@dataclass
class EcfRetry:
    """Reintento automático de un comprobante tras un rechazo transitorio de la DGII."""

    count: int
    """Reenvíos automáticos que NovaFE ya hizo de este comprobante."""
    max: int
    """Tope de reenvíos automáticos. Al agotarse, el rechazo queda firme."""
    next_attempt_at: str | None = None
    """Cuándo es el próximo intento; `null` si no hay uno pendiente."""

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "count": _ser(self.count),
            "max": _ser(self.max),
            "nextAttemptAt": _ser(self.next_attempt_at),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfRetry":
        return cls(
            count=data["count"],
            max=data["max"],
            next_attempt_at=(data["nextAttemptAt"] if data.get("nextAttemptAt") is not None else None),
        )


@dataclass
class EcfShippingPayload:
    """Datos de embarque (`<InformacionesAdicionales>`). Passthrough."""

    shipment_date: str | None = None
    shipment_number: str | None = None
    container_number: str | None = None
    reference_number: str | None = None
    gross_weight: Decimal | None = None
    net_weight: Decimal | None = None
    gross_weight_unit: str | None = None
    net_weight_unit: str | None = None
    package_count: Decimal | None = None
    package_unit: str | None = None
    volume: Decimal | None = None
    volume_unit: str | None = None
    export: EcfExportPayload | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "shipmentDate": _ser(self.shipment_date),
            "shipmentNumber": _ser(self.shipment_number),
            "containerNumber": _ser(self.container_number),
            "referenceNumber": _ser(self.reference_number),
            "grossWeight": _ser(self.gross_weight),
            "netWeight": _ser(self.net_weight),
            "grossWeightUnit": _ser(self.gross_weight_unit),
            "netWeightUnit": _ser(self.net_weight_unit),
            "packageCount": _ser(self.package_count),
            "packageUnit": _ser(self.package_unit),
            "volume": _ser(self.volume),
            "volumeUnit": _ser(self.volume_unit),
            "export": _ser(self.export),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfShippingPayload":
        return cls(
            shipment_date=(data["shipmentDate"] if data.get("shipmentDate") is not None else None),
            shipment_number=(data["shipmentNumber"] if data.get("shipmentNumber") is not None else None),
            container_number=(data["containerNumber"] if data.get("containerNumber") is not None else None),
            reference_number=(data["referenceNumber"] if data.get("referenceNumber") is not None else None),
            gross_weight=(Decimal(str(data["grossWeight"])) if data.get("grossWeight") is not None else None),
            net_weight=(Decimal(str(data["netWeight"])) if data.get("netWeight") is not None else None),
            gross_weight_unit=(data["grossWeightUnit"] if data.get("grossWeightUnit") is not None else None),
            net_weight_unit=(data["netWeightUnit"] if data.get("netWeightUnit") is not None else None),
            package_count=(Decimal(str(data["packageCount"])) if data.get("packageCount") is not None else None),
            package_unit=(data["packageUnit"] if data.get("packageUnit") is not None else None),
            volume=(Decimal(str(data["volume"])) if data.get("volume") is not None else None),
            volume_unit=(data["volumeUnit"] if data.get("volumeUnit") is not None else None),
            export=(EcfExportPayload.from_dict(data["export"]) if data.get("export") is not None else None),
        )


@dataclass
class EcfSubquantityPayload:
    quantity: Decimal
    unit_code: str

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "quantity": _ser(self.quantity),
            "unitCode": _ser(self.unit_code),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfSubquantityPayload":
        return cls(
            quantity=Decimal(str(data["quantity"])),
            unit_code=data["unitCode"],
        )


@dataclass
class EcfSubtotalPayload:
    """Subtotal informativo para la RI (Sección C). No afecta la base imponible."""

    number: int | None = None
    description: str | None = None
    order: int | None = None
    monto_gravado_total: Decimal | None = None
    monto_gravado_i1: Decimal | None = None
    monto_gravado_i2: Decimal | None = None
    monto_gravado_i3: Decimal | None = None
    total_itbis: Decimal | None = None
    itbis1: Decimal | None = None
    itbis2: Decimal | None = None
    itbis3: Decimal | None = None
    monto_impuesto_adicional: Decimal | None = None
    monto_exento: Decimal | None = None
    amount: Decimal | None = None
    lines: int | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "number": _ser(self.number),
            "description": _ser(self.description),
            "order": _ser(self.order),
            "montoGravadoTotal": _ser(self.monto_gravado_total),
            "montoGravadoI1": _ser(self.monto_gravado_i1),
            "montoGravadoI2": _ser(self.monto_gravado_i2),
            "montoGravadoI3": _ser(self.monto_gravado_i3),
            "totalItbis": _ser(self.total_itbis),
            "itbis1": _ser(self.itbis1),
            "itbis2": _ser(self.itbis2),
            "itbis3": _ser(self.itbis3),
            "montoImpuestoAdicional": _ser(self.monto_impuesto_adicional),
            "montoExento": _ser(self.monto_exento),
            "amount": _ser(self.amount),
            "lines": _ser(self.lines),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfSubtotalPayload":
        return cls(
            number=(data["number"] if data.get("number") is not None else None),
            description=(data["description"] if data.get("description") is not None else None),
            order=(data["order"] if data.get("order") is not None else None),
            monto_gravado_total=(Decimal(str(data["montoGravadoTotal"])) if data.get("montoGravadoTotal") is not None else None),
            monto_gravado_i1=(Decimal(str(data["montoGravadoI1"])) if data.get("montoGravadoI1") is not None else None),
            monto_gravado_i2=(Decimal(str(data["montoGravadoI2"])) if data.get("montoGravadoI2") is not None else None),
            monto_gravado_i3=(Decimal(str(data["montoGravadoI3"])) if data.get("montoGravadoI3") is not None else None),
            total_itbis=(Decimal(str(data["totalItbis"])) if data.get("totalItbis") is not None else None),
            itbis1=(Decimal(str(data["itbis1"])) if data.get("itbis1") is not None else None),
            itbis2=(Decimal(str(data["itbis2"])) if data.get("itbis2") is not None else None),
            itbis3=(Decimal(str(data["itbis3"])) if data.get("itbis3") is not None else None),
            monto_impuesto_adicional=(Decimal(str(data["montoImpuestoAdicional"])) if data.get("montoImpuestoAdicional") is not None else None),
            monto_exento=(Decimal(str(data["montoExento"])) if data.get("montoExento") is not None else None),
            amount=(Decimal(str(data["amount"])) if data.get("amount") is not None else None),
            lines=(data["lines"] if data.get("lines") is not None else None),
        )


@dataclass
class EcfSummaryDto:
    """Fila del listado de comprobantes emitidos."""

    id: str
    status: str
    encf: str
    type: int
    environment: str
    issue_date: str
    monto_total: Decimal
    created_at: str
    buyer_rnc: str | None = None
    buyer_name: str | None = None
    retry: EcfRetry | None = None
    """Reintento automático (ver EcfRetry? EcfDto.Retry); `null` si no hubo ninguno."""

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "id": _ser(self.id),
            "status": _ser(self.status),
            "encf": _ser(self.encf),
            "type": _ser(self.type),
            "environment": _ser(self.environment),
            "issueDate": _ser(self.issue_date),
            "montoTotal": _ser(self.monto_total),
            "buyerRnc": _ser(self.buyer_rnc),
            "buyerName": _ser(self.buyer_name),
            "createdAt": _ser(self.created_at),
            "retry": _ser(self.retry),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfSummaryDto":
        return cls(
            id=data["id"],
            status=data["status"],
            encf=data["encf"],
            type=data["type"],
            environment=data["environment"],
            issue_date=data["issueDate"],
            monto_total=Decimal(str(data["montoTotal"])),
            buyer_rnc=(data["buyerRnc"] if data.get("buyerRnc") is not None else None),
            buyer_name=(data["buyerName"] if data.get("buyerName") is not None else None),
            created_at=data["createdAt"],
            retry=(EcfRetry.from_dict(data["retry"]) if data.get("retry") is not None else None),
        )


@dataclass
class EcfTransportPayload:
    """Datos de transporte (`<Transporte>`). Passthrough."""

    driver: str | None = None
    transport_document: str | None = None
    vehicle_id: str | None = None
    plate: str | None = None
    route: str | None = None
    zone: str | None = None
    delivery_note: str | None = None
    via: str | None = None
    origin_country: str | None = None
    destination_address: str | None = None
    destination_country: str | None = None
    carrier_rnc: str | None = None
    carrier_name: str | None = None
    voyage_number: str | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "driver": _ser(self.driver),
            "transportDocument": _ser(self.transport_document),
            "vehicleId": _ser(self.vehicle_id),
            "plate": _ser(self.plate),
            "route": _ser(self.route),
            "zone": _ser(self.zone),
            "deliveryNote": _ser(self.delivery_note),
            "via": _ser(self.via),
            "originCountry": _ser(self.origin_country),
            "destinationAddress": _ser(self.destination_address),
            "destinationCountry": _ser(self.destination_country),
            "carrierRnc": _ser(self.carrier_rnc),
            "carrierName": _ser(self.carrier_name),
            "voyageNumber": _ser(self.voyage_number),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "EcfTransportPayload":
        return cls(
            driver=(data["driver"] if data.get("driver") is not None else None),
            transport_document=(data["transportDocument"] if data.get("transportDocument") is not None else None),
            vehicle_id=(data["vehicleId"] if data.get("vehicleId") is not None else None),
            plate=(data["plate"] if data.get("plate") is not None else None),
            route=(data["route"] if data.get("route") is not None else None),
            zone=(data["zone"] if data.get("zone") is not None else None),
            delivery_note=(data["deliveryNote"] if data.get("deliveryNote") is not None else None),
            via=(data["via"] if data.get("via") is not None else None),
            origin_country=(data["originCountry"] if data.get("originCountry") is not None else None),
            destination_address=(data["destinationAddress"] if data.get("destinationAddress") is not None else None),
            destination_country=(data["destinationCountry"] if data.get("destinationCountry") is not None else None),
            carrier_rnc=(data["carrierRnc"] if data.get("carrierRnc") is not None else None),
            carrier_name=(data["carrierName"] if data.get("carrierName") is not None else None),
            voyage_number=(data["voyageNumber"] if data.get("voyageNumber") is not None else None),
        )


@dataclass
class IssueEcfCommand:
    """Payload de emisión de un e-CF (`POST /api/v1/ecf`). Un solo objeto discriminado por int IssueEcfCommand.Type; el servidor asigna la secuencia, arma el bloque Emisor desde el perfil del tenant, calcula los totales, firma y persiste."""

    type: int
    """Código DGII del tipo de e-CF: 31, 32, 33, 34, 41, 43, 44, 45, 46, 47."""
    lines: list[EcfLinePayload]
    encf: str | None = None
    """e-NCF de 13 caracteres. Solo con `sequences.mode = external` (y obligatorio ahí); con `managed` se rechaza, NovaFE asigna el suyo."""
    sequence_expires_on: str | None = None
    """`<FechaVencimientoSecuencia>` del e-NCF. Con `sequences.mode = external` es obligatorio en los tipos que llevan vencimiento (todos menos 32 y 34) y no aplica en esos dos; en `managed` se rechaza."""
    issue_date: str | None = None
    """Fecha de emisión (calendario dominicano). Default: hoy."""
    income_type: str | None = None
    """`<TipoIngresos>` — "01"…"06". Obligatorio en 31/32/33/34/44/45/46."""
    prices_include_tax: bool | None = None
    """`true` si los precios de las líneas ya traen el ITBIS incluido."""
    deferred_delivery: bool | None = None
    """`<IndicadorEnvioDiferido>` — solo contribuyentes autorizados."""
    non_invoiceable_amount: Decimal | None = None
    """`<MontoNoFacturable>` — reembolsos, propina voluntaria. Puede ser negativo."""
    internal_number: str | None = None
    """`<NumeroFacturaInterna>` — clave de dedup de negocio."""
    seller_code: str | None = None
    """`<CodigoVendedor>`."""
    additional_info: EcfAdditionalInfoPayload | None = None
    buyer: EcfBuyerPayload | None = None
    payment: EcfPaymentPayload | None = None
    reference: EcfReferencePayload | None = None
    global_adjustments: list[EcfGlobalAdjustmentPayload] | None = None
    foreign_currency: EcfForeignCurrencyPayload | None = None
    shipping: EcfShippingPayload | None = None
    transport: EcfTransportPayload | None = None
    subtotals: list[EcfSubtotalPayload] | None = None
    pagination: list[EcfPagePayload] | None = None
    declared_totals: EcfDeclaredTotalsPayload | None = None
    """Totales declarados por el cliente (chequeo de tolerancia; nunca bloquea)."""

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "type": _ser(self.type),
            "encf": _ser(self.encf),
            "sequenceExpiresOn": _ser(self.sequence_expires_on),
            "issueDate": _ser(self.issue_date),
            "incomeType": _ser(self.income_type),
            "pricesIncludeTax": _ser(self.prices_include_tax),
            "deferredDelivery": _ser(self.deferred_delivery),
            "nonInvoiceableAmount": _ser(self.non_invoiceable_amount),
            "internalNumber": _ser(self.internal_number),
            "sellerCode": _ser(self.seller_code),
            "additionalInfo": _ser(self.additional_info),
            "buyer": _ser(self.buyer),
            "payment": _ser(self.payment),
            "lines": _ser(self.lines),
            "reference": _ser(self.reference),
            "globalAdjustments": _ser(self.global_adjustments),
            "foreignCurrency": _ser(self.foreign_currency),
            "shipping": _ser(self.shipping),
            "transport": _ser(self.transport),
            "subtotals": _ser(self.subtotals),
            "pagination": _ser(self.pagination),
            "declaredTotals": _ser(self.declared_totals),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "IssueEcfCommand":
        return cls(
            type=data["type"],
            encf=(data["encf"] if data.get("encf") is not None else None),
            sequence_expires_on=(data["sequenceExpiresOn"] if data.get("sequenceExpiresOn") is not None else None),
            issue_date=(data["issueDate"] if data.get("issueDate") is not None else None),
            income_type=(data["incomeType"] if data.get("incomeType") is not None else None),
            prices_include_tax=(data["pricesIncludeTax"] if data.get("pricesIncludeTax") is not None else None),
            deferred_delivery=(data["deferredDelivery"] if data.get("deferredDelivery") is not None else None),
            non_invoiceable_amount=(Decimal(str(data["nonInvoiceableAmount"])) if data.get("nonInvoiceableAmount") is not None else None),
            internal_number=(data["internalNumber"] if data.get("internalNumber") is not None else None),
            seller_code=(data["sellerCode"] if data.get("sellerCode") is not None else None),
            additional_info=(EcfAdditionalInfoPayload.from_dict(data["additionalInfo"]) if data.get("additionalInfo") is not None else None),
            buyer=(EcfBuyerPayload.from_dict(data["buyer"]) if data.get("buyer") is not None else None),
            payment=(EcfPaymentPayload.from_dict(data["payment"]) if data.get("payment") is not None else None),
            lines=[EcfLinePayload.from_dict(i) for i in data["lines"]],
            reference=(EcfReferencePayload.from_dict(data["reference"]) if data.get("reference") is not None else None),
            global_adjustments=([EcfGlobalAdjustmentPayload.from_dict(i) for i in data["globalAdjustments"]] if data.get("globalAdjustments") is not None else None),
            foreign_currency=(EcfForeignCurrencyPayload.from_dict(data["foreignCurrency"]) if data.get("foreignCurrency") is not None else None),
            shipping=(EcfShippingPayload.from_dict(data["shipping"]) if data.get("shipping") is not None else None),
            transport=(EcfTransportPayload.from_dict(data["transport"]) if data.get("transport") is not None else None),
            subtotals=([EcfSubtotalPayload.from_dict(i) for i in data["subtotals"]] if data.get("subtotals") is not None else None),
            pagination=([EcfPagePayload.from_dict(i) for i in data["pagination"]] if data.get("pagination") is not None else None),
            declared_totals=(EcfDeclaredTotalsPayload.from_dict(data["declaredTotals"]) if data.get("declaredTotals") is not None else None),
        )


@dataclass
class PagedResultOfEcfSummaryDto:
    items: list[EcfSummaryDto]
    total_count: int
    page: int
    page_size: int
    total_pages: int | None = None
    has_next_page: bool | None = None
    has_previous_page: bool | None = None

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "items": _ser(self.items),
            "totalCount": _ser(self.total_count),
            "page": _ser(self.page),
            "pageSize": _ser(self.page_size),
            "totalPages": _ser(self.total_pages),
            "hasNextPage": _ser(self.has_next_page),
            "hasPreviousPage": _ser(self.has_previous_page),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "PagedResultOfEcfSummaryDto":
        return cls(
            items=[EcfSummaryDto.from_dict(i) for i in data["items"]],
            total_count=data["totalCount"],
            page=data["page"],
            page_size=data["pageSize"],
            total_pages=(data["totalPages"] if data.get("totalPages") is not None else None),
            has_next_page=(data["hasNextPage"] if data.get("hasNextPage") is not None else None),
            has_previous_page=(data["hasPreviousPage"] if data.get("hasPreviousPage") is not None else None),
        )


@dataclass
class RepresentationLayout:
    pass

    def to_dict(self) -> dict[str, Any]:
        return _strip({
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "RepresentationLayout":
        return cls(
        )


@dataclass
class ValidateEcfIssueDto:
    """Un error o aviso de `POST /api/v1/ecf/validate`."""

    code: str
    """Código estable, apto para lógica del cliente."""
    message: str
    """Descripción lista para mostrar."""
    field: str | None = None
    """Campo del payload al que se refiere (p. ej. `Lines[0].Quantity`); `null` si es de todo el comprobante."""

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "field": _ser(self.field),
            "code": _ser(self.code),
            "message": _ser(self.message),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "ValidateEcfIssueDto":
        return cls(
            field=(data["field"] if data.get("field") is not None else None),
            code=data["code"],
            message=data["message"],
        )


@dataclass
class ValidateEcfPreviewDto:
    """Vista previa de `POST /api/v1/ecf/validate` para un payload válido: pasó la matriz de validación por tipo (Módulo 2) y el cálculo de totales (Módulo 6) sin asignar una secuencia real, firmar ni persistir nada."""

    type: int
    type_name: str
    sample_encf: str
    """Placeholder (`E{tipo}0000000000`) — nunca un e-NCF real, no se asignó ninguna secuencia para construir este documento."""
    issue_date: str
    monto_gravado_total: Decimal
    monto_exento: Decimal
    total_itbis: Decimal
    monto_impuesto_adicional: Decimal
    monto_total: Decimal
    expect_conditional_acceptance: bool
    """Los totales quedarían fuera de la tolerancia de cuadratura (RF-06.6) — la DGII probablemente aceptaría el comprobante de forma condicional si se emite así."""
    sequence_expires_on_estimate: str | None = None
    """Estimado (31-dic del año siguiente a DateOnly ValidateEcfPreviewDto.IssueDate) cuando el tipo lo lleva; no viene de una secuencia real. `null` en los tipos que no lo llevan (32, 34)."""

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "type": _ser(self.type),
            "typeName": _ser(self.type_name),
            "sampleEncf": _ser(self.sample_encf),
            "issueDate": _ser(self.issue_date),
            "sequenceExpiresOnEstimate": _ser(self.sequence_expires_on_estimate),
            "montoGravadoTotal": _ser(self.monto_gravado_total),
            "montoExento": _ser(self.monto_exento),
            "totalItbis": _ser(self.total_itbis),
            "montoImpuestoAdicional": _ser(self.monto_impuesto_adicional),
            "montoTotal": _ser(self.monto_total),
            "expectConditionalAcceptance": _ser(self.expect_conditional_acceptance),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "ValidateEcfPreviewDto":
        return cls(
            type=data["type"],
            type_name=data["typeName"],
            sample_encf=data["sampleEncf"],
            issue_date=data["issueDate"],
            sequence_expires_on_estimate=(data["sequenceExpiresOnEstimate"] if data.get("sequenceExpiresOnEstimate") is not None else None),
            monto_gravado_total=Decimal(str(data["montoGravadoTotal"])),
            monto_exento=Decimal(str(data["montoExento"])),
            total_itbis=Decimal(str(data["totalItbis"])),
            monto_impuesto_adicional=Decimal(str(data["montoImpuestoAdicional"])),
            monto_total=Decimal(str(data["montoTotal"])),
            expect_conditional_acceptance=data["expectConditionalAcceptance"],
        )


@dataclass
class ValidateEcfResultDto:
    """Resultado de `POST /api/v1/ecf/validate`. Siempre llega con `200` cuando el cuerpo se pudo leer y el contribuyente puede validar: bool ValidateEcfResultDto.Valid dice si el comprobante pasaría la validación de forma y la matriz estructural y fiscal por tipo (Módulo 2 + 6) que corre `POST /ecf`. No asigna una secuencia real, no firma y no persiste nada. Lo que impide validar (sin autenticar, perfil del emisor sin configurar) sigue siendo un error HTTP."""

    valid: bool
    """`true` si el comprobante se podría emitir tal cual en cuanto a su contenido."""
    message: str
    """Resumen en una frase, listo para mostrar."""
    errors: list[ValidateEcfIssueDto]
    """Todos los problemas encontrados, no solo el primero. Vacío si bool ValidateEcfResultDto.Valid."""
    warnings: list[ValidateEcfIssueDto]
    """Avisos que no impiden emitir (p. ej. aceptación condicional probable)."""
    preview: ValidateEcfPreviewDto | None = None
    """La vista previa de lo que se emitiría; `null` si el comprobante no es válido."""

    def to_dict(self) -> dict[str, Any]:
        return _strip({
            "valid": _ser(self.valid),
            "message": _ser(self.message),
            "errors": _ser(self.errors),
            "warnings": _ser(self.warnings),
            "preview": _ser(self.preview),
        })

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "ValidateEcfResultDto":
        return cls(
            valid=data["valid"],
            message=data["message"],
            errors=[ValidateEcfIssueDto.from_dict(i) for i in data["errors"]],
            warnings=[ValidateEcfIssueDto.from_dict(i) for i in data["warnings"]],
            preview=(ValidateEcfPreviewDto.from_dict(data["preview"]) if data.get("preview") is not None else None),
        )
