libredte-lib-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ """SDK Python para la API de LibreDTE Lib (facturación electrónica Chile)."""
2
+
3
+ from .billing import BillingPackage
4
+ from .billing.document import (
5
+ AutorizacionDte,
6
+ Document,
7
+ DocumentBuilderService,
8
+ DocumentComponent,
9
+ DocumentDispatcherService,
10
+ DocumentRendererService,
11
+ Emisor,
12
+ Envelope,
13
+ Rendering,
14
+ RenderResult,
15
+ )
16
+ from .billing.enums import SiiEnvironment
17
+ from .billing.identifier import (
18
+ Caf,
19
+ CafFakerService,
20
+ CafLoaderService,
21
+ CafValidatorService,
22
+ IdentifierComponent,
23
+ )
24
+ from .billing.integration import (
25
+ IntegrationComponent,
26
+ SendResult,
27
+ SiiDteService,
28
+ SiiStatus,
29
+ )
30
+ from .billing.trading_parties import (
31
+ Certificate,
32
+ MandatarioManagerService,
33
+ TradingPartiesComponent,
34
+ )
35
+ from .client import ApiClient
36
+ from .exceptions import (
37
+ LibreDteApiError,
38
+ LibreDteConnectionError,
39
+ LibreDteRateLimitError,
40
+ LibreDteSdkError,
41
+ )
42
+ from .sdk import LibreDTE
43
+
44
+ __all__ = [
45
+ 'ApiClient',
46
+ 'AutorizacionDte',
47
+ 'BillingPackage',
48
+ 'Caf',
49
+ 'CafFakerService',
50
+ 'CafLoaderService',
51
+ 'CafValidatorService',
52
+ 'Certificate',
53
+ 'Document',
54
+ 'DocumentBuilderService',
55
+ 'DocumentComponent',
56
+ 'DocumentDispatcherService',
57
+ 'DocumentRendererService',
58
+ 'Emisor',
59
+ 'Envelope',
60
+ 'IdentifierComponent',
61
+ 'IntegrationComponent',
62
+ 'LibreDTE',
63
+ 'LibreDteApiError',
64
+ 'LibreDteConnectionError',
65
+ 'LibreDteRateLimitError',
66
+ 'LibreDteSdkError',
67
+ 'MandatarioManagerService',
68
+ 'RenderResult',
69
+ 'Rendering',
70
+ 'SendResult',
71
+ 'SiiDteService',
72
+ 'SiiEnvironment',
73
+ 'SiiStatus',
74
+ 'TradingPartiesComponent',
75
+ ]
@@ -0,0 +1,31 @@
1
+ """
2
+ Paquete `billing`: facturación electrónica de Chile.
3
+
4
+ Organizado igual que la propia API (`paquete.componente.worker::operacion`):
5
+ un subpaquete por componente (`document`, `identifier`, `trading_parties`,
6
+ `integration`), cada uno con un `*Component` que agrupa sus servicios por
7
+ worker. Agregar un worker nuevo (de un componente ya soportado, u otro
8
+ componente/paquete de la API) es agregar un archivo y registrarlo en el
9
+ `__init__.py` de su componente — no toca nada de lo demás.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from ..client import ApiClient
15
+ from .document import DocumentComponent
16
+ from .identifier import IdentifierComponent
17
+ from .integration import IntegrationComponent
18
+ from .trading_parties import TradingPartiesComponent
19
+
20
+ __all__ = ['BillingPackage']
21
+
22
+
23
+ class BillingPackage:
24
+ """Agrupa los componentes del paquete `billing` de la API."""
25
+
26
+ def __init__(self, client: ApiClient) -> None:
27
+ """Crea los componentes del paquete sobre el `ApiClient` dado."""
28
+ self.document = DocumentComponent(client)
29
+ self.identifier = IdentifierComponent(client)
30
+ self.trading_parties = TradingPartiesComponent(client)
31
+ self.integration = IntegrationComponent(client)
@@ -0,0 +1,29 @@
1
+ """Utilidades compartidas entre los componentes de `billing`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+
7
+
8
+ class XmlPayloadMixin:
9
+ """
10
+ Mixin para un DTO cuyo dato principal es un XML en base64.
11
+
12
+ Espera un atributo `xml_base64: str` en la clase concreta (`Document`,
13
+ `Envelope`, `Caf`, ...) y expone `xml_bytes`/`xml` sobre él, para no
14
+ repetir esta misma conversión en cada DTO.
15
+ """
16
+
17
+ __slots__ = ()
18
+
19
+ xml_base64: str
20
+
21
+ @property
22
+ def xml_bytes(self) -> bytes:
23
+ """XML decodificado desde base64."""
24
+ return base64.b64decode(self.xml_base64)
25
+
26
+ @property
27
+ def xml(self) -> str:
28
+ """XML como texto (ISO-8859-1, la codificación que usa el SII)."""
29
+ return self.xml_bytes.decode('iso-8859-1')
@@ -0,0 +1,39 @@
1
+ """Componente `billing.document`: DTE — construcción, sobre y render."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ...client import ApiClient
6
+ from .builder import DocumentBuilderService
7
+ from .dispatcher import DocumentDispatcherService
8
+ from .models import (
9
+ AutorizacionDte,
10
+ Document,
11
+ Emisor,
12
+ Envelope,
13
+ Rendering,
14
+ RenderResult,
15
+ )
16
+ from .renderer import DocumentRendererService
17
+
18
+ __all__ = [
19
+ 'AutorizacionDte',
20
+ 'Document',
21
+ 'DocumentBuilderService',
22
+ 'DocumentComponent',
23
+ 'DocumentDispatcherService',
24
+ 'DocumentRendererService',
25
+ 'Emisor',
26
+ 'Envelope',
27
+ 'RenderResult',
28
+ 'Rendering',
29
+ ]
30
+
31
+
32
+ class DocumentComponent:
33
+ """Agrupa los servicios de `billing.document`."""
34
+
35
+ def __init__(self, client: ApiClient) -> None:
36
+ """Crea los servicios del componente sobre el `ApiClient` dado."""
37
+ self.builder = DocumentBuilderService(client)
38
+ self.dispatcher = DocumentDispatcherService(client)
39
+ self.renderer = DocumentRendererService(client)
@@ -0,0 +1,68 @@
1
+ """Servicio para `billing.document.builder`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ...client import ApiClient
8
+ from ..trading_parties.models import Certificate
9
+ from .models import Document
10
+
11
+
12
+ class DocumentBuilderService:
13
+ """
14
+ Construye documentos tributarios (`billing.document.builder`).
15
+
16
+ El mismo worker de la API sirve tanto para un borrador como para el
17
+ documento timbrado y firmado, según qué datos se le pasen — acá se
18
+ separa en dos métodos explícitos para que la intención de cada
19
+ llamada quede clara en el código que la usa.
20
+ """
21
+
22
+ _OPERATION = 'billing.document.builder::build'
23
+
24
+ def __init__(self, client: ApiClient) -> None:
25
+ """Guarda el `ApiClient` compartido usado para llamar a la API."""
26
+ self._client = client
27
+
28
+ def build_draft(self, parsed_data: dict[str, Any]) -> Document:
29
+ """
30
+ Emite el borrador de un DTE a partir de datos ya normalizados.
31
+
32
+ `parsed_data` es el `Encabezado`/`Detalle` (y demás nodos) del
33
+ formato DTE del SII, incluyendo `Encabezado.IdDoc.Folio` (el SDK
34
+ no asigna folios: eso lo decide quien llama, típicamente porque
35
+ lleva el correlativo). Sin CAF ni certificado, el resultado no
36
+ queda timbrado (`Document.is_timbrado` es `False`).
37
+ """
38
+ data = self._client.call(
39
+ self._OPERATION,
40
+ bag={'parsedData': parsed_data},
41
+ )
42
+ return Document.from_api(data)
43
+
44
+ def build_signed(
45
+ self,
46
+ parsed_data: dict[str, Any],
47
+ *,
48
+ caf_xml: str,
49
+ certificate: Certificate,
50
+ ) -> Document:
51
+ """
52
+ Genera el DTE real, timbrado y firmado.
53
+
54
+ Requiere un CAF real (XML tal como lo entrega el SII, cubriendo
55
+ el folio indicado en `parsed_data`) y el certificado digital del
56
+ emisor. Para pruebas, ambos se pueden generar con
57
+ `IdentifierComponent.caf_faker` y
58
+ `TradingPartiesComponent.mandatario_manager`.
59
+ """
60
+ data = self._client.call(
61
+ self._OPERATION,
62
+ bag={
63
+ 'parsedData': parsed_data,
64
+ 'caf': caf_xml,
65
+ 'certificate': certificate.to_payload(),
66
+ },
67
+ )
68
+ return Document.from_api(data)
@@ -0,0 +1,49 @@
1
+ """Servicio para `billing.document.dispatcher`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ...client import ApiClient
6
+ from ..trading_parties.models import Certificate
7
+ from .models import Emisor, Envelope
8
+
9
+
10
+ class DocumentDispatcherService:
11
+ """
12
+ Arma el sobre `EnvioDTE` (`billing.document.dispatcher`).
13
+
14
+ Solo cubre `create`: envolver y firmar un documento ya construido.
15
+ El worker también expone `loadXml`/`validate`/`validateSchema`/
16
+ `validateSignature`, no incluidas acá.
17
+ """
18
+
19
+ _CREATE_OPERATION = 'billing.document.dispatcher::create'
20
+
21
+ def __init__(self, client: ApiClient) -> None:
22
+ """Guarda el `ApiClient` compartido usado para llamar a la API."""
23
+ self._client = client
24
+
25
+ def create(
26
+ self,
27
+ document_xml_base64: str,
28
+ *,
29
+ certificate: Certificate,
30
+ emisor: Emisor,
31
+ ) -> Envelope:
32
+ """
33
+ Envuelve y firma un documento ya timbrado en un sobre `EnvioDTE`.
34
+
35
+ `document_xml_base64` es el XML en base64 del documento a
36
+ envolver — típicamente `Document.xml_base64` de un documento ya
37
+ timbrado y firmado (`DocumentBuilderService.build_signed`). El
38
+ sobre resultante es lo que se envía al SII
39
+ (`SiiDteService.send`).
40
+ """
41
+ data = self._client.call(
42
+ self._CREATE_OPERATION,
43
+ bag={
44
+ 'xmlDocument': document_xml_base64,
45
+ 'certificate': certificate.to_payload(),
46
+ 'emisor': emisor.to_payload(),
47
+ },
48
+ )
49
+ return Envelope.from_api(data)
@@ -0,0 +1,169 @@
1
+ """
2
+ DTO del componente `billing.document`.
3
+
4
+ No reimplementan el esquema DTE del SII (`Encabezado`/`Detalle`/etc. se
5
+ pasan como `dict` tal cual ese formato, ver `builder.py`): solo tipan lo
6
+ que el propio SDK arma o recibe de la API.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ from dataclasses import dataclass
13
+ from typing import Any
14
+
15
+ from ..common import XmlPayloadMixin
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class AutorizacionDte:
20
+ """Resolución del SII que autoriza al emisor a emitir DTE."""
21
+
22
+ fecha_resolucion: str
23
+ numero_resolucion: int
24
+
25
+ def to_payload(self) -> dict[str, Any]:
26
+ """Payload esperado por la API para la autorización."""
27
+ return {
28
+ 'fecha_resolucion': self.fecha_resolucion,
29
+ 'numero_resolucion': self.numero_resolucion,
30
+ }
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class Emisor:
35
+ """Datos del emisor requeridos para armar el sobre `EnvioDTE`."""
36
+
37
+ rut: str
38
+ razon_social: str
39
+ autorizacion_dte: AutorizacionDte
40
+
41
+ def to_payload(self) -> dict[str, Any]:
42
+ """Payload esperado por la API para el emisor del sobre."""
43
+ return {
44
+ 'rut': self.rut,
45
+ 'razon_social': self.razon_social,
46
+ 'autorizacion_dte': self.autorizacion_dte.to_payload(),
47
+ }
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class Document(XmlPayloadMixin):
52
+ """
53
+ Documento tributario construido.
54
+
55
+ Puede ser un borrador, un documento timbrado, o timbrado y firmado —
56
+ lo distingue `is_timbrado` (`ted is not None`), no una subclase
57
+ distinta: es el mismo recurso en distintos estados, según qué datos
58
+ se le hayan pasado a `DocumentBuilderService`.
59
+ """
60
+
61
+ id: str
62
+ datos: dict[str, Any]
63
+ ted: dict[str, Any] | None
64
+ xml_base64: str
65
+
66
+ @property
67
+ def is_timbrado(self) -> bool:
68
+ """Si el documento ya tiene Timbre Electrónico (TED)."""
69
+ return self.ted is not None
70
+
71
+ @classmethod
72
+ def from_api(cls, data: dict[str, Any]) -> Document:
73
+ """Construye un `Document` desde el `data` que devuelve la API."""
74
+ return cls(
75
+ id=data['id'],
76
+ datos=data.get('datos') or {},
77
+ ted=data.get('ted'),
78
+ xml_base64=data['xml'],
79
+ )
80
+
81
+
82
+ @dataclass(frozen=True, slots=True)
83
+ class Envelope(XmlPayloadMixin):
84
+ """Sobre `EnvioDTE` construido y firmado, listo para enviar al SII."""
85
+
86
+ tag: str
87
+ xml_base64: str
88
+
89
+ @classmethod
90
+ def from_api(cls, data: dict[str, Any]) -> Envelope:
91
+ """Construye un `Envelope` desde el `data` que devuelve la API."""
92
+ return cls(tag=data['tag'], xml_base64=data['xml'])
93
+
94
+
95
+ @dataclass(frozen=True, slots=True)
96
+ class Rendering:
97
+ """
98
+ Un archivo generado por `DocumentRendererService.render()`.
99
+
100
+ `content_base64` es el archivo completo en base64 (un PDF, un HTML,
101
+ lo que sea — `mime_type` dice qué es). `label` es la presentación
102
+ pedida en `renderings` (ej. `'tributaria'`, `'cedible'`); `copies`/
103
+ `copy_number` identifican esta copia entre las pedidas de ese mismo
104
+ `label` (ej. `copies=2, copy_number=1` es la primera de 2 copias
105
+ tributarias). El nombre de los campos en la API (`content`/
106
+ `mimeType`/`filename`/`copyNumber`) es camelCase; acá quedan en
107
+ snake_case como el resto del SDK.
108
+ """
109
+
110
+ content_base64: str
111
+ mime_type: str
112
+ filename: str
113
+ label: str
114
+ copies: int
115
+ copy_number: int
116
+
117
+ @property
118
+ def content_bytes(self) -> bytes:
119
+ """Contenido del archivo, decodificado desde base64."""
120
+ return base64.b64decode(self.content_base64)
121
+
122
+ @classmethod
123
+ def from_api(cls, data: dict[str, Any]) -> Rendering:
124
+ """Construye un `Rendering` desde un ítem de `data.renderings`."""
125
+ return cls(
126
+ content_base64=data['content'],
127
+ mime_type=data['mimeType'],
128
+ filename=data['filename'],
129
+ label=data['label'],
130
+ copies=data['copies'],
131
+ copy_number=data['copyNumber'],
132
+ )
133
+
134
+
135
+ @dataclass(frozen=True, slots=True)
136
+ class RenderResult:
137
+ """
138
+ Resultado de `DocumentRendererService.render()`.
139
+
140
+ `renderings` trae un ítem por copia generada: por defecto (sin pedir
141
+ `renderings` explícito) es un único `'tributaria'`; pidiendo más de
142
+ una presentación y/o copia (ej. `renderings={'tributaria': 2,
143
+ 'cedible': 1}`), trae uno por cada copia efectivamente generada —
144
+ una presentación que la API no pudo generar para ese documento (ej.
145
+ `'cedible'` en un tipo de documento sin acuse de recibo) se omite en
146
+ silencio, no rompe la llamada. `.first` es un atajo para el caso más
147
+ común (un solo archivo); `.by_label()` filtra por presentación
148
+ cuando se pidió más de una.
149
+ """
150
+
151
+ renderings: tuple[Rendering, ...]
152
+
153
+ @property
154
+ def first(self) -> Rendering:
155
+ """El primer archivo generado."""
156
+ return self.renderings[0]
157
+
158
+ def by_label(self, label: str) -> tuple[Rendering, ...]:
159
+ """Los renderings de una presentación (ej. `'tributaria'`)."""
160
+ return tuple(r for r in self.renderings if r.label == label)
161
+
162
+ @classmethod
163
+ def from_api(cls, data: dict[str, Any]) -> RenderResult:
164
+ """Construye un `RenderResult` desde el `data` que devuelve la API."""
165
+ return cls(
166
+ renderings=tuple(
167
+ Rendering.from_api(item) for item in data['renderings']
168
+ ),
169
+ )
@@ -0,0 +1,60 @@
1
+ """Servicio para `billing.document.renderer`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ...client import ApiClient
8
+ from .models import RenderResult
9
+
10
+
11
+ class DocumentRendererService:
12
+ """
13
+ Renderiza un documento tributario (`billing.document.renderer`).
14
+
15
+ Soporta `format='html'` y `format='pdf'`. La API devuelve siempre
16
+ `data.renderings`, una lista de archivos en base64 — ver
17
+ `RenderResult`/`Rendering` en `models.py`.
18
+ """
19
+
20
+ _OPERATION = 'billing.document.renderer::render'
21
+
22
+ def __init__(self, client: ApiClient) -> None:
23
+ """Guarda el `ApiClient` compartido usado para llamar a la API."""
24
+ self._client = client
25
+
26
+ def render(
27
+ self,
28
+ document_xml_base64: str,
29
+ *,
30
+ format: str = 'pdf', # noqa: A002
31
+ renderings: dict[str, int] | None = None,
32
+ ) -> RenderResult:
33
+ """
34
+ Genera el PDF (u otro formato soportado por la API) de un documento.
35
+
36
+ Sirve tanto para un borrador como para un documento ya timbrado y
37
+ firmado: el resultado depende solo del XML que se le pase (ej.
38
+ `Document.xml_base64`).
39
+
40
+ Sin `renderings`, la API genera una única copia `'tributaria'`
41
+ (comportamiento por defecto). `renderings` pide presentaciones y
42
+ cantidad de copias de cada una — ej. `{'tributaria': 1,
43
+ 'cedible': 1}` — y la API rechaza (`LibreDteApiError`, 500) una
44
+ presentación que no existe, o si ninguna de las pedidas pudo
45
+ generarse (ej. pedir solo `'cedible'` para un tipo de documento
46
+ sin acuse de recibo). Ver `RenderResult` para cómo se identifica
47
+ cada copia en la respuesta.
48
+ """
49
+ renderer_options: dict[str, Any] = {'format': format}
50
+ if renderings is not None:
51
+ renderer_options['renderings'] = renderings
52
+
53
+ data = self._client.call(
54
+ self._OPERATION,
55
+ bag={
56
+ 'xmlDocument': document_xml_base64,
57
+ 'options': {'renderer': renderer_options},
58
+ },
59
+ )
60
+ return RenderResult.from_api(data)
@@ -0,0 +1,18 @@
1
+ """Enumeraciones del paquete `billing`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import IntEnum
6
+
7
+
8
+ class SiiEnvironment(IntEnum):
9
+ """
10
+ Ambiente del SII contra el que se opera.
11
+
12
+ Convención estándar del ecosistema LibreDTE. El default es
13
+ `CERTIFICATION` a propósito: nadie debería terminar enviando algo a
14
+ producción por haber omitido este argumento.
15
+ """
16
+
17
+ CERTIFICATION = 0
18
+ PRODUCTION = 1
@@ -0,0 +1,27 @@
1
+ """Componente `billing.identifier`: folios de documentos tributarios."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ...client import ApiClient
6
+ from .caf_faker import CafFakerService
7
+ from .caf_loader import CafLoaderService
8
+ from .caf_validator import CafValidatorService
9
+ from .models import Caf
10
+
11
+ __all__ = [
12
+ 'Caf',
13
+ 'CafFakerService',
14
+ 'CafLoaderService',
15
+ 'CafValidatorService',
16
+ 'IdentifierComponent',
17
+ ]
18
+
19
+
20
+ class IdentifierComponent:
21
+ """Agrupa los servicios de `billing.identifier`."""
22
+
23
+ def __init__(self, client: ApiClient) -> None:
24
+ """Crea los servicios del componente sobre el `ApiClient` dado."""
25
+ self.caf_faker = CafFakerService(client)
26
+ self.caf_loader = CafLoaderService(client)
27
+ self.caf_validator = CafValidatorService(client)
@@ -0,0 +1,49 @@
1
+ """Servicio para `billing.identifier.caf_faker`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ...client import ApiClient
8
+ from .models import Caf
9
+
10
+
11
+ class CafFakerService:
12
+ """
13
+ Genera CAF ficticios (`billing.identifier.caf_faker::create`).
14
+
15
+ Solo para pruebas/desarrollo: el CAF resultante no está autorizado
16
+ de verdad por el SII, pero permite ejercitar el timbrado/firma de un
17
+ documento (y, con eso, el resto del flujo) sin depender de un folio
18
+ real. El SDK lo usa así para poder probarse de punta a punta.
19
+ """
20
+
21
+ _OPERATION = 'billing.identifier.caf_faker::create'
22
+
23
+ def __init__(self, client: ApiClient) -> None:
24
+ """Guarda el `ApiClient` compartido usado para llamar a la API."""
25
+ self._client = client
26
+
27
+ def create(
28
+ self,
29
+ emisor: dict[str, Any],
30
+ *,
31
+ codigo_documento: int,
32
+ folio_desde: int = 1,
33
+ folio_hasta: int | None = None,
34
+ ) -> Caf:
35
+ """
36
+ Genera un CAF ficticio para `emisor` y `codigo_documento`.
37
+
38
+ `emisor` es un `dict` con `rut`/`razon_social`, tal como lo
39
+ espera la API. `folio_hasta` por defecto cubre solo
40
+ `folio_desde` (un único folio), igual que hace la API.
41
+ """
42
+ data = self._client.call(
43
+ self._OPERATION,
44
+ emisor=emisor,
45
+ codigoDocumento=codigo_documento,
46
+ folioDesde=folio_desde,
47
+ folioHasta=folio_hasta,
48
+ )
49
+ return Caf.from_api(data)
@@ -0,0 +1,31 @@
1
+ """Servicio para `billing.identifier.caf_loader`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ...client import ApiClient
6
+ from .models import Caf
7
+
8
+
9
+ class CafLoaderService:
10
+ """
11
+ Carga un CAF real (`billing.identifier.caf_loader::load`).
12
+
13
+ A diferencia de `CafFakerService` (que genera un CAF ficticio para
14
+ pruebas), esto carga el XML de un CAF **real** — el que el SII le
15
+ entrega al emisor y que la app que use este SDK deja subir al
16
+ usuario — y lo entrega como la misma entidad `Caf` que devuelve
17
+ `CafFakerService.create`, con su folio/vigencia ya resueltos. Sin
18
+ esto, `DocumentBuilderService.build_signed()` no tiene de dónde
19
+ sacar un CAF real para timbrar en producción.
20
+ """
21
+
22
+ _OPERATION = 'billing.identifier.caf_loader::load'
23
+
24
+ def __init__(self, client: ApiClient) -> None:
25
+ """Guarda el `ApiClient` compartido usado para llamar a la API."""
26
+ self._client = client
27
+
28
+ def load(self, xml_base64: str) -> Caf:
29
+ """Carga el CAF cuyo XML (en base64) es `xml_base64`."""
30
+ data = self._client.call(self._OPERATION, xml=xml_base64)
31
+ return Caf.from_api(data)
@@ -0,0 +1,36 @@
1
+ """Servicio para `billing.identifier.caf_validator`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ...client import ApiClient
6
+ from .models import Caf
7
+
8
+
9
+ class CafValidatorService:
10
+ """
11
+ Valida un CAF (`billing.identifier.caf_validator::validate`).
12
+
13
+ Valida la firma y las llaves públicas/privadas asociadas al CAF —
14
+ útil antes de usarlo en `DocumentBuilderService.build_signed()`,
15
+ para poder avisarle al usuario que su CAF no es válido (ej. vencido)
16
+ con un error claro, en vez de que la falla aparezca recién al
17
+ intentar timbrar un documento.
18
+ """
19
+
20
+ _OPERATION = 'billing.identifier.caf_validator::validate'
21
+
22
+ def __init__(self, client: ApiClient) -> None:
23
+ """Guarda el `ApiClient` compartido usado para llamar a la API."""
24
+ self._client = client
25
+
26
+ def validate(self, caf_xml_base64: str) -> Caf:
27
+ """
28
+ Valida el CAF cuyo XML (en base64) es `caf_xml_base64`.
29
+
30
+ La API espera el mismo XML en base64 que `CafLoaderService.load`
31
+ (el parámetro de la operación se llama `caf`, no `xml`, pero es
32
+ el mismo dato). Devuelve el `Caf` ya validado si es válido;
33
+ levanta `LibreDteApiError` si no lo es.
34
+ """
35
+ data = self._client.call(self._OPERATION, caf=caf_xml_base64)
36
+ return Caf.from_api(data)