From 8872ea11434fc3b722748e02ac46cc37c2810ae8 Mon Sep 17 00:00:00 2001 From: Edwin Luis Barboza Pinedo Date: Sat, 28 Mar 2026 14:29:08 -0500 Subject: [PATCH 1/8] Add new XML test cases for DespatchAdvice and update summary documents - Created multiple XML test cases for DespatchAdvice under GRETransportistaCasuisticasTest, including: - vehiculoM1L.xml - transportistaBasico.xml - transportistaComercioExterior.xml - transportistaConCarreta.xml - transportistaMultiplesConductores.xml - Updated summaryDocuments.xml and summaryDocuments_anularBoletaExonerada.xml to include TaxPercent elements for tax subtotals. Signed-off-by: Edwin Luis Barboza Pinedo --- .../springboot/XBuilderController.java | 886 ++++++++++-------- .../xbuilder/content/catalogs/Catalog20.java | 92 +- .../xbuilder/content/catalogs/Catalog21.java | 47 +- .../xbuilder/content/catalogs/Catalog61.java | 68 ++ .../xbuilder/content/catalogs/Catalog62.java | 50 + .../xbuilder/content/catalogs/Catalog63.java | 60 ++ .../xbuilder/content/catalogs/Catalog64.java | 54 ++ .../content/catalogs/IndicadorEnvio.java | 113 +++ .../content/catalogs/TipoConductor.java | 39 + .../jaxb/mappers/DespatchAdviceMapper.java | 57 +- .../jaxb/mappers/SummaryDocumentsMapper.java | 178 ++-- .../jaxb/models/XMLSummaryDocumentsLine.java | 3 + .../models/standard/guia/Contenedor.java | 44 + .../standard/guia/DeclaracionAduanera.java | 50 + .../models/standard/guia/DespatchAdvice.java | 79 +- .../guia/DespatchAdviceValidator.java | 253 +++++ .../content/models/standard/guia/Envio.java | 88 +- .../models/standard/guia/GRERemitente.java | 239 +++++ .../standard/guia/GRETransportista.java | 302 ++++++ .../sunat/resumen/ComprobanteImpuestos.java | 3 + .../xbuilder/enricher/ContentEnricher.java | 323 ++++--- .../body/summaryDocumentItem/TasaIgvRule.java | 31 + .../DespatchAdviceTipoComprobanteRule.java | 44 + ....openubl.xbuilder.enricher.kie.RuleFactory | 4 + .../templates/Renderer/despatchAdvice.xml | 25 +- .../templates/Renderer/summaryDocuments.xml | 1 + .../core/src/test/java/e2e/AbstractTest.java | 19 +- .../java/e2e/renderer/XMLAssertUtils.java | 83 +- .../DespatchAdviceComercioExteriorTest.java | 230 +++++ .../DespatchAdviceComplexTest.java | 219 +++-- .../GRERemitenteCasuisticasTest.java | 481 ++++++++++ .../GRETransportistaCasuisticasTest.java | 326 +++++++ .../java/unit/catalogs/GRECatalogosTest.java | 114 +++ .../DespatchAdviceValidatorTest.java | 259 +++++ .../validator/GRERemitenteValidatorTest.java | 312 ++++++ .../GRETransportistaValidatorTest.java | 241 +++++ .../exportacionSinDAM.xml | 111 +++ .../importacionDAMTotal.xml | 130 +++ .../mercanciaExtranjera.xml | 115 +++ .../mercanciaExtranjeraMotivo19.xml | 121 +++ .../multiplesConductores.xml | 118 +++ .../transportePrivadoBasico.xml | 109 +++ .../transportePublico.xml | 104 ++ .../transporteSubcontratado.xml | 105 +++ .../vehiculoConCarreta.xml | 124 +++ .../vehiculoM1L.xml | 96 ++ .../transportistaBasico.xml | 123 +++ .../transportistaComercioExterior.xml | 135 +++ .../transportistaConCarreta.xml | 133 +++ .../transportistaMultiplesConductores.xml | 132 +++ .../SummaryDocumentsTest/summaryDocuments.xml | 2 + ...summaryDocuments_anularBoletaExonerada.xml | 1 + 52 files changed, 6255 insertions(+), 821 deletions(-) create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog61.java create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog62.java create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog63.java create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog64.java create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/IndicadorEnvio.java create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/TipoConductor.java create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Contenedor.java create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DeclaracionAduanera.java create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdviceValidator.java create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GRERemitente.java create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GRETransportista.java create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/enricher/kie/rules/enrich/body/summaryDocumentItem/TasaIgvRule.java create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/enricher/kie/rules/enrich/header/DespatchAdviceTipoComprobanteRule.java create mode 100644 xbuilder/core/src/test/java/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest.java create mode 100644 xbuilder/core/src/test/java/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest.java create mode 100644 xbuilder/core/src/test/java/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest.java create mode 100644 xbuilder/core/src/test/java/unit/catalogs/GRECatalogosTest.java create mode 100644 xbuilder/core/src/test/java/unit/validator/DespatchAdviceValidatorTest.java create mode 100644 xbuilder/core/src/test/java/unit/validator/GRERemitenteValidatorTest.java create mode 100644 xbuilder/core/src/test/java/unit/validator/GRETransportistaValidatorTest.java create mode 100644 xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/exportacionSinDAM.xml create mode 100644 xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/importacionDAMTotal.xml create mode 100644 xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/mercanciaExtranjera.xml create mode 100644 xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/mercanciaExtranjeraMotivo19.xml create mode 100644 xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/multiplesConductores.xml create mode 100644 xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transportePrivadoBasico.xml create mode 100644 xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transportePublico.xml create mode 100644 xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transporteSubcontratado.xml create mode 100644 xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/vehiculoConCarreta.xml create mode 100644 xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/vehiculoM1L.xml create mode 100644 xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaBasico.xml create mode 100644 xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaComercioExterior.xml create mode 100644 xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaConCarreta.xml create mode 100644 xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaMultiplesConductores.xml diff --git a/examples/springbot/src/main/java/io/github/project/openubl/quickstart/xbuilder/springboot/XBuilderController.java b/examples/springbot/src/main/java/io/github/project/openubl/quickstart/xbuilder/springboot/XBuilderController.java index 0c2fca82..bf7af195 100644 --- a/examples/springbot/src/main/java/io/github/project/openubl/quickstart/xbuilder/springboot/XBuilderController.java +++ b/examples/springbot/src/main/java/io/github/project/openubl/quickstart/xbuilder/springboot/XBuilderController.java @@ -32,403 +32,499 @@ @RestController public class XBuilderController { - Defaults defaults = Defaults.builder() - .icbTasa(new BigDecimal("0.2")) - .igvTasa(new BigDecimal("0.18")) - .build(); - - DateProvider dateProvider = LocalDate::now; - - @RequestMapping( - method = RequestMethod.POST, - value = "/api/create-xml/invoice", produces = "text/plain" - ) - public String createInvoiceXML(@RequestBody String clientName) throws Exception { - Invoice invoice = createInvoice(clientName); - - ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); - enricher.enrich(invoice); - String xml = TemplateProducer.getInstance().getInvoice().data(invoice).render(); - return signAndRender(xml); - } - - @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/credit-note", produces = "text/plain") - public String createCreditNoteXML(@RequestBody String clientName) throws Exception { - CreditNote input = createCreditNote(clientName); - ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); - enricher.enrich(input); - String xml = TemplateProducer.getInstance().getCreditNote().data(input).render(); - return signAndRender(xml); - } - - @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/debit-note", produces = "text/plain") - public String createDebitNoteXML(@RequestBody String clientName) throws Exception { - DebitNote input = createDebitNote(clientName); - ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); - enricher.enrich(input); - String xml = TemplateProducer.getInstance().getDebitNote().data(input).render(); - return signAndRender(xml); - } - - @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/voided-documents", produces = "text/plain") - public String createVoidedDocumentsXML(@RequestBody String clientName) throws Exception { - VoidedDocuments input = createVoidedDocuments(clientName); - ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); - enricher.enrich(input); - String xml = TemplateProducer.getInstance().getVoidedDocument().data(input).render(); - return signAndRender(xml); - } - - @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/summary-documents", produces = "text/plain") - public String createSummaryDocumentsXML(@RequestBody String clientName) throws Exception { - SummaryDocuments input = createSummaryDocuments(clientName); - ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); - enricher.enrich(input); - String xml = TemplateProducer.getInstance().getSummaryDocuments().data(input).render(); - return signAndRender(xml); - } - - @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/perception", produces = "text/plain") - public String createPerceptionXML(@RequestBody String clientName) throws Exception { - Perception input = createPerception(clientName); - ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); - enricher.enrich(input); - String xml = TemplateProducer.getInstance().getPerception().data(input).render(); - return signAndRender(xml); - } - - @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/retention", produces = "text/plain") - public String createRetentionXML(@RequestBody String clientName) throws Exception { - Retention input = createRetention(clientName); - ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); - enricher.enrich(input); - String xml = TemplateProducer.getInstance().getRetention().data(input).render(); - return signAndRender(xml); - } - - @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/despatch-advice", produces = "text/plain") - public String createDespatchAdviceXML(@RequestBody String clientName) throws Exception { - DespatchAdvice input = createDespatchAdvice(clientName); - ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); - enricher.enrich(input); - String xml = TemplateProducer.getInstance().getDespatchAdvice().data(input).render(); - return signAndRender(xml); - } - - @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/reversion", produces = "text/plain") - public String createReversionXML(@RequestBody String clientName) throws Exception { - Reversion input = createReversion(clientName); - ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); - enricher.enrich(input); - String xml = TemplateProducer.getInstance().getReversion().data(input).render(); - return signAndRender(xml); - } - - private String signAndRender(String xml) throws Exception { - // Sign XML - InputStream ksInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("LLAMA-PE-CERTIFICADO-DEMO-12345678912.pfx"); - CertificateDetails certificate = CertificateDetailsFactory.create(ksInputStream, "password"); - - X509Certificate x509Certificate = certificate.getX509Certificate(); - PrivateKey privateKey = certificate.getPrivateKey(); - Document signedXML = XMLSigner.signXML(xml, "Project OpenUBL", x509Certificate, privateKey); - - // Return - byte[] bytesFromDocument = XmlSignatureHelper.getBytesFromDocument(signedXML); - return new String(bytesFromDocument, StandardCharsets.ISO_8859_1); - } - - private Invoice createInvoice(String clientName) { - return Invoice.builder() - .serie("F001") - .numero(1) - .proveedor(Proveedor.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build() - ) - .cliente(Cliente.builder() - .nombre(clientName) - .numeroDocumentoIdentidad("12121212121") - .tipoDocumentoIdentidad(Catalog6.RUC.toString()) - .build() - ) - .detalle(DocumentoVentaDetalle.builder() - .descripcion("Item1") - .cantidad(new BigDecimal("10")) - .precio(new BigDecimal("100")) - .unidadMedida("KGM") - .build() - ) - .detalle(DocumentoVentaDetalle.builder() - .descripcion("Item2") - .cantidad(new BigDecimal("10")) - .precio(new BigDecimal("100")) - .unidadMedida("KGM") - .build() - ) - .build(); - } - - private CreditNote createCreditNote(String clientName) { - return CreditNote.builder() - .serie("FC01") - .numero(1) - .comprobanteAfectadoSerieNumero("F001-1") - .sustentoDescripcion("mi sustento") - .proveedor(Proveedor.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build() - ) - .cliente(Cliente.builder() - .nombre(clientName) - .numeroDocumentoIdentidad("12121212121") - .tipoDocumentoIdentidad(Catalog6.RUC.toString()) - .build() - ) - .detalle(DocumentoVentaDetalle.builder() - .descripcion("Item1") - .cantidad(new BigDecimal("10")) - .precio(new BigDecimal("100")) - .build() - ) - .build(); - } - - private DebitNote createDebitNote(String clientName) { - return DebitNote.builder() - .serie("FD01") - .numero(1) - .comprobanteAfectadoSerieNumero("F001-1") - .sustentoDescripcion("mi sustento") - .proveedor(Proveedor.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build() - ) - .cliente(Cliente.builder() - .nombre(clientName) - .numeroDocumentoIdentidad("12121212121") - .tipoDocumentoIdentidad(Catalog6.RUC.toString()) - .build() - ) - .detalle(DocumentoVentaDetalle.builder() - .descripcion("Item1") - .cantidad(new BigDecimal("10")) - .precio(new BigDecimal("100")) - .build() - ) - .build(); - } - - private VoidedDocuments createVoidedDocuments(String clientName) { - return VoidedDocuments.builder() - .numero(1) - .fechaEmision(LocalDate.of(2022, 01, 31)) - .fechaEmisionComprobantes(LocalDate.of(2022, 01, 29)) - .proveedor(Proveedor.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build() - ) - .comprobante(VoidedDocumentsItem.builder() - .serie("F001") - .numero(1) - .tipoComprobante(Catalog1_Invoice.FACTURA.getCode()) - .descripcionSustento("Mi sustento1") - .build() - ) - .comprobante(VoidedDocumentsItem.builder() - .serie("F001") - .numero(2) - .tipoComprobante(Catalog1_Invoice.FACTURA.getCode()) - .descripcionSustento("Mi sustento2") - .build() - ) - .build(); - } - - private SummaryDocuments createSummaryDocuments(String clientName) { - return SummaryDocuments.builder() - .numero(1) - .fechaEmisionComprobantes(dateProvider.now().minusDays(2)) - .proveedor(Proveedor.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build() - ) - .comprobante(SummaryDocumentsItem.builder() - .tipoOperacion(Catalog19.ADICIONAR.toString()) - .comprobante(Comprobante.builder() - .tipoComprobante(Catalog1_Invoice.BOLETA.getCode())// - .serieNumero("B001-1") + Defaults defaults = Defaults.builder() + .icbTasa(new BigDecimal("0.2")) + .igvTasa(new BigDecimal("0.18")) + .build(); + + DateProvider dateProvider = LocalDate::now; + + @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/invoice", produces = "text/plain") + public String createInvoiceXML(@RequestBody String clientName) throws Exception { + Invoice invoice = createInvoice(clientName); + + ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); + enricher.enrich(invoice); + String xml = TemplateProducer.getInstance().getInvoice().data(invoice).render(); + return signAndRender(xml); + } + + @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/credit-note", produces = "text/plain") + public String createCreditNoteXML(@RequestBody String clientName) throws Exception { + CreditNote input = createCreditNote(clientName); + ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); + enricher.enrich(input); + String xml = TemplateProducer.getInstance().getCreditNote().data(input).render(); + return signAndRender(xml); + } + + @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/debit-note", produces = "text/plain") + public String createDebitNoteXML(@RequestBody String clientName) throws Exception { + DebitNote input = createDebitNote(clientName); + ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); + enricher.enrich(input); + String xml = TemplateProducer.getInstance().getDebitNote().data(input).render(); + return signAndRender(xml); + } + + @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/voided-documents", produces = "text/plain") + public String createVoidedDocumentsXML(@RequestBody String clientName) throws Exception { + VoidedDocuments input = createVoidedDocuments(clientName); + ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); + enricher.enrich(input); + String xml = TemplateProducer.getInstance().getVoidedDocument().data(input).render(); + return signAndRender(xml); + } + + @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/summary-documents", produces = "text/plain") + public String createSummaryDocumentsXML(@RequestBody String clientName) throws Exception { + SummaryDocuments input = createSummaryDocuments(clientName); + ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); + enricher.enrich(input); + String xml = TemplateProducer.getInstance().getSummaryDocuments().data(input).render(); + return signAndRender(xml); + } + + @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/perception", produces = "text/plain") + public String createPerceptionXML(@RequestBody String clientName) throws Exception { + Perception input = createPerception(clientName); + ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); + enricher.enrich(input); + String xml = TemplateProducer.getInstance().getPerception().data(input).render(); + return signAndRender(xml); + } + + @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/retention", produces = "text/plain") + public String createRetentionXML(@RequestBody String clientName) throws Exception { + Retention input = createRetention(clientName); + ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); + enricher.enrich(input); + String xml = TemplateProducer.getInstance().getRetention().data(input).render(); + return signAndRender(xml); + } + + @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/despatch-advice", produces = "text/plain") + public String createDespatchAdviceXML(@RequestBody String clientName) throws Exception { + DespatchAdvice input = createDespatchAdvice(clientName); + ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); + enricher.enrich(input); + String xml = TemplateProducer.getInstance().getDespatchAdvice().data(input).render(); + return signAndRender(xml); + } + + @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/gre-remitente", produces = "text/plain") + public String createGRERemitenteXML(@RequestBody String clientName) throws Exception { + GRERemitente gre = createGRERemitente(clientName); + DespatchAdvice input = gre.toDespatchAdvice(); + ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); + enricher.enrich(input); + String xml = TemplateProducer.getInstance().getDespatchAdvice().data(input).render(); + return signAndRender(xml); + } + + @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/gre-transportista", produces = "text/plain") + public String createGRETransportistaXML(@RequestBody String clientName) throws Exception { + GRETransportista gre = createGRETransportista(clientName); + DespatchAdvice input = gre.toDespatchAdvice(); + ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); + enricher.enrich(input); + String xml = TemplateProducer.getInstance().getDespatchAdvice().data(input).render(); + return signAndRender(xml); + } + + @RequestMapping(method = RequestMethod.POST, value = "/api/create-xml/reversion", produces = "text/plain") + public String createReversionXML(@RequestBody String clientName) throws Exception { + Reversion input = createReversion(clientName); + ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); + enricher.enrich(input); + String xml = TemplateProducer.getInstance().getReversion().data(input).render(); + return signAndRender(xml); + } + + private String signAndRender(String xml) throws Exception { + // Sign XML + InputStream ksInputStream = Thread.currentThread().getContextClassLoader() + .getResourceAsStream("LLAMA-PE-CERTIFICADO-DEMO-12345678912.pfx"); + CertificateDetails certificate = CertificateDetailsFactory.create(ksInputStream, "password"); + + X509Certificate x509Certificate = certificate.getX509Certificate(); + PrivateKey privateKey = certificate.getPrivateKey(); + Document signedXML = XMLSigner.signXML(xml, "Project OpenUBL", x509Certificate, privateKey); + + // Return + byte[] bytesFromDocument = XmlSignatureHelper.getBytesFromDocument(signedXML); + return new String(bytesFromDocument, StandardCharsets.ISO_8859_1); + } + + private Invoice createInvoice(String clientName) { + return Invoice.builder() + .serie("F001") + .numero(1) + .proveedor(Proveedor.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .cliente(Cliente.builder() + .nombre(clientName) + .numeroDocumentoIdentidad("12121212121") + .tipoDocumentoIdentidad(Catalog6.RUC.toString()) + .build()) + .detalle(DocumentoVentaDetalle.builder() + .descripcion("Item1") + .cantidad(new BigDecimal("10")) + .precio(new BigDecimal("100")) + .unidadMedida("KGM") + .build()) + .detalle(DocumentoVentaDetalle.builder() + .descripcion("Item2") + .cantidad(new BigDecimal("10")) + .precio(new BigDecimal("100")) + .unidadMedida("KGM") + .build()) + .build(); + } + + private CreditNote createCreditNote(String clientName) { + return CreditNote.builder() + .serie("FC01") + .numero(1) + .comprobanteAfectadoSerieNumero("F001-1") + .sustentoDescripcion("mi sustento") + .proveedor(Proveedor.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .cliente(Cliente.builder() + .nombre(clientName) + .numeroDocumentoIdentidad("12121212121") + .tipoDocumentoIdentidad(Catalog6.RUC.toString()) + .build()) + .detalle(DocumentoVentaDetalle.builder() + .descripcion("Item1") + .cantidad(new BigDecimal("10")) + .precio(new BigDecimal("100")) + .build()) + .build(); + } + + private DebitNote createDebitNote(String clientName) { + return DebitNote.builder() + .serie("FD01") + .numero(1) + .comprobanteAfectadoSerieNumero("F001-1") + .sustentoDescripcion("mi sustento") + .proveedor(Proveedor.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) .cliente(Cliente.builder() - .nombre(clientName) - .numeroDocumentoIdentidad("12345678") - .tipoDocumentoIdentidad( - Catalog6.DNI.getCode()) - .build() - ) - .impuestos(ComprobanteImpuestos.builder() - .igv(new BigDecimal("18")) - .icb(new BigDecimal(2)) - .build() - ) - .valorVenta(ComprobanteValorVenta.builder() - .importeTotal(new BigDecimal("120")) - .gravado(new BigDecimal("120")) - .build() - ) - .build() - ) - .build() - ) - .build(); - } - - private Perception createPerception(String clientName) { - return Perception.builder() - .serie("P001") - .numero(1) - .fechaEmision(LocalDate.of(2022, 01, 31)) - .proveedor(Proveedor.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build() - ) - .cliente(Cliente.builder() - .nombre(clientName) - .numeroDocumentoIdentidad("12121212121") - .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) - .build() - ) - .importeTotalPercibido(new BigDecimal("10")) - .importeTotalCobrado(new BigDecimal("210")) - .tipoRegimen(Catalog22.VENTA_INTERNA.getCode()) - .tipoRegimenPorcentaje(Catalog22.VENTA_INTERNA.getPercent()) // - .operacion(PercepcionRetencionOperacion.builder() - .numeroOperacion(1) - .fechaOperacion(LocalDate.of(2022, 01, 31)) - .importeOperacion(new BigDecimal("100")) - .comprobante(io.github.project.openubl.xbuilder.content.models.sunat.percepcionretencion.ComprobanteAfectado - .builder() - .tipoComprobante(Catalog1.FACTURA.getCode()) - .serieNumero("F001-1") + .nombre(clientName) + .numeroDocumentoIdentidad("12121212121") + .tipoDocumentoIdentidad(Catalog6.RUC.toString()) + .build()) + .detalle(DocumentoVentaDetalle.builder() + .descripcion("Item1") + .cantidad(new BigDecimal("10")) + .precio(new BigDecimal("100")) + .build()) + .build(); + } + + private VoidedDocuments createVoidedDocuments(String clientName) { + return VoidedDocuments.builder() + .numero(1) + .fechaEmision(LocalDate.of(2022, 01, 31)) + .fechaEmisionComprobantes(LocalDate.of(2022, 01, 29)) + .proveedor(Proveedor.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .comprobante(VoidedDocumentsItem.builder() + .serie("F001") + .numero(1) + .tipoComprobante(Catalog1_Invoice.FACTURA.getCode()) + .descripcionSustento("Mi sustento1") + .build()) + .comprobante(VoidedDocumentsItem.builder() + .serie("F001") + .numero(2) + .tipoComprobante(Catalog1_Invoice.FACTURA.getCode()) + .descripcionSustento("Mi sustento2") + .build()) + .build(); + } + + private SummaryDocuments createSummaryDocuments(String clientName) { + return SummaryDocuments.builder() + .numero(1) + .fechaEmisionComprobantes(dateProvider.now().minusDays(2)) + .proveedor(Proveedor.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .comprobante(SummaryDocumentsItem.builder() + .tipoOperacion(Catalog19.ADICIONAR.toString()) + .comprobante(Comprobante.builder() + .tipoComprobante(Catalog1_Invoice.BOLETA.getCode())// + .serieNumero("B001-1") + .cliente(Cliente.builder() + .nombre(clientName) + .numeroDocumentoIdentidad("12345678") + .tipoDocumentoIdentidad( + Catalog6.DNI.getCode()) + .build()) + .impuestos(ComprobanteImpuestos.builder() + .igv(new BigDecimal("18")) + .icb(new BigDecimal(2)) + .build()) + .valorVenta(ComprobanteValorVenta.builder() + .importeTotal(new BigDecimal("120")) + .gravado(new BigDecimal("120")) + .build()) + .build()) + .build()) + .build(); + } + + private Perception createPerception(String clientName) { + return Perception.builder() + .serie("P001") + .numero(1) .fechaEmision(LocalDate.of(2022, 01, 31)) - .importeTotal(new BigDecimal("200")) - .moneda("PEN") - .build() - ) - .build() - ) - .build(); - } - - private Retention createRetention(String clientName) { - return Retention.builder() - .serie("R001") - .numero(1) - .fechaEmision(LocalDate.of(2022, 01, 31)) - .proveedor(Proveedor.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build()) - .cliente(Cliente.builder() - .nombre(clientName) - .numeroDocumentoIdentidad("12121212121") - .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) - .build() - ) - .importeTotalRetenido(new BigDecimal("10")) - .importeTotalPagado(new BigDecimal("200")) - .tipoRegimen(Catalog23.TASA_TRES.getCode()) - .tipoRegimenPorcentaje(Catalog23.TASA_TRES.getPercent()) // - .operacion(PercepcionRetencionOperacion.builder() - .numeroOperacion(1) - .fechaOperacion(LocalDate.of(2022, 01, 31)) - .importeOperacion(new BigDecimal("100")) - .comprobante(io.github.project.openubl.xbuilder.content.models.sunat.percepcionretencion.ComprobanteAfectado - .builder() - .tipoComprobante(Catalog1.FACTURA.getCode()) - .serieNumero("F001-1") + .proveedor(Proveedor.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .cliente(Cliente.builder() + .nombre(clientName) + .numeroDocumentoIdentidad("12121212121") + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .build()) + .importeTotalPercibido(new BigDecimal("10")) + .importeTotalCobrado(new BigDecimal("210")) + .tipoRegimen(Catalog22.VENTA_INTERNA.getCode()) + .tipoRegimenPorcentaje(Catalog22.VENTA_INTERNA.getPercent()) // + .operacion(PercepcionRetencionOperacion.builder() + .numeroOperacion(1) + .fechaOperacion(LocalDate.of(2022, 01, 31)) + .importeOperacion(new BigDecimal("100")) + .comprobante(io.github.project.openubl.xbuilder.content.models.sunat.percepcionretencion.ComprobanteAfectado + .builder() + .tipoComprobante(Catalog1.FACTURA.getCode()) + .serieNumero("F001-1") + .fechaEmision(LocalDate.of(2022, 01, 31)) + .importeTotal(new BigDecimal("200")) + .moneda("PEN") + .build()) + .build()) + .build(); + } + + private Retention createRetention(String clientName) { + return Retention.builder() + .serie("R001") + .numero(1) .fechaEmision(LocalDate.of(2022, 01, 31)) - .importeTotal(new BigDecimal("210")) - .moneda("PEN") - .build() - ) - .build() - ) - .build(); - } - - private DespatchAdvice createDespatchAdvice(String clientName) { - return DespatchAdvice.builder() - .serie("T001") - .numero(1) - .tipoComprobante(Catalog1.GUIA_REMISION_REMITENTE.getCode()) - .remitente(Remitente.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build() - ) - .destinatario(Destinatario.builder() - .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) - .numeroDocumentoIdentidad("12345678") - .nombre(clientName) - .build() - ) - .envio(Envio.builder() - .tipoTraslado(Catalog20.TRASLADO_EMISOR_ITINERANTE_CP.getCode()) - .pesoTotal(BigDecimal.ONE) - .pesoTotalUnidadMedida("KG") - .tipoModalidadTraslado(Catalog18.TRANSPORTE_PRIVADO.getCode()) - .fechaTraslado(dateProvider.now()) - .partida(Partida.builder() - .direccion("DireccionOrigen") - .ubigeo("010101") - .build() - ) - .destino(Destino.builder() - .direccion("DireccionDestino") - .ubigeo("020202") - .build() - ) - .build()) - .detalle(DespatchAdviceItem.builder() - .cantidad(new BigDecimal("0.5")) - .unidadMedida("KG") - .codigo("123456") - .build() - ) - .build(); - } - - private Reversion createReversion(String clientName) { - return Reversion.builder() - .numero(1) - .fechaEmision(LocalDate.now()) - .fechaEmisionComprobantes(LocalDate.now().minusDays(1)) - .proveedor(Proveedor.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build() - ) - .comprobante(VoidedDocumentsItem.builder() - .serie("P001") - .numero(1) - .tipoComprobante(Catalog1.PERCEPCION.getCode()) - .descripcionSustento("Anulacion de percepcion por error en emision") - .build() - ) - .comprobante(VoidedDocumentsItem.builder() - .serie("R001") - .numero(1) - .tipoComprobante(Catalog1.RETENCION.getCode()) - .descripcionSustento("Anulacion de retencion por duplicado") - .build() - ) - .build(); - } + .proveedor(Proveedor.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .cliente(Cliente.builder() + .nombre(clientName) + .numeroDocumentoIdentidad("12121212121") + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .build()) + .importeTotalRetenido(new BigDecimal("10")) + .importeTotalPagado(new BigDecimal("200")) + .tipoRegimen(Catalog23.TASA_TRES.getCode()) + .tipoRegimenPorcentaje(Catalog23.TASA_TRES.getPercent()) // + .operacion(PercepcionRetencionOperacion.builder() + .numeroOperacion(1) + .fechaOperacion(LocalDate.of(2022, 01, 31)) + .importeOperacion(new BigDecimal("100")) + .comprobante(io.github.project.openubl.xbuilder.content.models.sunat.percepcionretencion.ComprobanteAfectado + .builder() + .tipoComprobante(Catalog1.FACTURA.getCode()) + .serieNumero("F001-1") + .fechaEmision(LocalDate.of(2022, 01, 31)) + .importeTotal(new BigDecimal("210")) + .moneda("PEN") + .build()) + .build()) + .build(); + } + + private DespatchAdvice createDespatchAdvice(String clientName) { + return DespatchAdvice.builder() + .serie("T001") + .numero(1) + .tipoComprobante(Catalog1.GUIA_REMISION_REMITENTE.getCode()) + .remitente(Remitente.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("12345678") + .nombre(clientName) + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.TRASLADO_EMISOR_ITINERANTE_CP.getCode()) + .pesoTotal(BigDecimal.ONE) + .pesoTotalUnidadMedida("KG") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PRIVADO.getCode()) + .fechaTraslado(dateProvider.now()) + .partida(Partida.builder() + .direccion("DireccionOrigen") + .ubigeo("010101") + .build()) + .destino(Destino.builder() + .direccion("DireccionDestino") + .ubigeo("020202") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("0.5")) + .unidadMedida("KG") + .codigo("123456") + .build()) + .build(); + } + + private Reversion createReversion(String clientName) { + return Reversion.builder() + .numero(1) + .fechaEmision(LocalDate.now()) + .fechaEmisionComprobantes(LocalDate.now().minusDays(1)) + .proveedor(Proveedor.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .comprobante(VoidedDocumentsItem.builder() + .serie("P001") + .numero(1) + .tipoComprobante(Catalog1.PERCEPCION.getCode()) + .descripcionSustento("Anulacion de percepcion por error en emision") + .build()) + .comprobante(VoidedDocumentsItem.builder() + .serie("R001") + .numero(1) + .tipoComprobante(Catalog1.RETENCION.getCode()) + .descripcionSustento("Anulacion de retencion por duplicado") + .build()) + .build(); + } + + /** + * GRE-Remitente: transporte privado básico. + * El remitente traslada bienes con su propio vehículo y conductor. + */ + private GRERemitente createGRERemitente(String clientName) { + return GRERemitente.builder() + .serie("T001") + .numero(1) + .remitente(Remitente.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20200020002") + .nombre(clientName) + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .pesoTotal(new BigDecimal("50.000")) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PRIVADO.getCode()) + .fechaTraslado(dateProvider.now()) + .chofer(Driver.builder() + .tipo(TipoConductor.PRINCIPAL.getCode()) + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("12345678") + .nombres("Carlos") + .apellidos("Ramirez") + .licencia("Q1234567") + .build()) + .vehiculo(Vehicle.builder() + .placa("ABC-123") + .build()) + .partida(Partida.builder() + .ubigeo("150101") + .direccion("Av. Industrial 456, Lima") + .build()) + .destino(Destino.builder() + .ubigeo("150102") + .direccion("Jr. Comercio 789, Rímac") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("10.00")) + .unidadMedida("NIU") + .codigo("PROD-001") + .descripcion("Cajas de producto terminado") + .build()) + .build(); + } + + /** + * GRE-Transportista: el transportista emite la guía para trasladar bienes de un + * cliente. + * En esta casuística básica se consigna conductor y vehículo. + */ + private GRETransportista createGRETransportista(String clientName) { + return GRETransportista.builder() + .serie("V001") + .numero(1) + .transportistaEmisor(Transportista.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("12345678912") + .nombre("Softgreen S.A.C.") + .numeroRegistroMTC("MTC-001234") + .build()) + .remitente(Tercero.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20100010001") + .nombre("Empresa Remitente S.A.C.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20200020002") + .nombre(clientName) + .build()) + .conductor(Driver.builder() + .tipo(TipoConductor.PRINCIPAL.getCode()) + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("44444444") + .nombres("Miguel") + .apellidos("Torres") + .licencia("Q4444444") + .build()) + .vehiculo(Vehicle.builder() + .placa("JKL-012") + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .pesoTotal(new BigDecimal("300.000")) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PUBLICO.getCode()) + .fechaTraslado(dateProvider.now()) + .partida(Partida.builder() + .ubigeo("150101") + .direccion("Almacén Remitente, Lima") + .build()) + .destino(Destino.builder() + .ubigeo("040101") + .direccion("Sucursal Arequipa") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("25.00")) + .unidadMedida("NIU") + .codigo("MER-001") + .descripcion("Mercadería general") + .build()) + .build(); + } } diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog20.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog20.java index 0cec8b7f..02a09de1 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog20.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog20.java @@ -1,15 +1,93 @@ +/* + * Catálogo 20 - Motivo de traslado + * + * Fuente normativa: Anexo N.° 8 de la RS 000123-2022/SUNAT, + * actualizado por RS 000240-2024/SUNAT. + */ package io.github.project.openubl.xbuilder.content.catalogs; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Catálogo N.° 20 - Motivo de traslado. + *

+ * Vigente según RS 000123-2022/SUNAT y RS 000240-2024/SUNAT. + * Aplicable tanto a GRE-Remitente (09) como GRE-Transportista (31). + */ public enum Catalog20 implements Catalog { + + /** 01 - Venta */ VENTA("01"), - VENTA_SUJETA_A_CONFIRMACION_DEL_COMPRADOR("14"), + + /** 02 - Compra */ COMPRA("02"), - TRASLADO_ENTRE_ESTABLECIMIENTOS_DE_LA_MISMA_EMPRESA("04"), - TRASLADO_EMISOR_ITINERANTE_CP("18"), + + /** 03 - Venta con entrega a terceros (consignación) */ + CONSIGNACION("03"), + + /** 04 - Traslado entre establecimientos de la misma empresa */ + TRASLADO_ENTRE_ESTABLECIMIENTOS("04"), + + /** 05 - Devolución */ + DEVOLUCION("05"), + + /** 06 - Traslado de bienes para transformación */ + TRASLADO_TRANSFORMACION("06"), + + /** 07 - Recojo de bienes transformados */ + RECOJO_BIENES_TRANSFORMADOS("07"), + + /** 08 - Importación */ IMPORTACION("08"), + + /** 09 - Exportación */ EXPORTACION("09"), - TRASLADO_A_ZONA_PRIMARIA("19"), - OTROS("13"); + + /** + * 10 - Importación: traslado de bienes con DAM/DS con levante. + *

+ * Aplica cuando la mercancía tiene levante autorizado. + * Incorporado por RS 000240-2024/SUNAT para trazabilidad de comercio exterior. + * Vigente desde 14-nov-2024; uso como motivo mandatorio pospuesto al + * 01-jul-2026. + */ + IMPORTACION_CON_DAM("10"), + + /** 11 - Importación temporal */ + IMPORTACION_TEMPORAL("11"), + + /** 13 - Otros */ + OTROS("13"), + + /** 14 - Venta sujeta a confirmación del comprador */ + VENTA_SUJETA_A_CONFIRMACION("14"), + + /** + * 15 - Traslado de bienes zona IVAP. + *

+ * Aplica para traslados de bienes gravados con IVAP (arroz). + */ + TRASLADO_ZONA_IVAP("15"), + + /** 16 - Exportación temporal (admisión temporal) */ + EXPORTACION_TEMPORAL("16"), + + /** 17 - Reexportación */ + REEXPORTACION("17"), + + /** 18 - Traslado emisor itinerante de comprobantes de pago */ + TRASLADO_EMISOR_ITINERANTE_CP("18"), + + /** + * 19 - Traslado de mercancía extranjera (zona primaria a depósito temporal). + *

+ * Uso obligatorio a partir del 01-jul-2026 para traslado de mercancía + * extranjera sin destinación aduanera o sin levante, en reemplazo del ticket de + * salida. + * Vigencia de la derogación del ticket pospuesta por RS 000133-2025/SUNAT. + */ + TRASLADO_MERCANCIA_EXTRANJERA("19"); private final String code; @@ -17,6 +95,10 @@ public enum Catalog20 implements Catalog { this.code = code; } + public static Optional valueOfCode(String code) { + return Stream.of(Catalog20.values()).filter(p -> p.code.equals(code)).findFirst(); + } + @Override public String getCode() { return code; diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog21.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog21.java index 1237d471..abd3b35d 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog21.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog21.java @@ -1,12 +1,53 @@ package io.github.project.openubl.xbuilder.content.catalogs; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Catálogo N.° 21 - Tipo de documento relacionado a la Guía de Remisión + * Electrónica. + *

+ * Fuente: Anexo N.° 8 de la RS 000123-2022/SUNAT. + * Se utiliza en {@code cac:AdditionalDocumentReference / cbc:DocumentTypeCode}. + */ public enum Catalog21 implements Catalog { + /** 01 - Numeración DAM (Declaración Aduanera de Mercancías) */ NUMERACION_DAM("01"), + + /** 02 - Número de orden de entrega */ NUMERO_DE_ORDEN_DE_ENTREGA("02"), + + /** 03 - Número SCOP */ NUMERO_SCOP("03"), + + /** 04 - Número de manifiesto de carga */ NUMERO_DE_MANIFIESTO_DE_CARGA("04"), + + /** 05 - Número de constancia de detracción */ NUMERO_DE_CONSTANCIA_DE_DETRACCION("05"), - OTROS("06"); + + /** 06 - Otros */ + OTROS("06"), + + /** 09 - Guía de remisión remitente */ + GUIA_REMISION_REMITENTE("09"), + + /** 12 - Declaración Simplificada (DS) */ + DECLARACION_SIMPLIFICADA("12"), + + /** 31 - Guía de remisión transportista */ + GUIA_REMISION_TRANSPORTISTA("31"), + + /** + * 49 - Ticket de salida ENAPU. + *

+ * Vigente condicionalmente: la derogación del ticket de salida ha sido + * pospuesta al 01-jul-2026 por RS 000133-2025/SUNAT. + */ + TICKET_SALIDA("49"), + + /** 50 - Código de autorización emitido por SUNAT */ + CODIGO_AUTORIZACION_SUNAT("50"); private final String code; @@ -14,6 +55,10 @@ public enum Catalog21 implements Catalog { this.code = code; } + public static Optional valueOfCode(String code) { + return Stream.of(Catalog21.values()).filter(p -> p.code.equals(code)).findFirst(); + } + @Override public String getCode() { return code; diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog61.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog61.java new file mode 100644 index 00000000..053852c8 --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog61.java @@ -0,0 +1,68 @@ +/* + * Catálogo 61 - Tipo de documento adicional relacionado al transporte + * + * Fuente normativa: Anexo N.° 8, Catálogo N.° 61 de la RS 000123-2022/SUNAT. + * Estos documentos se consignan en cac:AdditionalDocumentReference de la GRE + * con un DocumentTypeCode basado en este catálogo. + */ +package io.github.project.openubl.xbuilder.content.catalogs; + +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Catálogo N.° 61 - Tipo de documento adicional relacionado al transporte. + *

+ * Aplicable a los campos {@code cac:AdditionalDocumentReference} de la GRE + * correspondientes a documentos del ámbito de transporte y comercio exterior. + */ +public enum Catalog61 implements Catalog { + + /** 01 - Factura */ + FACTURA("01"), + + /** 02 - Boleta de venta */ + BOLETA_VENTA("02"), + + /** 03 - Liquidación de compra */ + LIQUIDACION_COMPRA("03"), + + /** 04 - Guía de remisión remitente */ + GUIA_REMISION_REMITENTE("04"), + + /** 05 - Guía de remisión transportista */ + GUIA_REMISION_TRANSPORTISTA("05"), + + /** 06 - Carta de porte aéreo */ + CARTA_PORTE_AEREO("06"), + + /** 07 - Póliza de adjudicación */ + POLIZA_ADJUDICACION("07"), + + /** 09 - Guía de remisión remitente complementaria */ + GUIA_REMISION_REMITENTE_COMP("09"), + + /** 10 - Guía de remisión transportista complementaria */ + GUIA_REMISION_TRANSPORTISTA_COMP("10"), + + /** 50 - DAM (Declaración Aduanera de Mercancías) */ + DAM("50"), + + /** 52 - Declaración Simplificada de Importación/Exportación */ + DECLARACION_SIMPLIFICADA("52"); + + private final String code; + + Catalog61(String code) { + this.code = code; + } + + public static Optional valueOfCode(String code) { + return Stream.of(Catalog61.values()).filter(p -> p.code.equals(code)).findFirst(); + } + + @Override + public String getCode() { + return code; + } +} diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog62.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog62.java new file mode 100644 index 00000000..f4a79b02 --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog62.java @@ -0,0 +1,50 @@ +/* + * Catálogo 62 - Bienes normalizados sujetos a SPOT/IVAP + * + * Fuente normativa: Anexo N.° 8, Catálogo N.° 62 de la RS 000123-2022/SUNAT. + * + * Nota SUNAT FAQ #25: Se consideran bienes normalizados los bienes detallados + * en este catálogo CUANDO se encuentran sujetos al SPOT o IVAP. Si no están + * sujetos, no califican como bien normalizado y no se marca el indicador. + */ +package io.github.project.openubl.xbuilder.content.catalogs; + +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Catálogo N.° 62 - Bienes normalizados sujetos a detracción (SPOT) o IVAP. + *

+ * Se utiliza para marcar el indicador SUNAT_Envio_IndicadorBienNormalizado + * en el XML de la GRE cuando se transportan estos bienes y están sujetos + * a SPOT/IVAP. + */ +public enum Catalog62 implements Catalog { + + /** Azúcar - sujeto a SPOT */ + AZUCAR("01"), + + /** Arroz - sujeto a IVAP */ + ARROZ("02"), + + /** Alcohol etílico - sujeto a SPOT */ + ALCOHOL_ETILICO("03"), + + /** Cemento (zonas de control de IQBF) */ + CEMENTO("04"); + + private final String code; + + Catalog62(String code) { + this.code = code; + } + + public static Optional valueOfCode(String code) { + return Stream.of(Catalog62.values()).filter(p -> p.code.equals(code)).findFirst(); + } + + @Override + public String getCode() { + return code; + } +} diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog63.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog63.java new file mode 100644 index 00000000..4f9f6827 --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog63.java @@ -0,0 +1,60 @@ +/* + * Catálogo 63 - Puertos + * + * Fuente normativa: Anexo N.° 8, Catálogo N.° 63 de la RS 000123-2022/SUNAT. + * Se utiliza como código de ubicación en FirstArrivalPortLocation con LocationTypeCode=1. + * + * Nota: Los puertos mencionados explícitamente en el RCP art. 21 numeral 3.2.9 + * son Callao, Paita, Salaverry, Chimbote, Pisco, Ilo, Matarani y Chancay + * (este último agregado por RS 000240-2024/SUNAT). + */ +package io.github.project.openubl.xbuilder.content.catalogs; + +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Catálogo N.° 63 - Puertos nacionales. + *

+ * Utilizado en {@code cac:FirstArrivalPortLocation / cbc:ID} con + * {@code cbc:LocationTypeCode = 1} para indicar puerto de embarque/desembarque. + */ +public enum Catalog63 implements Catalog { + + CALLAO("CALLAO", "Puerto del Callao"), + PAITA("PAITA", "Puerto de Paita"), + SALAVERRY("SALAVERRY", "Puerto de Salaverry"), + CHIMBOTE("CHIMBOTE", "Puerto de Chimbote"), + PISCO("PISCO", "Puerto de Pisco"), + ILO("ILO", "Puerto de Ilo"), + MATARANI("MATARANI", "Puerto de Matarani"), + + /** + * Puerto de Chancay - agregado por RS 000240-2024/SUNAT para comercio exterior. + * Vigente desde 14-nov-2024. + */ + CHANCAY("CHANCAY", "Puerto de Chancay"); + + private final String code; + private final String description; + + Catalog63(String code, String description) { + this.code = code; + this.description = description; + } + + public String getDescription() { + return description; + } + + public static Optional valueOfCode(String code) { + return Stream.of(Catalog63.values()) + .filter(p -> p.code.equalsIgnoreCase(code)) + .findFirst(); + } + + @Override + public String getCode() { + return code; + } +} diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog64.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog64.java new file mode 100644 index 00000000..50c9fb59 --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog64.java @@ -0,0 +1,54 @@ +/* + * Catálogo 64 - Aeropuertos + * + * Fuente normativa: Anexo N.° 8, Catálogo N.° 64 de la RS 000123-2022/SUNAT. + * Se utiliza como código de ubicación en FirstArrivalPortLocation con LocationTypeCode=2. + */ +package io.github.project.openubl.xbuilder.content.catalogs; + +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Catálogo N.° 64 - Aeropuertos nacionales. + *

+ * Utilizado en {@code cac:FirstArrivalPortLocation / cbc:ID} con + * {@code cbc:LocationTypeCode = 2} para indicar aeropuerto de + * embarque/desembarque. + */ +public enum Catalog64 implements Catalog { + + JORGE_CHAVEZ("LIM", "Aeropuerto Internacional Jorge Chávez"), + RODRIGUEZ_BALLON("AQP", "Aeropuerto Alfredo Rodríguez Ballón"), + ALEJANDRO_VELASCO("CUZ", "Aeropuerto Alejandro Velasco Astete"), + CAP_FAP_CARLOS_MARTINEZ_DE_PINILLOS("TRU", "Aeropuerto Carlos Martínez de Pinillos"), + CAP_FAP_JOSE_A_QUINONES("CIX", "Aeropuerto José A. Quiñones"), + INCA_MANCO_CAPAC("JUL", "Aeropuerto Inca Manco Cápac"), + PADRE_ALDAMIZ("PEM", "Aeropuerto Padre Aldamiz"), + CORONEL_FAP_FRANCISCO_SECADA("IQT", "Aeropuerto Coronel FAP Francisco Secada"), + CAP_FAP_DAVID_ABENSUR("PCL", "Aeropuerto David Abensur Rengifo"), + MAYOR_GENERAL_FAP_ARMANDO_REVOREDO("CJA", "Aeropuerto Mayor General FAP Armando Revoredo"); + + private final String code; + private final String description; + + Catalog64(String code, String description) { + this.code = code; + this.description = description; + } + + public String getDescription() { + return description; + } + + public static Optional valueOfCode(String code) { + return Stream.of(Catalog64.values()) + .filter(p -> p.code.equalsIgnoreCase(code)) + .findFirst(); + } + + @Override + public String getCode() { + return code; + } +} diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/IndicadorEnvio.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/IndicadorEnvio.java new file mode 100644 index 00000000..89f7e5c9 --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/IndicadorEnvio.java @@ -0,0 +1,113 @@ +/* + * Indicadores especiales de envío para la GRE + * + * Fuente normativa: Anexo N.° 14, UBL 2.1 de la RS 000123-2022/SUNAT. + * Estos indicadores se consignan como cbc:SpecialInstructions dentro de cac:Shipment. + * + * Nota: No todos los indicadores aplican a ambos tipos de GRE. + * La columna "Aplica GRE-Remitente / GRE-Transportista" se documenta en cada valor. + */ +package io.github.project.openubl.xbuilder.content.catalogs; + +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Indicadores especiales de envío (SpecialInstructions) para la GRE. + *

+ * Se mapean a {@code cbc:SpecialInstructions} dentro de {@code cac:Shipment}. + * Cada indicador tiene un código literal que debe coincidir exactamente con + * lo que valida SUNAT. + */ +public enum IndicadorEnvio implements Catalog { + + /** + * Indica que el traslado involucra la totalidad de la DAM/DS. + *

+ * Aplica: GRE-Remitente (motivos 08, 09, 10, 19). + * Si se marca, no se requiere detalle de ítems (se acepta línea vacía + * obligatoria por UBL). + * Ref: FAQ #24 SUNAT. + */ + TRASLADO_TOTAL_DAM_DS("SUNAT_Envio_IndicadorTrasladoTotalDAMDS"), + + /** + * Indica que los bienes trasladados son bienes normalizados sujetos a + * SPOT/IVAP. + *

+ * Aplica: GRE-Remitente. + * Condición: solo se marca si el bien está en el Catálogo 62 Y está sujeto a + * SPOT o IVAP. + * Ref: FAQ #25 SUNAT - si un bien del catálogo 62 NO está sujeto a SPOT o IVAP, + * NO califica como bien normalizado. + */ + BIEN_NORMALIZADO("SUNAT_Envio_IndicadorBienNormalizado"), + + /** + * Indica que el traslado es en vehículos de categoría M1 o L (vehículos + * menores). + *

+ * Aplica: GRE-Remitente (transporte privado). + * Cuando se marca, no se requiere consignar placa ni conductor. + *

+ * PENDIENTE DE VALIDACIÓN: El token exacto de este indicador debe + * confirmarse contra el Anexo N.° 14 o el catálogo interno del portal SUNAT. + * La separación conceptual respecto de {@link #TRANSBORDO_PROGRAMADO} es + * correcta. + *

+ * Ref: Anexo N.° 14, RS 000123-2022/SUNAT — campo cbc:SpecialInstructions. + */ + VEHICULO_M1_L("SUNAT_Envio_IndicadorTrasladoVehiculoM1L"), + + /** + * Indica que se ha producido un transbordo programado durante el trayecto. + *

+ * Este indicador corresponde a la GRE por eventos: se utiliza cuando ocurre + * un hecho no imputable al emisor que obliga a un transbordo o reinicio del + * traslado. NO es equivalente a vehículo categoría M1/L. + *

+ * Aplica: GRE-Remitente, GRE-Transportista (GRE complementaria por eventos). + * Ref: Numeral 4, Anexo RS 000123-2022/SUNAT — GRE por eventos. + */ + TRANSBORDO_PROGRAMADO("SUNAT_Envio_IndicadorTransbordoProgramado"), + + /** + * Indica que el retorno del vehículo está programado y la GRE ampara el + * retorno. + *

+ * Aplica: GRE-Remitente. + */ + RETORNO_VEHICULO_ENVASES("SUNAT_Envio_IndicadorRetornoVehiculoEnvasesVacios"), + + /** + * Indica que el retorno del vehículo con envases vacíos está programado. + *

+ * Aplica: GRE-Remitente. + */ + RETORNO_VEHICULO_VACIO("SUNAT_Envio_IndicadorRetornoVehiculoVacio"), + + /** + * Indica que el traslado es operación de importación de bienes en zona + * primaria. + *

+ * Aplica: GRE-Remitente (motivo 19 mercancía extranjera). + * Incorporado por RS 000240-2024/SUNAT para trazabilidad. + * Vigente desde 14-nov-2024; obligatoriedad plena pospuesta al 01-jul-2026. + */ + TRASLADO_ZONA_PRIMARIA_COMEXT("SUNAT_Envio_IndicadorTrasladoVehiculoPesadoCarga"); + + private final String code; + + IndicadorEnvio(String code) { + this.code = code; + } + + public static Optional valueOfCode(String code) { + return Stream.of(IndicadorEnvio.values()).filter(p -> p.code.equals(code)).findFirst(); + } + + @Override + public String getCode() { + return code; + } +} diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/TipoConductor.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/TipoConductor.java new file mode 100644 index 00000000..70c133f5 --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/TipoConductor.java @@ -0,0 +1,39 @@ +package io.github.project.openubl.xbuilder.content.catalogs; + +/** + * Tipo de conductor en la Guía de Remisión Electrónica. + *

+ * Se mapea al campo {@code cbc:JobTitle} del elemento {@code cac:DriverPerson} + * en el XML UBL 2.1. + *

+ * SUNAT distingue dos roles: + *

+ *

+ * Ref: Anexo N.° 14 UBL 2.1, RS 000123-2022/SUNAT. + */ +public enum TipoConductor implements Catalog { + + /** + * Conductor principal — obligatorio cuando se requiere consignar conductor. + */ + PRINCIPAL("Principal"), + + /** + * Conductor secundario (relevo, copiloto) — opcional. + */ + SECUNDARIO("Secundario"); + + private final String code; + + TipoConductor(String code) { + this.code = code; + } + + @Override + public String getCode() { + return code; + } +} diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/jaxb/mappers/DespatchAdviceMapper.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/jaxb/mappers/DespatchAdviceMapper.java index 22332ed4..0a6fc682 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/jaxb/mappers/DespatchAdviceMapper.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/jaxb/mappers/DespatchAdviceMapper.java @@ -11,6 +11,7 @@ import io.github.project.openubl.xbuilder.content.models.common.Proveedor; import io.github.project.openubl.xbuilder.content.models.standard.guia.DespatchAdvice; import io.github.project.openubl.xbuilder.content.models.standard.guia.DespatchAdviceItem; +import io.github.project.openubl.xbuilder.content.models.standard.guia.Contenedor; import io.github.project.openubl.xbuilder.content.models.standard.guia.Comprador; import io.github.project.openubl.xbuilder.content.models.standard.guia.Tercero; import io.github.project.openubl.xbuilder.content.models.standard.guia.Destinatario; @@ -37,8 +38,10 @@ }, nullValuePropertyMappingStrategy = org.mapstruct.NullValuePropertyMappingStrategy.SET_TO_DEFAULT) public interface DespatchAdviceMapper { - @Mapping(target = "serie", source = "documentId", qualifiedBy = { SerieNumeroTranslator.class, SerieTranslator.class }) - @Mapping(target = "numero", source = "documentId", qualifiedBy = { SerieNumeroTranslator.class, Numero2Translator.class }) + @Mapping(target = "serie", source = "documentId", qualifiedBy = { SerieNumeroTranslator.class, + SerieTranslator.class }) + @Mapping(target = "numero", source = "documentId", qualifiedBy = { SerieNumeroTranslator.class, + Numero2Translator.class }) @Mapping(target = "version", source = "customizationId") @Mapping(target = "fechaEmision", source = "issueDate") @Mapping(target = "horaEmision", source = "issueTime") @@ -57,6 +60,8 @@ public interface DespatchAdviceMapper { @Mapping(target = "comprador", source = "buyerCustomerParty") @Mapping(target = "documentoAdicional", ignore = true) @Mapping(target = "detalle", ignore = true) + @Mapping(target = "documentoRelacionadoAdicional", ignore = true) + @Mapping(target = "documentosRelacionados", ignore = true) DespatchAdvice map(XMLDespatchAdvice xml); @Mapping(target = "tipoDocumento", source = "orderTypeCode") @@ -141,12 +146,15 @@ default Tercero mapDespatchAdviceTercero(XMLDespatchAdvice.SellerSupplierParty x @Mapping(target = "chofer", ignore = true) @Mapping(target = "indicador", ignore = true) @Mapping(target = "contenedor", ignore = true) + @Mapping(target = "declaracionAduanera", ignore = true) + @Mapping(target = "declaracionesAduaneras", ignore = true) + @Mapping(target = "numeroManifiesto", ignore = true) Envio mapEnvio(XMLDespatchAdvice.Shipment xml); @Condition @Named("transportistaRequirements") default boolean conditionTransportista(XMLDespatchAdvice.ShipmentStage xml) { - return xml.getCarrierParty() != null && xml.getTransportMeans() != null && xml.getDriverPersons() != null && !xml.getDriverPersons().isEmpty(); + return xml.getCarrierParty() != null; } @Mapping(target = "tipoDocumentoIdentidad", source = "carrierParty.partyIdentification.id.schemeID") @@ -225,23 +233,18 @@ default String mapPrimaryDriverNum(List drivers) } @Named("mapContenedores") - default List mapContenedores(List units) { + default List mapContenedores(List units) { if (units == null) return java.util.Collections.emptyList(); - List result = new java.util.ArrayList<>(); + List result = new java.util.ArrayList<>(); for (XMLDespatchAdvice.TransportHandlingUnit unit : units) { if (unit.getPackages() != null) { - unit.getPackages().stream().map(XMLDespatchAdvice.Package::getTraceID) - .filter(java.util.Objects::nonNull) - .forEach(result::add); - } - if (unit.getTransportEquipments() != null) { - // Only add as container if it's NOT a vehicle (simple ID, no transport means) - unit.getTransportEquipments().stream() - .filter(e -> e.getApplicableTransportMeans() == null) - .map(XMLDespatchAdvice.TransportEquipment::getId) - .filter(java.util.Objects::nonNull) - .forEach(result::add); + for (XMLDespatchAdvice.Package pkg : unit.getPackages()) { + result.add(Contenedor.builder() + .numero(pkg.getId()) + .precinto(pkg.getTraceID()) + .build()); + } } } return result; @@ -294,11 +297,27 @@ default Puerto mapAeropuerto(XMLDespatchAdvice.FirstArrivalPortLocation xml) { default Vehicle mapVehiculo(List units) { if (units == null) return null; - // The first equipment with transport means or multiple are usually vehicles - return units.stream() + // First try: find equipment with transport means or attached (vehicle with + // details) + Vehicle detailed = units.stream() .filter(u -> u.getTransportEquipments() != null) .flatMap(u -> u.getTransportEquipments().stream()) - .filter(e -> e.getApplicableTransportMeans() != null || e.getAttachedTransportEquipments() != null) + .filter(e -> e.getApplicableTransportMeans() != null || e.getAttachedTransportEquipments() != null + || e.getShipmentDocumentReferences() != null) + .findFirst() + .map(this::mapDetailedVehicle) + .orElse(null); + if (detailed != null) + return detailed; + + // Fallback: find any TransportEquipment as vehicle + // Identify vehicles by exclusion: TransportEquipment that is in a THU + // separate from packages (containers). A THU with only TransportEquipment + // and no Packages is assumed to be a vehicle. + return units.stream() + .filter(u -> u.getTransportEquipments() != null && !u.getTransportEquipments().isEmpty()) + .filter(u -> u.getPackages() == null || u.getPackages().isEmpty()) + .flatMap(u -> u.getTransportEquipments().stream()) .findFirst() .map(this::mapDetailedVehicle) .orElse(null); diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/jaxb/mappers/SummaryDocumentsMapper.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/jaxb/mappers/SummaryDocumentsMapper.java index 6ade9936..6efcd28a 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/jaxb/mappers/SummaryDocumentsMapper.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/jaxb/mappers/SummaryDocumentsMapper.java @@ -25,90 +25,110 @@ import java.util.stream.Collectors; @Mapper(uses = { - SerieNumeroMapper.class, - FirmanteMapper.class, - ProveedorMapper.class + SerieNumeroMapper.class, + FirmanteMapper.class, + ProveedorMapper.class }) public interface SummaryDocumentsMapper { - @Mapping(target = "fechaEmision", source = "issueDate") - @Mapping(target = "firmante", source = "signature") - @Mapping(target = "proveedor", source = "accountingSupplierParty") - - @Mapping(target = "numero", source = "documentId", qualifiedBy = {SerieNumeroTranslator.class, Numero3Translator.class}) - @Mapping(target = "fechaEmisionComprobantes", source = "referenceDate") - @Mapping(target = "comprobantes", source = "lines") - SummaryDocuments map(XMLSummaryDocuments xml); - - @Mapping(target = "tipoOperacion", source = "status.conditionCode") - @Mapping(target = "comprobante", source = ".") - SummaryDocumentsItem mapLines(XMLSummaryDocumentsLine xml); - - @Mapping(target = "moneda", source = "totalAmount.currencyID") - @Mapping(target = "tipoComprobante", source = "documentTypeCode") - @Mapping(target = "serieNumero", source = "documentId") - @Mapping(target = "cliente", source = "accountingCustomerParty") - @Mapping(target = "comprobanteAfectado.serieNumero", source = "billingReference.invoiceDocumentReference.id") - @Mapping(target = "comprobanteAfectado.tipoComprobante", source = "billingReference.invoiceDocumentReference.documentTypeCode") - @Mapping(target = "valorVenta", source = ".") - @Mapping(target = "impuestos", source = ".") - Comprobante mapLineComprobante(XMLSummaryDocumentsLine xml); - - @Mapping(target = "numeroDocumentoIdentidad", source = "customerAssignedAccountID") - @Mapping(target = "tipoDocumentoIdentidad", source = "additionalAccountID") - Cliente mapCliente(XMLSummaryDocumentsLine.AccountingCustomerParty xml); - - default ComprobanteValorVenta mapLineComprobanteValorVenta(XMLSummaryDocumentsLine xml) { - if (xml == null) { - return null; - } + @Mapping(target = "fechaEmision", source = "issueDate") + @Mapping(target = "firmante", source = "signature") + @Mapping(target = "proveedor", source = "accountingSupplierParty") + + @Mapping(target = "numero", source = "documentId", qualifiedBy = { SerieNumeroTranslator.class, + Numero3Translator.class }) + @Mapping(target = "fechaEmisionComprobantes", source = "referenceDate") + @Mapping(target = "comprobantes", source = "lines") + SummaryDocuments map(XMLSummaryDocuments xml); + + @Mapping(target = "tipoOperacion", source = "status.conditionCode") + @Mapping(target = "comprobante", source = ".") + SummaryDocumentsItem mapLines(XMLSummaryDocumentsLine xml); + + @Mapping(target = "moneda", source = "totalAmount.currencyID") + @Mapping(target = "tipoComprobante", source = "documentTypeCode") + @Mapping(target = "serieNumero", source = "documentId") + @Mapping(target = "cliente", source = "accountingCustomerParty") + @Mapping(target = "comprobanteAfectado.serieNumero", source = "billingReference.invoiceDocumentReference.id") + @Mapping(target = "comprobanteAfectado.tipoComprobante", source = "billingReference.invoiceDocumentReference.documentTypeCode") + @Mapping(target = "valorVenta", source = ".") + @Mapping(target = "impuestos", source = ".") + Comprobante mapLineComprobante(XMLSummaryDocumentsLine xml); + + @Mapping(target = "numeroDocumentoIdentidad", source = "customerAssignedAccountID") + @Mapping(target = "tipoDocumentoIdentidad", source = "additionalAccountID") + Cliente mapCliente(XMLSummaryDocumentsLine.AccountingCustomerParty xml); + + default ComprobanteValorVenta mapLineComprobanteValorVenta(XMLSummaryDocumentsLine xml) { + if (xml == null) { + return null; + } - Map billingPayments = Optional.ofNullable(xml.getBillingPayments()) - .orElse(Collections.emptyList()) - .stream() - .collect(Collectors.toMap( - XMLSummaryDocumentsLine.BillingPayment::getInstructionId, - XMLSummaryDocumentsLine.BillingPayment::getPaidAmount - )); - - BigDecimal importeTotal = Optional.ofNullable(xml.getTotalAmount()) - .map(XMLSummaryDocumentsLine.TotalAmount::getValue) - .orElse(null); - BigDecimal otrosCargos = Optional.ofNullable(xml.getAllowanceCharge()) - .map(XMLSummaryDocumentsLine.AllowanceCharge::getValue) - .orElse(null); - - return ComprobanteValorVenta.builder() - .importeTotal(importeTotal) - .gravado(billingPayments.get("01")) - .exonerado(billingPayments.get("02")) - .inafecto(billingPayments.get("03")) - .gratuito(billingPayments.get("05")) - .otrosCargos(otrosCargos) - .build(); - } - - default ComprobanteImpuestos mapLineComprobanteImpuestos(XMLSummaryDocumentsLine xml) { - if (xml == null) { - return null; + Map billingPayments = Optional.ofNullable(xml.getBillingPayments()) + .orElse(Collections.emptyList()) + .stream() + .collect(Collectors.toMap( + XMLSummaryDocumentsLine.BillingPayment::getInstructionId, + XMLSummaryDocumentsLine.BillingPayment::getPaidAmount)); + + BigDecimal importeTotal = Optional.ofNullable(xml.getTotalAmount()) + .map(XMLSummaryDocumentsLine.TotalAmount::getValue) + .orElse(null); + BigDecimal otrosCargos = Optional.ofNullable(xml.getAllowanceCharge()) + .map(XMLSummaryDocumentsLine.AllowanceCharge::getValue) + .orElse(null); + + return ComprobanteValorVenta.builder() + .importeTotal(importeTotal) + .gravado(billingPayments.get("01")) + .exonerado(billingPayments.get("02")) + .inafecto(billingPayments.get("03")) + .gratuito(billingPayments.get("05")) + .otrosCargos(otrosCargos) + .build(); } - Map taxTotals = Optional.ofNullable(xml.getTaxTotals()) - .orElse(Collections.emptyList()) - .stream() - .collect(Collectors.toMap( - taxTotal -> Optional.ofNullable(taxTotal.getTaxSubtotals()) + default ComprobanteImpuestos mapLineComprobanteImpuestos(XMLSummaryDocumentsLine xml) { + if (xml == null) { + return null; + } + + Map taxTotals = Optional.ofNullable(xml.getTaxTotals()) + .orElse(Collections.emptyList()) + .stream() + .collect(Collectors.toMap( + taxTotal -> Optional.ofNullable(taxTotal.getTaxSubtotals()) + .flatMap(f -> Optional.ofNullable(f.getTaxCategory())) + .flatMap(f -> Optional.ofNullable(f.getTaxScheme())) + .flatMap(taxScheme -> Optional + .ofNullable(taxScheme.getId())) + .flatMap(code -> Catalog.valueOfCode(Catalog5.class, + code)) + .orElse(null), + taxTotal -> Optional.ofNullable(taxTotal.getTaxAmount()) + .orElse(BigDecimal.ZERO))); + + BigDecimal tasaIgv = Optional.ofNullable(xml.getTaxTotals()) + .orElse(Collections.emptyList()) + .stream() + .filter(taxTotal -> Optional.ofNullable(taxTotal.getTaxSubtotals()) + .flatMap(f -> Optional.ofNullable(f.getTaxCategory())) + .flatMap(f -> Optional.ofNullable(f.getTaxScheme())) + .flatMap(taxScheme -> Optional.ofNullable(taxScheme.getId())) + .flatMap(code -> Catalog.valueOfCode(Catalog5.class, code)) + .filter(c -> c == Catalog5.IGV) + .isPresent()) + .findFirst() + .flatMap(taxTotal -> Optional.ofNullable(taxTotal.getTaxSubtotals())) .flatMap(f -> Optional.ofNullable(f.getTaxCategory())) - .flatMap(f -> Optional.ofNullable(f.getTaxScheme())) - .flatMap(taxScheme -> Optional.ofNullable(taxScheme.getId())) - .flatMap(code -> Catalog.valueOfCode(Catalog5.class, code)) - .orElse(null), - taxTotal -> Optional.ofNullable(taxTotal.getTaxAmount()).orElse(BigDecimal.ZERO) - )); - - return ComprobanteImpuestos.builder() - .igv(taxTotals.get(Catalog5.IGV)) - .icb(taxTotals.get(Catalog5.ICBPER)) - .build(); - } + .flatMap(f -> Optional.ofNullable(f.getPercent())) + .map(percent -> percent.divide(BigDecimal.valueOf(100))) + .orElse(null); + + return ComprobanteImpuestos.builder() + .igv(taxTotals.get(Catalog5.IGV)) + .tasaIgv(tasaIgv) + .icb(taxTotals.get(Catalog5.ICBPER)) + .build(); + } } diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/jaxb/models/XMLSummaryDocumentsLine.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/jaxb/models/XMLSummaryDocumentsLine.java index 7de9302b..0eea22e6 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/jaxb/models/XMLSummaryDocumentsLine.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/jaxb/models/XMLSummaryDocumentsLine.java @@ -149,6 +149,9 @@ public static class TaxSubtotal { @Data @NoArgsConstructor public static class TaxCategory { + @XmlElement(name = "Percent", namespace = XMLConstants.CBC) + private BigDecimal percent; + @XmlElement(name = "TaxScheme", namespace = XMLConstants.CAC) private TaxScheme taxScheme; } diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Contenedor.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Contenedor.java new file mode 100644 index 00000000..730ffd63 --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Contenedor.java @@ -0,0 +1,44 @@ +package io.github.project.openubl.xbuilder.content.models.standard.guia; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Modelo para contenedor y precinto de transporte. + *

+ * Según el Anexo N.° 14 UBL 2.1, cada contenedor se representa en + * {@code cac:TransportHandlingUnit / cac:Package} con su número de contenedor + * (cbc:ID) y opcionalmente su número de precinto (cbc:TraceID). + *

+ * RS 000240-2024/SUNAT agrega campos adicionales para comercio exterior + * (mercancía extranjera y zona primaria). + * + * @since 2.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Contenedor { + + /** + * Número del contenedor. + *

+ * Requerido cuando el motivo de traslado involucra comercio exterior + * (importación, exportación, mercancía extranjera). + * Para motivos domésticos es opcional. + */ + @Schema(description = "Número del contenedor", requiredMode = Schema.RequiredMode.REQUIRED) + private String numero; + + /** + * Número de precinto del contenedor. + *

+ * Opcional. Se consigna en {@code cbc:TraceID}. + */ + @Schema(description = "Número de precinto del contenedor") + private String precinto; +} diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DeclaracionAduanera.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DeclaracionAduanera.java new file mode 100644 index 00000000..98cfc19b --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DeclaracionAduanera.java @@ -0,0 +1,50 @@ +package io.github.project.openubl.xbuilder.content.models.standard.guia; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Modelo para referencia a Declaración Aduanera de Mercancías (DAM) + * o Declaración Simplificada (DS). + *

+ * Según RS 000240-2024/SUNAT, cuando el motivo de traslado es Importación (10), + * Exportación (09), o Traslado de Mercancía Extranjera (19), se debe consignar + * la información de la DAM o DS. + *

+ * Se mapea a un {@code cac:AdditionalDocumentReference} con el código de + * catálogo 61 + * correspondiente (50=DAM, 52=DS). + * + * @since 2.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DeclaracionAduanera { + + /** + * Tipo de declaración: "DAM" o "DS". + * Se usa para determinar el código del Catálogo 61: + * - DAM = "50" + * - DS = "52" + */ + @Schema(description = "Tipo: DAM o DS", requiredMode = Schema.RequiredMode.REQUIRED) + private String tipo; + + /** + * Número de la declaración aduanera. + * Formato típico: 118-2024-10-XXXXXX + */ + @Schema(description = "Número de la DAM/DS", requiredMode = Schema.RequiredMode.REQUIRED) + private String numero; + + /** + * RUC de la aduana o agente que emitió la declaración (opcional). + */ + @Schema(description = "RUC del emisor de la declaración") + private String rucEmisor; +} diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdvice.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdvice.java index 9337c08a..ba4f8ac6 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdvice.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdvice.java @@ -15,6 +15,27 @@ import java.time.LocalTime; import java.util.List; +/** + * Modelo principal de la Guía de Remisión Electrónica (GRE). + *

+ * Soporta ambos tipos: + *

+ *

+ * Fuente normativa: RS 000123-2022/SUNAT (base), RS 000240-2024/SUNAT (comercio + * exterior), + * RS 000133-2025/SUNAT (prórroga hasta 01-jul-2026). + */ @Jacksonized @Data @Builder @@ -27,7 +48,11 @@ public class DespatchAdvice { private String version; /** - * Serie del comprobante + * Serie del comprobante. + *

*/ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, minLength = 4, pattern = "^[T|t|V|v].*$") private String serie; @@ -50,6 +75,13 @@ public class DespatchAdvice { @Schema(description = "Format: \"HH:MM:SS\". Ejemplo 12:00:00", pattern = "^\\d{2}:\\d{2}:\\d{2}$") private LocalTime horaEmision; + /** + * Tipo de comprobante según Catálogo 01: + * + */ @Schema(description = "Catalogo 01") private String tipoComprobante; @@ -61,6 +93,18 @@ public class DespatchAdvice { @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED) private DocumentoRelacionado documentoRelacionado; + /** + * Documentos relacionados adicionales. + * Permite vincular múltiples documentos como GRE-Remitente, DAM, DS, etc. + * Ver también {@link Envio#getDeclaracionesAduaneras()} para referencias DAM/DS + * específicas de comercio exterior. + * + * @since 2.0 - Permite múltiples documentos relacionados (antes solo uno). + */ + @Singular("documentoRelacionadoAdicional") + @Schema(description = "Documentos relacionados adicionales (Catálogo 21)") + private List documentosRelacionados; + /** * Documentos adicionales relacionados al transporte (Catálogo 61) */ @@ -71,6 +115,14 @@ public class DespatchAdvice { @Schema(description = "Persona que firma electrónicamente el comprobante. Si NULL los datos del proveedor son usados.") private Firmante firmante; + /** + * Datos del remitente. + *
    + *
  • GRE-Remitente (09): El remitente es quien envía los bienes.
  • + *
  • GRE-Transportista (31): El remitente es el transportista emisor + * (DespatchSupplierParty).
  • + *
+ */ @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private Remitente remitente; @@ -81,13 +133,18 @@ public class DespatchAdvice { private Proveedor proveedor; /** - * Datos del tercero (vendedor de los bienes cuando aplica) + * Datos del tercero (vendedor/remitente original de los bienes). + *

+ * Aplica principalmente en GRE-Transportista (31): el tercero es el remitente + * original que solicita el servicio de transporte. + * Se mapea a {@code cac:SellerSupplierParty}. */ @Schema(description = "Tercero/Vendedor de los bienes") private Tercero tercero; /** - * Datos del comprador (adquiriente de los bienes) + * Datos del comprador (adquiriente de los bienes). + * Se mapea a {@code cac:BuyerCustomerParty}. */ @Schema(description = "Comprador/Adquiriente de los bienes") private Comprador comprador; @@ -98,4 +155,20 @@ public class DespatchAdvice { @Singular @ArraySchema(minItems = 1, schema = @Schema(requiredMode = Schema.RequiredMode.REQUIRED)) private List detalles; + + // == Métodos utilitarios == + + /** + * Determina si esta GRE es de tipo Remitente (código 09). + */ + public boolean isGRERemitente() { + return "09".equals(tipoComprobante); + } + + /** + * Determina si esta GRE es de tipo Transportista (código 31). + */ + public boolean isGRETransportista() { + return "31".equals(tipoComprobante); + } } diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdviceValidator.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdviceValidator.java new file mode 100644 index 00000000..b30f299d --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdviceValidator.java @@ -0,0 +1,253 @@ +package io.github.project.openubl.xbuilder.content.models.standard.guia; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Validador de reglas de negocio para la Guía de Remisión Electrónica (GRE). + *

+ * Implementa las reglas funcionales de SUNAT según: + *

    + *
  • RS 000123-2022/SUNAT — Reglas base GRE-Remitente y GRE-Transportista
  • + *
  • RS 000240-2024/SUNAT — Reglas de comercio exterior (vigentes desde + * 14-nov-2024)
  • + *
  • RS 000133-2025/SUNAT — Prórroga: derogatoria del ticket de salida al + * 01-jul-2026
  • + *
+ *

+ * Las validaciones retornan una lista de mensajes de error. Si la lista está + * vacía, + * el documento es válido respecto a las reglas implementadas. + *

+ * IMPORTANTE: Este validador NO reemplaza la validación de SUNAT + * (XSD/XSL). + * Es una capa de validación temprana para detectar errores comunes antes del + * envío. + */ +public class DespatchAdviceValidator { + + /** + * Motivos de traslado que requieren referencia a DAM/DS (comercio exterior). + */ + private static final Set MOTIVOS_COMERCIO_EXTERIOR = new HashSet<>(Arrays.asList( + "08", // Importación + "09", // Exportación + "10", // Importación con DAM (RS 240-2024) + "19" // Mercancía extranjera (RS 240-2024) + )); + + /** Motivos de traslado que requieren puerto o aeropuerto. */ + private static final Set MOTIVOS_CON_PUERTO = new HashSet<>(Arrays.asList( + "08", // Importación + "09", // Exportación + "10", // Importación con DAM + "19" // Mercancía extranjera + )); + + /** + * Valida un DespatchAdvice completo y retorna la lista de errores encontrados. + * + * @param da el DespatchAdvice a validar + * @return lista de mensajes de error (vacía si es válido) + */ + public static List validate(DespatchAdvice da) { + List errors = new ArrayList<>(); + + validateBasicFields(da, errors); + validateSerie(da, errors); + validateParties(da, errors); + validateEnvio(da, errors); + validateDetalles(da, errors); + + return errors; + } + + private static void validateBasicFields(DespatchAdvice da, List errors) { + if (da.getSerie() == null || da.getSerie().isBlank()) { + errors.add("La serie es requerida"); + } + if (da.getNumero() == null || da.getNumero() < 1) { + errors.add("El número debe ser mayor a 0"); + } + } + + /** + * Valida la coherencia entre la serie y el tipo de comprobante. + *

+ * Regla SUNAT FAQ #7: + * - GRE-Remitente: Serie TXXX + * - GRE-Transportista: Serie VXXX + */ + private static void validateSerie(DespatchAdvice da, List errors) { + if (da.getSerie() == null || da.getTipoComprobante() == null) { + return; + } + String serie = da.getSerie().toUpperCase(); + String tipo = da.getTipoComprobante(); + + if ("09".equals(tipo) && !serie.startsWith("T")) { + errors.add("GRE-Remitente (09) requiere serie que inicie con 'T'. Serie actual: " + da.getSerie()); + } + if ("31".equals(tipo) && !serie.startsWith("V")) { + errors.add("GRE-Transportista (31) requiere serie que inicie con 'V'. Serie actual: " + da.getSerie()); + } + } + + /** + * Valida las partes (remitente, destinatario, tercero) según el tipo de GRE. + */ + private static void validateParties(DespatchAdvice da, List errors) { + if (da.getRemitente() == null) { + errors.add("El remitente es requerido"); + return; + } + if (da.getRemitente().getRuc() == null || da.getRemitente().getRuc().length() != 11) { + errors.add("El RUC del remitente debe tener 11 dígitos"); + } + + if (da.getDestinatario() == null) { + // FAQ #22: Para emisor itinerante, el destinatario puede ser el mismo + // remitente. + // Pero el campo sigue siendo obligatorio en el XML. + errors.add("El destinatario es requerido"); + } + + // GRE-Transportista (31): el tercero (remitente original) debería estar + // presente + if ("31".equals(da.getTipoComprobante()) && da.getTercero() == null && da.getProveedor() == null) { + errors.add("GRE-Transportista (31): se recomienda consignar el tercero (remitente original) " + + "o proveedor en SellerSupplierParty"); + } + } + + /** + * Valida los datos de envío/shipment según las reglas funcionales SUNAT. + */ + private static void validateEnvio(DespatchAdvice da, List errors) { + Envio envio = da.getEnvio(); + if (envio == null) { + errors.add("Los datos de envío son requeridos"); + return; + } + + if (envio.getTipoTraslado() == null || envio.getTipoTraslado().isBlank()) { + errors.add("El motivo de traslado (Catálogo 20) es requerido"); + } + if (envio.getPesoTotal() == null) { + errors.add("El peso total es requerido"); + } + if (envio.getTipoModalidadTraslado() == null) { + errors.add("La modalidad de traslado (Catálogo 18) es requerida"); + } + if (envio.getFechaTraslado() == null) { + errors.add("La fecha de traslado es requerida"); + } + + validateEnvioModalidad(da, envio, errors); + validateEnvioComercioExterior(envio, errors); + } + + /** + * Valida reglas de modalidad de transporte. + *

+ * Transporte privado (02): requiere conductor y vehículo. + * Transporte público (01): requiere datos del transportista. + */ + private static void validateEnvioModalidad(DespatchAdvice da, Envio envio, List errors) { + String modalidad = envio.getTipoModalidadTraslado(); + if (modalidad == null) + return; + + boolean isGRERemitente = "09".equals(da.getTipoComprobante()); + + if ("02".equals(modalidad) && isGRERemitente) { + // Transporte privado en GRE-Remitente: conductor y vehículo requeridos + boolean tieneIndicadorM1L = envio.getIndicadores() != null && + envio.getIndicadores().contains("SUNAT_Envio_IndicadorTrasladoVehiculoM1L"); + + if (!tieneIndicadorM1L) { + if (envio.getChoferes() == null || envio.getChoferes().isEmpty()) { + errors.add("Transporte privado en GRE-Remitente requiere al menos un conductor " + + "(salvo vehículo categoría M1/L)"); + } + if (envio.getVehiculo() == null) { + errors.add("Transporte privado en GRE-Remitente requiere datos del vehículo " + + "(salvo vehículo categoría M1/L)"); + } + } + } + + if ("01".equals(modalidad) && isGRERemitente) { + // Transporte público en GRE-Remitente: transportista requerido + if (envio.getTransportista() == null) { + errors.add("Transporte público en GRE-Remitente requiere datos del transportista"); + } + } + + // GRE-Transportista (31): siempre requiere conductor y vehículo + if ("31".equals(da.getTipoComprobante())) { + if (envio.getChoferes() == null || envio.getChoferes().isEmpty()) { + errors.add("GRE-Transportista requiere al menos un conductor"); + } + if (envio.getVehiculo() == null) { + errors.add("GRE-Transportista requiere datos del vehículo"); + } + } + } + + /** + * Valida reglas de comercio exterior (RS 000240-2024/SUNAT). + *

+ * Cuando el motivo de traslado es de comercio exterior: + *

    + *
  • Se recomienda incluir referencia DAM/DS
  • + *
  • Se recomienda incluir puerto o aeropuerto
  • + *
  • Se recomienda incluir contenedores
  • + *
+ *

+ * NOTA: Estas reglas son actualmente de recomendación. La obligatoriedad plena + * del motivo 19 y la derogación del ticket de salida fue pospuesta al + * 01-jul-2026 + * por RS 000133-2025/SUNAT. Hasta esa fecha se aplica discrecionalidad. + */ + private static void validateEnvioComercioExterior(Envio envio, List errors) { + String motivo = envio.getTipoTraslado(); + if (motivo == null || !MOTIVOS_COMERCIO_EXTERIOR.contains(motivo)) { + return; + } + + // Advertencia suave: no es error pero se recomienda + boolean tieneDAM = (envio.getDeclaracionesAduaneras() != null && !envio.getDeclaracionesAduaneras().isEmpty()); + if (!tieneDAM) { + // No se marca como error porque puede ser traslado para exportación sin DAM + // numerada (FAQ #32) + // Este es un caso donde la ambigüedad normativa indica que se puede usar motivo + // "otros" + } + + // Puerto/aeropuerto recomendado para comercio exterior + if (MOTIVOS_CON_PUERTO.contains(motivo) && envio.getPuerto() == null && envio.getAeropuerto() == null) { + // No error estricto, pero recomendable + } + } + + /** + * Valida los detalles/líneas del documento. + *

+ * Siempre debe haber al menos 1 línea (requerimiento UBL). + * FAQ #24: Con indicador de traslado total DAM/DS, la línea puede estar vacía + * (campos mínimos obligatorios por UBL). + */ + private static void validateDetalles(DespatchAdvice da, List errors) { + if (da.getDetalles() == null || da.getDetalles().isEmpty()) { + errors.add("Se requiere al menos una línea de detalle (requerimiento UBL)"); + } + } + + private DespatchAdviceValidator() { + // Utility class + } +} diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Envio.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Envio.java index 5dcf868b..b34c6117 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Envio.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Envio.java @@ -11,6 +11,11 @@ import java.time.LocalDate; import java.util.List; +/** + * Modelo de envío/shipment de la Guía de Remisión Electrónica. + *

+ * Fuente: Anexo N.° 14 UBL 2.1, RS 000123-2022/SUNAT, RS 000240-2024/SUNAT. + */ @Data @Builder @NoArgsConstructor @@ -29,13 +34,17 @@ public class Envio { private String pesoTotalUnidadMedida; /** - * Peso de los ítems seleccionados (en KGM) + * Peso de los ítems seleccionados (en KGM). + * Requerido cuando el peso bruto total difiere del peso de los ítems + * (traslado parcial de DAM con carga a granel, FAQ #30 SUNAT). */ @Schema(description = "Peso bruto de los items seleccionados") private BigDecimal pesoItems; /** - * Sustento de la diferencia del peso bruto total respecto al peso de los ítems + * Sustento de la diferencia del peso bruto total respecto al peso de los ítems. + * Requerido cuando {@code pesoItems} difiere de {@code pesoTotal}. + * Ejemplo: "Retiro parcial de carga a granel", "Carga fraccionada" (FAQ #33). */ @Schema(description = "Sustento de diferencia de peso") private String sustentoPeso; @@ -49,20 +58,25 @@ public class Envio { private LocalDate fechaTraslado; /** - * Lista de contenedores/precintos + * Lista de contenedores con número y precinto. + *

+ * Cada contenedor puede incluir número de identificación y precinto. + * Aplica especialmente para comercio exterior (RS 000240-2024/SUNAT). */ @Singular("contenedor") - @Schema(description = "Lista de contenedores o precintos") - private List contenedores; + @Schema(description = "Lista de contenedores con número y precinto") + private List contenedores; /** - * Puerto de embarque/desembarque + * Puerto de embarque/desembarque. + * Catálogo 63 de SUNAT. */ @Schema(description = "Puerto de embarque/desembarque (Catalogo 63)") private Puerto puerto; /** - * Aeropuerto de embarque/desembarque + * Aeropuerto de embarque/desembarque. + * Catálogo 64 de SUNAT. */ @Schema(description = "Aeropuerto de embarque/desembarque (Catalogo 64)") private Puerto aeropuerto; @@ -71,20 +85,46 @@ public class Envio { private Transportista transportista; /** - * Lista de conductores (principal y secundarios) + * Lista de conductores (principal y secundarios). + *

+ * GRE-Remitente con transporte privado: requerido (al menos conductor + * principal). + * GRE-Remitente con transporte público: no aplica (lo provee el transportista). + * GRE-Transportista: requerido siempre. */ @Singular("chofer") @Schema(description = "Lista de conductores") private List choferes; /** - * Vehículo principal con posibles vehículos secundarios + * Vehículo principal con posibles vehículos secundarios. + *

+ * GRE-Remitente con transporte privado: requerido. + * GRE-Remitente con transporte público: no aplica. + * GRE-Transportista: requerido siempre. */ @Schema(description = "Vehículo de transporte") private Vehicle vehiculo; /** - * Indicadores especiales de transporte (SUNAT_Envio_*) + * Indicadores especiales de transporte. + *

+ * Se mapean a {@code cbc:SpecialInstructions} en el XML. + * Ver + * {@link io.github.project.openubl.xbuilder.content.catalogs.IndicadorEnvio} + * para la lista de valores tipados. Se acepta String libre para extensibilidad. + *

+ * Indicadores comunes: + *

    + *
  • SUNAT_Envio_IndicadorTrasladoTotalDAMDS: traslado total de DAM/DS
  • + *
  • SUNAT_Envio_IndicadorBienNormalizado: bien sujeto a SPOT/IVAP
  • + *
  • SUNAT_Envio_IndicadorTrasladoVehiculoM1L: vehículos categoría M1/L (exime + * conductor/vehículo)
  • + *
  • SUNAT_Envio_IndicadorTransbordoProgramado: transbordo programado por + * eventos
  • + *
  • SUNAT_Envio_IndicadorRetornoVehiculoEnvasesVacios
  • + *
  • SUNAT_Envio_IndicadorRetornoVehiculoVacio
  • + *
*/ @Singular("indicador") @Schema(description = "Indicadores especiales de transporte") @@ -95,4 +135,32 @@ public class Envio { @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED) private Destino destino; + + // == Campos de comercio exterior (RS 000240-2024/SUNAT) == + + /** + * Número de manifiesto de carga. + *

+ * Relevante para motivos de traslado de comercio exterior (08, 09, 10, 19). + * Se consigna como documento relacionado (Catálogo 21, código 04). + * + * @since 2.0 + */ + @Schema(description = "Número de manifiesto de carga") + private String numeroManifiesto; + + /** + * Referencias a Declaraciones Aduaneras de Mercancías (DAM) o + * Declaraciones Simplificadas (DS). + *

+ * Requerido para motivos 08 (Importación), 09 (Exportación), + * 10 (Importación con DAM) y 19 (Mercancía extranjera). + *

+ * Se mapean a {@code cac:AdditionalDocumentReference} con Catálogo 61. + * + * @since 2.0 + */ + @Singular("declaracionAduanera") + @Schema(description = "Declaraciones aduaneras (DAM/DS)") + private List declaracionesAduaneras; } diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GRERemitente.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GRERemitente.java new file mode 100644 index 00000000..e54118f5 --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GRERemitente.java @@ -0,0 +1,239 @@ +package io.github.project.openubl.xbuilder.content.models.standard.guia; + +import io.github.project.openubl.xbuilder.content.models.common.Firmante; +import lombok.Builder; +import lombok.Data; +import lombok.Singular; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; + +/** + * Modelo para Guía de Remisión Electrónica - Remitente (tipo 09). + *

+ * Emitida por el remitente de los bienes. Serie TXXX. + *

+ * Reglas según RS 000123-2022/SUNAT: + *

    + *
  • Serie debe iniciar con 'T'
  • + *
  • Transporte privado (02): requiere conductor y vehículo
  • + *
  • Transporte público (01): requiere datos del transportista
  • + *
  • El remitente es quien envía los bienes (DespatchSupplierParty)
  • + *
+ *

+ * Uso: + * + *

{@code
+ * GRERemitente gre = GRERemitente.builder()
+ *         .serie("T001").numero(1)
+ *         .remitente(Remitente.builder().ruc("20100010001").razonSocial("Mi Empresa").build())
+ *         .destinatario(Destinatario.builder()
+ *                 .tipoDocumentoIdentidad("6").numeroDocumentoIdentidad("20200020002").nombre("Cliente").build())
+ *         .envio(Envio.builder()
+ *                 .tipoTraslado("01").pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM")
+ *                 .tipoModalidadTraslado("02").fechaTraslado(LocalDate.now())
+ *                 .chofer(Driver.builder().tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("12345678")
+ *                         .nombres("Juan").apellidos("Perez").licencia("Q123").build())
+ *                 .vehiculo(Vehicle.builder().placa("ABC-123").build())
+ *                 .partida(Partida.builder().ubigeo("150101").direccion("Origen").build())
+ *                 .destino(Destino.builder().ubigeo("150102").direccion("Destino").build())
+ *                 .build())
+ *         .detalle(DespatchAdviceItem.builder().cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build())
+ *         .build();
+ *
+ * DespatchAdvice da = gre.toDespatchAdvice();
+ * }
+ * + * @see GRETransportista + * @see DespatchAdvice + */ +@Data +@Builder +public class GRERemitente { + + // == Datos del comprobante == + + private String serie; + private Integer numero; + private String version; + private LocalDate fechaEmision; + private LocalTime horaEmision; + private String observaciones; + + // == Partes == + + private Remitente remitente; + private Destinatario destinatario; + private Comprador comprador; + private Firmante firmante; + + // == Documentos relacionados == + + private DocumentoBaja documentoBaja; + private DocumentoRelacionado documentoRelacionado; + + @Singular("documentoRelacionadoAdicional") + private List documentosRelacionados; + + @Singular("documentoAdicional") + private List documentosAdicionales; + + // == Datos de envío == + + private Envio envio; + + // == Detalle de bienes == + + @Singular + private List detalles; + + /** + * Valida las reglas de negocio para GRE-Remitente y retorna los errores. + * + * @return lista de errores (vacía si es válido) + */ + public List validate() { + List errors = new ArrayList<>(); + + // Serie + if (serie == null || serie.isBlank()) { + errors.add("La serie es requerida"); + } else if (!serie.toUpperCase().startsWith("T")) { + errors.add("GRE-Remitente requiere serie que inicie con 'T'. Serie actual: " + serie); + } + + // Número + if (numero == null || numero < 1) { + errors.add("El número debe ser mayor a 0"); + } + + // Remitente + if (remitente == null) { + errors.add("El remitente es requerido"); + } else if (remitente.getRuc() == null || remitente.getRuc().length() != 11) { + errors.add("El RUC del remitente debe tener 11 dígitos"); + } + + // Destinatario + if (destinatario == null) { + errors.add("El destinatario es requerido"); + } + + // Envío + if (envio == null) { + errors.add("Los datos de envío son requeridos"); + } else { + validateEnvio(envio, errors); + } + + // Detalles + if (detalles == null || detalles.isEmpty()) { + errors.add("Se requiere al menos una línea de detalle"); + } + + return errors; + } + + private void validateEnvio(Envio envio, List errors) { + if (envio.getTipoTraslado() == null || envio.getTipoTraslado().isBlank()) { + errors.add("El motivo de traslado (Catálogo 20) es requerido"); + } + if (envio.getPesoTotal() == null) { + errors.add("El peso total es requerido"); + } + if (envio.getTipoModalidadTraslado() == null) { + errors.add("La modalidad de traslado (Catálogo 18) es requerida"); + } + if (envio.getFechaTraslado() == null) { + errors.add("La fecha de traslado es requerida"); + } + if (envio.getPartida() == null) { + errors.add("El punto de partida es requerido"); + } + if (envio.getDestino() == null) { + errors.add("El punto de destino es requerido"); + } + + String modalidad = envio.getTipoModalidadTraslado(); + if (modalidad == null) + return; + + boolean tieneIndicadorM1L = envio.getIndicadores() != null && + envio.getIndicadores().contains("SUNAT_Envio_IndicadorTrasladoVehiculoM1L"); + + if ("02".equals(modalidad)) { + // Transporte privado: requiere conductor y vehículo + if (!tieneIndicadorM1L) { + if (envio.getChoferes() == null || envio.getChoferes().isEmpty()) { + errors.add("Transporte privado requiere al menos un conductor (salvo vehículo categoría M1/L)"); + } + if (envio.getVehiculo() == null) { + errors.add("Transporte privado requiere datos del vehículo (salvo vehículo categoría M1/L)"); + } + } + // Transporte privado NO debe tener transportista externo + if (envio.getTransportista() != null) { + errors.add( + "Transporte privado no debe consignar transportista externo (usar modalidad pública si subcontrata)"); + } + } else if ("01".equals(modalidad)) { + // Transporte público: requiere transportista + if (envio.getTransportista() == null) { + errors.add("Transporte público requiere datos del transportista"); + } + } + } + + /** + * Convierte este modelo a {@link DespatchAdvice} para renderizado XML. + * El tipo de comprobante se fija a "09" (GRE-Remitente). + * + * @return DespatchAdvice listo para enriquecer y renderizar + */ + public DespatchAdvice toDespatchAdvice() { + DespatchAdvice.DespatchAdviceBuilder builder = DespatchAdvice.builder() + .serie(serie) + .numero(numero) + .version(version) + .fechaEmision(fechaEmision) + .horaEmision(horaEmision) + .tipoComprobante("09") + .observaciones(observaciones) + .remitente(remitente) + .destinatario(destinatario) + .comprador(comprador) + .firmante(firmante) + .documentoBaja(documentoBaja) + .documentoRelacionado(documentoRelacionado) + .envio(envio); + + if (documentosRelacionados != null) { + documentosRelacionados.forEach(builder::documentoRelacionadoAdicional); + } + if (documentosAdicionales != null) { + documentosAdicionales.forEach(builder::documentoAdicional); + } + if (detalles != null) { + detalles.forEach(builder::detalle); + } + + return builder.build(); + } + + /** + * Valida y convierte a DespatchAdvice. + * + * @return DespatchAdvice validado + * @throws IllegalStateException si hay errores de validación + */ + public DespatchAdvice toDespatchAdviceValidated() { + List errors = validate(); + if (!errors.isEmpty()) { + throw new IllegalStateException( + "GRE-Remitente inválido:\n- " + String.join("\n- ", errors)); + } + return toDespatchAdvice(); + } +} diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GRETransportista.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GRETransportista.java new file mode 100644 index 00000000..4ddf0cd3 --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GRETransportista.java @@ -0,0 +1,302 @@ +package io.github.project.openubl.xbuilder.content.models.standard.guia; + +import io.github.project.openubl.xbuilder.content.models.common.Firmante; +import lombok.Builder; +import lombok.Data; +import lombok.Singular; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; + +/** + * Modelo para Guía de Remisión Electrónica - Transportista (tipo 31). + *

+ * Emitida por el transportista que presta el servicio. Serie VXXX. + *

+ * Reglas según RS 000123-2022/SUNAT: + *

    + *
  • Serie debe iniciar con 'V'
  • + *
  • Siempre requiere al menos un conductor
  • + *
  • Siempre requiere vehículo
  • + *
  • El remitente/emisor del XML es el transportista + * (DespatchSupplierParty)
  • + *
  • El tercero (SellerSupplierParty) es el remitente original de los + * bienes
  • + *
+ *

+ * Uso: + * + *

{@code
+ * GRETransportista gre = GRETransportista.builder()
+ *         .serie("V001").numero(1)
+ *         .transportistaEmisor(Transportista.builder()
+ *                 .tipoDocumentoIdentidad("6").numeroDocumentoIdentidad("20300030003")
+ *                 .nombre("Transportes S.A.C.").numeroRegistroMTC("MTC-123").build())
+ *         .remitente(Tercero.builder()
+ *                 .tipoDocumentoIdentidad("6").numeroDocumentoIdentidad("20100010001")
+ *                 .nombre("Empresa Remitente S.A.C.").build())
+ *         .destinatario(Destinatario.builder()
+ *                 .tipoDocumentoIdentidad("6").numeroDocumentoIdentidad("20200020002").nombre("Cliente").build())
+ *         .conductor(Driver.builder()
+ *                 .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("12345678")
+ *                 .nombres("Juan").apellidos("Perez").licencia("Q123").build())
+ *         .vehiculo(Vehicle.builder().placa("ABC-123").build())
+ *         .envio(Envio.builder()
+ *                 .tipoTraslado("01").pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM")
+ *                 .tipoModalidadTraslado("01").fechaTraslado(LocalDate.now())
+ *                 .partida(Partida.builder().ubigeo("150101").direccion("Origen").build())
+ *                 .destino(Destino.builder().ubigeo("150102").direccion("Destino").build())
+ *                 .build())
+ *         .detalle(DespatchAdviceItem.builder().cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build())
+ *         .build();
+ *
+ * DespatchAdvice da = gre.toDespatchAdvice();
+ * }
+ * + * @see GRERemitente + * @see DespatchAdvice + */ +@Data +@Builder +public class GRETransportista { + + // == Datos del comprobante == + + private String serie; + private Integer numero; + private String version; + private LocalDate fechaEmision; + private LocalTime horaEmision; + private String observaciones; + + // == Partes == + + /** + * El transportista que emite la guía. + * Se convierte a {@link Remitente} para mapear a DespatchSupplierParty en el + * XML. + *

+ * Se usa {@link Transportista} en lugar de {@link Remitente} porque + * semánticamente + * SUNAT distingue remitente y transportista como sujetos diferentes. + */ + private Transportista transportistaEmisor; + + /** + * El remitente original de los bienes (quien contrata el transporte). + * Se mapea a SellerSupplierParty (tercero) en el XML. + */ + private Tercero remitente; + + /** + * El destinatario de los bienes. + */ + private Destinatario destinatario; + + private Comprador comprador; + private Firmante firmante; + + // == Conductor(es) y vehículo - siempre requeridos para transportista == + + /** + * Conductor(es) del transporte. Al menos uno es obligatorio. + * El primero es el conductor principal. + */ + @Singular("conductor") + private List conductores; + + /** + * Vehículo principal del transporte. Obligatorio. + */ + private Vehicle vehiculo; + + // == Documentos relacionados == + + private DocumentoBaja documentoBaja; + + @Singular("documentoRelacionadoAdicional") + private List documentosRelacionados; + + @Singular("documentoAdicional") + private List documentosAdicionales; + + // == Datos de envío == + + /** + * Datos de envío. El conductor y vehículo se inyectan automáticamente + * desde los campos {@code conductores} y {@code vehiculo} de este modelo. + */ + private Envio envio; + + // == Detalle de bienes == + + @Singular + private List detalles; + + /** + * Valida las reglas de negocio para GRE-Transportista y retorna los errores. + * + * @return lista de errores (vacía si es válido) + */ + public List validate() { + List errors = new ArrayList<>(); + + // Serie + if (serie == null || serie.isBlank()) { + errors.add("La serie es requerida"); + } else if (!serie.toUpperCase().startsWith("V")) { + errors.add("GRE-Transportista requiere serie que inicie con 'V'. Serie actual: " + serie); + } + + // Número + if (numero == null || numero < 1) { + errors.add("El número debe ser mayor a 0"); + } + + // Transportista emisor + if (transportistaEmisor == null) { + errors.add("El transportista emisor es requerido"); + } else { + if (transportistaEmisor.getNumeroDocumentoIdentidad() == null + || transportistaEmisor.getNumeroDocumentoIdentidad().length() != 11) { + errors.add("El RUC del transportista emisor debe tener 11 dígitos"); + } + } + + // Remitente original (tercero) + if (remitente == null) { + errors.add("El remitente original (tercero) es requerido para GRE-Transportista"); + } + + // Destinatario + if (destinatario == null) { + errors.add("El destinatario es requerido"); + } + + // Conductor - siempre obligatorio + if (conductores == null || conductores.isEmpty()) { + errors.add("GRE-Transportista requiere al menos un conductor"); + } + + // Vehículo - siempre obligatorio + if (vehiculo == null) { + errors.add("GRE-Transportista requiere datos del vehículo"); + } + + // Envío + if (envio == null) { + errors.add("Los datos de envío son requeridos"); + } else { + validateEnvio(envio, errors); + } + + // Detalles + if (detalles == null || detalles.isEmpty()) { + errors.add("Se requiere al menos una línea de detalle"); + } + + return errors; + } + + private void validateEnvio(Envio envio, List errors) { + if (envio.getTipoTraslado() == null || envio.getTipoTraslado().isBlank()) { + errors.add("El motivo de traslado (Catálogo 20) es requerido"); + } + if (envio.getPesoTotal() == null) { + errors.add("El peso total es requerido"); + } + if (envio.getTipoModalidadTraslado() == null) { + errors.add("La modalidad de traslado (Catálogo 18) es requerida"); + } + if (envio.getFechaTraslado() == null) { + errors.add("La fecha de traslado es requerida"); + } + if (envio.getPartida() == null) { + errors.add("El punto de partida es requerido"); + } + if (envio.getDestino() == null) { + errors.add("El punto de destino es requerido"); + } + } + + /** + * Convierte este modelo a {@link DespatchAdvice} para renderizado XML. + * El tipo de comprobante se fija a "31" (GRE-Transportista). + * Los conductores y vehículo se inyectan en el envío automáticamente. + * + * @return DespatchAdvice listo para enriquecer y renderizar + */ + public DespatchAdvice toDespatchAdvice() { + // Build envio with conductores and vehiculo injected + Envio envioConTransporte = envio != null ? Envio.builder() + .tipoTraslado(envio.getTipoTraslado()) + .motivoTraslado(envio.getMotivoTraslado()) + .pesoTotal(envio.getPesoTotal()) + .pesoTotalUnidadMedida(envio.getPesoTotalUnidadMedida()) + .pesoItems(envio.getPesoItems()) + .sustentoPeso(envio.getSustentoPeso()) + .numeroDeBultos(envio.getNumeroDeBultos()) + .tipoModalidadTraslado(envio.getTipoModalidadTraslado()) + .fechaTraslado(envio.getFechaTraslado()) + .contenedores(envio.getContenedores()) + .puerto(envio.getPuerto()) + .aeropuerto(envio.getAeropuerto()) + .choferes(conductores) + .vehiculo(vehiculo) + .indicadores(envio.getIndicadores()) + .partida(envio.getPartida()) + .destino(envio.getDestino()) + .numeroManifiesto(envio.getNumeroManifiesto()) + .declaracionesAduaneras(envio.getDeclaracionesAduaneras()) + .build() : null; + + DespatchAdvice.DespatchAdviceBuilder builder = DespatchAdvice.builder() + .serie(serie) + .numero(numero) + .version(version) + .fechaEmision(fechaEmision) + .horaEmision(horaEmision) + .tipoComprobante("31") + .observaciones(observaciones) + .remitente(transportistaEmisor != null ? Remitente.builder() + .ruc(transportistaEmisor.getNumeroDocumentoIdentidad()) + .razonSocial(transportistaEmisor.getNombre()) + .numeroRegistroMTC(transportistaEmisor.getNumeroRegistroMTC()) + .build() : null) + .destinatario(destinatario) + .comprador(comprador) + .tercero(remitente) + .firmante(firmante) + .documentoBaja(documentoBaja) + .envio(envioConTransporte); + + if (documentosRelacionados != null) { + documentosRelacionados.forEach(builder::documentoRelacionadoAdicional); + } + if (documentosAdicionales != null) { + documentosAdicionales.forEach(builder::documentoAdicional); + } + if (detalles != null) { + detalles.forEach(builder::detalle); + } + + return builder.build(); + } + + /** + * Valida y convierte a DespatchAdvice. + * + * @return DespatchAdvice validado + * @throws IllegalStateException si hay errores de validación + */ + public DespatchAdvice toDespatchAdviceValidated() { + List errors = validate(); + if (!errors.isEmpty()) { + throw new IllegalStateException( + "GRE-Transportista inválido:\n- " + String.join("\n- ", errors)); + } + return toDespatchAdvice(); + } +} diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/resumen/ComprobanteImpuestos.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/resumen/ComprobanteImpuestos.java index 49772424..2620eda6 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/resumen/ComprobanteImpuestos.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/resumen/ComprobanteImpuestos.java @@ -17,6 +17,9 @@ public class ComprobanteImpuestos { @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "IGV del comprobante") private BigDecimal igv; + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Tasa de IGV del comprobante. Ejemplo: 0.18") + private BigDecimal tasaIgv; + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "ICB del comprobante") private BigDecimal icb; diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/enricher/ContentEnricher.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/enricher/ContentEnricher.java index 35979d7f..93a1557d 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/enricher/ContentEnricher.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/enricher/ContentEnricher.java @@ -22,159 +22,174 @@ public class ContentEnricher { - private final Defaults defaults; - private final DateProvider dateProvider; - - public ContentEnricher(Defaults defaults, DateProvider dateProvider) { - this.defaults = defaults; - this.dateProvider = dateProvider; - } - - public void enrich(Invoice input) { - Stream - .of(RulePhase.PhaseType.ENRICH, RulePhase.PhaseType.PROCESS, RulePhase.PhaseType.SUMMARY) - .forEach(phaseType -> { - // Header - HeaderRuleContext ruleContextHeader = HeaderRuleContext.builder() - .localDate(dateProvider.now()) - .build(); - RuleUnit ruleUnitHeader = new HeaderRuleUnit(phaseType, defaults, ruleContextHeader); - ruleUnitHeader.modify(input); - - // Body - BodyRuleContext ruleContextBody = BodyRuleContext.builder() - .moneda(input.getMoneda()) - .tasaIgv(input.getTasaIgv()) - .tasaIvap(input.getTasaIvap()) - .tasaIcb(input.getTasaIcb()) - .build(); - RuleUnit ruleUnitBody = new BodyRuleUnit(phaseType, defaults, ruleContextBody); - - input.getDetalles().forEach(ruleUnitBody::modify); - input.getAnticipos().forEach(ruleUnitBody::modify); - input.getDescuentos().forEach(ruleUnitBody::modify); - }); - } - - public void enrich(CreditNote input) { - enrichNote(input); - } - - public void enrich(DebitNote input) { - enrichNote(input); - } - - private void enrichNote(Note input) { - Stream - .of(RulePhase.PhaseType.ENRICH, RulePhase.PhaseType.PROCESS, RulePhase.PhaseType.SUMMARY) - .forEach(phaseType -> { - // Header - HeaderRuleContext ruleContextHeader = HeaderRuleContext.builder() - .localDate(dateProvider.now()) - .build(); - RuleUnit ruleUnitHeader = new HeaderRuleUnit(phaseType, defaults, ruleContextHeader); - ruleUnitHeader.modify(input); - - // Body - BodyRuleContext ruleContextBody = BodyRuleContext.builder() - .moneda(input.getMoneda()) - .tasaIgv(input.getTasaIgv()) - .tasaIcb(input.getTasaIcb()) - .build(); - RuleUnit ruleUnitBody = new BodyRuleUnit(phaseType, defaults, ruleContextBody); - input.getDetalles().forEach(ruleUnitBody::modify); - }); - } - - public void enrich(VoidedDocuments input) { - Stream - .of(RulePhase.PhaseType.ENRICH, RulePhase.PhaseType.PROCESS, RulePhase.PhaseType.SUMMARY) - .forEach(phaseType -> { - // Header - HeaderRuleContext ruleContextHeader = HeaderRuleContext.builder() - .localDate(dateProvider.now()) - .build(); - RuleUnit ruleUnitHeader = new HeaderRuleUnit(phaseType, defaults, ruleContextHeader); - ruleUnitHeader.modify(input); - - // Body - BodyRuleContext ruleContextBody = BodyRuleContext.builder() - .moneda(input.getMoneda()) - .build(); - - RuleUnit ruleUnitBody = new BodyRuleUnit(phaseType, defaults, ruleContextBody); - input.getComprobantes().forEach(ruleUnitBody::modify); - }); - } - - public void enrich(SummaryDocuments input) { - Stream - .of(RulePhase.PhaseType.ENRICH, RulePhase.PhaseType.PROCESS, RulePhase.PhaseType.SUMMARY) - .forEach(phaseType -> { - // Header - HeaderRuleContext ruleContextHeader = HeaderRuleContext.builder() - .localDate(dateProvider.now()) - .build(); - RuleUnit ruleUnitHeader = new HeaderRuleUnit(phaseType, defaults, ruleContextHeader); - ruleUnitHeader.modify(input); - - // Body - BodyRuleContext ruleContextBody = BodyRuleContext.builder() - .moneda(input.getMoneda()) - .build(); - - RuleUnit ruleUnitBody = new BodyRuleUnit(phaseType, defaults, ruleContextBody); - input.getComprobantes().forEach(ruleUnitBody::modify); - }); - } - - public void enrich(Perception input) { - Stream - .of(RulePhase.PhaseType.ENRICH, RulePhase.PhaseType.PROCESS, RulePhase.PhaseType.SUMMARY) - .forEach(phaseType -> { - // Header - HeaderRuleContext ruleContextHeader = HeaderRuleContext.builder() - .localDate(dateProvider.now()) - .build(); - RuleUnit ruleUnitHeader = new HeaderRuleUnit(phaseType, defaults, ruleContextHeader); - ruleUnitHeader.modify(input); - - // Body - }); - } - - public void enrich(Retention input) { - Stream - .of(RulePhase.PhaseType.ENRICH, RulePhase.PhaseType.PROCESS, RulePhase.PhaseType.SUMMARY) - .forEach(phaseType -> { - // Header - HeaderRuleContext ruleContextHeader = HeaderRuleContext.builder() - .localDate(dateProvider.now()) - .build(); - RuleUnit ruleUnitHeader = new HeaderRuleUnit(phaseType, defaults, ruleContextHeader); - ruleUnitHeader.modify(input); - - // Body - }); - } - - public void enrich(DespatchAdvice input) { - Stream - .of(RulePhase.PhaseType.ENRICH, RulePhase.PhaseType.PROCESS, RulePhase.PhaseType.SUMMARY) - .forEach(phaseType -> { - // Header - HeaderRuleContext ruleContextHeader = HeaderRuleContext.builder() - .localDate(dateProvider.now()) - .build(); - RuleUnit ruleUnitHeader = new HeaderRuleUnit(phaseType, defaults, ruleContextHeader); - ruleUnitHeader.modify(input); - - // Body - BodyRuleContext ruleContextBody = BodyRuleContext.builder() - .build(); - RuleUnit ruleUnitBody = new BodyRuleUnit(phaseType, defaults, ruleContextBody); - input.getDetalles().forEach(ruleUnitBody::modify); - }); - } + private final Defaults defaults; + private final DateProvider dateProvider; + + public ContentEnricher(Defaults defaults, DateProvider dateProvider) { + this.defaults = defaults; + this.dateProvider = dateProvider; + } + + public void enrich(Invoice input) { + Stream + .of(RulePhase.PhaseType.ENRICH, RulePhase.PhaseType.PROCESS, + RulePhase.PhaseType.SUMMARY) + .forEach(phaseType -> { + // Header + HeaderRuleContext ruleContextHeader = HeaderRuleContext.builder() + .localDate(dateProvider.now()) + .build(); + RuleUnit ruleUnitHeader = new HeaderRuleUnit(phaseType, defaults, + ruleContextHeader); + ruleUnitHeader.modify(input); + + // Body + BodyRuleContext ruleContextBody = BodyRuleContext.builder() + .moneda(input.getMoneda()) + .tasaIgv(input.getTasaIgv()) + .tasaIvap(input.getTasaIvap()) + .tasaIcb(input.getTasaIcb()) + .build(); + RuleUnit ruleUnitBody = new BodyRuleUnit(phaseType, defaults, ruleContextBody); + + input.getDetalles().forEach(ruleUnitBody::modify); + input.getAnticipos().forEach(ruleUnitBody::modify); + input.getDescuentos().forEach(ruleUnitBody::modify); + }); + } + + public void enrich(CreditNote input) { + enrichNote(input); + } + + public void enrich(DebitNote input) { + enrichNote(input); + } + + private void enrichNote(Note input) { + Stream + .of(RulePhase.PhaseType.ENRICH, RulePhase.PhaseType.PROCESS, + RulePhase.PhaseType.SUMMARY) + .forEach(phaseType -> { + // Header + HeaderRuleContext ruleContextHeader = HeaderRuleContext.builder() + .localDate(dateProvider.now()) + .build(); + RuleUnit ruleUnitHeader = new HeaderRuleUnit(phaseType, defaults, + ruleContextHeader); + ruleUnitHeader.modify(input); + + // Body + BodyRuleContext ruleContextBody = BodyRuleContext.builder() + .moneda(input.getMoneda()) + .tasaIgv(input.getTasaIgv()) + .tasaIcb(input.getTasaIcb()) + .build(); + RuleUnit ruleUnitBody = new BodyRuleUnit(phaseType, defaults, ruleContextBody); + input.getDetalles().forEach(ruleUnitBody::modify); + }); + } + + public void enrich(VoidedDocuments input) { + Stream + .of(RulePhase.PhaseType.ENRICH, RulePhase.PhaseType.PROCESS, + RulePhase.PhaseType.SUMMARY) + .forEach(phaseType -> { + // Header + HeaderRuleContext ruleContextHeader = HeaderRuleContext.builder() + .localDate(dateProvider.now()) + .build(); + RuleUnit ruleUnitHeader = new HeaderRuleUnit(phaseType, defaults, + ruleContextHeader); + ruleUnitHeader.modify(input); + + // Body + BodyRuleContext ruleContextBody = BodyRuleContext.builder() + .moneda(input.getMoneda()) + .build(); + + RuleUnit ruleUnitBody = new BodyRuleUnit(phaseType, defaults, ruleContextBody); + input.getComprobantes().forEach(ruleUnitBody::modify); + }); + } + + public void enrich(SummaryDocuments input) { + Stream + .of(RulePhase.PhaseType.ENRICH, RulePhase.PhaseType.PROCESS, + RulePhase.PhaseType.SUMMARY) + .forEach(phaseType -> { + // Header + HeaderRuleContext ruleContextHeader = HeaderRuleContext.builder() + .localDate(dateProvider.now()) + .build(); + RuleUnit ruleUnitHeader = new HeaderRuleUnit(phaseType, defaults, + ruleContextHeader); + ruleUnitHeader.modify(input); + + // Body + BodyRuleContext ruleContextBody = BodyRuleContext.builder() + .moneda(input.getMoneda()) + .tasaIgv(defaults.getIgvTasa()) + .build(); + + RuleUnit ruleUnitBody = new BodyRuleUnit(phaseType, defaults, ruleContextBody); + input.getComprobantes().forEach(ruleUnitBody::modify); + }); + } + + public void enrich(Perception input) { + Stream + .of(RulePhase.PhaseType.ENRICH, RulePhase.PhaseType.PROCESS, + RulePhase.PhaseType.SUMMARY) + .forEach(phaseType -> { + // Header + HeaderRuleContext ruleContextHeader = HeaderRuleContext.builder() + .localDate(dateProvider.now()) + .build(); + RuleUnit ruleUnitHeader = new HeaderRuleUnit(phaseType, defaults, + ruleContextHeader); + ruleUnitHeader.modify(input); + + // Body + }); + } + + public void enrich(Retention input) { + Stream + .of(RulePhase.PhaseType.ENRICH, RulePhase.PhaseType.PROCESS, + RulePhase.PhaseType.SUMMARY) + .forEach(phaseType -> { + // Header + HeaderRuleContext ruleContextHeader = HeaderRuleContext.builder() + .localDate(dateProvider.now()) + .build(); + RuleUnit ruleUnitHeader = new HeaderRuleUnit(phaseType, defaults, + ruleContextHeader); + ruleUnitHeader.modify(input); + + // Body + }); + } + + public void enrich(DespatchAdvice input) { + Stream + .of(RulePhase.PhaseType.ENRICH, RulePhase.PhaseType.PROCESS, + RulePhase.PhaseType.SUMMARY) + .forEach(phaseType -> { + // Header + HeaderRuleContext ruleContextHeader = HeaderRuleContext.builder() + .localDate(dateProvider.now()) + .build(); + RuleUnit ruleUnitHeader = new HeaderRuleUnit(phaseType, defaults, + ruleContextHeader); + ruleUnitHeader.modify(input); + + // Body + BodyRuleContext ruleContextBody = BodyRuleContext.builder() + .build(); + RuleUnit ruleUnitBody = new BodyRuleUnit(phaseType, defaults, ruleContextBody); + input.getDetalles().forEach(ruleUnitBody::modify); + }); + } } diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/enricher/kie/rules/enrich/body/summaryDocumentItem/TasaIgvRule.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/enricher/kie/rules/enrich/body/summaryDocumentItem/TasaIgvRule.java new file mode 100644 index 00000000..00a00247 --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/enricher/kie/rules/enrich/body/summaryDocumentItem/TasaIgvRule.java @@ -0,0 +1,31 @@ +package io.github.project.openubl.xbuilder.enricher.kie.rules.enrich.body.summaryDocumentItem; + +import io.github.project.openubl.xbuilder.content.models.sunat.resumen.SummaryDocumentsItem; +import io.github.project.openubl.xbuilder.enricher.kie.AbstractBodyRule; +import io.github.project.openubl.xbuilder.enricher.kie.RulePhase; + +import java.util.function.Consumer; + +import static io.github.project.openubl.xbuilder.enricher.kie.rules.utils.Helpers.isSummaryDocumentsItem; +import static io.github.project.openubl.xbuilder.enricher.kie.rules.utils.Helpers.whenSummaryDocumentsItem; + +@RulePhase(type = RulePhase.PhaseType.ENRICH) +public class TasaIgvRule extends AbstractBodyRule { + + @Override + public boolean test(Object object) { + return (isSummaryDocumentsItem.test(object) && whenSummaryDocumentsItem.apply(object) + .map(item -> item.getComprobante() != null && + item.getComprobante().getImpuestos() != null && + item.getComprobante().getImpuestos().getTasaIgv() == null) + .orElse(false)); + } + + @Override + public void modify(Object object) { + Consumer consumer = item -> { + item.getComprobante().getImpuestos().setTasaIgv(getRuleContext().getTasaIgv()); + }; + whenSummaryDocumentsItem.apply(object).ifPresent(consumer); + } +} diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/enricher/kie/rules/enrich/header/DespatchAdviceTipoComprobanteRule.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/enricher/kie/rules/enrich/header/DespatchAdviceTipoComprobanteRule.java new file mode 100644 index 00000000..e029feed --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/enricher/kie/rules/enrich/header/DespatchAdviceTipoComprobanteRule.java @@ -0,0 +1,44 @@ +package io.github.project.openubl.xbuilder.enricher.kie.rules.enrich.header; + +import io.github.project.openubl.xbuilder.content.models.standard.guia.DespatchAdvice; +import io.github.project.openubl.xbuilder.enricher.kie.AbstractHeaderRule; +import io.github.project.openubl.xbuilder.enricher.kie.RulePhase; + +import static io.github.project.openubl.xbuilder.enricher.kie.rules.utils.Helpers.isDespatchAdvice; +import static io.github.project.openubl.xbuilder.enricher.kie.rules.utils.Helpers.whenDespatchAdvice; + +/** + * Regla de enriquecimiento para autodetectar el tipo de comprobante de la GRE + * a partir del prefijo de la serie. + *

+ * Regla funcional SUNAT: + *

    + *
  • Serie T* → GRE-Remitente (tipo "09")
  • + *
  • Serie V* → GRE-Transportista (tipo "31")
  • + *
+ *

+ * Esta regla solo se aplica si {@code tipoComprobante} no fue establecido + * explícitamente por el usuario, permitiendo autocompletar. + */ +@RulePhase(type = RulePhase.PhaseType.ENRICH) +public class DespatchAdviceTipoComprobanteRule extends AbstractHeaderRule { + + @Override + public boolean test(Object object) { + return isDespatchAdvice.test(object) && whenDespatchAdvice.apply(object) + .map(da -> da.getTipoComprobante() == null && da.getSerie() != null) + .orElse(false); + } + + @Override + public void modify(Object object) { + whenDespatchAdvice.apply(object).ifPresent(da -> { + String serie = da.getSerie().toUpperCase(); + if (serie.startsWith("T")) { + da.setTipoComprobante("09"); + } else if (serie.startsWith("V")) { + da.setTipoComprobante("31"); + } + }); + } +} diff --git a/xbuilder/core/src/main/resources/META-INF/services/io.github.project.openubl.xbuilder.enricher.kie.RuleFactory b/xbuilder/core/src/main/resources/META-INF/services/io.github.project.openubl.xbuilder.enricher.kie.RuleFactory index 4932d66b..d18c7562 100644 --- a/xbuilder/core/src/main/resources/META-INF/services/io.github.project.openubl.xbuilder.enricher.kie.RuleFactory +++ b/xbuilder/core/src/main/resources/META-INF/services/io.github.project.openubl.xbuilder.enricher.kie.RuleFactory @@ -11,6 +11,9 @@ io.github.project.openubl.xbuilder.enricher.kie.rules.enrich.header.ProveedorDir ## Enrich - Invoice io.github.project.openubl.xbuilder.enricher.kie.rules.enrich.header.invoice.FormaDePagoRule + +## Enrich - DespatchAdvice +io.github.project.openubl.xbuilder.enricher.kie.rules.enrich.header.DespatchAdviceTipoComprobanteRule io.github.project.openubl.xbuilder.enricher.kie.rules.enrich.header.invoice.FormaDePagoTipoRule io.github.project.openubl.xbuilder.enricher.kie.rules.enrich.header.invoice.FormaDePagoTotalRule io.github.project.openubl.xbuilder.enricher.kie.rules.enrich.header.invoice.TipoComprobanteRule @@ -63,6 +66,7 @@ io.github.project.openubl.xbuilder.enricher.kie.rules.enrich.body.voidedDocument ## Enrich - SummaryDocuments io.github.project.openubl.xbuilder.enricher.kie.rules.enrich.body.summaryDocumentItem.TipoOperacionRule io.github.project.openubl.xbuilder.enricher.kie.rules.enrich.body.summaryDocumentItem.MonedaRule +io.github.project.openubl.xbuilder.enricher.kie.rules.enrich.body.summaryDocumentItem.TasaIgvRule ## Process - Detalle io.github.project.openubl.xbuilder.enricher.kie.rules.process.body.detalle.PrecioDeReferenciaRule diff --git a/xbuilder/core/src/main/resources/templates/Renderer/despatchAdvice.xml b/xbuilder/core/src/main/resources/templates/Renderer/despatchAdvice.xml index 03ae1b94..75612bb3 100644 --- a/xbuilder/core/src/main/resources/templates/Renderer/despatchAdvice.xml +++ b/xbuilder/core/src/main/resources/templates/Renderer/despatchAdvice.xml @@ -23,6 +23,12 @@ {documentoRelacionado.tipoDocumento} {/if} + {#each documentosRelacionados.orEmpty} + + {it.serieNumero} + {it.tipoDocumento} + + {/each} {#each documentosAdicionales.orEmpty} {it.numero} @@ -39,6 +45,19 @@ {/if} {/each} + {#each envio.declaracionesAduaneras.orEmpty} + + {it.numero} + {#if it.tipo == 'DS'}52{#else}50{/if} + {#if it.rucEmisor} + + + {it.rucEmisor} + + + {/if} + + {/each} {#include ubl/common/signature.xml firmante=this.firmante /} {remitente.ruc} @@ -183,8 +202,10 @@ {#each envio.contenedores.orEmpty} - {it_index.add(1)} - {it} + {it.numero} + {#if it.precinto} + {it.precinto} + {/if} {/each} diff --git a/xbuilder/core/src/main/resources/templates/Renderer/summaryDocuments.xml b/xbuilder/core/src/main/resources/templates/Renderer/summaryDocuments.xml index 979271a2..329fffb9 100644 --- a/xbuilder/core/src/main/resources/templates/Renderer/summaryDocuments.xml +++ b/xbuilder/core/src/main/resources/templates/Renderer/summaryDocuments.xml @@ -86,6 +86,7 @@ {it.comprobante.impuestos.igv} + {it.comprobante.impuestos.tasaIgv.multiplyByInt(100).scale(2)} 1000 IGV diff --git a/xbuilder/core/src/test/java/e2e/AbstractTest.java b/xbuilder/core/src/test/java/e2e/AbstractTest.java index 495dbf6f..b32003a8 100644 --- a/xbuilder/core/src/test/java/e2e/AbstractTest.java +++ b/xbuilder/core/src/test/java/e2e/AbstractTest.java @@ -27,6 +27,8 @@ import io.github.project.openubl.xbuilder.content.models.standard.general.DebitNote; import io.github.project.openubl.xbuilder.content.models.standard.general.Invoice; import io.github.project.openubl.xbuilder.content.models.standard.guia.DespatchAdvice; +import io.github.project.openubl.xbuilder.content.models.standard.guia.GRERemitente; +import io.github.project.openubl.xbuilder.content.models.standard.guia.GRETransportista; import io.github.project.openubl.xbuilder.content.models.sunat.baja.VoidedDocuments; import io.github.project.openubl.xbuilder.content.models.sunat.baja.Reversion; import io.github.project.openubl.xbuilder.content.models.sunat.percepcionretencion.Perception; @@ -57,7 +59,8 @@ public class AbstractTest { private static final CreditNoteMapper creditNoteMapper = Mappers.getMapper(CreditNoteMapper.class); private static final DebitNoteMapper debitNoteMapper = Mappers.getMapper(DebitNoteMapper.class); private static final VoidedDocumentsMapper voidedDocumentsMapper = Mappers.getMapper(VoidedDocumentsMapper.class); - private static final SummaryDocumentsMapper summaryDocumentsMapper = Mappers.getMapper(SummaryDocumentsMapper.class); + private static final SummaryDocumentsMapper summaryDocumentsMapper = Mappers + .getMapper(SummaryDocumentsMapper.class); private static final PerceptionMapper perceptionMapper = Mappers.getMapper(PerceptionMapper.class); private static final RetentionMapper retentionMapper = Mappers.getMapper(RetentionMapper.class); private static final DespatchAdviceMapper despatchAdviceMapper = Mappers.getMapper(DespatchAdviceMapper.class); @@ -82,7 +85,8 @@ public YAMLMapper getYamlMapper() { public void writeYaml(String kind, Object input, String snapshotFilename) throws URISyntaxException, IOException { String rootDir = getClass().getName().replaceAll("\\.", "/"); - String snapshotFileContent = Files.readString(Paths.get(getClass().getClassLoader().getResource(rootDir + "/" + snapshotFilename).toURI())); + String snapshotFileContent = Files.readString( + Paths.get(getClass().getClassLoader().getResource(rootDir + "/" + snapshotFilename).toURI())); Path directoryPath = Paths.get("../quarkus-extension/integration-tests/src/test/resources").resolve(rootDir); Files.createDirectories(directoryPath); @@ -91,8 +95,7 @@ public void writeYaml(String kind, Object input, String snapshotFilename) throws getYamlMapper().writeValue(filePath.toFile(), Map.of( "kind", kind, "input", input, - "snapshot", snapshotFileContent - )); + "snapshot", snapshotFileContent)); } protected void assertInput(Invoice input, String snapshotFilename) throws Exception { @@ -287,6 +290,14 @@ protected void assertInput(DespatchAdvice input, String snapshotFilename) throws writeYaml("DespatchAdvice", input, snapshotFilename); } + protected void assertInput(GRERemitente input, String snapshotFilename) throws Exception { + assertInput(input.toDespatchAdvice(), snapshotFilename); + } + + protected void assertInput(GRETransportista input, String snapshotFilename) throws Exception { + assertInput(input.toDespatchAdvice(), snapshotFilename); + } + protected void assertInputReversion(Reversion input, String snapshotFilename) throws Exception { ContentEnricher enricher = new ContentEnricher(defaults, dateProvider); enricher.enrich(input); diff --git a/xbuilder/core/src/test/java/e2e/renderer/XMLAssertUtils.java b/xbuilder/core/src/test/java/e2e/renderer/XMLAssertUtils.java index 626fbbfb..3c06ec4d 100644 --- a/xbuilder/core/src/test/java/e2e/renderer/XMLAssertUtils.java +++ b/xbuilder/core/src/test/java/e2e/renderer/XMLAssertUtils.java @@ -85,7 +85,8 @@ public class XMLAssertUtils { private static void assertSnapshot(String expected, Class clasz, String snapshotFile) throws SAXException { String rootDir = clasz.getName().replaceAll("\\.", "/"); - // Update snapshots and if updated do not verify since it doesn't make sense anymore + // Update snapshots and if updated do not verify since it doesn't make sense + // anymore boolean updateSnapshots = Boolean.parseBoolean(System.getProperty("xbuilder.snapshot.update", "false")); if (updateSnapshots) { try { @@ -117,20 +118,21 @@ private static void assertSnapshot(String expected, Class clasz, String snaps assertFalse(myDiff.hasDifferences(), expected + "\n" + myDiff); } - public static void assertSnapshot(String expected, String expectedReverse, Class clasz, String snapshotFile) throws SAXException { + public static void assertSnapshot(String expected, String expectedReverse, Class clasz, String snapshotFile) + throws SAXException { assertSnapshot(expected, clasz, snapshotFile); assertSnapshot(expectedReverse, clasz, snapshotFile); } - public static void assertSendSunat(String xmlWithoutSignature, String xsdSchema, String... allowedNotes) throws Exception { + public static void assertSendSunat(String xmlWithoutSignature, String xsdSchema, String... allowedNotes) + throws Exception { String skipSunat = System.getProperty("skipSunat", "false"); if (skipSunat != null && skipSunat.equals("false")) { Document signedXML = XMLSigner.signXML( xmlWithoutSignature, SIGN_REFERENCE_ID, CERTIFICATE.getX509Certificate(), - CERTIFICATE.getPrivateKey() - ); + CERTIFICATE.getPrivateKey()); isCompliantWithXsd(xsdSchema, signedXML); sendFileToSunat(signedXML, xmlWithoutSignature, allowedNotes); } @@ -153,7 +155,8 @@ private static void isCompliantWithXsd(String xsdSchema, Document signedXML) thr // - private static void sendFileToSunat(Document document, String xmlWithoutSignature, String... allowedNotes) throws Exception { + private static void sendFileToSunat(Document document, String xmlWithoutSignature, String... allowedNotes) + throws Exception { byte[] bytesFromDocument = XmlSignatureHelper.getBytesFromDocument(document); CamelContext camelContext = StandaloneCamel.getInstance().getMainCamel().getCamelContext(); @@ -176,8 +179,7 @@ private static void sendFileToSunat(Document document, String xmlWithoutSignatur Constants.XSENDER_BILL_SERVICE_URI, camelData.getBody(), camelData.getHeaders(), - SunatResponse.class - ); + SunatResponse.class); if (sendFileSunatResponse.getMetadata() != null && sendFileSunatResponse.getMetadata().getNotes() != null) { List allowedNotesList = Arrays.asList(allowedNotes); @@ -195,43 +197,52 @@ private static void sendFileToSunat(Document document, String xmlWithoutSignatur XmlContent xmlContent = fileAnalyzer.getXmlContent(); // Check ticket - if ( - !xmlContent.getDocumentType().equals(DocumentType.VOIDED_DOCUMENT) && - !xmlContent.getDocumentType().equals(DocumentType.SUMMARY_DOCUMENT) - ) { + if (!xmlContent.getDocumentType().equals(DocumentType.VOIDED_DOCUMENT) && + !xmlContent.getDocumentType().equals(DocumentType.SUMMARY_DOCUMENT)) { assertEquals( Status.ACEPTADO, sendFileSunatResponse.getStatus(), - xmlWithoutSignature + " \n sunat [codigo=" + sendFileSunatResponse.getMetadata().getResponseCode() + "], [descripcion=" + sendFileSunatResponse.getMetadata().getDescription() + "]" - ); + xmlWithoutSignature + " \n sunat [codigo=" + sendFileSunatResponse.getMetadata().getResponseCode() + + "], [descripcion=" + sendFileSunatResponse.getMetadata().getDescription() + "]"); } else { + assertNotNull(sendFileSunatResponse.getSunat(), + "SunatResponse.getSunat() is null. Status=" + sendFileSunatResponse.getStatus() + + ", Description=" + + (sendFileSunatResponse.getMetadata() != null + ? sendFileSunatResponse.getMetadata().getDescription() + : "null")); assertNotNull(sendFileSunatResponse.getSunat().getTicket()); CamelData camelTicketData = CamelUtils.getBillServiceCamelData( sendFileSunatResponse.getSunat().getTicket(), ticketDestination, - credentials - ); - - // TODO ticket get status are not working in SUNAT BETA so stopping it until it is supporeted -// SunatResponse verifyTicketSunatResponse = camelContext -// .createProducerTemplate() -// .requestBodyAndHeaders( -// Constants.XSENDER_BILL_SERVICE_URI, -// camelTicketData.getBody(), -// camelTicketData.getHeaders(), -// SunatResponse.class -// ); -// -// assertEquals( -// Status.ACEPTADO, -// verifyTicketSunatResponse.getStatus(), -// xmlWithoutSignature + " sunat [status=" + verifyTicketSunatResponse.getStatus() + "], [descripcion=" + verifyTicketSunatResponse.getMetadata().getDescription() + "]" -// ); -// assertNotNull( -// verifyTicketSunatResponse.getSunat().getCdr(), -// xmlWithoutSignature + " sunat [codigo=" + verifyTicketSunatResponse.getMetadata().getResponseCode() + "], [descripcion=" + verifyTicketSunatResponse.getMetadata().getDescription() + "]" -// ); + credentials); + + // TODO ticket get status are not working in SUNAT BETA so stopping it until it + // is supporeted + // SunatResponse verifyTicketSunatResponse = camelContext + // .createProducerTemplate() + // .requestBodyAndHeaders( + // Constants.XSENDER_BILL_SERVICE_URI, + // camelTicketData.getBody(), + // camelTicketData.getHeaders(), + // SunatResponse.class + // ); + // + // assertEquals( + // Status.ACEPTADO, + // verifyTicketSunatResponse.getStatus(), + // xmlWithoutSignature + " sunat [status=" + + // verifyTicketSunatResponse.getStatus() + "], [descripcion=" + + // verifyTicketSunatResponse.getMetadata().getDescription() + "]" + // ); + // assertNotNull( + // verifyTicketSunatResponse.getSunat().getCdr(), + // xmlWithoutSignature + " sunat [codigo=" + + // verifyTicketSunatResponse.getMetadata().getResponseCode() + "], + // [descripcion=" + verifyTicketSunatResponse.getMetadata().getDescription() + + // "]" + // ); } } } diff --git a/xbuilder/core/src/test/java/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest.java b/xbuilder/core/src/test/java/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest.java new file mode 100644 index 00000000..33632645 --- /dev/null +++ b/xbuilder/core/src/test/java/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest.java @@ -0,0 +1,230 @@ +package e2e.renderer.despatchadvice; + +import e2e.AbstractTest; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog1; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog18; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog20; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog6; +import io.github.project.openubl.xbuilder.content.models.standard.guia.*; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +/** + * Tests de GRE-Remitente para operaciones de comercio exterior. + *

+ * Escenarios cubiertos: + *

    + *
  • Importación con DAM y traslado total
  • + *
  • Exportación con DAM/DS
  • + *
  • Mercancía extranjera (motivo 19) desde puerto a depósito temporal
  • + *
  • Contenedores con precinto
  • + *
+ *

+ * Fuentes normativas: + * - RS 000240-2024/SUNAT (trazabilidad comercio exterior) + * - RS 000133-2025/SUNAT (prórroga al 01-jul-2026) + * - FAQ #24, #30, #31, #32, #33, #40 de SUNAT CPE + */ +public class DespatchAdviceComercioExteriorTest extends AbstractTest { + + /** + * GRE-Remitente: Importación con DAM y traslado TOTAL. + *

+ * Según FAQ #24: cuando es traslado total de la DAM/DS, + * no se requiere detalle de bienes, pero UBL exige al menos una línea vacía. + * Se marca el indicador SUNAT_Envio_IndicadorTrasladoTotalDAMDS. + */ + @Test + public void testImportacionConDAMTrasladoTotal() throws Exception { + DespatchAdvice input = DespatchAdvice.builder() + .serie("T001") + .numero(200) + .tipoComprobante(Catalog1.GUIA_REMISION_REMITENTE.getCode()) + .remitente(Remitente.builder() + .ruc("20100010001") + .razonSocial("Importadora Nacional S.A.C.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20100010001") + .nombre("Importadora Nacional S.A.C.") + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.IMPORTACION_CON_DAM.getCode()) // "10" + .pesoTotal(new BigDecimal("5000.00")) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PUBLICO.getCode()) + .fechaTraslado(dateProvider.now()) + .transportista(Transportista.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20300030003") + .nombre("Transportes Pesados S.A.C.") + .numeroRegistroMTC("MTC-123456") + .build()) + .indicador("SUNAT_Envio_IndicadorTrasladoTotalDAMDS") + .contenedor(Contenedor.builder() + .numero("CONT-2024-001") + .precinto("PREC-001") + .build()) + .contenedor(Contenedor.builder() + .numero("CONT-2024-002") + .precinto("PREC-002") + .build()) + .puerto(Puerto.builder() + .codigo("CALLAO") + .nombre("Puerto del Callao") + .build()) + .declaracionAduanera(DeclaracionAduanera.builder() + .tipo("DAM") + .numero("118-2024-10-000123") + .rucEmisor("20100010001") + .build()) + .partida(Partida.builder() + .direccion("Terminal Portuario del Callao") + .ubigeo("070101") + .build()) + .destino(Destino.builder() + .direccion("Almacen Deposito Temporal S.A.") + .ubigeo("150101") + .build()) + .build()) + // FAQ #24: línea vacía mínima requerida por UBL cuando traslado total + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("1.00")) + .unidadMedida("ZZ") + .codigo("-") + .build()) + .build(); + + assertInput(input, "importacionDAMTotal.xml"); + } + + /** + * GRE-Remitente: Exportación sin DAM numerada aún. + *

+ * FAQ #32: cuando no se cuenta con la DAM/DS de exportación numerada, + * se debe emitir GRE con motivo "otros" (13) y especificar en observaciones. + */ + @Test + public void testExportacionSinDAM() throws Exception { + DespatchAdvice input = DespatchAdvice.builder() + .serie("T001") + .numero(201) + .tipoComprobante(Catalog1.GUIA_REMISION_REMITENTE.getCode()) + .observaciones("Traslado para exportación, DAM en trámite") + .remitente(Remitente.builder() + .ruc("20500050005") + .razonSocial("Exportadora Perú S.A.C.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20500050005") + .nombre("Exportadora Perú S.A.C.") + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.OTROS.getCode()) // "13" por FAQ #32 + .motivoTraslado("Traslado para exportación sin DAM numerada") + .pesoTotal(new BigDecimal("2000.00")) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PRIVADO.getCode()) + .fechaTraslado(dateProvider.now()) + .chofer(Driver.builder() + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("12345678") + .nombres("Pedro") + .apellidos("Gonzales") + .licencia("Q9876543") + .tipo("Principal") + .build()) + .vehiculo(Vehicle.builder() + .placa("ABC-123") + .build()) + .partida(Partida.builder() + .direccion("Planta de producción") + .ubigeo("150101") + .build()) + .destino(Destino.builder() + .direccion("Depósito Temporal para exportación") + .ubigeo("070101") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("100.00")) + .unidadMedida("KGM") + .codigo("EXPORT-001") + .descripcion("Quinua orgánica para exportación") + .build()) + .build(); + + assertInput(input, "exportacionSinDAM.xml"); + } + + /** + * GRE-Remitente: Mercancía extranjera motivo 19 desde puerto a depósito + * temporal. + *

+ * FAQ #40: Cuando se traslada del puerto un contenedor con carga consolidada + * con destino a un depósito temporal, corresponde emitir GRE-Remitente + * motivo "19 Traslado de mercancía extranjera" a cargo del DT. + *

+ * Nota: La obligatoriedad plena del motivo 19 en reemplazo del ticket de salida + * fue pospuesta al 01-jul-2026 por RS 000133-2025/SUNAT. + * Actualmente se puede usar tanto ticket de salida como GRE. + */ + @Test + public void testMercanciaExtranjeraMotivo19() throws Exception { + DespatchAdvice input = DespatchAdvice.builder() + .serie("T001") + .numero(202) + .tipoComprobante(Catalog1.GUIA_REMISION_REMITENTE.getCode()) + .remitente(Remitente.builder() + .ruc("20600060006") + .razonSocial("Depósito Temporal del Callao S.A.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20600060006") + .nombre("Depósito Temporal del Callao S.A.") + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.TRASLADO_MERCANCIA_EXTRANJERA.getCode()) // "19" + .pesoTotal(new BigDecimal("10000.00")) + .pesoTotalUnidadMedida("KGM") + .numeroDeBultos(1) + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PUBLICO.getCode()) + .fechaTraslado(dateProvider.now()) + .transportista(Transportista.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20700070007") + .nombre("Transportes Portuarios S.A.C.") + .build()) + .contenedor(Contenedor.builder() + .numero("MSKU1234567") + .precinto("SEAL-ABC123") + .build()) + .puerto(Puerto.builder() + .codigo("CALLAO") + .nombre("Puerto del Callao") + .build()) + .numeroManifiesto("2024-000456") + .partida(Partida.builder() + .direccion("Terminal Portuario del Callao") + .ubigeo("070101") + .build()) + .destino(Destino.builder() + .direccion("Depósito Temporal del Callao") + .ubigeo("070106") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("1.00")) + .unidadMedida("ZZ") + .codigo("CONT-CONSOLIDADO") + .descripcion("Contenedor carga consolidada") + .build()) + .build(); + + assertInput(input, "mercanciaExtranjera.xml"); + } +} diff --git a/xbuilder/core/src/test/java/e2e/renderer/despatchadvice/DespatchAdviceComplexTest.java b/xbuilder/core/src/test/java/e2e/renderer/despatchadvice/DespatchAdviceComplexTest.java index d8bfb9b7..f8ed100f 100644 --- a/xbuilder/core/src/test/java/e2e/renderer/despatchadvice/DespatchAdviceComplexTest.java +++ b/xbuilder/core/src/test/java/e2e/renderer/despatchadvice/DespatchAdviceComplexTest.java @@ -12,119 +12,112 @@ public class DespatchAdviceComplexTest extends AbstractTest { - @Test - public void testComplexData() throws Exception { - // Given - DespatchAdvice input = DespatchAdvice.builder() - .serie("T001") - .numero(100) - .version("2.1") - .tipoComprobante(Catalog1.GUIA_REMISION_REMITENTE.getCode()) - .remitente(Remitente.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build() - ) - .destinatario(Destinatario.builder() - .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) - .numeroDocumentoIdentidad("12345678") - .nombre("Mi Cliente S.A.C.") - .build() - ) - .comprador(Comprador.builder() - .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) - .numeroDocumentoIdentidad("12345678") - .nombre("Mi Cliente S.A.C.") - .build() - ) - .documentoAdicional(DocumentoAdicional.builder() - .tipoDocumento("09") - .numero("T001-1") - .rucEmisor("20100010001") - .tipoDocumentoDescripcion("GUIA REMISION") - .build() - ) - .envio(Envio.builder() - .tipoTraslado(Catalog20.VENTA.getCode()) - .pesoTotal(new BigDecimal("100.50")) - .pesoTotalUnidadMedida("KGM") - .pesoItems(new BigDecimal("90.00")) - .sustentoPeso("Empaque madera") - .numeroDeBultos(10) - .tipoModalidadTraslado(Catalog18.TRANSPORTE_PUBLICO.getCode()) - .fechaTraslado(dateProvider.now()) - .chofer(Driver.builder() - .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) - .numeroDocumentoIdentidad("11111111") - .nombres("Juan") - .apellidos("Perez") - .licencia("Q1234567") - .tipo("CONDUCTOR_PRINCIPAL") - .build()) - .chofer(Driver.builder() - .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) - .numeroDocumentoIdentidad("22222222") - .nombres("Jose") - .apellidos("Gomez") - .tipo("COPILOTO") - .build()) - .contenedor("CONT-001") - .contenedor("CONT-002") - .vehiculo(Vehicle.builder() - .placa("ABC-123") - .numeroCirculacion("TUC-001") - .numeroAutorizacion("AUTH-001") - .codigoEmisor("MTC") - .secundario(Vehicle.builder() - .placa("CAR-456") - .numeroCirculacion("TUC-SEC") - .build() - ) - .build() - ) - .puerto(Puerto.builder() - .codigo("CALLAO") - .nombre("Puerto del Callao") - .build() - ) - .partida(Partida.builder() - .direccion("Av. Origen 123") - .ubigeo("010101") - .codigoLocal("0001") - .ruc("12345678912") - .build() - ) - .destino(Destino.builder() - .direccion("Av. Destino 456") - .ubigeo("020202") - .codigoLocal("0002") - .ruc("87654321098") - .build() - ) - .build() - ) - .detalle(DespatchAdviceItem.builder() - .cantidad(new BigDecimal("5.00")) - .unidadMedida("NIU") - .codigo("ITEM-01") - .descripcion("Caja de herramientas") - .atributo(GuiaItemAttribute.builder() - .name("Color") - .code("1001") - .value("Rojo") - .build() - ) - .atributo(GuiaItemAttribute.builder() - .name("Marca") - .code("1002") - .value("ToolMaster") - .build() - ) - .build() - ) - .build(); + @Test + public void testComplexData() throws Exception { + // Given + DespatchAdvice input = DespatchAdvice.builder() + .serie("T001") + .numero(100) + .version("2.1") + .tipoComprobante(Catalog1.GUIA_REMISION_REMITENTE.getCode()) + .remitente(Remitente.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("12345678") + .nombre("Mi Cliente S.A.C.") + .build()) + .comprador(Comprador.builder() + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("12345678") + .nombre("Mi Cliente S.A.C.") + .build()) + .documentoAdicional(DocumentoAdicional.builder() + .tipoDocumento("09") + .numero("T001-1") + .rucEmisor("20100010001") + .tipoDocumentoDescripcion("GUIA REMISION") + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .pesoTotal(new BigDecimal("100.50")) + .pesoTotalUnidadMedida("KGM") + .pesoItems(new BigDecimal("90.00")) + .sustentoPeso("Empaque madera") + .numeroDeBultos(10) + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PUBLICO.getCode()) + .fechaTraslado(dateProvider.now()) + .chofer(Driver.builder() + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("11111111") + .nombres("Juan") + .apellidos("Perez") + .licencia("Q1234567") + .tipo("CONDUCTOR_PRINCIPAL") + .build()) + .chofer(Driver.builder() + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("22222222") + .nombres("Jose") + .apellidos("Gomez") + .tipo("COPILOTO") + .build()) + .contenedor(Contenedor.builder() + .numero("1") + .precinto("CONT-001") + .build()) + .contenedor(Contenedor.builder() + .numero("2") + .precinto("CONT-002") + .build()) + .vehiculo(Vehicle.builder() + .placa("ABC-123") + .numeroCirculacion("TUC-001") + .numeroAutorizacion("AUTH-001") + .codigoEmisor("MTC") + .secundario(Vehicle.builder() + .placa("CAR-456") + .numeroCirculacion("TUC-SEC") + .build()) + .build()) + .puerto(Puerto.builder() + .codigo("CALLAO") + .nombre("Puerto del Callao") + .build()) + .partida(Partida.builder() + .direccion("Av. Origen 123") + .ubigeo("010101") + .codigoLocal("0001") + .ruc("12345678912") + .build()) + .destino(Destino.builder() + .direccion("Av. Destino 456") + .ubigeo("020202") + .codigoLocal("0002") + .ruc("87654321098") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("5.00")) + .unidadMedida("NIU") + .codigo("ITEM-01") + .descripcion("Caja de herramientas") + .atributo(GuiaItemAttribute.builder() + .name("Color") + .code("1001") + .value("Rojo") + .build()) + .atributo(GuiaItemAttribute.builder() + .name("Marca") + .code("1002") + .value("ToolMaster") + .build()) + .build()) + .build(); - assertInput(input, "complexData.xml"); - } + assertInput(input, "complexData.xml"); + } } diff --git a/xbuilder/core/src/test/java/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest.java b/xbuilder/core/src/test/java/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest.java new file mode 100644 index 00000000..a95eb5d4 --- /dev/null +++ b/xbuilder/core/src/test/java/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest.java @@ -0,0 +1,481 @@ +package e2e.renderer.despatchadvice; + +import e2e.AbstractTest; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog18; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog20; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog6; +import io.github.project.openubl.xbuilder.content.catalogs.IndicadorEnvio; +import io.github.project.openubl.xbuilder.content.catalogs.TipoConductor; +import io.github.project.openubl.xbuilder.content.models.standard.guia.*; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +/** + * Tests de GRE-Remitente usando el modelo {@link GRERemitente}. + *

+ * Casuísticas cubiertas: + *

    + *
  1. Transporte privado básico (conductor + vehículo, solo placa)
  2. + *
  3. Transporte público (transportista contratado consignado en + * GRE-Remitente)
  4. + *
  5. Transporte privado con múltiples conductores (relevo)
  6. + *
  7. Transporte privado con vehículo principal + secundarios (TUC + * condicional)
  8. + *
  9. Transporte público con transportista consignado en GRE-Remitente — + * NO confundir con GRE-Transportista (que emite su propia guía V*)
  10. + *
  11. Vehículo categoría M1/L — indicador + * SUNAT_Envio_IndicadorTrasladoVehiculoM1L + * (sin conductor ni vehículo, distinto de transbordo programado)
  12. + *
  13. Mercancía extranjera motivo 19 desde zona primaria (contenedores + + * precintos)
  14. + *
+ */ +public class GRERemitenteCasuisticasTest extends AbstractTest { + + /** + * Casuística 1: Transporte PRIVADO básico. + *

+ * El remitente traslada sus propios bienes con su propio vehículo. + * Requiere: conductor principal + vehículo (solo placa, TUC no es + * obligatorio salvo inscripción ante el MTC). + */ + @Test + public void testTransportePrivadoBasico() throws Exception { + GRERemitente gre = GRERemitente.builder() + .serie("T001") + .numero(1) + .remitente(Remitente.builder() + .ruc("20100010001") + .razonSocial("Empresa Remitente S.A.C.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20200020002") + .nombre("Almacén Destino S.A.C.") + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .pesoTotal(new BigDecimal("50.000")) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PRIVADO.getCode()) + .fechaTraslado(dateProvider.now()) + .chofer(Driver.builder() + .tipo(TipoConductor.PRINCIPAL.getCode()) + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("12345678") + .nombres("Carlos") + .apellidos("Ramirez") + .licencia("Q1234567") + .build()) + .vehiculo(Vehicle.builder() + .placa("ABC-123") + .build()) + .partida(Partida.builder() + .ubigeo("150101") + .direccion("Av. Industrial 456, Lima") + .build()) + .destino(Destino.builder() + .ubigeo("150102") + .direccion("Jr. Comercio 789, Rímac") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("10.00")) + .unidadMedida("NIU") + .codigo("PROD-001") + .descripcion("Cajas de producto terminado") + .build()) + .build(); + + assertInput(gre, "transportePrivadoBasico.xml"); + } + + /** + * Casuística 2: Transporte PÚBLICO. + *

+ * El remitente contrata a un transportista. + * Requiere: datos del transportista (RUC, razón social). + * No requiere conductor/vehículo del remitente (lo provee el transportista). + */ + @Test + public void testTransportePublico() throws Exception { + GRERemitente gre = GRERemitente.builder() + .serie("T001") + .numero(2) + .remitente(Remitente.builder() + .ruc("20100010001") + .razonSocial("Empresa Remitente S.A.C.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20200020002") + .nombre("Cliente Final S.A.") + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .pesoTotal(new BigDecimal("200.000")) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PUBLICO.getCode()) + .fechaTraslado(dateProvider.now()) + .transportista(Transportista.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20300030003") + .nombre("Transportes Nacionales S.A.C.") + .numeroRegistroMTC("MTC-001234") + .build()) + .partida(Partida.builder() + .ubigeo("150101") + .direccion("Almacén Central, Lima") + .build()) + .destino(Destino.builder() + .ubigeo("040101") + .direccion("Sucursal Arequipa") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("50.00")) + .unidadMedida("NIU") + .codigo("PROD-002") + .descripcion("Mercadería general") + .build()) + .build(); + + assertInput(gre, "transportePublico.xml"); + } + + /** + * Casuística 3: Transporte privado con MÚLTIPLES CONDUCTORES. + *

+ * Según SUNAT, se pueden consignar conductores adicionales + * (copiloto, relevo) además del conductor principal. + */ + @Test + public void testMultiplesConductores() throws Exception { + GRERemitente gre = GRERemitente.builder() + .serie("T001") + .numero(3) + .remitente(Remitente.builder() + .ruc("20100010001") + .razonSocial("Empresa Remitente S.A.C.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20200020002") + .nombre("Destino S.A.C.") + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.TRASLADO_ENTRE_ESTABLECIMIENTOS.getCode()) + .pesoTotal(new BigDecimal("1500.000")) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PRIVADO.getCode()) + .fechaTraslado(dateProvider.now()) + .chofer(Driver.builder() + .tipo(TipoConductor.PRINCIPAL.getCode()) + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("11111111") + .nombres("Juan") + .apellidos("Perez") + .licencia("Q1111111") + .build()) + .chofer(Driver.builder() + .tipo(TipoConductor.SECUNDARIO.getCode()) + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("22222222") + .nombres("Pedro") + .apellidos("Gomez") + .licencia("Q2222222") + .build()) + .vehiculo(Vehicle.builder() + .placa("DEF-456") + .build()) + .partida(Partida.builder() + .ubigeo("150101") + .direccion("Planta Principal, Lima") + .build()) + .destino(Destino.builder() + .ubigeo("130101") + .direccion("Sucursal Tacna") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("100.00")) + .unidadMedida("KGM") + .codigo("MAT-001") + .descripcion("Materia prima") + .build()) + .build(); + + assertInput(gre, "multiplesConductores.xml"); + } + + /** + * Casuística 4: Transporte privado con VEHÍCULO + CARRETA (secundarios). + *

+ * El vehículo principal puede llevar vehículos secundarios adjuntos + * (carretas, semirremolques) con sus propias placas. + *

+ * El TUC (Tarjeta Única de Circulación) es condicional: solo se consigna + * cuando el vehículo tiene obligación de inscripción ante el MTC + * (vehículos de transporte de carga pesada). No es dato universal. + */ + @Test + public void testVehiculoConCarreta() throws Exception { + GRERemitente gre = GRERemitente.builder() + .serie("T001") + .numero(4) + .remitente(Remitente.builder() + .ruc("20100010001") + .razonSocial("Empresa Remitente S.A.C.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20200020002") + .nombre("Destino S.A.C.") + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .pesoTotal(new BigDecimal("8000.000")) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PRIVADO.getCode()) + .fechaTraslado(dateProvider.now()) + .chofer(Driver.builder() + .tipo(TipoConductor.PRINCIPAL.getCode()) + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("33333333") + .nombres("Roberto") + .apellidos("Silva") + .licencia("Q3333333") + .build()) + .vehiculo(Vehicle.builder() + .placa("GHI-789") + .numeroCirculacion("TUC-MAIN") // TUC: vehículo inscrito ante MTC + .numeroAutorizacion("AUTH-001") // Dato de ejemplo condicional, no obligatorio en todo + // vehículo + .codigoEmisor("MTC") // Dato de ejemplo condicional, asociado a numeroAutorizacion + .secundario(Vehicle.builder() + .placa("CAR-001") + .numeroCirculacion("TUC-SEC1") // Carreta también inscrita + .build()) + .secundario(Vehicle.builder() + .placa("CAR-002") + // Sin TUC: carreta sin obligación de inscripción + .build()) + .build()) + .partida(Partida.builder() + .ubigeo("150101") + .direccion("Centro de distribución, Lima") + .build()) + .destino(Destino.builder() + .ubigeo("060101") + .direccion("Depósito Cajamarca") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("500.00")) + .unidadMedida("KGM") + .codigo("FERT-001") + .descripcion("Fertilizantes") + .build()) + .build(); + + assertInput(gre, "vehiculoConCarreta.xml"); + } + + /** + * Casuística 5: Transporte público — transportista consignado en GRE-Remitente. + *

+ * El remitente contrata un servicio de transporte público y consigna los + * datos del transportista contratado (CarrierParty) en su GRE-Remitente. + *

+ * IMPORTANTE: Este XML refleja solo la perspectiva del remitente. + * El transportista efectivo que realiza el traslado debe emitir su propia + * GRE-Transportista (serie V*) con conductor, vehículo y referencia a esta GRE. + * En transporte público, la GRE-Remitente y la GRE-Transportista pueden + * coexistir para el mismo traslado, según corresponda al sujeto obligado + * y al flujo operativo aplicable. + * traslado. + *

+ * Ref: RS 000123-2022/SUNAT — relación entre GRE-Remitente y GRE-Transportista. + */ + @Test + public void testTransporteSubcontratado() throws Exception { + GRERemitente gre = GRERemitente.builder() + .serie("T001") + .numero(5) + .remitente(Remitente.builder() + .ruc("20100010001") + .razonSocial("Empresa Remitente S.A.C.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20200020002") + .nombre("Tienda Destino S.A.") + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .pesoTotal(new BigDecimal("3000.000")) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PUBLICO.getCode()) + .fechaTraslado(dateProvider.now()) + .transportista(Transportista.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20400040004") + .nombre("Logística Express S.A.C.") + .numeroRegistroMTC("MTC-567890") + .build()) + .partida(Partida.builder() + .ubigeo("150101") + .direccion("Almacén Principal, Lima") + .codigoLocal("0001") + .ruc("20100010001") + .build()) + .destino(Destino.builder() + .ubigeo("150132") + .direccion("Tienda San Isidro") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("200.00")) + .unidadMedida("NIU") + .codigo("ELEC-001") + .descripcion("Equipos electrónicos") + .build()) + .build(); + + assertInput(gre, "transporteSubcontratado.xml"); + } + + /** + * Casuística 6: Transporte privado en vehículo de categoría M1 o L. + *

+ * Cuando el traslado se realiza en vehículos de categoría M1 (automóviles) + * o categoría L (motocicletas, mototaxis), SUNAT permite no consignar + * conductor ni datos del vehículo. + *

+ * Se utiliza el indicador {@code SUNAT_Envio_IndicadorTrasladoVehiculoM1L}. + *

+ * PENDIENTE DE VALIDACIÓN DOCUMENTAL: El token exacto + * {@code SUNAT_Envio_IndicadorTrasladoVehiculoM1L} debe confirmarse contra + * el Anexo N.° 14 o el catálogo interno del portal SUNAT. La separación + * conceptual respecto de {@code SUNAT_Envio_IndicadorTransbordoProgramado} + * es correcta, pero el literal del indicador M1/L queda sujeto a + * verificación final antes de considerar este caso como cerrado. + *

+ * Ref: Anexo N.° 14 UBL 2.1, RS 000123-2022/SUNAT. + */ + @Test + public void testVehiculoM1L() throws Exception { + GRERemitente gre = GRERemitente.builder() + .serie("T001") + .numero(6) + .remitente(Remitente.builder() + .ruc("20100010001") + .razonSocial("Empresa Remitente S.A.C.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("87654321") + .nombre("Persona Natural") + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .pesoTotal(new BigDecimal("5.000")) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PRIVADO.getCode()) + .fechaTraslado(dateProvider.now()) + .indicador(IndicadorEnvio.VEHICULO_M1_L.getCode()) + .partida(Partida.builder() + .ubigeo("150101") + .direccion("Tienda Lima Centro") + .build()) + .destino(Destino.builder() + .ubigeo("150101") + .direccion("Domicilio Cliente") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("1.00")) + .unidadMedida("NIU") + .codigo("PEQ-001") + .descripcion("Paquete pequeño") + .build()) + .build(); + + assertInput(gre, "vehiculoM1L.xml"); + } + + /** + * Casuística 7: Mercancía extranjera — motivo 19 desde zona primaria. + *

+ * RS 000240-2024/SUNAT creó el motivo "19 - Traslado de mercancía extranjera" + * específicamente para la trazabilidad de mercancía que sale de zona primaria + * (puerto/aeropuerto) hacia depósito temporal, sin destinación aduanera o sin + * levante. + *

+ * Los campos contenedor, precinto y el indicador de zona primaria son + * condicionales al motivo 19 del Catálogo 20 — no aplican a cualquier + * motivo de importación. + *

+ * NOTA: La obligatoriedad plena del motivo 19 (en reemplazo del ticket + * de salida) fue pospuesta al 01-jul-2026 por RS 000133-2025/SUNAT. + * Hasta esa fecha coexisten el ticket de salida y la GRE con motivo 19. + */ + @Test + public void testMercanciaExtranjeraMotivo19() throws Exception { + GRERemitente gre = GRERemitente.builder() + .serie("T001") + .numero(7) + .remitente(Remitente.builder() + .ruc("20500050005") + .razonSocial("Depósito Temporal del Callao S.A.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20500050005") + .nombre("Depósito Temporal del Callao S.A.") + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.TRASLADO_MERCANCIA_EXTRANJERA.getCode()) // "19" + .pesoTotal(new BigDecimal("12000.000")) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PUBLICO.getCode()) + .fechaTraslado(dateProvider.now()) + .transportista(Transportista.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20600060006") + .nombre("Transporte Portuario S.A.C.") + .build()) + .indicador(IndicadorEnvio.TRASLADO_TOTAL_DAM_DS.getCode()) + .contenedor(Contenedor.builder() + .numero("MSKU-2024-001") + .precinto("SEAL-001") + .build()) + .contenedor(Contenedor.builder() + .numero("MSKU-2024-002") + .precinto("SEAL-002") + .build()) + .puerto(Puerto.builder() + .codigo("CALLAO") + .nombre("Puerto del Callao") + .build()) + .partida(Partida.builder() + .ubigeo("070101") + .direccion("Terminal Portuario del Callao") + .build()) + .destino(Destino.builder() + .ubigeo("150101") + .direccion("Almacén Depósito Temporal") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("1.00")) + .unidadMedida("ZZ") + .codigo("CONT-CONSOLIDADO") + .descripcion("Contenedor carga consolidada - mercancía extranjera") + .build()) + .build(); + + assertInput(gre, "mercanciaExtranjeraMotivo19.xml"); + } +} diff --git a/xbuilder/core/src/test/java/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest.java b/xbuilder/core/src/test/java/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest.java new file mode 100644 index 00000000..070675a7 --- /dev/null +++ b/xbuilder/core/src/test/java/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest.java @@ -0,0 +1,326 @@ +package e2e.renderer.despatchadvice; + +import e2e.AbstractTest; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog18; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog20; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog6; +import io.github.project.openubl.xbuilder.content.catalogs.IndicadorEnvio; +import io.github.project.openubl.xbuilder.content.catalogs.TipoConductor; +import io.github.project.openubl.xbuilder.content.models.standard.guia.*; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +/** + * Tests de GRE-Transportista usando el modelo {@link GRETransportista}. + *

+ * Casuísticas cubiertas: + *

    + *
  1. Transportista básico (un conductor, un vehículo)
  2. + *
  3. Transportista con múltiples conductores (relevo)
  4. + *
  5. Transportista con vehículo + carreta
  6. + *
  7. Transportista en comercio exterior con contenedores
  8. + *
+ */ +public class GRETransportistaCasuisticasTest extends AbstractTest { + + /** + * Casuística 1: GRE-Transportista BÁSICA. + *

+ * El transportista emite la guía para trasladar bienes de un cliente. + * En esta casuística básica se consigna conductor y vehículo. + */ + @Test + public void testTransportistaBasico() throws Exception { + GRETransportista gre = GRETransportista.builder() + .serie("V001") + .numero(1) + .transportistaEmisor(Transportista.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20300030003") + .nombre("Transportes Rápidos S.A.C.") + .numeroRegistroMTC("MTC-001234") + .build()) + .remitente(Tercero.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20100010001") + .nombre("Empresa Remitente S.A.C.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20200020002") + .nombre("Cliente Final S.A.") + .build()) + .conductor(Driver.builder() + .tipo(TipoConductor.PRINCIPAL.getCode()) + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("44444444") + .nombres("Miguel") + .apellidos("Torres") + .licencia("Q4444444") + .build()) + .vehiculo(Vehicle.builder() + .placa("JKL-012") + .numeroCirculacion("TUC-JKL") // TUC: transportista inscrito ante MTC + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .pesoTotal(new BigDecimal("300.000")) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PUBLICO.getCode()) + .fechaTraslado(dateProvider.now()) + .partida(Partida.builder() + .ubigeo("150101") + .direccion("Almacén Remitente, Lima") + .build()) + .destino(Destino.builder() + .ubigeo("040101") + .direccion("Sucursal Arequipa") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("25.00")) + .unidadMedida("NIU") + .codigo("MER-001") + .descripcion("Mercadería general") + .build()) + .build(); + + assertInput(gre, "transportistaBasico.xml"); + } + + /** + * Casuística 2: GRE-Transportista con MÚLTIPLES CONDUCTORES. + *

+ * Para viajes largos se requiere conductor de relevo. + */ + @Test + public void testTransportistaMultiplesConductores() throws Exception { + GRETransportista gre = GRETransportista.builder() + .serie("V001") + .numero(2) + .transportistaEmisor(Transportista.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20300030003") + .nombre("Transportes Rápidos S.A.C.") + .numeroRegistroMTC("MTC-001234") + .build()) + .remitente(Tercero.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20100010001") + .nombre("Empresa Remitente S.A.C.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20200020002") + .nombre("Destino S.A.") + .build()) + .conductor(Driver.builder() + .tipo(TipoConductor.PRINCIPAL.getCode()) + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("55555555") + .nombres("Luis") + .apellidos("Fernandez") + .licencia("Q5555555") + .build()) + .conductor(Driver.builder() + .tipo(TipoConductor.SECUNDARIO.getCode()) + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("66666666") + .nombres("Mario") + .apellidos("Vargas") + .licencia("Q6666666") + .build()) + .vehiculo(Vehicle.builder() + .placa("MNO-345") + .numeroCirculacion("TUC-MNO") // TUC: transportista inscrito ante MTC + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .pesoTotal(new BigDecimal("5000.000")) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PUBLICO.getCode()) + .fechaTraslado(dateProvider.now()) + .partida(Partida.builder() + .ubigeo("150101") + .direccion("Centro de Carga Lima") + .build()) + .destino(Destino.builder() + .ubigeo("130101") + .direccion("Terminal Tacna") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("300.00")) + .unidadMedida("KGM") + .codigo("CARG-001") + .descripcion("Carga general") + .build()) + .build(); + + assertInput(gre, "transportistaMultiplesConductores.xml"); + } + + /** + * Casuística 3: GRE-Transportista con VEHÍCULO + CARRETA. + *

+ * Vehículo principal con semirremolque adjunto. Los datos de autorización + * y código de entidad autorizadora se consignan como datos de ejemplo + * para esta casuística; son campos contextuales según el tipo de vehículo + * y autorización vigente, no universalmente obligatorios. + */ + @Test + public void testTransportistaConCarreta() throws Exception { + GRETransportista gre = GRETransportista.builder() + .serie("V001") + .numero(3) + .transportistaEmisor(Transportista.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20300030003") + .nombre("Transportes Pesados S.A.C.") + .numeroRegistroMTC("MTC-PESADOS") + .build()) + .remitente(Tercero.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20100010001") + .nombre("Minera del Sur S.A.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20200020002") + .nombre("Fundición Norte S.A.") + .build()) + .conductor(Driver.builder() + .tipo(TipoConductor.PRINCIPAL.getCode()) + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("77777777") + .nombres("Andres") + .apellidos("Quispe") + .licencia("Q7777777") + .build()) + .vehiculo(Vehicle.builder() + .placa("PQR-678") + .numeroCirculacion("TUC-PQR") + .numeroAutorizacion("AUTH-PESADOS") // Dato de ejemplo condicional, no obligatorio en todo + // vehículo + .codigoEmisor("MTC") // Dato de ejemplo condicional, asociado a numeroAutorizacion + .secundario(Vehicle.builder() + .placa("REM-001") + .numeroCirculacion("TUC-REM") + .build()) + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .pesoTotal(new BigDecimal("20000.000")) + .pesoTotalUnidadMedida("KGM") + .numeroDeBultos(1) + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PUBLICO.getCode()) + .fechaTraslado(dateProvider.now()) + .partida(Partida.builder() + .ubigeo("040101") + .direccion("Mina del Sur, Arequipa") + .build()) + .destino(Destino.builder() + .ubigeo("060101") + .direccion("Planta procesadora, Cajamarca") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("20000.00")) + .unidadMedida("KGM") + .codigo("MIN-001") + .descripcion("Concentrado de cobre") + .build()) + .build(); + + assertInput(gre, "transportistaConCarreta.xml"); + } + + /** + * Casuística 4: GRE-Transportista en COMERCIO EXTERIOR — mercancía extranjera + * motivo 19. + *

+ * Traslado de contenedores desde zona primaria (puerto) a depósito temporal. + * El motivo 19 fue introducido por RS 000240-2024/SUNAT dentro del esquema de + * trazabilidad de comercio exterior. RS 000133-2025/SUNAT movió al + * 01-jul-2026 la entrada en vigor de la disposición derogatoria del uso + * exclusivo del ticket de salida, vinculada a este motivo. + *

+ * Este caso valida el soporte de modelo/render para: contenedores con precinto, + * puerto e indicador de traslado total. Es una casuística de modelado soportada + * y no un ejemplo exhaustivo de todos los campos posibles del motivo 19. + *

+ * Nota: la inclusión de {@code declaracionAduanera} en este escenario + * específico + * depende del mapeo exacto del Anexo 14 para la variante motivo 19 con traslado + * total desde zona primaria. Se omite aquí hasta confirmar su obligatoriedad + * para esta casuística concreta. + */ + @Test + public void testTransportistaComercioExterior() throws Exception { + GRETransportista gre = GRETransportista.builder() + .serie("V001") + .numero(4) + .transportistaEmisor(Transportista.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20700070007") + .nombre("Transportes Portuarios S.A.C.") + .numeroRegistroMTC("MTC-PORT-001") + .build()) + .remitente(Tercero.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20500050005") + .nombre("Importadora Nacional S.A.C.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20500050005") + .nombre("Importadora Nacional S.A.C.") + .build()) + .conductor(Driver.builder() + .tipo(TipoConductor.PRINCIPAL.getCode()) + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("88888888") + .nombres("Fernando") + .apellidos("Ruiz") + .licencia("Q8888888") + .build()) + .vehiculo(Vehicle.builder() + .placa("STU-901") + .numeroCirculacion("TUC-STU") // TUC: transportista inscrito ante MTC + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.TRASLADO_MERCANCIA_EXTRANJERA.getCode()) // "19" + .pesoTotal(new BigDecimal("15000.000")) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PUBLICO.getCode()) + .fechaTraslado(dateProvider.now()) + .indicador(IndicadorEnvio.TRASLADO_TOTAL_DAM_DS.getCode()) + .contenedor(Contenedor.builder() + .numero("CONT-IMP-001") + .precinto("SEAL-IMP-001") + .build()) + .puerto(Puerto.builder() + .codigo("CALLAO") + .nombre("Puerto del Callao") + .build()) + .partida(Partida.builder() + .ubigeo("070101") + .direccion("Terminal Portuario del Callao") + .build()) + .destino(Destino.builder() + .ubigeo("150101") + .direccion("Almacén Lima") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("1.00")) + .unidadMedida("ZZ") + .codigo("CARGA-EXT-001") + .descripcion("Carga consolidada comercio exterior") + .build()) + .build(); + + assertInput(gre, "transportistaComercioExterior.xml"); + } +} diff --git a/xbuilder/core/src/test/java/unit/catalogs/GRECatalogosTest.java b/xbuilder/core/src/test/java/unit/catalogs/GRECatalogosTest.java new file mode 100644 index 00000000..8205b1d1 --- /dev/null +++ b/xbuilder/core/src/test/java/unit/catalogs/GRECatalogosTest.java @@ -0,0 +1,114 @@ +package unit.catalogs; + +import io.github.project.openubl.xbuilder.content.catalogs.*; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests unitarios para los catálogos SUNAT de la GRE. + * Verifica que los códigos sean correctos según Anexo N.° 8. + */ +public class GRECatalogosTest { + + @Test + public void testCatalog20_MotivoTraslado() { + assertEquals("01", Catalog20.VENTA.getCode()); + assertEquals("02", Catalog20.COMPRA.getCode()); + assertEquals("03", Catalog20.CONSIGNACION.getCode()); + assertEquals("04", Catalog20.TRASLADO_ENTRE_ESTABLECIMIENTOS.getCode()); + assertEquals("05", Catalog20.DEVOLUCION.getCode()); + assertEquals("06", Catalog20.TRASLADO_TRANSFORMACION.getCode()); + assertEquals("07", Catalog20.RECOJO_BIENES_TRANSFORMADOS.getCode()); + assertEquals("08", Catalog20.IMPORTACION.getCode()); + assertEquals("09", Catalog20.EXPORTACION.getCode()); + assertEquals("10", Catalog20.IMPORTACION_CON_DAM.getCode()); + assertEquals("11", Catalog20.IMPORTACION_TEMPORAL.getCode()); + assertEquals("13", Catalog20.OTROS.getCode()); + assertEquals("14", Catalog20.VENTA_SUJETA_A_CONFIRMACION.getCode()); + assertEquals("15", Catalog20.TRASLADO_ZONA_IVAP.getCode()); + assertEquals("16", Catalog20.EXPORTACION_TEMPORAL.getCode()); + assertEquals("17", Catalog20.REEXPORTACION.getCode()); + assertEquals("18", Catalog20.TRASLADO_EMISOR_ITINERANTE_CP.getCode()); + assertEquals("19", Catalog20.TRASLADO_MERCANCIA_EXTRANJERA.getCode()); + + // Verify lookup + assertTrue(Catalog20.valueOfCode("10").isPresent()); + assertEquals(Catalog20.IMPORTACION_CON_DAM, Catalog20.valueOfCode("10").get()); + assertFalse(Catalog20.valueOfCode("99").isPresent()); + } + + @Test + public void testCatalog21_DocumentoRelacionado() { + assertEquals("01", Catalog21.NUMERACION_DAM.getCode()); + assertEquals("02", Catalog21.NUMERO_DE_ORDEN_DE_ENTREGA.getCode()); + assertEquals("03", Catalog21.NUMERO_SCOP.getCode()); + assertEquals("04", Catalog21.NUMERO_DE_MANIFIESTO_DE_CARGA.getCode()); + assertEquals("05", Catalog21.NUMERO_DE_CONSTANCIA_DE_DETRACCION.getCode()); + assertEquals("06", Catalog21.OTROS.getCode()); + assertEquals("09", Catalog21.GUIA_REMISION_REMITENTE.getCode()); + assertEquals("12", Catalog21.DECLARACION_SIMPLIFICADA.getCode()); + assertEquals("31", Catalog21.GUIA_REMISION_TRANSPORTISTA.getCode()); + assertEquals("49", Catalog21.TICKET_SALIDA.getCode()); + assertEquals("50", Catalog21.CODIGO_AUTORIZACION_SUNAT.getCode()); + + assertTrue(Catalog21.valueOfCode("49").isPresent()); + } + + @Test + public void testCatalog61_DocumentoTransporte() { + assertEquals("01", Catalog61.FACTURA.getCode()); + assertEquals("04", Catalog61.GUIA_REMISION_REMITENTE.getCode()); + assertEquals("05", Catalog61.GUIA_REMISION_TRANSPORTISTA.getCode()); + assertEquals("50", Catalog61.DAM.getCode()); + assertEquals("52", Catalog61.DECLARACION_SIMPLIFICADA.getCode()); + } + + @Test + public void testCatalog62_BienesNormalizados() { + assertEquals("01", Catalog62.AZUCAR.getCode()); + assertEquals("02", Catalog62.ARROZ.getCode()); + assertEquals("03", Catalog62.ALCOHOL_ETILICO.getCode()); + assertEquals("04", Catalog62.CEMENTO.getCode()); + } + + @Test + public void testCatalog63_Puertos() { + assertEquals("CALLAO", Catalog63.CALLAO.getCode()); + assertEquals("PAITA", Catalog63.PAITA.getCode()); + assertEquals("CHANCAY", Catalog63.CHANCAY.getCode()); // RS 000240-2024 + + assertTrue(Catalog63.valueOfCode("CALLAO").isPresent()); + assertTrue(Catalog63.valueOfCode("callao").isPresent()); // Case insensitive + assertFalse(Catalog63.valueOfCode("INEXISTENTE").isPresent()); + } + + @Test + public void testCatalog64_Aeropuertos() { + assertEquals("LIM", Catalog64.JORGE_CHAVEZ.getCode()); + assertEquals("AQP", Catalog64.RODRIGUEZ_BALLON.getCode()); + assertEquals("CUZ", Catalog64.ALEJANDRO_VELASCO.getCode()); + + assertTrue(Catalog64.valueOfCode("LIM").isPresent()); + } + + @Test + public void testIndicadorEnvio() { + assertEquals("SUNAT_Envio_IndicadorTrasladoTotalDAMDS", + IndicadorEnvio.TRASLADO_TOTAL_DAM_DS.getCode()); + assertEquals("SUNAT_Envio_IndicadorBienNormalizado", + IndicadorEnvio.BIEN_NORMALIZADO.getCode()); + } + + @Test + public void testCatalog18_ModalidadTraslado() { + assertEquals("01", Catalog18.TRANSPORTE_PUBLICO.getCode()); + assertEquals("02", Catalog18.TRANSPORTE_PRIVADO.getCode()); + } + + @Test + public void testCatalog1Guia_TipoDocumento() { + assertEquals("09", Catalog1_Guia.GUIA_REMISION_REMITENTE.getCode()); + assertEquals("31", Catalog1_Guia.GUIA_REMISION_TRANSPORTISTA.getCode()); + } +} diff --git a/xbuilder/core/src/test/java/unit/validator/DespatchAdviceValidatorTest.java b/xbuilder/core/src/test/java/unit/validator/DespatchAdviceValidatorTest.java new file mode 100644 index 00000000..3cbac193 --- /dev/null +++ b/xbuilder/core/src/test/java/unit/validator/DespatchAdviceValidatorTest.java @@ -0,0 +1,259 @@ +package unit.validator; + +import io.github.project.openubl.xbuilder.content.catalogs.Catalog18; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog20; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog6; +import io.github.project.openubl.xbuilder.content.models.standard.guia.*; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests unitarios para {@link DespatchAdviceValidator}. + *

+ * Valida las reglas de negocio SUNAT sin depender del renderizado XML. + */ +public class DespatchAdviceValidatorTest { + + private static DespatchAdvice.DespatchAdviceBuilder minimalGRERemitente() { + return DespatchAdvice.builder() + .serie("T001") + .numero(1) + .tipoComprobante("09") + .remitente(Remitente.builder() + .ruc("12345678912") + .razonSocial("Test S.A.C.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("12345678") + .nombre("Cliente") + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .pesoTotal(BigDecimal.ONE) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PRIVADO.getCode()) + .fechaTraslado(LocalDate.now()) + .chofer(Driver.builder() + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("11111111") + .nombres("Juan") + .apellidos("Perez") + .licencia("Q1234567") + .build()) + .vehiculo(Vehicle.builder() + .placa("ABC-123") + .build()) + .partida(Partida.builder().ubigeo("010101").direccion("Origen").build()) + .destino(Destino.builder().ubigeo("020202").direccion("Destino").build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE) + .unidadMedida("NIU") + .codigo("001") + .build()); + } + + @Test + public void testValidGRERemitente() { + DespatchAdvice da = minimalGRERemitente().build(); + List errors = DespatchAdviceValidator.validate(da); + assertTrue(errors.isEmpty(), "GRE-Remitente válido: " + errors); + } + + @Test + public void testSerieInvalida_GRERemitente() { + DespatchAdvice da = minimalGRERemitente() + .serie("V001") // Serie V no corresponde a tipo 09 + .build(); + List errors = DespatchAdviceValidator.validate(da); + assertTrue(errors.stream().anyMatch(e -> e.contains("GRE-Remitente (09) requiere serie que inicie con 'T'")), + "Debe detectar serie inválida para GRE-Remitente"); + } + + @Test + public void testSerieInvalida_GRETransportista() { + DespatchAdvice da = minimalGRERemitente() + .serie("T001") + .tipoComprobante("31") // Transportista requiere V* + .build(); + List errors = DespatchAdviceValidator.validate(da); + assertTrue( + errors.stream().anyMatch(e -> e.contains("GRE-Transportista (31) requiere serie que inicie con 'V'")), + "Debe detectar serie inválida para GRE-Transportista"); + } + + @Test + public void testTransportePrivadoSinConductor() { + DespatchAdvice da = DespatchAdvice.builder() + .serie("T001") + .numero(1) + .tipoComprobante("09") + .remitente(Remitente.builder().ruc("12345678912").razonSocial("Test").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("12345678").nombre("C").build()) + .envio(Envio.builder() + .tipoTraslado("01") + .pesoTotal(BigDecimal.ONE) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PRIVADO.getCode()) // "02" + .fechaTraslado(LocalDate.now()) + // Sin conductor ni vehículo + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle( + DespatchAdviceItem.builder().cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()) + .build(); + + List errors = DespatchAdviceValidator.validate(da); + assertTrue(errors.stream().anyMatch(e -> e.contains("conductor")), + "Transporte privado requiere conductor"); + assertTrue(errors.stream().anyMatch(e -> e.contains("vehículo")), + "Transporte privado requiere vehículo"); + } + + @Test + public void testTransportePublicoSinTransportista() { + DespatchAdvice da = DespatchAdvice.builder() + .serie("T001") + .numero(1) + .tipoComprobante("09") + .remitente(Remitente.builder().ruc("12345678912").razonSocial("Test").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("12345678").nombre("C").build()) + .envio(Envio.builder() + .tipoTraslado("01") + .pesoTotal(BigDecimal.ONE) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PUBLICO.getCode()) // "01" + .fechaTraslado(LocalDate.now()) + // Sin transportista + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle( + DespatchAdviceItem.builder().cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()) + .build(); + + List errors = DespatchAdviceValidator.validate(da); + assertTrue(errors.stream().anyMatch(e -> e.contains("transportista")), + "Transporte público requiere datos del transportista"); + } + + @Test + public void testGRETransportistaSinConductor() { + DespatchAdvice da = DespatchAdvice.builder() + .serie("V001") + .numero(1) + .tipoComprobante("31") + .remitente(Remitente.builder().ruc("20123456789").razonSocial("Transportes S.A.C.").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("6").numeroDocumentoIdentidad("20876543210").nombre("C").build()) + .tercero(Tercero.builder() + .tipoDocumentoIdentidad("6").numeroDocumentoIdentidad("20555555555") + .nombre("Remitente Original").build()) + .envio(Envio.builder() + .tipoTraslado("01") + .pesoTotal(BigDecimal.ONE) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PUBLICO.getCode()) + .fechaTraslado(LocalDate.now()) + // Sin conductor ni vehículo (obligatorio para transportista) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle( + DespatchAdviceItem.builder().cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()) + .build(); + + List errors = DespatchAdviceValidator.validate(da); + assertTrue(errors.stream().anyMatch(e -> e.contains("GRE-Transportista requiere al menos un conductor")), + "GRE-Transportista siempre requiere conductor"); + assertTrue(errors.stream().anyMatch(e -> e.contains("GRE-Transportista requiere datos del vehículo")), + "GRE-Transportista siempre requiere vehículo"); + } + + @Test + public void testGRETransportistaSinTercero() { + DespatchAdvice da = DespatchAdvice.builder() + .serie("V001") + .numero(1) + .tipoComprobante("31") + .remitente(Remitente.builder().ruc("20123456789").razonSocial("Transportes S.A.C.").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("6").numeroDocumentoIdentidad("20876543210").nombre("C").build()) + // Sin tercero ni proveedor - se espera recomendación + .envio(Envio.builder() + .tipoTraslado("01") + .pesoTotal(BigDecimal.ONE) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("01") + .fechaTraslado(LocalDate.now()) + .chofer(Driver.builder() + .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("11111111") + .nombres("J").apellidos("P").licencia("Q1234567").build()) + .vehiculo(Vehicle.builder().placa("ABC-123").build()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle( + DespatchAdviceItem.builder().cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()) + .build(); + + List errors = DespatchAdviceValidator.validate(da); + assertTrue(errors.stream().anyMatch(e -> e.contains("tercero")), + "GRE-Transportista debe advertir que falta el tercero"); + } + + @Test + public void testSinDetalles() { + DespatchAdvice da = DespatchAdvice.builder() + .serie("T001") + .numero(1) + .tipoComprobante("09") + .remitente(Remitente.builder().ruc("12345678912").razonSocial("Test").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("12345678").nombre("C").build()) + .envio(Envio.builder() + .tipoTraslado("01").pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("02").fechaTraslado(LocalDate.now()) + .chofer(Driver.builder().tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("11111111") + .nombres("J").apellidos("P").licencia("Q1234567").build()) + .vehiculo(Vehicle.builder().placa("ABC-123").build()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .build(); + + List errors = DespatchAdviceValidator.validate(da); + assertTrue(errors.stream().anyMatch(e -> e.contains("línea de detalle")), + "Debe requerir al menos una línea de detalle"); + } + + @Test + public void testRUCInvalido() { + DespatchAdvice da = minimalGRERemitente().build(); + da.getRemitente().setRuc("123"); // RUC inválido + + List errors = DespatchAdviceValidator.validate(da); + assertTrue(errors.stream().anyMatch(e -> e.contains("11 dígitos")), + "Debe rechazar RUC con longitud incorrecta"); + } + + @Test + public void testHelperMethods() { + DespatchAdvice remitente = DespatchAdvice.builder().tipoComprobante("09").build(); + assertTrue(remitente.isGRERemitente()); + assertFalse(remitente.isGRETransportista()); + + DespatchAdvice transportista = DespatchAdvice.builder().tipoComprobante("31").build(); + assertFalse(transportista.isGRERemitente()); + assertTrue(transportista.isGRETransportista()); + } +} diff --git a/xbuilder/core/src/test/java/unit/validator/GRERemitenteValidatorTest.java b/xbuilder/core/src/test/java/unit/validator/GRERemitenteValidatorTest.java new file mode 100644 index 00000000..fb067354 --- /dev/null +++ b/xbuilder/core/src/test/java/unit/validator/GRERemitenteValidatorTest.java @@ -0,0 +1,312 @@ +package unit.validator; + +import io.github.project.openubl.xbuilder.content.catalogs.Catalog18; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog20; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog6; +import io.github.project.openubl.xbuilder.content.models.standard.guia.*; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests unitarios para la validación de {@link GRERemitente}. + */ +public class GRERemitenteValidatorTest { + + private static GRERemitente.GRERemitenteBuilder minimalPrivado() { + return GRERemitente.builder() + .serie("T001") + .numero(1) + .remitente(Remitente.builder().ruc("12345678912").razonSocial("Test S.A.C.").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("12345678").nombre("Cliente").build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PRIVADO.getCode()) + .fechaTraslado(LocalDate.now()) + .chofer(Driver.builder() + .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("11111111") + .nombres("J").apellidos("P").licencia("Q123").build()) + .vehiculo(Vehicle.builder().placa("ABC-123").build()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()); + } + + private static GRERemitente.GRERemitenteBuilder minimalPublico() { + return GRERemitente.builder() + .serie("T001") + .numero(1) + .remitente(Remitente.builder().ruc("12345678912").razonSocial("Test S.A.C.").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20200020002").nombre("Cliente S.A.").build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PUBLICO.getCode()) + .fechaTraslado(LocalDate.now()) + .transportista(Transportista.builder() + .tipoDocumentoIdentidad("6").numeroDocumentoIdentidad("20300030003") + .nombre("Transportes S.A.C.").build()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()); + } + + @Nested + class ValidCases { + + @Test + public void testGRERemitentePrivadoValido() { + GRERemitente gre = minimalPrivado().build(); + List errors = gre.validate(); + assertTrue(errors.isEmpty(), "Debería ser válido: " + errors); + } + + @Test + public void testGRERemitentePublicoValido() { + GRERemitente gre = minimalPublico().build(); + List errors = gre.validate(); + assertTrue(errors.isEmpty(), "Debería ser válido: " + errors); + } + + @Test + public void testVehiculoM1LSinConductor() { + GRERemitente gre = GRERemitente.builder() + .serie("T001").numero(1) + .remitente(Remitente.builder().ruc("12345678912").razonSocial("Test").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("12345678").nombre("C").build()) + .envio(Envio.builder() + .tipoTraslado("01").pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("02").fechaTraslado(LocalDate.now()) + .indicador("SUNAT_Envio_IndicadorTrasladoVehiculoM1L") + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()) + .build(); + List errors = gre.validate(); + assertTrue(errors.isEmpty(), "M1/L no requiere conductor: " + errors); + } + } + + @Nested + class SerieValidation { + + @Test + public void testSerieFaltante() { + GRERemitente gre = minimalPrivado().serie(null).build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("serie"))); + } + + @Test + public void testSerieInvalidaV() { + GRERemitente gre = minimalPrivado().serie("V001").build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("inicie con 'T'"))); + } + + @Test + public void testNumeroInvalido() { + GRERemitente gre = minimalPrivado().numero(0).build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("mayor a 0"))); + } + } + + @Nested + class PartyValidation { + + @Test + public void testRemitenteFaltante() { + GRERemitente gre = minimalPrivado().remitente(null).build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("remitente"))); + } + + @Test + public void testRUCInvalido() { + GRERemitente gre = minimalPrivado() + .remitente(Remitente.builder().ruc("123").razonSocial("X").build()) + .build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("11 dígitos"))); + } + + @Test + public void testDestinatarioFaltante() { + GRERemitente gre = minimalPrivado().destinatario(null).build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("destinatario"))); + } + } + + @Nested + class TransportePrivadoValidation { + + @Test + public void testSinConductor() { + GRERemitente gre = GRERemitente.builder() + .serie("T001").numero(1) + .remitente(Remitente.builder().ruc("12345678912").razonSocial("Test").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("12345678").nombre("C").build()) + .envio(Envio.builder() + .tipoTraslado("01").pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("02").fechaTraslado(LocalDate.now()) + .vehiculo(Vehicle.builder().placa("ABC-123").build()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()) + .build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("conductor"))); + } + + @Test + public void testSinVehiculo() { + GRERemitente gre = GRERemitente.builder() + .serie("T001").numero(1) + .remitente(Remitente.builder().ruc("12345678912").razonSocial("Test").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("12345678").nombre("C").build()) + .envio(Envio.builder() + .tipoTraslado("01").pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("02").fechaTraslado(LocalDate.now()) + .chofer(Driver.builder().tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("11111111") + .nombres("J").apellidos("P").licencia("Q123").build()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()) + .build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("vehículo"))); + } + + @Test + public void testPrivadoConTransportista() { + // Transporte privado NO debe tener transportista externo + GRERemitente gre = minimalPrivado().envio(Envio.builder() + .tipoTraslado("01").pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("02").fechaTraslado(LocalDate.now()) + .chofer(Driver.builder().tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("11111111") + .nombres("J").apellidos("P").licencia("Q123").build()) + .vehiculo(Vehicle.builder().placa("ABC-123").build()) + .transportista(Transportista.builder() + .tipoDocumentoIdentidad("6").numeroDocumentoIdentidad("20300030003") + .nombre("Trans S.A.C.").build()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()).build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("Transporte privado no debe consignar transportista"))); + } + } + + @Nested + class TransportePublicoValidation { + + @Test + public void testSinTransportista() { + GRERemitente gre = GRERemitente.builder() + .serie("T001").numero(1) + .remitente(Remitente.builder().ruc("12345678912").razonSocial("Test").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("12345678").nombre("C").build()) + .envio(Envio.builder() + .tipoTraslado("01").pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("01").fechaTraslado(LocalDate.now()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()) + .build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("transportista"))); + } + } + + @Nested + class EnvioValidation { + + @Test + public void testSinEnvio() { + GRERemitente gre = minimalPrivado().envio(null).build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("envío"))); + } + + @Test + public void testSinDetalles() { + GRERemitente gre = GRERemitente.builder() + .serie("T001").numero(1) + .remitente(Remitente.builder().ruc("12345678912").razonSocial("Test").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("12345678").nombre("C").build()) + .envio(Envio.builder() + .tipoTraslado("01").pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("02").fechaTraslado(LocalDate.now()) + .chofer(Driver.builder().tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("11111111") + .nombres("J").apellidos("P").licencia("Q123").build()) + .vehiculo(Vehicle.builder().placa("ABC-123").build()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("detalle"))); + } + } + + @Nested + class ConversionTests { + + @Test + public void testToDespatchAdvice() { + GRERemitente gre = minimalPrivado().build(); + DespatchAdvice da = gre.toDespatchAdvice(); + + assertEquals("09", da.getTipoComprobante()); + assertEquals("T001", da.getSerie()); + assertEquals(1, da.getNumero()); + assertNotNull(da.getRemitente()); + assertNotNull(da.getDestinatario()); + assertNotNull(da.getEnvio()); + assertFalse(da.getDetalles().isEmpty()); + } + + @Test + public void testToDespatchAdviceValidatedSuccess() { + GRERemitente gre = minimalPrivado().build(); + DespatchAdvice da = gre.toDespatchAdviceValidated(); + assertNotNull(da); + assertTrue(da.isGRERemitente()); + } + + @Test + public void testToDespatchAdviceValidatedFails() { + GRERemitente gre = minimalPrivado().serie("V001").build(); + assertThrows(IllegalStateException.class, gre::toDespatchAdviceValidated); + } + } +} diff --git a/xbuilder/core/src/test/java/unit/validator/GRETransportistaValidatorTest.java b/xbuilder/core/src/test/java/unit/validator/GRETransportistaValidatorTest.java new file mode 100644 index 00000000..811e65e5 --- /dev/null +++ b/xbuilder/core/src/test/java/unit/validator/GRETransportistaValidatorTest.java @@ -0,0 +1,241 @@ +package unit.validator; + +import io.github.project.openubl.xbuilder.content.catalogs.Catalog18; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog20; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog6; +import io.github.project.openubl.xbuilder.content.models.standard.guia.*; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests unitarios para la validación de {@link GRETransportista}. + */ +public class GRETransportistaValidatorTest { + + private static GRETransportista.GRETransportistaBuilder minimalTransportista() { + return GRETransportista.builder() + .serie("V001") + .numero(1) + .transportistaEmisor(Transportista.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20300030003").nombre("Transportes S.A.C.").build()) + .remitente(Tercero.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20100010001").nombre("Remitente S.A.C.").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20200020002").nombre("Destino S.A.").build()) + .conductor(Driver.builder() + .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("44444444") + .nombres("M").apellidos("T").licencia("Q444").build()) + .vehiculo(Vehicle.builder().placa("XYZ-789").build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PUBLICO.getCode()) + .fechaTraslado(LocalDate.now()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()); + } + + @Nested + class ValidCases { + + @Test + public void testTransportistaValido() { + GRETransportista gre = minimalTransportista().build(); + List errors = gre.validate(); + assertTrue(errors.isEmpty(), "Debería ser válido: " + errors); + } + } + + @Nested + class SerieValidation { + + @Test + public void testSerieFaltante() { + GRETransportista gre = minimalTransportista().serie(null).build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("serie"))); + } + + @Test + public void testSerieInvalidaT() { + GRETransportista gre = minimalTransportista().serie("T001").build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("inicie con 'V'"))); + } + + @Test + public void testNumeroInvalido() { + GRETransportista gre = minimalTransportista().numero(0).build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("mayor a 0"))); + } + } + + @Nested + class PartyValidation { + + @Test + public void testTransportistaEmisorFaltante() { + GRETransportista gre = minimalTransportista().transportistaEmisor(null).build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("transportista emisor"))); + } + + @Test + public void testRUCTransportistaInvalido() { + GRETransportista gre = minimalTransportista() + .transportistaEmisor(Transportista.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("123").nombre("X").build()) + .build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("11 dígitos"))); + } + + @Test + public void testRemitenteFaltante() { + GRETransportista gre = minimalTransportista().remitente(null).build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("remitente original"))); + } + + @Test + public void testDestinatarioFaltante() { + GRETransportista gre = minimalTransportista().destinatario(null).build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("destinatario"))); + } + } + + @Nested + class ConductorVehiculoValidation { + + @Test + public void testSinConductor() { + GRETransportista gre = GRETransportista.builder() + .serie("V001").numero(1) + .transportistaEmisor(Transportista.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20300030003").nombre("T").build()) + .remitente(Tercero.builder().tipoDocumentoIdentidad("6") + .numeroDocumentoIdentidad("20100010001").nombre("R").build()) + .destinatario(Destinatario.builder().tipoDocumentoIdentidad("6") + .numeroDocumentoIdentidad("20200020002").nombre("D").build()) + // Sin conductor + .vehiculo(Vehicle.builder().placa("XYZ-789").build()) + .envio(Envio.builder() + .tipoTraslado("01").pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("01").fechaTraslado(LocalDate.now()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()) + .build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("conductor")), + "Transportista siempre requiere conductor"); + } + + @Test + public void testSinVehiculo() { + GRETransportista gre = minimalTransportista().vehiculo(null).build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("vehículo")), + "Transportista siempre requiere vehículo"); + } + } + + @Nested + class EnvioValidation { + + @Test + public void testSinEnvio() { + GRETransportista gre = minimalTransportista().envio(null).build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("envío"))); + } + + @Test + public void testSinDetalles() { + GRETransportista gre = GRETransportista.builder() + .serie("V001").numero(1) + .transportistaEmisor(Transportista.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20300030003").nombre("T").build()) + .remitente(Tercero.builder().tipoDocumentoIdentidad("6") + .numeroDocumentoIdentidad("20100010001").nombre("R").build()) + .destinatario(Destinatario.builder().tipoDocumentoIdentidad("6") + .numeroDocumentoIdentidad("20200020002").nombre("D").build()) + .conductor(Driver.builder().tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("44444444") + .nombres("M").apellidos("T").licencia("Q444").build()) + .vehiculo(Vehicle.builder().placa("XYZ-789").build()) + .envio(Envio.builder() + .tipoTraslado("01").pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("01").fechaTraslado(LocalDate.now()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .build(); + List errors = gre.validate(); + assertTrue(errors.stream().anyMatch(e -> e.contains("detalle"))); + } + } + + @Nested + class ConversionTests { + + @Test + public void testToDespatchAdvice() { + GRETransportista gre = minimalTransportista().build(); + DespatchAdvice da = gre.toDespatchAdvice(); + + assertEquals("31", da.getTipoComprobante()); + assertEquals("V001", da.getSerie()); + assertEquals(1, da.getNumero()); + assertTrue(da.isGRETransportista()); + assertNotNull(da.getTercero(), "Tercero debe estar presente"); + assertNotNull(da.getEnvio().getChoferes(), "Conductores deben inyectarse en envío"); + assertFalse(da.getEnvio().getChoferes().isEmpty()); + assertNotNull(da.getEnvio().getVehiculo(), "Vehículo debe inyectarse en envío"); + } + + @Test + public void testConductoresInyectadosEnEnvio() { + GRETransportista gre = minimalTransportista() + .conductor(Driver.builder() + .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("99999999") + .nombres("Extra").apellidos("Driver").licencia("Q999").build()) + .build(); + DespatchAdvice da = gre.toDespatchAdvice(); + assertEquals(2, da.getEnvio().getChoferes().size(), + "Ambos conductores deben inyectarse en el envío"); + } + + @Test + public void testToDespatchAdviceValidatedSuccess() { + GRETransportista gre = minimalTransportista().build(); + DespatchAdvice da = gre.toDespatchAdviceValidated(); + assertNotNull(da); + assertTrue(da.isGRETransportista()); + } + + @Test + public void testToDespatchAdviceValidatedFails() { + GRETransportista gre = minimalTransportista().serie("T001").build(); + assertThrows(IllegalStateException.class, gre::toDespatchAdviceValidated); + } + } +} diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/exportacionSinDAM.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/exportacionSinDAM.xml new file mode 100644 index 00000000..7816e40e --- /dev/null +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/exportacionSinDAM.xml @@ -0,0 +1,111 @@ + + + + + + + + 2.1 + 2.0 + T001-201 + 2019-12-24 + 09 + + + 20500050005 + + + 20500050005 + + + + + + + + #PROJECT-OPENUBL-SIGN + + + + + 20500050005 + + + 20500050005 + + + + + + + + + + 20500050005 + + + + + + + + SUNAT_Envio + 13 + Traslado para exportación sin DAM numerada + 2000.000 + + 02 + + 2019-12-24 + + + 12345678 + Pedro + Gonzales + Principal + + Q9876543 + + + + + + 070101 + + Depósito Temporal para exportación + + + + + 150101 + + Planta de producción + + + + + + + ABC-123 + + + + + 1 + 100.00 + + 1 + + + + + EXPORT-001 + + + + diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/importacionDAMTotal.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/importacionDAMTotal.xml new file mode 100644 index 00000000..f61cdb24 --- /dev/null +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/importacionDAMTotal.xml @@ -0,0 +1,130 @@ + + + + + + + + 2.1 + 2.0 + T001-200 + 2019-12-24 + 09 + + 118-2024-10-000123 + 50 + + + 20100010001 + + + + + 20100010001 + + + 20100010001 + + + + + + + + #PROJECT-OPENUBL-SIGN + + + + + 20100010001 + + + 20100010001 + + + + + + + + + + 20100010001 + + + + + + + + SUNAT_Envio + 10 + 5000.000 + SUNAT_Envio_IndicadorTrasladoTotalDAMDS + + 01 + + 2019-12-24 + + + + 20300030003 + + + + MTC-123456 + + + + + + 150101 + + Almacen Deposito Temporal S.A. + + + + + 070101 + + Terminal Portuario del Callao + + + + + + + CONT-2024-001 + PREC-001 + + + + + CONT-2024-002 + PREC-002 + + + + CALLAO + 1 + Puerto del Callao + + + + 1 + 1.00 + + 1 + + + + - + + + + diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/mercanciaExtranjera.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/mercanciaExtranjera.xml new file mode 100644 index 00000000..6a4a181c --- /dev/null +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/mercanciaExtranjera.xml @@ -0,0 +1,115 @@ + + + + + + + + 2.1 + 2.0 + T001-202 + 2019-12-24 + 09 + + 20600060006 + + + 20600060006 + + + + + + + + #PROJECT-OPENUBL-SIGN + + + + + 20600060006 + + + 20600060006 + + + + + + + + + + 20600060006 + + + + + + + + SUNAT_Envio + 19 + 10000.000 + 1 + + 01 + + 2019-12-24 + + + + 20700070007 + + + + + + + + + 070106 + + Depósito Temporal del Callao + + + + + 070101 + + Terminal Portuario del Callao + + + + + + + MSKU1234567 + SEAL-ABC123 + + + + CALLAO + 1 + Puerto del Callao + + + + 1 + 1.00 + + 1 + + + + + CONT-CONSOLIDADO + + + + diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/mercanciaExtranjeraMotivo19.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/mercanciaExtranjeraMotivo19.xml new file mode 100644 index 00000000..40c8490f --- /dev/null +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/mercanciaExtranjeraMotivo19.xml @@ -0,0 +1,121 @@ + + + + + + + + 2.1 + 2.0 + T001-7 + 2019-12-24 + 09 + + 20500050005 + + + 20500050005 + + + + + + + + #PROJECT-OPENUBL-SIGN + + + + + 20500050005 + + + 20500050005 + + + + + + + + + + 20500050005 + + + + + + + + SUNAT_Envio + 19 + 12000.000 + SUNAT_Envio_IndicadorTrasladoTotalDAMDS + + 01 + + 2019-12-24 + + + + 20600060006 + + + + + + + + + 150101 + + Almacén Depósito Temporal + + + + + 070101 + + Terminal Portuario del Callao + + + + + + + MSKU-2024-001 + SEAL-001 + + + + + MSKU-2024-002 + SEAL-002 + + + + CALLAO + 1 + Puerto del Callao + + + + 1 + 1.00 + + 1 + + + + + CONT-CONSOLIDADO + + + + diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/multiplesConductores.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/multiplesConductores.xml new file mode 100644 index 00000000..85afa4c7 --- /dev/null +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/multiplesConductores.xml @@ -0,0 +1,118 @@ + + + + + + + + 2.1 + 2.0 + T001-3 + 2019-12-24 + 09 + + 20100010001 + + + 20100010001 + + + + + + + + #PROJECT-OPENUBL-SIGN + + + + + 20100010001 + + + 20100010001 + + + + + + + + + + 20200020002 + + + + + + + + SUNAT_Envio + 04 + 1500.000 + + 02 + + 2019-12-24 + + + 11111111 + Juan + Perez + Principal + + Q1111111 + + + + 22222222 + Pedro + Gomez + Secundario + + Q2222222 + + + + + + 130101 + + Sucursal Tacna + + + + + 150101 + + Planta Principal, Lima + + + + + + + DEF-456 + + + + + 1 + 100.00 + + 1 + + + + + MAT-001 + + + + diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transportePrivadoBasico.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transportePrivadoBasico.xml new file mode 100644 index 00000000..7c908473 --- /dev/null +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transportePrivadoBasico.xml @@ -0,0 +1,109 @@ + + + + + + + + 2.1 + 2.0 + T001-1 + 2019-12-24 + 09 + + 20100010001 + + + 20100010001 + + + + + + + + #PROJECT-OPENUBL-SIGN + + + + + 20100010001 + + + 20100010001 + + + + + + + + + + 20200020002 + + + + + + + + SUNAT_Envio + 01 + 50.000 + + 02 + + 2019-12-24 + + + 12345678 + Carlos + Ramirez + Principal + + Q1234567 + + + + + + 150102 + + Jr. Comercio 789, Rímac + + + + + 150101 + + Av. Industrial 456, Lima + + + + + + + ABC-123 + + + + + 1 + 10.00 + + 1 + + + + + PROD-001 + + + + diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transportePublico.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transportePublico.xml new file mode 100644 index 00000000..8fa0c8b9 --- /dev/null +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transportePublico.xml @@ -0,0 +1,104 @@ + + + + + + + + 2.1 + 2.0 + T001-2 + 2019-12-24 + 09 + + 20100010001 + + + 20100010001 + + + + + + + + #PROJECT-OPENUBL-SIGN + + + + + 20100010001 + + + 20100010001 + + + + + + + + + + 20200020002 + + + + + + + + SUNAT_Envio + 01 + 200.000 + + 01 + + 2019-12-24 + + + + 20300030003 + + + + MTC-001234 + + + + + + 040101 + + Sucursal Arequipa + + + + + 150101 + + Almacén Central, Lima + + + + + + + 1 + 50.00 + + 1 + + + + + PROD-002 + + + + diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transporteSubcontratado.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transporteSubcontratado.xml new file mode 100644 index 00000000..adf1b78c --- /dev/null +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transporteSubcontratado.xml @@ -0,0 +1,105 @@ + + + + + + + + 2.1 + 2.0 + T001-5 + 2019-12-24 + 09 + + 20100010001 + + + 20100010001 + + + + + + + + #PROJECT-OPENUBL-SIGN + + + + + 20100010001 + + + 20100010001 + + + + + + + + + + 20200020002 + + + + + + + + SUNAT_Envio + 01 + 3000.000 + + 01 + + 2019-12-24 + + + + 20400040004 + + + + MTC-567890 + + + + + + 150132 + + Tienda San Isidro + + + + + 150101 + 0001 + + Almacén Principal, Lima + + + + + + + 1 + 200.00 + + 1 + + + + + ELEC-001 + + + + diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/vehiculoConCarreta.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/vehiculoConCarreta.xml new file mode 100644 index 00000000..dfa18926 --- /dev/null +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/vehiculoConCarreta.xml @@ -0,0 +1,124 @@ + + + + + + + + 2.1 + 2.0 + T001-4 + 2019-12-24 + 09 + + 20100010001 + + + 20100010001 + + + + + + + + #PROJECT-OPENUBL-SIGN + + + + + 20100010001 + + + 20100010001 + + + + + + + + + + 20200020002 + + + + + + + + SUNAT_Envio + 01 + 8000.000 + + 02 + + 2019-12-24 + + + 33333333 + Roberto + Silva + Principal + + Q3333333 + + + + + + 060101 + + Depósito Cajamarca + + + + + 150101 + + Centro de distribución, Lima + + + + + + + GHI-789 + + TUC-MAIN + + + CAR-001 + + TUC-SEC1 + + + + CAR-002 + + + AUTH-001 + + + + + + 1 + 500.00 + + 1 + + + + + FERT-001 + + + + diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/vehiculoM1L.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/vehiculoM1L.xml new file mode 100644 index 00000000..d5bed493 --- /dev/null +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/vehiculoM1L.xml @@ -0,0 +1,96 @@ + + + + + + + + 2.1 + 2.0 + T001-6 + 2019-12-24 + 09 + + 20100010001 + + + 20100010001 + + + + + + + + #PROJECT-OPENUBL-SIGN + + + + + 20100010001 + + + 20100010001 + + + + + + + + + + 87654321 + + + + + + + + SUNAT_Envio + 01 + 5.000 + SUNAT_Envio_IndicadorTrasladoVehiculoM1L + + 02 + + 2019-12-24 + + + + + 150101 + + Domicilio Cliente + + + + + 150101 + + Tienda Lima Centro + + + + + + + 1 + 1.00 + + 1 + + + + + PEQ-001 + + + + diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaBasico.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaBasico.xml new file mode 100644 index 00000000..5c5ee363 --- /dev/null +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaBasico.xml @@ -0,0 +1,123 @@ + + + + + + + + 2.1 + 2.0 + V001-1 + 2019-12-24 + 31 + + 20300030003 + + + 20300030003 + + + + + + + + #PROJECT-OPENUBL-SIGN + + + + + 20300030003 + + + 20300030003 + + + + MTC-001234 + + + + + + + 20200020002 + + + + + + + + + + 20100010001 + + + + + + + + SUNAT_Envio + 01 + 300.000 + + 01 + + 2019-12-24 + + + 44444444 + Miguel + Torres + Principal + + Q4444444 + + + + + + 040101 + + Sucursal Arequipa + + + + + 150101 + + Almacén Remitente, Lima + + + + + + + JKL-012 + + TUC-JKL + + + + + + 1 + 25.00 + + 1 + + + + + MER-001 + + + + diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaComercioExterior.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaComercioExterior.xml new file mode 100644 index 00000000..157e2198 --- /dev/null +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaComercioExterior.xml @@ -0,0 +1,135 @@ + + + + + + + + 2.1 + 2.0 + V001-4 + 2019-12-24 + 31 + + 20700070007 + + + 20700070007 + + + + + + + + #PROJECT-OPENUBL-SIGN + + + + + 20700070007 + + + 20700070007 + + + + MTC-PORT-001 + + + + + + + 20500050005 + + + + + + + + + + 20500050005 + + + + + + + + SUNAT_Envio + 19 + 15000.000 + SUNAT_Envio_IndicadorTrasladoTotalDAMDS + + 01 + + 2019-12-24 + + + 88888888 + Fernando + Ruiz + Principal + + Q8888888 + + + + + + 150101 + + Almacén Lima + + + + + 070101 + + Terminal Portuario del Callao + + + + + + + CONT-IMP-001 + SEAL-IMP-001 + + + + + STU-901 + + TUC-STU + + + + + CALLAO + 1 + Puerto del Callao + + + + 1 + 1.00 + + 1 + + + + + CARGA-EXT-001 + + + + diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaConCarreta.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaConCarreta.xml new file mode 100644 index 00000000..d8c01686 --- /dev/null +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaConCarreta.xml @@ -0,0 +1,133 @@ + + + + + + + + 2.1 + 2.0 + V001-3 + 2019-12-24 + 31 + + 20300030003 + + + 20300030003 + + + + + + + + #PROJECT-OPENUBL-SIGN + + + + + 20300030003 + + + 20300030003 + + + + MTC-PESADOS + + + + + + + 20200020002 + + + + + + + + + + 20100010001 + + + + + + + + SUNAT_Envio + 01 + 20000.000 + 1 + + 01 + + 2019-12-24 + + + 77777777 + Andres + Quispe + Principal + + Q7777777 + + + + + + 060101 + + Planta procesadora, Cajamarca + + + + + 040101 + + Mina del Sur, Arequipa + + + + + + + PQR-678 + + TUC-PQR + + + REM-001 + + TUC-REM + + + + AUTH-PESADOS + + + + + + 1 + 20000.00 + + 1 + + + + + MIN-001 + + + + diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaMultiplesConductores.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaMultiplesConductores.xml new file mode 100644 index 00000000..13a1d414 --- /dev/null +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaMultiplesConductores.xml @@ -0,0 +1,132 @@ + + + + + + + + 2.1 + 2.0 + V001-2 + 2019-12-24 + 31 + + 20300030003 + + + 20300030003 + + + + + + + + #PROJECT-OPENUBL-SIGN + + + + + 20300030003 + + + 20300030003 + + + + MTC-001234 + + + + + + + 20200020002 + + + + + + + + + + 20100010001 + + + + + + + + SUNAT_Envio + 01 + 5000.000 + + 01 + + 2019-12-24 + + + 55555555 + Luis + Fernandez + Principal + + Q5555555 + + + + 66666666 + Mario + Vargas + Secundario + + Q6666666 + + + + + + 130101 + + Terminal Tacna + + + + + 150101 + + Centro de Carga Lima + + + + + + + MNO-345 + + TUC-MNO + + + + + + 1 + 300.00 + + 1 + + + + + CARG-001 + + + + diff --git a/xbuilder/core/src/test/resources/e2e/renderer/summarydocuments/SummaryDocumentsTest/summaryDocuments.xml b/xbuilder/core/src/test/resources/e2e/renderer/summarydocuments/SummaryDocumentsTest/summaryDocuments.xml index 94e5c73d..7a1eded6 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/summarydocuments/SummaryDocumentsTest/summaryDocuments.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/summarydocuments/SummaryDocumentsTest/summaryDocuments.xml @@ -65,6 +65,7 @@ 18 + 18.00 1000 IGV @@ -114,6 +115,7 @@ 18 + 18.00 1000 IGV diff --git a/xbuilder/core/src/test/resources/e2e/renderer/summarydocuments/SummaryDocumentsTest/summaryDocuments_anularBoletaExonerada.xml b/xbuilder/core/src/test/resources/e2e/renderer/summarydocuments/SummaryDocumentsTest/summaryDocuments_anularBoletaExonerada.xml index daead507..29d3c279 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/summarydocuments/SummaryDocumentsTest/summaryDocuments_anularBoletaExonerada.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/summarydocuments/SummaryDocumentsTest/summaryDocuments_anularBoletaExonerada.xml @@ -61,6 +61,7 @@ 0 + 18.00 1000 IGV From b578bdbe5b93f5f2c9b21909108e41ffc92ad765 Mon Sep 17 00:00:00 2001 From: Edwin Luis Barboza Pinedo Date: Sat, 28 Mar 2026 15:01:41 -0500 Subject: [PATCH 2/8] fix: add missing Percent element in TaxSubtotal for DespatchAdvice Signed-off-by: Edwin Luis Barboza Pinedo --- .../quarkus/xbuilder/it/QuarkusXbuilderResourceTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xbuilder/quarkus-extension/integration-tests/src/test/java/io/github/project/openubl/quarkus/xbuilder/it/QuarkusXbuilderResourceTest.java b/xbuilder/quarkus-extension/integration-tests/src/test/java/io/github/project/openubl/quarkus/xbuilder/it/QuarkusXbuilderResourceTest.java index 74018cf0..568b5f54 100644 --- a/xbuilder/quarkus-extension/integration-tests/src/test/java/io/github/project/openubl/quarkus/xbuilder/it/QuarkusXbuilderResourceTest.java +++ b/xbuilder/quarkus-extension/integration-tests/src/test/java/io/github/project/openubl/quarkus/xbuilder/it/QuarkusXbuilderResourceTest.java @@ -832,6 +832,7 @@ public void testSummaryDocuments() { " \n" + " 18\n" + " \n" + + " 20.00\n" + " \n" + " 1000\n" + " IGV\n" + @@ -881,6 +882,7 @@ public void testSummaryDocuments() { " \n" + " 18\n" + " \n" + + " 20.00\n" + " \n" + " 1000\n" + " IGV\n" + From 86b46d71264f7b471d4bbc465054dacb1b494826 Mon Sep 17 00:00:00 2001 From: Edwin Luis Barboza Pinedo Date: Sat, 28 Mar 2026 20:24:43 -0500 Subject: [PATCH 3/8] Refactor XBuilder and XSender configurations to use ConfigMapping; update dependencies and Java version - Changed XBuilderConfig from a class to an interface and applied ConfigMapping for better configuration handling. - Updated method calls in DefaultXBuilder to use the new configuration method signatures. - Refactored XSenderConfig to use ConfigMapping and added a default value for enableLoggingFeature. - Updated dependency versions in xsender/core/pom.xml for commons-codec, junit-jupiter-engine, mockito-core, and cxf-codegen-plugin. - Changed Java version in xsender/spring-boot-extension/integration-tests/pom.xml to 21 for compatibility with newer features. Signed-off-by: Edwin Luis Barboza Pinedo --- .github/workflows/ci.yml | 14 +- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/sonar.yml | 2 +- README.md | 245 +- examples/springbot/pom.xml | 12 +- examples/tomcat/pom.xml | 8 +- pom.xml | 14 +- xbuilder/core/pom.xml | 24 +- .../models/standard/general/CreditNote.java | 2 - .../models/standard/general/DebitNote.java | 2 - .../models/standard/general/Invoice.java | 2 - .../models/standard/guia/DespatchAdvice.java | 2 - .../standard/guia/DespatchAdviceItem.java | 2 - .../standard/guia/GuiaItemAttribute.java | 2 - .../content/models/sunat/baja/Reversion.java | 2 - .../models/sunat/baja/VoidedDocuments.java | 2 - .../sunat/percepcionretencion/Perception.java | 2 - .../sunat/percepcionretencion/Retention.java | 2 - .../sunat/resumen/SummaryDocuments.java | 2 - .../core/src/test/java/e2e/AbstractTest.java | 45 +- .../integration-tests/pom.xml | 22 +- .../xbuilder/it/QuarkusXbuilderResource.java | 49 +- .../it/QuarkusXbuilderResourceTest.java | 2856 +++++++++-------- .../xbuilder/runtime/DefaultXBuilder.java | 7 +- .../xbuilder/runtime/XBuilderConfig.java | 13 +- xsender/core/pom.xml | 8 +- .../openubl/quarkus/xsender/XSender.java | 6 +- .../xsender/runtime/XSenderConfig.java | 12 +- .../integration-tests/pom.xml | 6 +- 30 files changed, 1825 insertions(+), 1544 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b46c6f14..7d0ee599 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: name: JDK ${{matrix.java-version}} JVM Tests strategy: matrix: - java-version: [ 17 ] + java-version: [ 21 ] fail-fast: false runs-on: ubuntu-latest steps: @@ -47,7 +47,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: - java-version: 17 + java-version: 21 distribution: temurin cache: maven - name: Maven tests @@ -66,14 +66,14 @@ jobs: strategy: matrix: module: [ xbuilder, xsender ] - quarkus-version: [ 3.0.4.Final, 3.2.6.Final, 3.3.3 ] + quarkus-version: [ 3.8.6, 3.12.0 ] fail-fast: false steps: - uses: actions/checkout@v4 - name: Set up JDK uses: actions/setup-java@v4 with: - java-version: 17 + java-version: 21 distribution: temurin cache: maven - name: Maven tests @@ -94,7 +94,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v4 with: - java-version: 17 + java-version: 21 distribution: temurin cache: maven - name: Maven tests @@ -107,14 +107,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - springboot-version: [ 3.0.0, 3.1.3 ] + springboot-version: [ 3.2.0, 3.3.0 ] fail-fast: false steps: - uses: actions/checkout@v4 - name: Set up JDK uses: actions/setup-java@v4 with: - java-version: 17 + java-version: 21 distribution: temurin cache: maven - name: Maven tests diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 43ff305d..2599e1f4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -43,7 +43,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v4 with: - java-version: 17 + java-version: 21 distribution: temurin # Initializes the CodeQL tools for scanning. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ee950cce..77415c72 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: ref: ${{ github.event.inputs.branch }} - uses: actions/setup-java@v4 with: - java-version: 17 + java-version: 21 distribution: temurin cache: maven - name: Set release version diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 31091fb4..afe78efd 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/setup-java@v4 with: distribution: "temurin" - java-version: 17 + java-version: 21 cache: maven - name: Build with Maven and Coverage/Sonar run: mvn verify -P coverage,sonar diff --git a/README.md b/README.md index f22134d1..6796cee9 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,27 @@ [![CI](https://github.com/project-openubl/xhandler/actions/workflows/ci.yml/badge.svg)](https://github.com/project-openubl/xhandler/actions/workflows/ci.yml) [![Project Chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg?style=for-the-badge&logo=zulip)](https://projectopenubl.zulipchat.com/) -[![Supported JVM Versions](https://img.shields.io/badge/JVM--17-brightgreen.svg?style=for-the-badge&logo=Java)](https://github.com/project-openubl/xhandler/actions/) +[![Supported JVM Versions](https://img.shields.io/badge/JVM--21-brightgreen.svg?style=for-the-badge&logo=Java)](https://github.com/project-openubl/xhandler/actions/) -**XHandler Java** es una suite de herramientas diseñada para facilitar la integración de **Facturación Electrónica en Perú (SUNAT)** en aplicaciones Java. Este repositorio es un "monorepo" que alberga las librerías `XBuilder` y `XSender`, proporcionando una solución integral para crear, firmar y enviar comprobantes de pago electrónicos. +Suite de librerías Java para **Facturación Electrónica en Perú (SUNAT)**: creación, firma y envío de comprobantes de pago electrónicos conforme a UBL 2.1. > [!TIP] -> Si buscas integrar facturación electrónica de manera rápida y estándar, estás en el lugar correcto. +> Si buscas integrar facturación electrónica SUNAT de manera rápida y estándar, estás en el lugar correcto. --- -## 📦 Ecosistema +## Requisitos -El proyecto se divide en módulos principales y extensiones para frameworks populares: +| Requisito | Versión mínima | +|--------------|----------------| +| **Java** | 21 | +| **Maven** | 3.8+ | +| **Quarkus** | 3.8+ (extensiones) | +| **Spring Boot** | 3.2+ (extensión) | + +--- + +## Ecosistema | Componente | Descripción | Maven Central | |------------|-------------|---------------| @@ -27,70 +36,216 @@ El proyecto se divide en módulos principales y extensiones para frameworks popu --- -## 🛠️ XBuilder +## Inicio rápido + +### 1. Agregar dependencia + +```xml + + io.github.project-openubl + xbuilder + LATEST + +``` -XBuilder abstrae la complejidad de los estándares UBL y XML, permitiéndote construir documentos tributarios válidos escribiendo código Java simple. +### 2. Crear una Factura -### Características -- **Simple**: No necesitas manipular XML directamente ni conciliar namespaces complejos. -- **Completo**: Soporte para Facturas, Boletas, Notas de Crédito/Débito, Guías de Remisión y Percepciones/Retenciones. -- **Validado**: Realiza cálculos automáticos y validaciones básicas según normativa SUNAT. +```java +var invoice = Invoice.builder() + .serie("F001") + .numero(1) + .proveedor(Proveedor.builder() + .ruc("20123456789") + .razonSocial("Mi Empresa S.A.C.") + .build()) + .cliente(Cliente.builder() + .nombre("Cliente Ejemplo") + .numeroDocumentoIdentidad("10467793549") + .tipoDocumentoIdentidad(Catalog6.RUC.toString()) + .build()) + .detalle(DocumentoVentaDetalle.builder() + .descripcion("Servicio de consultoría") + .cantidad(new BigDecimal("1")) + .precio(new BigDecimal("500")) + .build()) + .build(); +``` -### Ejemplo de Uso +### 3. Generar XML UBL 2.1 ```java -// Ejemplo simplificado de creación de factura -Invoice invoice = Invoice.builder() - .serie("F001") - .numero(1) - .proveedor(proveedor) - .cliente(cliente) - .detalle(detalle) - .build(); - -XMLInvoice xml = new InvoiceXMLBuilder().build(invoice); +var enricher = new ContentEnricher(defaults, dateProvider); +enricher.enrich(invoice); + +Template template = TemplateProducer.getInstance().getInvoice(); +String xml = template.data(invoice).render(); ``` -> [!NOTE] -> Para actualizar los snapshots de prueba en desarrollo local, ejecuta: -> `mvn clean test -Dxbuilder.snapshot.update` +### 4. Enviar a SUNAT (con XSender) + +```xml + + io.github.project-openubl + xsender + LATEST + +``` --- -## 🚀 XSender +## Documentos soportados + +| Documento | Clase | Descripción | +|-----------|-------|-------------| +| Factura | `Invoice` | Ventas gravadas, exoneradas, inafectas, gratuitas | +| Boleta de Venta | `Invoice` | Con `tipoComprobante = "03"` | +| Nota de Crédito | `CreditNote` | Anulaciones, descuentos, devoluciones | +| Nota de Débito | `DebitNote` | Intereses, penalidades, ajustes | +| Guía de Remisión | `DespatchAdvice` | GRE-Remitente (09) y GRE-Transportista (31) | +| Resumen Diario | `SummaryDocuments` | Informar emisión de boletas | +| Comunicación de Baja | `VoidedDocuments` | Anular comprobantes emitidos | +| Percepción | `Perception` | Comprobantes de percepción | +| Retención | `Retention` | Comprobantes de retención | +| Reversión | `Reversion` | Anular percepciones/retenciones | -XSender se encarga de la comunicación con los servicios SOAP de la SUNAT o de los Operadores de Servicios Electrónicos (OSE). +--- -### Características -- **Compatible**: Soporta los diversos endpoints de SUNAT (Beta/Producción) y OSEs. -- **Resiliente**: Gestiona el envío de archivos ZIP y el procesamiento de respuestas (CDR, Tickets). -- **Flexible**: Fácil integración con frameworks modernos como Quarkus y Spring Boot. +## Extensiones para frameworks + +### Quarkus + +```xml + + + io.github.project-openubl + quarkus-xbuilder + LATEST + + + + + io.github.project-openubl + quarkus-xsender + LATEST + +``` + +Configuración en `application.properties`: +```properties +quarkus.xbuilder.igv-tasa=0.18 +quarkus.xbuilder.icb-tasa=0.2 +quarkus.xsender.enable-logging-feature=false +``` + +Compilación nativa con GraalVM soportada: +```bash +mvn package -Pnative +``` + +### Spring Boot + +```xml + + io.github.project-openubl + spring-boot-xsender + LATEST + +``` --- -## 💻 Ejemplos +## Estructura del proyecto -Explora la carpeta `examples/` para ver implementaciones de referencia: +``` +xhandler-java/ +├── xbuilder/ +│ ├── core/ # Librería principal XBuilder +│ └── quarkus-extension/ # Extensión Quarkus para XBuilder +│ ├── deployment/ +│ ├── runtime/ +│ └── integration-tests/ +├── xsender/ +│ ├── core/ # Librería principal XSender +│ ├── quarkus-extension/ # Extensión Quarkus para XSender +│ └── spring-boot-extension/ # Starter Spring Boot para XSender +└── examples/ + ├── xbuilder/ # Ejemplo standalone XBuilder + ├── xsender/ # Ejemplo standalone XSender + ├── springbot/ # Integración Spring Boot + ├── tomcat/ # Despliegue en Tomcat + └── wildfly/ # Despliegue en WildFly +``` + +--- + +## Dependencias principales + +| Librería | Versión | Uso | +|----------|---------|-----| +| Fastjson2 | 2.0.49 | Serialización JSON | +| Quarkus Qute | 3.15.1 | Plantillas XML | +| Apache Camel | 4.4.0 | Rutas SOAP (XSender) | +| Lombok | 1.18.34 | Reducción de boilerplate | +| MapStruct | 1.5.5 | Mapeo XML-JAXB a modelos | +| JAXB | 4.0.5 | Parsing XML UBL | + +--- + +## Desarrollo local + +### Compilar y ejecutar tests + +```bash +# Tests unitarios (xbuilder + xsender) +mvn clean verify -- [**Spring Boot**](./examples/springbot): Ejemplo de integración completa usando Spring Boot. -- [**Wildfly**](./examples/wildfly): Ejemplo para servidores de aplicaciones Jakarta EE. -- [**Tomcat**](./examples/tomcat): Ejemplo ligero desplegable en Tomcat. -- [**XBuilder/XSender**](./examples): Ejemplos "standalone" de uso de las librerías. +# Solo xbuilder core (278 tests) +cd xbuilder/core && mvn test -DskipSunat=true + +# Extensión Quarkus (JVM) +mvn clean install -f xbuilder/quarkus-extension/integration-tests/ + +# Extensión Quarkus (nativa GraalVM) +mvn -Pnative-image install -f xbuilder/quarkus-extension/integration-tests/ + +# Spring Boot +mvn clean install -f xsender/spring-boot-extension/integration-tests/ +``` + +### Actualizar snapshots de test + +```bash +mvn clean test -Dxbuilder.snapshot.update +``` + +### Matriz de compatibilidad CI + +El CI valida automáticamente estas combinaciones: + +| Framework | Versiones probadas | +|-----------|--------------------| +| Quarkus | 3.8.6, 3.12.0, 3.15.1 (actual) | +| Spring Boot | 3.2.0, 3.2.5 (actual), 3.3.0 | --- -## 📚 Documentación +## Contribuir + +1. Fork del repositorio +2. Crear branch: `git checkout -b feature/mi-mejora` +3. Hacer commit: `git commit -m 'Agregar mejora'` +4. Push: `git push origin feature/mi-mejora` +5. Abrir Pull Request -Para guías detalladas, referencia de API y tutoriales, consulta nuestra documentación oficial. +### Comunidad -- 📖 **Sitio Web**: [project-openubl.github.io](https://project-openubl.github.io) -- 💬 **Comunidad**: [Únete al chat en Zulip](https://projectopenubl.zulipchat.com/) -- 🐛 **Soporte**: [Reportar un problema o discutir mejoras](https://github.com/project-openubl/xsender/discussions) +- [Chat en Zulip](https://projectopenubl.zulipchat.com/) +- [Reportar problemas](https://github.com/project-openubl/xsender/discussions) --- -## 📄 Licencia +## Licencia -Este proyecto se distribuye bajo la licencia **Apache 2.0**. Consulta el archivo [LICENSE](LICENSE) para más detalles. +Distribuido bajo licencia [Apache 2.0](LICENSE). -Copyright © Project OpenUBL. +Copyright Project OpenUBL. diff --git a/examples/springbot/pom.xml b/examples/springbot/pom.xml index 22bfe55d..009399b1 100644 --- a/examples/springbot/pom.xml +++ b/examples/springbot/pom.xml @@ -15,9 +15,9 @@ UTF-8 - 3.0.0-M4 + 3.3.1 UTF-8 - 2.7.8 + 3.2.5 @@ -32,7 +32,7 @@ org.apache.camel.springboot camel-spring-boot-bom - 3.20.3 + 4.4.0 pom import @@ -91,9 +91,9 @@ maven-compiler-plugin 3.13.0 - 11 - 11 - 11 + 21 + 21 + 21 diff --git a/examples/tomcat/pom.xml b/examples/tomcat/pom.xml index 058e7588..d137f50e 100644 --- a/examples/tomcat/pom.xml +++ b/examples/tomcat/pom.xml @@ -17,7 +17,7 @@ UTF-8 - 3.0.0 + 3.3.1 UTF-8 9x @@ -76,9 +76,9 @@ maven-compiler-plugin 3.13.0 - 11 - 11 - 11 + 21 + 21 + 21 diff --git a/pom.xml b/pom.xml index 5d63e4b0..cf6c4fc8 100644 --- a/pom.xml +++ b/pom.xml @@ -12,9 +12,9 @@ pom - 17 - 17 - 17 + 21 + 21 + 21 UTF-8 UTF-8 @@ -26,13 +26,13 @@ ${basedir} - 1.18.30 + 1.18.34 1.5.5.Final 2.10.0 - 3.4.1 - 4.0.0 - 3.0.6 + 3.15.1 + 4.4.0 + 3.2.5 https://project-openubl.github.io/ diff --git a/xbuilder/core/pom.xml b/xbuilder/core/pom.xml index 410fa797..3c790f9c 100644 --- a/xbuilder/core/pom.xml +++ b/xbuilder/core/pom.xml @@ -36,27 +36,21 @@ - com.fasterxml.jackson.core - jackson-databind - 2.14.2 + com.alibaba.fastjson2 + fastjson2 + 2.0.49 - com.fasterxml.jackson.dataformat - jackson-dataformat-yaml - 2.14.2 - test - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - 2.14.2 + org.yaml + snakeyaml + 2.2 test io.swagger.core.v3 swagger-annotations - 2.2.15 + 2.2.22 @@ -94,13 +88,13 @@ org.junit.jupiter junit-jupiter-engine - 5.10.0 + 5.10.3 test org.junit.jupiter junit-jupiter-params - 5.10.0 + 5.10.3 test diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/CreditNote.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/CreditNote.java index 27b47a83..9a37aae4 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/CreditNote.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/CreditNote.java @@ -4,14 +4,12 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; -import lombok.extern.jackson.Jacksonized; /** * Clase base para CreditNote y DebitNote. * * @author Carlos Feria */ -@Jacksonized @Data @SuperBuilder @NoArgsConstructor diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/DebitNote.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/DebitNote.java index b37c2000..b873532c 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/DebitNote.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/DebitNote.java @@ -4,9 +4,7 @@ import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; -import lombok.extern.jackson.Jacksonized; -@Jacksonized @Data @SuperBuilder @NoArgsConstructor diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Invoice.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Invoice.java index 63257183..8160f6d5 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Invoice.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Invoice.java @@ -8,12 +8,10 @@ import lombok.Singular; import lombok.ToString; import lombok.experimental.SuperBuilder; -import lombok.extern.jackson.Jacksonized; import java.time.LocalDate; import java.util.List; -@Jacksonized @Data @SuperBuilder @NoArgsConstructor diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdvice.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdvice.java index ba4f8ac6..66da1a43 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdvice.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdvice.java @@ -9,7 +9,6 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.Singular; -import lombok.extern.jackson.Jacksonized; import java.time.LocalDate; import java.time.LocalTime; @@ -36,7 +35,6 @@ * exterior), * RS 000133-2025/SUNAT (prórroga hasta 01-jul-2026). */ -@Jacksonized @Data @Builder @NoArgsConstructor diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdviceItem.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdviceItem.java index 6184d717..681b7946 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdviceItem.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdviceItem.java @@ -5,12 +5,10 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.Singular; -import lombok.extern.jackson.Jacksonized; import java.math.BigDecimal; import java.util.List; -@Jacksonized @Data @Builder @NoArgsConstructor diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GuiaItemAttribute.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GuiaItemAttribute.java index d107dbee..9ceae0fb 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GuiaItemAttribute.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GuiaItemAttribute.java @@ -4,9 +4,7 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import lombok.extern.jackson.Jacksonized; -@Jacksonized @Data @Builder @NoArgsConstructor diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/baja/Reversion.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/baja/Reversion.java index 57fa796c..413f60d9 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/baja/Reversion.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/baja/Reversion.java @@ -5,9 +5,7 @@ import lombok.NoArgsConstructor; import lombok.ToString; import lombok.experimental.SuperBuilder; -import lombok.extern.jackson.Jacksonized; -@Jacksonized @Data @SuperBuilder @NoArgsConstructor diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/baja/VoidedDocuments.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/baja/VoidedDocuments.java index 71705892..cdd9e31a 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/baja/VoidedDocuments.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/baja/VoidedDocuments.java @@ -9,11 +9,9 @@ import lombok.Singular; import lombok.ToString; import lombok.experimental.SuperBuilder; -import lombok.extern.jackson.Jacksonized; import java.util.List; -@Jacksonized @Data @SuperBuilder @NoArgsConstructor diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/percepcionretencion/Perception.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/percepcionretencion/Perception.java index c394f87e..0f043512 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/percepcionretencion/Perception.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/percepcionretencion/Perception.java @@ -6,11 +6,9 @@ import lombok.NoArgsConstructor; import lombok.ToString; import lombok.experimental.SuperBuilder; -import lombok.extern.jackson.Jacksonized; import java.math.BigDecimal; -@Jacksonized @Data @SuperBuilder @NoArgsConstructor diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/percepcionretencion/Retention.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/percepcionretencion/Retention.java index a6554abc..5d1df7ba 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/percepcionretencion/Retention.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/percepcionretencion/Retention.java @@ -6,11 +6,9 @@ import lombok.NoArgsConstructor; import lombok.ToString; import lombok.experimental.SuperBuilder; -import lombok.extern.jackson.Jacksonized; import java.math.BigDecimal; -@Jacksonized @Data @SuperBuilder @NoArgsConstructor diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/resumen/SummaryDocuments.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/resumen/SummaryDocuments.java index 2ec38cc8..aa6edf1d 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/resumen/SummaryDocuments.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/sunat/resumen/SummaryDocuments.java @@ -9,11 +9,9 @@ import lombok.Singular; import lombok.ToString; import lombok.experimental.SuperBuilder; -import lombok.extern.jackson.Jacksonized; import java.util.List; -@Jacksonized @Data @SuperBuilder @NoArgsConstructor diff --git a/xbuilder/core/src/test/java/e2e/AbstractTest.java b/xbuilder/core/src/test/java/e2e/AbstractTest.java index b32003a8..c89223b2 100644 --- a/xbuilder/core/src/test/java/e2e/AbstractTest.java +++ b/xbuilder/core/src/test/java/e2e/AbstractTest.java @@ -1,11 +1,7 @@ package e2e; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; -import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONWriter; import e2e.renderer.XMLAssertUtils; import io.github.project.openubl.xbuilder.content.jaxb.mappers.CreditNoteMapper; import io.github.project.openubl.xbuilder.content.jaxb.mappers.DebitNoteMapper; @@ -41,8 +37,11 @@ import io.quarkus.qute.Template; import org.mapstruct.factory.Mappers; import org.xml.sax.InputSource; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; import jakarta.xml.bind.JAXBContext; +import java.io.FileWriter; import java.io.IOException; import java.io.StringReader; import java.math.BigDecimal; @@ -51,6 +50,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDate; +import java.util.LinkedHashMap; import java.util.Map; public class AbstractTest { @@ -73,29 +73,36 @@ public class AbstractTest { protected static final DateProvider dateProvider = () -> LocalDate.of(2019, 12, 24); - public YAMLMapper getYamlMapper() { - YAMLMapper mapper = new YAMLMapper(new YAMLFactory()); - mapper.registerModule(new JavaTimeModule()); - mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); - mapper.configure(YAMLGenerator.Feature.LITERAL_BLOCK_STYLE, true); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - return mapper; + private Yaml createYaml() { + var options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setDefaultScalarStyle(DumperOptions.ScalarStyle.LITERAL); + options.setPrettyFlow(true); + return new Yaml(options); } + @SuppressWarnings("unchecked") public void writeYaml(String kind, Object input, String snapshotFilename) throws URISyntaxException, IOException { - String rootDir = getClass().getName().replaceAll("\\.", "/"); + String rootDir = getClass().getName().replace('.', '/'); String snapshotFileContent = Files.readString( Paths.get(getClass().getClassLoader().getResource(rootDir + "/" + snapshotFilename).toURI())); Path directoryPath = Paths.get("../quarkus-extension/integration-tests/src/test/resources").resolve(rootDir); Files.createDirectories(directoryPath); - Path filePath = directoryPath.resolve(snapshotFilename.replaceAll(".xml", "") + ".yaml"); + Path filePath = directoryPath.resolve(snapshotFilename.replace(".xml", "") + ".yaml"); - getYamlMapper().writeValue(filePath.toFile(), Map.of( - "kind", kind, - "input", input, - "snapshot", snapshotFileContent)); + String jsonStr = JSON.toJSONString(input, JSONWriter.Feature.NotWriteDefaultValue); + Map inputMap = JSON.parseObject(jsonStr, LinkedHashMap.class); + + Map data = new LinkedHashMap<>(); + data.put("kind", kind); + data.put("input", inputMap); + data.put("snapshot", snapshotFileContent); + + try (var writer = new FileWriter(filePath.toFile())) { + createYaml().dump(data, writer); + } } protected void assertInput(Invoice input, String snapshotFilename) throws Exception { diff --git a/xbuilder/quarkus-extension/integration-tests/pom.xml b/xbuilder/quarkus-extension/integration-tests/pom.xml index 80076e28..cef766ca 100644 --- a/xbuilder/quarkus-extension/integration-tests/pom.xml +++ b/xbuilder/quarkus-extension/integration-tests/pom.xml @@ -16,31 +16,25 @@ - com.fasterxml.jackson.core - jackson-databind - 2.14.2 + org.yaml + snakeyaml + 2.2 test - com.fasterxml.jackson.dataformat - jackson-dataformat-yaml - 2.14.2 - test - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - 2.14.2 + com.alibaba.fastjson2 + fastjson2 + 2.0.49 test io.quarkus - quarkus-resteasy-qute + quarkus-resteasy-jackson io.quarkus - quarkus-jackson + quarkus-resteasy-qute io.github.project-openubl diff --git a/xbuilder/quarkus-extension/integration-tests/src/main/java/io/github/project/openubl/quarkus/xbuilder/it/QuarkusXbuilderResource.java b/xbuilder/quarkus-extension/integration-tests/src/main/java/io/github/project/openubl/quarkus/xbuilder/it/QuarkusXbuilderResource.java index 9ab218ba..d1996a42 100644 --- a/xbuilder/quarkus-extension/integration-tests/src/main/java/io/github/project/openubl/quarkus/xbuilder/it/QuarkusXbuilderResource.java +++ b/xbuilder/quarkus-extension/integration-tests/src/main/java/io/github/project/openubl/quarkus/xbuilder/it/QuarkusXbuilderResource.java @@ -28,7 +28,6 @@ import io.github.project.openubl.xbuilder.content.models.sunat.resumen.SummaryDocuments; import io.github.project.openubl.xbuilder.enricher.ContentEnricher; import io.quarkus.qute.Template; -import io.vertx.core.json.JsonObject; import org.mapstruct.factory.Mappers; import org.xml.sax.InputSource; @@ -72,15 +71,15 @@ public class QuarkusXbuilderResource { private static final CreditNoteMapper creditNoteMapper = Mappers.getMapper(CreditNoteMapper.class); private static final DebitNoteMapper debitNoteMapper = Mappers.getMapper(DebitNoteMapper.class); private static final VoidedDocumentsMapper voidedDocumentsMapper = Mappers.getMapper(VoidedDocumentsMapper.class); - private static final SummaryDocumentsMapper summaryDocumentsMapper = Mappers.getMapper(SummaryDocumentsMapper.class); + private static final SummaryDocumentsMapper summaryDocumentsMapper = Mappers + .getMapper(SummaryDocumentsMapper.class); private static final PerceptionMapper perceptionMapper = Mappers.getMapper(PerceptionMapper.class); private static final RetentionMapper retentionMapper = Mappers.getMapper(RetentionMapper.class); private static final DespatchAdviceMapper despatchAdviceMapper = Mappers.getMapper(DespatchAdviceMapper.class); @POST @Path("Invoice/from-json") - public String createInvoiceXml(JsonObject json) { - Invoice invoice = json.mapTo(Invoice.class); + public String createInvoiceFromJson(Invoice invoice) { ContentEnricher enricher = new ContentEnricher(xBuilder.getDefaults(), () -> LocalDate.of(2022, 1, 25)); enricher.enrich(invoice); @@ -92,7 +91,7 @@ public String createInvoiceXml(JsonObject json) { @POST @Consumes(MediaType.TEXT_PLAIN) @Path("Invoice/from-xml") - public String createInvoiceXml(String xml) throws IOException, JAXBException { + public String createInvoiceFromXml(String xml) throws IOException, JAXBException { Template template = xBuilder.getTemplate(INVOICE); try (StringReader reader = new StringReader(xml)) { @@ -106,8 +105,7 @@ public String createInvoiceXml(String xml) throws IOException, JAXBException { @POST @Path("CreditNote/from-json") - public String createCreditNote(JsonObject json) { - CreditNote creditNote = json.mapTo(CreditNote.class); + public String createCreditNoteFromJson(CreditNote creditNote) { ContentEnricher enricher = new ContentEnricher(xBuilder.getDefaults(), () -> LocalDate.of(2022, 1, 25)); enricher.enrich(creditNote); @@ -119,7 +117,7 @@ public String createCreditNote(JsonObject json) { @POST @Consumes(MediaType.TEXT_PLAIN) @Path("CreditNote/from-xml") - public String createCreditNoteXml(String xml) { + public String createCreditNoteFromXml(String xml) { Template template = xBuilder.getTemplate(CREDIT_NOTE); try (StringReader reader = new StringReader(xml)) { @@ -133,8 +131,7 @@ public String createCreditNoteXml(String xml) { @POST @Path("DebitNote/from-json") - public String createDebitNote(JsonObject json) { - DebitNote debitNote = json.mapTo(DebitNote.class); + public String createDebitNoteFromJson(DebitNote debitNote) { ContentEnricher enricher = new ContentEnricher(xBuilder.getDefaults(), () -> LocalDate.of(2022, 1, 25)); enricher.enrich(debitNote); @@ -146,7 +143,7 @@ public String createDebitNote(JsonObject json) { @POST @Consumes(MediaType.TEXT_PLAIN) @Path("DebitNote/from-xml") - public String createDebitNoteXml(String xml) { + public String createDebitNoteFromXml(String xml) { Template template = xBuilder.getTemplate(DEBIT_NOTE); try (StringReader reader = new StringReader(xml)) { @@ -160,8 +157,7 @@ public String createDebitNoteXml(String xml) { @POST @Path("VoidedDocuments/from-json") - public String createVoidedDocuments(JsonObject json) { - VoidedDocuments voidedDocuments = json.mapTo(VoidedDocuments.class); + public String createVoidedDocumentsFromJson(VoidedDocuments voidedDocuments) { ContentEnricher enricher = new ContentEnricher(xBuilder.getDefaults(), () -> LocalDate.of(2022, 1, 25)); enricher.enrich(voidedDocuments); @@ -173,7 +169,7 @@ public String createVoidedDocuments(JsonObject json) { @POST @Consumes(MediaType.TEXT_PLAIN) @Path("VoidedDocuments/from-xml") - public String createVoidedDocumentsXml(String xml) { + public String createVoidedDocumentsFromXml(String xml) { Template template = xBuilder.getTemplate(VOIDED_DOCUMENTS); try (StringReader reader = new StringReader(xml)) { @@ -187,8 +183,7 @@ public String createVoidedDocumentsXml(String xml) { @POST @Path("SummaryDocuments/from-json") - public String createSummaryDocuments(JsonObject json) { - SummaryDocuments summaryDocuments = json.mapTo(SummaryDocuments.class); + public String createSummaryDocumentsFromJson(SummaryDocuments summaryDocuments) { ContentEnricher enricher = new ContentEnricher(xBuilder.getDefaults(), () -> LocalDate.of(2022, 1, 25)); enricher.enrich(summaryDocuments); @@ -200,7 +195,7 @@ public String createSummaryDocuments(JsonObject json) { @POST @Consumes(MediaType.TEXT_PLAIN) @Path("SummaryDocuments/from-xml") - public String createSummaryDocumentsXml(String xml) { + public String createSummaryDocumentsFromXml(String xml) { Template template = xBuilder.getTemplate(SUMMARY_DOCUMENTS); try (StringReader reader = new StringReader(xml)) { @@ -214,8 +209,7 @@ public String createSummaryDocumentsXml(String xml) { @POST @Path("Perception/from-json") - public String createPerception(JsonObject json) { - Perception perception = json.mapTo(Perception.class); + public String createPerceptionFromJson(Perception perception) { ContentEnricher enricher = new ContentEnricher(xBuilder.getDefaults(), () -> LocalDate.of(2022, 1, 25)); enricher.enrich(perception); @@ -227,7 +221,7 @@ public String createPerception(JsonObject json) { @POST @Consumes(MediaType.TEXT_PLAIN) @Path("Perception/from-xml") - public String createPerceptionXml(String xml) { + public String createPerceptionFromXml(String xml) { Template template = xBuilder.getTemplate(PERCEPTION); try (StringReader reader = new StringReader(xml)) { @@ -241,8 +235,7 @@ public String createPerceptionXml(String xml) { @POST @Path("Retention/from-json") - public String createRetention(JsonObject json) { - Retention retention = json.mapTo(Retention.class); + public String createRetentionFromJson(Retention retention) { ContentEnricher enricher = new ContentEnricher(xBuilder.getDefaults(), () -> LocalDate.of(2022, 1, 25)); enricher.enrich(retention); @@ -254,7 +247,7 @@ public String createRetention(JsonObject json) { @POST @Consumes(MediaType.TEXT_PLAIN) @Path("Retention/from-xml") - public String createRetentionXml(String xml) { + public String createRetentionFromXml(String xml) { Template template = xBuilder.getTemplate(RETENTION); try (StringReader reader = new StringReader(xml)) { @@ -268,8 +261,7 @@ public String createRetentionXml(String xml) { @POST @Path("DespatchAdvice/from-json") - public String createDespatchAdvice(JsonObject json) { - DespatchAdvice despatchAdvice = json.mapTo(DespatchAdvice.class); + public String createDespatchAdviceFromJson(DespatchAdvice despatchAdvice) { ContentEnricher enricher = new ContentEnricher(xBuilder.getDefaults(), () -> LocalDate.of(2022, 1, 25)); enricher.enrich(despatchAdvice); @@ -281,7 +273,7 @@ public String createDespatchAdvice(JsonObject json) { @POST @Consumes(MediaType.TEXT_PLAIN) @Path("DespatchAdvice/from-xml") - public String createDespatchAdviceXml(String xml) { + public String createDespatchAdviceFromXml(String xml) { Template template = xBuilder.getTemplate(DESPATCH_ADVICE); try (StringReader reader = new StringReader(xml)) { @@ -295,8 +287,7 @@ public String createDespatchAdviceXml(String xml) { @POST @Path("Reversion/from-json") - public String createReversion(JsonObject json) { - Reversion reversion = json.mapTo(Reversion.class); + public String createReversionFromJson(Reversion reversion) { ContentEnricher enricher = new ContentEnricher(xBuilder.getDefaults(), () -> LocalDate.of(2022, 1, 25)); enricher.enrich(reversion); @@ -308,7 +299,7 @@ public String createReversion(JsonObject json) { @POST @Consumes(MediaType.TEXT_PLAIN) @Path("Reversion/from-xml") - public String createReversionXml(String xml) { + public String createReversionFromXml(String xml) { Template template = xBuilder.getTemplate(REVERSION); try (StringReader reader = new StringReader(xml)) { diff --git a/xbuilder/quarkus-extension/integration-tests/src/test/java/io/github/project/openubl/quarkus/xbuilder/it/QuarkusXbuilderResourceTest.java b/xbuilder/quarkus-extension/integration-tests/src/test/java/io/github/project/openubl/quarkus/xbuilder/it/QuarkusXbuilderResourceTest.java index 568b5f54..af4c9fdc 100644 --- a/xbuilder/quarkus-extension/integration-tests/src/test/java/io/github/project/openubl/quarkus/xbuilder/it/QuarkusXbuilderResourceTest.java +++ b/xbuilder/quarkus-extension/integration-tests/src/test/java/io/github/project/openubl/quarkus/xbuilder/it/QuarkusXbuilderResourceTest.java @@ -1,11 +1,6 @@ package io.github.project.openubl.quarkus.xbuilder.it; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; -import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.alibaba.fastjson2.JSON; import io.github.project.openubl.xbuilder.content.catalogs.Catalog1; import io.github.project.openubl.xbuilder.content.catalogs.Catalog18; import io.github.project.openubl.xbuilder.content.catalogs.Catalog19; @@ -41,6 +36,7 @@ import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.Yaml; import java.io.IOException; import java.math.BigDecimal; @@ -48,7 +44,6 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.time.LocalDate; import java.util.Map; @@ -58,1371 +53,1538 @@ @QuarkusTest public class QuarkusXbuilderResourceTest { - public YAMLMapper getYamlMapper() { - YAMLMapper mapper = new YAMLMapper(new YAMLFactory()); - mapper.registerModule(new JavaTimeModule()); - mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); - mapper.configure(YAMLGenerator.Feature.LITERAL_BLOCK_STYLE, true); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - return mapper; - } + @SuppressWarnings("unchecked") + @Test + public void testAllYamlFilesFromSnapshot() throws URISyntaxException, IOException { + var yaml = new Yaml(); - @Test - public void testAllYamlFilesFromSnapshot() throws URISyntaxException, IOException { - YAMLMapper yamlMapper = getYamlMapper(); + URL url = getClass().getClassLoader().getResource("e2e"); + Path path = Path.of(url.toURI()); + try (var walker = Files.walk(path, 5)) { + walker.filter(p -> !p.toFile().isDirectory()) + .forEach(p -> { + try (var reader = Files.newBufferedReader(p)) { + Map yamlObject = yaml.load(reader); + String kind = (String) yamlObject.get("kind"); + String snapshot = (String) yamlObject.get("snapshot"); + Map input = (Map) yamlObject + .get("input"); - URL url = getClass().getClassLoader().getResource("e2e"); - Path path = Paths.get(url.toURI()); - Files.walk(path, 5) - .filter(p -> !p.toFile().isDirectory()) - .forEach(p -> { - try { - Map jsonObject = yamlMapper.readValue(p.toFile(), Map.class); - String kind = (String) jsonObject.get("kind"); - String snapshot = (String) jsonObject.get("snapshot"); - Map input = (Map) jsonObject.get("input"); + String jsonBody = JSON.toJSONString(input); - given() + given() + .when() + .contentType(ContentType.JSON) + .body(jsonBody) + .post("/quarkus-xbuilder/" + kind + + "/from-json") + .then() + .statusCode(200) + .body(is(snapshot)); + + given() + .when() + .contentType(ContentType.TEXT) + .body(snapshot) + .post("/quarkus-xbuilder/" + kind + "/from-xml") + .then() + .statusCode(200) + .body(is(snapshot)); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + } + + @Test + public void testInvoice() { + Invoice invoice = Invoice.builder() + .serie("F001") + .numero(1) + .proveedor(Proveedor.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .cliente(Cliente.builder() + .nombre("Carlos Feria") + .numeroDocumentoIdentidad("12121212121") + .tipoDocumentoIdentidad(Catalog6.RUC.toString()) + .build()) + .detalle(DocumentoVentaDetalle.builder() + .descripcion("Item1") + .cantidad(new BigDecimal("10")) + .precio(new BigDecimal("100")) + .build()) + .build(); + + given() .when() .contentType(ContentType.JSON) - .body(input) - .post("/quarkus-xbuilder/" + kind + "/from-json") + .body(JSON.toJSONString(invoice)) + .post("/quarkus-xbuilder/Invoice/from-json") .then() .statusCode(200) - .body(is(snapshot)); + .body(is( + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 2.1\n" + + " 2.0\n" + + " F001-1\n" + + " 2022-01-25\n" + + " 01\n" + + + " PEN\n" + + + " \n" + + " 12345678912\n" + + " \n" + + " \n" + + " 12345678912\n" + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " #PROJECT-OPENUBL-SIGN\n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 12345678912\n" + + + " \n" + + " \n" + + " \n" + + + " \n" + + " 0000\n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 12121212121\n" + + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " FormaPago\n" + + " Contado\n" + + + " \n" + + " \n" + + " 200.00\n" + + + " \n" + + " 1000.00\n" + + + " 200.00\n" + + + " \n" + + " S\n" + + + " \n" + + " 1000\n" + + + " IGV\n" + + " VAT\n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 1000.00\n" + + + " 1200.00\n" + + + " 0\n" + + + " 0\n" + + + " 1200.00\n" + + + " \n" + + " \n" + + " 1\n" + + " 10\n" + + + " 1000.00\n" + + + " \n" + + " \n" + + " 120.00\n" + + + " 01\n" + + + " \n" + + " \n" + + " \n" + + " 200.00\n" + + + " \n" + + " 1000.00\n" + + + " 200.00\n" + + + " \n" + + " S\n" + + + " 20.00\n" + + + " 10\n" + + + " \n" + + " 1000\n" + + + " IGV\n" + + " VAT\n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + " 100.00\n" + + + " \n" + + " \n" + + "\n")); + } + + @Test + public void testCreditNote() { + CreditNote creditNote = CreditNote.builder() + .serie("FC01") + .numero(1) + .comprobanteAfectadoSerieNumero("F001-1") + .sustentoDescripcion("mi sustento") + .proveedor(Proveedor.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .cliente(Cliente.builder() + .nombre("Carlos Feria") + .numeroDocumentoIdentidad("12121212121") + .tipoDocumentoIdentidad(Catalog6.RUC.toString()) + .build()) + .detalle(DocumentoVentaDetalle.builder() + .descripcion("Item1") + .cantidad(new BigDecimal("10")) + .precio(new BigDecimal("100")) + .build()) + .build(); - given() + given() .when() - .contentType(ContentType.TEXT) - .body(snapshot) - .post("/quarkus-xbuilder/" + kind + "/from-xml") + .contentType(ContentType.JSON) + .body(JSON.toJSONString(creditNote)) + .post("/quarkus-xbuilder/CreditNote/from-json") .then() .statusCode(200) - .body(is(snapshot)); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } - - @Test - public void testInvoice() { - Invoice invoice = Invoice.builder() - .serie("F001") - .numero(1) - .proveedor(Proveedor.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build() - ) - .cliente(Cliente.builder() - .nombre("Carlos Feria") - .numeroDocumentoIdentidad("12121212121") - .tipoDocumentoIdentidad(Catalog6.RUC.toString()) - .build() - ) - .detalle(DocumentoVentaDetalle.builder() - .descripcion("Item1") - .cantidad(new BigDecimal("10")) - .precio(new BigDecimal("100")) - .build() - ) - .build(); - - given() - .when() - .contentType(ContentType.JSON) - .body(invoice) - .post("/quarkus-xbuilder/Invoice/from-json") - .then() - .statusCode(200) - .body(is( - "\n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 2.1\n" + - " 2.0\n" + - " F001-1\n" + - " 2022-01-25\n" + - " 01\n" + - " PEN\n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " #PROJECT-OPENUBL-SIGN\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 0000\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 12121212121\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " FormaPago\n" + - " Contado\n" + - " \n" + - " \n" + - " 200.00\n" + - " \n" + - " 1000.00\n" + - " 200.00\n" + - " \n" + - " S\n" + - " \n" + - " 1000\n" + - " IGV\n" + - " VAT\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 1000.00\n" + - " 1200.00\n" + - " 0\n" + - " 0\n" + - " 1200.00\n" + - " \n" + - " \n" + - " 1\n" + - " 10\n" + - " 1000.00\n" + - " \n" + - " \n" + - " 120.00\n" + - " 01\n" + - " \n" + - " \n" + - " \n" + - " 200.00\n" + - " \n" + - " 1000.00\n" + - " 200.00\n" + - " \n" + - " S\n" + - " 20.00\n" + - " 10\n" + - " \n" + - " 1000\n" + - " IGV\n" + - " VAT\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 100.00\n" + - " \n" + - " \n" + - "\n" - ) - ); - } + .body(is( + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 2.1\n" + + " 2.0\n" + + " FC01-1\n" + + " 2022-01-25\n" + + " PEN\n" + + + " \n" + + " F001-1\n" + + " 01\n" + + " \n" + + + " \n" + + " \n" + + " \n" + + " F001-1\n" + + " 01\n" + + + " \n" + + " \n" + + " \n" + + " 12345678912\n" + + " \n" + + " \n" + + " 12345678912\n" + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " #PROJECT-OPENUBL-SIGN\n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 12345678912\n" + + + " \n" + + " \n" + + " \n" + + + " \n" + + " 0000\n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 12121212121\n" + + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " 200.00\n" + + + " \n" + + " 1000.00\n" + + + " 200.00\n" + + + " \n" + + " S\n" + + + " \n" + + " 1000\n" + + + " IGV\n" + + " VAT\n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 1000.00\n" + + + " 1200.00\n" + + + " 1200.00\n" + + + " \n" + + " \n" + + " 1\n" + + " 10\n" + + + " 1000.00\n" + + + " \n" + + " \n" + + " 120.00\n" + + + " 01\n" + + + " \n" + + " \n" + + " \n" + + " 200.00\n" + + + " \n" + + " 1000.00\n" + + + " 200.00\n" + + + " \n" + + " S\n" + + + " 20.00\n" + + + " 10\n" + + + " \n" + + " 1000\n" + + + " IGV\n" + + " VAT\n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + " 100.00\n" + + + " \n" + + " \n" + + "\n")); + } - @Test - public void testCreditNote() { - CreditNote creditNote = CreditNote.builder() - .serie("FC01") - .numero(1) - .comprobanteAfectadoSerieNumero("F001-1") - .sustentoDescripcion("mi sustento") - .proveedor(Proveedor.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build() - ) - .cliente(Cliente.builder() - .nombre("Carlos Feria") - .numeroDocumentoIdentidad("12121212121") - .tipoDocumentoIdentidad(Catalog6.RUC.toString()) - .build() - ) - .detalle(DocumentoVentaDetalle.builder() - .descripcion("Item1") - .cantidad(new BigDecimal("10")) - .precio(new BigDecimal("100")) - .build() - ) - .build(); - - given() - .when() - .contentType(ContentType.JSON) - .body(creditNote) - .post("/quarkus-xbuilder/CreditNote/from-json") - .then() - .statusCode(200) - .body(is( - "\n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 2.1\n" + - " 2.0\n" + - " FC01-1\n" + - " 2022-01-25\n" + - " PEN\n" + - " \n" + - " F001-1\n" + - " 01\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " F001-1\n" + - " 01\n" + - " \n" + - " \n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " #PROJECT-OPENUBL-SIGN\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 0000\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 12121212121\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 200.00\n" + - " \n" + - " 1000.00\n" + - " 200.00\n" + - " \n" + - " S\n" + - " \n" + - " 1000\n" + - " IGV\n" + - " VAT\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 1000.00\n" + - " 1200.00\n" + - " 1200.00\n" + - " \n" + - " \n" + - " 1\n" + - " 10\n" + - " 1000.00\n" + - " \n" + - " \n" + - " 120.00\n" + - " 01\n" + - " \n" + - " \n" + - " \n" + - " 200.00\n" + - " \n" + - " 1000.00\n" + - " 200.00\n" + - " \n" + - " S\n" + - " 20.00\n" + - " 10\n" + - " \n" + - " 1000\n" + - " IGV\n" + - " VAT\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 100.00\n" + - " \n" + - " \n" + - "\n" - ) - ); - } - - @Test - public void testDebitNote() { - DebitNote debitNote = DebitNote.builder() - .serie("FD01") - .numero(1) - .comprobanteAfectadoSerieNumero("F001-1") - .sustentoDescripcion("mi sustento") - .proveedor(Proveedor.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build() - ) - .cliente(Cliente.builder() - .nombre("Carlos Feria") - .numeroDocumentoIdentidad("12121212121") - .tipoDocumentoIdentidad(Catalog6.RUC.toString()) - .build() - ) - .detalle(DocumentoVentaDetalle.builder() - .descripcion("Item1") - .cantidad(new BigDecimal("10")) - .precio(new BigDecimal("100")) - .build() - ) - .build(); + @Test + public void testDebitNote() { + DebitNote debitNote = DebitNote.builder() + .serie("FD01") + .numero(1) + .comprobanteAfectadoSerieNumero("F001-1") + .sustentoDescripcion("mi sustento") + .proveedor(Proveedor.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .cliente(Cliente.builder() + .nombre("Carlos Feria") + .numeroDocumentoIdentidad("12121212121") + .tipoDocumentoIdentidad(Catalog6.RUC.toString()) + .build()) + .detalle(DocumentoVentaDetalle.builder() + .descripcion("Item1") + .cantidad(new BigDecimal("10")) + .precio(new BigDecimal("100")) + .build()) + .build(); - given() - .when() - .contentType(ContentType.JSON) - .body(debitNote) - .post("/quarkus-xbuilder/DebitNote/from-json") - .then() - .statusCode(200) - .body(is( - "\n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 2.1\n" + - " 2.0\n" + - " FD01-1\n" + - " 2022-01-25\n" + - " PEN\n" + - " \n" + - " F001-1\n" + - " 01\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " F001-1\n" + - " 01\n" + - " \n" + - " \n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " #PROJECT-OPENUBL-SIGN\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 0000\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 12121212121\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 200.00\n" + - " \n" + - " 1000.00\n" + - " 200.00\n" + - " \n" + - " S\n" + - " \n" + - " 1000\n" + - " IGV\n" + - " VAT\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 1000.00\n" + - " 1200.00\n" + - " 1200.00\n" + - " \n" + - " \n" + - " 1\n" + - " 10\n" + - " 1000.00\n" + - " \n" + - " \n" + - " 120.00\n" + - " 01\n" + - " \n" + - " \n" + - " \n" + - " 200.00\n" + - " \n" + - " 1000.00\n" + - " 200.00\n" + - " \n" + - " S\n" + - " 20.00\n" + - " 10\n" + - " \n" + - " 1000\n" + - " IGV\n" + - " VAT\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 100.00\n" + - " \n" + - " \n" + - "\n" - ) - ); - } + given() + .when() + .contentType(ContentType.JSON) + .body(JSON.toJSONString(debitNote)) + .post("/quarkus-xbuilder/DebitNote/from-json") + .then() + .statusCode(200) + .body(is( + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 2.1\n" + + " 2.0\n" + + " FD01-1\n" + + " 2022-01-25\n" + + " PEN\n" + + + " \n" + + " F001-1\n" + + " 01\n" + + " \n" + + + " \n" + + " \n" + + " \n" + + " F001-1\n" + + " 01\n" + + + " \n" + + " \n" + + " \n" + + " 12345678912\n" + + " \n" + + " \n" + + " 12345678912\n" + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " #PROJECT-OPENUBL-SIGN\n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 12345678912\n" + + + " \n" + + " \n" + + " \n" + + + " \n" + + " 0000\n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 12121212121\n" + + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " 200.00\n" + + + " \n" + + " 1000.00\n" + + + " 200.00\n" + + + " \n" + + " S\n" + + + " \n" + + " 1000\n" + + + " IGV\n" + + " VAT\n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 1000.00\n" + + + " 1200.00\n" + + + " 1200.00\n" + + + " \n" + + " \n" + + " 1\n" + + " 10\n" + + + " 1000.00\n" + + + " \n" + + " \n" + + " 120.00\n" + + + " 01\n" + + + " \n" + + " \n" + + " \n" + + " 200.00\n" + + + " \n" + + " 1000.00\n" + + + " 200.00\n" + + + " \n" + + " S\n" + + + " 20.00\n" + + + " 10\n" + + + " \n" + + " 1000\n" + + + " IGV\n" + + " VAT\n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + " 100.00\n" + + + " \n" + + " \n" + + "\n")); + } - @Test - public void testVoidedDocuments() { - VoidedDocuments voidedDocuments = VoidedDocuments.builder() - .numero(1) - .fechaEmision(LocalDate.of(2022, 01, 31)) - .fechaEmisionComprobantes(LocalDate.of(2022, 01, 29)) - .proveedor(Proveedor.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build() - ) - .comprobante(VoidedDocumentsItem.builder() - .serie("F001") - .numero(1) - .tipoComprobante(Catalog1_Invoice.FACTURA.getCode()) - .descripcionSustento("Mi sustento1") - .build() - ) - .comprobante(VoidedDocumentsItem.builder() - .serie("F001") - .numero(2) - .tipoComprobante(Catalog1_Invoice.FACTURA.getCode()) - .descripcionSustento("Mi sustento2") - .build() - ) - .build(); + @Test + public void testVoidedDocuments() { + VoidedDocuments voidedDocuments = VoidedDocuments.builder() + .numero(1) + .fechaEmision(LocalDate.of(2022, 01, 31)) + .fechaEmisionComprobantes(LocalDate.of(2022, 01, 29)) + .proveedor(Proveedor.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .comprobante(VoidedDocumentsItem.builder() + .serie("F001") + .numero(1) + .tipoComprobante(Catalog1_Invoice.FACTURA.getCode()) + .descripcionSustento("Mi sustento1") + .build()) + .comprobante(VoidedDocumentsItem.builder() + .serie("F001") + .numero(2) + .tipoComprobante(Catalog1_Invoice.FACTURA.getCode()) + .descripcionSustento("Mi sustento2") + .build()) + .build(); - given() - .when() - .contentType(ContentType.JSON) - .body(voidedDocuments) - .post("/quarkus-xbuilder/VoidedDocuments/from-json") - .then() - .statusCode(200) - .body(is("\n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 2.0\n" + - " 1.0\n" + - " RA-20220131-1\n" + - " 2022-01-29\n" + - " 2022-01-31\n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " #PROJECT-OPENUBL-SIGN\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 12345678912\n" + - " 6\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 1\n" + - " 01\n" + - " F001\n" + - " 1\n" + - " Mi sustento1\n" + - " \n" + - " \n" + - " 2\n" + - " 01\n" + - " F001\n" + - " 2\n" + - " Mi sustento2\n" + - " \n" + - "\n")); - } + given() + .when() + .contentType(ContentType.JSON) + .body(JSON.toJSONString(voidedDocuments)) + .post("/quarkus-xbuilder/VoidedDocuments/from-json") + .then() + .statusCode(200) + .body(is("\n" + + "\n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 2.0\n" + + " 1.0\n" + + " RA-20220131-1\n" + + " 2022-01-29\n" + + " 2022-01-31\n" + + " \n" + + " 12345678912\n" + + " \n" + + " \n" + + " 12345678912\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " #PROJECT-OPENUBL-SIGN\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 12345678912\n" + + + " 6\n" + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " 1\n" + + " 01\n" + + " F001\n" + + " 1\n" + + " Mi sustento1\n" + + + " \n" + + " \n" + + " 2\n" + + " 01\n" + + " F001\n" + + " 2\n" + + " Mi sustento2\n" + + + " \n" + + "\n")); + } - @Test - public void testSummaryDocuments() { - SummaryDocuments summaryDocuments = SummaryDocuments.builder() - .numero(1) - .fechaEmision(LocalDate.of(2022, 01, 31)) - .fechaEmisionComprobantes(LocalDate.of(2022, 01, 29)) - .proveedor(Proveedor.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build() - ) - .comprobante(SummaryDocumentsItem.builder() - .tipoOperacion(Catalog19.ADICIONAR.toString()) - .comprobante(Comprobante.builder() - .tipoComprobante(Catalog1_Invoice.BOLETA.getCode())// - .serieNumero("B001-1") - .cliente(Cliente.builder() - .nombre("Carlos Feria") - .numeroDocumentoIdentidad("12345678") - .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) - .build() - ) - .impuestos(ComprobanteImpuestos.builder() - .igv(new BigDecimal("18")) - .icb(new BigDecimal(2)) - .build() - ) - .valorVenta(ComprobanteValorVenta.builder() - .importeTotal(new BigDecimal("120")) - .gravado(new BigDecimal("120")) - .build() - ) - .build() - ) - .build() - ) - .comprobante(SummaryDocumentsItem.builder() - .tipoOperacion(Catalog19.ADICIONAR.toString()) - .comprobante(Comprobante.builder() - .tipoComprobante(Catalog1.NOTA_CREDITO.getCode()) - .serieNumero("BC02-2") - .comprobanteAfectado(ComprobanteAfectado.builder() - .serieNumero("B002-2") - .tipoComprobante(Catalog1.BOLETA.getCode()) // - .build() - ) - .cliente(Cliente.builder() - .nombre("Carlos Feria") - .numeroDocumentoIdentidad("12345678") - .tipoDocumentoIdentidad(Catalog6.DNI.getCode())// - .build() - ) - .impuestos(ComprobanteImpuestos.builder() - .igv(new BigDecimal("18")) - .build() - ) - .valorVenta(ComprobanteValorVenta.builder() - .importeTotal(new BigDecimal("118")) - .gravado(new BigDecimal("118")) - .build() - ) - .build() - ) - .build() - ) - .build(); + @Test + public void testSummaryDocuments() { + SummaryDocuments summaryDocuments = SummaryDocuments.builder() + .numero(1) + .fechaEmision(LocalDate.of(2022, 01, 31)) + .fechaEmisionComprobantes(LocalDate.of(2022, 01, 29)) + .proveedor(Proveedor.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .comprobante(SummaryDocumentsItem.builder() + .tipoOperacion(Catalog19.ADICIONAR.toString()) + .comprobante(Comprobante.builder() + .tipoComprobante(Catalog1_Invoice.BOLETA.getCode())// + .serieNumero("B001-1") + .cliente(Cliente.builder() + .nombre("Carlos Feria") + .numeroDocumentoIdentidad("12345678") + .tipoDocumentoIdentidad( + Catalog6.DNI.getCode()) + .build()) + .impuestos(ComprobanteImpuestos.builder() + .igv(new BigDecimal("18")) + .icb(new BigDecimal(2)) + .build()) + .valorVenta(ComprobanteValorVenta.builder() + .importeTotal(new BigDecimal("120")) + .gravado(new BigDecimal("120")) + .build()) + .build()) + .build()) + .comprobante(SummaryDocumentsItem.builder() + .tipoOperacion(Catalog19.ADICIONAR.toString()) + .comprobante(Comprobante.builder() + .tipoComprobante(Catalog1.NOTA_CREDITO.getCode()) + .serieNumero("BC02-2") + .comprobanteAfectado(ComprobanteAfectado.builder() + .serieNumero("B002-2") + .tipoComprobante(Catalog1.BOLETA + .getCode()) // + .build()) + .cliente(Cliente.builder() + .nombre("Carlos Feria") + .numeroDocumentoIdentidad("12345678") + .tipoDocumentoIdentidad( + Catalog6.DNI.getCode())// + .build()) + .impuestos(ComprobanteImpuestos.builder() + .igv(new BigDecimal("18")) + .build()) + .valorVenta(ComprobanteValorVenta.builder() + .importeTotal(new BigDecimal("118")) + .gravado(new BigDecimal("118")) + .build()) + .build()) + .build()) + .build(); - given() - .when() - .contentType(ContentType.JSON) - .body(summaryDocuments) - .post("/quarkus-xbuilder/SummaryDocuments/from-json") - .then() - .statusCode(200) - .body(is("\n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 2.0\n" + - " 1.1\n" + - " RC-20220131-1\n" + - " 2022-01-29\n" + - " 2022-01-31\n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " #PROJECT-OPENUBL-SIGN\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 12345678912\n" + - " 6\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 1\n" + - " 03\n" + - " B001-1\n" + - " \n" + - " 12345678\n" + - " 1\n" + - " \n" + - " \n" + - " 1\n" + - " \n" + - " 120\n" + - " \n" + - " 120\n" + - " 01\n" + - " \n" + - " \n" + - " 18\n" + - " \n" + - " 18\n" + - " \n" + - " 20.00\n" + - " \n" + - " 1000\n" + - " IGV\n" + - " VAT\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 2\n" + - " \n" + - " 2\n" + - " \n" + - " \n" + - " 7152\n" + - " ICBPER\n" + - " OTH\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 2\n" + - " 07\n" + - " BC02-2\n" + - " \n" + - " 12345678\n" + - " 1\n" + - " \n" + - " \n" + - " \n" + - " B002-2\n" + - " 03\n" + - " \n" + - " \n" + - " \n" + - " 1\n" + - " \n" + - " 118\n" + - " \n" + - " 118\n" + - " 01\n" + - " \n" + - " \n" + - " 18\n" + - " \n" + - " 18\n" + - " \n" + - " 20.00\n" + - " \n" + - " 1000\n" + - " IGV\n" + - " VAT\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "\n")); - } + given() + .when() + .contentType(ContentType.JSON) + .body(JSON.toJSONString(summaryDocuments)) + .post("/quarkus-xbuilder/SummaryDocuments/from-json") + .then() + .statusCode(200) + .body(is("\n" + + "\n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 2.0\n" + + " 1.1\n" + + " RC-20220131-1\n" + + " 2022-01-29\n" + + " 2022-01-31\n" + + " \n" + + " 12345678912\n" + + " \n" + + " \n" + + " 12345678912\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " #PROJECT-OPENUBL-SIGN\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 12345678912\n" + + + " 6\n" + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " 1\n" + + " 03\n" + + " B001-1\n" + + " \n" + + " 12345678\n" + + + " 1\n" + + " \n" + + " \n" + + " 1\n" + + " \n" + + " 120\n" + + " \n" + + " 120\n" + + + " 01\n" + + " \n" + + " \n" + + " 18\n" + + " \n" + + " 18\n" + + + " \n" + + " 20.00\n" + + " \n" + + " 1000\n" + + " IGV\n" + + " VAT\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 2\n" + + " \n" + + " 2\n" + + + " \n" + + " \n" + + " 7152\n" + + " ICBPER\n" + + " OTH\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 2\n" + + " 07\n" + + " BC02-2\n" + + " \n" + + " 12345678\n" + + + " 1\n" + + " \n" + + " \n" + + " \n" + + " B002-2\n" + + " 03\n" + + " \n" + + " \n" + + " \n" + + " 1\n" + + " \n" + + " 118\n" + + " \n" + + " 118\n" + + + " 01\n" + + " \n" + + " \n" + + " 18\n" + + " \n" + + " 18\n" + + + " \n" + + " 20.00\n" + + " \n" + + " 1000\n" + + " IGV\n" + + " VAT\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n")); + } - @Test - public void testPerception() { - Perception perception = Perception.builder() - .serie("P001") - .numero(1) - .fechaEmision(LocalDate.of(2022, 01, 31)) - .proveedor(Proveedor.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build() - ) - .cliente(Cliente.builder() - .nombre("Carlos Feria") - .numeroDocumentoIdentidad("12121212121") - .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) - .build() - ) - .importeTotalPercibido(new BigDecimal("10")) - .importeTotalCobrado(new BigDecimal("210")) - .tipoRegimen(Catalog22.VENTA_INTERNA.getCode()) - .tipoRegimenPorcentaje(Catalog22.VENTA_INTERNA.getPercent()) // - .operacion(PercepcionRetencionOperacion.builder() - .numeroOperacion(1) - .fechaOperacion(LocalDate.of(2022, 01, 31)) - .importeOperacion(new BigDecimal("100")) - .comprobante(io.github.project.openubl.xbuilder.content.models.sunat.percepcionretencion.ComprobanteAfectado.builder() - .tipoComprobante(Catalog1.FACTURA.getCode()) - .serieNumero("F001-1") + @Test + public void testPerception() { + Perception perception = Perception.builder() + .serie("P001") + .numero(1) .fechaEmision(LocalDate.of(2022, 01, 31)) - .importeTotal(new BigDecimal("200")) - .moneda("PEN") - .build() - ) - .build() - ) - .build(); + .proveedor(Proveedor.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .cliente(Cliente.builder() + .nombre("Carlos Feria") + .numeroDocumentoIdentidad("12121212121") + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .build()) + .importeTotalPercibido(new BigDecimal("10")) + .importeTotalCobrado(new BigDecimal("210")) + .tipoRegimen(Catalog22.VENTA_INTERNA.getCode()) + .tipoRegimenPorcentaje(Catalog22.VENTA_INTERNA.getPercent()) // + .operacion(PercepcionRetencionOperacion.builder() + .numeroOperacion(1) + .fechaOperacion(LocalDate.of(2022, 01, 31)) + .importeOperacion(new BigDecimal("100")) + .comprobante(io.github.project.openubl.xbuilder.content.models.sunat.percepcionretencion.ComprobanteAfectado + .builder() + .tipoComprobante(Catalog1.FACTURA.getCode()) + .serieNumero("F001-1") + .fechaEmision(LocalDate.of(2022, 01, 31)) + .importeTotal(new BigDecimal("200")) + .moneda("PEN") + .build()) + .build()) + .build(); - given() - .when() - .contentType(ContentType.JSON) - .body(perception) - .post("/quarkus-xbuilder/Perception/from-json") - .then() - .statusCode(200) - .body(is("\n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 2.0\n" + - " 1.0\n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " #PROJECT-OPENUBL-SIGN\n" + - " \n" + - " \n" + - " \n" + - " P001-1\n" + - " 2022-01-31\n" + - " \n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 12121212121\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 01\n" + - " 2\n" + - " 10\n" + - " 210\n" + - " \n" + - " F001-1\n" + - " 2022-01-31\n" + - " 200\n" + - " \n" + - " 1\n" + - " 100\n" + - " 2022-01-31\n" + - " \n" + - " \n" + - " 10\n" + - " 2022-01-31\n" + - " 210\n" + - " \n" + - " \n" + - "\n")); - } + given() + .when() + .contentType(ContentType.JSON) + .body(JSON.toJSONString(perception)) + .post("/quarkus-xbuilder/Perception/from-json") + .then() + .statusCode(200) + .body(is("\n" + + "\n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 2.0\n" + + " 1.0\n" + + " \n" + + " 12345678912\n" + + " \n" + + " \n" + + " 12345678912\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " #PROJECT-OPENUBL-SIGN\n" + + " \n" + + " \n" + + " \n" + + " P001-1\n" + + " 2022-01-31\n" + + " \n" + + " \n" + + " 12345678912\n" + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " 12121212121\n" + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + " 01\n" + + + " 2\n" + + " 10\n" + + + " 210\n" + + + " \n" + + " F001-1\n" + + " 2022-01-31\n" + + " 200\n" + + + " \n" + + " 1\n" + + " 100\n" + + + " 2022-01-31\n" + + " \n" + + " \n" + + " 10\n" + + + " 2022-01-31\n" + + + " 210\n" + + + " \n" + + " \n" + + "\n")); + } - @Test - public void testRetention() { - Retention retention = Retention.builder() - .serie("R001") - .numero(1) - .fechaEmision(LocalDate.of(2022, 01, 31)) - .proveedor(Proveedor.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build() - ) - .cliente(Cliente.builder() - .nombre("Carlos Feria") - .numeroDocumentoIdentidad("12121212121") - .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) - .build() - ) - .importeTotalRetenido(new BigDecimal("10")) - .importeTotalPagado(new BigDecimal("200")) - .tipoRegimen(Catalog23.TASA_TRES.getCode()) - .tipoRegimenPorcentaje(Catalog23.TASA_TRES.getPercent()) // - .operacion(PercepcionRetencionOperacion.builder() - .numeroOperacion(1) - .fechaOperacion(LocalDate.of(2022, 01, 31)) - .importeOperacion(new BigDecimal("100")) - .comprobante(io.github.project.openubl.xbuilder.content.models.sunat.percepcionretencion.ComprobanteAfectado.builder() - .tipoComprobante(Catalog1.FACTURA.getCode()) - .serieNumero("F001-1") + @Test + public void testRetention() { + Retention retention = Retention.builder() + .serie("R001") + .numero(1) .fechaEmision(LocalDate.of(2022, 01, 31)) - .importeTotal(new BigDecimal("210")) - .moneda("PEN") - .build() - ) - .build() - ) - .build(); + .proveedor(Proveedor.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .cliente(Cliente.builder() + .nombre("Carlos Feria") + .numeroDocumentoIdentidad("12121212121") + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .build()) + .importeTotalRetenido(new BigDecimal("10")) + .importeTotalPagado(new BigDecimal("200")) + .tipoRegimen(Catalog23.TASA_TRES.getCode()) + .tipoRegimenPorcentaje(Catalog23.TASA_TRES.getPercent()) // + .operacion(PercepcionRetencionOperacion.builder() + .numeroOperacion(1) + .fechaOperacion(LocalDate.of(2022, 01, 31)) + .importeOperacion(new BigDecimal("100")) + .comprobante(io.github.project.openubl.xbuilder.content.models.sunat.percepcionretencion.ComprobanteAfectado + .builder() + .tipoComprobante(Catalog1.FACTURA.getCode()) + .serieNumero("F001-1") + .fechaEmision(LocalDate.of(2022, 01, 31)) + .importeTotal(new BigDecimal("210")) + .moneda("PEN") + .build()) + .build()) + .build(); - given() - .when() - .contentType(ContentType.JSON) - .body(retention) - .post("/quarkus-xbuilder/Retention/from-json") - .then() - .statusCode(200) - .body(is("\n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 2.0\n" + - " 1.0\n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " #PROJECT-OPENUBL-SIGN\n" + - " \n" + - " \n" + - " \n" + - " R001-1\n" + - " 2022-01-31\n" + - " \n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 12121212121\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 01\n" + - " 3\n" + - " 10\n" + - " 200\n" + - " \n" + - " F001-1\n" + - " 2022-01-31\n" + - " 210\n" + - " \n" + - " 1\n" + - " 100\n" + - " 2022-01-31\n" + - " \n" + - " \n" + - " 10\n" + - " 2022-01-31\n" + - " 200\n" + - " \n" + - " \n" + - "\n")); - } + given() + .when() + .contentType(ContentType.JSON) + .body(JSON.toJSONString(retention)) + .post("/quarkus-xbuilder/Retention/from-json") + .then() + .statusCode(200) + .body(is("\n" + + "\n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 2.0\n" + + " 1.0\n" + + " \n" + + " 12345678912\n" + + " \n" + + " \n" + + " 12345678912\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " #PROJECT-OPENUBL-SIGN\n" + + " \n" + + " \n" + + " \n" + + " R001-1\n" + + " 2022-01-31\n" + + " \n" + + " \n" + + " 12345678912\n" + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " 12121212121\n" + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + " 01\n" + + + " 3\n" + + " 10\n" + + + " 200\n" + + + " \n" + + " F001-1\n" + + " 2022-01-31\n" + + " 210\n" + + + " \n" + + " 1\n" + + " 100\n" + + + " 2022-01-31\n" + + " \n" + + " \n" + + " 10\n" + + + " 2022-01-31\n" + + + " 200\n" + + + " \n" + + " \n" + + "\n")); + } - @Test - public void testDespatchAdvice() { - DespatchAdvice despatchAdvice = DespatchAdvice.builder() - .serie("T001") - .numero(1) - .tipoComprobante(Catalog1.GUIA_REMISION_REMITENTE.getCode()) - .remitente(Remitente.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build() - ) - .destinatario(Destinatario.builder() - .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) - .numeroDocumentoIdentidad("12345678") - .nombre("mi cliente") - .build() - ) - .envio(Envio.builder() - .tipoTraslado(Catalog20.TRASLADO_EMISOR_ITINERANTE_CP.getCode()) - .pesoTotal(BigDecimal.ONE) - .pesoTotalUnidadMedida("KG") - .tipoModalidadTraslado(Catalog18.TRANSPORTE_PRIVADO.getCode()) - .fechaTraslado(LocalDate.of(2022, 1, 25)) - .partida(Partida.builder() - .direccion("DireccionOrigen") - .ubigeo("010101") - .build() - ) - .destino(Destino.builder() - .direccion("DireccionDestino") - .ubigeo("020202") - .build() - ) - .build() - ) - .detalle(DespatchAdviceItem.builder() - .cantidad(new BigDecimal("0.5")) - .unidadMedida("KG") - .codigo("123456") - .build() - ) - .build(); + @Test + public void testDespatchAdvice() { + DespatchAdvice despatchAdvice = DespatchAdvice.builder() + .serie("T001") + .numero(1) + .tipoComprobante(Catalog1.GUIA_REMISION_REMITENTE.getCode()) + .remitente(Remitente.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("12345678") + .nombre("mi cliente") + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.TRASLADO_EMISOR_ITINERANTE_CP.getCode()) + .pesoTotal(BigDecimal.ONE) + .pesoTotalUnidadMedida("KG") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PRIVADO.getCode()) + .fechaTraslado(LocalDate.of(2022, 1, 25)) + .partida(Partida.builder() + .direccion("DireccionOrigen") + .ubigeo("010101") + .build()) + .destino(Destino.builder() + .direccion("DireccionDestino") + .ubigeo("020202") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("0.5")) + .unidadMedida("KG") + .codigo("123456") + .build()) + .build(); - given() - .when() - .contentType(ContentType.JSON) - .body(despatchAdvice) - .post("/quarkus-xbuilder/DespatchAdvice/from-json") - .then() - .statusCode(200) - .body(is("\n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 2.1\n" + - " 2.0\n" + - " T001-1\n" + - " 2022-01-25\n" + - " 09\n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " #PROJECT-OPENUBL-SIGN\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " 12345678912\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 12345678\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " SUNAT_Envio\n" + - " 18\n" + - " 1.000\n" + - " \n" + - " 02\n" + - " \n" + - " 2022-01-25\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 020202\n" + - " \n" + - " DireccionDestino\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 010101\n" + - " \n" + - " DireccionOrigen\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " 1\n" + - " 0.50\n" + - " \n" + - " 1\n" + - " \n" + - " \n" + - " \n" + - " 123456\n" + - " \n" + - " \n" + - " \n" + - "\n")); - } + given() + .when() + .contentType(ContentType.JSON) + .body(JSON.toJSONString(despatchAdvice)) + .post("/quarkus-xbuilder/DespatchAdvice/from-json") + .then() + .statusCode(200) + .body(is("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 2.1\n" + + " 2.0\n" + + " T001-1\n" + + " 2022-01-25\n" + + " 09\n" + + + " \n" + + " 12345678912\n" + + " \n" + + " \n" + + " 12345678912\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " #PROJECT-OPENUBL-SIGN\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 12345678912\n" + + + " \n" + + " \n" + + " 12345678912\n" + + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 12345678\n" + + + " \n" + + " \n" + + " \n" + + + " \n" + + " \n" + + " \n" + + " \n" + + " SUNAT_Envio\n" + + " 18\n" + + + " 1.000\n" + + + " \n" + + " 02\n" + + + " \n" + + " 2022-01-25\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 020202\n" + + + " \n" + + " DireccionDestino\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 010101\n" + + + " \n" + + " DireccionOrigen\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 1\n" + + " 0.50\n" + + + " \n" + + " 1\n" + + " \n" + + " \n" + + " \n" + + " 123456\n" + + " \n" + + " \n" + + " \n" + + "\n")); + } - @Test - public void testDespatchAdvice2() { - DespatchAdvice despatchAdvice = DespatchAdvice.builder() - .serie("T001") - .numero(1) - .tipoComprobante(Catalog1.GUIA_REMISION_REMITENTE.getCode()) - .remitente(Remitente.builder() - .ruc("12345678912") - .razonSocial("Softgreen S.A.C.") - .build() - ) - .destinatario(Destinatario.builder() - .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) - .numeroDocumentoIdentidad("12345678") - .nombre("mi cliente") - .build() - ) - .envio(Envio.builder() - .tipoTraslado(Catalog20.TRASLADO_EMISOR_ITINERANTE_CP.getCode()) - .pesoTotal(BigDecimal.ONE) - .pesoTotalUnidadMedida("KG") - .tipoModalidadTraslado(Catalog18.TRANSPORTE_PRIVADO.getCode()) - .fechaTraslado(LocalDate.of(2022, 1, 25)) - .partida(Partida.builder() - .direccion("DireccionOrigen") - .ubigeo("010101") - .build() - ) - .destino(Destino.builder() - .direccion("DireccionDestino") - .ubigeo("020202") - .build() - ) - .build() - ) - .detalle(DespatchAdviceItem.builder() - .cantidad(new BigDecimal("0.5")) - .unidadMedida("KG") - .codigo("123456") - .build() - ) - .build(); + @Test + public void testDespatchAdvice2() { + DespatchAdvice despatchAdvice = DespatchAdvice.builder() + .serie("T001") + .numero(1) + .tipoComprobante(Catalog1.GUIA_REMISION_REMITENTE.getCode()) + .remitente(Remitente.builder() + .ruc("12345678912") + .razonSocial("Softgreen S.A.C.") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("12345678") + .nombre("mi cliente") + .build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.TRASLADO_EMISOR_ITINERANTE_CP.getCode()) + .pesoTotal(BigDecimal.ONE) + .pesoTotalUnidadMedida("KG") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PRIVADO.getCode()) + .fechaTraslado(LocalDate.of(2022, 1, 25)) + .partida(Partida.builder() + .direccion("DireccionOrigen") + .ubigeo("010101") + .build()) + .destino(Destino.builder() + .direccion("DireccionDestino") + .ubigeo("020202") + .build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(new BigDecimal("0.5")) + .unidadMedida("KG") + .codigo("123456") + .build()) + .build(); - given() - .when() - .contentType(ContentType.JSON) - .body(despatchAdvice) - .post("/quarkus-xbuilder/DespatchAdvice/from-json") - .then() - .statusCode(200) - .body(is( - """ - - - - - - - - 2.1 - 2.0 - T001-1 - 2022-01-25 - 09 - - 12345678912 - - - 12345678912 - - - - - - - - #PROJECT-OPENUBL-SIGN - - - - - 12345678912 - - - 12345678912 - - - - - - - - - - 12345678 - - - - - - - - SUNAT_Envio - 18 - 1.000 - - 02 - - 2022-01-25 - - - - - 020202 - - DireccionDestino - - - - - 010101 - - DireccionOrigen - - - - - - - 1 - 0.50 - - 1 - - - - 123456 - - - - - """)); - } + given() + .when() + .contentType(ContentType.JSON) + .body(JSON.toJSONString(despatchAdvice)) + .post("/quarkus-xbuilder/DespatchAdvice/from-json") + .then() + .statusCode(200) + .body(is( + """ + + + + + + + + 2.1 + 2.0 + T001-1 + 2022-01-25 + 09 + + 12345678912 + + + 12345678912 + + + + + + + + #PROJECT-OPENUBL-SIGN + + + + + 12345678912 + + + 12345678912 + + + + + + + + + + 12345678 + + + + + + + + SUNAT_Envio + 18 + 1.000 + + 02 + + 2022-01-25 + + + + + 020202 + + DireccionDestino + + + + + 010101 + + DireccionOrigen + + + + + + + 1 + 0.50 + + 1 + + + + 123456 + + + + + """)); + } } diff --git a/xbuilder/quarkus-extension/runtime/src/main/java/io/github/project/openubl/quarkus/xbuilder/runtime/DefaultXBuilder.java b/xbuilder/quarkus-extension/runtime/src/main/java/io/github/project/openubl/quarkus/xbuilder/runtime/DefaultXBuilder.java index fa392b14..59d36513 100644 --- a/xbuilder/quarkus-extension/runtime/src/main/java/io/github/project/openubl/quarkus/xbuilder/runtime/DefaultXBuilder.java +++ b/xbuilder/quarkus-extension/runtime/src/main/java/io/github/project/openubl/quarkus/xbuilder/runtime/DefaultXBuilder.java @@ -25,8 +25,7 @@ public class DefaultXBuilder implements XBuilder { void configureEngine(@Observes EngineBuilder builder) { builder.addResultMapper( - new HtmlEscaper(List.of("text/html", "text/xml", "application/xml", "application/xhtml+xml")) - ); + new HtmlEscaper(List.of("text/html", "text/xml", "application/xml", "application/xhtml+xml"))); EngineProducer.getInstance().getEngine().getValueResolvers().forEach(builder::addValueResolver); } @@ -39,8 +38,8 @@ public Template getTemplate(Type type) { @Override public Defaults getDefaults() { return Defaults.builder() - .igvTasa(config.igvTasa.orElse(new BigDecimal("0.18"))) - .icbTasa(config.icbTasa.orElse(new BigDecimal("0.2"))) + .igvTasa(config.igvTasa().orElse(new BigDecimal("0.18"))) + .icbTasa(config.icbTasa().orElse(new BigDecimal("0.2"))) .build(); } } diff --git a/xbuilder/quarkus-extension/runtime/src/main/java/io/github/project/openubl/quarkus/xbuilder/runtime/XBuilderConfig.java b/xbuilder/quarkus-extension/runtime/src/main/java/io/github/project/openubl/quarkus/xbuilder/runtime/XBuilderConfig.java index 317876d6..26f73367 100644 --- a/xbuilder/quarkus-extension/runtime/src/main/java/io/github/project/openubl/quarkus/xbuilder/runtime/XBuilderConfig.java +++ b/xbuilder/quarkus-extension/runtime/src/main/java/io/github/project/openubl/quarkus/xbuilder/runtime/XBuilderConfig.java @@ -1,24 +1,23 @@ package io.github.project.openubl.quarkus.xbuilder.runtime; -import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; import java.math.BigDecimal; import java.util.Optional; -@ConfigRoot(name = "xbuilder", phase = ConfigPhase.RUN_TIME) -public class XBuilderConfig { +@ConfigRoot(phase = ConfigPhase.RUN_TIME) +@ConfigMapping(prefix = "quarkus.xbuilder") +public interface XBuilderConfig { /** * Default igvTasa */ - @ConfigItem - public Optional igvTasa; + Optional igvTasa(); /** * Default icbTasa */ - @ConfigItem - public Optional icbTasa; + Optional icbTasa(); } diff --git a/xsender/core/pom.xml b/xsender/core/pom.xml index bd1197a0..2607d086 100644 --- a/xsender/core/pom.xml +++ b/xsender/core/pom.xml @@ -85,7 +85,7 @@ commons-codec commons-codec - 1.16.0 + 1.17.0 @@ -96,7 +96,7 @@ org.junit.jupiter junit-jupiter-engine - 5.10.0 + 5.10.3 test @@ -108,7 +108,7 @@ org.mockito mockito-core - 5.5.0 + 5.11.0 test @@ -138,7 +138,7 @@ org.apache.cxf cxf-codegen-plugin - 4.0.2 + 4.0.5 generate-sources diff --git a/xsender/quarkus-extension/runtime/src/main/java/io/github/project/openubl/quarkus/xsender/XSender.java b/xsender/quarkus-extension/runtime/src/main/java/io/github/project/openubl/quarkus/xsender/XSender.java index 06ac2486..c1df2000 100644 --- a/xsender/quarkus-extension/runtime/src/main/java/io/github/project/openubl/quarkus/xsender/XSender.java +++ b/xsender/quarkus-extension/runtime/src/main/java/io/github/project/openubl/quarkus/xsender/XSender.java @@ -18,21 +18,21 @@ public class XSender { @ApplicationScoped @Named("cxfBillServiceEndpoint") CxfEndpoint produceCxfBillServiceEndpoint() { - return new CxfEndpointConfiguration().cxfBillServiceEndpoint(config.enableLoggingFeature); + return new CxfEndpointConfiguration().cxfBillServiceEndpoint(config.enableLoggingFeature()); } @Produces @ApplicationScoped @Named("cxfBillConsultServiceEndpoint") CxfEndpoint produceCxfBillConsultServiceEndpoint() { - return new CxfEndpointConfiguration().cxfBillConsultServiceEndpoint(config.enableLoggingFeature); + return new CxfEndpointConfiguration().cxfBillConsultServiceEndpoint(config.enableLoggingFeature()); } @Produces @ApplicationScoped @Named("cxfBillValidServiceEndpoint") CxfEndpoint produceCxfBillValidServiceEndpoint() { - return new CxfEndpointConfiguration().cxfBillValidServiceEndpoint(config.enableLoggingFeature); + return new CxfEndpointConfiguration().cxfBillValidServiceEndpoint(config.enableLoggingFeature()); } } diff --git a/xsender/quarkus-extension/runtime/src/main/java/io/github/project/openubl/quarkus/xsender/runtime/XSenderConfig.java b/xsender/quarkus-extension/runtime/src/main/java/io/github/project/openubl/quarkus/xsender/runtime/XSenderConfig.java index e1a25a9a..da45a3e0 100644 --- a/xsender/quarkus-extension/runtime/src/main/java/io/github/project/openubl/quarkus/xsender/runtime/XSenderConfig.java +++ b/xsender/quarkus-extension/runtime/src/main/java/io/github/project/openubl/quarkus/xsender/runtime/XSenderConfig.java @@ -1,15 +1,17 @@ package io.github.project.openubl.quarkus.xsender.runtime; -import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; -@ConfigRoot(name = "xsender", phase = ConfigPhase.RUN_TIME) -public class XSenderConfig { +@ConfigRoot(phase = ConfigPhase.RUN_TIME) +@ConfigMapping(prefix = "quarkus.xsender") +public interface XSenderConfig { /** * Enable logging feature */ - @ConfigItem - public boolean enableLoggingFeature; + @WithDefault("false") + boolean enableLoggingFeature(); } diff --git a/xsender/spring-boot-extension/integration-tests/pom.xml b/xsender/spring-boot-extension/integration-tests/pom.xml index 4e64a11d..9c2dfdb7 100644 --- a/xsender/spring-boot-extension/integration-tests/pom.xml +++ b/xsender/spring-boot-extension/integration-tests/pom.xml @@ -70,9 +70,9 @@ org.apache.maven.plugins maven-compiler-plugin - 11 - 11 - 11 + 21 + 21 + 21 From d2e3da4200392554e50dfb6b56a316406bb78f17 Mon Sep 17 00:00:00 2001 From: Edwin Luis Barboza Pinedo Date: Sat, 28 Mar 2026 22:29:57 -0500 Subject: [PATCH 4/8] chore: update documentation, upgrade Camel version, and reformat XSenderController code --- README.md | 17 +++++ examples/springbot/pom.xml | 12 +++- .../springboot/XSenderController.java | 65 ++++++++++--------- 3 files changed, 60 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 6796cee9..1c5706ef 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,23 @@ Compilación nativa con GraalVM soportada: mvn package -Pnative ``` +Pruebas unitarias y de integracion. +```bash +mvn clean install compile +mvn verify -Pexamples +mvn -Pnative-image install -f xbuilder/quarkus-extension/integration-tests/ -Dquarkus.version=3.12.0 +mvn -Pnative-image install -f xbuilder/quarkus-extension/integration-tests/ -Dquarkus.version=3.8.6 +mvn -Pnative-image install -f xbuilder/quarkus-extension/integration-tests/ +mvn -Pnative-image install -f xsender/quarkus-extension/integration-tests/ -Dquarkus.version=3.12.0 +mvn -Pnative-image install -f xsender/quarkus-extension/integration-tests/ -Dquarkus.version=3.8.6 +mvn -Pnative-image install -f xsender/quarkus-extension/integration-tests/ + +mvn install -f xsender/spring-boot-extension/integration-tests/ -Dspringboot.version=3.3.0 +mvn install -f xsender/spring-boot-extension/integration-tests/ +mvn install -f xsender/spring-boot-extension/integration-tests/ -Dspringboot.version=3.2.0 + +``` + ### Spring Boot ```xml diff --git a/examples/springbot/pom.xml b/examples/springbot/pom.xml index 009399b1..34cf4089 100644 --- a/examples/springbot/pom.xml +++ b/examples/springbot/pom.xml @@ -18,6 +18,7 @@ 3.3.1 UTF-8 3.2.5 + 4.18.1 @@ -32,7 +33,14 @@ org.apache.camel.springboot camel-spring-boot-bom - 4.4.0 + ${camel.version} + pom + import + + + org.apache.camel + camel-bom + ${camel.version} pom import @@ -47,7 +55,7 @@ io.github.project-openubl spring-boot-xsender - 4.1.4 + ${project.version} diff --git a/examples/springbot/src/main/java/io/github/project/openubl/quickstart/xbuilder/springboot/XSenderController.java b/examples/springbot/src/main/java/io/github/project/openubl/quickstart/xbuilder/springboot/XSenderController.java index 263e2955..87fd306f 100644 --- a/examples/springbot/src/main/java/io/github/project/openubl/quickstart/xbuilder/springboot/XSenderController.java +++ b/examples/springbot/src/main/java/io/github/project/openubl/quickstart/xbuilder/springboot/XSenderController.java @@ -20,46 +20,47 @@ @RestController public class XSenderController { - @Autowired - private CamelContext camelContext; + @Autowired + private CamelContext camelContext; - CompanyURLs companyURLs = CompanyURLs.builder() - .invoice("https://e-beta.sunat.gob.pe/ol-ti-itcpfegem-beta/billService") - .perceptionRetention("https://e-beta.sunat.gob.pe/ol-ti-itemision-otroscpe-gem-beta/billService") - .despatch("https://api-cpe.sunat.gob.pe/v1/contribuyente/gem") - .build(); + CompanyURLs companyURLs = CompanyURLs.builder() + .invoice("https://e-beta.sunat.gob.pe/ol-ti-itcpfegem-beta/billService") + .perceptionRetention( + "https://e-beta.sunat.gob.pe/ol-ti-itemision-otroscpe-gem-beta/billService") + .despatch("https://api-cpe.sunat.gob.pe/v1/contribuyente/gem") + .build(); - CompanyCredentials credentials = CompanyCredentials.builder() - .username("12345678959MODDATOS") - .password("MODDATOS") - .token("accessTokenParaGuiasDeRemision") - .build(); + CompanyCredentials credentials = CompanyCredentials.builder() + .username("12345678959MODDATOS") + .password("MODDATOS") + .token("accessTokenParaGuiasDeRemision") + .build(); - @PostMapping("/api/file/upload") - public String uploadFile(@RequestParam("file") MultipartFile file) throws Exception { - byte[] bytes = file.getBytes(); + @PostMapping("/api/file/upload") + public String uploadFile(@RequestParam("file") + MultipartFile file) throws Exception { + byte[] bytes = file.getBytes(); - BillServiceFileAnalyzer fileAnalyzer = new BillServiceXMLFileAnalyzer(bytes, companyURLs); + BillServiceFileAnalyzer fileAnalyzer = new BillServiceXMLFileAnalyzer(bytes, companyURLs); - // Archivo ZIP - ZipFile zipFile = fileAnalyzer.getZipFile(); + // Archivo ZIP + ZipFile zipFile = fileAnalyzer.getZipFile(); - // Configuración para enviar xml y Configuración para consultar ticket - BillServiceDestination fileDestination = fileAnalyzer.getSendFileDestination(); - BillServiceDestination ticketDestination = fileAnalyzer.getVerifyTicketDestination(); + // Configuración para enviar xml y Configuración para consultar ticket + BillServiceDestination fileDestination = fileAnalyzer.getSendFileDestination(); + BillServiceDestination ticketDestination = fileAnalyzer.getVerifyTicketDestination(); - // Send file - CamelData camelData = CamelUtils.getBillServiceCamelData(zipFile, fileDestination, credentials); + // Send file + CamelData camelData = CamelUtils.getBillServiceCamelData(zipFile, fileDestination, credentials); - SunatResponse sendFileSunatResponse = camelContext.createProducerTemplate() - .requestBodyAndHeaders( - Constants.XSENDER_BILL_SERVICE_URI, - camelData.getBody(), - camelData.getHeaders(), - SunatResponse.class - ); + SunatResponse sendFileSunatResponse = camelContext.createProducerTemplate() + .requestBodyAndHeaders( + Constants.XSENDER_BILL_SERVICE_URI, + camelData.getBody(), + camelData.getHeaders(), + SunatResponse.class); - return fileAnalyzer.getXmlContent().getDocumentType() + " " + sendFileSunatResponse.getStatus(); - } + return fileAnalyzer.getXmlContent().getDocumentType() + " " + sendFileSunatResponse.getStatus(); + } } From 1d4c60b04b7cf1785f4f4cb6ad6af9a87e950553 Mon Sep 17 00:00:00 2001 From: Edwin Luis Barboza Pinedo Date: Sat, 28 Mar 2026 22:37:09 -0500 Subject: [PATCH 5/8] chore: upgrade camel version to 4.18.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cf6c4fc8..be03ce4e 100644 --- a/pom.xml +++ b/pom.xml @@ -31,7 +31,7 @@ 2.10.0 3.15.1 - 4.4.0 + 4.18.1 3.2.5 From 43794dc9ab07938f690cdefd42a99ff63370ea1a Mon Sep 17 00:00:00 2001 From: Edwin Luis Barboza Pinedo Date: Sun, 29 Mar 2026 00:42:00 -0500 Subject: [PATCH 6/8] Add integration tests for validation coherence between GRERemitente and GRETransportista - Implemented ValidationCoherenceTest to ensure consistent validation results across GRERemitente, GRETransportista, and DespatchAdviceValidator. - Added tests for valid and invalid scenarios, including series and RUC validation. - Verified that validate() and validateDetailed() methods produce consistent results. - Ensured that warnings and errors are uniformly handled across all validators. Signed-off-by: Edwin Luis Barboza Pinedo --- xbuilder/core/docs/despatch-advice.md | 175 ++++ xbuilder/core/docs/validation-architecture.md | 106 ++ .../standard/guia/DespatchAdviceItem.java | 22 + .../guia/DespatchAdviceValidator.java | 264 +---- .../models/standard/guia/Destinatario.java | 14 + .../content/models/standard/guia/Destino.java | 11 + .../models/standard/guia/DocumentoBaja.java | 10 + .../standard/guia/DocumentoRelacionado.java | 12 + .../models/standard/guia/GRERemitente.java | 144 +-- .../standard/guia/GRETransportista.java | 172 ++- .../standard/guia/GuiaItemAttribute.java | 17 + .../content/models/standard/guia/Partida.java | 11 + .../models/standard/guia/Remitente.java | 13 + .../models/standard/guia/Transportista.java | 18 + .../DespatchAdviceCommonValidator.java | 438 ++++++++ .../guia/validation/ValidationMessage.java | 80 ++ .../guia/validation/ValidationResult.java | 99 ++ .../guia/validation/ValidationSeverity.java | 29 + .../DespatchAdviceTipoComprobanteRule.java | 8 +- .../DespatchAdviceCommonValidatorTest.java | 983 ++++++++++++++++++ .../validator/ValidationCoherenceTest.java | 315 ++++++ 21 files changed, 2548 insertions(+), 393 deletions(-) create mode 100644 xbuilder/core/docs/despatch-advice.md create mode 100644 xbuilder/core/docs/validation-architecture.md create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/validation/DespatchAdviceCommonValidator.java create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/validation/ValidationMessage.java create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/validation/ValidationResult.java create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/validation/ValidationSeverity.java create mode 100644 xbuilder/core/src/test/java/unit/validator/DespatchAdviceCommonValidatorTest.java create mode 100644 xbuilder/core/src/test/java/unit/validator/ValidationCoherenceTest.java diff --git a/xbuilder/core/docs/despatch-advice.md b/xbuilder/core/docs/despatch-advice.md new file mode 100644 index 00000000..5113922a --- /dev/null +++ b/xbuilder/core/docs/despatch-advice.md @@ -0,0 +1,175 @@ +# xbuilder — Guía de Remisión Electrónica (GRE) + +Módulo de construcción, validación y renderizado XML de Guías de Remisión Electrónica +para SUNAT. Soporta los dos tipos definidos por RS 000123-2022/SUNAT: + +| Tipo | Código | Serie | Emisor | +|------|--------|-------|----------------------------| +| GRE-Remitente | 09 | Txxx | Remitente de los bienes | +| GRE-Transportista | 31 | Vxxx | Transportista contratado | + +## Arquitectura + +``` +content/models/standard/guia/ +├── DespatchAdvice.java # Modelo principal UBL +├── GRERemitente.java # Wrapper tipo 09 con validación y conversión +├── GRETransportista.java # Wrapper tipo 31 con validación y conversión +├── DespatchAdviceValidator.java # Validador estático para DespatchAdvice +├── validation/ +│ ├── DespatchAdviceCommonValidator.java # Reglas compartidas centralizadas +│ ├── ValidationSeverity.java # ERROR | WARNING +│ ├── ValidationMessage.java # Mensaje con severidad +│ └── ValidationResult.java # Resultado agregado (errores + advertencias) +├── Envio.java # Datos de shipment +├── Remitente.java # Datos del remitente (RUC + razón social) +├── Transportista.java # Datos del transportista +├── Destinatario.java # Destinatario de los bienes +├── Tercero.java # Remitente original (para tipo 31) +├── Driver.java # Conductor/chofer +├── Vehicle.java # Vehículo (principal + secundarios) +├── Partida.java # Punto de partida (UBIGEO + dirección) +├── Destino.java # Punto de destino +├── DespatchAdviceItem.java # Línea de detalle (bien trasladado) +├── Contenedor.java # Contenedor (comercio exterior) +├── DeclaracionAduanera.java # DAM/DS (comercio exterior) +├── Puerto.java # Puerto/aeropuerto +├── DocumentoBaja.java # Referencia a GRE anulada +├── DocumentoRelacionado.java # Documento relacionado (Cat. 21) +├── DocumentoAdicional.java # Documento adicional (Cat. 61) +└── GuiaItemAttribute.java # Atributo adicional de ítem +``` + +## Flujo de uso + +```java +// 1. Construir modelo +GRERemitente gre = GRERemitente.builder() + .serie("T001").numero(1) + .remitente(Remitente.builder().ruc("20100010001").razonSocial("Mi Empresa").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("6").numeroDocumentoIdentidad("20200020002") + .nombre("Cliente S.A.C.").build()) + .envio(Envio.builder() + .tipoTraslado("01").pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("02").fechaTraslado(LocalDate.now()) + .chofer(Driver.builder().tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("12345678").nombres("Juan") + .apellidos("Perez").licencia("Q123").build()) + .vehiculo(Vehicle.builder().placa("ABC-123").build()) + .partida(Partida.builder().ubigeo("150101").direccion("Av. Origen 100").build()) + .destino(Destino.builder().ubigeo("150102").direccion("Av. Destino 200").build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()) + .build(); + +// 2. Validar (opción A: solo errores) +List errors = gre.validate(); + +// 2b. Validar (opción B: errores + advertencias) +ValidationResult result = gre.validateDetailed(); +result.getErrors(); // List — bloquean emisión +result.getWarnings(); // List — recomendaciones + +// 3. Convertir a DespatchAdvice (valida automáticamente) +DespatchAdvice da = gre.toDespatchAdviceValidated(); + +// 4. Enriquecer +contentEnricher.enrich(da); + +// 5. Renderizar XML +Template template = TemplateProducer.getInstance().getDespatchAdvice(); +String xml = template.data(da).render(); +``` + +## Validación + +### Validador centralizado + +Toda la lógica de validación compartida vive en `DespatchAdviceCommonValidator`. Los tres +puntos de entrada (`DespatchAdviceValidator.validate()`, `GRERemitente.validate()`, +`GRETransportista.validate()`) delegan a este validador común para evitar duplicación. + +### Criterio de severidad + +| Severidad | Significado | Ejemplo | +|-----------|-------------|---------| +| **ERROR** | SUNAT rechaza el documento | Serie T* con tipo 31, falta conductor | +| **WARNING** | Recomendación normativa, no bloquea | Comercio exterior sin DAM/DS | + +Las advertencias surgen principalmente de las reglas de comercio exterior +(RS 000240-2024/SUNAT) cuya obligatoriedad fue pospuesta al 01-jul-2026 +por RS 000133-2025/SUNAT. + +### Reglas comunes (centralizadas) + +| Regla | Aplica a | Severidad | +|-------|----------|-----------| +| Serie requerida | Todos | ERROR | +| Número > 0 | Todos | ERROR | +| Serie T* para tipo 09 | DespatchAdvice, GRERemitente | ERROR | +| Serie V* para tipo 31 | DespatchAdvice, GRETransportista | ERROR | +| Remitente con RUC 11 dígitos | Todos | ERROR | +| Destinatario requerido | Todos | ERROR | +| Envío requerido (motivo, peso, modalidad, fecha) | Todos | ERROR | +| Partida y destino requeridos | Todos | ERROR | +| Al menos 1 línea de detalle | Todos | ERROR | +| Comercio exterior: DAM/DS recomendado | Todos | WARNING | +| Comercio exterior: puerto/aeropuerto recomendado | Todos | WARNING | + +### Reglas específicas GRE-Remitente (tipo 09) + +| Regla | Severidad | +|-------|-----------| +| Transporte privado (02): conductor y vehículo obligatorios (salvo M1/L) | ERROR | +| Transporte privado: NO transportista externo | ERROR | +| Transporte público (01): transportista obligatorio | ERROR | + +### Reglas específicas GRE-Transportista (tipo 31) + +| Regla | Severidad | +|-------|-----------| +| TransportistaEmisor con RUC 11 dígitos | ERROR | +| Tercero (remitente original) obligatorio | ERROR | +| Conductor(es) siempre obligatorio(s) | ERROR | +| Vehículo siempre obligatorio | ERROR | + +## Tipos 09 vs 31 + +| Aspecto | GRE-Remitente (09) | GRE-Transportista (31) | +|---------|-------------------|----------------------| +| Serie | Txxx | Vxxx | +| Emisor del XML | Remitente de bienes | Transportista | +| `DespatchSupplierParty` | Remitente | TransportistaEmisor | +| `SellerSupplierParty` | — | Tercero (remitente original) | +| Conductor/vehículo | Según modalidad | Siempre obligatorio | +| Transportista en envío | Solo en transporte público | — | + +## Comercio exterior (RS 000240-2024) + +Para motivos de traslado 08, 09, 10, 19: + +- `Envio.declaracionesAduaneras`: lista de `DeclaracionAduanera` (DAM/DS) +- `Envio.puerto` / `Envio.aeropuerto`: puerto o aeropuerto +- `Envio.contenedores`: lista de `Contenedor` con número y precinto + +> **Nota**: La obligatoriedad del motivo 19 y la derogación del ticket de salida +> fue pospuesta al 01-jul-2026 por RS 000133-2025/SUNAT. + +## Enriquecimiento (enricher) + +El `ContentEnricher` aplica reglas automáticas post-construcción: + +- `DespatchAdviceTipoComprobanteRule`: si `tipoComprobante` es null, lo deduce + de la serie (T* → "09", V* → "31") +- `FechaEmisionRule`: completa `fechaEmision` si es null +- `FirmanteRule`: completa `firmante` si es null + +## Normativa de referencia + +| Resolución | Contenido | +|------------|-----------| +| RS 000123-2022/SUNAT | Reglas base GRE-Remitente y GRE-Transportista | +| RS 000240-2024/SUNAT | Comercio exterior: DAM/DS, contenedores, puertos | +| RS 000133-2025/SUNAT | Prórroga: motivo 19 y ticket de salida al 01-jul-2026 | diff --git a/xbuilder/core/docs/validation-architecture.md b/xbuilder/core/docs/validation-architecture.md new file mode 100644 index 00000000..c5ca0d75 --- /dev/null +++ b/xbuilder/core/docs/validation-architecture.md @@ -0,0 +1,106 @@ +# xbuilder — Validation Architecture + +## Overview + +The validation subsystem provides early detection of business rule violations +before submitting GRE documents to SUNAT. It does **not** replace SUNAT's +XSD/XSL validation; it is a complementary layer. + +## Design Principles + +### Centralized Rules (DRY) + +All shared validation rules live in a single class: + +``` +validation/DespatchAdviceCommonValidator.java +``` + +The three entry points delegate to it: + +``` +DespatchAdviceValidator.validate(DespatchAdvice) → common + generic-type rules +GRERemitente.validate() → common + remitente rules +GRETransportista.validate() → common + transportista rules +``` + +This eliminates the prior duplication where the same rule (e.g., "serie required", +"RUC must be 11 digits") was coded in 3 places with risk of functional drift. + +### Severity Levels + +```java +public enum ValidationSeverity { + ERROR, // SUNAT will reject the document + WARNING // Recommendation; not blocking +} +``` + +**Criteria for ERROR**: +- Fields required by UBL schema +- Fields required by SUNAT functional rules (RS 000123-2022) +- Coherence rules (serie prefix vs. tipo comprobante) + +**Criteria for WARNING**: +- Commerce exterior recommendations (DAM/DS, puerto) whose mandatory enforcement + is deferred by RS 000133-2025/SUNAT to 01-jul-2026 +- Fields that depend on business context + +### Backward Compatibility + +The `validate()` methods continue returning `List` (errors only). +A new `validateDetailed()` method returns `ValidationResult` with both +errors and warnings. + +```java +// Old API (unchanged) +List errors = gre.validate(); + +// New API +ValidationResult result = gre.validateDetailed(); +result.getErrors(); // List +result.getWarnings(); // List +result.hasErrors(); // boolean +result.isValid(); // boolean (no errors) +``` + +## Class Diagram + +``` +┌──────────────────────────────────┐ +│ DespatchAdviceCommonValidator │ ← All shared rules +│ ───────────────────────────── │ +│ + validateBasicFields() │ +│ + validateSerieCoherence() │ +│ + validateSerieRemitente() │ +│ + validateSerieTransportista() │ +│ + validateRemitente() │ +│ + validateTransportistaEmisor() │ +│ + validateDestinatario() │ +│ + validateTerceroTransportista()│ +│ + validateEnvioRequired() │ +│ + validatePartidaDestino() │ +│ + validateModalidadRemitente() │ +│ + validateModalidadGeneric() │ +│ + validateConductorVehiculo...()│ +│ + validateComercioExterior() │ +│ + validateDetalles() │ +└──────────┬───────────────────────┘ + │ delegates to + ┌──────┼──────────────┐ + │ │ │ + ▼ ▼ ▼ +┌────────┐ ┌──────────┐ ┌───────────────┐ +│Dispatch│ │GRERemit. │ │GRETransport. │ +│Advice │ │.validate │ │.validate() │ +│Validat.│ │() │ │ │ +└────────┘ └──────────┘ └───────────────┘ +``` + +## Adding New Rules + +1. Add the method to `DespatchAdviceCommonValidator` if the rule is shared. +2. Call it from the relevant entry points. +3. Choose the correct severity (`ERROR` or `WARNING`). +4. Add corresponding unit tests to the existing test classes. +5. Update `despatch-advice.md` rule tables. diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdviceItem.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdviceItem.java index 681b7946..dd973c79 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdviceItem.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdviceItem.java @@ -9,18 +9,40 @@ import java.math.BigDecimal; import java.util.List; +/** + * Línea de detalle de la Guía de Remisión Electrónica. + *

+ * Cada ítem representa un bien trasladado con su cantidad, unidad de medida, descripción y código. Toda GRE requiere al + * menos un ítem (requerimiento UBL). + *

+ * FAQ #24 SUNAT: cuando se usa el indicador de traslado total DAM/DS, la línea puede contener solo los campos mínimos + * obligatorios. + * + * @since 2.0 + * @see DespatchAdvice + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class DespatchAdviceItem { + + /** Unidad de medida según código UN/ECE Rec. 20 (ejemplo: "NIU", "KGM"). */ private String unidadMedida; + + /** Cantidad del bien trasladado. */ private BigDecimal cantidad; + /** Descripción del bien. */ private String descripcion; + + /** Código interno del bien asignado por el emisor. */ private String codigo; + + /** Código SUNAT del bien (Catálogo según corresponda). */ private String codigoSunat; + /** Atributos adicionales del ítem (pares código-valor). */ @Singular private List atributos; } diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdviceValidator.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdviceValidator.java index b30f299d..fc27f93e 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdviceValidator.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DespatchAdviceValidator.java @@ -1,10 +1,11 @@ package io.github.project.openubl.xbuilder.content.models.standard.guia; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.DespatchAdviceCommonValidator; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationMessage; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationResult; + import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; import java.util.List; -import java.util.Set; /** * Validador de reglas de negocio para la Guía de Remisión Electrónica (GRE). @@ -12,239 +13,88 @@ * Implementa las reglas funcionales de SUNAT según: *

    *
  • RS 000123-2022/SUNAT — Reglas base GRE-Remitente y GRE-Transportista
  • - *
  • RS 000240-2024/SUNAT — Reglas de comercio exterior (vigentes desde - * 14-nov-2024)
  • - *
  • RS 000133-2025/SUNAT — Prórroga: derogatoria del ticket de salida al - * 01-jul-2026
  • + *
  • RS 000240-2024/SUNAT — Reglas de comercio exterior (vigentes desde 14-nov-2024)
  • + *
  • RS 000133-2025/SUNAT — Prórroga: derogatoria del ticket de salida al 01-jul-2026
  • *
*

- * Las validaciones retornan una lista de mensajes de error. Si la lista está - * vacía, - * el documento es válido respecto a las reglas implementadas. + * Delega la lógica de validación a {@link DespatchAdviceCommonValidator} para evitar duplicación de reglas con + * {@link GRERemitente} y {@link GRETransportista}. + *

+ * Las validaciones retornan una lista de mensajes de error. Si la lista está vacía, el documento es válido respecto a + * las reglas implementadas. *

- * IMPORTANTE: Este validador NO reemplaza la validación de SUNAT - * (XSD/XSL). - * Es una capa de validación temprana para detectar errores comunes antes del - * envío. + * IMPORTANTE: Este validador NO reemplaza la validación de SUNAT (XSD/XSL). Es una capa de validación temprana + * para detectar errores comunes antes del envío. + * + * @since 5.0.0 + * @see DespatchAdviceCommonValidator + * @see ValidationResult */ public class DespatchAdviceValidator { - /** - * Motivos de traslado que requieren referencia a DAM/DS (comercio exterior). - */ - private static final Set MOTIVOS_COMERCIO_EXTERIOR = new HashSet<>(Arrays.asList( - "08", // Importación - "09", // Exportación - "10", // Importación con DAM (RS 240-2024) - "19" // Mercancía extranjera (RS 240-2024) - )); - - /** Motivos de traslado que requieren puerto o aeropuerto. */ - private static final Set MOTIVOS_CON_PUERTO = new HashSet<>(Arrays.asList( - "08", // Importación - "09", // Exportación - "10", // Importación con DAM - "19" // Mercancía extranjera - )); - /** * Valida un DespatchAdvice completo y retorna la lista de errores encontrados. + *

+ * Nota: Solo retorna errores (severidad {@code ERROR}). Para obtener errores y advertencias con severidad + * diferenciada, use {@link #validateDetailed(DespatchAdvice)}. * * @param da el DespatchAdvice a validar * @return lista de mensajes de error (vacía si es válido) */ public static List validate(DespatchAdvice da) { - List errors = new ArrayList<>(); - - validateBasicFields(da, errors); - validateSerie(da, errors); - validateParties(da, errors); - validateEnvio(da, errors); - validateDetalles(da, errors); - - return errors; - } - - private static void validateBasicFields(DespatchAdvice da, List errors) { - if (da.getSerie() == null || da.getSerie().isBlank()) { - errors.add("La serie es requerida"); - } - if (da.getNumero() == null || da.getNumero() < 1) { - errors.add("El número debe ser mayor a 0"); - } + return validateDetailed(da).getErrors(); } /** - * Valida la coherencia entre la serie y el tipo de comprobante. + * Valida un DespatchAdvice completo y retorna un {@link ValidationResult} con errores y advertencias diferenciados + * por severidad. *

- * Regla SUNAT FAQ #7: - * - GRE-Remitente: Serie TXXX - * - GRE-Transportista: Serie VXXX + * Criterio de severidad: + *

    + *
  • {@code ERROR} — Regla UBL o funcional SUNAT que causa rechazo
  • + *
  • {@code WARNING} — Recomendación normativa (discrecionalidad vigente)
  • + *
+ * + * @param da el DespatchAdvice a validar + * @return resultado de validación con errores y advertencias + * @since 5.2.0 */ - private static void validateSerie(DespatchAdvice da, List errors) { - if (da.getSerie() == null || da.getTipoComprobante() == null) { - return; - } - String serie = da.getSerie().toUpperCase(); - String tipo = da.getTipoComprobante(); + public static ValidationResult validateDetailed(DespatchAdvice da) { + List messages = new ArrayList<>(); - if ("09".equals(tipo) && !serie.startsWith("T")) { - errors.add("GRE-Remitente (09) requiere serie que inicie con 'T'. Serie actual: " + da.getSerie()); - } - if ("31".equals(tipo) && !serie.startsWith("V")) { - errors.add("GRE-Transportista (31) requiere serie que inicie con 'V'. Serie actual: " + da.getSerie()); - } - } + // Campos básicos + DespatchAdviceCommonValidator.validateBasicFields(da.getSerie(), da.getNumero(), messages); - /** - * Valida las partes (remitente, destinatario, tercero) según el tipo de GRE. - */ - private static void validateParties(DespatchAdvice da, List errors) { - if (da.getRemitente() == null) { - errors.add("El remitente es requerido"); - return; - } - if (da.getRemitente().getRuc() == null || da.getRemitente().getRuc().length() != 11) { - errors.add("El RUC del remitente debe tener 11 dígitos"); - } + // Coherencia serie ↔ tipo comprobante + DespatchAdviceCommonValidator.validateSerieCoherence( + da.getSerie(), da.getTipoComprobante(), messages); - if (da.getDestinatario() == null) { - // FAQ #22: Para emisor itinerante, el destinatario puede ser el mismo - // remitente. - // Pero el campo sigue siendo obligatorio en el XML. - errors.add("El destinatario es requerido"); - } + // Partes + DespatchAdviceCommonValidator.validateRemitente(da.getRemitente(), messages); + DespatchAdviceCommonValidator.validateDestinatario(da.getDestinatario(), messages); - // GRE-Transportista (31): el tercero (remitente original) debería estar - // presente + // Tercero obligatorio para GRE-Transportista (31) if ("31".equals(da.getTipoComprobante()) && da.getTercero() == null && da.getProveedor() == null) { - errors.add("GRE-Transportista (31): se recomienda consignar el tercero (remitente original) " + - "o proveedor en SellerSupplierParty"); - } - } - - /** - * Valida los datos de envío/shipment según las reglas funcionales SUNAT. - */ - private static void validateEnvio(DespatchAdvice da, List errors) { - Envio envio = da.getEnvio(); - if (envio == null) { - errors.add("Los datos de envío son requeridos"); - return; - } - - if (envio.getTipoTraslado() == null || envio.getTipoTraslado().isBlank()) { - errors.add("El motivo de traslado (Catálogo 20) es requerido"); - } - if (envio.getPesoTotal() == null) { - errors.add("El peso total es requerido"); - } - if (envio.getTipoModalidadTraslado() == null) { - errors.add("La modalidad de traslado (Catálogo 18) es requerida"); - } - if (envio.getFechaTraslado() == null) { - errors.add("La fecha de traslado es requerida"); - } - - validateEnvioModalidad(da, envio, errors); - validateEnvioComercioExterior(envio, errors); - } - - /** - * Valida reglas de modalidad de transporte. - *

- * Transporte privado (02): requiere conductor y vehículo. - * Transporte público (01): requiere datos del transportista. - */ - private static void validateEnvioModalidad(DespatchAdvice da, Envio envio, List errors) { - String modalidad = envio.getTipoModalidadTraslado(); - if (modalidad == null) - return; - - boolean isGRERemitente = "09".equals(da.getTipoComprobante()); - - if ("02".equals(modalidad) && isGRERemitente) { - // Transporte privado en GRE-Remitente: conductor y vehículo requeridos - boolean tieneIndicadorM1L = envio.getIndicadores() != null && - envio.getIndicadores().contains("SUNAT_Envio_IndicadorTrasladoVehiculoM1L"); - - if (!tieneIndicadorM1L) { - if (envio.getChoferes() == null || envio.getChoferes().isEmpty()) { - errors.add("Transporte privado en GRE-Remitente requiere al menos un conductor " + - "(salvo vehículo categoría M1/L)"); - } - if (envio.getVehiculo() == null) { - errors.add("Transporte privado en GRE-Remitente requiere datos del vehículo " + - "(salvo vehículo categoría M1/L)"); - } - } - } - - if ("01".equals(modalidad) && isGRERemitente) { - // Transporte público en GRE-Remitente: transportista requerido - if (envio.getTransportista() == null) { - errors.add("Transporte público en GRE-Remitente requiere datos del transportista"); - } + messages.add(ValidationMessage.error( + "GRE-Transportista (31): el tercero (remitente original) o proveedor " + + "en SellerSupplierParty es requerido")); } - // GRE-Transportista (31): siempre requiere conductor y vehículo - if ("31".equals(da.getTipoComprobante())) { - if (envio.getChoferes() == null || envio.getChoferes().isEmpty()) { - errors.add("GRE-Transportista requiere al menos un conductor"); - } - if (envio.getVehiculo() == null) { - errors.add("GRE-Transportista requiere datos del vehículo"); - } - } - } + // Envío + DespatchAdviceCommonValidator.validateEnvioRequired(da.getEnvio(), messages); + DespatchAdviceCommonValidator.validatePartidaDestino(da.getEnvio(), messages); - /** - * Valida reglas de comercio exterior (RS 000240-2024/SUNAT). - *

- * Cuando el motivo de traslado es de comercio exterior: - *

    - *
  • Se recomienda incluir referencia DAM/DS
  • - *
  • Se recomienda incluir puerto o aeropuerto
  • - *
  • Se recomienda incluir contenedores
  • - *
- *

- * NOTA: Estas reglas son actualmente de recomendación. La obligatoriedad plena - * del motivo 19 y la derogación del ticket de salida fue pospuesta al - * 01-jul-2026 - * por RS 000133-2025/SUNAT. Hasta esa fecha se aplica discrecionalidad. - */ - private static void validateEnvioComercioExterior(Envio envio, List errors) { - String motivo = envio.getTipoTraslado(); - if (motivo == null || !MOTIVOS_COMERCIO_EXTERIOR.contains(motivo)) { - return; - } + // Modalidad de transporte + DespatchAdviceCommonValidator.validateModalidadGeneric( + da.getTipoComprobante(), da.getEnvio(), messages); - // Advertencia suave: no es error pero se recomienda - boolean tieneDAM = (envio.getDeclaracionesAduaneras() != null && !envio.getDeclaracionesAduaneras().isEmpty()); - if (!tieneDAM) { - // No se marca como error porque puede ser traslado para exportación sin DAM - // numerada (FAQ #32) - // Este es un caso donde la ambigüedad normativa indica que se puede usar motivo - // "otros" - } + // Comercio exterior (advertencias) + DespatchAdviceCommonValidator.validateComercioExterior(da.getEnvio(), messages); - // Puerto/aeropuerto recomendado para comercio exterior - if (MOTIVOS_CON_PUERTO.contains(motivo) && envio.getPuerto() == null && envio.getAeropuerto() == null) { - // No error estricto, pero recomendable - } - } + // Detalles + DespatchAdviceCommonValidator.validateDetalles(da.getDetalles(), messages); - /** - * Valida los detalles/líneas del documento. - *

- * Siempre debe haber al menos 1 línea (requerimiento UBL). - * FAQ #24: Con indicador de traslado total DAM/DS, la línea puede estar vacía - * (campos mínimos obligatorios por UBL). - */ - private static void validateDetalles(DespatchAdvice da, List errors) { - if (da.getDetalles() == null || da.getDetalles().isEmpty()) { - errors.add("Se requiere al menos una línea de detalle (requerimiento UBL)"); - } + return new ValidationResult(messages); } private DespatchAdviceValidator() { diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Destinatario.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Destinatario.java index 31bba93d..c2258865 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Destinatario.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Destinatario.java @@ -6,18 +6,32 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Datos del destinatario de los bienes en la Guía de Remisión Electrónica. + *

+ * Se mapea a {@code cac:DeliveryCustomerParty} en el XML UBL. El destinatario es obligatorio en toda GRE. + *

+ * FAQ #22 SUNAT: para emisor itinerante, el destinatario puede ser el mismo remitente, pero el campo sigue siendo + * obligatorio en el XML. + * + * @since 2.0 + * @see DespatchAdvice#getDestinatario() + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Destinatario { + /** Tipo de documento de identidad del destinatario (Catálogo 06). */ @Schema(description = "Catalogo 06", requiredMode = Schema.RequiredMode.REQUIRED) private String tipoDocumentoIdentidad; + /** Número de documento de identidad del destinatario. */ @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private String numeroDocumentoIdentidad; + /** Razón social o nombre del destinatario. */ @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private String nombre; } diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Destino.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Destino.java index 1093c9d0..b77edeb1 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Destino.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Destino.java @@ -6,15 +6,26 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Punto de destino/llegada del traslado en la Guía de Remisión Electrónica. + *

+ * Se mapea a {@code cac:Shipment/cac:Delivery/cac:DeliveryAddress} en el XML UBL. El UBIGEO y la dirección son + * obligatorios. + * + * @since 2.0 + * @see Envio#getDestino() + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Destino { + /** Código UBIGEO INEI del punto de destino (6 dígitos). */ @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private String ubigeo; + /** Dirección completa del punto de destino. */ @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private String direccion; diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DocumentoBaja.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DocumentoBaja.java index f0a7e6a9..19c636e0 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DocumentoBaja.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DocumentoBaja.java @@ -6,15 +6,25 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Referencia al documento dado de baja que justifica la emisión de una nueva Guía de Remisión Electrónica. + *

+ * Se mapea a {@code cac:OrderReference} en el XML UBL. Aplica cuando la GRE actual reemplaza a una GRE previamente + * anulada. + * + * @since 2.0 + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class DocumentoBaja { + /** Tipo de documento del comprobante dado de baja (Catálogo 01). */ @Schema(description = "Catalog 01", requiredMode = Schema.RequiredMode.REQUIRED) private String tipoDocumento; + /** Serie-número del comprobante dado de baja (ejemplo: "T001-123"). */ @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private String serieNumero; } diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DocumentoRelacionado.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DocumentoRelacionado.java index 47b86140..6f032142 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DocumentoRelacionado.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/DocumentoRelacionado.java @@ -6,15 +6,27 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Referencia a un documento relacionado con la Guía de Remisión Electrónica. + *

+ * Se mapea a {@code cac:AdditionalDocumentReference} en el XML UBL. Permite vincular la GRE con facturas, guías + * previas, u otros documentos tributarios (Catálogo 21). + * + * @since 2.0 + * @see DespatchAdvice#getDocumentoRelacionado() + * @see DespatchAdvice#getDocumentosRelacionados() + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class DocumentoRelacionado { + /** Tipo de documento relacionado (Catálogo 21). */ @Schema(description = "Catalog 21", requiredMode = Schema.RequiredMode.REQUIRED) private String tipoDocumento; + /** Serie-número del documento relacionado (ejemplo: "F001-456"). */ @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private String serieNumero; } diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GRERemitente.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GRERemitente.java index e54118f5..cfb4bcc1 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GRERemitente.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GRERemitente.java @@ -1,6 +1,9 @@ package io.github.project.openubl.xbuilder.content.models.standard.guia; import io.github.project.openubl.xbuilder.content.models.common.Firmante; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.DespatchAdviceCommonValidator; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationMessage; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationResult; import lombok.Builder; import lombok.Data; import lombok.Singular; @@ -27,15 +30,27 @@ * *

{@code
  * GRERemitente gre = GRERemitente.builder()
- *         .serie("T001").numero(1)
+ *         .serie("T001")
+ *         .numero(1)
  *         .remitente(Remitente.builder().ruc("20100010001").razonSocial("Mi Empresa").build())
  *         .destinatario(Destinatario.builder()
- *                 .tipoDocumentoIdentidad("6").numeroDocumentoIdentidad("20200020002").nombre("Cliente").build())
+ *                 .tipoDocumentoIdentidad("6")
+ *                 .numeroDocumentoIdentidad("20200020002")
+ *                 .nombre("Cliente")
+ *                 .build())
  *         .envio(Envio.builder()
- *                 .tipoTraslado("01").pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM")
- *                 .tipoModalidadTraslado("02").fechaTraslado(LocalDate.now())
- *                 .chofer(Driver.builder().tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("12345678")
- *                         .nombres("Juan").apellidos("Perez").licencia("Q123").build())
+ *                 .tipoTraslado("01")
+ *                 .pesoTotal(BigDecimal.ONE)
+ *                 .pesoTotalUnidadMedida("KGM")
+ *                 .tipoModalidadTraslado("02")
+ *                 .fechaTraslado(LocalDate.now())
+ *                 .chofer(Driver.builder()
+ *                         .tipoDocumentoIdentidad("1")
+ *                         .numeroDocumentoIdentidad("12345678")
+ *                         .nombres("Juan")
+ *                         .apellidos("Perez")
+ *                         .licencia("Q123")
+ *                         .build())
  *                 .vehiculo(Vehicle.builder().placa("ABC-123").build())
  *                 .partida(Partida.builder().ubigeo("150101").direccion("Origen").build())
  *                 .destino(Destino.builder().ubigeo("150102").direccion("Destino").build())
@@ -91,104 +106,59 @@ public class GRERemitente {
 
     /**
      * Valida las reglas de negocio para GRE-Remitente y retorna los errores.
+     * 

+ * Delega las validaciones comunes a {@link DespatchAdviceCommonValidator} y aplica solo las reglas específicas del + * tipo 09 (serie T*, modalidad remitente). + *

+ * Solo retorna errores (severidad {@code ERROR}). Para obtener errores y advertencias diferenciados, use + * {@link #validateDetailed()}. * * @return lista de errores (vacía si es válido) + * @see #validateDetailed() */ public List validate() { - List errors = new ArrayList<>(); + return validateDetailed().getErrors(); + } - // Serie - if (serie == null || serie.isBlank()) { - errors.add("La serie es requerida"); - } else if (!serie.toUpperCase().startsWith("T")) { - errors.add("GRE-Remitente requiere serie que inicie con 'T'. Serie actual: " + serie); - } + /** + * Valida las reglas de negocio y retorna un {@link ValidationResult} con errores y advertencias diferenciados por + * severidad. + * + * @return resultado de validación con errores y advertencias + * @since 5.2.0 + */ + public ValidationResult validateDetailed() { + List messages = new ArrayList<>(); - // Número - if (numero == null || numero < 1) { - errors.add("El número debe ser mayor a 0"); - } + // Campos básicos (serie, número) + DespatchAdviceCommonValidator.validateBasicFields(serie, numero, messages); - // Remitente - if (remitente == null) { - errors.add("El remitente es requerido"); - } else if (remitente.getRuc() == null || remitente.getRuc().length() != 11) { - errors.add("El RUC del remitente debe tener 11 dígitos"); - } + // Serie específica remitente: debe iniciar con 'T' + DespatchAdviceCommonValidator.validateSerieRemitente(serie, messages); - // Destinatario - if (destinatario == null) { - errors.add("El destinatario es requerido"); - } + // Partes + DespatchAdviceCommonValidator.validateRemitente(remitente, messages); + DespatchAdviceCommonValidator.validateDestinatario(destinatario, messages); // Envío - if (envio == null) { - errors.add("Los datos de envío son requeridos"); - } else { - validateEnvio(envio, errors); - } + DespatchAdviceCommonValidator.validateEnvioRequired(envio, messages); + DespatchAdviceCommonValidator.validatePartidaDestino(envio, messages); - // Detalles - if (detalles == null || detalles.isEmpty()) { - errors.add("Se requiere al menos una línea de detalle"); - } + // Modalidad específica remitente (privado/público) + DespatchAdviceCommonValidator.validateModalidadRemitente(envio, messages); - return errors; - } + // Comercio exterior (advertencias) + DespatchAdviceCommonValidator.validateComercioExterior(envio, messages); - private void validateEnvio(Envio envio, List errors) { - if (envio.getTipoTraslado() == null || envio.getTipoTraslado().isBlank()) { - errors.add("El motivo de traslado (Catálogo 20) es requerido"); - } - if (envio.getPesoTotal() == null) { - errors.add("El peso total es requerido"); - } - if (envio.getTipoModalidadTraslado() == null) { - errors.add("La modalidad de traslado (Catálogo 18) es requerida"); - } - if (envio.getFechaTraslado() == null) { - errors.add("La fecha de traslado es requerida"); - } - if (envio.getPartida() == null) { - errors.add("El punto de partida es requerido"); - } - if (envio.getDestino() == null) { - errors.add("El punto de destino es requerido"); - } + // Detalles + DespatchAdviceCommonValidator.validateDetalles(detalles, messages); - String modalidad = envio.getTipoModalidadTraslado(); - if (modalidad == null) - return; - - boolean tieneIndicadorM1L = envio.getIndicadores() != null && - envio.getIndicadores().contains("SUNAT_Envio_IndicadorTrasladoVehiculoM1L"); - - if ("02".equals(modalidad)) { - // Transporte privado: requiere conductor y vehículo - if (!tieneIndicadorM1L) { - if (envio.getChoferes() == null || envio.getChoferes().isEmpty()) { - errors.add("Transporte privado requiere al menos un conductor (salvo vehículo categoría M1/L)"); - } - if (envio.getVehiculo() == null) { - errors.add("Transporte privado requiere datos del vehículo (salvo vehículo categoría M1/L)"); - } - } - // Transporte privado NO debe tener transportista externo - if (envio.getTransportista() != null) { - errors.add( - "Transporte privado no debe consignar transportista externo (usar modalidad pública si subcontrata)"); - } - } else if ("01".equals(modalidad)) { - // Transporte público: requiere transportista - if (envio.getTransportista() == null) { - errors.add("Transporte público requiere datos del transportista"); - } - } + return new ValidationResult(messages); } /** - * Convierte este modelo a {@link DespatchAdvice} para renderizado XML. - * El tipo de comprobante se fija a "09" (GRE-Remitente). + * Convierte este modelo a {@link DespatchAdvice} para renderizado XML. El tipo de comprobante se fija a "09" + * (GRE-Remitente). * * @return DespatchAdvice listo para enriquecer y renderizar */ diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GRETransportista.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GRETransportista.java index 4ddf0cd3..a5002f4b 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GRETransportista.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GRETransportista.java @@ -1,6 +1,9 @@ package io.github.project.openubl.xbuilder.content.models.standard.guia; import io.github.project.openubl.xbuilder.content.models.common.Firmante; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.DespatchAdviceCommonValidator; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationMessage; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationResult; import lombok.Builder; import lombok.Data; import lombok.Singular; @@ -20,32 +23,46 @@ *

  • Serie debe iniciar con 'V'
  • *
  • Siempre requiere al menos un conductor
  • *
  • Siempre requiere vehículo
  • - *
  • El remitente/emisor del XML es el transportista - * (DespatchSupplierParty)
  • - *
  • El tercero (SellerSupplierParty) es el remitente original de los - * bienes
  • + *
  • El remitente/emisor del XML es el transportista (DespatchSupplierParty)
  • + *
  • El tercero (SellerSupplierParty) es el remitente original de los bienes
  • * *

    * Uso: * *

    {@code
      * GRETransportista gre = GRETransportista.builder()
    - *         .serie("V001").numero(1)
    + *         .serie("V001")
    + *         .numero(1)
      *         .transportistaEmisor(Transportista.builder()
    - *                 .tipoDocumentoIdentidad("6").numeroDocumentoIdentidad("20300030003")
    - *                 .nombre("Transportes S.A.C.").numeroRegistroMTC("MTC-123").build())
    + *                 .tipoDocumentoIdentidad("6")
    + *                 .numeroDocumentoIdentidad("20300030003")
    + *                 .nombre("Transportes S.A.C.")
    + *                 .numeroRegistroMTC("MTC-123")
    + *                 .build())
      *         .remitente(Tercero.builder()
    - *                 .tipoDocumentoIdentidad("6").numeroDocumentoIdentidad("20100010001")
    - *                 .nombre("Empresa Remitente S.A.C.").build())
    + *                 .tipoDocumentoIdentidad("6")
    + *                 .numeroDocumentoIdentidad("20100010001")
    + *                 .nombre("Empresa Remitente S.A.C.")
    + *                 .build())
      *         .destinatario(Destinatario.builder()
    - *                 .tipoDocumentoIdentidad("6").numeroDocumentoIdentidad("20200020002").nombre("Cliente").build())
    + *                 .tipoDocumentoIdentidad("6")
    + *                 .numeroDocumentoIdentidad("20200020002")
    + *                 .nombre("Cliente")
    + *                 .build())
      *         .conductor(Driver.builder()
    - *                 .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("12345678")
    - *                 .nombres("Juan").apellidos("Perez").licencia("Q123").build())
    + *                 .tipoDocumentoIdentidad("1")
    + *                 .numeroDocumentoIdentidad("12345678")
    + *                 .nombres("Juan")
    + *                 .apellidos("Perez")
    + *                 .licencia("Q123")
    + *                 .build())
      *         .vehiculo(Vehicle.builder().placa("ABC-123").build())
      *         .envio(Envio.builder()
    - *                 .tipoTraslado("01").pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM")
    - *                 .tipoModalidadTraslado("01").fechaTraslado(LocalDate.now())
    + *                 .tipoTraslado("01")
    + *                 .pesoTotal(BigDecimal.ONE)
    + *                 .pesoTotalUnidadMedida("KGM")
    + *                 .tipoModalidadTraslado("01")
    + *                 .fechaTraslado(LocalDate.now())
      *                 .partida(Partida.builder().ubigeo("150101").direccion("Origen").build())
      *                 .destino(Destino.builder().ubigeo("150102").direccion("Destino").build())
      *                 .build())
    @@ -74,19 +91,17 @@ public class GRETransportista {
         // == Partes ==
     
         /**
    -     * El transportista que emite la guía.
    -     * Se convierte a {@link Remitente} para mapear a DespatchSupplierParty en el
    +     * El transportista que emite la guía. Se convierte a {@link Remitente} para mapear a DespatchSupplierParty en el
          * XML.
          * 

    - * Se usa {@link Transportista} en lugar de {@link Remitente} porque - * semánticamente - * SUNAT distingue remitente y transportista como sujetos diferentes. + * Se usa {@link Transportista} en lugar de {@link Remitente} porque semánticamente SUNAT distingue remitente y + * transportista como sujetos diferentes. */ private Transportista transportistaEmisor; /** - * El remitente original de los bienes (quien contrata el transporte). - * Se mapea a SellerSupplierParty (tercero) en el XML. + * El remitente original de los bienes (quien contrata el transporte). Se mapea a SellerSupplierParty (tercero) en + * el XML. */ private Tercero remitente; @@ -101,8 +116,7 @@ public class GRETransportista { // == Conductor(es) y vehículo - siempre requeridos para transportista == /** - * Conductor(es) del transporte. Al menos uno es obligatorio. - * El primero es el conductor principal. + * Conductor(es) del transporte. Al menos uno es obligatorio. El primero es el conductor principal. */ @Singular("conductor") private List conductores; @@ -125,8 +139,8 @@ public class GRETransportista { // == Datos de envío == /** - * Datos de envío. El conductor y vehículo se inyectan automáticamente - * desde los campos {@code conductores} y {@code vehiculo} de este modelo. + * Datos de envío. El conductor y vehículo se inyectan automáticamente desde los campos {@code conductores} y + * {@code vehiculo} de este modelo. */ private Envio envio; @@ -137,94 +151,64 @@ public class GRETransportista { /** * Valida las reglas de negocio para GRE-Transportista y retorna los errores. + *

    + * Delega las validaciones comunes a {@link DespatchAdviceCommonValidator} y aplica solo las reglas específicas del + * tipo 31 (serie V*, transportista emisor, conductor/vehículo siempre obligatorios, tercero obligatorio). + *

    + * Solo retorna errores (severidad {@code ERROR}). Para obtener errores y advertencias diferenciados, use + * {@link #validateDetailed()}. * * @return lista de errores (vacía si es válido) + * @see #validateDetailed() */ public List validate() { - List errors = new ArrayList<>(); + return validateDetailed().getErrors(); + } - // Serie - if (serie == null || serie.isBlank()) { - errors.add("La serie es requerida"); - } else if (!serie.toUpperCase().startsWith("V")) { - errors.add("GRE-Transportista requiere serie que inicie con 'V'. Serie actual: " + serie); - } + /** + * Valida las reglas de negocio y retorna un {@link ValidationResult} con errores y advertencias diferenciados por + * severidad. + * + * @return resultado de validación con errores y advertencias + * @since 5.2.0 + */ + public ValidationResult validateDetailed() { + List messages = new ArrayList<>(); - // Número - if (numero == null || numero < 1) { - errors.add("El número debe ser mayor a 0"); - } + // Campos básicos (serie, número) + DespatchAdviceCommonValidator.validateBasicFields(serie, numero, messages); - // Transportista emisor - if (transportistaEmisor == null) { - errors.add("El transportista emisor es requerido"); - } else { - if (transportistaEmisor.getNumeroDocumentoIdentidad() == null - || transportistaEmisor.getNumeroDocumentoIdentidad().length() != 11) { - errors.add("El RUC del transportista emisor debe tener 11 dígitos"); - } - } + // Serie específica transportista: debe iniciar con 'V' + DespatchAdviceCommonValidator.validateSerieTransportista(serie, messages); - // Remitente original (tercero) - if (remitente == null) { - errors.add("El remitente original (tercero) es requerido para GRE-Transportista"); - } + // Transportista emisor (RUC 11 dígitos) + DespatchAdviceCommonValidator.validateTransportistaEmisor(transportistaEmisor, messages); - // Destinatario - if (destinatario == null) { - errors.add("El destinatario es requerido"); - } + // Remitente original (tercero) — obligatorio para tipo 31 + DespatchAdviceCommonValidator.validateTerceroTransportista(remitente, messages); - // Conductor - siempre obligatorio - if (conductores == null || conductores.isEmpty()) { - errors.add("GRE-Transportista requiere al menos un conductor"); - } + // Destinatario + DespatchAdviceCommonValidator.validateDestinatario(destinatario, messages); - // Vehículo - siempre obligatorio - if (vehiculo == null) { - errors.add("GRE-Transportista requiere datos del vehículo"); - } + // Conductor y vehículo — siempre obligatorios para transportista + DespatchAdviceCommonValidator.validateConductorVehiculoTransportista(conductores, vehiculo, messages); // Envío - if (envio == null) { - errors.add("Los datos de envío son requeridos"); - } else { - validateEnvio(envio, errors); - } + DespatchAdviceCommonValidator.validateEnvioRequired(envio, messages); + DespatchAdviceCommonValidator.validatePartidaDestino(envio, messages); - // Detalles - if (detalles == null || detalles.isEmpty()) { - errors.add("Se requiere al menos una línea de detalle"); - } + // Comercio exterior (advertencias) + DespatchAdviceCommonValidator.validateComercioExterior(envio, messages); - return errors; - } + // Detalles + DespatchAdviceCommonValidator.validateDetalles(detalles, messages); - private void validateEnvio(Envio envio, List errors) { - if (envio.getTipoTraslado() == null || envio.getTipoTraslado().isBlank()) { - errors.add("El motivo de traslado (Catálogo 20) es requerido"); - } - if (envio.getPesoTotal() == null) { - errors.add("El peso total es requerido"); - } - if (envio.getTipoModalidadTraslado() == null) { - errors.add("La modalidad de traslado (Catálogo 18) es requerida"); - } - if (envio.getFechaTraslado() == null) { - errors.add("La fecha de traslado es requerida"); - } - if (envio.getPartida() == null) { - errors.add("El punto de partida es requerido"); - } - if (envio.getDestino() == null) { - errors.add("El punto de destino es requerido"); - } + return new ValidationResult(messages); } /** - * Convierte este modelo a {@link DespatchAdvice} para renderizado XML. - * El tipo de comprobante se fija a "31" (GRE-Transportista). - * Los conductores y vehículo se inyectan en el envío automáticamente. + * Convierte este modelo a {@link DespatchAdvice} para renderizado XML. El tipo de comprobante se fija a "31" + * (GRE-Transportista). Los conductores y vehículo se inyectan en el envío automáticamente. * * @return DespatchAdvice listo para enriquecer y renderizar */ diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GuiaItemAttribute.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GuiaItemAttribute.java index 9ceae0fb..e08f3fa9 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GuiaItemAttribute.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/GuiaItemAttribute.java @@ -5,12 +5,29 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Atributo adicional de un ítem de la Guía de Remisión Electrónica. + *

    + * Permite agregar pares clave-valor con información complementaria sobre el bien trasladado (por ejemplo, lote, fecha + * de vencimiento, número de serie, etc.). + *

    + * Se mapea a {@code cac:DespatchLine/cac:Item/cac:AdditionalItemProperty}. + * + * @since 2.0 + * @see DespatchAdviceItem#getAtributos() + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class GuiaItemAttribute { + + /** Código del atributo (identificador del tipo de propiedad). */ private String code; + + /** Nombre descriptivo del atributo. */ private String name; + + /** Valor del atributo. */ private String value; } diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Partida.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Partida.java index fbff322b..8b717012 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Partida.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Partida.java @@ -6,15 +6,26 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Punto de partida del traslado en la Guía de Remisión Electrónica. + *

    + * Se mapea a {@code cac:Shipment/cac:Delivery/cac:Despatch/cac:DespatchAddress} en el XML UBL. El UBIGEO y la dirección + * son obligatorios. + * + * @since 2.0 + * @see Envio#getPartida() + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Partida { + /** Código UBIGEO INEI del punto de partida (6 dígitos). */ @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private String ubigeo; + /** Dirección completa del punto de partida. */ @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private String direccion; diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Remitente.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Remitente.java index dd2fc4c5..24c7389b 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Remitente.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Remitente.java @@ -6,18 +6,31 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Datos del remitente de los bienes en la Guía de Remisión Electrónica. + *

    + * En una GRE-Remitente (09), el remitente es quien envía los bienes (se mapea a {@code cac:DespatchSupplierParty}). En + * una GRE-Transportista (31), este campo contiene los datos del transportista emitente (el mapeo se hace vía + * {@link GRETransportista#toDespatchAdvice()}). + * + * @since 2.0 + * @see DespatchAdvice#getRemitente() + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Remitente { + /** RUC del remitente (11 dígitos, obligatorio). */ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, minLength = 11, maxLength = 11, pattern = "[0-9]+") private String ruc; + /** Razón social del remitente. */ @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private String razonSocial; + /** Número de registro del Ministerio de Transportes y Comunicaciones (opcional). */ @Schema(description = "Número de registro del Ministerio de Transportes y Comunicaciones") private String numeroRegistroMTC; } diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Transportista.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Transportista.java index 4413f90a..d7f53476 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Transportista.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/Transportista.java @@ -6,18 +6,36 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * Datos del transportista en la Guía de Remisión Electrónica. + *

    + * Se usa en dos contextos: + *

      + *
    • GRE-Remitente con transporte público (01): el transportista contratado se consigna en + * {@link Envio#getTransportista()}.
    • + *
    • GRE-Transportista: el transportista emisor se mapea desde {@link GRETransportista#getTransportistaEmisor()} hacia + * {@code cac:DespatchSupplierParty}.
    • + *
    + * + * @since 2.0 + * @see Envio#getTransportista() + * @see GRETransportista#getTransportistaEmisor() + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Transportista { + /** Tipo de documento de identidad (Catálogo 06). */ @Schema(description = "Catalogo 06", requiredMode = Schema.RequiredMode.REQUIRED) private String tipoDocumentoIdentidad; + /** Número de documento de identidad (RUC para empresas). */ @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private String numeroDocumentoIdentidad; + /** Razón social o nombre del transportista. */ @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private String nombre; diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/validation/DespatchAdviceCommonValidator.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/validation/DespatchAdviceCommonValidator.java new file mode 100644 index 00000000..f22b94d4 --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/validation/DespatchAdviceCommonValidator.java @@ -0,0 +1,438 @@ +package io.github.project.openubl.xbuilder.content.models.standard.guia.validation; + +import io.github.project.openubl.xbuilder.content.models.standard.guia.*; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Reglas de validación compartidas para la Guía de Remisión Electrónica (GRE). + *

    + * Centraliza las validaciones comunes entre {@code DespatchAdviceValidator}, + * {@code GRERemitente.validate()} y {@code GRETransportista.validate()}, eliminando + * la duplicación de reglas y garantizando coherencia funcional. + *

    + * Criterio de severidad: + *

      + *
    • {@link ValidationSeverity#ERROR} — Regla de UBL o funcional SUNAT cuyo incumplimiento + * causa rechazo del documento. Siempre bloquea la emisión.
    • + *
    • {@link ValidationSeverity#WARNING} — Recomendación normativa cuya obligatoriedad está + * diferida (RS 000133-2025/SUNAT), es discrecional, o depende del contexto del emisor.
    • + *
    + *

    + * Fuente normativa: RS 000123-2022/SUNAT, RS 000240-2024/SUNAT, RS 000133-2025/SUNAT. + * + * @since 5.2.0 + * @see io.github.project.openubl.xbuilder.content.models.standard.guia.DespatchAdviceValidator + * @see io.github.project.openubl.xbuilder.content.models.standard.guia.GRERemitente + * @see io.github.project.openubl.xbuilder.content.models.standard.guia.GRETransportista + */ +public final class DespatchAdviceCommonValidator { + + /** + * Motivos de traslado de comercio exterior (RS 000240-2024/SUNAT). + */ + static final Set MOTIVOS_COMERCIO_EXTERIOR = new HashSet<>(Arrays.asList( + "08", // Importación + "09", // Exportación + "10", // Importación con DAM + "19" // Mercancía extranjera + )); + + /** + * Motivos que requieren puerto o aeropuerto. + */ + static final Set MOTIVOS_CON_PUERTO = new HashSet<>(Arrays.asList( + "08", "09", "10", "19" + )); + + private DespatchAdviceCommonValidator() { + // Utility class + } + + // ======================================================================== + // Reglas comunes — usadas por los tres validadores + // ======================================================================== + + /** + * Valida campos básicos del comprobante: serie y número. + *

    + * Regla UBL: serie y número son obligatorios para identificar el documento. + * + * @param serie serie del comprobante + * @param numero número del comprobante + * @param messages lista donde se agregan los mensajes + */ + public static void validateBasicFields(String serie, Integer numero, + List messages) { + if (serie == null || serie.isBlank()) { + messages.add(ValidationMessage.error("La serie es requerida")); + } + if (numero == null || numero < 1) { + messages.add(ValidationMessage.error("El número debe ser mayor a 0")); + } + } + + /** + * Valida que la serie sea coherente con el tipo de comprobante (09 → T*, 31 → V*). + *

    + * Regla SUNAT FAQ #7. + * + * @param serie serie del comprobante + * @param tipoComprobante tipo de comprobante ("09" o "31") + * @param messages lista donde se agregan los mensajes + */ + public static void validateSerieCoherence(String serie, String tipoComprobante, + List messages) { + if (serie == null || tipoComprobante == null) { + return; + } + String serieUpper = serie.toUpperCase(); + if ("09".equals(tipoComprobante) && !serieUpper.startsWith("T")) { + messages.add(ValidationMessage.error( + "GRE-Remitente (09) requiere serie que inicie con 'T'. Serie actual: " + serie)); + } + if ("31".equals(tipoComprobante) && !serieUpper.startsWith("V")) { + messages.add(ValidationMessage.error( + "GRE-Transportista (31) requiere serie que inicie con 'V'. Serie actual: " + serie)); + } + } + + /** + * Valida que la serie del GRE-Remitente inicie con 'T'. + * + * @param serie serie del comprobante + * @param messages lista donde se agregan los mensajes + */ + public static void validateSerieRemitente(String serie, List messages) { + if (serie != null && !serie.isBlank() && !serie.toUpperCase().startsWith("T")) { + messages.add(ValidationMessage.error( + "GRE-Remitente requiere serie que inicie con 'T'. Serie actual: " + serie)); + } + } + + /** + * Valida que la serie del GRE-Transportista inicie con 'V'. + * + * @param serie serie del comprobante + * @param messages lista donde se agregan los mensajes + */ + public static void validateSerieTransportista(String serie, List messages) { + if (serie != null && !serie.isBlank() && !serie.toUpperCase().startsWith("V")) { + messages.add(ValidationMessage.error( + "GRE-Transportista requiere serie que inicie con 'V'. Serie actual: " + serie)); + } + } + + /** + * Valida el RUC del remitente (debe tener 11 dígitos). + *

    + * Regla SUNAT: el RUC es obligatorio y de longitud 11. + * + * @param remitente datos del remitente + * @param messages lista donde se agregan los mensajes + */ + public static void validateRemitente(Remitente remitente, List messages) { + if (remitente == null) { + messages.add(ValidationMessage.error("El remitente es requerido")); + return; + } + if (remitente.getRuc() == null || remitente.getRuc().length() != 11) { + messages.add(ValidationMessage.error("El RUC del remitente debe tener 11 dígitos")); + } + } + + /** + * Valida el RUC del transportista emisor (debe tener 11 dígitos). + * + * @param transportista datos del transportista emisor + * @param messages lista donde se agregan los mensajes + */ + public static void validateTransportistaEmisor(Transportista transportista, + List messages) { + if (transportista == null) { + messages.add(ValidationMessage.error("El transportista emisor es requerido")); + return; + } + if (transportista.getNumeroDocumentoIdentidad() == null + || transportista.getNumeroDocumentoIdentidad().length() != 11) { + messages.add(ValidationMessage.error( + "El RUC del transportista emisor debe tener 11 dígitos")); + } + } + + /** + * Valida que el destinatario esté presente. + * + * @param destinatario datos del destinatario + * @param messages lista donde se agregan los mensajes + */ + public static void validateDestinatario(Destinatario destinatario, + List messages) { + if (destinatario == null) { + messages.add(ValidationMessage.error("El destinatario es requerido")); + } + } + + /** + * Valida que el tercero (remitente original) esté presente en GRE-Transportista. + *

    + * Para GRE-Transportista (31), el tercero identifica al remitente de los bienes. + * Es un campo requerido por SUNAT en SellerSupplierParty. + * + * @param tercero datos del tercero + * @param messages lista donde se agregan los mensajes + */ + public static void validateTerceroTransportista(Tercero tercero, + List messages) { + if (tercero == null) { + messages.add(ValidationMessage.error( + "El remitente original (tercero) es requerido para GRE-Transportista")); + } + } + + /** + * Valida los campos obligatorios del envío (shipment). + *

    + * Campos obligatorios según UBL/SUNAT: + *

      + *
    • Motivo de traslado (Catálogo 20)
    • + *
    • Peso total
    • + *
    • Modalidad de traslado (Catálogo 18)
    • + *
    • Fecha de traslado
    • + *
    + * + * @param envio datos de envío + * @param messages lista donde se agregan los mensajes + */ + public static void validateEnvioRequired(Envio envio, List messages) { + if (envio == null) { + messages.add(ValidationMessage.error("Los datos de envío son requeridos")); + return; + } + if (envio.getTipoTraslado() == null || envio.getTipoTraslado().isBlank()) { + messages.add(ValidationMessage.error( + "El motivo de traslado (Catálogo 20) es requerido")); + } + if (envio.getPesoTotal() == null) { + messages.add(ValidationMessage.error("El peso total es requerido")); + } + if (envio.getTipoModalidadTraslado() == null) { + messages.add(ValidationMessage.error( + "La modalidad de traslado (Catálogo 18) es requerida")); + } + if (envio.getFechaTraslado() == null) { + messages.add(ValidationMessage.error("La fecha de traslado es requerida")); + } + } + + /** + * Valida punto de partida y destino del envío. + *

    + * Ambos son obligatorios según el esquema UBL de la GRE. + * + * @param envio datos de envío + * @param messages lista donde se agregan los mensajes + */ + public static void validatePartidaDestino(Envio envio, List messages) { + if (envio == null) { + return; + } + if (envio.getPartida() == null) { + messages.add(ValidationMessage.error("El punto de partida es requerido")); + } + if (envio.getDestino() == null) { + messages.add(ValidationMessage.error("El punto de destino es requerido")); + } + } + + /** + * Valida las reglas de modalidad de transporte para GRE-Remitente (tipo 09). + *

    + *

      + *
    • Transporte privado (02): requiere conductor y vehículo, salvo que se + * indique categoría M1/L. No debe consignar transportista externo.
    • + *
    • Transporte público (01): requiere datos del transportista.
    • + *
    + * + * @param envio datos de envío + * @param messages lista donde se agregan los mensajes + */ + public static void validateModalidadRemitente(Envio envio, + List messages) { + if (envio == null || envio.getTipoModalidadTraslado() == null) { + return; + } + String modalidad = envio.getTipoModalidadTraslado(); + + boolean tieneIndicadorM1L = envio.getIndicadores() != null + && envio.getIndicadores().contains("SUNAT_Envio_IndicadorTrasladoVehiculoM1L"); + + if ("02".equals(modalidad)) { + // Transporte privado + if (!tieneIndicadorM1L) { + if (envio.getChoferes() == null || envio.getChoferes().isEmpty()) { + messages.add(ValidationMessage.error( + "Transporte privado requiere al menos un conductor " + + "(salvo vehículo categoría M1/L)")); + } + if (envio.getVehiculo() == null) { + messages.add(ValidationMessage.error( + "Transporte privado requiere datos del vehículo " + + "(salvo vehículo categoría M1/L)")); + } + } + if (envio.getTransportista() != null) { + messages.add(ValidationMessage.error( + "Transporte privado no debe consignar transportista externo " + + "(usar modalidad pública si subcontrata)")); + } + } else if ("01".equals(modalidad)) { + // Transporte público + if (envio.getTransportista() == null) { + messages.add(ValidationMessage.error( + "Transporte público requiere datos del transportista")); + } + } + } + + /** + * Valida las reglas de modalidad de transporte para GRE-Transportista (tipo 31). + *

    + * El transportista siempre requiere al menos un conductor y vehículo, + * independientemente de la modalidad de traslado. + * + * @param conductores lista de conductores + * @param vehiculo vehículo principal + * @param messages lista donde se agregan los mensajes + */ + public static void validateConductorVehiculoTransportista(List conductores, + Vehicle vehiculo, + List messages) { + if (conductores == null || conductores.isEmpty()) { + messages.add(ValidationMessage.error( + "GRE-Transportista requiere al menos un conductor")); + } + if (vehiculo == null) { + messages.add(ValidationMessage.error( + "GRE-Transportista requiere datos del vehículo")); + } + } + + /** + * Valida las reglas de modalidad para un DespatchAdvice genérico (post-conversión). + *

    + * Se usa cuando se valida el modelo después de la conversión desde GRERemitente + * o GRETransportista. Aplica reglas según el tipo de comprobante detectado. + * + * @param tipoComprobante tipo de comprobante ("09" o "31") + * @param envio datos de envío + * @param messages lista donde se agregan los mensajes + */ + public static void validateModalidadGeneric(String tipoComprobante, Envio envio, + List messages) { + if (envio == null || envio.getTipoModalidadTraslado() == null) { + return; + } + String modalidad = envio.getTipoModalidadTraslado(); + boolean isRemitente = "09".equals(tipoComprobante); + + if ("02".equals(modalidad) && isRemitente) { + boolean tieneM1L = envio.getIndicadores() != null + && envio.getIndicadores().contains("SUNAT_Envio_IndicadorTrasladoVehiculoM1L"); + if (!tieneM1L) { + if (envio.getChoferes() == null || envio.getChoferes().isEmpty()) { + messages.add(ValidationMessage.error( + "Transporte privado en GRE-Remitente requiere al menos un conductor " + + "(salvo vehículo categoría M1/L)")); + } + if (envio.getVehiculo() == null) { + messages.add(ValidationMessage.error( + "Transporte privado en GRE-Remitente requiere datos del vehículo " + + "(salvo vehículo categoría M1/L)")); + } + } + } + + if ("01".equals(modalidad) && isRemitente) { + if (envio.getTransportista() == null) { + messages.add(ValidationMessage.error( + "Transporte público en GRE-Remitente requiere datos del transportista")); + } + } + + if ("31".equals(tipoComprobante)) { + if (envio.getChoferes() == null || envio.getChoferes().isEmpty()) { + messages.add(ValidationMessage.error( + "GRE-Transportista requiere al menos un conductor")); + } + if (envio.getVehiculo() == null) { + messages.add(ValidationMessage.error( + "GRE-Transportista requiere datos del vehículo")); + } + } + } + + /** + * Valida reglas de comercio exterior (RS 000240-2024/SUNAT). + *

    + * Cuando el motivo de traslado es de comercio exterior (08, 09, 10, 19): + *

      + *
    • WARNING: Se recomienda incluir referencia DAM/DS
    • + *
    • WARNING: Se recomienda incluir puerto o aeropuerto
    • + *
    + *

    + * NOTA: La obligatoriedad plena del motivo 19 y la derogación del ticket de + * salida fue pospuesta al 01-jul-2026 por RS 000133-2025/SUNAT. + * Hasta esa fecha se aplica discrecionalidad. + * + * @param envio datos de envío + * @param messages lista donde se agregan los mensajes + */ + public static void validateComercioExterior(Envio envio, + List messages) { + if (envio == null) { + return; + } + String motivo = envio.getTipoTraslado(); + if (motivo == null || !MOTIVOS_COMERCIO_EXTERIOR.contains(motivo)) { + return; + } + + boolean tieneDAM = envio.getDeclaracionesAduaneras() != null + && !envio.getDeclaracionesAduaneras().isEmpty(); + if (!tieneDAM) { + messages.add(ValidationMessage.warning( + "Comercio exterior: se recomienda incluir referencia a DAM/DS " + + "(puede no ser obligatorio según FAQ #32; discrecionalidad vigente " + + "hasta 01-jul-2026 por RS 000133-2025/SUNAT)")); + } + + if (MOTIVOS_CON_PUERTO.contains(motivo) + && envio.getPuerto() == null && envio.getAeropuerto() == null) { + messages.add(ValidationMessage.warning( + "Comercio exterior: se recomienda incluir puerto o aeropuerto " + + "para motivos de traslado 08, 09, 10 o 19")); + } + } + + /** + * Valida que haya al menos una línea de detalle. + *

    + * Requerimiento UBL: toda GRE debe tener al menos un ítem. + * FAQ #24: con indicador de traslado total DAM/DS la línea puede tener solo + * campos mínimos, pero debe existir. + * + * @param detalles lista de ítems + * @param messages lista donde se agregan los mensajes + */ + public static void validateDetalles(List detalles, + List messages) { + if (detalles == null || detalles.isEmpty()) { + messages.add(ValidationMessage.error( + "Se requiere al menos una línea de detalle (requerimiento UBL)")); + } + } +} diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/validation/ValidationMessage.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/validation/ValidationMessage.java new file mode 100644 index 00000000..715341ce --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/validation/ValidationMessage.java @@ -0,0 +1,80 @@ +package io.github.project.openubl.xbuilder.content.models.standard.guia.validation; + +/** + * Mensaje individual de validación con severidad asociada. + *

    + * Cada instancia representa una regla evaluada que no se cumplió. + * + * @since 5.2.0 + * @see ValidationSeverity + * @see ValidationResult + */ +public class ValidationMessage { + + private final ValidationSeverity severity; + private final String message; + + /** + * Crea un mensaje de validación. + * + * @param severity severidad del mensaje ({@link ValidationSeverity#ERROR} o {@link ValidationSeverity#WARNING}) + * @param message texto descriptivo del problema detectado + */ + public ValidationMessage(ValidationSeverity severity, String message) { + this.severity = severity; + this.message = message; + } + + /** + * Crea un mensaje de error ({@link ValidationSeverity#ERROR}). + * + * @param message texto descriptivo + * @return nueva instancia con severidad ERROR + */ + public static ValidationMessage error(String message) { + return new ValidationMessage(ValidationSeverity.ERROR, message); + } + + /** + * Crea un mensaje de advertencia ({@link ValidationSeverity#WARNING}). + * + * @param message texto descriptivo + * @return nueva instancia con severidad WARNING + */ + public static ValidationMessage warning(String message) { + return new ValidationMessage(ValidationSeverity.WARNING, message); + } + + /** + * @return la severidad de este mensaje + */ + public ValidationSeverity getSeverity() { + return severity; + } + + /** + * @return el texto descriptivo del problema + */ + public String getMessage() { + return message; + } + + /** + * @return {@code true} si la severidad es {@link ValidationSeverity#ERROR} + */ + public boolean isError() { + return severity == ValidationSeverity.ERROR; + } + + /** + * @return {@code true} si la severidad es {@link ValidationSeverity#WARNING} + */ + public boolean isWarning() { + return severity == ValidationSeverity.WARNING; + } + + @Override + public String toString() { + return "[" + severity + "] " + message; + } +} diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/validation/ValidationResult.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/validation/ValidationResult.java new file mode 100644 index 00000000..969236fe --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/validation/ValidationResult.java @@ -0,0 +1,99 @@ +package io.github.project.openubl.xbuilder.content.models.standard.guia.validation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Resultado agregado de una validación de la GRE. + *

    + * Contiene la lista completa de {@link ValidationMessage} y ofrece métodos de conveniencia para consultar errores y + * advertencias por separado. + * + *

    {@code
    + * ValidationResult result = DespatchAdviceValidator.validateDetailed(da);
    + * if (result.hasErrors()) {
    + *     result.getErrors().forEach(System.err::println);
    + * }
    + * result.getWarnings().forEach(log::warn);
    + * }
    + * + * @since 5.2.0 + * @see DespatchAdviceCommonValidator + */ +public class ValidationResult { + + private final List messages; + + /** + * Crea un resultado con la lista de mensajes proporcionada. + * + * @param messages lista de mensajes de validación + */ + public ValidationResult(List messages) { + this.messages = messages != null ? Collections.unmodifiableList(new ArrayList<>(messages)) + : Collections.emptyList(); + } + + /** + * @return lista completa e inmutable de todos los mensajes (errores + advertencias) + */ + public List getMessages() { + return messages; + } + + /** + * @return solo los textos de los mensajes con severidad {@link ValidationSeverity#ERROR} + */ + public List getErrors() { + return messages.stream() + .filter(ValidationMessage::isError) + .map(ValidationMessage::getMessage) + .collect(Collectors.toList()); + } + + /** + * @return solo los textos de los mensajes con severidad {@link ValidationSeverity#WARNING} + */ + public List getWarnings() { + return messages.stream() + .filter(ValidationMessage::isWarning) + .map(ValidationMessage::getMessage) + .collect(Collectors.toList()); + } + + /** + * @return {@code true} si hay al menos un mensaje de severidad {@link ValidationSeverity#ERROR} + */ + public boolean hasErrors() { + return messages.stream().anyMatch(ValidationMessage::isError); + } + + /** + * @return {@code true} si hay al menos un mensaje de severidad {@link ValidationSeverity#WARNING} + */ + public boolean hasWarnings() { + return messages.stream().anyMatch(ValidationMessage::isWarning); + } + + /** + * @return {@code true} si no hay errores (puede haber advertencias) + */ + public boolean isValid() { + return !hasErrors(); + } + + /** + * @return solo los textos de mensajes de error (retrocompatible con {@code List}) + */ + public List getErrorMessages() { + return getErrors(); + } + + @Override + public String toString() { + return "ValidationResult{errors=" + getErrors().size() + + ", warnings=" + getWarnings().size() + "}"; + } +} diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/validation/ValidationSeverity.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/validation/ValidationSeverity.java new file mode 100644 index 00000000..e9c60a01 --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/guia/validation/ValidationSeverity.java @@ -0,0 +1,29 @@ +package io.github.project.openubl.xbuilder.content.models.standard.guia.validation; + +/** + * Severidad de un mensaje de validación de la GRE. + *

    + * Define dos niveles: + *

      + *
    • {@link #ERROR} — El documento será rechazado por SUNAT (falla de UBL/regla funcional). Siempre impide la + * emisión.
    • + *
    • {@link #WARNING} — Recomendación funcional o normativa cuya obligatoriedad está pospuesta, es discrecional, o + * depende del contexto de negocio. No impide la emisión, pero conviene atender.
    • + *
    + * + * @since 5.2.0 + * @see ValidationMessage + * @see ValidationResult + */ +public enum ValidationSeverity { + + /** + * Error duro: el documento será rechazado por SUNAT si se envía con este defecto. + */ + ERROR, + + /** + * Advertencia: recomendación de cumplimiento normativo, no bloquea el envío. + */ + WARNING +} diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/enricher/kie/rules/enrich/header/DespatchAdviceTipoComprobanteRule.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/enricher/kie/rules/enrich/header/DespatchAdviceTipoComprobanteRule.java index e029feed..8c7d8773 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/enricher/kie/rules/enrich/header/DespatchAdviceTipoComprobanteRule.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/enricher/kie/rules/enrich/header/DespatchAdviceTipoComprobanteRule.java @@ -1,6 +1,5 @@ package io.github.project.openubl.xbuilder.enricher.kie.rules.enrich.header; -import io.github.project.openubl.xbuilder.content.models.standard.guia.DespatchAdvice; import io.github.project.openubl.xbuilder.enricher.kie.AbstractHeaderRule; import io.github.project.openubl.xbuilder.enricher.kie.RulePhase; @@ -8,8 +7,7 @@ import static io.github.project.openubl.xbuilder.enricher.kie.rules.utils.Helpers.whenDespatchAdvice; /** - * Regla de enriquecimiento para autodetectar el tipo de comprobante de la GRE - * a partir del prefijo de la serie. + * Regla de enriquecimiento para autodetectar el tipo de comprobante de la GRE a partir del prefijo de la serie. *

    * Regla funcional SUNAT: *

      @@ -17,8 +15,8 @@ *
    • Serie V* → GRE-Transportista (tipo "31")
    • *
    *

    - * Esta regla solo se aplica si {@code tipoComprobante} no fue establecido - * explícitamente por el usuario, permitiendo autocompletar. + * Esta regla solo se aplica si {@code tipoComprobante} no fue establecido explícitamente por el usuario, permitiendo + * autocompletar. */ @RulePhase(type = RulePhase.PhaseType.ENRICH) public class DespatchAdviceTipoComprobanteRule extends AbstractHeaderRule { diff --git a/xbuilder/core/src/test/java/unit/validator/DespatchAdviceCommonValidatorTest.java b/xbuilder/core/src/test/java/unit/validator/DespatchAdviceCommonValidatorTest.java new file mode 100644 index 00000000..3a22a2f3 --- /dev/null +++ b/xbuilder/core/src/test/java/unit/validator/DespatchAdviceCommonValidatorTest.java @@ -0,0 +1,983 @@ +package unit.validator; + +import io.github.project.openubl.xbuilder.content.catalogs.Catalog18; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog20; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog6; +import io.github.project.openubl.xbuilder.content.models.standard.guia.*; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.DespatchAdviceCommonValidator; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationMessage; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationResult; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationSeverity; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests unitarios para {@link DespatchAdviceCommonValidator} y el modelo de validación. + *

    + * Verifica que las reglas centralizadas producen los mensajes correctos con la severidad adecuada (ERROR vs WARNING). + */ +public class DespatchAdviceCommonValidatorTest { + + // == Helpers para construir datos mínimos == + + private static Envio minimalEnvio() { + return Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .pesoTotal(BigDecimal.ONE) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PRIVADO.getCode()) + .fechaTraslado(LocalDate.now()) + .chofer(Driver.builder() + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("11111111") + .nombres("Juan") + .apellidos("Perez") + .licencia("Q123") + .build()) + .vehiculo(Vehicle.builder().placa("ABC-123").build()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build(); + } + + // ================================================================ + // ValidationMessage / ValidationResult tests + // ================================================================ + + @Nested + class ValidationModelTests { + + @Test + public void testValidationMessageError() { + ValidationMessage msg = ValidationMessage.error("test error"); + assertTrue(msg.isError()); + assertFalse(msg.isWarning()); + assertEquals(ValidationSeverity.ERROR, msg.getSeverity()); + assertEquals("test error", msg.getMessage()); + } + + @Test + public void testValidationMessageWarning() { + ValidationMessage msg = ValidationMessage.warning("test warning"); + assertFalse(msg.isError()); + assertTrue(msg.isWarning()); + assertEquals(ValidationSeverity.WARNING, msg.getSeverity()); + } + + @Test + public void testValidationResultEmpty() { + ValidationResult result = new ValidationResult(new ArrayList<>()); + assertTrue(result.isValid()); + assertFalse(result.hasErrors()); + assertFalse(result.hasWarnings()); + assertTrue(result.getErrors().isEmpty()); + assertTrue(result.getWarnings().isEmpty()); + } + + @Test + public void testValidationResultWithErrorsAndWarnings() { + List messages = List.of( + ValidationMessage.error("error 1"), + ValidationMessage.warning("warning 1"), + ValidationMessage.error("error 2")); + ValidationResult result = new ValidationResult(messages); + + assertFalse(result.isValid()); + assertTrue(result.hasErrors()); + assertTrue(result.hasWarnings()); + assertEquals(2, result.getErrors().size()); + assertEquals(1, result.getWarnings().size()); + assertEquals(3, result.getMessages().size()); + } + + @Test + public void testValidationResultOnlyWarnings() { + List messages = List.of( + ValidationMessage.warning("w1"), + ValidationMessage.warning("w2")); + ValidationResult result = new ValidationResult(messages); + + assertTrue(result.isValid()); + assertFalse(result.hasErrors()); + assertTrue(result.hasWarnings()); + } + + @Test + public void testValidationResultNullMessages() { + ValidationResult result = new ValidationResult(null); + assertTrue(result.isValid()); + assertTrue(result.getMessages().isEmpty()); + } + } + + // ================================================================ + // BasicFields + // ================================================================ + + @Nested + class BasicFieldsTests { + + @Test + public void testSerieNull() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateBasicFields(null, 1, msgs); + assertTrue(msgs.stream().anyMatch(m -> m.isError() && m.getMessage().contains("serie"))); + } + + @Test + public void testSerieBlank() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateBasicFields(" ", 1, msgs); + assertTrue(msgs.stream().anyMatch(m -> m.isError() && m.getMessage().contains("serie"))); + } + + @Test + public void testNumeroNull() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateBasicFields("T001", null, msgs); + assertTrue(msgs.stream().anyMatch(m -> m.isError() && m.getMessage().contains("número"))); + } + + @Test + public void testNumeroZero() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateBasicFields("T001", 0, msgs); + assertTrue(msgs.stream().anyMatch(m -> m.isError() && m.getMessage().contains("mayor a 0"))); + } + + @Test + public void testValidBasicFields() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateBasicFields("T001", 1, msgs); + assertTrue(msgs.isEmpty()); + } + } + + // ================================================================ + // Serie coherence + // ================================================================ + + @Nested + class SerieCoherenceTests { + + @Test + public void testRemitenteSerieTOK() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateSerieCoherence("T001", "09", msgs); + assertTrue(msgs.isEmpty()); + } + + @Test + public void testRemitenteSerieVFails() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateSerieCoherence("V001", "09", msgs); + assertTrue(msgs.stream() + .anyMatch(m -> m.isError() + && m.getMessage().contains("GRE-Remitente (09)"))); + } + + @Test + public void testTransportistaSerieVOK() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateSerieCoherence("V001", "31", msgs); + assertTrue(msgs.isEmpty()); + } + + @Test + public void testTransportistaSerieTFails() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateSerieCoherence("T001", "31", msgs); + assertTrue(msgs.stream() + .anyMatch(m -> m.isError() + && m.getMessage().contains("GRE-Transportista (31)"))); + } + + @Test + public void testNullSerieSkips() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateSerieCoherence(null, "09", msgs); + assertTrue(msgs.isEmpty()); + } + + @Test + public void testNullTipoSkips() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateSerieCoherence("T001", null, msgs); + assertTrue(msgs.isEmpty()); + } + } + + // ================================================================ + // Remitente / Transportista Emisor + // ================================================================ + + @Nested + class PartyTests { + + @Test + public void testRemitenteNull() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateRemitente(null, msgs); + assertTrue(msgs.stream().anyMatch(m -> m.isError() && m.getMessage().contains("remitente"))); + } + + @Test + public void testRemitenteRucInvalido() { + List msgs = new ArrayList<>(); + Remitente rem = Remitente.builder().ruc("123").razonSocial("X").build(); + DespatchAdviceCommonValidator.validateRemitente(rem, msgs); + assertTrue(msgs.stream().anyMatch(m -> m.isError() && m.getMessage().contains("11 dígitos"))); + } + + @Test + public void testRemitenteValido() { + List msgs = new ArrayList<>(); + Remitente rem = Remitente.builder().ruc("12345678901").razonSocial("X").build(); + DespatchAdviceCommonValidator.validateRemitente(rem, msgs); + assertTrue(msgs.isEmpty()); + } + + @Test + public void testTransportistaEmisorNull() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateTransportistaEmisor(null, msgs); + assertTrue(msgs.stream() + .anyMatch(m -> m.isError() + && m.getMessage().contains("transportista emisor"))); + } + + @Test + public void testTransportistaEmisorRucInvalido() { + List msgs = new ArrayList<>(); + Transportista t = Transportista.builder() + .tipoDocumentoIdentidad("6") + .numeroDocumentoIdentidad("123") + .nombre("X") + .build(); + DespatchAdviceCommonValidator.validateTransportistaEmisor(t, msgs); + assertTrue(msgs.stream().anyMatch(m -> m.isError() && m.getMessage().contains("11 dígitos"))); + } + + @Test + public void testDestinatarioNull() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateDestinatario(null, msgs); + assertTrue(msgs.stream().anyMatch(m -> m.isError() && m.getMessage().contains("destinatario"))); + } + + @Test + public void testDestinatarioPresente() { + List msgs = new ArrayList<>(); + Destinatario d = Destinatario.builder() + .tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("12345678") + .nombre("C") + .build(); + DespatchAdviceCommonValidator.validateDestinatario(d, msgs); + assertTrue(msgs.isEmpty()); + } + + @Test + public void testTerceroNull() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateTerceroTransportista(null, msgs); + assertTrue(msgs.stream() + .anyMatch(m -> m.isError() + && m.getMessage().contains("remitente original"))); + } + + @Test + public void testTerceroPresente() { + List msgs = new ArrayList<>(); + Tercero t = Tercero.builder() + .tipoDocumentoIdentidad("6") + .numeroDocumentoIdentidad("20100010001") + .nombre("R") + .build(); + DespatchAdviceCommonValidator.validateTerceroTransportista(t, msgs); + assertTrue(msgs.isEmpty()); + } + } + + // ================================================================ + // Envio required fields + // ================================================================ + + @Nested + class EnvioRequiredTests { + + @Test + public void testEnvioNull() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateEnvioRequired(null, msgs); + assertTrue(msgs.stream().anyMatch(m -> m.isError() && m.getMessage().contains("envío"))); + } + + @Test + public void testEnvioSinMotivo() { + List msgs = new ArrayList<>(); + Envio envio = Envio.builder() + .pesoTotal(BigDecimal.ONE) + .tipoModalidadTraslado("02") + .fechaTraslado(LocalDate.now()) + .build(); + DespatchAdviceCommonValidator.validateEnvioRequired(envio, msgs); + assertTrue(msgs.stream() + .anyMatch(m -> m.isError() + && m.getMessage().contains("motivo de traslado"))); + } + + @Test + public void testEnvioSinPeso() { + List msgs = new ArrayList<>(); + Envio envio = Envio.builder() + .tipoTraslado("01") + .tipoModalidadTraslado("02") + .fechaTraslado(LocalDate.now()) + .build(); + DespatchAdviceCommonValidator.validateEnvioRequired(envio, msgs); + assertTrue(msgs.stream() + .anyMatch(m -> m.isError() + && m.getMessage().contains("peso total"))); + } + + @Test + public void testEnvioSinModalidad() { + List msgs = new ArrayList<>(); + Envio envio = Envio.builder() + .tipoTraslado("01") + .pesoTotal(BigDecimal.ONE) + .fechaTraslado(LocalDate.now()) + .build(); + DespatchAdviceCommonValidator.validateEnvioRequired(envio, msgs); + assertTrue(msgs.stream() + .anyMatch(m -> m.isError() + && m.getMessage().contains("modalidad de traslado"))); + } + + @Test + public void testEnvioSinFecha() { + List msgs = new ArrayList<>(); + Envio envio = Envio.builder() + .tipoTraslado("01") + .pesoTotal(BigDecimal.ONE) + .tipoModalidadTraslado("02") + .build(); + DespatchAdviceCommonValidator.validateEnvioRequired(envio, msgs); + assertTrue(msgs.stream() + .anyMatch(m -> m.isError() + && m.getMessage().contains("fecha de traslado"))); + } + + @Test + public void testEnvioCompletoValido() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateEnvioRequired(minimalEnvio(), msgs); + assertTrue(msgs.isEmpty()); + } + } + + // ================================================================ + // Partida / Destino + // ================================================================ + + @Nested + class PartidaDestinoTests { + + @Test + public void testSinPartida() { + List msgs = new ArrayList<>(); + Envio envio = Envio.builder() + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build(); + DespatchAdviceCommonValidator.validatePartidaDestino(envio, msgs); + assertTrue(msgs.stream().anyMatch(m -> m.isError() && m.getMessage().contains("partida"))); + } + + @Test + public void testSinDestino() { + List msgs = new ArrayList<>(); + Envio envio = Envio.builder() + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .build(); + DespatchAdviceCommonValidator.validatePartidaDestino(envio, msgs); + assertTrue(msgs.stream().anyMatch(m -> m.isError() && m.getMessage().contains("destino"))); + } + + @Test + public void testPartidaDestinoPresentes() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validatePartidaDestino(minimalEnvio(), msgs); + assertTrue(msgs.isEmpty()); + } + + @Test + public void testEnvioNullNoFalla() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validatePartidaDestino(null, msgs); + assertTrue(msgs.isEmpty()); + } + } + + // ================================================================ + // Modalidad Remitente + // ================================================================ + + @Nested + class ModalidadRemitenteTests { + + @Test + public void testPrivadoSinConductorNiVehiculo() { + List msgs = new ArrayList<>(); + Envio envio = Envio.builder() + .tipoTraslado("01") + .pesoTotal(BigDecimal.ONE) + .tipoModalidadTraslado("02") + .fechaTraslado(LocalDate.now()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build(); + DespatchAdviceCommonValidator.validateModalidadRemitente(envio, msgs); + assertEquals(2, msgs.stream().filter(ValidationMessage::isError).count()); + assertTrue(msgs.stream().anyMatch(m -> m.getMessage().contains("conductor"))); + assertTrue(msgs.stream().anyMatch(m -> m.getMessage().contains("vehículo"))); + } + + @Test + public void testPrivadoConIndicadorM1L() { + List msgs = new ArrayList<>(); + Envio envio = Envio.builder() + .tipoTraslado("01") + .pesoTotal(BigDecimal.ONE) + .tipoModalidadTraslado("02") + .fechaTraslado(LocalDate.now()) + .indicador("SUNAT_Envio_IndicadorTrasladoVehiculoM1L") + .build(); + DespatchAdviceCommonValidator.validateModalidadRemitente(envio, msgs); + assertTrue(msgs.isEmpty(), "M1/L exime conductor y vehículo"); + } + + @Test + public void testPrivadoConTransportista() { + List msgs = new ArrayList<>(); + Envio envio = Envio.builder() + .tipoTraslado("01") + .pesoTotal(BigDecimal.ONE) + .tipoModalidadTraslado("02") + .fechaTraslado(LocalDate.now()) + .chofer(Driver.builder() + .tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("11111111") + .nombres("J") + .apellidos("P") + .licencia("Q123") + .build()) + .vehiculo(Vehicle.builder().placa("ABC-123").build()) + .transportista(Transportista.builder() + .tipoDocumentoIdentidad("6") + .numeroDocumentoIdentidad("20300030003") + .nombre("T") + .build()) + .build(); + DespatchAdviceCommonValidator.validateModalidadRemitente(envio, msgs); + assertTrue(msgs.stream() + .anyMatch(m -> m.isError() + && m.getMessage().contains("Transporte privado no debe consignar transportista"))); + } + + @Test + public void testPublicoSinTransportista() { + List msgs = new ArrayList<>(); + Envio envio = Envio.builder() + .tipoTraslado("01") + .pesoTotal(BigDecimal.ONE) + .tipoModalidadTraslado("01") + .fechaTraslado(LocalDate.now()) + .build(); + DespatchAdviceCommonValidator.validateModalidadRemitente(envio, msgs); + assertTrue(msgs.stream() + .anyMatch(m -> m.isError() + && m.getMessage().contains("transportista"))); + } + + @Test + public void testPublicoConTransportista() { + List msgs = new ArrayList<>(); + Envio envio = Envio.builder() + .tipoTraslado("01") + .pesoTotal(BigDecimal.ONE) + .tipoModalidadTraslado("01") + .fechaTraslado(LocalDate.now()) + .transportista(Transportista.builder() + .tipoDocumentoIdentidad("6") + .numeroDocumentoIdentidad("20300030003") + .nombre("T") + .build()) + .build(); + DespatchAdviceCommonValidator.validateModalidadRemitente(envio, msgs); + assertTrue(msgs.isEmpty()); + } + + @Test + public void testModalidadNullNoFalla() { + List msgs = new ArrayList<>(); + Envio envio = Envio.builder().tipoTraslado("01").build(); + DespatchAdviceCommonValidator.validateModalidadRemitente(envio, msgs); + assertTrue(msgs.isEmpty()); + } + } + + // ================================================================ + // Conductor/Vehículo Transportista + // ================================================================ + + @Nested + class ConductorVehiculoTransportistaTests { + + @Test + public void testSinConductorNiVehiculo() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateConductorVehiculoTransportista(null, null, msgs); + assertEquals(2, msgs.stream().filter(ValidationMessage::isError).count()); + } + + @Test + public void testConductorVacio() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateConductorVehiculoTransportista( + new ArrayList<>(), Vehicle.builder().placa("X").build(), msgs); + assertEquals(1, msgs.stream().filter(ValidationMessage::isError).count()); + assertTrue(msgs.stream().anyMatch(m -> m.getMessage().contains("conductor"))); + } + + @Test + public void testVehiculoNull() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateConductorVehiculoTransportista( + List.of(Driver.builder().build()), null, msgs); + assertEquals(1, msgs.stream().filter(ValidationMessage::isError).count()); + assertTrue(msgs.stream().anyMatch(m -> m.getMessage().contains("vehículo"))); + } + + @Test + public void testConductorYVehiculoPresentes() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateConductorVehiculoTransportista( + List.of(Driver.builder().build()), + Vehicle.builder().placa("X").build(), msgs); + assertTrue(msgs.isEmpty()); + } + } + + // ================================================================ + // Comercio Exterior (WARNINGS) + // ================================================================ + + @Nested + class ComercioExteriorTests { + + @Test + public void testImportacionSinDAM() { + List msgs = new ArrayList<>(); + Envio envio = Envio.builder().tipoTraslado("08").build(); + DespatchAdviceCommonValidator.validateComercioExterior(envio, msgs); + assertTrue(msgs.stream().anyMatch(m -> m.isWarning() && m.getMessage().contains("DAM/DS")), + "Debe generar warning por falta de DAM/DS"); + } + + @Test + public void testExportacionSinPuerto() { + List msgs = new ArrayList<>(); + Envio envio = Envio.builder().tipoTraslado("09").build(); + DespatchAdviceCommonValidator.validateComercioExterior(envio, msgs); + assertTrue(msgs.stream() + .anyMatch(m -> m.isWarning() + && m.getMessage().contains("puerto o aeropuerto"))); + } + + @Test + public void testMotivoVentaSinAdvertencias() { + List msgs = new ArrayList<>(); + Envio envio = Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .build(); + DespatchAdviceCommonValidator.validateComercioExterior(envio, msgs); + assertTrue(msgs.isEmpty(), "Venta no requiere reglas de comercio exterior"); + } + + @Test + public void testComercioExteriorConDAMYPuerto() { + List msgs = new ArrayList<>(); + Envio envio = Envio.builder() + .tipoTraslado("08") + .declaracionAduanera(DeclaracionAduanera.builder() + .tipo("DAM") + .numero("118-2024-10-001") + .build()) + .puerto(Puerto.builder().codigo("PECLL").nombre("Callao").build()) + .build(); + DespatchAdviceCommonValidator.validateComercioExterior(envio, msgs); + assertTrue(msgs.isEmpty(), "Con DAM y puerto no debe haber warnings"); + } + + @Test + public void testComercioExteriorConAeropuerto() { + List msgs = new ArrayList<>(); + Envio envio = Envio.builder() + .tipoTraslado("09") + .declaracionAduanera(DeclaracionAduanera.builder() + .tipo("DAM") + .numero("118-2024-10-001") + .build()) + .aeropuerto(Puerto.builder().codigo("SPJC").nombre("Jorge Chávez").build()) + .build(); + DespatchAdviceCommonValidator.validateComercioExterior(envio, msgs); + assertTrue(msgs.isEmpty(), "Aeropuerto satisface el requisito de puerto"); + } + + @Test + public void testEnvioNullNoFalla() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateComercioExterior(null, msgs); + assertTrue(msgs.isEmpty()); + } + + @Test + public void testSeveridadEsWarningNoError() { + List msgs = new ArrayList<>(); + Envio envio = Envio.builder().tipoTraslado("10").build(); + DespatchAdviceCommonValidator.validateComercioExterior(envio, msgs); + assertTrue(msgs.stream().allMatch(ValidationMessage::isWarning), + "Todas las reglas de comercio exterior deben ser WARNING, no ERROR"); + assertTrue(msgs.stream().noneMatch(ValidationMessage::isError)); + } + } + + // ================================================================ + // Detalles + // ================================================================ + + @Nested + class DetallesTests { + + @Test + public void testDetallesNull() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateDetalles(null, msgs); + assertTrue(msgs.stream().anyMatch(m -> m.isError() && m.getMessage().contains("detalle"))); + } + + @Test + public void testDetallesVacios() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateDetalles(new ArrayList<>(), msgs); + assertTrue(msgs.stream().anyMatch(m -> m.isError() && m.getMessage().contains("detalle"))); + } + + @Test + public void testDetallesConItems() { + List msgs = new ArrayList<>(); + DespatchAdviceCommonValidator.validateDetalles( + List.of(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE) + .unidadMedida("NIU") + .codigo("001") + .build()), + msgs); + assertTrue(msgs.isEmpty()); + } + } + + // ================================================================ + // validateDetailed integration via DespatchAdviceValidator + // ================================================================ + + @Nested + class ValidateDetailedIntegration { + + @Test + public void testValidateDetailedReturnsValidResult() { + DespatchAdvice da = DespatchAdvice.builder() + .serie("T001") + .numero(1) + .tipoComprobante("09") + .remitente(Remitente.builder().ruc("12345678901").razonSocial("Test").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("12345678") + .nombre("C") + .build()) + .envio(minimalEnvio()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE) + .unidadMedida("NIU") + .codigo("001") + .build()) + .build(); + + ValidationResult result = DespatchAdviceValidator.validateDetailed(da); + assertTrue(result.isValid(), "Debe ser válido: " + result.getErrors()); + assertFalse(result.hasErrors()); + } + + @Test + public void testValidateDetailedReturnsWarningsForComercioExterior() { + DespatchAdvice da = DespatchAdvice.builder() + .serie("T001") + .numero(1) + .tipoComprobante("09") + .remitente(Remitente.builder().ruc("12345678901").razonSocial("Test").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("12345678") + .nombre("C") + .build()) + .envio(Envio.builder() + .tipoTraslado("08") // Importación + .pesoTotal(BigDecimal.ONE) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("02") + .fechaTraslado(LocalDate.now()) + .chofer(Driver.builder() + .tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("11111111") + .nombres("J") + .apellidos("P") + .licencia("Q123") + .build()) + .vehiculo(Vehicle.builder().placa("ABC-123").build()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE) + .unidadMedida("NIU") + .codigo("001") + .build()) + .build(); + + ValidationResult result = DespatchAdviceValidator.validateDetailed(da); + assertTrue(result.isValid(), "No debe tener errores: " + result.getErrors()); + assertTrue(result.hasWarnings(), "Debe tener warnings de comercio exterior"); + assertTrue(result.getWarnings().stream().anyMatch(w -> w.contains("DAM/DS"))); + } + + @Test + public void testValidateReturnsOnlyErrors() { + DespatchAdvice da = DespatchAdvice.builder() + .serie("T001") + .numero(1) + .tipoComprobante("09") + .remitente(Remitente.builder().ruc("12345678901").razonSocial("Test").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("12345678") + .nombre("C") + .build()) + .envio(Envio.builder() + .tipoTraslado("08") // Importación + .pesoTotal(BigDecimal.ONE) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("02") + .fechaTraslado(LocalDate.now()) + .chofer(Driver.builder() + .tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("11111111") + .nombres("J") + .apellidos("P") + .licencia("Q123") + .build()) + .vehiculo(Vehicle.builder().placa("ABC-123").build()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE) + .unidadMedida("NIU") + .codigo("001") + .build()) + .build(); + + List errors = DespatchAdviceValidator.validate(da); + assertTrue(errors.isEmpty(), + "validate() must not include warnings: " + errors); + } + } + + // ================================================================ + // GRERemitente.validateDetailed() + // ================================================================ + + @Nested + class GRERemitenteValidateDetailedTests { + + @Test + public void testValidateDetailedValid() { + GRERemitente gre = GRERemitente.builder() + .serie("T001") + .numero(1) + .remitente(Remitente.builder().ruc("12345678901").razonSocial("Test").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("12345678") + .nombre("C") + .build()) + .envio(minimalEnvio()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE) + .unidadMedida("NIU") + .codigo("001") + .build()) + .build(); + + ValidationResult result = gre.validateDetailed(); + assertTrue(result.isValid(), "GRERemitente válido: " + result.getErrors()); + } + + @Test + public void testValidateDetailedWithImportWarnings() { + GRERemitente gre = GRERemitente.builder() + .serie("T001") + .numero(1) + .remitente(Remitente.builder().ruc("12345678901").razonSocial("Test").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("12345678") + .nombre("C") + .build()) + .envio(Envio.builder() + .tipoTraslado("08") // Importación + .pesoTotal(BigDecimal.ONE) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("02") + .fechaTraslado(LocalDate.now()) + .chofer(Driver.builder() + .tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("11111111") + .nombres("J") + .apellidos("P") + .licencia("Q123") + .build()) + .vehiculo(Vehicle.builder().placa("ABC-123").build()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE) + .unidadMedida("NIU") + .codigo("001") + .build()) + .build(); + + ValidationResult result = gre.validateDetailed(); + assertTrue(result.isValid(), "No errors: " + result.getErrors()); + assertTrue(result.hasWarnings()); + } + } + + // ================================================================ + // GRETransportista.validateDetailed() + // ================================================================ + + @Nested + class GRETransportistaValidateDetailedTests { + + @Test + public void testValidateDetailedValid() { + GRETransportista gre = GRETransportista.builder() + .serie("V001") + .numero(1) + .transportistaEmisor(Transportista.builder() + .tipoDocumentoIdentidad("6") + .numeroDocumentoIdentidad("20300030003") + .nombre("Transportes") + .build()) + .remitente(Tercero.builder() + .tipoDocumentoIdentidad("6") + .numeroDocumentoIdentidad("20100010001") + .nombre("R") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("6") + .numeroDocumentoIdentidad("20200020002") + .nombre("D") + .build()) + .conductor(Driver.builder() + .tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("44444444") + .nombres("M") + .apellidos("T") + .licencia("Q444") + .build()) + .vehiculo(Vehicle.builder().placa("XYZ-789").build()) + .envio(Envio.builder() + .tipoTraslado("01") + .pesoTotal(BigDecimal.ONE) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("01") + .fechaTraslado(LocalDate.now()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE) + .unidadMedida("NIU") + .codigo("001") + .build()) + .build(); + + ValidationResult result = gre.validateDetailed(); + assertTrue(result.isValid(), "Válido: " + result.getErrors()); + } + + @Test + public void testValidateDetailedSinConductorEsError() { + GRETransportista gre = GRETransportista.builder() + .serie("V001") + .numero(1) + .transportistaEmisor(Transportista.builder() + .tipoDocumentoIdentidad("6") + .numeroDocumentoIdentidad("20300030003") + .nombre("T") + .build()) + .remitente(Tercero.builder() + .tipoDocumentoIdentidad("6") + .numeroDocumentoIdentidad("20100010001") + .nombre("R") + .build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("6") + .numeroDocumentoIdentidad("20200020002") + .nombre("D") + .build()) + // Sin conductor + .vehiculo(Vehicle.builder().placa("XYZ-789").build()) + .envio(Envio.builder() + .tipoTraslado("01") + .pesoTotal(BigDecimal.ONE) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("01") + .fechaTraslado(LocalDate.now()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE) + .unidadMedida("NIU") + .codigo("001") + .build()) + .build(); + + ValidationResult result = gre.validateDetailed(); + assertFalse(result.isValid()); + assertTrue(result.getErrors().stream().anyMatch(e -> e.contains("conductor"))); + } + } +} diff --git a/xbuilder/core/src/test/java/unit/validator/ValidationCoherenceTest.java b/xbuilder/core/src/test/java/unit/validator/ValidationCoherenceTest.java new file mode 100644 index 00000000..d9a0cbc2 --- /dev/null +++ b/xbuilder/core/src/test/java/unit/validator/ValidationCoherenceTest.java @@ -0,0 +1,315 @@ +package unit.validator; + +import io.github.project.openubl.xbuilder.content.catalogs.Catalog18; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog20; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog6; +import io.github.project.openubl.xbuilder.content.models.standard.guia.*; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationResult; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests de integración que verifican la coherencia entre los tres validadores + * (DespatchAdviceValidator, GRERemitente, GRETransportista) después de la + * centralización en DespatchAdviceCommonValidator. + *

    + * Asegura que: + *

      + *
    • La misma regla produce el mismo resultado en los tres puntos de entrada
    • + *
    • Los mensajes de error tienen el texto esperado
    • + *
    • La conversión toDespatchAdvice() + validación produce resultados coherentes
    • + *
    • validate() y validateDetailed() son consistentes
    • + *
    + */ +public class ValidationCoherenceTest { + + // ================================================================ + // Helpers + // ================================================================ + + private static GRERemitente.GRERemitenteBuilder minimalRemitente() { + return GRERemitente.builder() + .serie("T001").numero(1) + .remitente(Remitente.builder().ruc("12345678912").razonSocial("Test S.A.C.").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) + .numeroDocumentoIdentidad("12345678").nombre("Cliente").build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PRIVADO.getCode()) + .fechaTraslado(LocalDate.now()) + .chofer(Driver.builder() + .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("11111111") + .nombres("J").apellidos("P").licencia("Q123").build()) + .vehiculo(Vehicle.builder().placa("ABC-123").build()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()); + } + + private static GRETransportista.GRETransportistaBuilder minimalTransportista() { + return GRETransportista.builder() + .serie("V001").numero(1) + .transportistaEmisor(Transportista.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20300030003").nombre("Transportes S.A.C.").build()) + .remitente(Tercero.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20100010001").nombre("Remitente S.A.C.").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) + .numeroDocumentoIdentidad("20200020002").nombre("Destino S.A.").build()) + .conductor(Driver.builder() + .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("44444444") + .nombres("M").apellidos("T").licencia("Q444").build()) + .vehiculo(Vehicle.builder().placa("XYZ-789").build()) + .envio(Envio.builder() + .tipoTraslado(Catalog20.VENTA.getCode()) + .pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado(Catalog18.TRANSPORTE_PUBLICO.getCode()) + .fechaTraslado(LocalDate.now()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()); + } + + // ================================================================ + // Coherencia: GRERemitente.validate() ↔ DespatchAdviceValidator.validate() + // ================================================================ + + @Nested + class RemitenteCoherence { + + @Test + public void testValidRemitentePassesBothValidators() { + GRERemitente gre = minimalRemitente().build(); + + // GRERemitente propio + List remitenteErrors = gre.validate(); + assertTrue(remitenteErrors.isEmpty(), "GRERemitente.validate: " + remitenteErrors); + + // Post-conversión + DespatchAdvice da = gre.toDespatchAdvice(); + List daErrors = DespatchAdviceValidator.validate(da); + assertTrue(daErrors.isEmpty(), "DespatchAdviceValidator.validate: " + daErrors); + } + + @Test + public void testInvalidSerieDetectedByBoth() { + GRERemitente gre = minimalRemitente().serie("V001").build(); + + List remitenteErrors = gre.validate(); + assertTrue(remitenteErrors.stream().anyMatch(e -> e.contains("'T'")), + "GRERemitente debe detectar serie inválida"); + + DespatchAdvice da = gre.toDespatchAdvice(); + List daErrors = DespatchAdviceValidator.validate(da); + assertTrue(daErrors.stream().anyMatch(e -> e.contains("GRE-Remitente (09)")), + "DespatchAdviceValidator debe detectar serie incoherente con tipo 09"); + } + + @Test + public void testRucInvalidoDetectedByBoth() { + GRERemitente gre = minimalRemitente() + .remitente(Remitente.builder().ruc("123").razonSocial("X").build()) + .build(); + + List remitenteErrors = gre.validate(); + assertTrue(remitenteErrors.stream().anyMatch(e -> e.contains("11 dígitos"))); + + DespatchAdvice da = gre.toDespatchAdvice(); + List daErrors = DespatchAdviceValidator.validate(da); + assertTrue(daErrors.stream().anyMatch(e -> e.contains("11 dígitos"))); + } + + @Test + public void testValidateAndValidateDetailedAreConsistent() { + GRERemitente gre = minimalRemitente().serie(null).build(); + + List errors = gre.validate(); + ValidationResult result = gre.validateDetailed(); + + assertEquals(errors, result.getErrors(), + "validate() y validateDetailed().getErrors() deben coincidir"); + } + } + + // ================================================================ + // Coherencia: GRETransportista.validate() ↔ DespatchAdviceValidator.validate() + // ================================================================ + + @Nested + class TransportistaCoherence { + + @Test + public void testValidTransportistaPassesBothValidators() { + GRETransportista gre = minimalTransportista().build(); + + List transportistaErrors = gre.validate(); + assertTrue(transportistaErrors.isEmpty(), + "GRETransportista.validate: " + transportistaErrors); + + DespatchAdvice da = gre.toDespatchAdvice(); + List daErrors = DespatchAdviceValidator.validate(da); + assertTrue(daErrors.isEmpty(), + "DespatchAdviceValidator.validate: " + daErrors); + } + + @Test + public void testInvalidSerieDetectedByBoth() { + GRETransportista gre = minimalTransportista().serie("T001").build(); + + List transportistaErrors = gre.validate(); + assertTrue(transportistaErrors.stream().anyMatch(e -> e.contains("'V'"))); + + DespatchAdvice da = gre.toDespatchAdvice(); + List daErrors = DespatchAdviceValidator.validate(da); + assertTrue(daErrors.stream().anyMatch(e -> e.contains("GRE-Transportista (31)"))); + } + + @Test + public void testConversionInjectsConductoresAndVehiculo() { + GRETransportista gre = minimalTransportista() + .conductor(Driver.builder() + .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("99999999") + .nombres("Extra").apellidos("D").licencia("Q999").build()) + .build(); + + DespatchAdvice da = gre.toDespatchAdvice(); + assertNotNull(da.getEnvio().getChoferes()); + assertEquals(2, da.getEnvio().getChoferes().size(), + "Conductores deben inyectarse en envío"); + assertNotNull(da.getEnvio().getVehiculo(), + "Vehículo debe inyectarse en envío"); + } + + @Test + public void testValidateAndValidateDetailedAreConsistent() { + GRETransportista gre = minimalTransportista() + .transportistaEmisor(null).build(); + + List errors = gre.validate(); + ValidationResult result = gre.validateDetailed(); + + assertEquals(errors, result.getErrors()); + } + } + + // ================================================================ + // toDespatchAdviceValidated() integration + // ================================================================ + + @Nested + class ValidatedConversion { + + @Test + public void testRemitenteValidatedSuccess() { + GRERemitente gre = minimalRemitente().build(); + DespatchAdvice da = gre.toDespatchAdviceValidated(); + assertNotNull(da); + assertEquals("09", da.getTipoComprobante()); + assertTrue(da.isGRERemitente()); + } + + @Test + public void testRemitenteValidatedFails() { + GRERemitente gre = minimalRemitente().serie("V001").build(); + IllegalStateException ex = assertThrows(IllegalStateException.class, + gre::toDespatchAdviceValidated); + assertTrue(ex.getMessage().contains("GRE-Remitente inválido")); + } + + @Test + public void testTransportistaValidatedSuccess() { + GRETransportista gre = minimalTransportista().build(); + DespatchAdvice da = gre.toDespatchAdviceValidated(); + assertNotNull(da); + assertEquals("31", da.getTipoComprobante()); + assertTrue(da.isGRETransportista()); + } + + @Test + public void testTransportistaValidatedFails() { + GRETransportista gre = minimalTransportista().serie("T001").build(); + IllegalStateException ex = assertThrows(IllegalStateException.class, + gre::toDespatchAdviceValidated); + assertTrue(ex.getMessage().contains("GRE-Transportista inválido")); + } + } + + // ================================================================ + // Severidad uniformidad + // ================================================================ + + @Nested + class SeverityUniformity { + + @Test + public void testComercioExteriorWarningsInAllThreeValidators() { + // DespatchAdviceValidator + DespatchAdvice da = DespatchAdvice.builder() + .serie("T001").numero(1).tipoComprobante("09") + .remitente(Remitente.builder().ruc("12345678912").razonSocial("T").build()) + .destinatario(Destinatario.builder() + .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("12345678").nombre("C").build()) + .envio(Envio.builder() + .tipoTraslado("08") // Importación + .pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("02").fechaTraslado(LocalDate.now()) + .chofer(Driver.builder().tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("11111111").nombres("J") + .apellidos("P").licencia("Q123").build()) + .vehiculo(Vehicle.builder().placa("ABC-123").build()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()) + .detalle(DespatchAdviceItem.builder() + .cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()) + .build(); + + ValidationResult daResult = DespatchAdviceValidator.validateDetailed(da); + assertTrue(daResult.isValid(), "Importación sin DAM: valid (warnings only)"); + assertTrue(daResult.hasWarnings(), "Debe tener warnings"); + assertTrue(daResult.getWarnings().stream().anyMatch(w -> w.contains("DAM/DS"))); + + // GRERemitente: same + GRERemitente gre = minimalRemitente().envio(Envio.builder() + .tipoTraslado("08").pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("02").fechaTraslado(LocalDate.now()) + .chofer(Driver.builder().tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("11111111").nombres("J") + .apellidos("P").licencia("Q123").build()) + .vehiculo(Vehicle.builder().placa("ABC-123").build()) + .partida(Partida.builder().ubigeo("010101").direccion("O").build()) + .destino(Destino.builder().ubigeo("020202").direccion("D").build()) + .build()).build(); + + ValidationResult greResult = gre.validateDetailed(); + assertTrue(greResult.isValid()); + assertTrue(greResult.hasWarnings()); + assertTrue(greResult.getWarnings().stream().anyMatch(w -> w.contains("DAM/DS"))); + } + + @Test + public void testHardErrorsNeverDowngradedToWarning() { + // Missing serie: must be ERROR everywhere + GRERemitente gre = minimalRemitente().serie(null).build(); + ValidationResult result = gre.validateDetailed(); + assertTrue(result.hasErrors()); + assertTrue(result.getMessages().stream() + .filter(m -> m.getMessage().contains("serie")) + .allMatch(m -> m.getSeverity() == io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationSeverity.ERROR)); + } + } +} From 78e8d79ad6e1f13d741787e605a77df2d6cf5752 Mon Sep 17 00:00:00 2001 From: Edwin Luis Barboza Pinedo Date: Sun, 29 Mar 2026 00:42:43 -0500 Subject: [PATCH 7/8] Refactor ValidationCoherenceTest for improved readability and consistency in builder patterns Signed-off-by: Edwin Luis Barboza Pinedo --- .../validator/ValidationCoherenceTest.java | 131 ++++++++++++------ 1 file changed, 92 insertions(+), 39 deletions(-) diff --git a/xbuilder/core/src/test/java/unit/validator/ValidationCoherenceTest.java b/xbuilder/core/src/test/java/unit/validator/ValidationCoherenceTest.java index d9a0cbc2..063accb2 100644 --- a/xbuilder/core/src/test/java/unit/validator/ValidationCoherenceTest.java +++ b/xbuilder/core/src/test/java/unit/validator/ValidationCoherenceTest.java @@ -15,16 +15,15 @@ import static org.junit.jupiter.api.Assertions.*; /** - * Tests de integración que verifican la coherencia entre los tres validadores - * (DespatchAdviceValidator, GRERemitente, GRETransportista) después de la - * centralización en DespatchAdviceCommonValidator. + * Tests de integración que verifican la coherencia entre los tres validadores (DespatchAdviceValidator, GRERemitente, + * GRETransportista) después de la centralización en DespatchAdviceCommonValidator. *

    * Asegura que: *

      - *
    • La misma regla produce el mismo resultado en los tres puntos de entrada
    • - *
    • Los mensajes de error tienen el texto esperado
    • - *
    • La conversión toDespatchAdvice() + validación produce resultados coherentes
    • - *
    • validate() y validateDetailed() son consistentes
    • + *
    • La misma regla produce el mismo resultado en los tres puntos de entrada
    • + *
    • Los mensajes de error tienen el texto esperado
    • + *
    • La conversión toDespatchAdvice() + validación produce resultados coherentes
    • + *
    • validate() y validateDetailed() son consistentes
    • *
    */ public class ValidationCoherenceTest { @@ -35,53 +34,79 @@ public class ValidationCoherenceTest { private static GRERemitente.GRERemitenteBuilder minimalRemitente() { return GRERemitente.builder() - .serie("T001").numero(1) + .serie("T001") + .numero(1) .remitente(Remitente.builder().ruc("12345678912").razonSocial("Test S.A.C.").build()) .destinatario(Destinatario.builder() .tipoDocumentoIdentidad(Catalog6.DNI.getCode()) - .numeroDocumentoIdentidad("12345678").nombre("Cliente").build()) + .numeroDocumentoIdentidad("12345678") + .nombre("Cliente") + .build()) .envio(Envio.builder() .tipoTraslado(Catalog20.VENTA.getCode()) - .pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .pesoTotal(BigDecimal.ONE) + .pesoTotalUnidadMedida("KGM") .tipoModalidadTraslado(Catalog18.TRANSPORTE_PRIVADO.getCode()) .fechaTraslado(LocalDate.now()) .chofer(Driver.builder() - .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("11111111") - .nombres("J").apellidos("P").licencia("Q123").build()) + .tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("11111111") + .nombres("J") + .apellidos("P") + .licencia("Q123") + .build()) .vehiculo(Vehicle.builder().placa("ABC-123").build()) .partida(Partida.builder().ubigeo("010101").direccion("O").build()) .destino(Destino.builder().ubigeo("020202").direccion("D").build()) .build()) .detalle(DespatchAdviceItem.builder() - .cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()); + .cantidad(BigDecimal.ONE) + .unidadMedida("NIU") + .codigo("001") + .build()); } private static GRETransportista.GRETransportistaBuilder minimalTransportista() { return GRETransportista.builder() - .serie("V001").numero(1) + .serie("V001") + .numero(1) .transportistaEmisor(Transportista.builder() .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) - .numeroDocumentoIdentidad("20300030003").nombre("Transportes S.A.C.").build()) + .numeroDocumentoIdentidad("20300030003") + .nombre("Transportes S.A.C.") + .build()) .remitente(Tercero.builder() .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) - .numeroDocumentoIdentidad("20100010001").nombre("Remitente S.A.C.").build()) + .numeroDocumentoIdentidad("20100010001") + .nombre("Remitente S.A.C.") + .build()) .destinatario(Destinatario.builder() .tipoDocumentoIdentidad(Catalog6.RUC.getCode()) - .numeroDocumentoIdentidad("20200020002").nombre("Destino S.A.").build()) + .numeroDocumentoIdentidad("20200020002") + .nombre("Destino S.A.") + .build()) .conductor(Driver.builder() - .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("44444444") - .nombres("M").apellidos("T").licencia("Q444").build()) + .tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("44444444") + .nombres("M") + .apellidos("T") + .licencia("Q444") + .build()) .vehiculo(Vehicle.builder().placa("XYZ-789").build()) .envio(Envio.builder() .tipoTraslado(Catalog20.VENTA.getCode()) - .pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") + .pesoTotal(BigDecimal.ONE) + .pesoTotalUnidadMedida("KGM") .tipoModalidadTraslado(Catalog18.TRANSPORTE_PUBLICO.getCode()) .fechaTraslado(LocalDate.now()) .partida(Partida.builder().ubigeo("010101").direccion("O").build()) .destino(Destino.builder().ubigeo("020202").direccion("D").build()) .build()) .detalle(DespatchAdviceItem.builder() - .cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()); + .cantidad(BigDecimal.ONE) + .unidadMedida("NIU") + .codigo("001") + .build()); } // ================================================================ @@ -182,8 +207,12 @@ public void testInvalidSerieDetectedByBoth() { public void testConversionInjectsConductoresAndVehiculo() { GRETransportista gre = minimalTransportista() .conductor(Driver.builder() - .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("99999999") - .nombres("Extra").apellidos("D").licencia("Q999").build()) + .tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("99999999") + .nombres("Extra") + .apellidos("D") + .licencia("Q999") + .build()) .build(); DespatchAdvice da = gre.toDespatchAdvice(); @@ -197,7 +226,8 @@ public void testConversionInjectsConductoresAndVehiculo() { @Test public void testValidateAndValidateDetailedAreConsistent() { GRETransportista gre = minimalTransportista() - .transportistaEmisor(null).build(); + .transportistaEmisor(null) + .build(); List errors = gre.validate(); ValidationResult result = gre.validateDetailed(); @@ -259,23 +289,37 @@ class SeverityUniformity { public void testComercioExteriorWarningsInAllThreeValidators() { // DespatchAdviceValidator DespatchAdvice da = DespatchAdvice.builder() - .serie("T001").numero(1).tipoComprobante("09") + .serie("T001") + .numero(1) + .tipoComprobante("09") .remitente(Remitente.builder().ruc("12345678912").razonSocial("T").build()) .destinatario(Destinatario.builder() - .tipoDocumentoIdentidad("1").numeroDocumentoIdentidad("12345678").nombre("C").build()) + .tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("12345678") + .nombre("C") + .build()) .envio(Envio.builder() .tipoTraslado("08") // Importación - .pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") - .tipoModalidadTraslado("02").fechaTraslado(LocalDate.now()) - .chofer(Driver.builder().tipoDocumentoIdentidad("1") - .numeroDocumentoIdentidad("11111111").nombres("J") - .apellidos("P").licencia("Q123").build()) + .pesoTotal(BigDecimal.ONE) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("02") + .fechaTraslado(LocalDate.now()) + .chofer(Driver.builder() + .tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("11111111") + .nombres("J") + .apellidos("P") + .licencia("Q123") + .build()) .vehiculo(Vehicle.builder().placa("ABC-123").build()) .partida(Partida.builder().ubigeo("010101").direccion("O").build()) .destino(Destino.builder().ubigeo("020202").direccion("D").build()) .build()) .detalle(DespatchAdviceItem.builder() - .cantidad(BigDecimal.ONE).unidadMedida("NIU").codigo("001").build()) + .cantidad(BigDecimal.ONE) + .unidadMedida("NIU") + .codigo("001") + .build()) .build(); ValidationResult daResult = DespatchAdviceValidator.validateDetailed(da); @@ -285,11 +329,18 @@ public void testComercioExteriorWarningsInAllThreeValidators() { // GRERemitente: same GRERemitente gre = minimalRemitente().envio(Envio.builder() - .tipoTraslado("08").pesoTotal(BigDecimal.ONE).pesoTotalUnidadMedida("KGM") - .tipoModalidadTraslado("02").fechaTraslado(LocalDate.now()) - .chofer(Driver.builder().tipoDocumentoIdentidad("1") - .numeroDocumentoIdentidad("11111111").nombres("J") - .apellidos("P").licencia("Q123").build()) + .tipoTraslado("08") + .pesoTotal(BigDecimal.ONE) + .pesoTotalUnidadMedida("KGM") + .tipoModalidadTraslado("02") + .fechaTraslado(LocalDate.now()) + .chofer(Driver.builder() + .tipoDocumentoIdentidad("1") + .numeroDocumentoIdentidad("11111111") + .nombres("J") + .apellidos("P") + .licencia("Q123") + .build()) .vehiculo(Vehicle.builder().placa("ABC-123").build()) .partida(Partida.builder().ubigeo("010101").direccion("O").build()) .destino(Destino.builder().ubigeo("020202").direccion("D").build()) @@ -307,9 +358,11 @@ public void testHardErrorsNeverDowngradedToWarning() { GRERemitente gre = minimalRemitente().serie(null).build(); ValidationResult result = gre.validateDetailed(); assertTrue(result.hasErrors()); - assertTrue(result.getMessages().stream() + assertTrue(result.getMessages() + .stream() .filter(m -> m.getMessage().contains("serie")) - .allMatch(m -> m.getSeverity() == io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationSeverity.ERROR)); + .allMatch(m -> m + .getSeverity() == io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationSeverity.ERROR)); } } } From 2643b574bbbfbe4aef0b19295030ccb968334812 Mon Sep 17 00:00:00 2001 From: Edwin Luis Barboza Pinedo Date: Sun, 29 Mar 2026 03:24:24 -0500 Subject: [PATCH 8/8] feat: Implement business rules validation for electronic invoices and notes - Added InvoiceValidator for validating electronic invoices (01) and sales receipts (03) according to SUNAT guidelines. - Introduced NoteValidator for validating credit and debit notes, ensuring compliance with SUNAT regulations. - Created SalesDocumentValidator for shared validation logic between invoices and notes. - Developed unit tests for InvoiceValidator and NoteValidator to ensure correctness of validation rules. - Updated XML test resources to reflect changes in tax tier range. - Refactored RestSunatResponseProcessor for improved readability and structure. --- .../xbuilder/content/catalogs/Catalog51.java | 62 ++- .../xbuilder/content/catalogs/Catalog54.java | 52 +- .../xbuilder/content/catalogs/Catalog8.java | 15 +- .../content/models/common/Firmante.java | 8 + .../models/standard/general/Detraccion.java | 15 +- .../general/DocumentoVentaDetalle.java | 103 +++- .../models/standard/general/Invoice.java | 18 + .../standard/general/InvoiceValidator.java | 211 ++++++++ .../content/models/standard/general/Note.java | 16 +- .../standard/general/NoteValidator.java | 106 ++++ .../models/standard/general/Percepcion.java | 17 + .../standard/general/SalesDocument.java | 8 + .../general/SalesDocumentValidator.java | 84 ++++ .../templates/Renderer/despatchAdvice.xml | 3 +- .../templates/ubl/common/signature.xml | 2 +- .../ubl/standard/include/document-line.xml | 15 + .../general/InvoiceValidatorTest.java | 474 ++++++++++++++++++ .../standard/general/NoteValidatorTest.java | 199 ++++++++ .../isc_sistemaDePreciosDeVentalAlPublico.xml | 2 +- .../DespatchAdviceCarrierTest/carrierData.xml | 1 - .../exportacionSinDAM.xml | 1 - .../importacionDAMTotal.xml | 1 - .../mercanciaExtranjera.xml | 1 - .../DespatchAdviceComplexTest/complexData.xml | 1 - .../DespatchAdviceTest/minData.xml | 1 - .../mercanciaExtranjeraMotivo19.xml | 1 - .../multiplesConductores.xml | 1 - .../transportePrivadoBasico.xml | 1 - .../transportePublico.xml | 1 - .../transporteSubcontratado.xml | 1 - .../vehiculoConCarreta.xml | 1 - .../vehiculoM1L.xml | 1 - .../transportistaBasico.xml | 1 - .../transportistaComercioExterior.xml | 1 - .../transportistaConCarreta.xml | 1 - .../transportistaMultiplesConductores.xml | 1 - .../isc_sistemaDePreciosDeVentalAlPublico.xml | 2 +- .../routes/RestSunatResponseProcessor.java | 17 +- 38 files changed, 1401 insertions(+), 45 deletions(-) create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/InvoiceValidator.java create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/NoteValidator.java create mode 100644 xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/SalesDocumentValidator.java create mode 100644 xbuilder/core/src/test/java/io/github/project/openubl/xbuilder/content/models/standard/general/InvoiceValidatorTest.java create mode 100644 xbuilder/core/src/test/java/io/github/project/openubl/xbuilder/content/models/standard/general/NoteValidatorTest.java diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog51.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog51.java index cd7c0274..38f6c814 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog51.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog51.java @@ -1,9 +1,69 @@ package io.github.project.openubl.xbuilder.content.catalogs; +/** + * Catálogo 51: Código de tipo de operación. + *

    + * Fuente: SUNAT – Anexo V de la RS 097-2012/SUNAT y modificatorias. Usado en {@code cbc:InvoiceTypeCode/@listID} para + * facturas y boletas. + *

    + * + * @see Guía XML Factura 2.1 – Catálogo 51 + */ public enum Catalog51 implements Catalog { + + // ── Operaciones comunes ────────────────────────────────────────── + /** Venta interna (operación estándar gravada, exonerada, inafecta o mixta). */ VENTA_INTERNA("0101"), + /** Venta interna – anticipos. */ + VENTA_INTERNA_ANTICIPOS("0113"), + /** Venta itinerante. */ + VENTA_ITINERANTE("0112"), + + // ── Exportación ────────────────────────────────────────────────── + /** Exportación de bienes. */ + EXPORTACION_BIENES("0200"), + /** Exportación de servicios – prestación de servicios (num. 1 art. 33 Ley IGV). */ + EXPORTACION_SERVICIOS_PRESTACION("0201"), + /** Exportación de servicios – hospedaje no domiciliado. */ + EXPORTACION_SERVICIOS_HOSPEDAJE("0202"), + /** Exportación de servicios – transporte navieros. */ + EXPORTACION_SERVICIOS_TRANSPORTE_NAVIEROS("0203"), + /** Exportación de servicios – servicios a turistas no domiciliados. */ + EXPORTACION_SERVICIOS_TURISTAS("0204"), + /** Exportación de servicios – venta de bienes a pasajeros. */ + EXPORTACION_SERVICIOS_BIENES_PASAJEROS("0205"), + /** Exportación de servicios – asistencia técnica. */ + EXPORTACION_SERVICIOS_ASISTENCIA_TECNICA("0206"), + /** Exportación de servicios – otros (arts. 33, 33-A, 76 Ley IGV). */ + EXPORTACION_SERVICIOS_OTROS("0207"), + /** Exportación de servicios – prestación realizada en zona franca. */ + EXPORTACION_SERVICIOS_ZONA_FRANCA("0208"), + + // ── Operaciones con no domiciliados ────────────────────────────── + /** Operación sujeta a detracción – recursos hidrobiológicos. */ OPERACION_SUJETA_A_DETRACCION("1001"), - OPERACION_SUJETA_A_PERCEPCION("2001"); + /** Operación sujeta a detracción – servicios de transporte pasajeros. */ + OPERACION_SUJETA_DETRACCION_TRANSPORTE_PASAJEROS("1002"), + /** Operación sujeta a detracción – servicios de transporte carga. */ + OPERACION_SUJETA_DETRACCION_TRANSPORTE_CARGA("1003"), + /** Operación sujeta a detracción – IVAP (arroz pilado). */ + OPERACION_SUJETA_DETRACCION_IVAP("1004"), + + // ── Percepción ─────────────────────────────────────────────────── + /** Operación sujeta a percepción. */ + OPERACION_SUJETA_A_PERCEPCION("2001"), + + // ── Gratuitas ──────────────────────────────────────────────────── + /** Operación gratuita – transferencia gratuita. */ + OPERACION_GRATUITA("0112"), + + // ── NRUS ───────────────────────────────────────────────────────── + /** Venta realizada por sujeto del NRUS. */ + VENTA_NRUS("0113"), + + // ── Otros ──────────────────────────────────────────────────────── + /** Factura guía (venta interna + guía de remisión embebida). */ + FACTURA_GUIA("0401"); private final String code; diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog54.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog54.java index 58aa6fef..d4ff6868 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog54.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog54.java @@ -1,9 +1,59 @@ package io.github.project.openubl.xbuilder.content.catalogs; +/** + * Catálogo 54: Códigos de bienes y servicios sujetos a detracción. + *

    + * Fuente: SUNAT – RS 183-2004/SUNAT y modificatorias (RS 071-2018, RS 082-2018, etc.). Usado en + * {@code cbc:PaymentMeansID/@schemeName="Codigo de detraccion SUNAT"}. + *

    + * + * @see Guía XML Factura 2.1 – Catálogo 54 + */ public enum Catalog54 implements Catalog { + + // ── Venta de bienes (Anexo 1) ───────────────────────────────── AZUCAR("001"), ALCOHOL_ETILICO("003"), - RECURSOS_HIDROBIOLOGICOS("004"); + RECURSOS_HIDROBIOLOGICOS("004"), + MAIZ_AMARILLO_DURO("005"), + ALGODON("006"), + CANA_DE_AZUCAR("007"), + MADERA("008"), + ARENA_Y_PIEDRA("009"), + RESIDUOS_SUBPRODUCTOS("010"), + BIENES_GRAVADOS_CON_IGV_RENUNCIANDO_EXONERACION("011"), + INTERMEDIACION_LABORAL("012"), + ANIMALES_VIVOS("013"), + CARNES_Y_DESPOJOS("014"), + ABONOS_CUEROS_PIELES("015"), + ACEITE_DE_PESCADO("016"), + HARINA_POLVO_PELLETS_PESCADO("017"), + EMBARCACIONES_PESQUERAS("018"), + LECHE("019"), + ARROZ_PILADO("020"), + MINERALES_METALICOS_NO_METALICOS("021"), + BIENES_EXONERADOS_IGV("022"), + ORO_DEMAS_MINERALES("023"), + MINERALES_NO_METALICOS("024"), + ORO_AMALGAMA("025"), + PLOMO("026"), + PIMIENTO_PIQUILLO("027"), + ESPARRAGOS("028"), + ZINC("029"), + JUREL_Y_CABALLA("030"), + PAPA("031"), + + // ── Servicios (Anexo 3) ─────────────────────────────────────── + SERVICIOS_TRANSPORTE_BIENES_VIA_TERRESTRE("012"), + SERVICIOS_TRANSPORTE_PASAJEROS_VIA_TERRESTRE("020"), + ARRENDAMIENTO_BIENES("014"), + MANTENIMIENTO_REPARACION("017"), + MOVIMIENTO_DE_CARGA("019"), + OTROS_SERVICIOS_EMPRESARIALES("022"), + FABRICACION_ENCARGO("023"), + SERVICIO_TRANSPORTE_PERSONAS("027"), + CONTRATOS_CONSTRUCCION("037"), + DEMAS_SERVICIOS_GRAVADOS_IGV("012"); private final String code; diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog8.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog8.java index 7e517368..6f830c72 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog8.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/catalogs/Catalog8.java @@ -1,9 +1,22 @@ package io.github.project.openubl.xbuilder.content.catalogs; +/** + * Catálogo 08: Sistema de cálculo del ISC (Impuesto Selectivo al Consumo). + *

    + * Fuente: SUNAT – Guía XML Factura 2.1, Catálogo 08. Usado en {@code cac:TaxSubtotal/cac:TaxCategory/cbc:TierRange}. + *

    + *

    + * Nota: Los códigos "02" y "03" corresponden a sistemas distintos del ISC según la normativa. El código correcto para + * "Sistema de precios de venta al público" es "03" (corregido de la versión anterior que usaba "02" erróneamente). + *

    + */ public enum Catalog8 implements Catalog { + /** Sistema al valor (porcentaje sobre valor de venta). */ SISTEMA_AL_VALOR("01"), + /** Aplicación al monto fijo (monto fijo por unidad). */ APLICACION_AL_MONTO_FIJO("02"), - SISTEMA_DE_PRECIOS_DE_VENTA_AL_PUBLICO("02"); + /** Sistema de precios de venta al público (ISC sobre PVP). */ + SISTEMA_DE_PRECIOS_DE_VENTA_AL_PUBLICO("03"); private final String code; diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/common/Firmante.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/common/Firmante.java index 5e3f3e9f..1c4cbcac 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/common/Firmante.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/common/Firmante.java @@ -28,4 +28,12 @@ public class Firmante { */ @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private String razonSocial; + + /** + * URI de referencia para la firma digital (cbc:URI dentro de cac:DigitalSignatureAttachment). Si no se proporciona, + * se usa el valor por defecto {@code #PROJECT-OPENUBL-SIGN}. + */ + @Schema(description = "URI de referencia para la firma digital", defaultValue = "#PROJECT-OPENUBL-SIGN") + @Builder.Default + private String signatureUri = "#PROJECT-OPENUBL-SIGN"; } diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Detraccion.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Detraccion.java index cc1a29e6..7c3a63f0 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Detraccion.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Detraccion.java @@ -9,9 +9,20 @@ import java.math.BigDecimal; /** - * Detracción asociada a un Invoice + * Detracción asociada a una factura electrónica. + *

    + * Obligatoria cuando {@code tipoOperacion} = "1001" (Catálogo 51). Se renderiza en el XML como: + *

      + *
    • {@code cac:PaymentMeans} con {@code PaymentMeansCode} = Catálogo 59
    • + *
    • {@code cac:PaymentTerms} con monto, porcentaje y código de detracción (Catálogo 54)
    • + *
    + *

    + * Regla SUNAT: El monto de detracción se calcula como {@code porcentaje × importeConImpuestos} y se auto-calcula + * por el enricher si no se especifica explícitamente. + *

    * - * @author Carlos Feria + * @see io.github.project.openubl.xbuilder.content.catalogs.Catalog54 + * @see io.github.project.openubl.xbuilder.content.catalogs.Catalog59 */ @Data @Builder diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/DocumentoVentaDetalle.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/DocumentoVentaDetalle.java index b95cce8a..0005e6d1 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/DocumentoVentaDetalle.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/DocumentoVentaDetalle.java @@ -5,70 +5,157 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.Singular; import java.math.BigDecimal; - +import java.util.List; + +/** + * Línea de detalle de un documento de venta (factura, boleta, nota de crédito/débito). + *

    + * Cada instancia representa una línea {@code cac:InvoiceLine} / {@code cac:CreditNoteLine} / {@code cac:DebitNoteLine} + * en el XML UBL 2.1. + *

    + *

    + * Campos calculados automáticamente por el enricher: {@code igv}, {@code igvBaseImponible}, {@code isc}, + * {@code iscBaseImponible}, {@code icb}, {@code totalImpuestos}, {@code precioReferencia}, + * {@code precioReferenciaTipo}. + *

    + * + * @see io.github.project.openubl.xbuilder.content.catalogs.Catalog7 + * @see io.github.project.openubl.xbuilder.content.catalogs.Catalog8 + * @see io.github.project.openubl.xbuilder.content.catalogs.Catalog16 + */ @Data @Builder @NoArgsConstructor @AllArgsConstructor public class DocumentoVentaDetalle { - @Schema(description = "Descripcion del bien o servicio", requiredMode = Schema.RequiredMode.REQUIRED) + // ── Descripción e identificación ────────────────────────────── + + /** Descripción del bien o servicio ({@code cbc:Description}). */ + @Schema(description = "Descripción del bien o servicio", requiredMode = Schema.RequiredMode.REQUIRED) private String descripcion; + /** + * Código interno del producto del vendedor ({@code cac:SellersItemIdentification/cbc:ID}). Opcional, pero + * recomendado para trazabilidad. + */ + @Schema(description = "Código interno del vendedor") + private String codigoProducto; + + /** + * Código de producto SUNAT / UNSPSC ({@code cac:CommodityClassification/cbc:ItemClassificationCode}). Obligatorio + * para: exportaciones, detracciones y cuando RS 133-2019/SUNAT lo exija según cronograma. + */ + @Schema(description = "Código UNSPSC (Catálogo 25 SUNAT)") + private String codigoProductoSunat; + + /** + * Código de producto estándar GS1 – GTIN/EAN ({@code cac:StandardItemIdentification/cbc:ID}). Opcional. + */ + @Schema(description = "Código GS1/GTIN/EAN del producto") + private String codigoProductoGS1; + + // ── Cantidad y medida ───────────────────────────────────────── + + /** Unidad de medida (Catálogo 03 SUNAT). Default: "NIU" (unidad). */ private String unidadMedida; + /** Cantidad del bien o servicio. */ @Schema(requiredMode = Schema.RequiredMode.REQUIRED, minimum = "0", exclusiveMinimum = true) private BigDecimal cantidad; + // ── Precios ─────────────────────────────────────────────────── + + /** Precio unitario sin incluir impuestos ({@code cac:Price/cbc:PriceAmount}). */ @Schema(description = "Precio sin incluir impuestos", minimum = "0") private BigDecimal precio; + /** Si {@code true}, el campo {@code precio} ya incluye IGV y se recalculará internamente. */ @Schema(description = "Precio incluyendo impuestos") private boolean precioConImpuestos; + /** Precio de referencia unitario ({@code cac:PricingReference/cac:AlternativeConditionPrice}). Calculado. */ @Schema(minimum = "0") private BigDecimal precioReferencia; - @Schema(description = "Catalog 16") + /** Tipo de precio de referencia (Catálogo 16). Calculado. */ + @Schema(description = "Catálogo 16") private String precioReferenciaTipo; - // Impuestos + // ── IGV ─────────────────────────────────────────────────────── + + /** Tasa de IGV. Ejemplo: 0.18. Heredada del documento padre si no se especifica. */ @Schema(description = "Ejemplo: 0.18", minimum = "0", maximum = "1") private BigDecimal tasaIgv; + /** Monto total de IGV de esta línea. Calculado. */ @Schema(description = "Monto total de IGV", minimum = "0") private BigDecimal igv; + /** Base imponible del IGV. Calculado. */ @Schema(minimum = "0") private BigDecimal igvBaseImponible; - @Schema(description = "Catalogo 07") + /** + * Tipo de afectación al IGV (Catálogo 07). Default: "10" (gravado – operación onerosa). + */ + @Schema(description = "Catálogo 07") private String igvTipo; + // ── ICBPER (Impuesto al Consumo de Bolsas de Plástico) ─────── + + /** Tasa del ICBPER por unidad. Ejemplo: 0.50 (PEN por bolsa). */ @Schema(minimum = "0") private BigDecimal tasaIcb; + /** Monto total del ICBPER. Calculado: cantidad × tasaIcb. */ @Schema(minimum = "0") private BigDecimal icb; - @Schema(description = "'true' si ICB is aplicado a este bien o servicio") + /** {@code true} si el ICBPER aplica a esta línea (bolsas de plástico). */ + @Schema(description = "'true' si ICB es aplicado a este bien o servicio") private boolean icbAplica; + // ── ISC (Impuesto Selectivo al Consumo) ────────────────────── + + /** Tasa del ISC. Ejemplo: 0.17. */ @Schema(description = "Ejemplo: 0.17", minimum = "0", maximum = "1") private BigDecimal tasaIsc; + /** Monto total del ISC. Calculado. */ @Schema(description = "Monto total de ISC", minimum = "0") private BigDecimal isc; + /** Base imponible del ISC. Calculado. */ @Schema(minimum = "0") private BigDecimal iscBaseImponible; - @Schema(description = "Catalogo 08") + /** Sistema de cálculo del ISC (Catálogo 08). */ + @Schema(description = "Catálogo 08") private String iscTipo; - // Totales + // ── Descuentos/cargos por línea ────────────────────────────── + + /** + * Descuentos aplicados a esta línea ({@code cac:AllowanceCharge} con {@code ChargeIndicator=false}). Catálogo 53 – + * código "00" (descuento que afecta base imponible) o "01" (descuento que no afecta). + */ + @Singular + private List descuentos; + + /** + * Cargos aplicados a esta línea ({@code cac:AllowanceCharge} con {@code ChargeIndicator=true}). Catálogo 53 – + * código "47" (cargo que afecta base imponible) o "48" (otros cargos). + */ + @Singular + private List cargos; + + // ── Totales ────────────────────────────────────────────────── + + /** Total de impuestos de la línea (IGV + ISC + ICBPER). Calculado. */ @Schema(minimum = "0") private BigDecimal totalImpuestos; } diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Invoice.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Invoice.java index 8160f6d5..7cf196be 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Invoice.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Invoice.java @@ -12,6 +12,24 @@ import java.time.LocalDate; import java.util.List; +/** + * Modelo de Factura Electrónica (01) y Boleta de Venta Electrónica (03). + *

    + * Ambos documentos comparten la misma estructura UBL 2.1 ({@code Invoice}). La diferencia normativa está en: + *

      + *
    • Serie: Factura = Fxxx, Boleta = Bxxx
    • + *
    • Tipo comprobante (Catálogo 01): Factura = "01", Boleta = "03"
    • + *
    • Receptor: Factura requiere RUC (6). Boleta acepta DNI (1), CE (4), etc.
    • + *
    • Detracción: Solo aplica a facturas
    • + *
    • Resumen diario: Las boletas se informan vía ResumenDiario; las facturas se envían individualmente
    • + *
    + *

    + * El campo {@code tipoComprobante} se deduce automáticamente de la serie si no se especifica. + *

    + * + * @see io.github.project.openubl.xbuilder.content.catalogs.Catalog1_Invoice + * @see io.github.project.openubl.xbuilder.content.catalogs.Catalog51 + */ @Data @SuperBuilder @NoArgsConstructor diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/InvoiceValidator.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/InvoiceValidator.java new file mode 100644 index 00000000..c7d52de5 --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/InvoiceValidator.java @@ -0,0 +1,211 @@ +package io.github.project.openubl.xbuilder.content.models.standard.general; + +import io.github.project.openubl.xbuilder.content.catalogs.Catalog; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog1_Invoice; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog51; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog7; +import io.github.project.openubl.xbuilder.content.models.common.Cliente; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationMessage; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationResult; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +/** + * Validador de reglas de negocio para Factura Electrónica (01) y Boleta de Venta Electrónica (03). + *

    + * Implementa validaciones previas al renderizado XML según: + *

      + *
    • Guía XML Factura 2.1 – SUNAT
    • + *
    • Guía XML Boleta 2.1 – SUNAT
    • + *
    • Reglas de Validación CPE (versión vigente)
    • + *
    • RS 007-99/SUNAT – Reglamento de Comprobantes de Pago
    • + *
    + *

    + * IMPORTANTE: Este validador NO reemplaza la validación de SUNAT (XSD/XSL). Es una capa de validación temprana + * para detectar errores comunes antes del firmado y envío. + *

    + * + * @since 5.3.0 + * @see Invoice + * @see SalesDocumentValidator + */ +public final class InvoiceValidator { + + private InvoiceValidator() { + } + + /** + * Valida un Invoice y retorna solo los mensajes de error. + * + * @param invoice el Invoice a validar + * @return lista de errores (vacía si es válido) + */ + public static List validate(Invoice invoice) { + return validateDetailed(invoice).getErrors(); + } + + /** + * Valida un Invoice y retorna un {@link ValidationResult} con errores y advertencias. + * + * @param invoice el Invoice a validar + * @return resultado detallado con severidad + */ + public static ValidationResult validateDetailed(Invoice invoice) { + List messages = new ArrayList<>(); + + // ── Campos básicos ─────────────────────────────────────── + SalesDocumentValidator.validateBasicFields(invoice, messages); + + // ── Tipo de comprobante ────────────────────────────────── + String tipo = invoice.getTipoComprobante(); + boolean isFactura = "01".equals(tipo); + boolean isBoleta = "03".equals(tipo); + + if (tipo != null && !isFactura && !isBoleta) { + messages.add(ValidationMessage.error( + "tipoComprobante debe ser '01' (Factura) o '03' (Boleta), valor: " + tipo)); + } + + // ── Serie coherente con tipo ───────────────────────────── + String serie = invoice.getSerie(); + if (serie != null && tipo != null) { + if (isFactura && !serie.toUpperCase().startsWith("F")) { + messages.add(ValidationMessage.error( + "Factura (01) debe tener serie que empiece con 'F', valor: " + serie)); + } + if (isBoleta && !serie.toUpperCase().startsWith("B")) { + messages.add(ValidationMessage.error( + "Boleta (03) debe tener serie que empiece con 'B', valor: " + serie)); + } + } + + // ── Tipo de operación (Catálogo 51) ────────────────────── + String tipoOp = invoice.getTipoOperacion(); + if (tipoOp != null && Catalog.valueOfCode(Catalog51.class, tipoOp).isEmpty()) { + messages.add(ValidationMessage.warning( + "tipoOperacion '" + tipoOp + "' no encontrado en Catálogo 51")); + } + + // ── Cliente/receptor ───────────────────────────────────── + validateCliente(invoice.getCliente(), isFactura, isBoleta, messages); + + // ── Detracción solo en facturas ────────────────────────── + if (invoice.getDetraccion() != null && isBoleta) { + messages.add(ValidationMessage.error( + "La detracción solo aplica a Facturas (01), no a Boletas (03)")); + } + + // ── Detracción requiere tipoOperacion 1001 ────────────── + if (invoice.getDetraccion() != null) { + validateDetraccion(invoice.getDetraccion(), tipoOp, messages); + } + + // ── Percepción requiere tipoOperacion 2001 ────────────── + if (invoice.getPercepcion() != null) { + validatePercepcion(invoice.getPercepcion(), tipoOp, messages); + } + + // ── Detalles ───────────────────────────────────────────── + SalesDocumentValidator.validateDetalles(invoice.getDetalles(), messages); + + // ── Exportación requiere código producto SUNAT ─────────── + if (tipoOp != null && tipoOp.startsWith("02")) { + validateExportacionItems(invoice.getDetalles(), messages); + } + + return new ValidationResult(messages); + } + + // ── Validaciones de cliente ────────────────────────────────── + + private static void validateCliente(Cliente cliente, boolean isFactura, boolean isBoleta, + List messages) { + if (cliente == null) { + messages.add(ValidationMessage.error("El cliente/receptor es requerido")); + return; + } + if (isBlank(cliente.getNombre())) { + messages.add(ValidationMessage.error("El nombre del cliente es requerido")); + } + + String tipoDoc = cliente.getTipoDocumentoIdentidad(); + String numDoc = cliente.getNumeroDocumentoIdentidad(); + + if (isFactura) { + // Factura: receptor debe tener RUC (tipo "6") o DNI para montos <= 700 PEN + if (isBlank(tipoDoc) || isBlank(numDoc)) { + messages.add(ValidationMessage.error( + "Factura: el tipo y número de documento del receptor son obligatorios")); + } + if ("6".equals(tipoDoc) && (numDoc == null || numDoc.length() != 11)) { + messages.add(ValidationMessage.error( + "Factura: si tipoDocumentoIdentidad='6' (RUC), el número debe tener 11 dígitos")); + } + } + + if (isBoleta) { + // Boleta: para montos > 700 PEN se requiere documento del receptor + // (validación del monto se hace en el enricher/post-enricher) + if (isBlank(tipoDoc) && isBlank(numDoc)) { + messages.add(ValidationMessage.warning( + "Boleta: se recomienda consignar documento de identidad del receptor")); + } + } + } + + // ── Validaciones de detracción ────────────────────────────── + + private static void validateDetraccion(Detraccion detraccion, String tipoOp, + List messages) { + if (tipoOp != null && !tipoOp.startsWith("10")) { + messages.add(ValidationMessage.error( + "Detracción requiere tipoOperacion 1001-1004 (Catálogo 51), valor: " + tipoOp)); + } + if (isBlank(detraccion.getMedioDePago())) { + messages.add(ValidationMessage.error("Detracción: medioDePago es requerido (Catálogo 59)")); + } + if (isBlank(detraccion.getCuentaBancaria())) { + messages.add(ValidationMessage.error("Detracción: cuentaBancaria es requerida")); + } + if (isBlank(detraccion.getTipoBienDetraido())) { + messages.add(ValidationMessage.error("Detracción: tipoBienDetraido es requerido (Catálogo 54)")); + } + if (detraccion.getPorcentaje() == null || detraccion.getPorcentaje().compareTo(BigDecimal.ZERO) <= 0) { + messages.add(ValidationMessage.error("Detracción: porcentaje debe ser > 0")); + } + } + + // ── Validaciones de percepción ────────────────────────────── + + private static void validatePercepcion(Percepcion percepcion, String tipoOp, + List messages) { + if (!"2001".equals(tipoOp)) { + messages.add(ValidationMessage.error( + "Percepción requiere tipoOperacion='2001' (Catálogo 51), valor: " + tipoOp)); + } + if (isBlank(percepcion.getTipo())) { + messages.add(ValidationMessage.error("Percepción: tipo es requerido (Catálogo 53)")); + } + } + + // ── Exportación ───────────────────────────────────────────── + + private static void validateExportacionItems(List detalles, + List messages) { + if (detalles == null) + return; + for (int i = 0; i < detalles.size(); i++) { + DocumentoVentaDetalle d = detalles.get(i); + if (isBlank(d.getCodigoProductoSunat())) { + messages.add(ValidationMessage.warning( + "Línea " + (i + 1) + ": código producto SUNAT (UNSPSC) recomendado para exportación")); + } + } + } + + private static boolean isBlank(String value) { + return value == null || value.isBlank(); + } +} diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Note.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Note.java index 817e0f03..d6e57007 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Note.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Note.java @@ -7,9 +7,18 @@ import lombok.experimental.SuperBuilder; /** - * Clase base para CreditNote y DebitNOte. + * Clase base abstracta para Nota de Crédito ({@link CreditNote}) y Nota de Débito ({@link DebitNote}). + *

    + * Ambas notas comparten la misma estructura: referencia al comprobante afectado, motivo de emisión y sustento + * descriptivo. Las diferencias normativas son: + *

      + *
    • Nota de Crédito: Catálogo 09 para {@code tipoNota}. Anula total o parcialmente una factura/boleta.
    • + *
    • Nota de Débito: Catálogo 10 para {@code tipoNota}. Incrementa el importe del documento afectado.
    • + *
    + *

    * - * @author Carlos Feria + * @see io.github.project.openubl.xbuilder.content.catalogs.Catalog9 + * @see io.github.project.openubl.xbuilder.content.catalogs.Catalog10 */ @Data @SuperBuilder @@ -28,8 +37,7 @@ public abstract class Note extends SalesDocument { private String tipoNota; /** - * Serie y número del comprobante al que le aplica la nota de crédito/débito. - * Ejemplo: F001-1 + * Serie y número del comprobante al que le aplica la nota de crédito/débito. Ejemplo: F001-1 */ @Schema(requiredMode = Schema.RequiredMode.REQUIRED) private String comprobanteAfectadoSerieNumero; diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/NoteValidator.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/NoteValidator.java new file mode 100644 index 00000000..8e19e2bc --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/NoteValidator.java @@ -0,0 +1,106 @@ +package io.github.project.openubl.xbuilder.content.models.standard.general; + +import io.github.project.openubl.xbuilder.content.catalogs.Catalog; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog9; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog10; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationMessage; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationResult; + +import java.util.ArrayList; +import java.util.List; + +/** + * Validador de reglas de negocio para Notas de Crédito y Notas de Débito. + *

    + * Implementa validaciones previas al renderizado según: + *

      + *
    • Guía XML Nota de Crédito 2.1 – SUNAT
    • + *
    • Guía XML Nota de Débito 2.1 – SUNAT
    • + *
    • Reglas de Validación CPE (versión vigente)
    • + *
    + * + * @since 5.3.0 + * @see CreditNote + * @see DebitNote + */ +public final class NoteValidator { + + private NoteValidator() { + } + + /** + * Valida una CreditNote y retorna solo los mensajes de error. + */ + public static List validate(CreditNote note) { + return validateCreditNoteDetailed(note).getErrors(); + } + + /** + * Valida una DebitNote y retorna solo los mensajes de error. + */ + public static List validate(DebitNote note) { + return validateDebitNoteDetailed(note).getErrors(); + } + + /** + * Valida una CreditNote con resultado detallado (errores + advertencias). + */ + public static ValidationResult validateCreditNoteDetailed(CreditNote note) { + List messages = new ArrayList<>(); + validateCommon(note, messages); + + // tipoNota debe ser del Catálogo 09 + if (note.getTipoNota() != null && Catalog.valueOfCode(Catalog9.class, note.getTipoNota()).isEmpty()) { + messages.add(ValidationMessage.error( + "tipoNota '" + note.getTipoNota() + "' no encontrado en Catálogo 09 (Nota de Crédito)")); + } + + return new ValidationResult(messages); + } + + /** + * Valida una DebitNote con resultado detallado (errores + advertencias). + */ + public static ValidationResult validateDebitNoteDetailed(DebitNote note) { + List messages = new ArrayList<>(); + validateCommon(note, messages); + + // tipoNota debe ser del Catálogo 10 + if (note.getTipoNota() != null && Catalog.valueOfCode(Catalog10.class, note.getTipoNota()).isEmpty()) { + messages.add(ValidationMessage.error( + "tipoNota '" + note.getTipoNota() + "' no encontrado en Catálogo 10 (Nota de Débito)")); + } + + return new ValidationResult(messages); + } + + private static void validateCommon(Note note, List messages) { + SalesDocumentValidator.validateBasicFields(note, messages); + + if (isBlank(note.getComprobanteAfectadoSerieNumero())) { + messages.add(ValidationMessage.error( + "comprobanteAfectadoSerieNumero es requerido (serie-numero del documento afectado)")); + } + + if (isBlank(note.getComprobanteAfectadoTipo())) { + messages.add(ValidationMessage.error( + "comprobanteAfectadoTipo es requerido (Catálogo 01: tipo del documento afectado)")); + } + + if (isBlank(note.getTipoNota())) { + messages.add(ValidationMessage.error( + "tipoNota es requerido (Catálogo 09 para NC, Catálogo 10 para ND)")); + } + + if (isBlank(note.getSustentoDescripcion())) { + messages.add(ValidationMessage.error( + "sustentoDescripcion es requerido (motivo de la nota)")); + } + + SalesDocumentValidator.validateDetalles(note.getDetalles(), messages); + } + + private static boolean isBlank(String value) { + return value == null || value.isBlank(); + } +} diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Percepcion.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Percepcion.java index 0790e28a..f023a188 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Percepcion.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/Percepcion.java @@ -8,6 +8,23 @@ import java.math.BigDecimal; +/** + * Percepción asociada a una factura electrónica. + *

    + * Obligatoria cuando {@code tipoOperacion} = "2001" (Catálogo 51). El {@code tipo} corresponde al Catálogo 53 (códigos + * de percepción: "51", "52", "53"). + *

    + *

    + * Campos auto-calculados por el enricher: + *

      + *
    • {@code montoBase} = importeSinImpuestos del documento
    • + *
    • {@code monto} = montoBase × porcentaje
    • + *
    • {@code montoTotal} = montoBase + monto
    • + *
    + *

    + * + * @see io.github.project.openubl.xbuilder.content.catalogs.Catalog53 + */ @Data @Builder @NoArgsConstructor diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/SalesDocument.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/SalesDocument.java index 3d2d6581..ed53f81a 100644 --- a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/SalesDocument.java +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/SalesDocument.java @@ -2,6 +2,7 @@ import io.github.project.openubl.xbuilder.content.models.common.Cliente; import io.github.project.openubl.xbuilder.content.models.common.Document; +import io.github.project.openubl.xbuilder.content.models.common.TipoCambio; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -100,6 +101,13 @@ public abstract class SalesDocument extends Document { @ArraySchema private List documentosRelacionados; + /** + * Tipo de cambio aplicable cuando la moneda es distinta a PEN. Obligatorio si {@code moneda} ≠ "PEN" (Regla de + * validación SUNAT). Fuente: Guía XML Factura 2.1 – "PaymentExchangeRate". + */ + @Schema(description = "Tipo de cambio cuando moneda ≠ PEN") + private TipoCambio tipoCambio; + /** * Cargos globales del documento */ diff --git a/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/SalesDocumentValidator.java b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/SalesDocumentValidator.java new file mode 100644 index 00000000..7e96b64d --- /dev/null +++ b/xbuilder/core/src/main/java/io/github/project/openubl/xbuilder/content/models/standard/general/SalesDocumentValidator.java @@ -0,0 +1,84 @@ +package io.github.project.openubl.xbuilder.content.models.standard.general; + +import io.github.project.openubl.xbuilder.content.catalogs.Catalog; +import io.github.project.openubl.xbuilder.content.catalogs.Catalog7; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationMessage; + +import java.math.BigDecimal; +import java.util.List; + +/** + * Validaciones compartidas entre Invoice, CreditNote y DebitNote. + *

    + * Valida campos comunes de {@link SalesDocument} y sus líneas de detalle. + *

    + * + * @since 5.3.0 + */ +public final class SalesDocumentValidator { + + private SalesDocumentValidator() { + } + + /** + * Valida campos básicos del documento de venta: serie, número, proveedor. + */ + static void validateBasicFields(SalesDocument doc, List messages) { + if (doc.getSerie() == null || doc.getSerie().isBlank()) { + messages.add(ValidationMessage.error("La serie es requerida")); + } else if (doc.getSerie().length() != 4) { + messages.add(ValidationMessage.error( + "La serie debe tener 4 caracteres, valor: '" + doc.getSerie() + "'")); + } + + if (doc.getNumero() == null) { + messages.add(ValidationMessage.error("El número es requerido")); + } else if (doc.getNumero() < 1 || doc.getNumero() > 99999999) { + messages.add(ValidationMessage.error( + "El número debe estar entre 1 y 99999999, valor: " + doc.getNumero())); + } + + if (doc.getProveedor() == null) { + messages.add(ValidationMessage.error("El proveedor (emisor) es requerido")); + } else { + if (doc.getProveedor().getRuc() == null || doc.getProveedor().getRuc().length() != 11) { + messages.add(ValidationMessage.error("El RUC del proveedor debe tener 11 dígitos")); + } + if (doc.getProveedor().getRazonSocial() == null || doc.getProveedor().getRazonSocial().isBlank()) { + messages.add(ValidationMessage.error("La razón social del proveedor es requerida")); + } + } + } + + /** + * Valida las líneas de detalle del documento. + */ + static void validateDetalles(List detalles, List messages) { + if (detalles == null || detalles.isEmpty()) { + messages.add(ValidationMessage.error("El documento debe tener al menos una línea de detalle")); + return; + } + + for (int i = 0; i < detalles.size(); i++) { + DocumentoVentaDetalle d = detalles.get(i); + String prefix = "Línea " + (i + 1) + ": "; + + if (d.getDescripcion() == null || d.getDescripcion().isBlank()) { + messages.add(ValidationMessage.error(prefix + "descripción es requerida")); + } + if (d.getCantidad() == null || d.getCantidad().compareTo(BigDecimal.ZERO) <= 0) { + messages.add(ValidationMessage.error(prefix + "cantidad debe ser > 0")); + } + if (d.getPrecio() == null && !d.isPrecioConImpuestos()) { + messages.add(ValidationMessage.error(prefix + "precio es requerido")); + } + + // Validar igvTipo si se especifica + String igvTipo = d.getIgvTipo(); + if (igvTipo != null && Catalog.valueOfCode(Catalog7.class, igvTipo).isEmpty()) { + messages.add(ValidationMessage.warning( + prefix + "igvTipo '" + igvTipo + "' no encontrado en Catálogo 07")); + } + } + } +} diff --git a/xbuilder/core/src/main/resources/templates/Renderer/despatchAdvice.xml b/xbuilder/core/src/main/resources/templates/Renderer/despatchAdvice.xml index 75612bb3..b499d0e9 100644 --- a/xbuilder/core/src/main/resources/templates/Renderer/despatchAdvice.xml +++ b/xbuilder/core/src/main/resources/templates/Renderer/despatchAdvice.xml @@ -60,7 +60,6 @@ {/each} {#include ubl/common/signature.xml firmante=this.firmante /} - {remitente.ruc} {remitente.ruc} @@ -266,9 +265,11 @@ {#if it.descripcion} {/if} + {#if it.codigo} {it.codigo} + {/if} {#if it.codigoSunat} {it.codigoSunat} diff --git a/xbuilder/core/src/main/resources/templates/ubl/common/signature.xml b/xbuilder/core/src/main/resources/templates/ubl/common/signature.xml index 18dc6461..b73a1577 100644 --- a/xbuilder/core/src/main/resources/templates/ubl/common/signature.xml +++ b/xbuilder/core/src/main/resources/templates/ubl/common/signature.xml @@ -10,7 +10,7 @@ - #PROJECT-OPENUBL-SIGN + {firmante.signatureUri ?: '#PROJECT-OPENUBL-SIGN'} diff --git a/xbuilder/core/src/main/resources/templates/ubl/standard/include/document-line.xml b/xbuilder/core/src/main/resources/templates/ubl/standard/include/document-line.xml index 38abf80d..9346af02 100644 --- a/xbuilder/core/src/main/resources/templates/ubl/standard/include/document-line.xml +++ b/xbuilder/core/src/main/resources/templates/ubl/standard/include/document-line.xml @@ -57,6 +57,21 @@ + {#if item.codigoProducto} + + {item.codigoProducto} + + {/if} + {#if item.codigoProductoGS1} + + {item.codigoProductoGS1} + + {/if} + {#if item.codigoProductoSunat} + + {item.codigoProductoSunat} + + {/if} {item.precio.scale(2)} diff --git a/xbuilder/core/src/test/java/io/github/project/openubl/xbuilder/content/models/standard/general/InvoiceValidatorTest.java b/xbuilder/core/src/test/java/io/github/project/openubl/xbuilder/content/models/standard/general/InvoiceValidatorTest.java new file mode 100644 index 00000000..8006fc46 --- /dev/null +++ b/xbuilder/core/src/test/java/io/github/project/openubl/xbuilder/content/models/standard/general/InvoiceValidatorTest.java @@ -0,0 +1,474 @@ +package io.github.project.openubl.xbuilder.content.models.standard.general; + +import io.github.project.openubl.xbuilder.content.catalogs.Catalog51; +import io.github.project.openubl.xbuilder.content.models.common.Cliente; +import io.github.project.openubl.xbuilder.content.models.common.Firmante; +import io.github.project.openubl.xbuilder.content.models.common.Proveedor; +import io.github.project.openubl.xbuilder.content.models.standard.guia.validation.ValidationResult; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests de validación para Factura Electrónica y Boleta de Venta Electrónica. + *

    + * Casos de prueba basados en: + *

      + *
    • Guía XML Factura 2.1 – SUNAT
    • + *
    • Guía XML Boleta 2.1 – SUNAT
    • + *
    • Reglas de Validación CPE vigentes
    • + *
    + */ +@DisplayName("InvoiceValidator") +class InvoiceValidatorTest { + + // ── Fixtures de datos reales ───────────────────────────────── + + private static Proveedor proveedorValido() { + return Proveedor.builder() + .ruc("20601234567") + .razonSocial("EMPRESA DE PRUEBA SAC") + .build(); + } + + private static Firmante firmanteValido() { + return Firmante.builder() + .ruc("20601234567") + .razonSocial("EMPRESA DE PRUEBA SAC") + .build(); + } + + private static Cliente clienteRuc() { + return Cliente.builder() + .nombre("ACME CORPORATION SAC") + .numeroDocumentoIdentidad("20100047218") + .tipoDocumentoIdentidad("6") + .build(); + } + + private static Cliente clienteDni() { + return Cliente.builder() + .nombre("JUAN PEREZ GARCIA") + .numeroDocumentoIdentidad("44556677") + .tipoDocumentoIdentidad("1") + .build(); + } + + private static DocumentoVentaDetalle lineaGravada() { + return DocumentoVentaDetalle.builder() + .descripcion("Laptop HP ProBook 450 G8") + .cantidad(BigDecimal.valueOf(2)) + .precio(BigDecimal.valueOf(3500.00)) + .codigoProducto("LAPTOP-HP-450") + .codigoProductoSunat("43211507") + .igvTipo("10") + .build(); + } + + private static DocumentoVentaDetalle lineaExonerada() { + return DocumentoVentaDetalle.builder() + .descripcion("Consulta médica general") + .cantidad(BigDecimal.ONE) + .precio(BigDecimal.valueOf(150.00)) + .igvTipo("20") + .build(); + } + + private static DocumentoVentaDetalle lineaGratuita() { + return DocumentoVentaDetalle.builder() + .descripcion("Muestra gratis - Producto promocional") + .cantidad(BigDecimal.ONE) + .precio(BigDecimal.ZERO) + .igvTipo("31") + .build(); + } + + private static DocumentoVentaDetalle lineaConIcbper() { + return DocumentoVentaDetalle.builder() + .descripcion("Bolsa plástica") + .cantidad(BigDecimal.valueOf(3)) + .precio(BigDecimal.valueOf(0.50)) + .igvTipo("10") + .icbAplica(true) + .build(); + } + + private static Invoice facturaBase() { + return Invoice.builder() + .serie("F001") + .numero(1) + .fechaEmision(LocalDate.now()) + .moneda("PEN") + .proveedor(proveedorValido()) + .firmante(firmanteValido()) + .cliente(clienteRuc()) + .tipoOperacion(Catalog51.VENTA_INTERNA.getCode()) + .detalle(lineaGravada()) + .build(); + } + + private static Invoice boletaBase() { + return Invoice.builder() + .serie("B001") + .numero(1) + .fechaEmision(LocalDate.now()) + .moneda("PEN") + .proveedor(proveedorValido()) + .firmante(firmanteValido()) + .cliente(clienteDni()) + .tipoOperacion(Catalog51.VENTA_INTERNA.getCode()) + .detalle(lineaGravada()) + .build(); + } + + // ══════════════════════════════════════════════════════════════ + // Factura Electrónica (01) — Casos válidos + // ══════════════════════════════════════════════════════════════ + + @Nested + @DisplayName("Factura válida - casos comunes") + class FacturaValidaTests { + + @Test + @DisplayName("Factura gravada estándar - venta interna con IGV") + void facturaGravadaEstandar() { + Invoice invoice = facturaBase(); + List errors = InvoiceValidator.validate(invoice); + assertTrue(errors.isEmpty(), "Factura gravada estándar debe ser válida: " + errors); + } + + @Test + @DisplayName("Factura con operación exonerada (IGV tipo 20)") + void facturaExonerada() { + Invoice invoice = Invoice.builder() + .serie("F001") + .numero(1) + .fechaEmision(LocalDate.now()) + .moneda("PEN") + .proveedor(proveedorValido()) + .firmante(firmanteValido()) + .cliente(clienteRuc()) + .tipoOperacion(Catalog51.VENTA_INTERNA.getCode()) + .detalle(lineaExonerada()) + .build(); + List errors = InvoiceValidator.validate(invoice); + assertTrue(errors.isEmpty(), "Factura exonerada debe ser válida: " + errors); + } + + @Test + @DisplayName("Factura con líneas mixtas (gravada + exonerada + ICBPER)") + void facturaMixta() { + Invoice invoice = Invoice.builder() + .serie("F001") + .numero(1) + .fechaEmision(LocalDate.now()) + .moneda("PEN") + .proveedor(proveedorValido()) + .firmante(firmanteValido()) + .cliente(clienteRuc()) + .tipoOperacion(Catalog51.VENTA_INTERNA.getCode()) + .detalles(List.of(lineaGravada(), lineaExonerada(), lineaConIcbper())) + .build(); + List errors = InvoiceValidator.validate(invoice); + assertTrue(errors.isEmpty(), "Factura mixta debe ser válida: " + errors); + } + + @Test + @DisplayName("Factura con detracción (tipo operación 1001)") + void facturaConDetraccion() { + Invoice invoice = Invoice.builder() + .serie("F001") + .numero(1) + .fechaEmision(LocalDate.now()) + .moneda("PEN") + .proveedor(proveedorValido()) + .firmante(firmanteValido()) + .cliente(clienteRuc()) + .tipoOperacion(Catalog51.OPERACION_SUJETA_A_DETRACCION.getCode()) + .detalle(lineaGravada()) + .detraccion(Detraccion.builder() + .medioDePago("001") + .cuentaBancaria("00-123-456789") + .tipoBienDetraido("004") + .porcentaje(BigDecimal.valueOf(0.08)) + .monto(BigDecimal.valueOf(500)) + .build()) + .build(); + List errors = InvoiceValidator.validate(invoice); + assertTrue(errors.isEmpty(), "Factura con detracción debe ser válida: " + errors); + } + + @Test + @DisplayName("Factura con percepción (tipo operación 2001)") + void facturaConPercepcion() { + Invoice invoice = Invoice.builder() + .serie("F001") + .numero(1) + .fechaEmision(LocalDate.now()) + .moneda("PEN") + .proveedor(proveedorValido()) + .firmante(firmanteValido()) + .cliente(clienteRuc()) + .tipoOperacion(Catalog51.OPERACION_SUJETA_A_PERCEPCION.getCode()) + .detalle(lineaGravada()) + .percepcion(Percepcion.builder() + .tipo("51") + .porcentaje(BigDecimal.valueOf(0.02)) + .build()) + .build(); + List errors = InvoiceValidator.validate(invoice); + assertTrue(errors.isEmpty(), "Factura con percepción debe ser válida: " + errors); + } + + @Test + @DisplayName("Factura al crédito con cuotas") + void facturaCredito() { + Invoice invoice = Invoice.builder() + .serie("F001") + .numero(1) + .fechaEmision(LocalDate.now()) + .moneda("PEN") + .proveedor(proveedorValido()) + .firmante(firmanteValido()) + .cliente(clienteRuc()) + .tipoOperacion(Catalog51.VENTA_INTERNA.getCode()) + .detalle(lineaGravada()) + .formaDePago(FormaDePago.builder() + .tipo("Credito") + .total(BigDecimal.valueOf(4130.00)) + .cuota(CuotaDePago.builder() + .importe(BigDecimal.valueOf(2065.00)) + .fechaPago(LocalDate.now().plusDays(30)) + .build()) + .cuota(CuotaDePago.builder() + .importe(BigDecimal.valueOf(2065.00)) + .fechaPago(LocalDate.now().plusDays(60)) + .build()) + .build()) + .build(); + List errors = InvoiceValidator.validate(invoice); + assertTrue(errors.isEmpty(), "Factura al crédito debe ser válida: " + errors); + } + + @Test + @DisplayName("Factura con transferencia gratuita (IGV tipo 31)") + void facturaGratuita() { + Invoice invoice = Invoice.builder() + .serie("F001") + .numero(1) + .fechaEmision(LocalDate.now()) + .moneda("PEN") + .proveedor(proveedorValido()) + .firmante(firmanteValido()) + .cliente(clienteRuc()) + .tipoOperacion(Catalog51.VENTA_INTERNA.getCode()) + .detalle(lineaGratuita()) + .build(); + List errors = InvoiceValidator.validate(invoice); + assertTrue(errors.isEmpty(), "Factura gratuita debe ser válida: " + errors); + } + } + + // ══════════════════════════════════════════════════════════════ + // Boleta de Venta (03) — Casos válidos + // ══════════════════════════════════════════════════════════════ + + @Nested + @DisplayName("Boleta válida - casos comunes") + class BoletaValidaTests { + + @Test + @DisplayName("Boleta estándar con DNI") + void boletaEstandarConDni() { + Invoice boleta = boletaBase(); + List errors = InvoiceValidator.validate(boleta); + assertTrue(errors.isEmpty(), "Boleta con DNI debe ser válida: " + errors); + } + + @Test + @DisplayName("Boleta sin documento de identidad (monto bajo)") + void boletaSinDocumento() { + Invoice boleta = Invoice.builder() + .serie("B001") + .numero(1) + .fechaEmision(LocalDate.now()) + .moneda("PEN") + .proveedor(proveedorValido()) + .firmante(firmanteValido()) + .cliente(Cliente.builder().nombre("CONSUMIDOR FINAL").build()) + .tipoOperacion(Catalog51.VENTA_INTERNA.getCode()) + .detalle(lineaGravada()) + .build(); + ValidationResult result = InvoiceValidator.validateDetailed(boleta); + assertTrue(result.getErrors().isEmpty(), + "Boleta sin documento para montos bajos no debe tener errores: " + result.getErrors()); + } + + @Test + @DisplayName("Boleta con ICBPER (bolsas de plástico)") + void boletaConIcbper() { + Invoice boleta = Invoice.builder() + .serie("B001") + .numero(1) + .fechaEmision(LocalDate.now()) + .moneda("PEN") + .proveedor(proveedorValido()) + .firmante(firmanteValido()) + .cliente(clienteDni()) + .tipoOperacion(Catalog51.VENTA_INTERNA.getCode()) + .detalles(List.of(lineaGravada(), lineaConIcbper())) + .build(); + List errors = InvoiceValidator.validate(boleta); + assertTrue(errors.isEmpty(), "Boleta con ICBPER debe ser válida: " + errors); + } + } + + // ══════════════════════════════════════════════════════════════ + // Casos inválidos — Errores de validación + // ══════════════════════════════════════════════════════════════ + + @Nested + @DisplayName("Casos inválidos") + class CasosInvalidosTests { + + @Test + @DisplayName("Serie incorrecta para factura (B en lugar de F)") + void serieIncorrectaFactura() { + Invoice invoice = Invoice.builder() + .serie("B001") + .numero(1) + .moneda("PEN") + .proveedor(proveedorValido()) + .cliente(clienteRuc()) + .tipoComprobante("01") + .tipoOperacion(Catalog51.VENTA_INTERNA.getCode()) + .detalle(lineaGravada()) + .build(); + List errors = InvoiceValidator.validate(invoice); + assertFalse(errors.isEmpty(), "Factura con serie B debe ser inválida"); + assertTrue(errors.stream().anyMatch(e -> e.contains("serie")), + "Debe mencionar error de serie"); + } + + @Test + @DisplayName("Factura sin cliente") + void facturaSinCliente() { + Invoice invoice = Invoice.builder() + .serie("F001") + .numero(1) + .moneda("PEN") + .proveedor(proveedorValido()) + .detalle(lineaGravada()) + .build(); + List errors = InvoiceValidator.validate(invoice); + assertTrue(errors.stream().anyMatch(e -> e.contains("cliente")), + "Debe requerir cliente"); + } + + @Test + @DisplayName("Factura sin detalles") + void facturaSinDetalles() { + Invoice invoice = Invoice.builder() + .serie("F001") + .numero(1) + .moneda("PEN") + .proveedor(proveedorValido()) + .cliente(clienteRuc()) + .build(); + List errors = InvoiceValidator.validate(invoice); + assertTrue(errors.stream().anyMatch(e -> e.contains("detalle")), + "Debe requerir al menos una línea"); + } + + @Test + @DisplayName("Detracción en boleta debe ser error") + void detraccionEnBoleta() { + Invoice boleta = Invoice.builder() + .serie("B001") + .numero(1) + .moneda("PEN") + .proveedor(proveedorValido()) + .cliente(clienteDni()) + .tipoComprobante("03") + .tipoOperacion("1001") + .detalle(lineaGravada()) + .detraccion(Detraccion.builder() + .medioDePago("001") + .cuentaBancaria("00-123-456789") + .tipoBienDetraido("004") + .porcentaje(BigDecimal.valueOf(0.08)) + .monto(BigDecimal.valueOf(100)) + .build()) + .build(); + List errors = InvoiceValidator.validate(boleta); + assertTrue( + errors.stream() + .anyMatch(e -> e.toLowerCase().contains("detracción") + || e.toLowerCase().contains("detraccion")), + "Detracción en boleta debe ser error"); + } + + @Test + @DisplayName("Detracción sin cuenta bancaria") + void detraccionSinCuenta() { + Invoice invoice = Invoice.builder() + .serie("F001") + .numero(1) + .moneda("PEN") + .proveedor(proveedorValido()) + .cliente(clienteRuc()) + .tipoOperacion("1001") + .detalle(lineaGravada()) + .detraccion(Detraccion.builder() + .medioDePago("001") + .tipoBienDetraido("004") + .porcentaje(BigDecimal.valueOf(0.08)) + .build()) + .build(); + List errors = InvoiceValidator.validate(invoice); + assertTrue(errors.stream().anyMatch(e -> e.contains("cuentaBancaria")), + "Debe requerir cuenta bancaria"); + } + + @Test + @DisplayName("Línea sin descripción") + void lineaSinDescripcion() { + Invoice invoice = Invoice.builder() + .serie("F001") + .numero(1) + .moneda("PEN") + .proveedor(proveedorValido()) + .cliente(clienteRuc()) + .detalle(DocumentoVentaDetalle.builder() + .cantidad(BigDecimal.ONE) + .precio(BigDecimal.TEN) + .build()) + .build(); + List errors = InvoiceValidator.validate(invoice); + assertTrue(errors.stream().anyMatch(e -> e.contains("descripción")), + "Debe requerir descripción en línea"); + } + + @Test + @DisplayName("RUC del proveedor inválido") + void rucProveedorInvalido() { + Invoice invoice = Invoice.builder() + .serie("F001") + .numero(1) + .moneda("PEN") + .proveedor(Proveedor.builder().ruc("12345").razonSocial("TEST").build()) + .cliente(clienteRuc()) + .detalle(lineaGravada()) + .build(); + List errors = InvoiceValidator.validate(invoice); + assertTrue(errors.stream().anyMatch(e -> e.contains("11 dígitos")), + "Debe validar longitud de RUC"); + } + } +} diff --git a/xbuilder/core/src/test/java/io/github/project/openubl/xbuilder/content/models/standard/general/NoteValidatorTest.java b/xbuilder/core/src/test/java/io/github/project/openubl/xbuilder/content/models/standard/general/NoteValidatorTest.java new file mode 100644 index 00000000..c2af7509 --- /dev/null +++ b/xbuilder/core/src/test/java/io/github/project/openubl/xbuilder/content/models/standard/general/NoteValidatorTest.java @@ -0,0 +1,199 @@ +package io.github.project.openubl.xbuilder.content.models.standard.general; + +import io.github.project.openubl.xbuilder.content.models.common.Cliente; +import io.github.project.openubl.xbuilder.content.models.common.Firmante; +import io.github.project.openubl.xbuilder.content.models.common.Proveedor; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests de validación para Notas de Crédito y Notas de Débito. + */ +@DisplayName("NoteValidator") +class NoteValidatorTest { + + private static Proveedor proveedor() { + return Proveedor.builder() + .ruc("20601234567") + .razonSocial("EMPRESA SAC") + .build(); + } + + private static Cliente cliente() { + return Cliente.builder() + .nombre("CLIENTE SAC") + .numeroDocumentoIdentidad("20100047218") + .tipoDocumentoIdentidad("6") + .build(); + } + + private static DocumentoVentaDetalle linea() { + return DocumentoVentaDetalle.builder() + .descripcion("Producto devuelto") + .cantidad(BigDecimal.ONE) + .precio(BigDecimal.valueOf(100)) + .build(); + } + + @Nested + @DisplayName("Nota de Crédito - casos válidos") + class CreditNoteValidTests { + + @Test + @DisplayName("NC estándar por anulación total") + void ncAnulacionTotal() { + CreditNote nc = CreditNote.builder() + .serie("F001") + .numero(1) + .fechaEmision(LocalDate.now()) + .moneda("PEN") + .proveedor(proveedor()) + .firmante(Firmante.builder().ruc("20601234567").razonSocial("EMPRESA SAC").build()) + .cliente(cliente()) + .tipoNota("01") + .comprobanteAfectadoSerieNumero("F001-100") + .comprobanteAfectadoTipo("01") + .sustentoDescripcion("Anulación de la operación") + .detalle(linea()) + .build(); + List errors = NoteValidator.validate(nc); + assertTrue(errors.isEmpty(), "NC válida: " + errors); + } + + @Test + @DisplayName("NC por corrección de nombre/razón social (motivo 10)") + void ncCorreccionNombre() { + CreditNote nc = CreditNote.builder() + .serie("B001") + .numero(1) + .fechaEmision(LocalDate.now()) + .moneda("PEN") + .proveedor(proveedor()) + .firmante(Firmante.builder().ruc("20601234567").razonSocial("EMPRESA SAC").build()) + .cliente(cliente()) + .tipoNota("10") + .comprobanteAfectadoSerieNumero("B001-50") + .comprobanteAfectadoTipo("03") + .sustentoDescripcion("Corrección de denominación del receptor") + .detalle(linea()) + .build(); + List errors = NoteValidator.validate(nc); + assertTrue(errors.isEmpty(), "NC motivo 10 válida: " + errors); + } + } + + @Nested + @DisplayName("Nota de Débito - casos válidos") + class DebitNoteValidTests { + + @Test + @DisplayName("ND por intereses por mora (motivo 01)") + void ndInteresesMora() { + DebitNote nd = DebitNote.builder() + .serie("F001") + .numero(1) + .fechaEmision(LocalDate.now()) + .moneda("PEN") + .proveedor(proveedor()) + .firmante(Firmante.builder().ruc("20601234567").razonSocial("EMPRESA SAC").build()) + .cliente(cliente()) + .tipoNota("01") + .comprobanteAfectadoSerieNumero("F001-100") + .comprobanteAfectadoTipo("01") + .sustentoDescripcion("Intereses por mora en pago") + .detalle(DocumentoVentaDetalle.builder() + .descripcion("Intereses por mora") + .cantidad(BigDecimal.ONE) + .precio(BigDecimal.valueOf(250)) + .build()) + .build(); + List errors = NoteValidator.validate(nd); + assertTrue(errors.isEmpty(), "ND válida: " + errors); + } + } + + @Nested + @DisplayName("Nota - casos inválidos") + class InvalidTests { + + @Test + @DisplayName("NC sin comprobante afectado") + void ncSinComprobanteAfectado() { + CreditNote nc = CreditNote.builder() + .serie("F001") + .numero(1) + .proveedor(proveedor()) + .cliente(cliente()) + .tipoNota("01") + .sustentoDescripcion("Anulación") + .detalle(linea()) + .build(); + List errors = NoteValidator.validate(nc); + assertTrue(errors.stream().anyMatch(e -> e.contains("comprobanteAfectadoSerieNumero")), + "Debe requerir comprobante afectado"); + } + + @Test + @DisplayName("NC sin sustento") + void ncSinSustento() { + CreditNote nc = CreditNote.builder() + .serie("F001") + .numero(1) + .proveedor(proveedor()) + .cliente(cliente()) + .tipoNota("01") + .comprobanteAfectadoSerieNumero("F001-1") + .comprobanteAfectadoTipo("01") + .detalle(linea()) + .build(); + List errors = NoteValidator.validate(nc); + assertTrue(errors.stream().anyMatch(e -> e.contains("sustento")), + "Debe requerir sustento descriptivo"); + } + + @Test + @DisplayName("NC con tipo nota inválido") + void ncTipoNotaInvalido() { + CreditNote nc = CreditNote.builder() + .serie("F001") + .numero(1) + .proveedor(proveedor()) + .cliente(cliente()) + .tipoNota("99") + .comprobanteAfectadoSerieNumero("F001-1") + .comprobanteAfectadoTipo("01") + .sustentoDescripcion("Test") + .detalle(linea()) + .build(); + List errors = NoteValidator.validate(nc); + assertTrue(errors.stream().anyMatch(e -> e.contains("Catálogo 09")), + "Debe validar tipo nota contra Catálogo 09"); + } + + @Test + @DisplayName("ND con tipo nota inválido") + void ndTipoNotaInvalido() { + DebitNote nd = DebitNote.builder() + .serie("F001") + .numero(1) + .proveedor(proveedor()) + .cliente(cliente()) + .tipoNota("99") + .comprobanteAfectadoSerieNumero("F001-1") + .comprobanteAfectadoTipo("01") + .sustentoDescripcion("Test") + .detalle(linea()) + .build(); + List errors = NoteValidator.validate(nd); + assertTrue(errors.stream().anyMatch(e -> e.contains("Catálogo 10")), + "Debe validar tipo nota contra Catálogo 10"); + } + } +} diff --git a/xbuilder/core/src/test/resources/e2e/renderer/creditnote/CreditNoteIscTest/isc_sistemaDePreciosDeVentalAlPublico.xml b/xbuilder/core/src/test/resources/e2e/renderer/creditnote/CreditNoteIscTest/isc_sistemaDePreciosDeVentalAlPublico.xml index 1215b9f5..507f233e 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/creditnote/CreditNoteIscTest/isc_sistemaDePreciosDeVentalAlPublico.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/creditnote/CreditNoteIscTest/isc_sistemaDePreciosDeVentalAlPublico.xml @@ -119,7 +119,7 @@ 20.00 10.00 - 02 + 03 2000 ISC diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceCarrierTest/carrierData.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceCarrierTest/carrierData.xml index 5bdcab82..bdb28c75 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceCarrierTest/carrierData.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceCarrierTest/carrierData.xml @@ -32,7 +32,6 @@ - 20123456789 20123456789 diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/exportacionSinDAM.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/exportacionSinDAM.xml index 7816e40e..a0e5210d 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/exportacionSinDAM.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/exportacionSinDAM.xml @@ -33,7 +33,6 @@ - 20500050005 20500050005 diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/importacionDAMTotal.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/importacionDAMTotal.xml index f61cdb24..f63ce4ae 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/importacionDAMTotal.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/importacionDAMTotal.xml @@ -41,7 +41,6 @@ - 20100010001 20100010001 diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/mercanciaExtranjera.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/mercanciaExtranjera.xml index 6a4a181c..bee5ab3e 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/mercanciaExtranjera.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComercioExteriorTest/mercanciaExtranjera.xml @@ -32,7 +32,6 @@ - 20600060006 20600060006 diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComplexTest/complexData.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComplexTest/complexData.xml index 4bfd1add..7cdeb9fb 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComplexTest/complexData.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceComplexTest/complexData.xml @@ -42,7 +42,6 @@ - 12345678912 12345678912 diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceTest/minData.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceTest/minData.xml index 13d36baf..a45e5686 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceTest/minData.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/DespatchAdviceTest/minData.xml @@ -32,7 +32,6 @@ - 12345678912 12345678912 diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/mercanciaExtranjeraMotivo19.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/mercanciaExtranjeraMotivo19.xml index 40c8490f..86b06a42 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/mercanciaExtranjeraMotivo19.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/mercanciaExtranjeraMotivo19.xml @@ -32,7 +32,6 @@ - 20500050005 20500050005 diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/multiplesConductores.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/multiplesConductores.xml index 85afa4c7..642a3e80 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/multiplesConductores.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/multiplesConductores.xml @@ -32,7 +32,6 @@ - 20100010001 20100010001 diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transportePrivadoBasico.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transportePrivadoBasico.xml index 7c908473..bb1bb8ae 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transportePrivadoBasico.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transportePrivadoBasico.xml @@ -32,7 +32,6 @@ - 20100010001 20100010001 diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transportePublico.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transportePublico.xml index 8fa0c8b9..0b7b93fb 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transportePublico.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transportePublico.xml @@ -32,7 +32,6 @@ - 20100010001 20100010001 diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transporteSubcontratado.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transporteSubcontratado.xml index adf1b78c..b2726483 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transporteSubcontratado.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/transporteSubcontratado.xml @@ -32,7 +32,6 @@ - 20100010001 20100010001 diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/vehiculoConCarreta.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/vehiculoConCarreta.xml index dfa18926..82065b79 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/vehiculoConCarreta.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/vehiculoConCarreta.xml @@ -32,7 +32,6 @@ - 20100010001 20100010001 diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/vehiculoM1L.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/vehiculoM1L.xml index d5bed493..23fd2e7d 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/vehiculoM1L.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRERemitenteCasuisticasTest/vehiculoM1L.xml @@ -32,7 +32,6 @@ - 20100010001 20100010001 diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaBasico.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaBasico.xml index 5c5ee363..8dc7b690 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaBasico.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaBasico.xml @@ -32,7 +32,6 @@ - 20300030003 20300030003 diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaComercioExterior.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaComercioExterior.xml index 157e2198..c524e65a 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaComercioExterior.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaComercioExterior.xml @@ -32,7 +32,6 @@ - 20700070007 20700070007 diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaConCarreta.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaConCarreta.xml index d8c01686..eff84f34 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaConCarreta.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaConCarreta.xml @@ -32,7 +32,6 @@ - 20300030003 20300030003 diff --git a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaMultiplesConductores.xml b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaMultiplesConductores.xml index 13a1d414..e3cdf72f 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaMultiplesConductores.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/despatchadvice/GRETransportistaCasuisticasTest/transportistaMultiplesConductores.xml @@ -32,7 +32,6 @@ - 20300030003 20300030003 diff --git a/xbuilder/core/src/test/resources/e2e/renderer/invoice/InvoiceIscTest/isc_sistemaDePreciosDeVentalAlPublico.xml b/xbuilder/core/src/test/resources/e2e/renderer/invoice/InvoiceIscTest/isc_sistemaDePreciosDeVentalAlPublico.xml index 12a84ca9..931a29ff 100644 --- a/xbuilder/core/src/test/resources/e2e/renderer/invoice/InvoiceIscTest/isc_sistemaDePreciosDeVentalAlPublico.xml +++ b/xbuilder/core/src/test/resources/e2e/renderer/invoice/InvoiceIscTest/isc_sistemaDePreciosDeVentalAlPublico.xml @@ -115,7 +115,7 @@ 20.00 10.00 - 02 + 03 2000 ISC diff --git a/xsender/core/src/main/java/io/github/project/openubl/xsender/camel/routes/RestSunatResponseProcessor.java b/xsender/core/src/main/java/io/github/project/openubl/xsender/camel/routes/RestSunatResponseProcessor.java index b37a4bb7..02e65d02 100644 --- a/xsender/core/src/main/java/io/github/project/openubl/xsender/camel/routes/RestSunatResponseProcessor.java +++ b/xsender/core/src/main/java/io/github/project/openubl/xsender/camel/routes/RestSunatResponseProcessor.java @@ -26,8 +26,7 @@ public void process(Exchange exchange) throws Exception { sunatResponse = SunatResponse.builder() .sunat(Sunat.builder() .ticket(responseDto.getNumTicket()) - .build() - ) + .build()) .build(); } else if (responseDto.getArcCdr() != null) { String cdrBase64Hex = responseDto.getArcCdr(); @@ -35,18 +34,22 @@ public void process(Exchange exchange) throws Exception { byte[] cdrBytes = Base64.getDecoder().decode(bytes); CdrReader cdrReader = new CdrReader(cdrBytes); - SunatResponse.builder() + sunatResponse = SunatResponse.builder() .status(cdrReader.getStatus()) .metadata(cdrReader.getMetadata()) .sunat(Sunat.builder() .cdr(cdrBytes) - .build() - ); + .build()) + .build(); } else if (responseDto.getCodRespuesta() != null) { int statusCode = Integer.parseInt(responseDto.getCodRespuesta()); - Optional responseErrorCode = responseDto.getError() != null ? Optional.ofNullable(responseDto.getError().getNumError()) : Optional.empty(); - Optional responseErrorDescription = responseDto.getError() != null ? Optional.ofNullable(responseDto.getError().getDesError()) : Optional.empty(); + Optional responseErrorCode = responseDto.getError() != null + ? Optional.ofNullable(responseDto.getError().getNumError()) + : Optional.empty(); + Optional responseErrorDescription = responseDto.getError() != null + ? Optional.ofNullable(responseDto.getError().getDesError()) + : Optional.empty(); Metadata metadata = Metadata.builder() .responseCode(responseErrorCode.map(Integer::parseInt).orElse(statusCode))