diff --git a/.bumpversion.cfg b/.bumpversion.cfg index b940d94e..b2c02822 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.0 +current_version = 0.5.0 commit = True tag = True diff --git a/HISTORY.rst b/HISTORY.rst index dac52651..086009e9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,21 @@ History ------- +0.5.0 (2019-04-25) ++++++++++++++++++++++++ + +* (PR #29, 2019-04-25) dte.data_models: modify new fields of `DteDataL2` +* (PR #28, 2019-04-25) libs: add module `crypto_utils` +* (PR #27, 2019-04-25) libs: add module `encoding_utils` +* (PR #26, 2019-04-25) test_data: add files +* (PR #25, 2019-04-25) libs.xml_utils: fix class alias `XmlElementTree` +* (PR #24, 2019-04-25) requirements: add and update packages +* (PR #22, 2019-04-24) test_data: add files +* (PR #21, 2019-04-22) dte: many improvements +* (PR #20, 2019-04-22) libs.xml_utils: misc improvements +* (PR #19, 2019-04-22) test_data: fix and add real SII DTE & AEC XML files +* (PR #18, 2019-04-22) data.ref: add XML schemas for "Cesion" (RTC) + 0.4.0 (2019-04-16) +++++++++++++++++++++++ diff --git a/cl_sii/__init__.py b/cl_sii/__init__.py index 795a5582..43f5d1e4 100644 --- a/cl_sii/__init__.py +++ b/cl_sii/__init__.py @@ -5,4 +5,4 @@ """ -__version__ = '0.4.0' +__version__ = '0.5.0' diff --git a/cl_sii/data/ref/factura_electronica/schemas-xml/AEC_v10.xsd b/cl_sii/data/ref/factura_electronica/schemas-xml/AEC_v10.xsd new file mode 100644 index 00000000..da646f8f --- /dev/null +++ b/cl_sii/data/ref/factura_electronica/schemas-xml/AEC_v10.xsd @@ -0,0 +1,91 @@ + + + + + + + + + Archivo Electronico de Cesion + + + + + + Documento de AEC + + + + + + Informacion de AEC + + + + + + RUT que Genera el Archivo de Transferencias + + + + + RUT a Quien Va Dirigido el Archivo de Transferencias + + + + + Persona de Contacto para aclarar dudas + + + + + Telefono de Contacto + + + + + Correo Electronico de Contacto + + + + + Fecha y Hora de la Firma del Archivo de Transferencias + + + + + + + + + Cesiones + + + + + + Representacion XML y Grafica del DTE Cedido + + + + + Informacion Electronica de Recepcion y Aceptacion del DTE por Parte del Receptor + + + + + + + + + + + + Firma Digital sobre Transferencia + + + + + + + diff --git a/cl_sii/data/ref/factura_electronica/schemas-xml/Cesion_v10.xsd b/cl_sii/data/ref/factura_electronica/schemas-xml/Cesion_v10.xsd new file mode 100644 index 00000000..aef37cb1 --- /dev/null +++ b/cl_sii/data/ref/factura_electronica/schemas-xml/Cesion_v10.xsd @@ -0,0 +1,230 @@ + + + + + + + + + Envio de Informacion de Transferencias Electronicas + + + + + Documento Tributario Electronico + + + + + + + + Secuencia de Cesiones (1, 2, 3, ... ) + + + + + + + + + + Identificacion del DTE Cedido + + + + + + Tipo de DTE + + + + + RUT Emisor del DTE + + + + + RUT Receptor del DTE + + + + + Folio del DTE + + + + + Fecha Emision Contable del DTE (AAAA-MM-DD) + + + + + Monto Total del DTE + + + + + + + + Identificacion del Cedente + + + + + + RUT del Cedente del DTE + + + + + Razon Social o Nombre del Cedente + + + + + + + + + + Direccion del Cedente + + + + + + + + + + Correo Electronico del Cedente + + + + + + + + + + Lista de Personas Autorizadas por el Cedente a Firmar la Transferencia + + + + + + RUT de Persona Autorizada + + + + + Nombre de Persona Autorizada + + + + + + + + Declaracion Jurada de Disponibilidad de Documentacion No Electronica + + + + + + + + + + + + + Identificacion del Cesionario + + + + + + RUT del Cesionario + + + + + Razon Social o Nombre del Cesionario + + + + + + + + + + Direccion del Cesionario + + + + + + + + + + Correo Electronico del Cesionario + + + + + + + + + + + + + Monto del Credito Cedido + + + + + Fecha de Ultimo Vencimiento + + + + + Otras Condiciones de la Cesion + + + + + + + + + + Correo Electronico del Deudor del DTE + + + + + TimeStamp de la Cesion del DTE + + + + + + + + + Firmas Digitales sobre Cesion + + + + + + diff --git a/cl_sii/data/ref/factura_electronica/schemas-xml/DTECedido_v10.xsd b/cl_sii/data/ref/factura_electronica/schemas-xml/DTECedido_v10.xsd new file mode 100644 index 00000000..f81b34fa --- /dev/null +++ b/cl_sii/data/ref/factura_electronica/schemas-xml/DTECedido_v10.xsd @@ -0,0 +1,59 @@ + + + + + + + + + DTE con Imagen y Recibos + + + + + + + + + + Representacion XML del DTE Cedido + + + + + Representacion PDF del DTE Cedido + + + + + Informacion Electronica de Recepcion y Aceptacion del DTE por Parte del Receptor + + + + + Representacion PDF del los Acuse de Recibo + + + + + + + + + + Fecha y Hora en que se Firmo Digitalmente el Documento Cedido AAAA-MM-DDTHH:MI:SS + + + + + + + + + Firma Digital sobre Documento + + + + + + diff --git a/cl_sii/data/ref/factura_electronica/schemas-xml/README.md b/cl_sii/data/ref/factura_electronica/schemas-xml/README.md index 04c5d36e..18dce65f 100644 --- a/cl_sii/data/ref/factura_electronica/schemas-xml/README.md +++ b/cl_sii/data/ref/factura_electronica/schemas-xml/README.md @@ -17,6 +17,8 @@ The most significant structures are: Note: - DTE means "Documento Tributario Electr贸nico". +- RTC: "Registro Transferencia de Cr茅dito" aka RPETC; "Registro Electr贸nico de Cesi贸n de Cr茅ditos". +- RPETC: "Registro P煤blico Electr贸nico de Transferencia de Cr茅dito" aka RTC. - IECV means "Informaci贸n Electr贸nica de Libros de Compra y Venta". - LCE means "Libros Contables Electr贸nicos". @@ -41,6 +43,23 @@ as "[Bajar schema XML de Documentos Tributarios Electr贸nicos](http://www.sii.cl/factura_electronica/schema_dte.zip) (Incluye Documentos de exportaci贸n)" +#### Cesion (RTC) + +Archive [schema_cesion.zip](http://www.sii.cl/factura_electronica/schema_cesion.zip), +referenced from official webpage +[SII](http://www.sii.cl) +/ [Servicios online](http://www.sii.cl/servicios_online/index.html) +/ [Factura electr贸nica](http://www.sii.cl/servicios_online/1039-.html) +/ [Sistema de facturaci贸n de mercado](http://www.sii.cl/servicios_online/1039-1184.html) +/ [Registro electr贸nico de cesi贸n de cr茅ditos](https://palena.sii.cl/rtc/RTC/RTCMenu.html) +/ [Formatos de archivos electr贸nicos](http://www.sii.cl/factura_electronica/form_ele.htm) +as +"[Formato XML del Archivo Electr贸nico de Cesi贸n](http://www.sii.cl/factura_electronica/schema_cesion.zip)" + +- Retrieval date: 2019-04-16 +- MD5 checksum: `82d426fc3bd5f3a29e61a1d07ed4d6dd`. + + #### IECV [schema_iecv.zip](http://www.sii.cl/factura_electronica/schema_iecv.zip) (2018-11-28), @@ -137,6 +156,82 @@ Schema files will be updated as necessary, indicating the source in the correspo - `PctType`: "Monto de Porcentaje ( 3 y 2)". +#### Cesion (RTC) + +- `AEC_v10.xsd`: main schema; it includes (directly or indirectly) all the others of this section. + - XML target namespace: `http://www.sii.cl/SiiDte`. + - XML included/imported schemas: `Cesion_v10.xsd`, `DTECedido_v10.xsd`, `xmldsignature_v10.xsd`. + - XML elements: + - `AEC`: "Archivo Electronico de Cesion" + - `DocumentoAEC`: "Documento de AEC" + - `Caratula`: "Informacion de AEC" + - `Cesiones`: "Cesiones" + - ref `DTECedido`: "Representacion XML y Grafica del DTE Cedido" + - ref `Cesion` (1..N occurrences): + "Informacion Electronica de Recepcion y Aceptacion del DTE por Parte del Receptor" + - XML data types: no explicit definitions. + +- `Cesion_v10.xsd`: ? + - XML target namespace: `http://www.sii.cl/SiiDte`. + - XML included/imported schemas: `SiiTypes_v10.xsd`, `xmldsignature_v10.xsd`. + - XML elements: + - `Cesion`: "Envio de Informacion de Transferencias Electronicas". + - XML data types: + - `CesionDefType`: "Documento Tributario Electronico" (sic). + Relevant elements: + - `DocumentoCesion`: (no description nor annotations) + - `SeqCesion`: "Secuencia de Cesiones (1, 2, 3, ... )". + - `IdDTE`: "Identificacion del DTE Cedido". + - `Cedente`: "Identificacion del Cedente". + - `Cesionario`: "Identificacion del Cesionario". + - `MontoCesion`: "Monto del Credito Cedido". + - `UltimoVencimiento`: "Fecha de Ultimo Vencimiento". + - `OtrasCondiciones`: "Otras Condiciones de la Cesion". + - `eMailDeudor`: "Correo Electronico del Deudor del DTE". + - `TmstCesion`: "TimeStamp de la Cesion del DTE". + +- `DTECedido_v10.xsd`: ? + - XML target namespace: `http://www.sii.cl/SiiDte`. + - XML included/imported schemas: `DTE_v10.xsd`, `Recibos_v10.xsd`, `xmldsignature_v10.xsd`. + - XML elements: + - `DTECedido`: "DTE con Imagen y Recibos". + - XML data types: + - `DTECedidoDefType`: "Documento Tributario Electronico". + Relevant elements: + - `DocumentoDTECedido`: (no description nor annotations) + - ref `DTE`: "Representacion XML del DTE Cedido". + - `ImagenDTE` (optional): "Representacion PDF del DTE Cedido" (binary as base64) + - ref `Recibo` (0..N occurrences): + "Informacion Electronica de Recepcion y Aceptacion del DTE por Parte del Receptor". + - `ImagenAR` (optional): + "Representacion PDF del los Acuse de Recibo" (sic) (binary as base64) + - `TmstFirma`: + "Fecha y Hora en que se Firmo Digitalmente el Documento Cedido AAAA-MM-DDTHH:MI:SS". + +- `Recibos_v10.xsd`: ? + - XML target namespace: `http://www.sii.cl/SiiDte`. + - XML included/imported schemas: `SiiTypes_v10.xsd`, `xmldsignature_v10.xsd`. + - XML elements: + - `Recibo`: + doc 1: "Comprobante de Recepcion de Mercaderias o Servicios Prestados". + doc 2: "Recibos de Recepcion de Mercaderias o Servicios Prestados". + - XML data types: + - `ReciboDefType`: "Documento Tributario Electronico" (sic) + Relevant elements: + - `DocumentoRecibo`: "Identificacion del Documento Recibido" (sic) + - `TipoDoc`: "Tipo de Documento". + - `Folio`: "Folio del Documento". + - `FchEmis`: "Fecha Emision Contable del Documento (AAAA-MM-DD)". + - `RUTEmisor`: "RUT Emisor del Documento". + - `RUTRecep`: "RUT Receptor del Documento". + - `MntTotal`: "Monto Total del Documento". + - `Recinto`: "Lugar donde se materializa la recepci贸n conforme". + - `RutFirma`: "RUT de quien Firma el Recibo". + - `Declaracion` (fixed string): + "Texto Ley 19.983, acredita la recepcion mercader铆as o servicio.". + - `TmstFirmaRecibo`: "Fecha y Hora de la Firma del Recibo". + + #### IECV - `LceCal_v10.xsd` diff --git a/cl_sii/data/ref/factura_electronica/schemas-xml/Recibos_v10.xsd b/cl_sii/data/ref/factura_electronica/schemas-xml/Recibos_v10.xsd new file mode 100644 index 00000000..c2ec588e --- /dev/null +++ b/cl_sii/data/ref/factura_electronica/schemas-xml/Recibos_v10.xsd @@ -0,0 +1,95 @@ + + + + + + + + Comprobante de Recepcion de Mercaderias o Servicios Prestados + Recibos de Recepcion de Mercaderias o Servicios Prestados + + + + + Documento Tributario Electronico + + + + + Identificacion del Documento Recibido + + + + + + Tipo de Documento + + + + + Folio del Documento + + + + + Fecha Emision Contable del Documento (AAAA-MM-DD) + + + + + RUT Emisor del Documento + + + + + RUT Receptor del Documento + + + + + Monto Total del Documento + + + + + Lugar donde se materializa la recepci髇 conforme + + + + + + + + + + RUT de quien Firma el Recibo + + + + + Texto Ley 19.983, acredita la recepcion mercader韆s o servicio. + + + + + + + + + + Fecha y Hora de la Firma del Recibo + + + + + + + + + Firma Digital sobre Documento + + + + + + diff --git a/cl_sii/dte/constants.py b/cl_sii/dte/constants.py index 4b489c1e..34ec8c1d 100644 --- a/cl_sii/dte/constants.py +++ b/cl_sii/dte/constants.py @@ -72,7 +72,7 @@ class TipoDteEnum(enum.IntEnum): """ - Enum of Tipo de DTE. + Enum of "Tipo de DTE". Source: XML type ``DTEType`` (enum) in official schema ``SiiTypes_v10.xsd``. https://github.com/fyndata/lib-cl-sii-python/blob/f57a326/cl_sii/data/ref/factura_electronica/schemas-xml/SiiTypes_v10.xsd#L63-L99 @@ -80,21 +80,78 @@ class TipoDteEnum(enum.IntEnum): """ FACTURA_ELECTRONICA = 33 - """Factura Electr贸nica.""" + """Factura electr贸nica de venta.""" FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA = 34 - """Factura no Afecta o Exenta Electr贸nica.""" + """Factura electr贸nica de venta, no afecta o exenta de IVA.""" + # aka 'Factura no Afecta o Exenta Electr贸nica' # aka 'Factura Electr贸nica de Venta de Bienes y Servicios No afectos o Exento de IVA' FACTURA_COMPRA_ELECTRONICA = 46 - """Factura de Compra Electr贸nica.""" + """Factura electr贸nica de compra.""" + # aka 'Factura de Compra Electr贸nica' # Name should have been 'Factura Electr贸nica de Compra'. GUIA_DESPACHO_ELECTRONICA = 52 - """Gu铆a de Despacho Electr贸nica.""" + """Gu铆a electr贸nica de despacho.""" + # aka 'Gu铆a de Despacho Electr贸nica' NOTA_DEBITO_ELECTRONICA = 56 - """Nota de D茅bito Electr贸nica.""" + """Nota electr贸nica de d茅bito.""" + # aka 'Nota de D茅bito Electr贸nica' NOTA_CREDITO_ELECTRONICA = 61 - """Nota de Cr茅dito Electr贸nica.""" + """Nota electr贸nica de cr茅dito.""" + # aka 'Nota de Cr茅dito Electr贸nica' + + @property + def is_factura(self) -> bool: + if self is TipoDteEnum.FACTURA_ELECTRONICA: + result = True + elif self is TipoDteEnum.FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA: + result = True + elif self is TipoDteEnum.FACTURA_COMPRA_ELECTRONICA: + result = True + else: + result = False + + return result + + @property + def is_factura_venta(self) -> bool: + if self is TipoDteEnum.FACTURA_ELECTRONICA: + result = True + elif self is TipoDteEnum.FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA: + result = True + else: + result = False + + return result + + @property + def is_factura_compra(self) -> bool: + if self is TipoDteEnum.FACTURA_COMPRA_ELECTRONICA: + result = True + else: + result = False + + return result + + @property + def is_nota(self) -> bool: + if self is TipoDteEnum.NOTA_DEBITO_ELECTRONICA: + result = True + elif self is TipoDteEnum.NOTA_CREDITO_ELECTRONICA: + result = True + else: + result = False + + return result + + @property + def emisor_is_vendedor(self) -> bool: + return self.is_factura_venta + + @property + def receptor_is_vendedor(self) -> bool: + return self.is_factura_compra diff --git a/cl_sii/dte/data_models.py b/cl_sii/dte/data_models.py index fa7ac32d..4591cda5 100644 --- a/cl_sii/dte/data_models.py +++ b/cl_sii/dte/data_models.py @@ -1,10 +1,28 @@ +""" +DTE data models +=============== + +Concepts +-------- + +In the domain of a DTE, a: + +* "Vendedor": is who sold goods or services to "deudor" in a + transaction for which the DTE was issued. + It *usually* corresponds to the DTE's "emisor", but not always. +* "Deudor": is who purchased goods or services from "vendedor" in a + transaction for which the DTE was issued. + It *usually* corresponds to the DTE's "receptor", but not always. + +""" import dataclasses from dataclasses import field as dc_field -from datetime import date +from datetime import date, datetime from typing import Mapping, Optional import cl_sii.contribuyente.constants import cl_sii.rut.constants +from cl_sii.libs import encoding_utils from cl_sii.rut import Rut from . import constants @@ -57,6 +75,26 @@ def validate_contribuyente_razon_social(value: str) -> None: raise ValueError("Value exceeds max allowed length.") +def validate_clean_str(value: str) -> None: + if len(value.strip()) != len(value): + raise ValueError("Value has leading or trailing whitespace characters.", value) + + +def validate_clean_bytes(value: bytes) -> None: + if len(value.strip()) != len(value): + raise ValueError("Value has leading or trailing whitespace characters.", value) + + +def validate_non_empty_str(value: str) -> None: + if len(value.strip()) == 0: + raise ValueError("String value length (stripped) is 0.") + + +def validate_non_empty_bytes(value: bytes) -> None: + if len(value.strip()) == 0: + raise ValueError("Bytes value length (stripped) is 0.") + + @dataclasses.dataclass(frozen=True) class DteNaturalKey: @@ -230,8 +268,38 @@ def __post_init__(self) -> None: validate_dte_monto_total(self.monto_total) @property - def natural_key(self) -> DteNaturalKey: - return DteNaturalKey(emisor_rut=self.emisor_rut, tipo_dte=self.tipo_dte, folio=self.folio) + def vendedor_rut(self) -> Rut: + """ + Return the RUT of the "vendedor". + + :raises ValueError: + """ + if self.tipo_dte.emisor_is_vendedor: + result = self.emisor_rut + elif self.tipo_dte.receptor_is_vendedor: + result = self.receptor_rut + else: + raise ValueError( + "Concept \"vendedor\" does not apply for this 'tipo_dte'.", self.tipo_dte) + + return result + + @property + def deudor_rut(self) -> Rut: + """ + Return the RUT of the "deudor". + + :raises ValueError: + """ + if self.tipo_dte.emisor_is_vendedor: + result = self.receptor_rut + elif self.tipo_dte.receptor_is_vendedor: + result = self.emisor_rut + else: + raise ValueError( + "Concept \"deudor\" does not apply for this 'tipo_dte'.", self.tipo_dte) + + return result @dataclasses.dataclass(frozen=True) @@ -265,6 +333,38 @@ class DteDataL2(DteDataL1): "Fecha de vencimiento (pago)" of the DTE. """ + firma_documento_dt_naive: Optional[datetime] = dc_field(default=None) + """ + Datetime on which the "documento" was digitally signed. + """ + + signature_value: Optional[bytes] = dc_field(default=None) + """ + DTE's digital signature's value (raw bytes, without base64 encoding). + """ + + signature_x509_cert_pem: Optional[bytes] = dc_field(default=None) + """ + DTE's digital signature's PEM-encoded X.509 cert. + + PEM-encoded implies base64-encoded. + """ + + emisor_giro: Optional[str] = dc_field(default=None) + """ + "Giro" of the "emisor" of the DTE. + """ + + emisor_email: Optional[str] = dc_field(default=None) + """ + Email address of the "emisor" of the DTE. + """ + + receptor_email: Optional[str] = dc_field(default=None) + """ + Email address of the "receptor" of the DTE. + """ + def __post_init__(self) -> None: """ Run validation automatically after setting the fields values. @@ -285,3 +385,38 @@ def __post_init__(self) -> None: if self.fecha_vencimiento_date is not None: if not isinstance(self.fecha_vencimiento_date, date): raise TypeError("Inappropriate type of 'fecha_vencimiento_date'.") + + if self.firma_documento_dt_naive is not None: + if not isinstance(self.firma_documento_dt_naive, datetime): + raise TypeError("Inappropriate type of 'firma_documento_dt_naive'.") + + if self.signature_value is not None: + if not isinstance(self.signature_value, bytes): + raise TypeError("Inappropriate type of 'signature_value'.") + validate_clean_bytes(self.signature_value) + validate_non_empty_bytes(self.signature_value) + + if self.signature_x509_cert_pem is not None: + if not isinstance(self.signature_x509_cert_pem, bytes): + raise TypeError("Inappropriate type of 'signature_x509_cert_pem'.") + validate_clean_bytes(self.signature_x509_cert_pem) + validate_non_empty_bytes(self.signature_x509_cert_pem) + encoding_utils.validate_base64(self.signature_x509_cert_pem) + + if self.emisor_giro is not None: + if not isinstance(self.emisor_giro, str): + raise TypeError("Inappropriate type of 'emisor_giro'.") + validate_clean_str(self.emisor_giro) + validate_non_empty_str(self.emisor_giro) + + if self.emisor_email is not None: + if not isinstance(self.emisor_email, str): + raise TypeError("Inappropriate type of 'emisor_email'.") + validate_clean_str(self.emisor_email) + validate_non_empty_str(self.emisor_email) + + if self.receptor_email is not None: + if not isinstance(self.receptor_email, str): + raise TypeError("Inappropriate type of 'receptor_email'.") + validate_clean_str(self.receptor_email) + validate_non_empty_str(self.receptor_email) diff --git a/cl_sii/dte/parse.py b/cl_sii/dte/parse.py index aac22134..5cd5831f 100644 --- a/cl_sii/dte/parse.py +++ b/cl_sii/dte/parse.py @@ -20,13 +20,12 @@ import io import logging import os -from dataclasses import MISSING, _MISSING_TYPE -from datetime import date -from typing import Optional, Tuple, Union - -import lxml.etree +from datetime import date, datetime +from typing import Tuple +from cl_sii.libs import encoding_utils from cl_sii.libs import xml_utils +from cl_sii.libs.xml_utils import XmlElement, XmlElementTree from cl_sii.rut import Rut from . import constants from . import data_models @@ -72,10 +71,10 @@ ############################################################################### def clean_dte_xml( - xml_doc: lxml.etree.ElementBase, + xml_doc: XmlElement, set_missing_xmlns: bool = False, remove_doc_personalizado: bool = True, -) -> Tuple[lxml.etree.ElementBase, bool]: +) -> Tuple[XmlElement, bool]: """ Apply changes to ``xml_doc`` towards compliance to DTE XML schema. @@ -103,7 +102,7 @@ def clean_dte_xml( return xml_doc, modified -def validate_dte_xml(xml_doc: lxml.etree.ElementBase) -> None: +def validate_dte_xml(xml_doc: XmlElement) -> None: """ Validate ``xml_doc`` against DTE's XML schema. @@ -114,39 +113,371 @@ def validate_dte_xml(xml_doc: lxml.etree.ElementBase) -> None: xml_utils.validate_xml_doc(DTE_XML_SCHEMA_OBJ, xml_doc) -def parse_dte_xml(xml_doc: lxml.etree.ElementBase) -> data_models.DteDataL2: +# TODO: rename to 'parse_dte_xml_data' +def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: """ - Parse and deserialize DTE data from ``xml_doc``. + Parse data from a DTE XML doc. + + .. warning:: + It is assumed that ``xml_doc`` is an + ``{http://www.sii.cl/SiiDte}/DTE`` XML element. + + :raises ValueError: + :raises TypeError: + :raises NotImplementedError: """ + # TODO: change response type to a dataclass like 'DteXmlData'. # TODO: separate the XML parsing stage from the deserialization stage, which could be # performed by XML-agnostic code (perhaps using Marshmallow or data clacases?). # See :class:`cl_sii.rcv.parse.RcvCsvRowSchema`. - xml_element_root_tree = xml_doc.getroottree() - - obj_struct = data_models.DteDataL2( - emisor_rut=_get_emisor_rut(xml_element_root_tree), - tipo_dte=_get_tipo_dte(xml_element_root_tree), - folio=_get_folio(xml_element_root_tree), - fecha_emision_date=_get_fecha_emision(xml_element_root_tree), - receptor_rut=_get_receptor_rut(xml_element_root_tree), - monto_total=_get_monto_total(xml_element_root_tree), - emisor_razon_social=_get_emisor_razon_social(xml_element_root_tree), - receptor_razon_social=_get_receptor_razon_social(xml_element_root_tree), - fecha_vencimiento_date=_get_fecha_vencimiento(xml_element_root_tree, default=None), + if not isinstance(xml_doc, (XmlElement, XmlElementTree)): + raise TypeError("'xml_doc' must be an 'XmlElement'.") + + xml_em = xml_doc + + ########################################################################### + # XML elements finding + ########################################################################### + + # Schema requires one, and only one, of these: + # a) 'Documento' + # b) 'Liquidacion' + # c) 'Exportaciones' + documento_em = xml_em.find( + 'sii-dte:Documento', # "Informacion Tributaria del DTE" + namespaces=DTE_XMLNS_MAP) + liquidacion_em = xml_em.find( + 'sii-dte:Liquidacion', # "Informacion Tributaria de Liquidaciones" + namespaces=DTE_XMLNS_MAP) + exportaciones_em = xml_em.find( + 'sii-dte:Exportaciones', # "Informacion Tributaria de exportaciones" + namespaces=DTE_XMLNS_MAP) + signature_em = xml_em.find( + 'ds:Signature', # "Firma Digital sobre Documento" + namespaces=xml_utils.XML_DSIG_NS_MAP) + + if liquidacion_em is not None or exportaciones_em is not None: + raise NotImplementedError("XML element 'Documento' is the only one supported.") + + if documento_em is None: + raise ValueError("Top level XML element 'Document' is required.") + + # This value seems to be worthless (only useful for internal references in the XML doc). + # e.g. 'MiPE76354771-13419', 'MiPE76399752-6048' + # documento_em_id = documento_em.attrib['ID'] + + # 'Documento' + # Excluded elements (optional according to the XML schema but the SII may require some of these + # depending on 'tipo_dte' and other criteria): + # - 'Detalle': (occurrences: 0..60) + # "Detalle de Itemes del Documento" + # - 'SubTotInfo': (occurrences: 0..20) + # "Subtotales Informativos" + # - 'DscRcgGlobal': (occurrences: 0..20) + # "Descuentos y/o Recargos que afectan al total del Documento" + # - 'Referencia': (occurrences: 0..40) + # "Identificacion de otros documentos Referenciados por Documento" + # - 'Comisiones': (occurrences: 0..20) + # "Comisiones y otros cargos es obligatoria para Liquidaciones Factura" + encabezado_em = documento_em.find( + 'sii-dte:Encabezado', # "Identificacion y Totales del Documento" + namespaces=DTE_XMLNS_MAP) + # note: excluded because currently it is not useful. + # ted_em = documento_em.find( + # 'sii-dte:TED', # "Timbre Electronico de DTE" + # namespaces=DTE_XMLNS_MAP) + tmst_firma_em = documento_em.find( + 'sii-dte:TmstFirma', # "Fecha y Hora en que se Firmo Digitalmente el Documento" + namespaces=DTE_XMLNS_MAP) + + # 'Documento.Encabezado' + # Excluded elements (optional according to the XML schema but the SII may require some of these + # depending on 'tipo_dte' and other criteria): + # - 'RUTMandante': + # "RUT a Cuenta de Quien se Emite el DTE" + # - 'RUTSolicita': + # "RUT que solicita el DTE en Venta a Publico" + # - 'Transporte': + # "Informacion de Transporte de Mercaderias" + # - 'OtraMoneda': + # "Otra Moneda" + id_doc_em = encabezado_em.find( + 'sii-dte:IdDoc', # "Identificacion del DTE" + namespaces=DTE_XMLNS_MAP) + emisor_em = encabezado_em.find( + 'sii-dte:Emisor', # "Datos del Emisor" + namespaces=DTE_XMLNS_MAP) + receptor_em = encabezado_em.find( + 'sii-dte:Receptor', # "Datos del Receptor" + namespaces=DTE_XMLNS_MAP) + totales_em = encabezado_em.find( + 'sii-dte:Totales', # "Montos Totales del DTE" + namespaces=DTE_XMLNS_MAP) + + # 'Documento.Encabezado.IdDoc' + # Excluded elements (optional according to the XML schema but the SII may require some of these + # depending on 'tipo_dte' and other criteria): + # - 'IndNoRebaja': + # "Nota de Credito sin Derecho a Descontar Debito" + # - 'TipoDespacho': + # "Indica Modo de Despacho de los Bienes que Acompanan al DTE" + # - 'IndTraslado': + # "Incluido en Guias de Despacho para Especifiicar el Tipo de Traslado de Productos" + # - 'TpoImpresion': + # "Tipo de impresi贸n N (Normal) o T (Ticket)" + # - 'IndServicio': + # "Indica si Transaccion Corresponde a la Prestacion de un Servicio" + # - 'MntBruto': + # "Indica el Uso de Montos Brutos en Detalle" + # - 'TpoTranCompra': + # "Tipo de Transacci贸n para el comprador" + # - 'TpoTranVenta': + # "Tipo de Transacci贸n para el vendedor" + # - 'FmaPago': + # "Forma de Pago del DTE" + # - 'FmaPagExp': + # "Forma de Pago Exportaci贸n Tabla Formas de Pago de Aduanas" + # - 'FchCancel': + # "Fecha de Cancelacion del DTE" + # - 'MntCancel': + # "Monto Cancelado al emitirse el documento" + # - 'SaldoInsol': + # "Saldo Insoluto al emitirse el documento" + # - 'MntPagos': (occurrences: 0..30) + # "Tabla de Montos de Pago" + # - 'PeriodoDesde': + # "Periodo de Facturacion - Desde" + # - 'PeriodoHasta': + # "Periodo Facturacion - Hasta" + # - 'MedioPago': + # "Medio de Pago" + # - 'TpoCtaPago': + # "Tipo Cuenta de Pago" + # - 'NumCtaPago': + # "N煤mero de la cuenta del pago" + # - 'BcoPago': + # "Banco donde se realiza el pago" + # - 'TermPagoCdg': + # "Codigo del Termino de Pago Acordado" + # - 'TermPagoGlosa': + # "T茅rminos del Pago - glosa" + # - 'TermPagoDias': + # "Dias de Acuerdo al Codigo de Termino de Pago" + # (required): + tipo_dte_em = id_doc_em.find( + 'sii-dte:TipoDTE', # "Tipo de DTE" + namespaces=DTE_XMLNS_MAP) + folio_em = id_doc_em.find( + 'sii-dte:Folio', # "Folio del Documento Electronico" + namespaces=DTE_XMLNS_MAP) + fecha_emision_em = id_doc_em.find( + 'sii-dte:FchEmis', # "Fecha Emision Contable del DTE" + namespaces=DTE_XMLNS_MAP) + # (optional): + fecha_vencimiento_em = id_doc_em.find( + 'sii-dte:FchVenc', # "Fecha de Vencimiento del Pago" + namespaces=DTE_XMLNS_MAP) + + # 'Documento.Encabezado.Emisor' + # Excluded elements (optional according to the XML schema but the SII may require some of these + # depending on 'tipo_dte' and other criteria): + # - 'Telefono': (occurrences: 0..2) + # "Telefono Emisor" + # - 'Acteco': (occurrences: 0..4) + # "Codigo de Actividad Economica del Emisor Relevante para el DTE" + # - 'GuiaExport': + # "Emisor de una Gu铆a de despacho para Exportaci贸n" + # - 'Sucursal': + # "Sucursal que Emite el DTE" + # - 'CdgSIISucur': + # "Codigo de Sucursal Entregado por el SII" + # - 'DirOrigen': + # "Direccion de Origen" + # - 'CmnaOrigen': + # "Comuna de Origen" + # - 'CiudadOrigen': + # "Ciudad de Origen" + # - 'CdgVendedor': + # "Codigo del Vendedor" + # - 'IdAdicEmisor': + # "Identificador Adicional del Emisor" + # (required): + emisor_rut_em = emisor_em.find( + 'sii-dte:RUTEmisor', # "RUT del Emisor del DTE" + namespaces=DTE_XMLNS_MAP) + emisor_razon_social_em = emisor_em.find( + 'sii-dte:RznSoc', # "Nombre o Razon Social del Emisor" + namespaces=DTE_XMLNS_MAP) + emisor_giro_em = emisor_em.find( + 'sii-dte:GiroEmis', # "Giro Comercial del Emisor Relevante para el DTE" + namespaces=DTE_XMLNS_MAP) + # (optional): + emisor_email_em = emisor_em.find( + 'sii-dte:CorreoEmisor', # "Correo Elect. de contacto en empresa del receptor" (wrong!) + namespaces=DTE_XMLNS_MAP) + + # 'Documento.Encabezado.Receptor' + # Excluded elements (optional according to the XML schema but the SII may require some of these + # depending on 'tipo_dte' and other criteria): + # - 'CdgIntRecep': + # "Codigo Interno del Receptor" + # - 'Extranjero': + # "Receptor Extranjero" + # - 'GiroRecep': + # "Giro Comercial del Receptor" + # - 'Contacto': + # "Telefono o E-mail de Contacto del Receptor" + # - 'CorreoRecep': + # "Correo Elect. de contacto en empresa del receptor" + # - 'DirRecep': + # "Direccion en la Cual se Envian los Productos o se Prestan los Servicios" + # - 'CmnaRecep': + # "Comuna de Recepcion" + # - 'CiudadRecep': + # "Ciudad de Recepcion" + # - 'DirPostal': + # "Direccion Postal" + # - 'CmnaPostal': + # "Comuna Postal" + # - 'CiudadPostal': + # "Ciudad Postal" + # (required): + receptor_rut_em = receptor_em.find( + 'sii-dte:RUTRecep', # "RUT del Receptor del DTE" + namespaces=DTE_XMLNS_MAP) + receptor_razon_social_em = receptor_em.find( + 'sii-dte:RznSocRecep', # "Nombre o Razon Social del Receptor" + namespaces=DTE_XMLNS_MAP) + # (optional): + receptor_email_em = emisor_em.find( + 'sii-dte:CorreoRecep', # "Correo Elect. de contacto en empresa del receptor" + namespaces=DTE_XMLNS_MAP) + + # 'Documento.Encabezado.Totales' + # Excluded elements (optional according to the XML schema but the SII may require some of these + # depending on 'tipo_dte' and other criteria): + # - 'MntNeto': + # "Monto Neto del DTE" + # - 'MntExe': + # "Monto Exento del DTE" + # - 'MntBase': + # "Monto Base Faenamiento Carne" (???) + # - 'MntMargenCom': + # "Monto Base de M谩rgenes de Comercializaci贸n. Monto informado" + # - 'TasaIVA': + # "Tasa de IVA" (percentage) + # - 'IVA': + # "Monto de IVA del DTE" + # - 'IVAProp': + # "Monto del IVA propio" + # - 'IVATerc': + # "Monto del IVA de Terceros" + # - 'ImptoReten': (occurrences: 0..20) + # "Impuestos y Retenciones Adicionales" + # - 'IVANoRet': + # "IVA No Retenido" + # - 'CredEC': + # "Credito Especial Empresas Constructoras" + # - 'GrntDep': + # "Garantia por Deposito de Envases o Embalajes" + # - 'Comisiones': + # "Comisiones y otros cargos es obligatoria para Liquidaciones Factura" + # - 'MontoNF': + # "Monto No Facturable - Corresponde a Bienes o Servicios Facturados Previamente" + # - 'MontoPeriodo': + # "Total de Ventas o Servicios del Periodo" + # - 'SaldoAnterior': + # "Saldo Anterior - Puede ser Negativo o Positivo" + # - 'VlrPagar': + # "Valor a Pagar Total del documento" + monto_total_em = totales_em.find( + 'sii-dte:MntTotal', # "Monto Total del DTE" + namespaces=DTE_XMLNS_MAP) + + # 'Signature' + # signature_signed_info_em = signature_em.find( + # 'ds:SignedInfo', # "Descripcion de la Informacion Firmada y del Metodo de Firma" + # namespaces=xml_utils.XML_DSIG_NS_MAP) + # signature_signed_info_canonicalization_method_em = signature_signed_info_em.find( + # 'ds:CanonicalizationMethod', # "Algoritmo de Canonicalizacion" + # namespaces=xml_utils.XML_DSIG_NS_MAP) + # signature_signed_info_signature_method_em = signature_signed_info_em.find( + # 'ds:SignatureMethod', # "Algoritmo de Firma" + # namespaces=xml_utils.XML_DSIG_NS_MAP) + # signature_signed_info_reference_em = signature_signed_info_em.find( + # 'ds:Reference', # "Referencia a Elemento Firmado" + # namespaces=xml_utils.XML_DSIG_NS_MAP) + signature_signature_value_em = signature_em.find( + 'ds:SignatureValue', # "Valor de la Firma Digital" + namespaces=xml_utils.XML_DSIG_NS_MAP) + signature_key_info_em = signature_em.find( + 'ds:KeyInfo', # "Informacion de Claves Publicas y Certificado" + namespaces=xml_utils.XML_DSIG_NS_MAP) + # signature_key_info_key_value_em = signature_key_info_em.find( + # 'ds:KeyValue', + # namespaces=xml_utils.XML_DSIG_NS_MAP) + signature_key_info_x509_data_em = signature_key_info_em.find( + 'ds:X509Data', # "Informacion del Certificado Publico" + namespaces=xml_utils.XML_DSIG_NS_MAP) + signature_key_info_x509_cert_em = signature_key_info_x509_data_em.find( + 'ds:X509Certificate', # "Certificado Publico" + namespaces=xml_utils.XML_DSIG_NS_MAP) + + ########################################################################### + # values parsing + ########################################################################### + + tipo_dte_value = constants.TipoDteEnum(int(tipo_dte_em.text.strip())) + folio_value = int(folio_em.text.strip()) + fecha_emision_value = date.fromisoformat(fecha_emision_em.text.strip()) + fecha_vencimiento_value = None + if fecha_vencimiento_em is not None: + fecha_vencimiento_value = date.fromisoformat(fecha_vencimiento_em.text.strip()) + + emisor_rut_value = Rut(emisor_rut_em.text.strip()) + emisor_razon_social_value = emisor_razon_social_em.text.strip() + emisor_giro_value = emisor_giro_em.text.strip() + emisor_email_value = emisor_email_em.text.strip() if emisor_email_em is not None else None + + receptor_rut_value = Rut(receptor_rut_em.text.strip()) + receptor_razon_social_value = receptor_razon_social_em.text.strip() + receptor_email_value = receptor_email_em.text.strip() if receptor_email_em is not None else None + + monto_total_value = int(monto_total_em.text.strip()) + + tmst_firma_value = datetime.fromisoformat(tmst_firma_em.text) + + signature_signature_value = encoding_utils.decode_base64_strict( + signature_signature_value_em.text.strip()) + signature_key_info_x509_cert_pem = encoding_utils.clean_base64( + signature_key_info_x509_cert_em.text.strip()) + + return data_models.DteDataL2( + emisor_rut=emisor_rut_value, + tipo_dte=tipo_dte_value, + folio=folio_value, + fecha_emision_date=fecha_emision_value, + receptor_rut=receptor_rut_value, + monto_total=monto_total_value, + emisor_razon_social=emisor_razon_social_value, + receptor_razon_social=receptor_razon_social_value, + fecha_vencimiento_date=fecha_vencimiento_value, + firma_documento_dt_naive=tmst_firma_value, + signature_value=signature_signature_value, + signature_x509_cert_pem=signature_key_info_x509_cert_pem, + emisor_giro=emisor_giro_value, + emisor_email=emisor_email_value, + receptor_email=receptor_email_value, ) - return obj_struct - ############################################################################### # helpers ############################################################################### -def _set_dte_xml_missing_xmlns( - xml_doc: lxml.etree.ElementBase, -) -> Tuple[lxml.etree.ElementBase, bool]: +def _set_dte_xml_missing_xmlns(xml_doc: XmlElement) -> Tuple[XmlElement, bool]: # source: name of the XML element without namespace. # cl_sii/data/ref/factura_electronica/schemas-xml/DTE_v10.xsd#L22 (f57a326) @@ -180,9 +511,7 @@ def _set_dte_xml_missing_xmlns( return xml_doc, modified -def _remove_dte_xml_doc_personalizado( - xml_doc: lxml.etree.ElementBase, -) -> Tuple[lxml.etree.ElementBase, bool]: +def _remove_dte_xml_doc_personalizado(xml_doc: XmlElement) -> Tuple[XmlElement, bool]: # Remove non-standard but popular element 'DocPersonalizado', it if exists. modified = False @@ -194,96 +523,3 @@ def _remove_dte_xml_doc_personalizado( xml_doc.remove(xml_em) return xml_doc, modified - - -def _get_tipo_dte(xml_etree: lxml.etree.ElementTree) -> constants.TipoDteEnum: - em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:IdDoc/sii-dte:TipoDTE' - - value_str = xml_etree.findtext(em_path, namespaces=DTE_XMLNS_MAP) - if value_str is None: - raise Exception("Element 'TipoDTE' was not found in the XML document.") - return constants.TipoDteEnum(int(value_str)) - - -def _get_folio(xml_etree: lxml.etree.ElementTree) -> int: - em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:IdDoc/sii-dte:Folio' - - value_str = xml_etree.findtext(em_path, namespaces=DTE_XMLNS_MAP) - if value_str is None: - raise Exception("Element 'Folio' was not found in the XML document.") - return int(value_str) - - -def _get_fecha_emision(xml_etree: lxml.etree.ElementTree) -> date: - em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:IdDoc/sii-dte:FchEmis' - - value_str = xml_etree.findtext(em_path, namespaces=DTE_XMLNS_MAP) - if value_str is None: - raise Exception("Element 'FchEmis' was not found in the XML document.") - return date.fromisoformat(value_str) - - -def _get_fecha_vencimiento( - xml_etree: lxml.etree.ElementTree, - default: Union[date, None, _MISSING_TYPE] = MISSING, -) -> Optional[date]: - - em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:IdDoc/sii-dte:FchVenc' - - value_str = xml_etree.findtext(em_path, namespaces=DTE_XMLNS_MAP) - if value_str is None: - if default is None or isinstance(default, date): - value = default - elif default is MISSING: - raise Exception("Element 'FchVenc' was not found in the XML document.") - else: - raise TypeError("Invalid type of 'default'.") - else: - value = date.fromisoformat(value_str) - - return value - - -def _get_emisor_rut(xml_etree: lxml.etree.ElementTree) -> Rut: - em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:Emisor/sii-dte:RUTEmisor' - - value_str = xml_etree.findtext(em_path, namespaces=DTE_XMLNS_MAP) - if value_str is None: - raise Exception("Element 'RUTEmisor' was not found in the XML document.") - return Rut(value_str) - - -def _get_emisor_razon_social(xml_etree: lxml.etree.ElementTree) -> str: - em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:Emisor/sii-dte:RznSoc' - - value_str: str = xml_etree.findtext(em_path, namespaces=DTE_XMLNS_MAP) - if value_str is None: - raise Exception("Element 'RznSoc' was not found in the XML document.") - return value_str - - -def _get_receptor_rut(xml_etree: lxml.etree.ElementTree) -> Rut: - em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:Receptor/sii-dte:RUTRecep' - - value_str = xml_etree.findtext(em_path, namespaces=DTE_XMLNS_MAP) - if value_str is None: - raise Exception("Element 'RUTRecep' was not found in the XML document.") - return Rut(value_str) - - -def _get_receptor_razon_social(xml_etree: lxml.etree.ElementTree) -> str: - em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:Receptor/sii-dte:RznSocRecep' - - value_str: str = xml_etree.findtext(em_path, namespaces=DTE_XMLNS_MAP) - if value_str is None: - raise Exception("Element 'RznSocRecep' was not found in the XML document.") - return value_str - - -def _get_monto_total(xml_etree: lxml.etree.ElementTree) -> int: - em_path = 'sii-dte:Documento/sii-dte:Encabezado/sii-dte:Totales/sii-dte:MntTotal' - - value_str = xml_etree.findtext(em_path, namespaces=DTE_XMLNS_MAP) - if value_str is None: - raise Exception("Element 'MntTotal' was not found in the XML document.") - return int(value_str) diff --git a/cl_sii/libs/crypto_utils.py b/cl_sii/libs/crypto_utils.py new file mode 100644 index 00000000..00452e1d --- /dev/null +++ b/cl_sii/libs/crypto_utils.py @@ -0,0 +1,61 @@ +from typing import Union + +import cryptography.x509 +import signxml.util +from cryptography.hazmat.backends.openssl import backend as _crypto_x509_backend +from cryptography.x509 import Certificate as X509Cert +from OpenSSL.crypto import X509 as _X509CertOpenSsl # noqa: F401 + + +def load_pem_x509_cert(pem_value: Union[str, bytes]) -> X509Cert: + """ + Load an X.509 certificate from a PEM-formatted value. + + .. seealso:: + https://cryptography.io/en/latest/faq/#why-can-t-i-import-my-pem-file + + :raises TypeError: + :raises ValueError: + + """ + if isinstance(pem_value, str): + pem_value_bytes = pem_value.encode('ascii') + elif isinstance(pem_value, bytes): + pem_value_bytes = pem_value + else: + raise TypeError("Value must be str or bytes.") + + mod_pem_value_bytes = add_pem_cert_header_footer(pem_value_bytes) + try: + x509_cert = cryptography.x509.load_pem_x509_certificate( + data=mod_pem_value_bytes, + backend=_crypto_x509_backend) + except ValueError: + # e.g. + # "Unable to load certificate. See + # https://cryptography.io/en/latest/faq/#why-can-t-i-import-my-pem-file for more details." + raise + + return x509_cert + + +def add_pem_cert_header_footer(pem_cert: bytes) -> bytes: + """ + Add certificate PEM header and footer (if not already present). + """ + pem_value_str = pem_cert.decode('ascii') + # note: it would be great if 'add_pem_header' did not forcefully convert bytes to str. + mod_pem_value_str = signxml.util.add_pem_header(pem_value_str) + mod_pem_value: bytes = mod_pem_value_str.encode('ascii') + return mod_pem_value + + +def remove_pem_cert_header_footer(pem_cert: bytes) -> bytes: + """ + Remove certificate PEM header and footer (if they are present). + """ + pem_value_str = pem_cert.decode('ascii') + # note: it would be great if 'strip_pem_header' did not expect input to be a str. + mod_pem_value_str = signxml.util.strip_pem_header(pem_value_str) + mod_pem_value: bytes = mod_pem_value_str.encode('ascii').strip() + return mod_pem_value diff --git a/cl_sii/libs/encoding_utils.py b/cl_sii/libs/encoding_utils.py new file mode 100644 index 00000000..9a6adb14 --- /dev/null +++ b/cl_sii/libs/encoding_utils.py @@ -0,0 +1,55 @@ +import base64 +import binascii +from typing import Union + + +def clean_base64(value: Union[str, bytes]) -> bytes: + """ + Force bytes and remove line breaks and spaces. + + Does not validate base64 format. + + :raises ValueError: + :raises TypeError: + + """ + if isinstance(value, bytes): + value_base64_bytes = value + elif isinstance(value, str): + try: + value_base64_bytes = value.strip().encode(encoding='ascii', errors='strict') + except UnicodeEncodeError as exc: + raise ValueError("Only ASCII characters are accepted.", str(exc)) from exc + else: + raise TypeError("Value must be str or bytes.") + + # remove line breaks and spaces + value_base64_bytes_cleaned = value_base64_bytes.replace(b'\n', b'').replace(b' ', b'') + + return value_base64_bytes_cleaned + + +def decode_base64_strict(value: Union[str, bytes]) -> bytes: + """ + Strict conversion for str/bytes, tolerating only line breaks and spaces. + + :raises ValueError: non-base64 input or non-ASCII characters included + + """ + value_base64_bytes_cleaned = clean_base64(value) + try: + value_bytes = base64.b64decode(value_base64_bytes_cleaned, validate=True) + except binascii.Error as exc: + raise ValueError("Input is not a valid base64 value.", str(exc)) from exc + return value_bytes + + +def validate_base64(value: Union[str, bytes]) -> None: + """ + Validate that ``value`` is base64-encoded data. + + :raises ValueError: + :raises TypeError: + + """ + decode_base64_strict(value) diff --git a/cl_sii/libs/xml_utils.py b/cl_sii/libs/xml_utils.py index 83a9d69e..6ed10205 100644 --- a/cl_sii/libs/xml_utils.py +++ b/cl_sii/libs/xml_utils.py @@ -1,3 +1,24 @@ +""" +XML utils +========= + + +XML (Digital) Signature +----------------------- + +a.k.a. 'XMLDSig', 'XML-DSig', XML-Sig' + +XML Signature [..] defines an XML syntax for digital signatures and is +defined in the W3C recommendation "XML Signature Syntax and Processing" +(``xmldsig-core``). Functionally, it has much in common with ``PKCS#7 `` +but is more extensible and geared towards signing XML documents. +It is used by various Web technologies such as SOAP, SAML, and others. + +.. seealso:: + https://en.wikipedia.org/wiki/XML_Signature + + +""" import logging import os from typing import IO @@ -7,11 +28,33 @@ import lxml.etree import xml.parsers.expat import xml.parsers.expat.errors +from lxml.etree import ElementBase as XmlElement # noqa: F401 +# note: 'lxml.etree.ElementTree' is a **function**, not a class. +from lxml.etree import _ElementTree as XmlElementTree # noqa: F401 +from lxml.etree import XMLSchema as XmlSchema # noqa: F401 logger = logging.getLogger(__name__) +XML_DSIG_NS_MAP = dict( + ds='http://www.w3.org/2000/09/xmldsig#', + dsig11='http://www.w3.org/2009/xmldsig11#', + dsig2='http://www.w3.org/2010/xmldsig2#', + ec='http://www.w3.org/2001/10/xml-exc-c14n#', + dsig_more='http://www.w3.org/2001/04/xmldsig-more#', + xenc='http://www.w3.org/2001/04/xmlenc#', + xenc11='http://www.w3.org/2009/xmlenc11#', +) +""" +Mapping from XML namespace prefix to full name, for XML Signature. + +Source: +``signxml.namespaces`` @ 16503242 (~ v2.6.0) +https://github.com/XML-Security/signxml/blob/16503242/signxml/__init__.py#L23-L31 +""" + + ############################################################################### # exceptions ############################################################################### @@ -72,7 +115,7 @@ class XmlSchemaDocValidationError(Exception): # functions ############################################################################### -def parse_untrusted_xml(value: bytes) -> lxml.etree.ElementBase: +def parse_untrusted_xml(value: bytes) -> XmlElement: """ Parse XML-encoded content in value. @@ -115,7 +158,7 @@ def parse_untrusted_xml(value: bytes) -> lxml.etree.ElementBase: base_url=None, # default: None forbid_dtd=False, # default: False (allow Document Type Definition) forbid_entities=True, # default: True (forbid Entity definitions/declarations) - ) # type: lxml.etree.ElementBase + ) # type: XmlElement except (defusedxml.DTDForbidden, defusedxml.EntitiesForbidden, @@ -192,7 +235,7 @@ def parse_untrusted_xml(value: bytes) -> lxml.etree.ElementBase: return xml_root_em -def read_xml_schema(filename: str) -> lxml.etree.XMLSchema: +def read_xml_schema(filename: str) -> XmlSchema: """ Instantiate an XML schema object from a file. @@ -200,11 +243,11 @@ def read_xml_schema(filename: str) -> lxml.etree.XMLSchema: """ if os.path.exists(filename) and os.path.isfile(filename): - return lxml.etree.XMLSchema(file=filename) + return XmlSchema(file=filename) raise ValueError("XML schema file not found.", filename) -def validate_xml_doc(xml_schema: lxml.etree.XMLSchema, xml_doc: lxml.etree.ElementBase) -> None: +def validate_xml_doc(xml_schema: XmlSchema, xml_doc: XmlElement) -> None: """ Validate ``xml_doc`` against XML schema ``xml_schema``. @@ -240,7 +283,7 @@ def validate_xml_doc(xml_schema: lxml.etree.XMLSchema, xml_doc: lxml.etree.Eleme raise XmlSchemaDocValidationError(validation_error_msg) from exc -def write_xml_doc(xml_doc: lxml.etree.ElementBase, output: IO[bytes]) -> None: +def write_xml_doc(xml_doc: XmlElement, output: IO[bytes]) -> None: """ Write ``xml_doc`` to bytes stream ``output``. @@ -264,7 +307,7 @@ def write_xml_doc(xml_doc: lxml.etree.ElementBase, output: IO[bytes]) -> None: # note: use `IO[X]` for arguments and `TextIO`/`BinaryIO` for return types (says GVR). # https://github.com/python/typing/issues/518#issuecomment-350903120 - xml_etree: lxml.etree.ElementTree = xml_doc.getroottree() + xml_etree: XmlElementTree = xml_doc.getroottree() # See: # https://lxml.de/api/lxml.etree._ElementTree-class.html#write diff --git a/requirements/base.txt b/requirements/base.txt index 0cb7cfe3..ac889be3 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -2,10 +2,33 @@ # note: it is mandatory to register all dependencies of the required packages. # Required packages: +cryptography==2.6.1 defusedxml==0.5.0 -lxml==4.2.5 +lxml==4.2.6 marshmallow==2.16.3 -pytz==2018.9 +pyOpenSSL==18.0.0 +pytz==2019.1 +signxml==2.6.0 # Packages dependencies: -#none +# - cryptography: +# - asn1crypto +# - cffi: +# - pycparser +# - six +# - signxml: +# - certifi +# - cryptography +# - defusedxml +# - eight +# - future +# - lxml +# - pyOpenSSL +# - six +asn1crypto==0.24.0 +certifi==2019.3.9 +cffi==1.12.3 +eight==0.4.2 +future==0.16.0 +pycparser==2.19 +six==1.12.0 diff --git a/requirements/extras.txt b/requirements/extras.txt index d0e3ad4f..a59af561 100644 --- a/requirements/extras.txt +++ b/requirements/extras.txt @@ -2,4 +2,4 @@ # Required packages: Django<2.2 -djangorestframework<3.9 +djangorestframework<3.10 diff --git a/requirements/release.txt b/requirements/release.txt index 49ca0a96..db36ef42 100644 --- a/requirements/release.txt +++ b/requirements/release.txt @@ -4,7 +4,7 @@ # Required packages: bumpversion==0.5.3 -setuptools==40.8.0 +setuptools==41.0.1 twine==1.13.0 wheel==0.33.1 diff --git a/requirements/test.txt b/requirements/test.txt index 5fb90db5..daefd728 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -13,6 +13,7 @@ tox==3.7.0 # - coverage # - requests # - flake8: +# - entrypoints # - mccabe # - pycodestyle # - pyflakes @@ -25,13 +26,14 @@ tox==3.7.0 # - py # - toml # - virtualenv +entrypoints==0.3 filelock==3.0.10 mccabe==0.6.1 mypy-extensions==0.4.1 pluggy==0.9.0 py==1.8.0 pycodestyle==2.5.0 -pyflakes==2.1.0 +pyflakes==2.1.1 toml==0.10.0 -typed-ast==1.3.1 -virtualenv==16.4.3 +typed-ast==1.3.4 +virtualenv==16.5.0 diff --git a/setup.cfg b/setup.cfg index 79258364..c6360984 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,9 @@ disallow_untyped_defs = True check_untyped_defs = True warn_return_any = True +[mypy-cryptography.*] +ignore_missing_imports = True + [mypy-defusedxml.*] ignore_missing_imports = True @@ -39,6 +42,12 @@ ignore_missing_imports = True [mypy-marshmallow.*] ignore_missing_imports = True +[mypy-OpenSSL.*] +ignore_missing_imports = True + +[mypy-signxml.*] +ignore_missing_imports = True + [mypy-rest_framework.*] ignore_missing_imports = True diff --git a/setup.py b/setup.py index d8cd39da..5e9d2663 100755 --- a/setup.py +++ b/setup.py @@ -23,10 +23,13 @@ def get_version(*file_paths: Sequence[str]) -> str: # TODO: add reasonable upper-bound for some of these packages? requirements = [ + 'cryptography>=2.6.1', 'defusedxml>=0.5.0', - 'lxml>=4.2.5', + 'lxml>=4.2.6', 'marshmallow>=2.16.3', + 'pyOpenSSL>=18.0.0', 'pytz>=2018.7', + 'signxml>=2.6.0', ] extras_requirements = { diff --git a/tests/test_data/crypto/wildcard-google-com-cert.pem b/tests/test_data/crypto/wildcard-google-com-cert.pem new file mode 100644 index 00000000..7fa3f3cf --- /dev/null +++ b/tests/test_data/crypto/wildcard-google-com-cert.pem @@ -0,0 +1,46 @@ +-----BEGIN CERTIFICATE----- +MIIIDTCCBvWgAwIBAgIQXD9eCvh/44P1ET5RI1LuJjANBgkqhkiG9w0BAQsFADBU +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMSUw +IwYDVQQDExxHb29nbGUgSW50ZXJuZXQgQXV0aG9yaXR5IEczMB4XDTE5MDMyNjEz +NDA0MFoXDTE5MDYxODEzMjQwMFowZjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNh +bGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxEzARBgNVBAoMCkdvb2ds +ZSBMTEMxFTATBgNVBAMMDCouZ29vZ2xlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49 +AwEHA0IABANpWSLXLbJm5eRzc1EJmvSIbz0nANT+b11r+XhSUCAbfQhS+4M/91YJ +gVE6UtZJrLO7GGxvp1tV/DL857NaLEWjggWSMIIFjjATBgNVHSUEDDAKBggrBgEF +BQcDATAOBgNVHQ8BAf8EBAMCB4AwggRXBgNVHREEggROMIIESoIMKi5nb29nbGUu +Y29tgg0qLmFuZHJvaWQuY29tghYqLmFwcGVuZ2luZS5nb29nbGUuY29tghIqLmNs +b3VkLmdvb2dsZS5jb22CGCouY3Jvd2Rzb3VyY2UuZ29vZ2xlLmNvbYIGKi5nLmNv +gg4qLmdjcC5ndnQyLmNvbYIKKi5nZ3BodC5jboIWKi5nb29nbGUtYW5hbHl0aWNz +LmNvbYILKi5nb29nbGUuY2GCCyouZ29vZ2xlLmNsgg4qLmdvb2dsZS5jby5pboIO +Ki5nb29nbGUuY28uanCCDiouZ29vZ2xlLmNvLnVrgg8qLmdvb2dsZS5jb20uYXKC +DyouZ29vZ2xlLmNvbS5hdYIPKi5nb29nbGUuY29tLmJygg8qLmdvb2dsZS5jb20u +Y2+CDyouZ29vZ2xlLmNvbS5teIIPKi5nb29nbGUuY29tLnRygg8qLmdvb2dsZS5j +b20udm6CCyouZ29vZ2xlLmRlggsqLmdvb2dsZS5lc4ILKi5nb29nbGUuZnKCCyou +Z29vZ2xlLmh1ggsqLmdvb2dsZS5pdIILKi5nb29nbGUubmyCCyouZ29vZ2xlLnBs +ggsqLmdvb2dsZS5wdIISKi5nb29nbGVhZGFwaXMuY29tgg8qLmdvb2dsZWFwaXMu +Y26CESouZ29vZ2xlY25hcHBzLmNughQqLmdvb2dsZWNvbW1lcmNlLmNvbYIRKi5n +b29nbGV2aWRlby5jb22CDCouZ3N0YXRpYy5jboINKi5nc3RhdGljLmNvbYISKi5n +c3RhdGljY25hcHBzLmNuggoqLmd2dDEuY29tggoqLmd2dDIuY29tghQqLm1ldHJp +Yy5nc3RhdGljLmNvbYIMKi51cmNoaW4uY29tghAqLnVybC5nb29nbGUuY29tghYq +LnlvdXR1YmUtbm9jb29raWUuY29tgg0qLnlvdXR1YmUuY29tghYqLnlvdXR1YmVl +ZHVjYXRpb24uY29tghEqLnlvdXR1YmVraWRzLmNvbYIHKi55dC5iZYILKi55dGlt +Zy5jb22CGmFuZHJvaWQuY2xpZW50cy5nb29nbGUuY29tggthbmRyb2lkLmNvbYIb +ZGV2ZWxvcGVyLmFuZHJvaWQuZ29vZ2xlLmNughxkZXZlbG9wZXJzLmFuZHJvaWQu +Z29vZ2xlLmNuggRnLmNvgghnZ3BodC5jboIGZ29vLmdsghRnb29nbGUtYW5hbHl0 +aWNzLmNvbYIKZ29vZ2xlLmNvbYIPZ29vZ2xlY25hcHBzLmNughJnb29nbGVjb21t +ZXJjZS5jb22CGHNvdXJjZS5hbmRyb2lkLmdvb2dsZS5jboIKdXJjaGluLmNvbYIK +d3d3Lmdvby5nbIIIeW91dHUuYmWCC3lvdXR1YmUuY29tghR5b3V0dWJlZWR1Y2F0 +aW9uLmNvbYIPeW91dHViZWtpZHMuY29tggV5dC5iZTBoBggrBgEFBQcBAQRcMFow +LQYIKwYBBQUHMAKGIWh0dHA6Ly9wa2kuZ29vZy9nc3IyL0dUU0dJQUczLmNydDAp +BggrBgEFBQcwAYYdaHR0cDovL29jc3AucGtpLmdvb2cvR1RTR0lBRzMwHQYDVR0O +BBYEFM8C2hpNgJL/BEX/yzeB408dhba2MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgw +FoAUd8K4UJpndnaxLcKG0IOgfqZ+ukswIQYDVR0gBBowGDAMBgorBgEEAdZ5AgUD +MAgGBmeBDAECAjAxBgNVHR8EKjAoMCagJKAihiBodHRwOi8vY3JsLnBraS5nb29n +L0dUU0dJQUczLmNybDANBgkqhkiG9w0BAQsFAAOCAQEAF9PM41ShwCbhtJG7tj2y +ZvF2sHbQ5YuZrMfJc6eeCG+nCKm1U5iJzXnXctFGvfJnUCZpj9YrfwDswdEddWyZ +IG6m6wONF3ZiQifQrcDi0oDA+0BwjEuzYGCGkbfE+Xxb30bVEyDRe51DpJf+cqsb ++DW2pYdikbdrPem5/hwdNerc7nqrQOJ93sqwbVNGktuyJsTOGNKkSwSaejxdN7yl +g5aa4CJsE94gy4+mCywWjnnsjcLGJM3RBUxDdAdTGMldU/r33HCUCXl33Qxc4nvP +MlE9LyFOTIJoajWcpGOsbKWiL3Zr19DKNBSn4Xof0onbtCH7dbpyMwP8XcA2O1dA +ow== +-----END CERTIFICATE----- diff --git a/tests/test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem b/tests/test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem new file mode 100644 index 00000000..1271a9f7 --- /dev/null +++ b/tests/test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIGVDCCBTygAwIBAgIKMUWmvgAAAAjUHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx +HTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREwDwYDVQQHEwhTYW50aWFnbzEUMBIGA1UE +ChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsTF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMTAwLgYDVQQD +EydFLUNFUlRDSElMRSBDQSBGSVJNQSBFTEVDVFJPTklDQSBTSU1QTEUxJzAlBgkqhkiG9w0BCQEW +GHNjbGllbnRlc0BlLWNlcnRjaGlsZS5jbDAeFw0xNzA5MDQyMTExMTJaFw0yMDA5MDMyMTExMTJa +MIHXMQswCQYDVQQGEwJDTDEUMBIGA1UECBMLVkFMUEFSQUlTTyAxETAPBgNVBAcTCFF1aWxsb3Rh +MS8wLQYDVQQKEyZTZXJ2aWNpb3MgQm9uaWxsYSB5IExvcGV6IHkgQ2lhLiBMdGRhLjEkMCIGA1UE +CwwbSW5nZW5pZXLDrWEgeSBDb25zdHJ1Y2Npw7NuMSMwIQYDVQQDExpSYW1vbiBodW1iZXJ0byBM +b3BleiAgSmFyYTEjMCEGCSqGSIb3DQEJARYUZW5hY29ubHRkYUBnbWFpbC5jb20wgZ8wDQYJKoZI +hvcNAQEBBQADgY0AMIGJAoGBAKQeAbNDqfi9M2v86RUGAYgq1ZSDioFC6OLr0SwiOaYnLsSOl+Kx +O394PVwSGa6rZk1ErIZonyi15fU/0nHZLi8iHLB49EB5G3tCwh0s8NfqR9ck0/3Z+TXhVUdiJyJC +/z8x5I5lSUfzNEedJRidVvp6jVGr7P/SfoEfQQTLP3mBAgMBAAGjggKnMIICozA9BgkrBgEEAYI3 +FQcEMDAuBiYrBgEEAYI3FQiC3IMvhZOMZoXVnReC4twnge/sPGGBy54UhqiCWAIBZAIBBDAdBgNV +HQ4EFgQU1dVHhF0UVe7RXIz4cjl3/Vew+qowCwYDVR0PBAQDAgTwMB8GA1UdIwQYMBaAFHjhPp/S +ErN6PI3NMA5Ts0MpB7NVMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jcmwuZS1jZXJ0Y2hpbGUu +Y2wvZWNlcnRjaGlsZWNhRkVTLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0dHA6 +Ly9vY3NwLmVjZXJ0Y2hpbGUuY2wvb2NzcDAjBgNVHREEHDAaoBgGCCsGAQQBwQEBoAwWCjEzMTg1 +MDk1LTYwIwYDVR0SBBwwGqAYBggrBgEEAcEBAqAMFgo5NjkyODE4MC01MIIBTQYDVR0gBIIBRDCC +AUAwggE8BggrBgEEAcNSBTCCAS4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cuZS1jZXJ0Y2hpbGUu +Y2wvQ1BTLmh0bTCB/AYIKwYBBQUHAgIwge8egewAQwBlAHIAdABpAGYAaQBjAGEAZABvACAARgBp +AHIAbQBhACAAUwBpAG0AcABsAGUALgAgAEgAYQAgAHMAaQBkAG8AIAB2AGEAbABpAGQAYQBkAG8A +IABlAG4AIABmAG8AcgBtAGEAIABwAHIAZQBzAGUAbgBjAGkAYQBsACwAIABxAHUAZQBkAGEAbgBk +AG8AIABoAGEAYgBpAGwAaQB0AGEAZABvACAAZQBsACAAQwBlAHIAdABpAGYAaQBjAGEAZABvACAA +cABhAHIAYQAgAHUAcwBvACAAdAByAGkAYgB1AHQAYQByAGkAbzANBgkqhkiG9w0BAQUFAAOCAQEA +mxtPpXWslwI0+uJbyuS9s/S3/Vs0imn758xMU8t4BHUd+OlMdNAMQI1G2+q/OugdLQ/a9Sg3clKD +qXR4lHGl8d/Yq4yoJzDD3Ceez8qenY3JwGUhPzw9oDpg4mXWvxQDXSFeW/u/BgdadhfGnpwx61Un ++/fU24ZgU1dDJ4GKj5oIPHUIjmoSBhnstEhIr6GJWSTcDKTyzRdqBlaVhenH2Qs6Mw6FrOvRPuud +B7lo1+OgxMb/Gjyu6XnEaPu7Vq4XlLYMoCD2xrV7WEADaDTm7KcNLczVAYqWSF1WUqYSxmPoQDFY ++kMTThJyCXBlE0NADInrkwWgLLygkKI7zXkwaw== +-----END CERTIFICATE----- diff --git a/tests/test_data/sii-crypto/DTE--76354771-K--33--170-signature-value-base64.txt b/tests/test_data/sii-crypto/DTE--76354771-K--33--170-signature-value-base64.txt new file mode 100644 index 00000000..4453bbd4 --- /dev/null +++ b/tests/test_data/sii-crypto/DTE--76354771-K--33--170-signature-value-base64.txt @@ -0,0 +1 @@ +fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnkddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKiwqSxDcYjTT6vXsLPrZk= diff --git a/tests/test_data/sii-crypto/DTE--76399752-9--33--25568-cert.pem b/tests/test_data/sii-crypto/DTE--76399752-9--33--25568-cert.pem new file mode 100644 index 00000000..92083cd6 --- /dev/null +++ b/tests/test_data/sii-crypto/DTE--76399752-9--33--25568-cert.pem @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNM +MRgwFgYDVQQKEw9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20g +QXV0b3JpZGFkIENlcnRpZmljYWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwg +LSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0BhY2VwdGEuY29tMRMwEQYDVQQFEwo5 +NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0MDI1NFowgY8xCzAJ +BgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMTGkdJ +QU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwu +YXJhdmVuYUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2Ld +pvaShxU6iU2xB+CQs74HZ+oS1BINzmL1g9oY7hHvT+/H+hucOlN7xomH/UuDikjo +ySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIPHTctMNJ32mIQ/fGU8H+Q +f7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNShqiKeGfsh +/qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1a +h6dSEbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEA +AaOCAkowggJGMB8GA1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1Ud +DgQWBBSHoSD4nd2UJuwzmJnJud0LWSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYw +FAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG+EIBAQQEAwIFoDB1BgNVHSAE +bjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8vYWNnNC5hY2Vw +dGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t +IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkw +NTAtOKAkBggrBgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZv +QGFjZXB0YS5jb20waAYDVR0RBGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05 +oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTItOQYIKwYBBAHBAQKBHWRhbmllbC5h +cmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDswOTA3BggrBgEFBQcw +AYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1HNDA/ +BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2Ny +bC9DbGFzZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFD +CYfcyhU5t5iKV+8Pr8LVWZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb +9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/w8w2VMhrCILonVjnhLX8VHNMkc3Xy17J +gvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KBB/QNi7AB5U9kB7M5wfGr +2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACgjhl1gijA +NMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkf +zmrw +-----END CERTIFICATE----- diff --git a/tests/test_data/sii-crypto/DTE--76399752-9--33--25568-signature-value-base64.txt b/tests/test_data/sii-crypto/DTE--76399752-9--33--25568-signature-value-base64.txt new file mode 100644 index 00000000..9f5c23a8 --- /dev/null +++ b/tests/test_data/sii-crypto/DTE--76399752-9--33--25568-signature-value-base64.txt @@ -0,0 +1 @@ +wwOMQuFqa6c5gzYSJ5PWfo0OiAf+yNcJK6wx4xJ3VNehlAcMrUB2q+rK/DDhCvjxAoX4NxBACiFDMrTMIfvxrwXjLd1oX37lSFOtsWX6JxL0SV+tLF7qvWCu1Yzw8ypUf7GDkbymJkoTYDF9JFF8kYU4FdU2wttiwne9XH8QFHgXsocKP/aygwiOeGqiNX9o/O5XS2GWpt+KM20jrvtYn7UFMED/3aPacCb1GABizr8mlVEZggZgJunMDChpFQyEigSXMK5I737Ac8D2bw7WB47Wj1WBL3sCFRDlXUXtnMvChBVp0HRUXYuKHyfpCzqIBXygYrIZexxXgOSnKu/yGg== diff --git a/tests/test_data/sii-crypto/prueba-sii-cert.pem b/tests/test_data/sii-crypto/prueba-sii-cert.pem new file mode 100644 index 00000000..3ba15fa7 --- /dev/null +++ b/tests/test_data/sii-crypto/prueba-sii-cert.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEPjCCA6mgAwIBAgIDAgGKMAsGCSqGSIb3DQEBBDCBsTEdMBsGA1UECBQUUmVn +aW9uIE1ldHJvcG9saXRhbmExETAPBgNVBAcUCFNhbnRpYWdvMSIwIAYDVQQDFBlF +LUNlcnRjaGlsZSBDQSBJbnRlcm1lZGlhMTYwNAYDVQQLFC1FbXByZXNhIE5hY2lv +bmFsIGRlIENlcnRpZmljYWNpb24gRWxlY3Ryb25pY2ExFDASBgNVBAoUC0UtQ0VS +VENISUxFMQswCQYDVQQGEwJDTDAeFw0wMjEwMDIxOTExNTlaFw0wMzEwMDIwMDAw +MDBaMIHXMR0wGwYDVQQIFBRSZWdpb24gTWV0cm9wb2xpdGFuYTEnMCUGA1UECxQe +U2VydmljaW8gZGUgSW1wdWVzdG9zIEludGVybm9zMScwJQYDVQQKFB5TZXJ2aWNp +byBkZSBJbXB1ZXN0b3MgSW50ZXJub3MxETAPBgNVBAcUCFNhbnRpYWdvMR8wHQYJ +KoZIhvcNAQkBFhB3Z29uemFsZXpAc2lpLmNsMSMwIQYDVQQDFBpXaWxpYmFsZG8g +R29uemFsZXogQ2FicmVyYTELMAkGA1UEBhMCQ0wwXDANBgkqhkiG9w0BAQEFAANL +ADBIAkEAvNQyaLPd3cQlBr0fQWooAKXSFan/WbaFtD5P7QDzcE1pBIvKY2Uv6uid +ur/mGVB9IS4Fq/1xRIXy13FFmxLwTQIDAQABo4IBgjCCAX4wIwYDVR0RBBwwGqAY +BggrBgEEAcNSAaAMFgowNzg4MDQ0Mi00MDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6 +Ly9jcmwuZS1jZXJ0Y2hpbGUuY2wvRWNlcnRjaGlsZUNBSS5jcmwwIwYDVR0SBBww +GqAYBggrBgEEAcEBAqAMFgo5NjkyODE4MC01MIHmBgNVHSAEgd4wgdswgdgGCCsG +AQQBw1IAMIHLMDYGCCsGAQUFBwIBFipodHRwOi8vd3d3LmUtY2VydGNoaWxlLmNs +L3BvbGl0aWNhL2Nwcy5odG0wgZAGCCsGAQUFBwICMIGDGoGARWwgdGl0dWxhciBo +YSBzaWRvIHZhbGlkYWRvIGVuIGZvcm1hIHByZXNlbmNpYWwsIHF1ZWRhbmRvIGhh +YmlsaXRhZG8gZWwgQ2VydGlmaWNhZG8gcGFyYSB1c28gdHJpYnV0YXJpbywgcGFn +b3MsIGNvbWVyY2lvIHUgb3Ryb3MwCwYDVR0PBAQDAgTwMAsGCSqGSIb3DQEBBAOB +gQB2V4cTj7jo1RawmsRQUSnnvJjMCrZstcHY+Ss3IghVPO9eGoYzu5Q63vzt0Pi8 +CS91SBc7xo+LDoljaUyjOzj7zvU7TpWoFndiTQF3aCOtTkV+vjCMWW3sVHes4UCM +DkF3VYK+rDTAadiaeDArTwsx4eNEpxFuA/TJwcXpLQRCDg== +-----END CERTIFICATE----- diff --git a/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-bad-cert-no-base64.xml b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-bad-cert-no-base64.xml new file mode 100644 index 00000000..6bf67797 --- /dev/null +++ b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-bad-cert-no-base64.xml @@ -0,0 +1,94 @@ + + + + + + + 33 + 170 + 2019-04-01 + 1 + 1 + 2 + + + 76354771-K + INGENIERIA ENACON SPA + Ingenieria y Construccion + ENACONLTDA@GMAIL.COM + 421000 + 078525666 + MERCED 753 16 ARBOLEDA DE QUIILOTA + QUILLOTA + QUILLOTA + + + 96790240-3 + MINERA LOS PELAMBRES + EXTRACCION Y PROCESAMIENTO DE COBRE + Felipe Barria + Av. Apoquindo 4001 1802 + LAS CONDES + SANTIAGO + + + 2517900 + 19.00 + 478401 + 2996301 + + + + 1 + Tableros electricos 3 tom + as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.- + 2.00 + Unid + 1258950.00 + 2517900 + + + 1 + 801 + 4510083633 + 2019-03-22 + +
76354771-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
+ 2019-04-01T01:36:40 + +
+ + + + + + + + + +ij2Qn6xOc2eRx3hwyO/GrzptoBk= + + + +fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk +ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi +wqSxDcYjTT6vXsLPrZk= + + + + + +pB4Bs0Op+L0za/zpFQYBiCrVlIOKgULo4uvRLCI5picuxI6X4rE7f3g9XBIZrqtmTUSshmifKLXl +9T/ScdkuLyIcsHj0QHkbe0LCHSzw1+pH1yTT/dn5NeFVR2InIkL/PzHkjmVJR/M0R50lGJ1W+nqN +Uavs/9J+gR9BBMs/eYE= + +AQAB + + + + +abc + + + +
\ No newline at end of file diff --git a/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-bad-cert.xml b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-bad-cert.xml new file mode 100644 index 00000000..1722e121 --- /dev/null +++ b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-bad-cert.xml @@ -0,0 +1,95 @@ + + + + + + + 33 + 170 + 2019-04-01 + 1 + 1 + 2 + + + 76354771-K + INGENIERIA ENACON SPA + Ingenieria y Construccion + ENACONLTDA@GMAIL.COM + 421000 + 078525666 + MERCED 753 16 ARBOLEDA DE QUIILOTA + QUILLOTA + QUILLOTA + + + 96790240-3 + MINERA LOS PELAMBRES + EXTRACCION Y PROCESAMIENTO DE COBRE + Felipe Barria + Av. Apoquindo 4001 1802 + LAS CONDES + SANTIAGO + + + 2517900 + 19.00 + 478401 + 2996301 + + + + 1 + Tableros electricos 3 tom + as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.- + 2.00 + Unid + 1258950.00 + 2517900 + + + 1 + 801 + 4510083633 + 2019-03-22 + +
76354771-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
+ 2019-04-01T01:36:40 + +
+ + + + + + + + + +ij2Qn6xOc2eRx3hwyO/GrzptoBk= + + + +fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk +ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi +wqSxDcYjTT6vXsLPrZk= + + + + + +pB4Bs0Op+L0za/zpFQYBiCrVlIOKgULo4uvRLCI5picuxI6X4rE7f3g9XBIZrqtmTUSshmifKLXl +9T/ScdkuLyIcsHj0QHkbe0LCHSzw1+pH1yTT/dn5NeFVR2InIkL/PzHkjmVJR/M0R50lGJ1W+nqN +Uavs/9J+gR9BBMs/eYE= + +AQAB + + + + +MIIGVDCCBTygAwIBAgIKMUWmvgAAAAjUHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx ++kMTThJyCXBlE0NADInrkwWgLLygkKI7zXkwaw== + + + +
\ No newline at end of file diff --git a/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-changed-monto.xml b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-changed-monto.xml new file mode 100644 index 00000000..2cd61095 --- /dev/null +++ b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-changed-monto.xml @@ -0,0 +1,122 @@ + + + + + + + 33 + 170 + 2019-04-01 + 1 + 1 + 2 + + + 76354771-K + INGENIERIA ENACON SPA + Ingenieria y Construccion + ENACONLTDA@GMAIL.COM + 421000 + 078525666 + MERCED 753 16 ARBOLEDA DE QUIILOTA + QUILLOTA + QUILLOTA + + + 96790240-3 + MINERA LOS PELAMBRES + EXTRACCION Y PROCESAMIENTO DE COBRE + Felipe Barria + Av. Apoquindo 4001 1802 + LAS CONDES + SANTIAGO + + + 2517000 + 19.00 + 478230 + 2995230 + + + + 1 + Tableros electricos 3 tom + as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.- + 2.00 + Unid + 1258500.00 + 2517000 + + + 1 + 801 + 4510083633 + 2019-03-22 + +
76354771-K331702019-04-0196790240-3MINERA LOS PELAMBRES2995230Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
+ 2019-04-01T01:36:40 + +
+ + + + + + + + + +ij2Qn6xOc2eRx3hwyO/GrzptoBk= + + + +fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk +ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi +wqSxDcYjTT6vXsLPrZk= + + + + + +pB4Bs0Op+L0za/zpFQYBiCrVlIOKgULo4uvRLCI5picuxI6X4rE7f3g9XBIZrqtmTUSshmifKLXl +9T/ScdkuLyIcsHj0QHkbe0LCHSzw1+pH1yTT/dn5NeFVR2InIkL/PzHkjmVJR/M0R50lGJ1W+nqN +Uavs/9J+gR9BBMs/eYE= + +AQAB + + + + +MIIGVDCCBTygAwIBAgIKMUWmvgAAAAjUHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx +HTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREwDwYDVQQHEwhTYW50aWFnbzEUMBIGA1UE +ChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsTF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMTAwLgYDVQQD +EydFLUNFUlRDSElMRSBDQSBGSVJNQSBFTEVDVFJPTklDQSBTSU1QTEUxJzAlBgkqhkiG9w0BCQEW +GHNjbGllbnRlc0BlLWNlcnRjaGlsZS5jbDAeFw0xNzA5MDQyMTExMTJaFw0yMDA5MDMyMTExMTJa +MIHXMQswCQYDVQQGEwJDTDEUMBIGA1UECBMLVkFMUEFSQUlTTyAxETAPBgNVBAcTCFF1aWxsb3Rh +MS8wLQYDVQQKEyZTZXJ2aWNpb3MgQm9uaWxsYSB5IExvcGV6IHkgQ2lhLiBMdGRhLjEkMCIGA1UE +CwwbSW5nZW5pZXLDrWEgeSBDb25zdHJ1Y2Npw7NuMSMwIQYDVQQDExpSYW1vbiBodW1iZXJ0byBM +b3BleiAgSmFyYTEjMCEGCSqGSIb3DQEJARYUZW5hY29ubHRkYUBnbWFpbC5jb20wgZ8wDQYJKoZI +hvcNAQEBBQADgY0AMIGJAoGBAKQeAbNDqfi9M2v86RUGAYgq1ZSDioFC6OLr0SwiOaYnLsSOl+Kx +O394PVwSGa6rZk1ErIZonyi15fU/0nHZLi8iHLB49EB5G3tCwh0s8NfqR9ck0/3Z+TXhVUdiJyJC +/z8x5I5lSUfzNEedJRidVvp6jVGr7P/SfoEfQQTLP3mBAgMBAAGjggKnMIICozA9BgkrBgEEAYI3 +FQcEMDAuBiYrBgEEAYI3FQiC3IMvhZOMZoXVnReC4twnge/sPGGBy54UhqiCWAIBZAIBBDAdBgNV +HQ4EFgQU1dVHhF0UVe7RXIz4cjl3/Vew+qowCwYDVR0PBAQDAgTwMB8GA1UdIwQYMBaAFHjhPp/S +ErN6PI3NMA5Ts0MpB7NVMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jcmwuZS1jZXJ0Y2hpbGUu +Y2wvZWNlcnRjaGlsZWNhRkVTLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0dHA6 +Ly9vY3NwLmVjZXJ0Y2hpbGUuY2wvb2NzcDAjBgNVHREEHDAaoBgGCCsGAQQBwQEBoAwWCjEzMTg1 +MDk1LTYwIwYDVR0SBBwwGqAYBggrBgEEAcEBAqAMFgo5NjkyODE4MC01MIIBTQYDVR0gBIIBRDCC +AUAwggE8BggrBgEEAcNSBTCCAS4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cuZS1jZXJ0Y2hpbGUu +Y2wvQ1BTLmh0bTCB/AYIKwYBBQUHAgIwge8egewAQwBlAHIAdABpAGYAaQBjAGEAZABvACAARgBp +AHIAbQBhACAAUwBpAG0AcABsAGUALgAgAEgAYQAgAHMAaQBkAG8AIAB2AGEAbABpAGQAYQBkAG8A +IABlAG4AIABmAG8AcgBtAGEAIABwAHIAZQBzAGUAbgBjAGkAYQBsACwAIABxAHUAZQBkAGEAbgBk +AG8AIABoAGEAYgBpAGwAaQB0AGEAZABvACAAZQBsACAAQwBlAHIAdABpAGYAaQBjAGEAZABvACAA +cABhAHIAYQAgAHUAcwBvACAAdAByAGkAYgB1AHQAYQByAGkAbzANBgkqhkiG9w0BAQUFAAOCAQEA +mxtPpXWslwI0+uJbyuS9s/S3/Vs0imn758xMU8t4BHUd+OlMdNAMQI1G2+q/OugdLQ/a9Sg3clKD +qXR4lHGl8d/Yq4yoJzDD3Ceez8qenY3JwGUhPzw9oDpg4mXWvxQDXSFeW/u/BgdadhfGnpwx61Un ++/fU24ZgU1dDJ4GKj5oIPHUIjmoSBhnstEhIr6GJWSTcDKTyzRdqBlaVhenH2Qs6Mw6FrOvRPuud +B7lo1+OgxMb/Gjyu6XnEaPu7Vq4XlLYMoCD2xrV7WEADaDTm7KcNLczVAYqWSF1WUqYSxmPoQDFY ++kMTThJyCXBlE0NADInrkwWgLLygkKI7zXkwaw== + + + +
\ No newline at end of file diff --git a/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-removed-signature.xml b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-removed-signature.xml new file mode 100644 index 00000000..8a7b6b49 --- /dev/null +++ b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-removed-signature.xml @@ -0,0 +1,60 @@ + + + + + + + 33 + 170 + 2019-04-01 + 1 + 1 + 2 + + + 76354771-K + INGENIERIA ENACON SPA + Ingenieria y Construccion + ENACONLTDA@GMAIL.COM + 421000 + 078525666 + MERCED 753 16 ARBOLEDA DE QUIILOTA + QUILLOTA + QUILLOTA + + + 96790240-3 + MINERA LOS PELAMBRES + EXTRACCION Y PROCESAMIENTO DE COBRE + Felipe Barria + Av. Apoquindo 4001 1802 + LAS CONDES + SANTIAGO + + + 2517900 + 19.00 + 478401 + 2996301 + + + + 1 + Tableros electricos 3 tom + as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.- + 2.00 + Unid + 1258950.00 + 2517900 + + + 1 + 801 + 4510083633 + 2019-03-22 + +
76354771-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
+ 2019-04-01T01:36:40 + +
+
diff --git a/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-replaced-cert.xml b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-replaced-cert.xml new file mode 100644 index 00000000..ded4f5c2 --- /dev/null +++ b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-mod-replaced-cert.xml @@ -0,0 +1,137 @@ + + + + + + + 33 + 170 + 2019-04-01 + 1 + 1 + 2 + + + 76354771-K + INGENIERIA ENACON SPA + Ingenieria y Construccion + ENACONLTDA@GMAIL.COM + 421000 + 078525666 + MERCED 753 16 ARBOLEDA DE QUIILOTA + QUILLOTA + QUILLOTA + + + 96790240-3 + MINERA LOS PELAMBRES + EXTRACCION Y PROCESAMIENTO DE COBRE + Felipe Barria + Av. Apoquindo 4001 1802 + LAS CONDES + SANTIAGO + + + 2517900 + 19.00 + 478401 + 2996301 + + + + 1 + Tableros electricos 3 tom + as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.- + 2.00 + Unid + 1258950.00 + 2517900 + + + 1 + 801 + 4510083633 + 2019-03-22 + +
76354771-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
+ 2019-04-01T01:36:40 + +
+ + + + + + + + + +ij2Qn6xOc2eRx3hwyO/GrzptoBk= + + + +fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk +ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi +wqSxDcYjTT6vXsLPrZk= + + + + + +pB4Bs0Op+L0za/zpFQYBiCrVlIOKgULo4uvRLCI5picuxI6X4rE7f3g9XBIZrqtmTUSshmifKLXl +9T/ScdkuLyIcsHj0QHkbe0LCHSzw1+pH1yTT/dn5NeFVR2InIkL/PzHkjmVJR/M0R50lGJ1W+nqN +Uavs/9J+gR9BBMs/eYE= + +AQAB + + + + +MIIIDTCCBvWgAwIBAgIQXD9eCvh/44P1ET5RI1LuJjANBgkqhkiG9w0BAQsFADBU +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMSUw +IwYDVQQDExxHb29nbGUgSW50ZXJuZXQgQXV0aG9yaXR5IEczMB4XDTE5MDMyNjEz +NDA0MFoXDTE5MDYxODEzMjQwMFowZjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNh +bGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxEzARBgNVBAoMCkdvb2ds +ZSBMTEMxFTATBgNVBAMMDCouZ29vZ2xlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49 +AwEHA0IABANpWSLXLbJm5eRzc1EJmvSIbz0nANT+b11r+XhSUCAbfQhS+4M/91YJ +gVE6UtZJrLO7GGxvp1tV/DL857NaLEWjggWSMIIFjjATBgNVHSUEDDAKBggrBgEF +BQcDATAOBgNVHQ8BAf8EBAMCB4AwggRXBgNVHREEggROMIIESoIMKi5nb29nbGUu +Y29tgg0qLmFuZHJvaWQuY29tghYqLmFwcGVuZ2luZS5nb29nbGUuY29tghIqLmNs +b3VkLmdvb2dsZS5jb22CGCouY3Jvd2Rzb3VyY2UuZ29vZ2xlLmNvbYIGKi5nLmNv +gg4qLmdjcC5ndnQyLmNvbYIKKi5nZ3BodC5jboIWKi5nb29nbGUtYW5hbHl0aWNz +LmNvbYILKi5nb29nbGUuY2GCCyouZ29vZ2xlLmNsgg4qLmdvb2dsZS5jby5pboIO +Ki5nb29nbGUuY28uanCCDiouZ29vZ2xlLmNvLnVrgg8qLmdvb2dsZS5jb20uYXKC +DyouZ29vZ2xlLmNvbS5hdYIPKi5nb29nbGUuY29tLmJygg8qLmdvb2dsZS5jb20u +Y2+CDyouZ29vZ2xlLmNvbS5teIIPKi5nb29nbGUuY29tLnRygg8qLmdvb2dsZS5j +b20udm6CCyouZ29vZ2xlLmRlggsqLmdvb2dsZS5lc4ILKi5nb29nbGUuZnKCCyou +Z29vZ2xlLmh1ggsqLmdvb2dsZS5pdIILKi5nb29nbGUubmyCCyouZ29vZ2xlLnBs +ggsqLmdvb2dsZS5wdIISKi5nb29nbGVhZGFwaXMuY29tgg8qLmdvb2dsZWFwaXMu +Y26CESouZ29vZ2xlY25hcHBzLmNughQqLmdvb2dsZWNvbW1lcmNlLmNvbYIRKi5n +b29nbGV2aWRlby5jb22CDCouZ3N0YXRpYy5jboINKi5nc3RhdGljLmNvbYISKi5n +c3RhdGljY25hcHBzLmNuggoqLmd2dDEuY29tggoqLmd2dDIuY29tghQqLm1ldHJp +Yy5nc3RhdGljLmNvbYIMKi51cmNoaW4uY29tghAqLnVybC5nb29nbGUuY29tghYq +LnlvdXR1YmUtbm9jb29raWUuY29tgg0qLnlvdXR1YmUuY29tghYqLnlvdXR1YmVl +ZHVjYXRpb24uY29tghEqLnlvdXR1YmVraWRzLmNvbYIHKi55dC5iZYILKi55dGlt +Zy5jb22CGmFuZHJvaWQuY2xpZW50cy5nb29nbGUuY29tggthbmRyb2lkLmNvbYIb +ZGV2ZWxvcGVyLmFuZHJvaWQuZ29vZ2xlLmNughxkZXZlbG9wZXJzLmFuZHJvaWQu +Z29vZ2xlLmNuggRnLmNvgghnZ3BodC5jboIGZ29vLmdsghRnb29nbGUtYW5hbHl0 +aWNzLmNvbYIKZ29vZ2xlLmNvbYIPZ29vZ2xlY25hcHBzLmNughJnb29nbGVjb21t +ZXJjZS5jb22CGHNvdXJjZS5hbmRyb2lkLmdvb2dsZS5jboIKdXJjaGluLmNvbYIK +d3d3Lmdvby5nbIIIeW91dHUuYmWCC3lvdXR1YmUuY29tghR5b3V0dWJlZWR1Y2F0 +aW9uLmNvbYIPeW91dHViZWtpZHMuY29tggV5dC5iZTBoBggrBgEFBQcBAQRcMFow +LQYIKwYBBQUHMAKGIWh0dHA6Ly9wa2kuZ29vZy9nc3IyL0dUU0dJQUczLmNydDAp +BggrBgEFBQcwAYYdaHR0cDovL29jc3AucGtpLmdvb2cvR1RTR0lBRzMwHQYDVR0O +BBYEFM8C2hpNgJL/BEX/yzeB408dhba2MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgw +FoAUd8K4UJpndnaxLcKG0IOgfqZ+ukswIQYDVR0gBBowGDAMBgorBgEEAdZ5AgUD +MAgGBmeBDAECAjAxBgNVHR8EKjAoMCagJKAihiBodHRwOi8vY3JsLnBraS5nb29n +L0dUU0dJQUczLmNybDANBgkqhkiG9w0BAQsFAAOCAQEAF9PM41ShwCbhtJG7tj2y +ZvF2sHbQ5YuZrMfJc6eeCG+nCKm1U5iJzXnXctFGvfJnUCZpj9YrfwDswdEddWyZ +IG6m6wONF3ZiQifQrcDi0oDA+0BwjEuzYGCGkbfE+Xxb30bVEyDRe51DpJf+cqsb ++DW2pYdikbdrPem5/hwdNerc7nqrQOJ93sqwbVNGktuyJsTOGNKkSwSaejxdN7yl +g5aa4CJsE94gy4+mCywWjnnsjcLGJM3RBUxDdAdTGMldU/r33HCUCXl33Qxc4nvP +MlE9LyFOTIJoajWcpGOsbKWiL3Zr19DKNBSn4Xof0onbtCH7dbpyMwP8XcA2O1dA +ow== + + + +
\ No newline at end of file diff --git a/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signature_xml.xml b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signature_xml.xml new file mode 100644 index 00000000..59b2e818 --- /dev/null +++ b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signature_xml.xml @@ -0,0 +1,64 @@ + + + + + + + + + + +ij2Qn6xOc2eRx3hwyO/GrzptoBk= + + + +fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk +ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi +wqSxDcYjTT6vXsLPrZk= + + + + + +pB4Bs0Op+L0za/zpFQYBiCrVlIOKgULo4uvRLCI5picuxI6X4rE7f3g9XBIZrqtmTUSshmifKLXl +9T/ScdkuLyIcsHj0QHkbe0LCHSzw1+pH1yTT/dn5NeFVR2InIkL/PzHkjmVJR/M0R50lGJ1W+nqN +Uavs/9J+gR9BBMs/eYE= + +AQAB + + + + +MIIGVDCCBTygAwIBAgIKMUWmvgAAAAjUHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx +HTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREwDwYDVQQHEwhTYW50aWFnbzEUMBIGA1UE +ChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsTF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMTAwLgYDVQQD +EydFLUNFUlRDSElMRSBDQSBGSVJNQSBFTEVDVFJPTklDQSBTSU1QTEUxJzAlBgkqhkiG9w0BCQEW +GHNjbGllbnRlc0BlLWNlcnRjaGlsZS5jbDAeFw0xNzA5MDQyMTExMTJaFw0yMDA5MDMyMTExMTJa +MIHXMQswCQYDVQQGEwJDTDEUMBIGA1UECBMLVkFMUEFSQUlTTyAxETAPBgNVBAcTCFF1aWxsb3Rh +MS8wLQYDVQQKEyZTZXJ2aWNpb3MgQm9uaWxsYSB5IExvcGV6IHkgQ2lhLiBMdGRhLjEkMCIGA1UE +CwwbSW5nZW5pZXLDrWEgeSBDb25zdHJ1Y2Npw7NuMSMwIQYDVQQDExpSYW1vbiBodW1iZXJ0byBM +b3BleiAgSmFyYTEjMCEGCSqGSIb3DQEJARYUZW5hY29ubHRkYUBnbWFpbC5jb20wgZ8wDQYJKoZI +hvcNAQEBBQADgY0AMIGJAoGBAKQeAbNDqfi9M2v86RUGAYgq1ZSDioFC6OLr0SwiOaYnLsSOl+Kx +O394PVwSGa6rZk1ErIZonyi15fU/0nHZLi8iHLB49EB5G3tCwh0s8NfqR9ck0/3Z+TXhVUdiJyJC +/z8x5I5lSUfzNEedJRidVvp6jVGr7P/SfoEfQQTLP3mBAgMBAAGjggKnMIICozA9BgkrBgEEAYI3 +FQcEMDAuBiYrBgEEAYI3FQiC3IMvhZOMZoXVnReC4twnge/sPGGBy54UhqiCWAIBZAIBBDAdBgNV +HQ4EFgQU1dVHhF0UVe7RXIz4cjl3/Vew+qowCwYDVR0PBAQDAgTwMB8GA1UdIwQYMBaAFHjhPp/S +ErN6PI3NMA5Ts0MpB7NVMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jcmwuZS1jZXJ0Y2hpbGUu +Y2wvZWNlcnRjaGlsZWNhRkVTLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0dHA6 +Ly9vY3NwLmVjZXJ0Y2hpbGUuY2wvb2NzcDAjBgNVHREEHDAaoBgGCCsGAQQBwQEBoAwWCjEzMTg1 +MDk1LTYwIwYDVR0SBBwwGqAYBggrBgEEAcEBAqAMFgo5NjkyODE4MC01MIIBTQYDVR0gBIIBRDCC +AUAwggE8BggrBgEEAcNSBTCCAS4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cuZS1jZXJ0Y2hpbGUu +Y2wvQ1BTLmh0bTCB/AYIKwYBBQUHAgIwge8egewAQwBlAHIAdABpAGYAaQBjAGEAZABvACAARgBp +AHIAbQBhACAAUwBpAG0AcABsAGUALgAgAEgAYQAgAHMAaQBkAG8AIAB2AGEAbABpAGQAYQBkAG8A +IABlAG4AIABmAG8AcgBtAGEAIABwAHIAZQBzAGUAbgBjAGkAYQBsACwAIABxAHUAZQBkAGEAbgBk +AG8AIABoAGEAYgBpAGwAaQB0AGEAZABvACAAZQBsACAAQwBlAHIAdABpAGYAaQBjAGEAZABvACAA +cABhAHIAYQAgAHUAcwBvACAAdAByAGkAYgB1AHQAYQByAGkAbzANBgkqhkiG9w0BAQUFAAOCAQEA +mxtPpXWslwI0+uJbyuS9s/S3/Vs0imn758xMU8t4BHUd+OlMdNAMQI1G2+q/OugdLQ/a9Sg3clKD +qXR4lHGl8d/Yq4yoJzDD3Ceez8qenY3JwGUhPzw9oDpg4mXWvxQDXSFeW/u/BgdadhfGnpwx61Un ++/fU24ZgU1dDJ4GKj5oIPHUIjmoSBhnstEhIr6GJWSTcDKTyzRdqBlaVhenH2Qs6Mw6FrOvRPuud +B7lo1+OgxMb/Gjyu6XnEaPu7Vq4XlLYMoCD2xrV7WEADaDTm7KcNLczVAYqWSF1WUqYSxmPoQDFY ++kMTThJyCXBlE0NADInrkwWgLLygkKI7zXkwaw== + + + + \ No newline at end of file diff --git a/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signed_data.xml b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signed_data.xml new file mode 100644 index 00000000..e2071898 --- /dev/null +++ b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signed_data.xml @@ -0,0 +1,56 @@ + + + + 33 + 170 + 2019-04-01 + 1 + 1 + 2 + + + 76354771-K + INGENIERIA ENACON SPA + Ingenieria y Construccion + ENACONLTDA@GMAIL.COM + 421000 + 078525666 + MERCED 753 16 ARBOLEDA DE QUIILOTA + QUILLOTA + QUILLOTA + + + 96790240-3 + MINERA LOS PELAMBRES + EXTRACCION Y PROCESAMIENTO DE COBRE + Felipe Barria + Av. Apoquindo 4001 1802 + LAS CONDES + SANTIAGO + + + 2517900 + 19.00 + 478401 + 2996301 + + + + 1 + Tableros electricos 3 tom + as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.- + 2.00 + Unid + 1258950.00 + 2517900 + + + 1 + 801 + 4510083633 + 2019-03-22 + +
76354771-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
+ 2019-04-01T01:36:40 + +
\ No newline at end of file diff --git a/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signed_xml.xml b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signed_xml.xml new file mode 100644 index 00000000..0cb88a90 --- /dev/null +++ b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned-signed_xml.xml @@ -0,0 +1,57 @@ + + + + + 33 + 170 + 2019-04-01 + 1 + 1 + 2 + + + 76354771-K + INGENIERIA ENACON SPA + Ingenieria y Construccion + ENACONLTDA@GMAIL.COM + 421000 + 078525666 + MERCED 753 16 ARBOLEDA DE QUIILOTA + QUILLOTA + QUILLOTA + + + 96790240-3 + MINERA LOS PELAMBRES + EXTRACCION Y PROCESAMIENTO DE COBRE + Felipe Barria + Av. Apoquindo 4001 1802 + LAS CONDES + SANTIAGO + + + 2517900 + 19.00 + 478401 + 2996301 + + + + 1 + Tableros electricos 3 tom + as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.- + 2.00 + Unid + 1258950.00 + 2517900 + + + 1 + 801 + 4510083633 + 2019-03-22 + +
76354771-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
+ 2019-04-01T01:36:40 + +
\ No newline at end of file diff --git a/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned.xml b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned.xml new file mode 100644 index 00000000..263ab7c2 --- /dev/null +++ b/tests/test_data/sii-dte/DTE--76354771-K--33--170--cleaned.xml @@ -0,0 +1,122 @@ + + + + + + + 33 + 170 + 2019-04-01 + 1 + 1 + 2 + + + 76354771-K + INGENIERIA ENACON SPA + Ingenieria y Construccion + ENACONLTDA@GMAIL.COM + 421000 + 078525666 + MERCED 753 16 ARBOLEDA DE QUIILOTA + QUILLOTA + QUILLOTA + + + 96790240-3 + MINERA LOS PELAMBRES + EXTRACCION Y PROCESAMIENTO DE COBRE + Felipe Barria + Av. Apoquindo 4001 1802 + LAS CONDES + SANTIAGO + + + 2517900 + 19.00 + 478401 + 2996301 + + + + 1 + Tableros electricos 3 tom + as 3p + t; 380v; 50 hz; 32a; 3 tomas monofasicas 2p + t; 240v; 50 hz; 16a; proteccion ip, segun orden de compra de la referencia.- + 2.00 + Unid + 1258950.00 + 2517900 + + + 1 + 801 + 4510083633 + 2019-03-22 + +
76354771-K331702019-04-0196790240-3MINERA LOS PELAMBRES2996301Tableros electricos 3 tom76354771-KINGENIERIA ENACON SPA331701702019-04-01uv7BUO3yg/7RoMjh1mPXXG/8YIwjtXsu7kcOq7dZQj66QCiY4FVz2fIhF1jaU0GSikq/jq26IFGylGus92OnPQ==Aw==300PI7bw8y0RNUJrGxyhb2gr6BjFtv/Ikyo/6g69wycoXTHSoRML3xvZvOBytreN7REw9JF0Ldoj91RRtaZbH38bA==2019-04-01T01:36:40
DKFS7bNYRpVYLNEII+eyLcBHmNwQIHVkbqgR96wKcnDEcU6NsHQUMUyXpr7ql7xD9iuGkZDmNxHuY+Mq913oSA==
+ 2019-04-01T01:36:40 + +
+ + + + + + + + + +ij2Qn6xOc2eRx3hwyO/GrzptoBk= + + + +fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk +ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi +wqSxDcYjTT6vXsLPrZk= + + + + + +pB4Bs0Op+L0za/zpFQYBiCrVlIOKgULo4uvRLCI5picuxI6X4rE7f3g9XBIZrqtmTUSshmifKLXl +9T/ScdkuLyIcsHj0QHkbe0LCHSzw1+pH1yTT/dn5NeFVR2InIkL/PzHkjmVJR/M0R50lGJ1W+nqN +Uavs/9J+gR9BBMs/eYE= + +AQAB + + + + +MIIGVDCCBTygAwIBAgIKMUWmvgAAAAjUHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx +HTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREwDwYDVQQHEwhTYW50aWFnbzEUMBIGA1UE +ChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsTF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMTAwLgYDVQQD +EydFLUNFUlRDSElMRSBDQSBGSVJNQSBFTEVDVFJPTklDQSBTSU1QTEUxJzAlBgkqhkiG9w0BCQEW +GHNjbGllbnRlc0BlLWNlcnRjaGlsZS5jbDAeFw0xNzA5MDQyMTExMTJaFw0yMDA5MDMyMTExMTJa +MIHXMQswCQYDVQQGEwJDTDEUMBIGA1UECBMLVkFMUEFSQUlTTyAxETAPBgNVBAcTCFF1aWxsb3Rh +MS8wLQYDVQQKEyZTZXJ2aWNpb3MgQm9uaWxsYSB5IExvcGV6IHkgQ2lhLiBMdGRhLjEkMCIGA1UE +CwwbSW5nZW5pZXLDrWEgeSBDb25zdHJ1Y2Npw7NuMSMwIQYDVQQDExpSYW1vbiBodW1iZXJ0byBM +b3BleiAgSmFyYTEjMCEGCSqGSIb3DQEJARYUZW5hY29ubHRkYUBnbWFpbC5jb20wgZ8wDQYJKoZI +hvcNAQEBBQADgY0AMIGJAoGBAKQeAbNDqfi9M2v86RUGAYgq1ZSDioFC6OLr0SwiOaYnLsSOl+Kx +O394PVwSGa6rZk1ErIZonyi15fU/0nHZLi8iHLB49EB5G3tCwh0s8NfqR9ck0/3Z+TXhVUdiJyJC +/z8x5I5lSUfzNEedJRidVvp6jVGr7P/SfoEfQQTLP3mBAgMBAAGjggKnMIICozA9BgkrBgEEAYI3 +FQcEMDAuBiYrBgEEAYI3FQiC3IMvhZOMZoXVnReC4twnge/sPGGBy54UhqiCWAIBZAIBBDAdBgNV +HQ4EFgQU1dVHhF0UVe7RXIz4cjl3/Vew+qowCwYDVR0PBAQDAgTwMB8GA1UdIwQYMBaAFHjhPp/S +ErN6PI3NMA5Ts0MpB7NVMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jcmwuZS1jZXJ0Y2hpbGUu +Y2wvZWNlcnRjaGlsZWNhRkVTLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0dHA6 +Ly9vY3NwLmVjZXJ0Y2hpbGUuY2wvb2NzcDAjBgNVHREEHDAaoBgGCCsGAQQBwQEBoAwWCjEzMTg1 +MDk1LTYwIwYDVR0SBBwwGqAYBggrBgEEAcEBAqAMFgo5NjkyODE4MC01MIIBTQYDVR0gBIIBRDCC +AUAwggE8BggrBgEEAcNSBTCCAS4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cuZS1jZXJ0Y2hpbGUu +Y2wvQ1BTLmh0bTCB/AYIKwYBBQUHAgIwge8egewAQwBlAHIAdABpAGYAaQBjAGEAZABvACAARgBp +AHIAbQBhACAAUwBpAG0AcABsAGUALgAgAEgAYQAgAHMAaQBkAG8AIAB2AGEAbABpAGQAYQBkAG8A +IABlAG4AIABmAG8AcgBtAGEAIABwAHIAZQBzAGUAbgBjAGkAYQBsACwAIABxAHUAZQBkAGEAbgBk +AG8AIABoAGEAYgBpAGwAaQB0AGEAZABvACAAZQBsACAAQwBlAHIAdABpAGYAaQBjAGEAZABvACAA +cABhAHIAYQAgAHUAcwBvACAAdAByAGkAYgB1AHQAYQByAGkAbzANBgkqhkiG9w0BAQUFAAOCAQEA +mxtPpXWslwI0+uJbyuS9s/S3/Vs0imn758xMU8t4BHUd+OlMdNAMQI1G2+q/OugdLQ/a9Sg3clKD +qXR4lHGl8d/Yq4yoJzDD3Ceez8qenY3JwGUhPzw9oDpg4mXWvxQDXSFeW/u/BgdadhfGnpwx61Un ++/fU24ZgU1dDJ4GKj5oIPHUIjmoSBhnstEhIr6GJWSTcDKTyzRdqBlaVhenH2Qs6Mw6FrOvRPuud +B7lo1+OgxMb/Gjyu6XnEaPu7Vq4XlLYMoCD2xrV7WEADaDTm7KcNLczVAYqWSF1WUqYSxmPoQDFY ++kMTThJyCXBlE0NADInrkwWgLLygkKI7zXkwaw== + + + +
\ No newline at end of file diff --git a/tests/test_data/sii-dte/DTE--76399752-9--33--25568--cleaned.xml b/tests/test_data/sii-dte/DTE--76399752-9--33--25568--cleaned.xml new file mode 100644 index 00000000..e4914e04 --- /dev/null +++ b/tests/test_data/sii-dte/DTE--76399752-9--33--25568--cleaned.xml @@ -0,0 +1,129 @@ + + + + + + + 33 + 25568 + 2019-03-29 + 1 + 1 + 2 + + + 76399752-9 + COMERCIALIZADORA INNOVA MOBEL SPA + COMERCIALIZACION DE PRODUCTOS PARA EL HOGAR + 87 472133 + ANGEL.PEZO@APCASESORIAS.CL + 310001 + 078904860 + LOS CIPRESES 2834 + LA PINTANA + SANTIAGO + + + 96874030-K + EMPRESAS LA POLAR S.A. + VENTA AL POR MENOR EN COMERCIOS DE VESTU + N Lote Despacho: 20921554 / N Sello: 660620 + AVDA. SANTA CLARA 207 62 CIUDAD EMPRESARIAL + HUECHURABA + SANTIAGO + + + 194111 + 19.00 + 36881 + 230992 + + + + 1 + + SKU + 19586316 + + JUEGO_LIVI - CHOCOLATE + ROMA 3.1.1 + 1.00 + UN + 194111.00 + 194111 + + + 1 + 801 + 638370 + 2019-03-28 + +
76399752-933255682019-03-2996874030-KEMPRESAS LA POLAR S.A.230992JUEGO_LIVI - CHOCOLATE76399752-9COMERCIALIZADORA INNOVA MOBEL SPA3325568255682019-03-287EKJUPVmefPeVcgm9Q81Dp6q1MP+UvccH0mfsugbuK6UPYLn3tO7DxpZQIgoQC9LgdwYTtC9EHajZlgsk0iZjw==Aw==300byDdqUAqlKoALIOrNLlGmuFCOk866v4BQvnZqdiqGvrHk6jneiTMjYBSMB2GaY4t/dTFgVSOsqa/BnkRskel7Q==2019-03-28T13:59:52
viuqScpeQueqAnye1MLhttAAOAnO4raWlPdJ5kbSpUEeUT+pZgE/rr79kgVqirnIRM+HUpB3Yt4fbyMaARGqtA==
+ 2019-03-28T13:59:52 + +
+ + + + + + + + + +tk/D3mfO/KtdWyFXYZHe7dtYijg= + + + +wwOMQuFqa6c5gzYSJ5PWfo0OiAf+yNcJK6wx4xJ3VNehlAcMrUB2q+rK/DDhCvjxAoX4NxBACiFD +MrTMIfvxrwXjLd1oX37lSFOtsWX6JxL0SV+tLF7qvWCu1Yzw8ypUf7GDkbymJkoTYDF9JFF8kYU4 +FdU2wttiwne9XH8QFHgXsocKP/aygwiOeGqiNX9o/O5XS2GWpt+KM20jrvtYn7UFMED/3aPacCb1 +GABizr8mlVEZggZgJunMDChpFQyEigSXMK5I737Ac8D2bw7WB47Wj1WBL3sCFRDlXUXtnMvChBVp +0HRUXYuKHyfpCzqIBXygYrIZexxXgOSnKu/yGg== + + + + + +0tBhZ9dE624+LIifJE5Bz4NnYt2m9pKHFTqJTbEH4JCzvgdn6hLUEg3OYvWD2hjuEe9P78f6G5w6 +U3vGiYf9S4OKSOjJKOFsffEEzOHqpYe8Opx9OzBi4cRLaE72R5PPDK3JQg8dNy0w0nfaYhD98ZTw +f5B/tp21X4DuTeNeC8K7cNDlx55HXFTINtNchYkO2DbXmxrdhKS2jeI81KGqIp4Z+yH+pQRofegr +9N/SU4b8Ib9ue8t25tpxz2jsHlBLokXkgsx98IS7MGvHIxkuEFBibVqHp1IRsKwM2RzqxAwctiD/ +SobU35wgtdXK6wYYIIQNN+Zdv8AjisQpom3Rcw== + +AQAB + + + + +MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNMMRgwFgYDVQQK +Ew9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20gQXV0b3JpZGFkIENlcnRpZmlj +YWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwgLSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0Bh +Y2VwdGEuY29tMRMwEQYDVQQFEwo5NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0 +MDI1NFowgY8xCzAJBgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMT +GkdJQU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwuYXJhdmVu +YUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2LdpvaShxU6iU2xB+CQs74HZ+oS1BINzmL1 +g9oY7hHvT+/H+hucOlN7xomH/UuDikjoySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIP +HTctMNJ32mIQ/fGU8H+Qf7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNSh +qiKeGfsh/qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1ah6dS +EbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEAAaOCAkowggJGMB8G +A1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1UdDgQWBBSHoSD4nd2UJuwzmJnJud0L +WSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG ++EIBAQQEAwIFoDB1BgNVHSAEbjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8v +YWNnNC5hY2VwdGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t +IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkwNTAtOKAkBggr +BgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZvQGFjZXB0YS5jb20waAYDVR0R +BGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTIt +OQYIKwYBBAHBAQKBHWRhbmllbC5hcmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDsw +OTA3BggrBgEFBQcwAYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1H +NDA/BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2NybC9DbGFz +ZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFDCYfcyhU5t5iKV+8Pr8LV +WZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/ +w8w2VMhrCILonVjnhLX8VHNMkc3Xy17JgvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KB +B/QNi7AB5U9kB7M5wfGr2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACg +jhl1gijANMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkfzmrw + + + +
\ No newline at end of file diff --git a/tests/test_data/sii-dte/DTE--76399752-9--33--25568.xml b/tests/test_data/sii-dte/DTE--76399752-9--33--25568.xml new file mode 100644 index 00000000..bdecf2ff --- /dev/null +++ b/tests/test_data/sii-dte/DTE--76399752-9--33--25568.xml @@ -0,0 +1,129 @@ + + + + + + + 33 + 25568 + 2019-03-29 + 1 + 1 + 2 + + + 76399752-9 + COMERCIALIZADORA INNOVA MOBEL SPA + COMERCIALIZACION DE PRODUCTOS PARA EL HOGAR + 87 472133 + ANGEL.PEZO@APCASESORIAS.CL + 310001 + 078904860 + LOS CIPRESES 2834 + LA PINTANA + SANTIAGO + + + 96874030-K + EMPRESAS LA POLAR S.A. + VENTA AL POR MENOR EN COMERCIOS DE VESTU + N Lote Despacho: 20921554 / N Sello: 660620 + AVDA. SANTA CLARA 207 62 CIUDAD EMPRESARIAL + HUECHURABA + SANTIAGO + + + 194111 + 19.00 + 36881 + 230992 + + + + 1 + + SKU + 19586316 + + JUEGO_LIVI - CHOCOLATE + ROMA 3.1.1 + 1.00 + UN + 194111.00 + 194111 + + + 1 + 801 + 638370 + 2019-03-28 + +
76399752-933255682019-03-2996874030-KEMPRESAS LA POLAR S.A.230992JUEGO_LIVI - CHOCOLATE76399752-9COMERCIALIZADORA INNOVA MOBEL SPA3325568255682019-03-287EKJUPVmefPeVcgm9Q81Dp6q1MP+UvccH0mfsugbuK6UPYLn3tO7DxpZQIgoQC9LgdwYTtC9EHajZlgsk0iZjw==Aw==300byDdqUAqlKoALIOrNLlGmuFCOk866v4BQvnZqdiqGvrHk6jneiTMjYBSMB2GaY4t/dTFgVSOsqa/BnkRskel7Q==2019-03-28T13:59:52
viuqScpeQueqAnye1MLhttAAOAnO4raWlPdJ5kbSpUEeUT+pZgE/rr79kgVqirnIRM+HUpB3Yt4fbyMaARGqtA==
+ 2019-03-28T13:59:52 + +
+ + + + + + + + + +tk/D3mfO/KtdWyFXYZHe7dtYijg= + + + +wwOMQuFqa6c5gzYSJ5PWfo0OiAf+yNcJK6wx4xJ3VNehlAcMrUB2q+rK/DDhCvjxAoX4NxBACiFD +MrTMIfvxrwXjLd1oX37lSFOtsWX6JxL0SV+tLF7qvWCu1Yzw8ypUf7GDkbymJkoTYDF9JFF8kYU4 +FdU2wttiwne9XH8QFHgXsocKP/aygwiOeGqiNX9o/O5XS2GWpt+KM20jrvtYn7UFMED/3aPacCb1 +GABizr8mlVEZggZgJunMDChpFQyEigSXMK5I737Ac8D2bw7WB47Wj1WBL3sCFRDlXUXtnMvChBVp +0HRUXYuKHyfpCzqIBXygYrIZexxXgOSnKu/yGg== + + + + + +0tBhZ9dE624+LIifJE5Bz4NnYt2m9pKHFTqJTbEH4JCzvgdn6hLUEg3OYvWD2hjuEe9P78f6G5w6 +U3vGiYf9S4OKSOjJKOFsffEEzOHqpYe8Opx9OzBi4cRLaE72R5PPDK3JQg8dNy0w0nfaYhD98ZTw +f5B/tp21X4DuTeNeC8K7cNDlx55HXFTINtNchYkO2DbXmxrdhKS2jeI81KGqIp4Z+yH+pQRofegr +9N/SU4b8Ib9ue8t25tpxz2jsHlBLokXkgsx98IS7MGvHIxkuEFBibVqHp1IRsKwM2RzqxAwctiD/ +SobU35wgtdXK6wYYIIQNN+Zdv8AjisQpom3Rcw== + +AQAB + + + + +MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNMMRgwFgYDVQQK +Ew9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20gQXV0b3JpZGFkIENlcnRpZmlj +YWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwgLSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0Bh +Y2VwdGEuY29tMRMwEQYDVQQFEwo5NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0 +MDI1NFowgY8xCzAJBgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMT +GkdJQU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwuYXJhdmVu +YUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2LdpvaShxU6iU2xB+CQs74HZ+oS1BINzmL1 +g9oY7hHvT+/H+hucOlN7xomH/UuDikjoySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIP +HTctMNJ32mIQ/fGU8H+Qf7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNSh +qiKeGfsh/qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1ah6dS +EbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEAAaOCAkowggJGMB8G +A1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1UdDgQWBBSHoSD4nd2UJuwzmJnJud0L +WSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG ++EIBAQQEAwIFoDB1BgNVHSAEbjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8v +YWNnNC5hY2VwdGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t +IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkwNTAtOKAkBggr +BgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZvQGFjZXB0YS5jb20waAYDVR0R +BGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTIt +OQYIKwYBBAHBAQKBHWRhbmllbC5hcmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDsw +OTA3BggrBgEFBQcwAYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1H +NDA/BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2NybC9DbGFz +ZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFDCYfcyhU5t5iKV+8Pr8LV +WZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/ +w8w2VMhrCILonVjnhLX8VHNMkc3Xy17JgvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KB +B/QNi7AB5U9kB7M5wfGr2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACg +jhl1gijANMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkfzmrw + + + +
diff --git a/tests/test_data/sii-rtc/AEC--76354771-K--33---170--SEQ-2.xml b/tests/test_data/sii-rtc/AEC--76354771-K--33--170--SEQ-2.xml similarity index 100% rename from tests/test_data/sii-rtc/AEC--76354771-K--33---170--SEQ-2.xml rename to tests/test_data/sii-rtc/AEC--76354771-K--33--170--SEQ-2.xml diff --git a/tests/test_data/sii-rtc/AEC--76399752-9--33--25568--SEQ-1.xml b/tests/test_data/sii-rtc/AEC--76399752-9--33--25568--SEQ-1.xml new file mode 100644 index 00000000..4c8c579d --- /dev/null +++ b/tests/test_data/sii-rtc/AEC--76399752-9--33--25568--SEQ-1.xml @@ -0,0 +1,322 @@ + + + + + 76399752-9 + 76389992-6 + fynpal-app-notif-st-capital@fynpal.com + 2019-04-04T09:09:52 + + + + + + + + + + + 33 + 25568 + 2019-03-29 + 1 + 1 + 2 + + + 76399752-9 + COMERCIALIZADORA INNOVA MOBEL SPA + COMERCIALIZACION DE PRODUCTOS PARA EL HOGAR + 87 472133 + ANGEL.PEZO@APCASESORIAS.CL + 310001 + 078904860 + LOS CIPRESES 2834 + LA PINTANA + SANTIAGO + + + 96874030-K + EMPRESAS LA POLAR S.A. + VENTA AL POR MENOR EN COMERCIOS DE VESTU + N Lote Despacho: 20921554 / N Sello: 660620 + AVDA. SANTA CLARA 207 62 CIUDAD EMPRESARIAL + HUECHURABA + SANTIAGO + + + 194111 + 19.00 + 36881 + 230992 + + + + 1 + + SKU + 19586316 + + JUEGO_LIVI - CHOCOLATE + ROMA 3.1.1 + 1.00 + UN + 194111.00 + 194111 + + + 1 + 801 + 638370 + 2019-03-28 + +
76399752-933255682019-03-2996874030-KEMPRESAS LA POLAR S.A.230992JUEGO_LIVI - CHOCOLATE76399752-9COMERCIALIZADORA INNOVA MOBEL SPA3325568255682019-03-287EKJUPVmefPeVcgm9Q81Dp6q1MP+UvccH0mfsugbuK6UPYLn3tO7DxpZQIgoQC9LgdwYTtC9EHajZlgsk0iZjw==Aw==300byDdqUAqlKoALIOrNLlGmuFCOk866v4BQvnZqdiqGvrHk6jneiTMjYBSMB2GaY4t/dTFgVSOsqa/BnkRskel7Q==2019-03-28T13:59:52
viuqScpeQueqAnye1MLhttAAOAnO4raWlPdJ5kbSpUEeUT+pZgE/rr79kgVqirnIRM+HUpB3Yt4fbyMaARGqtA==
+ 2019-03-28T13:59:52 + +
+ + + + + + + + + +tk/D3mfO/KtdWyFXYZHe7dtYijg= + + + +wwOMQuFqa6c5gzYSJ5PWfo0OiAf+yNcJK6wx4xJ3VNehlAcMrUB2q+rK/DDhCvjxAoX4NxBACiFD +MrTMIfvxrwXjLd1oX37lSFOtsWX6JxL0SV+tLF7qvWCu1Yzw8ypUf7GDkbymJkoTYDF9JFF8kYU4 +FdU2wttiwne9XH8QFHgXsocKP/aygwiOeGqiNX9o/O5XS2GWpt+KM20jrvtYn7UFMED/3aPacCb1 +GABizr8mlVEZggZgJunMDChpFQyEigSXMK5I737Ac8D2bw7WB47Wj1WBL3sCFRDlXUXtnMvChBVp +0HRUXYuKHyfpCzqIBXygYrIZexxXgOSnKu/yGg== + + + + + +0tBhZ9dE624+LIifJE5Bz4NnYt2m9pKHFTqJTbEH4JCzvgdn6hLUEg3OYvWD2hjuEe9P78f6G5w6 +U3vGiYf9S4OKSOjJKOFsffEEzOHqpYe8Opx9OzBi4cRLaE72R5PPDK3JQg8dNy0w0nfaYhD98ZTw +f5B/tp21X4DuTeNeC8K7cNDlx55HXFTINtNchYkO2DbXmxrdhKS2jeI81KGqIp4Z+yH+pQRofegr +9N/SU4b8Ib9ue8t25tpxz2jsHlBLokXkgsx98IS7MGvHIxkuEFBibVqHp1IRsKwM2RzqxAwctiD/ +SobU35wgtdXK6wYYIIQNN+Zdv8AjisQpom3Rcw== + +AQAB + + + + +MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNMMRgwFgYDVQQK +Ew9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20gQXV0b3JpZGFkIENlcnRpZmlj +YWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwgLSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0Bh +Y2VwdGEuY29tMRMwEQYDVQQFEwo5NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0 +MDI1NFowgY8xCzAJBgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMT +GkdJQU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwuYXJhdmVu +YUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2LdpvaShxU6iU2xB+CQs74HZ+oS1BINzmL1 +g9oY7hHvT+/H+hucOlN7xomH/UuDikjoySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIP +HTctMNJ32mIQ/fGU8H+Qf7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNSh +qiKeGfsh/qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1ah6dS +EbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEAAaOCAkowggJGMB8G +A1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1UdDgQWBBSHoSD4nd2UJuwzmJnJud0L +WSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG ++EIBAQQEAwIFoDB1BgNVHSAEbjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8v +YWNnNC5hY2VwdGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t +IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkwNTAtOKAkBggr +BgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZvQGFjZXB0YS5jb20waAYDVR0R +BGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTIt +OQYIKwYBBAHBAQKBHWRhbmllbC5hcmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDsw +OTA3BggrBgEFBQcwAYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1H +NDA/BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2NybC9DbGFz +ZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFDCYfcyhU5t5iKV+8Pr8LV +WZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/ +w8w2VMhrCILonVjnhLX8VHNMkc3Xy17JgvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KB +B/QNi7AB5U9kB7M5wfGr2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACg +jhl1gijANMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkfzmrw + + + +
2019-04-04T09:09:52
f4zqbr0mGWngOkRb16XxngcE96o= +vzMWkBYcBj9ziyTa0Aqkw9a1fePGC4AfOmt/v36OEajaKszI5Fz/E53deCZU+EQS +h4puWlbiMxq6OiTi3ebrb/Kwlc1ZxmXkKzjIveDmeZhiVI9bR7o4zg3BGXdov+0L +n+d4nvtLrxA1M78WhSp2IK9KOyxOhazXsTu4iYqoJH+zh3VTwKUncpJTnA6Aytqx +YC+zIxAty2bfs7DsqP9y7DTjAsS//nNjRJGZ80yjVr8t5l/ystveobLCvPVYZe+f +ezH8wN+M4lQ9EQvPJG5gAhVkudttJCysXTJkFGgV1sywgH9DxUHOcvwUpfDN6o0x +OxXwwfwLQRgyPZFBpCjleg== + +0tBhZ9dE624+LIifJE5Bz4NnYt2m9pKHFTqJTbEH4JCzvgdn6hLUEg3OYvWD2hju +Ee9P78f6G5w6U3vGiYf9S4OKSOjJKOFsffEEzOHqpYe8Opx9OzBi4cRLaE72R5PP +DK3JQg8dNy0w0nfaYhD98ZTwf5B/tp21X4DuTeNeC8K7cNDlx55HXFTINtNchYkO +2DbXmxrdhKS2jeI81KGqIp4Z+yH+pQRofegr9N/SU4b8Ib9ue8t25tpxz2jsHlBL +okXkgsx98IS7MGvHIxkuEFBibVqHp1IRsKwM2RzqxAwctiD/SobU35wgtdXK6wYY +IIQNN+Zdv8AjisQpom3Rcw== +AQAB +MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNM +MRgwFgYDVQQKEw9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20g +QXV0b3JpZGFkIENlcnRpZmljYWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwg +LSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0BhY2VwdGEuY29tMRMwEQYDVQQFEwo5 +NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0MDI1NFowgY8xCzAJ +BgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMTGkdJ +QU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwu +YXJhdmVuYUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2Ld +pvaShxU6iU2xB+CQs74HZ+oS1BINzmL1g9oY7hHvT+/H+hucOlN7xomH/UuDikjo +ySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIPHTctMNJ32mIQ/fGU8H+Q +f7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNShqiKeGfsh +/qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1a +h6dSEbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEA +AaOCAkowggJGMB8GA1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1Ud +DgQWBBSHoSD4nd2UJuwzmJnJud0LWSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYw +FAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG+EIBAQQEAwIFoDB1BgNVHSAE +bjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8vYWNnNC5hY2Vw +dGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t +IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkw +NTAtOKAkBggrBgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZv +QGFjZXB0YS5jb20waAYDVR0RBGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05 +oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTItOQYIKwYBBAHBAQKBHWRhbmllbC5h +cmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDswOTA3BggrBgEFBQcw +AYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1HNDA/ +BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2Ny +bC9DbGFzZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFD +CYfcyhU5t5iKV+8Pr8LVWZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb +9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/w8w2VMhrCILonVjnhLX8VHNMkc3Xy17J +gvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KBB/QNi7AB5U9kB7M5wfGr +2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACgjhl1gijA +NMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkf +zmrw + +
+ + + 1 + + 33 + 76399752-9 + 96874030-K + 25568 + 2019-03-29 + 230992 + + + 76399752-9 + COMERCIALIZADORA INNOVA MOBEL SPA + LOS CIPRESES 2834 + camilo.perez@innovamobel.cl + + 76399752-9 + COMERCIALIZADORA INNOVA MOBEL SPA + + Se declara bajo juramento que COMERCIALIZADORA INNOVA MOBEL SPA, RUT 76399752-9 ha puesto a disposici髇 del cesionario ST CAPITAL S.A., RUT 76389992-6, el o los documentos donde constan los recibos de las mercader韆s entregadas o servicios prestados, entregados por parte del deudor de la factura EMPRESAS LA POLAR S.A., RUT 96874030-K, deacuerdo a lo establecido en la Ley N19.983. + + + 76389992-6 + ST CAPITAL S.A. + Isidora Goyenechea 2939 Oficina 602 + fynpal-app-notif-st-capital@fynpal.com + + 230992 + 2019-04-28 + 2019-04-04T09:09:52 + qvEd+eB0/sEOf1o/7FpHckvpcLg= +V+K6hraGHgDnk9LJoEC/A2jOPgfnb7akYxZ8n6TlKMxf+1hHb9njiaiJOfyp1owV +/zQHd74CXlFy9v52pPpwVA9BFbda0J/AZCt8lmT21/WBqDwjrByhsfZ/+kmRxdxg +ZnfFV/41zHtlo4ifUkyQLinLJHsM5KCjHiKMvQhc9QF/f9V0K9iPV0O8bJcPsYdZ +H2JvmyWjJBB2Z1wEg7lQLlFmztM5tLGM8Iy58ENYiwfNbkVFx0Kiu/vUSqtNKDlu +eb+1D4npn9KletXlgp7xFUH8lGJhk0QfjC691z51E0Pf1klcOz0tJChteiBDWSFZ +SMCiVGE1L4Pv8cUN+CaRSg== + +0tBhZ9dE624+LIifJE5Bz4NnYt2m9pKHFTqJTbEH4JCzvgdn6hLUEg3OYvWD2hju +Ee9P78f6G5w6U3vGiYf9S4OKSOjJKOFsffEEzOHqpYe8Opx9OzBi4cRLaE72R5PP +DK3JQg8dNy0w0nfaYhD98ZTwf5B/tp21X4DuTeNeC8K7cNDlx55HXFTINtNchYkO +2DbXmxrdhKS2jeI81KGqIp4Z+yH+pQRofegr9N/SU4b8Ib9ue8t25tpxz2jsHlBL +okXkgsx98IS7MGvHIxkuEFBibVqHp1IRsKwM2RzqxAwctiD/SobU35wgtdXK6wYY +IIQNN+Zdv8AjisQpom3Rcw== +AQAB +MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNM +MRgwFgYDVQQKEw9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20g +QXV0b3JpZGFkIENlcnRpZmljYWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwg +LSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0BhY2VwdGEuY29tMRMwEQYDVQQFEwo5 +NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0MDI1NFowgY8xCzAJ +BgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMTGkdJ +QU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwu +YXJhdmVuYUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2Ld +pvaShxU6iU2xB+CQs74HZ+oS1BINzmL1g9oY7hHvT+/H+hucOlN7xomH/UuDikjo +ySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIPHTctMNJ32mIQ/fGU8H+Q +f7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNShqiKeGfsh +/qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1a +h6dSEbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEA +AaOCAkowggJGMB8GA1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1Ud +DgQWBBSHoSD4nd2UJuwzmJnJud0LWSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYw +FAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG+EIBAQQEAwIFoDB1BgNVHSAE +bjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8vYWNnNC5hY2Vw +dGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t +IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkw +NTAtOKAkBggrBgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZv +QGFjZXB0YS5jb20waAYDVR0RBGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05 +oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTItOQYIKwYBBAHBAQKBHWRhbmllbC5h +cmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDswOTA3BggrBgEFBQcw +AYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1HNDA/ +BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2Ny +bC9DbGFzZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFD +CYfcyhU5t5iKV+8Pr8LVWZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb +9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/w8w2VMhrCILonVjnhLX8VHNMkc3Xy17J +gvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KBB/QNi7AB5U9kB7M5wfGr +2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACgjhl1gijA +NMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkf +zmrw + + +
+
KUMkp+Ku3epZ2QCjeZ75tiEncMQ= +hBUBX/XDhmNokXXfZ7R3drK78N5SX8xLn6sYyAaBTut4wILA4kHB9BW45oV0wS/A +53l7EX5yg42KHRXQ+vVzc5R+zYpGvgAPnv8eM2lCQKmyEdhR0YoQ1YnRL/7vchJ2 +8TnrTxSMMePj589rOAUD8IeTr1vKyfdih+r6maTA6C+O2dzVf3zl/GtTstoZdX2B +ZEf6/yzX9T7kFQ27zZ3WKGLFFjQKaQa2Nh/dIPEcfci1KgCZhozGPw9++xPG3P9I +ewG3h95UvHjL1jOag3grvrEG+yCYlUpMq4vnUTuGfbwcW7nYq+HSU0IKDPccmzlh +PCUn28yVEm+JlH0/P8QL3w== + +0tBhZ9dE624+LIifJE5Bz4NnYt2m9pKHFTqJTbEH4JCzvgdn6hLUEg3OYvWD2hju +Ee9P78f6G5w6U3vGiYf9S4OKSOjJKOFsffEEzOHqpYe8Opx9OzBi4cRLaE72R5PP +DK3JQg8dNy0w0nfaYhD98ZTwf5B/tp21X4DuTeNeC8K7cNDlx55HXFTINtNchYkO +2DbXmxrdhKS2jeI81KGqIp4Z+yH+pQRofegr9N/SU4b8Ib9ue8t25tpxz2jsHlBL +okXkgsx98IS7MGvHIxkuEFBibVqHp1IRsKwM2RzqxAwctiD/SobU35wgtdXK6wYY +IIQNN+Zdv8AjisQpom3Rcw== +AQAB +MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNM +MRgwFgYDVQQKEw9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20g +QXV0b3JpZGFkIENlcnRpZmljYWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwg +LSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0BhY2VwdGEuY29tMRMwEQYDVQQFEwo5 +NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0MDI1NFowgY8xCzAJ +BgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMTGkdJ +QU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwu +YXJhdmVuYUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2Ld +pvaShxU6iU2xB+CQs74HZ+oS1BINzmL1g9oY7hHvT+/H+hucOlN7xomH/UuDikjo +ySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIPHTctMNJ32mIQ/fGU8H+Q +f7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNShqiKeGfsh +/qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1a +h6dSEbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEA +AaOCAkowggJGMB8GA1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1Ud +DgQWBBSHoSD4nd2UJuwzmJnJud0LWSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYw +FAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG+EIBAQQEAwIFoDB1BgNVHSAE +bjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8vYWNnNC5hY2Vw +dGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t +IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkw +NTAtOKAkBggrBgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZv +QGFjZXB0YS5jb20waAYDVR0RBGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05 +oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTItOQYIKwYBBAHBAQKBHWRhbmllbC5h +cmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDswOTA3BggrBgEFBQcw +AYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1HNDA/ +BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2Ny +bC9DbGFzZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFD +CYfcyhU5t5iKV+8Pr8LVWZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb +9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/w8w2VMhrCILonVjnhLX8VHNMkc3Xy17J +gvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KBB/QNi7AB5U9kB7M5wfGr +2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACgjhl1gijA +NMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkf +zmrw + +
\ No newline at end of file diff --git a/tests/test_dte_constants.py b/tests/test_dte_constants.py new file mode 100644 index 00000000..b680fb48 --- /dev/null +++ b/tests/test_dte_constants.py @@ -0,0 +1,128 @@ +import unittest + +from cl_sii.dte import constants # noqa: F401 +from cl_sii.dte.constants import TipoDteEnum + + +class TipoDteEnumTest(unittest.TestCase): + + def test_members(self): + self.assertSetEqual( + {x for x in TipoDteEnum}, + { + TipoDteEnum.FACTURA_ELECTRONICA, + TipoDteEnum.FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA, + TipoDteEnum.FACTURA_COMPRA_ELECTRONICA, + TipoDteEnum.GUIA_DESPACHO_ELECTRONICA, + TipoDteEnum.NOTA_DEBITO_ELECTRONICA, + TipoDteEnum.NOTA_CREDITO_ELECTRONICA, + } + ) + + def test_FACTURA_ELECTRONICA(self): + value = TipoDteEnum.FACTURA_ELECTRONICA + + self.assertEqual(value.name, 'FACTURA_ELECTRONICA') + self.assertEqual(value.value, 33) + + assertions = [ + (value.is_factura, True), + (value.is_factura_venta, True), + (value.is_factura_compra, False), + (value.is_nota, False), + (value.emisor_is_vendedor, True), + (value.receptor_is_vendedor, False), + ] + + for (result, expected) in assertions: + self.assertEqual(result, expected) + + def test_FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA(self): + value = TipoDteEnum.FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA + + self.assertEqual(value.name, 'FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA') + self.assertEqual(value.value, 34) + + assertions = [ + (value.is_factura, True), + (value.is_factura_venta, True), + (value.is_factura_compra, False), + (value.is_nota, False), + (value.emisor_is_vendedor, True), + (value.receptor_is_vendedor, False), + ] + + for (result, expected) in assertions: + self.assertTrue(result is expected) + + def test_FACTURA_COMPRA_ELECTRONICA(self): + value = TipoDteEnum.FACTURA_COMPRA_ELECTRONICA + + self.assertEqual(value.name, 'FACTURA_COMPRA_ELECTRONICA') + self.assertEqual(value.value, 46) + + assertions = [ + (value.is_factura, True), + (value.is_factura_venta, False), + (value.is_factura_compra, True), + (value.is_nota, False), + (value.emisor_is_vendedor, False), + (value.receptor_is_vendedor, True), + ] + + for (result, expected) in assertions: + self.assertTrue(result is expected) + + def test_GUIA_DESPACHO_ELECTRONICA(self): + value = TipoDteEnum.GUIA_DESPACHO_ELECTRONICA + + self.assertEqual(value.name, 'GUIA_DESPACHO_ELECTRONICA') + self.assertEqual(value.value, 52) + + assertions = [ + (value.is_factura, False), + (value.is_factura_venta, False), + (value.is_factura_compra, False), + (value.is_nota, False), + (value.emisor_is_vendedor, False), + (value.receptor_is_vendedor, False), + ] + + for (result, expected) in assertions: + self.assertTrue(result is expected) + + def test_NOTA_DEBITO_ELECTRONICA(self): + value = TipoDteEnum.NOTA_DEBITO_ELECTRONICA + + self.assertEqual(value.name, 'NOTA_DEBITO_ELECTRONICA') + self.assertEqual(value.value, 56) + + assertions = [ + (value.is_factura, False), + (value.is_factura_venta, False), + (value.is_factura_compra, False), + (value.is_nota, True), + (value.emisor_is_vendedor, False), + (value.receptor_is_vendedor, False), + ] + + for (result, expected) in assertions: + self.assertTrue(result is expected) + + def test_NOTA_CREDITO_ELECTRONICA(self): + value = TipoDteEnum.NOTA_CREDITO_ELECTRONICA + + self.assertEqual(value.name, 'NOTA_CREDITO_ELECTRONICA') + self.assertEqual(value.value, 61) + + assertions = [ + (value.is_factura, False), + (value.is_factura_venta, False), + (value.is_factura_compra, False), + (value.is_nota, True), + (value.emisor_is_vendedor, False), + (value.receptor_is_vendedor, False), + ] + + for (result, expected) in assertions: + self.assertTrue(result is expected) diff --git a/tests/test_dte_data_models.py b/tests/test_dte_data_models.py index 648c7251..d0d4acad 100644 --- a/tests/test_dte_data_models.py +++ b/tests/test_dte_data_models.py @@ -1,4 +1,6 @@ +import dataclasses import unittest +from datetime import date, datetime from cl_sii.rut import Rut # noqa: F401 @@ -11,38 +13,187 @@ class DteNaturalKeyTest(unittest.TestCase): - # TODO: implement! - pass + def setUp(self) -> None: + super().setUp() + + self.dte_nk_1 = DteNaturalKey( + emisor_rut=Rut('76354771-K'), + tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA, + folio=170, + ) + + def test_init_fail(self) -> None: + # TODO: implement for 'DteNaturalKey()' + pass + + def test_as_dict(self) -> None: + self.assertDictEqual( + self.dte_nk_1.as_dict(), + dict( + emisor_rut=Rut('76354771-K'), + tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA, + folio=170, + ) + ) + + def test_slug(self) -> None: + self.assertEqual(self.dte_nk_1.slug, '76354771-K--33--170') class DteDataL0Test(unittest.TestCase): - # TODO: implement! - pass + def setUp(self) -> None: + super().setUp() + + self.dte_l0_1 = DteDataL0( + emisor_rut=Rut('76354771-K'), + tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA, + folio=170, + ) + + def test_init_fail(self) -> None: + # TODO: implement for 'DteDataL0()' + pass + + def test_as_dict(self) -> None: + self.assertDictEqual( + self.dte_l0_1.as_dict(), + dict( + emisor_rut=Rut('76354771-K'), + tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA, + folio=170, + )) + + def test_natural_key(self) -> None: + self.assertEqual( + self.dte_l0_1.natural_key, + DteNaturalKey( + emisor_rut=Rut('76354771-K'), + tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA, + folio=170, + )) class DteDataL1Test(unittest.TestCase): - # TODO: implement! - pass + def setUp(self) -> None: + super().setUp() + + self.dte_l1_1 = DteDataL1( + emisor_rut=Rut('76354771-K'), + tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA, + folio=170, + fecha_emision_date=date(2019, 4, 1), + receptor_rut=Rut('96790240-3'), + monto_total=2996301, + ) + + def test_init_fail(self) -> None: + # TODO: implement for 'DteDataL1()' + pass + + def test_as_dict(self) -> None: + self.assertDictEqual( + self.dte_l1_1.as_dict(), + dict( + emisor_rut=Rut('76354771-K'), + tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA, + folio=170, + fecha_emision_date=date(2019, 4, 1), + receptor_rut=Rut('96790240-3'), + monto_total=2996301, + )) + + def test_vendedor_rut_deudor_rut(self) -> None: + emisor_rut = self.dte_l1_1.emisor_rut + receptor_rut = self.dte_l1_1.receptor_rut + dte_factura_venta = dataclasses.replace( + self.dte_l1_1, tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA) + dte_factura_venta_exenta = dataclasses.replace( + self.dte_l1_1, tipo_dte=TipoDteEnum.FACTURA_NO_AFECTA_O_EXENTA_ELECTRONICA) + dte_factura_compra = dataclasses.replace( + self.dte_l1_1, tipo_dte=TipoDteEnum.FACTURA_COMPRA_ELECTRONICA) + dte_nota_credito = dataclasses.replace( + self.dte_l1_1, tipo_dte=TipoDteEnum.NOTA_CREDITO_ELECTRONICA) + + self.assertEqual(dte_factura_venta.vendedor_rut, emisor_rut) + self.assertEqual(dte_factura_venta_exenta.vendedor_rut, emisor_rut) + self.assertEqual(dte_factura_compra.vendedor_rut, receptor_rut) + with self.assertRaises(ValueError) as cm: + self.assertIsNone(dte_nota_credito.vendedor_rut) + self.assertEqual( + cm.exception.args, + ("Concept \"vendedor\" does not apply for this 'tipo_dte'.", dte_nota_credito.tipo_dte)) + + self.assertEqual(dte_factura_venta.deudor_rut, receptor_rut) + self.assertEqual(dte_factura_venta_exenta.deudor_rut, receptor_rut) + self.assertEqual(dte_factura_compra.deudor_rut, emisor_rut) + with self.assertRaises(ValueError) as cm: + self.assertIsNone(dte_nota_credito.deudor_rut) + self.assertEqual( + cm.exception.args, + ("Concept \"deudor\" does not apply for this 'tipo_dte'.", dte_nota_credito.tipo_dte)) class DteDataL2Test(unittest.TestCase): - # TODO: implement! - pass + def setUp(self) -> None: + super().setUp() + + self.dte_l2_1 = DteDataL2( + emisor_rut=Rut('76354771-K'), + tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA, + folio=170, + fecha_emision_date=date(2019, 4, 1), + receptor_rut=Rut('96790240-3'), + monto_total=2996301, + emisor_razon_social='INGENIERIA ENACON SPA', + receptor_razon_social='MINERA LOS PELAMBRES', + fecha_vencimiento_date=None, + firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40), + signature_value=None, + signature_x509_cert_pem=None, + emisor_giro='Ingenieria y Construccion', + emisor_email='hello@example.com', + receptor_email=None, + ) + + def test_init_fail(self) -> None: + # TODO: implement for 'DteDataL2()' + pass + + def test_as_dict(self) -> None: + self.assertDictEqual( + self.dte_l2_1.as_dict(), + dict( + emisor_rut=Rut('76354771-K'), + tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA, + folio=170, + fecha_emision_date=date(2019, 4, 1), + receptor_rut=Rut('96790240-3'), + monto_total=2996301, + emisor_razon_social='INGENIERIA ENACON SPA', + receptor_razon_social='MINERA LOS PELAMBRES', + fecha_vencimiento_date=None, + firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40), + signature_value=None, + signature_x509_cert_pem=None, + emisor_giro='Ingenieria y Construccion', + emisor_email='hello@example.com', + receptor_email=None, + )) class FunctionsTest(unittest.TestCase): def test_validate_contribuyente_razon_social(self) -> None: - # TODO: implement! + # TODO: implement for 'validate_contribuyente_razon_social' pass def test_validate_dte_folio(self) -> None: - # TODO: implement! + # TODO: implement for 'validate_dte_folio' pass def test_validate_dte_monto_total(self) -> None: - # TODO: implement! + # TODO: implement for 'validate_dte_monto_total' pass diff --git a/tests/test_dte_parse.py b/tests/test_dte_parse.py index fedb651b..1944f17d 100644 --- a/tests/test_dte_parse.py +++ b/tests/test_dte_parse.py @@ -1,9 +1,11 @@ import difflib import io import unittest -from datetime import date +from datetime import date, datetime import cl_sii.dte.constants +from cl_sii.libs import crypto_utils +from cl_sii.libs import encoding_utils from cl_sii.libs import xml_utils from cl_sii.rut import Rut @@ -16,21 +18,95 @@ from .utils import read_test_file_bytes -_TEST_DTE_NEEDS_CLEAN_FILE_PATH = 'test_data/sii-dte/DTE--76354771-K--33--170.xml' - - class OthersTest(unittest.TestCase): def test_DTE_XML_SCHEMA_OBJ(self) -> None: # TODO: implement pass - def test_integration_ok(self) -> None: - # TODO: split in separate tests, with more coverage. - dte_bad_xml_file_path = _TEST_DTE_NEEDS_CLEAN_FILE_PATH +class FunctionValidateDteXmlTest(unittest.TestCase): + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + cls.dte_bad_xml_1_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76354771-K--33--170.xml') + cls.dte_bad_xml_2_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76399752-9--33--25568.xml') + + cls.dte_clean_xml_1_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76354771-K--33--170--cleaned.xml') + cls.dte_clean_xml_2_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76399752-9--33--25568--cleaned.xml') + + def test_validate_dte_xml_ok_dte_1(self) -> None: + xml_doc = xml_utils.parse_untrusted_xml(self.dte_clean_xml_1_xml_bytes) + validate_dte_xml(xml_doc) + + self.assertEqual( + xml_doc.getroottree().getroot().tag, + '{%s}DTE' % DTE_XMLNS) + + def test_validate_dte_xml_ok_dte_2(self) -> None: + xml_doc = xml_utils.parse_untrusted_xml(self.dte_clean_xml_2_xml_bytes) + validate_dte_xml(xml_doc) + + self.assertEqual( + xml_doc.getroottree().getroot().tag, + '{%s}DTE' % DTE_XMLNS) + + def test_validate_dte_xml_fail_x(self) -> None: + # TODO: implement more cases + pass + + def test_validate_dte_xml_fail_dte_1(self) -> None: + file_bytes = self.dte_bad_xml_1_xml_bytes + xml_doc = xml_utils.parse_untrusted_xml(file_bytes) + + self.assertEqual( + xml_doc.getroottree().getroot().tag, + 'DTE') + + with self.assertRaises(xml_utils.XmlSchemaDocValidationError) as cm: + validate_dte_xml(xml_doc) + self.assertSequenceEqual( + cm.exception.args, + ("Element 'DTE': No matching global declaration available for the validation root., " + "line 2", ) + ) + + def test_validate_dte_xml_fail_dte_2(self) -> None: + file_bytes = self.dte_bad_xml_2_xml_bytes + xml_doc = xml_utils.parse_untrusted_xml(file_bytes) + + self.assertEqual( + xml_doc.getroottree().getroot().tag, + 'DTE') + + with self.assertRaises(xml_utils.XmlSchemaDocValidationError) as cm: + validate_dte_xml(xml_doc) + self.assertSequenceEqual( + cm.exception.args, + ("Element 'DTE': No matching global declaration available for the validation root., " + "line 2", ) + ) + + +class FunctionCleanDteXmlTest(unittest.TestCase): - file_bytes = read_test_file_bytes(dte_bad_xml_file_path) + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + cls.dte_bad_xml_1_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76354771-K--33--170.xml') + cls.dte_bad_xml_2_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76399752-9--33--25568.xml') + + def test_clean_dte_xml_ok_1(self) -> None: + file_bytes = self.dte_bad_xml_1_xml_bytes xml_doc = xml_utils.parse_untrusted_xml(file_bytes) self.assertEqual( @@ -44,8 +120,6 @@ def test_integration_ok(self) -> None: ("Element 'DTE': No matching global declaration available for the validation root., " "line 2", ) ) - # This would raise: - # parse_dte_xml(xml_doc) xml_doc_cleaned, modified = clean_dte_xml( xml_doc, @@ -57,10 +131,6 @@ def test_integration_ok(self) -> None: # This will not raise. validate_dte_xml(xml_doc_cleaned) - self.assertEqual( - xml_doc_cleaned.getroottree().getroot().tag, - '{%s}DTE' % DTE_XMLNS) - f = io.BytesIO() xml_utils.write_xml_doc(xml_doc_cleaned, f) file_bytes_rewritten = f.getvalue() @@ -68,21 +138,6 @@ def test_integration_ok(self) -> None: xml_doc_rewritten = xml_utils.parse_untrusted_xml(file_bytes_rewritten) validate_dte_xml(xml_doc_rewritten) - parsed_dte_rewritten = parse_dte_xml(xml_doc_cleaned) - - self.assertDictEqual( - dict(parsed_dte_rewritten.as_dict()), - dict( - emisor_rut=Rut('76354771-K'), - tipo_dte=cl_sii.dte.constants.TipoDteEnum.FACTURA_ELECTRONICA, - folio=170, - fecha_emision_date=date(2019, 4, 1), - receptor_rut=Rut('96790240-3'), - monto_total=2996301, - emisor_razon_social='INGENIERIA ENACON SPA', - receptor_razon_social='MINERA LOS PELAMBRES', - fecha_vencimiento_date=None, - )) expected_file_bytes_diff = ( b'--- \n', @@ -124,41 +179,220 @@ def test_integration_ok(self) -> None: expected_file_bytes_diff ) + def test_clean_dte_xml_ok_2(self) -> None: + file_bytes = self.dte_bad_xml_2_xml_bytes + xml_doc = xml_utils.parse_untrusted_xml(file_bytes) -class FunctionCleanDteXmlTest(unittest.TestCase): + self.assertEqual( + xml_doc.getroottree().getroot().tag, + 'DTE') - def test_clean_dte_xml_ok(self) -> None: - # TODO: implement - pass + with self.assertRaises(xml_utils.XmlSchemaDocValidationError) as cm: + validate_dte_xml(xml_doc) + self.assertSequenceEqual( + cm.exception.args, + ("Element 'DTE': No matching global declaration available for the validation root., " + "line 2", ) + ) + + xml_doc_cleaned, modified = clean_dte_xml( + xml_doc, + set_missing_xmlns=True, + remove_doc_personalizado=True, + ) + self.assertTrue(modified) + + # This will not raise. + validate_dte_xml(xml_doc_cleaned) + + f = io.BytesIO() + xml_utils.write_xml_doc(xml_doc_cleaned, f) + file_bytes_rewritten = f.getvalue() + del f + + xml_doc_rewritten = xml_utils.parse_untrusted_xml(file_bytes_rewritten) + validate_dte_xml(xml_doc_rewritten) + + expected_file_bytes_diff = ( + b'--- \n', + b'+++ \n', + b'@@ -1,5 +1,5 @@\n', + b'-', + b'-', + b"+", + b'+', + b' ', + b' ', + b' ', + b'@@ -64,13 +64,13 @@\n', + b' ', + b' ', + b' ', + b'-', # noqa: E501 + b'-', + b'+', # noqa: E501 + b'+', + b' ', + b' ', + b'-', + b'+', + b' ', + b'-', + b'+', + b' tk/D3mfO/KtdWyFXYZHe7dtYijg=', + b' ', + b' ', + ) + + file_bytes_diff_gen = difflib.diff_bytes( + dfunc=difflib.unified_diff, + a=file_bytes.splitlines(), + b=file_bytes_rewritten.splitlines()) + self.assertSequenceEqual( + [diff_line for diff_line in file_bytes_diff_gen], + expected_file_bytes_diff + ) def test_clean_dte_xml_fail(self) -> None: - # TODO: implement + # TODO: implement for 'clean_dte_xml', for many cases. pass def test__set_dte_xml_missing_xmlns_ok(self) -> None: - # TODO: implement + # TODO: implement for '_set_dte_xml_missing_xmlns'. pass def test__set_dte_xml_missing_xmlns_fail(self) -> None: - # TODO: implement + # TODO: implement for '_set_dte_xml_missing_xmlns'. pass def test__remove_dte_xml_doc_personalizado_ok(self) -> None: - # TODO: implement + # TODO: implement for '_remove_dte_xml_doc_personalizado'. pass def test__remove_dte_xml_doc_personalizado_fail(self) -> None: - # TODO: implement + # TODO: implement for '_remove_dte_xml_doc_personalizado'. pass class FunctionParseDteXmlTest(unittest.TestCase): - # TODO: implement - pass + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + cls.dte_bad_xml_1_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76354771-K--33--170.xml') + cls.dte_bad_xml_2_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76399752-9--33--25568.xml') + + cls.dte_clean_xml_1_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76354771-K--33--170--cleaned.xml') + cls.dte_clean_xml_2_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76399752-9--33--25568--cleaned.xml') + + cls.dte_clean_xml_1_cert_pem_bytes = encoding_utils.clean_base64( + crypto_utils.remove_pem_cert_header_footer( + read_test_file_bytes('test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem'))) + cls.dte_clean_xml_2_cert_pem_bytes = encoding_utils.clean_base64( + crypto_utils.remove_pem_cert_header_footer( + read_test_file_bytes('test_data/sii-crypto/DTE--76399752-9--33--25568-cert.pem'))) + + cls._TEST_DTE_1_SIGNATURE_VALUE = encoding_utils.decode_base64_strict( + read_test_file_bytes( + 'test_data/sii-crypto/DTE--76354771-K--33--170-signature-value-base64.txt')) + cls._TEST_DTE_2_SIGNATURE_VALUE = encoding_utils.decode_base64_strict( + read_test_file_bytes( + 'test_data/sii-crypto/DTE--76399752-9--33--25568-signature-value-base64.txt')) + + def test_data(self): + self.assertEqual( + self._TEST_DTE_1_SIGNATURE_VALUE, + b'~\xc6\x0f\xe6\x9f\xe55\xfa\x1f\x03?\x0f9(k&:\x97t\x14\xcd6\xdb\xef\xe3\xf4\xd6' + b'\x0b\x16\xef\xc12\x00^\xbe\xc1.\xb9o_p\xbf\x1e\x97\xe8\xe2\xf8\xaak\x14\xae\xd4' + b'\xe1\x85\x80\xf9\xe4u\xd0\xc8\x17\x08\xfff\xc5]m\xd0~2\x1aJ\x93(Z\xf3tq\x84\x9a' + b'X\x05PX\xdd\xcf\xb2\xf4\x9e\xa81\xf7Ht\xc0\x18^\x11$\x17\x0f0\xebr\x87\xca\x17_' + b'\xd8O]\x9d\xb2\xa2\xc2\xa4\xb1\r\xc6#M>\xaf^\xc2\xcf\xad\x99') + self.assertEqual( + self._TEST_DTE_2_SIGNATURE_VALUE, + b"\xc3\x03\x8cB\xe1jk\xa79\x836\x12'\x93\xd6~\x8d\x0e\x88\x07\xfe\xc8\xd7\t+\xac1" + b"\xe3\x12wT\xd7\xa1\x94\x07\x0c\xad@v\xab\xea\xca\xfc0\xe1\n\xf8\xf1\x02\x85\xf87" + b"\x10@\n!C2\xb4\xcc!\xfb\xf1\xaf\x05\xe3-\xddh_~\xe5HS\xad\xb1e\xfa'\x12\xf4I_" + b"\xad,^\xea\xbd`\xae\xd5\x8c\xf0\xf3*T\x7f\xb1\x83\x91\xbc\xa6&J\x13`1}$Q|\x91" + b"\x858\x15\xd56\xc2\xdbb\xc2w\xbd\\\x7f\x10\x14x\x17\xb2\x87\n?\xf6\xb2\x83\x08" + b"\x8exj\xa25\x7fh\xfc\xeeWKa\x96\xa6\xdf\x8a3m#\xae\xfbX\x9f\xb5\x050@\xff\xdd" + b"\xa3\xdap&\xf5\x18\x00b\xce\xbf&\x95Q\x19\x82\x06`&\xe9\xcc\x0c(i\x15\x0c\x84" + b"\x8a\x04\x970\xaeH\xef~\xc0s\xc0\xf6o\x0e\xd6\x07\x8e\xd6\x8fU\x81/{\x02\x15\x10" + b"\xe5]E\xed\x9c\xcb\xc2\x84\x15i\xd0tT]\x8b\x8a\x1f'\xe9\x0b:\x88\x05|\xa0b\xb2" + b"\x19{\x1cW\x80\xe4\xa7*\xef\xf2\x1a") + + def test_parse_dte_xml_ok_1(self) -> None: + xml_doc = xml_utils.parse_untrusted_xml(self.dte_clean_xml_1_xml_bytes) + + parsed_dte = parse_dte_xml(xml_doc) + self.assertDictEqual( + dict(parsed_dte.as_dict()), + dict( + emisor_rut=Rut('76354771-K'), + tipo_dte=cl_sii.dte.constants.TipoDteEnum.FACTURA_ELECTRONICA, + folio=170, + fecha_emision_date=date(2019, 4, 1), + receptor_rut=Rut('96790240-3'), + monto_total=2996301, + emisor_razon_social='INGENIERIA ENACON SPA', + receptor_razon_social='MINERA LOS PELAMBRES', + fecha_vencimiento_date=None, + firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40), + signature_value=self._TEST_DTE_1_SIGNATURE_VALUE, + signature_x509_cert_pem=self.dte_clean_xml_1_cert_pem_bytes, + emisor_giro='Ingenieria y Construccion', + emisor_email='ENACONLTDA@GMAIL.COM', + receptor_email=None, + )) + + def test_parse_dte_xml_ok_2(self) -> None: + xml_doc = xml_utils.parse_untrusted_xml(self.dte_clean_xml_2_xml_bytes) + parsed_dte = parse_dte_xml(xml_doc) + self.assertDictEqual( + dict(parsed_dte.as_dict()), + dict( + emisor_rut=Rut('76399752-9'), + tipo_dte=cl_sii.dte.constants.TipoDteEnum.FACTURA_ELECTRONICA, + folio=25568, + fecha_emision_date=date(2019, 3, 29), + receptor_rut=Rut('96874030-K'), + monto_total=230992, + emisor_razon_social='COMERCIALIZADORA INNOVA MOBEL SPA', + receptor_razon_social='EMPRESAS LA POLAR S.A.', + fecha_vencimiento_date=None, + firma_documento_dt_naive=datetime(2019, 3, 28, 13, 59, 52), + signature_value=self._TEST_DTE_2_SIGNATURE_VALUE, + signature_x509_cert_pem=self.dte_clean_xml_2_cert_pem_bytes, + emisor_giro='COMERCIALIZACION DE PRODUCTOS PARA EL HOGAR', + emisor_email='ANGEL.PEZO@APCASESORIAS.CL', + receptor_email=None, + )) -class FunctionValidateDteXmlTest(unittest.TestCase): + def test_parse_dte_xml_fail_x(self) -> None: + # TODO: implement more cases + pass + + def test_parse_dte_xml_fail_1(self) -> None: + xml_doc = xml_utils.parse_untrusted_xml(self.dte_bad_xml_1_xml_bytes) + + with self.assertRaises(ValueError) as cm: + parse_dte_xml(xml_doc) + self.assertSequenceEqual( + cm.exception.args, + ("Top level XML element 'Document' is required.", ) + ) + + def test_parse_dte_xml_fail_2(self) -> None: + xml_doc = xml_utils.parse_untrusted_xml(self.dte_bad_xml_2_xml_bytes) - # TODO: implement - pass + with self.assertRaises(ValueError) as cm: + parse_dte_xml(xml_doc) + self.assertSequenceEqual( + cm.exception.args, + ("Top level XML element 'Document' is required.", ) + ) diff --git a/tests/test_libs_crypto_utils.py b/tests/test_libs_crypto_utils.py new file mode 100644 index 00000000..4fe317f7 --- /dev/null +++ b/tests/test_libs_crypto_utils.py @@ -0,0 +1,644 @@ +import unittest +from datetime import datetime + +import cryptography.hazmat.primitives.hashes +import cryptography.x509 +from cryptography.x509 import oid + +from cl_sii.libs.crypto_utils import ( # noqa: F401 + X509Cert, add_pem_cert_header_footer, load_pem_x509_cert, remove_pem_cert_header_footer, +) + +from . import utils + +# TODO: get fake certificates, keys, and all the variations from +# https://github.com/urllib3/urllib3/tree/1.24.2/dummyserver/certs + +# TODO: move me into 'cl_sii/crypto/constants.py' +# - Organismo: MINISTERIO DE ECONOM脥A / SUBSECRETARIA DE ECONOMIA +# - Decreto 181 (Julio-Agosto 2002) +# "APRUEBA REGLAMENTO DE LA LEY 19.799 SOBRE DOCUMENTOS ELECTRONICOS, FIRMA ELECTRONICA +# Y LA CERTIFICACION DE DICHA FIRMA" +# - ref: https://www.leychile.cl/Consulta/m/norma_plana?org=&idNorma=201668 +# dice: +# > RUT del titular del certificado : 1.3.6.1.4.1.8321.1 +# > RUT de la certificadora emisora : 1.3.6.1.4.1.8321.2 +_SII_CERT_CERTIFICADORA_EMISORA_RUT_OID = oid.ObjectIdentifier("1.3.6.1.4.1.8321.2") +_SII_CERT_TITULAR_RUT_OID = oid.ObjectIdentifier("1.3.6.1.4.1.8321.1") + + +class FunctionsTest(unittest.TestCase): + + def test_add_pem_cert_header_footer(self) -> None: + # TODO: implement for function 'add_pem_cert_header_footer'. + pass + + def test_remove_pem_cert_header_footer(self) -> None: + # TODO: implement for function 'remove_pem_cert_header_footer'. + pass + + +class LoadPemX509CertTest(unittest.TestCase): + + def test_load_pem_x509_cert_ok(self) -> None: + cert_pem_bytes = utils.read_test_file_bytes( + 'test_data/crypto/wildcard-google-com-cert.pem') + + x509_cert = load_pem_x509_cert(cert_pem_bytes) + + self.assertIsInstance(x509_cert, X509Cert) + + ####################################################################### + # main properties + ####################################################################### + + self.assertEqual( + x509_cert.version, + cryptography.x509.Version.v3) + self.assertIsInstance( + x509_cert.signature_hash_algorithm, + cryptography.hazmat.primitives.hashes.SHA256) + self.assertEqual( + x509_cert.signature_algorithm_oid, + oid.SignatureAlgorithmOID.RSA_WITH_SHA256) + + self.assertEqual( + x509_cert.serial_number, + 122617997729991213273569581938043448870) + self.assertEqual( + x509_cert.not_valid_after, + datetime(2019, 6, 18, 13, 24)) + self.assertEqual( + x509_cert.not_valid_before, + datetime(2019, 3, 26, 13, 40, 40)) + + ####################################################################### + # issuer + ####################################################################### + + self.assertEqual(len(x509_cert.issuer.rdns), 3) + self.assertEqual( + x509_cert.issuer.rfc4514_string(), + 'C=US,' + 'O=Google Trust Services,' + 'CN=Google Internet Authority G3') + self.assertEqual( + x509_cert.issuer.get_attributes_for_oid(oid.NameOID.COUNTRY_NAME)[0].value, + 'US') + self.assertEqual( + x509_cert.issuer.get_attributes_for_oid(oid.NameOID.ORGANIZATION_NAME)[0].value, + 'Google Trust Services') + self.assertEqual( + x509_cert.issuer.get_attributes_for_oid(oid.NameOID.COMMON_NAME)[0].value, + 'Google Internet Authority G3') + + ####################################################################### + # subject + ####################################################################### + + self.assertEqual(len(x509_cert.subject.rdns), 5) + self.assertEqual( + x509_cert.subject.rfc4514_string(), + 'C=US,' + 'ST=California,' + 'L=Mountain View,' + 'O=Google LLC,' + 'CN=*.google.com') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.COUNTRY_NAME)[0].value, + 'US') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.STATE_OR_PROVINCE_NAME)[0].value, + 'California') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.LOCALITY_NAME)[0].value, + 'Mountain View') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.ORGANIZATION_NAME)[0].value, + 'Google LLC') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.COMMON_NAME)[0].value, + '*.google.com') + + ####################################################################### + # extensions + ####################################################################### + + cert_extensions = x509_cert.extensions + self.assertEqual(len(cert_extensions._extensions), 9) + + # BASIC_CONSTRAINTS + basic_constraints_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.BasicConstraints) + self.assertEqual(basic_constraints_ext.critical, True) + self.assertEqual(basic_constraints_ext.value.ca, False) + self.assertIs(basic_constraints_ext.value.path_length, None) + + # KEY_USAGE + key_usage_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.KeyUsage) + self.assertEqual(key_usage_ext.critical, True) + self.assertEqual(key_usage_ext.value.content_commitment, False) + self.assertEqual(key_usage_ext.value.crl_sign, False) + self.assertEqual(key_usage_ext.value.data_encipherment, False) + self.assertEqual(key_usage_ext.value.digital_signature, True) + self.assertEqual(key_usage_ext.value.key_agreement, False) + self.assertEqual(key_usage_ext.value.key_cert_sign, False) + self.assertEqual(key_usage_ext.value.key_encipherment, False) + + # EXTENDED_KEY_USAGE + extended_key_usage_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.ExtendedKeyUsage) + self.assertEqual(extended_key_usage_ext.critical, False) + self.assertEqual( + extended_key_usage_ext.value._usages, + [oid.ExtendedKeyUsageOID.SERVER_AUTH]) + + # SUBJECT_ALTERNATIVE_NAME + subject_alt_name_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.SubjectAlternativeName) + self.assertEqual(subject_alt_name_ext.critical, False) + self.assertEqual(len(subject_alt_name_ext.value._general_names._general_names), 67) + self.assertEqual( + subject_alt_name_ext.value._general_names._general_names[0].value, + '*.google.com') + + # AUTHORITY_INFORMATION_ACCESS + authority_information_access_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.AuthorityInformationAccess) + self.assertEqual(authority_information_access_ext.critical, False) + self.assertEqual(len(authority_information_access_ext.value._descriptions), 2) + + # SUBJECT_KEY_IDENTIFIER + subject_key_identifier_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.SubjectKeyIdentifier) + self.assertEqual(subject_key_identifier_ext.critical, False) + self.assertEqual( + subject_key_identifier_ext.value.digest, + b'\xcf\x02\xda\x1aM\x80\x92\xff\x04E\xff\xcb7\x81\xe3O\x1d\x85\xb6\xb6') + + # AUTHORITY_KEY_IDENTIFIER + authority_key_identifier_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.AuthorityKeyIdentifier) + self.assertEqual(authority_key_identifier_ext.critical, False) + self.assertIs(authority_key_identifier_ext.value.authority_cert_issuer, None) + self.assertIs(authority_key_identifier_ext.value.authority_cert_serial_number, None) + self.assertEqual( + authority_key_identifier_ext.value.key_identifier, + b'w\xc2\xb8P\x9agvv\xb1-\xc2\x86\xd0\x83\xa0~\xa6~\xbaK' + ) + + # CERTIFICATE_POLICIES + certificate_policies_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.CertificatePolicies) + self.assertEqual(certificate_policies_ext.critical, False) + self.assertSetEqual( + {policy_info.policy_identifier.dotted_string for policy_info in + certificate_policies_ext.value._policies}, + { + # 'Google Trust Services' + # https://github.com/zmap/constants/blob/0816f6f/x509/certificate_policies.csv#L34 + '1.3.6.1.4.1.11129.2.5.3', + # 'CA/B Forum Organization Validated' + # https://github.com/zmap/constants/blob/0816f6f/x509/certificate_policies.csv#L193 + '2.23.140.1.2.2', + } + ) + + # CRL_DISTRIBUTION_POINTS + crl_distribution_points_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.CRLDistributionPoints) + self.assertEqual(crl_distribution_points_ext.critical, False) + self.assertEqual(len(crl_distribution_points_ext.value._distribution_points), 1) + self.assertEqual( + crl_distribution_points_ext.value._distribution_points[0].full_name[0].value, + 'http://crl.pki.goog/GTSGIAG3.crl') + self.assertIs(crl_distribution_points_ext.value._distribution_points[0].crl_issuer, None) + self.assertIs(crl_distribution_points_ext.value._distribution_points[0].reasons, None) + self.assertIs(crl_distribution_points_ext.value._distribution_points[0].relative_name, None) + + def test_load_pem_x509_cert_ok_cert_real_dte(self) -> None: + cert_pem_bytes = utils.read_test_file_bytes( + 'test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem') + + x509_cert = load_pem_x509_cert(cert_pem_bytes) + + self.assertIsInstance(x509_cert, X509Cert) + + ####################################################################### + # main properties + ####################################################################### + + self.assertEqual( + x509_cert.version, + cryptography.x509.Version.v3) + self.assertIsInstance( + x509_cert.signature_hash_algorithm, + cryptography.hazmat.primitives.hashes.SHA1) + self.assertEqual( + x509_cert.signature_algorithm_oid, + oid.SignatureAlgorithmOID.RSA_WITH_SHA1) + + self.assertEqual( + x509_cert.serial_number, + 232680798042554446173213) + self.assertEqual( + x509_cert.not_valid_after, + datetime(2020, 9, 3, 21, 11, 12)) + self.assertEqual( + x509_cert.not_valid_before, + datetime(2017, 9, 4, 21, 11, 12)) + + ####################################################################### + # issuer + ####################################################################### + + self.assertEqual(len(x509_cert.issuer.rdns), 7) + self.assertEqual( + x509_cert.issuer.rfc4514_string(), + 'C=CL,ST=Region Metropolitana,' + 'L=Santiago,' + 'O=E-CERTCHILE,' + 'OU=Autoridad Certificadora,' + 'CN=E-CERTCHILE CA FIRMA ELECTRONICA SIMPLE,' + '1.2.840.113549.1.9.1=sclientes@e-certchile.cl') + + self.assertEqual( + x509_cert.issuer.get_attributes_for_oid(oid.NameOID.COUNTRY_NAME)[0].value, + 'CL') + self.assertEqual( + x509_cert.issuer.get_attributes_for_oid(oid.NameOID.STATE_OR_PROVINCE_NAME)[0].value, + 'Region Metropolitana') + self.assertEqual( + x509_cert.issuer.get_attributes_for_oid(oid.NameOID.LOCALITY_NAME)[0].value, + 'Santiago') + self.assertEqual( + x509_cert.issuer.get_attributes_for_oid(oid.NameOID.ORGANIZATION_NAME)[0].value, + 'E-CERTCHILE') + self.assertEqual( + x509_cert.issuer.get_attributes_for_oid(oid.NameOID.ORGANIZATIONAL_UNIT_NAME)[0].value, + 'Autoridad Certificadora') + self.assertEqual( + x509_cert.issuer.get_attributes_for_oid(oid.NameOID.COMMON_NAME)[0].value, + 'E-CERTCHILE CA FIRMA ELECTRONICA SIMPLE') + self.assertEqual( + x509_cert.issuer.get_attributes_for_oid(oid.NameOID.EMAIL_ADDRESS)[0].value, + 'sclientes@e-certchile.cl') + + ####################################################################### + # subject + ####################################################################### + + self.assertEqual(len(x509_cert.subject.rdns), 7) + self.assertEqual( + x509_cert.subject.rfc4514_string(), + 'C=CL,' + 'ST=VALPARAISO\\ ,' + 'L=Quillota,' + 'O=Servicios Bonilla y Lopez y Cia. Ltda.,' + 'OU=Ingenier铆a y Construcci贸n,' + 'CN=Ramon humberto Lopez Jara,' + '1.2.840.113549.1.9.1=enaconltda@gmail.com') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.COUNTRY_NAME)[0].value, + 'CL') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.STATE_OR_PROVINCE_NAME)[0].value, + 'VALPARAISO ') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.LOCALITY_NAME)[0].value, + 'Quillota') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.ORGANIZATION_NAME)[0].value, + 'Servicios Bonilla y Lopez y Cia. Ltda.') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.ORGANIZATIONAL_UNIT_NAME)[0].value, + 'Ingenier铆a y Construcci贸n') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.COMMON_NAME)[0].value, + 'Ramon humberto Lopez Jara') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.EMAIL_ADDRESS)[0].value, + 'enaconltda@gmail.com') + + ####################################################################### + # extensions + ####################################################################### + + cert_extensions = x509_cert.extensions + self.assertEqual(len(cert_extensions._extensions), 9) + + # KEY_USAGE + key_usage_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.KeyUsage) + self.assertEqual(key_usage_ext.critical, False) + self.assertEqual(key_usage_ext.value.content_commitment, True) + self.assertEqual(key_usage_ext.value.crl_sign, False) + self.assertEqual(key_usage_ext.value.data_encipherment, True) + self.assertEqual(key_usage_ext.value.digital_signature, True) + self.assertEqual(key_usage_ext.value.key_agreement, False) + self.assertEqual(key_usage_ext.value.key_cert_sign, False) + self.assertEqual(key_usage_ext.value.key_encipherment, True) + + # ISSUER_ALTERNATIVE_NAME + issuer_alt_name_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.IssuerAlternativeName) + self.assertEqual(issuer_alt_name_ext.critical, False) + self.assertEqual(len(issuer_alt_name_ext.value._general_names._general_names), 1) + self.assertEqual( + issuer_alt_name_ext.value._general_names._general_names[0].type_id, + _SII_CERT_CERTIFICADORA_EMISORA_RUT_OID) + self.assertEqual( + issuer_alt_name_ext.value._general_names._general_names[0].value, + b'\x16\n96928180-5') + + # SUBJECT_ALTERNATIVE_NAME + subject_alt_name_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.SubjectAlternativeName) + self.assertEqual(subject_alt_name_ext.critical, False) + self.assertEqual(len(subject_alt_name_ext.value._general_names._general_names), 1) + self.assertEqual( + subject_alt_name_ext.value._general_names._general_names[0].type_id, + _SII_CERT_TITULAR_RUT_OID) + self.assertEqual( + subject_alt_name_ext.value._general_names._general_names[0].value, + b'\x16\n13185095-6') + + # AUTHORITY_INFORMATION_ACCESS + authority_information_access_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.AuthorityInformationAccess) + self.assertEqual(authority_information_access_ext.critical, False) + self.assertEqual(len(authority_information_access_ext.value._descriptions), 1) + self.assertEqual( + authority_information_access_ext.value._descriptions[0].access_location.value, + 'http://ocsp.ecertchile.cl/ocsp') + self.assertEqual( + authority_information_access_ext.value._descriptions[0].access_method, + oid.AuthorityInformationAccessOID.OCSP) + + # SUBJECT_KEY_IDENTIFIER + subject_key_identifier_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.SubjectKeyIdentifier) + self.assertEqual(subject_key_identifier_ext.critical, False) + self.assertEqual( + subject_key_identifier_ext.value.digest, + b'\xd5\xd5G\x84]\x14U\xee\xd1\\\x8c\xf8r9w\xfdW\xb0\xfa\xaa') + + # AUTHORITY_KEY_IDENTIFIER + authority_key_identifier_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.AuthorityKeyIdentifier) + self.assertEqual(authority_key_identifier_ext.critical, False) + self.assertIs(authority_key_identifier_ext.value.authority_cert_issuer, None) + self.assertIs(authority_key_identifier_ext.value.authority_cert_serial_number, None) + self.assertEqual( + authority_key_identifier_ext.value.key_identifier, + b'x\xe1>\x9f\xd2\x12\xb3z<\x8d\xcd0\x0eS\xb3C)\x07\xb3U') + + # CERTIFICATE_POLICIES + certificate_policies_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.CertificatePolicies) + self.assertEqual(certificate_policies_ext.critical, False) + self.assertEqual(len(certificate_policies_ext.value._policies), 1) + # TODO: find out where did OID '1.3.6.1.4.1.8658.5' come from. + # Perhaps it was '1.3.6.1.4.1.8658'? + # https://oidref.com/1.3.6.1.4.1.8658 + self.assertEqual( + certificate_policies_ext.value._policies[0].policy_identifier, + oid.ObjectIdentifier("1.3.6.1.4.1.8658.5")) + self.assertEqual(len(certificate_policies_ext.value._policies[0].policy_qualifiers), 2) + self.assertEqual( + certificate_policies_ext.value._policies[0].policy_qualifiers[0], + "http://www.e-certchile.cl/CPS.htm") + self.assertEqual( + certificate_policies_ext.value._policies[0].policy_qualifiers[1].explicit_text, + "Certificado Firma Simple. Ha sido validado en forma presencial, quedando habilitado " + "el Certificado para uso tributario") + + # CRL_DISTRIBUTION_POINTS + crl_distribution_points_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.CRLDistributionPoints) + self.assertEqual(crl_distribution_points_ext.critical, False) + self.assertEqual(len(crl_distribution_points_ext.value._distribution_points), 1) + self.assertEqual( + crl_distribution_points_ext.value._distribution_points[0].full_name[0].value, + 'http://crl.e-certchile.cl/ecertchilecaFES.crl') + self.assertIs(crl_distribution_points_ext.value._distribution_points[0].crl_issuer, None) + self.assertIs(crl_distribution_points_ext.value._distribution_points[0].reasons, None) + self.assertIs(crl_distribution_points_ext.value._distribution_points[0].relative_name, None) + + ####################################################################### + # extra extensions + ####################################################################### + + # "Microsoft" / "Microsoft CertSrv Infrastructure" / "szOID_CERTIFICATE_TEMPLATE" + # See: + # http://oidref.com/1.3.6.1.4.1.311.21.7 + # https://support.microsoft.com/en-ae/help/287547/object-ids-associated-with-microsoft-cryptography + some_microsoft_extension_oid = oid.ObjectIdentifier("1.3.6.1.4.1.311.21.7") + some_microsoft_ext = cert_extensions.get_extension_for_oid(some_microsoft_extension_oid) + self.assertEqual(some_microsoft_ext.critical, False) + self.assertTrue(isinstance(some_microsoft_ext.value.value, bytes)) + + def test_load_pem_x509_cert_ok_prueba_sii(self) -> None: + cert_pem_bytes = utils.read_test_file_bytes('test_data/sii-crypto/prueba-sii-cert.pem') + + x509_cert = load_pem_x509_cert(cert_pem_bytes) + + self.assertIsInstance(x509_cert, X509Cert) + + ####################################################################### + # main properties + ####################################################################### + + self.assertEqual( + x509_cert.version, + cryptography.x509.Version.v3) + self.assertIsInstance( + x509_cert.signature_hash_algorithm, + cryptography.hazmat.primitives.hashes.MD5) + self.assertEqual( + x509_cert.signature_algorithm_oid, + oid.SignatureAlgorithmOID.RSA_WITH_MD5) + + self.assertEqual( + x509_cert.serial_number, + 131466) + self.assertEqual( + x509_cert.not_valid_after, + datetime(2003, 10, 2, 0, 0)) + self.assertEqual( + x509_cert.not_valid_before, + datetime(2002, 10, 2, 19, 11, 59)) + + ####################################################################### + # issuer + ####################################################################### + + self.assertEqual(len(x509_cert.issuer.rdns), 6) + self.assertEqual( + x509_cert.issuer.rfc4514_string(), + 'ST=Region Metropolitana,' + 'L=Santiago,' + 'CN=E-Certchile CA Intermedia,' + 'OU=Empresa Nacional de Certificacion Electronica,' + 'O=E-CERTCHILE,' + 'C=CL') + self.assertEqual( + x509_cert.issuer.get_attributes_for_oid(oid.NameOID.COUNTRY_NAME)[0].value, + 'CL') + self.assertEqual( + x509_cert.issuer.get_attributes_for_oid(oid.NameOID.STATE_OR_PROVINCE_NAME)[0].value, + 'Region Metropolitana') + self.assertEqual( + x509_cert.issuer.get_attributes_for_oid(oid.NameOID.LOCALITY_NAME)[0].value, + 'Santiago') + self.assertEqual( + x509_cert.issuer.get_attributes_for_oid(oid.NameOID.ORGANIZATION_NAME)[0].value, + 'E-CERTCHILE') + self.assertEqual( + x509_cert.issuer.get_attributes_for_oid(oid.NameOID.ORGANIZATIONAL_UNIT_NAME)[0].value, + 'Empresa Nacional de Certificacion Electronica') + self.assertEqual( + x509_cert.issuer.get_attributes_for_oid(oid.NameOID.COMMON_NAME)[0].value, + 'E-Certchile CA Intermedia') + + ####################################################################### + # subject + ####################################################################### + + self.assertEqual(len(x509_cert.subject.rdns), 7) + self.assertEqual( + x509_cert.subject.rfc4514_string(), + 'ST=Region Metropolitana,' + 'OU=Servicio de Impuestos Internos,' + 'O=Servicio de Impuestos Internos,' + 'L=Santiago,' + '1.2.840.113549.1.9.1=wgonzalez@sii.cl,' + 'CN=Wilibaldo Gonzalez Cabrera,' + 'C=CL') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.COUNTRY_NAME)[0].value, + 'CL') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.STATE_OR_PROVINCE_NAME)[0].value, + 'Region Metropolitana') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.LOCALITY_NAME)[0].value, + 'Santiago') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.ORGANIZATION_NAME)[0].value, + 'Servicio de Impuestos Internos') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.ORGANIZATIONAL_UNIT_NAME)[0].value, + 'Servicio de Impuestos Internos') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.COMMON_NAME)[0].value, + 'Wilibaldo Gonzalez Cabrera') + self.assertEqual( + x509_cert.subject.get_attributes_for_oid(oid.NameOID.EMAIL_ADDRESS)[0].value, + 'wgonzalez@sii.cl') + + ####################################################################### + # extensions + ####################################################################### + + cert_extensions = x509_cert.extensions + self.assertEqual(len(cert_extensions._extensions), 5) + + # KEY_USAGE + key_usage_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.KeyUsage) + self.assertEqual(key_usage_ext.critical, False) + self.assertEqual(key_usage_ext.value.content_commitment, True) + self.assertEqual(key_usage_ext.value.crl_sign, False) + self.assertEqual(key_usage_ext.value.data_encipherment, True) + self.assertEqual(key_usage_ext.value.digital_signature, True) + self.assertEqual(key_usage_ext.value.key_agreement, False) + self.assertEqual(key_usage_ext.value.key_cert_sign, False) + self.assertEqual(key_usage_ext.value.key_encipherment, True) + + # ISSUER_ALTERNATIVE_NAME + issuer_alt_name_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.IssuerAlternativeName) + self.assertEqual(issuer_alt_name_ext.critical, False) + self.assertEqual(len(issuer_alt_name_ext.value._general_names._general_names), 1) + self.assertEqual( + issuer_alt_name_ext.value._general_names._general_names[0].type_id, + _SII_CERT_CERTIFICADORA_EMISORA_RUT_OID) + self.assertEqual( + issuer_alt_name_ext.value._general_names._general_names[0].value, + b'\x16\n96928180-5') + + # SUBJECT_ALTERNATIVE_NAME + subject_alt_name_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.SubjectAlternativeName) + self.assertEqual(subject_alt_name_ext.critical, False) + self.assertEqual(len(subject_alt_name_ext.value._general_names._general_names), 1) + # TODO: find out where did OID '1.3.6.1.4.1.8658.1' come from. + # Shouldn't it have been equal to '_SII_CERT_TITULAR_RUT_OID'? + self.assertEqual( + subject_alt_name_ext.value._general_names._general_names[0].type_id, + oid.ObjectIdentifier("1.3.6.1.4.1.8658.1")) + self.assertEqual( + subject_alt_name_ext.value._general_names._general_names[0].value, + b'\x16\n07880442-4') + + # CERTIFICATE_POLICIES + certificate_policies_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.CertificatePolicies) + self.assertEqual(certificate_policies_ext.critical, False) + self.assertEqual(len(certificate_policies_ext.value._policies), 1) + # TODO: find out where did OID '1.3.6.1.4.1.8658.0' come from. + # Perhaps it was '1.3.6.1.4.1.8658'? + # https://oidref.com/1.3.6.1.4.1.8658 + self.assertEqual( + certificate_policies_ext.value._policies[0].policy_identifier, + oid.ObjectIdentifier("1.3.6.1.4.1.8658.0")) + self.assertEqual(len(certificate_policies_ext.value._policies[0].policy_qualifiers), 2) + self.assertEqual( + certificate_policies_ext.value._policies[0].policy_qualifiers[0], + "http://www.e-certchile.cl/politica/cps.htm") + self.assertEqual( + certificate_policies_ext.value._policies[0].policy_qualifiers[1].explicit_text, + "El titular ha sido validado en forma presencial, quedando habilitado el Certificado " + "para uso tributario, pagos, comercio u otros") + + # CRL_DISTRIBUTION_POINTS + crl_distribution_points_ext = cert_extensions.get_extension_for_class( + cryptography.x509.extensions.CRLDistributionPoints) + self.assertEqual(crl_distribution_points_ext.critical, False) + self.assertEqual(len(crl_distribution_points_ext.value._distribution_points), 1) + self.assertEqual( + crl_distribution_points_ext.value._distribution_points[0].full_name[0].value, + 'http://crl.e-certchile.cl/EcertchileCAI.crl') + self.assertIs(crl_distribution_points_ext.value._distribution_points[0].crl_issuer, None) + self.assertIs(crl_distribution_points_ext.value._distribution_points[0].reasons, None) + self.assertIs(crl_distribution_points_ext.value._distribution_points[0].relative_name, None) + + def test_load_pem_x509_cert_ok_str_ascii(self) -> None: + cert_pem_str_ascii = utils.read_test_file_str_ascii( + 'test_data/crypto/wildcard-google-com-cert.pem') + + x509_cert = load_pem_x509_cert(cert_pem_str_ascii) + self.assertIsInstance(x509_cert, X509Cert) + + def test_load_pem_x509_cert_ok_str_utf8(self) -> None: + cert_pem_str_utf8 = utils.read_test_file_str_utf8( + 'test_data/crypto/wildcard-google-com-cert.pem') + + x509_cert = load_pem_x509_cert(cert_pem_str_utf8) + self.assertIsInstance(x509_cert, X509Cert) + + def test_load_pem_x509_cert_fail_type_error(self) -> None: + with self.assertRaises(TypeError) as cm: + load_pem_x509_cert(1) + self.assertEqual(cm.exception.args, ("Value must be str or bytes.", )) + + def test_load_pem_x509_cert_fail_value_error(self) -> None: + with self.assertRaises(ValueError) as cm: + load_pem_x509_cert('hello') + self.assertEqual( + cm.exception.args, + ("Unable to load certificate. See " + "https://cryptography.io/en/latest/faq/#why-can-t-i-import-my-pem-file " + "for more details.", )) diff --git a/tests/test_libs_encoding_utils.py b/tests/test_libs_encoding_utils.py new file mode 100644 index 00000000..0065d6f6 --- /dev/null +++ b/tests/test_libs_encoding_utils.py @@ -0,0 +1,18 @@ +import unittest + +from cl_sii.libs.encoding_utils import clean_base64, decode_base64_strict, validate_base64 # noqa: F401,E501 + + +class FunctionsTest(unittest.TestCase): + + def test_clean_base64(self): + # TODO: implement for function 'clean_base64'. + pass + + def test_decode_base64_strict(self): + # TODO: implement for function 'decode_base64_strict'. + pass + + def test_validate_base64(self): + # TODO: implement for function 'validate_base64'. + pass diff --git a/tests/test_libs_xml_utils.py b/tests/test_libs_xml_utils.py index a8a15723..33ec3037 100644 --- a/tests/test_libs_xml_utils.py +++ b/tests/test_libs_xml_utils.py @@ -2,6 +2,7 @@ import lxml.etree +from cl_sii.libs.xml_utils import XmlElement from cl_sii.libs.xml_utils import ( # noqa: F401 XmlSyntaxError, XmlFeatureForbidden, parse_untrusted_xml, read_xml_schema, validate_xml_doc, write_xml_doc, @@ -20,7 +21,7 @@ def test_parse_untrusted_xml_valid(self) -> None: b' \n' b'') xml = parse_untrusted_xml(value) - self.assertIsInstance(xml, lxml.etree.ElementBase) + self.assertIsInstance(xml, XmlElement) # print(xml) self.assertEqual( lxml.etree.tostring(xml, pretty_print=False), diff --git a/tests/utils.py b/tests/utils.py index ae424d5d..5d27943d 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -13,3 +13,25 @@ def read_test_file_bytes(path: str) -> bytes: content = file.read() return content + + +def read_test_file_str_ascii(path: str) -> str: + filepath = os.path.join( + _TESTS_DIR_PATH, + path, + ) + with open(filepath, mode='rt', encoding='ascii') as file: + content = file.read() + + return content + + +def read_test_file_str_utf8(path: str) -> str: + filepath = os.path.join( + _TESTS_DIR_PATH, + path, + ) + with open(filepath, mode='rt', encoding='utf8') as file: + content = file.read() + + return content