diff --git a/README.md b/README.md index a6f64b6..961b94e 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ ## Introduction `en16931-validator` is a small service for validating XML against the official EN16931 schematron rules. It exposes a validation endpoint which takes the -to be validated XML and returns the schematron report. The HTTP status code indicates if the +to be validated XML and returns a JSON payload which contains possible warnings or errors. The HTTP status code indicates if the provided XML is valid (200) or has issues (400). UBL and CII is supported. -### Currently supported validation artifacts: [v1.3.12](https://github.com/ConnectingEurope/eInvoicing-EN16931/releases/tag/validation-1.3.12) +### Currently supported validation artifacts: [v1.3.13](https://github.com/ConnectingEurope/eInvoicing-EN16931/releases/tag/validation-1.3.13) ## Usage This service was mainly designed with containerization in mind. So general idea is to use the following @@ -51,7 +51,7 @@ final class EN16931Validator $response = $httpClient->request('POST', 'http://localhost:8081/validation', [ RequestOptions::HEADERS => [ - 'Content-Type' => 'application/xml', + 'Content-Type' => 'application/json', ], RequestOptions::BODY => $xml, RequestOptions::TIMEOUT => 10, @@ -66,73 +66,25 @@ final class EN16931Validator - Example response in case the XML is invalid -> The `svrl:failed-assert` elements are relevant to be inspected. They contain the error message or warnings. -```xml - - - - - - - - - - - - [BR-10]-An Invoice shall contain the Buyer postal address (BG-8). - - - [BR-11]-The Buyer postal address shall contain a Buyer country code (BT-55). - - - [BR-16]-An Invoice shall have at least one Invoice line (BG-25). - - - [BR-CO-25]-In case the Amount due for payment (BT-115) is positive, either the Payment due date (BT-9) or the Payment terms (BT-20) shall be present. - - - - - - - [BR-12]-An Invoice shall have the Sum of Invoice line net amount (BT-106). - - - [BR-CO-10]-Sum of Invoice line net amount (BT-106) = Σ Invoice line net amount (BT-131). - - - [BR-CO-13]-Invoice total amount without VAT (BT-109) = Σ Invoice line net amount (BT-131) - Sum of allowances on document level (BT-107) + Sum of charges on document level (BT-108). - - - - [BR-CO-14]-Invoice total VAT amount (BT-110) = Σ VAT category tax amount (BT-117). - - - - - - - - - - - - - - - - - - - - - - - - - - - +```JSON +{ + "meta": { + "validation_profile": "UBL", + "validation_profile_version": "1.3.13" + }, + "errors": [ + { + "rule_id": "BR-03", + "rule_location": "/*:Invoice[namespace-uri()='urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'][1]", + "rule_severity": "FATAL", + "rule_messages": [ + "[BR-03]-An Invoice shall have an Invoice issue date (BT-2)." + ] + } + ], + "warnings": [], + "is_valid": false +} ``` ## Issues & Contribution diff --git a/src/main/java/io/github/easybill/Controllers/IndexController.java b/src/main/java/io/github/easybill/Controllers/ValidationController.java similarity index 79% rename from src/main/java/io/github/easybill/Controllers/IndexController.java rename to src/main/java/io/github/easybill/Controllers/ValidationController.java index 020cd2a..c0448ae 100644 --- a/src/main/java/io/github/easybill/Controllers/IndexController.java +++ b/src/main/java/io/github/easybill/Controllers/ValidationController.java @@ -15,18 +15,18 @@ import org.jboss.resteasy.reactive.RestResponse; @Path("/") -public final class IndexController { +public final class ValidationController { private final IValidationService validationService; - public IndexController(IValidationService validationService) { + public ValidationController(IValidationService validationService) { this.validationService = validationService; } @POST @Path("/validation") @Consumes(MediaType.APPLICATION_XML) - @Produces(MediaType.APPLICATION_XML) + @Produces(MediaType.APPLICATION_JSON) @APIResponses( { @APIResponse( @@ -39,8 +39,9 @@ public IndexController(IValidationService validationService) { ), } ) - public RestResponse<@NonNull String> validation(InputStream xmlInputStream) - throws Exception { + public RestResponse<@NonNull ValidationResult> validation( + InputStream xmlInputStream + ) throws Exception { try { ValidationResult result = validationService.validateXml( xmlInputStream @@ -48,13 +49,13 @@ public IndexController(IValidationService validationService) { if (result.isValid()) { return RestResponse.ResponseBuilder - .ok(result.getXmlReport(), MediaType.APPLICATION_XML) + .ok(result, MediaType.APPLICATION_JSON) .build(); } return RestResponse.ResponseBuilder - .create(RestResponse.Status.BAD_REQUEST, result.getXmlReport()) - .type(MediaType.APPLICATION_XML) + .create(RestResponse.Status.BAD_REQUEST, result) + .type(MediaType.APPLICATION_JSON) .build(); } catch (InvalidXmlException exception) { return RestResponse.status(RestResponse.Status.BAD_REQUEST); diff --git a/src/main/java/io/github/easybill/Dtos/ValidationResult.java b/src/main/java/io/github/easybill/Dtos/ValidationResult.java index 4031046..03e9fd7 100644 --- a/src/main/java/io/github/easybill/Dtos/ValidationResult.java +++ b/src/main/java/io/github/easybill/Dtos/ValidationResult.java @@ -1,53 +1,22 @@ package io.github.easybill.Dtos; -import com.helger.schematron.svrl.jaxb.FailedAssert; -import com.helger.schematron.svrl.jaxb.SchematronOutputType; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Collections; import java.util.List; import org.checkerframework.checker.nullness.qual.NonNull; -public final class ValidationResult { - - @NonNull - private final SchematronOutputType report; - - @NonNull - private final String xmlReport; - - @NonNull - private final List errors; - - @NonNull - private final List warnings; - - public ValidationResult( - @NonNull SchematronOutputType report, - @NonNull String xmlReport, - @NonNull List errors, - @NonNull List warnings - ) { - this.report = report.clone(); - this.xmlReport = xmlReport; - this.errors = errors.stream().toList(); - this.warnings = warnings.stream().toList(); +public record ValidationResult( + @NonNull ValidationResultMetaData meta, + @NonNull List<@NonNull ValidationResultField> errors, + @NonNull List<@NonNull ValidationResultField> warnings +) { + public ValidationResult { + errors = Collections.unmodifiableList(errors); + warnings = Collections.unmodifiableList(warnings); } + @JsonProperty("is_valid") public boolean isValid() { - return (long) errors.size() == 0; - } - - public @NonNull SchematronOutputType getReport() { - return report.clone(); - } - - public @NonNull String getXmlReport() { - return xmlReport; - } - - public @NonNull List getErrors() { - return errors.stream().toList(); - } - - public @NonNull List getWarnings() { - return warnings.stream().toList(); + return errors.isEmpty(); } } diff --git a/src/main/java/io/github/easybill/Dtos/ValidationResultField.java b/src/main/java/io/github/easybill/Dtos/ValidationResultField.java new file mode 100644 index 0000000..4b0964d --- /dev/null +++ b/src/main/java/io/github/easybill/Dtos/ValidationResultField.java @@ -0,0 +1,52 @@ +package io.github.easybill.Dtos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.helger.schematron.svrl.jaxb.FailedAssert; +import com.helger.schematron.svrl.jaxb.Text; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import org.checkerframework.checker.nullness.qual.NonNull; + +enum Severity { + FATAL, + WARNING, +} + +public record ValidationResultField( + @JsonProperty("rule_id") @NonNull String id, + @JsonProperty("rule_location") @NonNull String location, + @JsonProperty("rule_severity") @NonNull Severity severity, + @JsonProperty("rule_messages") @NonNull List<@NonNull String> messages +) { + public ValidationResultField { + messages = Collections.unmodifiableList(messages); + } + + public static ValidationResultField fromFailedAssert( + @NonNull FailedAssert failedAssert + ) { + var messages = failedAssert + .getDiagnosticReferenceOrPropertyReferenceOrText() + .stream() + .filter(element -> element instanceof Text) + .map(element -> + ((Text) element).getContent() + .stream() + .filter(innerElement -> innerElement instanceof String) + .map(innerElement -> (String) innerElement) + .toList() + ) + .flatMap(List::stream) + .toList(); + + return new ValidationResultField( + Objects.requireNonNullElse(failedAssert.getId(), ""), + Objects.requireNonNullElse(failedAssert.getLocation(), ""), + Objects.equals(failedAssert.getFlag(), "fatal") + ? Severity.FATAL + : Severity.WARNING, + messages + ); + } +} diff --git a/src/main/java/io/github/easybill/Dtos/ValidationResultMetaData.java b/src/main/java/io/github/easybill/Dtos/ValidationResultMetaData.java new file mode 100644 index 0000000..1ff9af1 --- /dev/null +++ b/src/main/java/io/github/easybill/Dtos/ValidationResultMetaData.java @@ -0,0 +1,14 @@ +package io.github.easybill.Dtos; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.github.easybill.Enums.XMLSyntaxType; +import org.checkerframework.checker.nullness.qual.NonNull; + +public record ValidationResultMetaData( + @NonNull + @JsonProperty("validation_profile") + XMLSyntaxType validationProfile, + @NonNull + @JsonProperty("validation_profile_version") + String validation_profile_version +) {} diff --git a/src/main/java/io/github/easybill/EN16931ValidatorApplication.java b/src/main/java/io/github/easybill/EN16931ValidatorApplication.java deleted file mode 100644 index ff5f2ce..0000000 --- a/src/main/java/io/github/easybill/EN16931ValidatorApplication.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.github.easybill; - -import jakarta.ws.rs.core.Application; -import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition; -import org.eclipse.microprofile.openapi.annotations.info.Contact; -import org.eclipse.microprofile.openapi.annotations.info.Info; -import org.eclipse.microprofile.openapi.annotations.info.License; - -@OpenAPIDefinition( - info = @Info( - title = "EN16931 Validator API", - version = "0.1.0", - contact = @Contact( - name = "easybill GmbH", - url = "https://github.com/easybill", - email = "dev@easybill.de" - ), - license = @License(name = "MIT", url = "https://mit-license.org") - ) -) -public final class EN16931ValidatorApplication extends Application {} diff --git a/src/main/java/io/github/easybill/Interceptors/GlobalExceptionInterceptor.java b/src/main/java/io/github/easybill/Interceptors/GlobalExceptionInterceptor.java new file mode 100644 index 0000000..620b4df --- /dev/null +++ b/src/main/java/io/github/easybill/Interceptors/GlobalExceptionInterceptor.java @@ -0,0 +1,26 @@ +package io.github.easybill.Interceptors; + +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; +import org.jboss.logging.Logger; + +@Provider +public class GlobalExceptionInterceptor implements ExceptionMapper { + + private static final Logger LOGGER = Logger.getLogger( + GlobalExceptionInterceptor.class + ); + + @Override + public Response toResponse(Throwable exception) { + if (exception instanceof WebApplicationException) { + return ((WebApplicationException) exception).getResponse(); + } + + LOGGER.error("Encountered an exception:", exception); + + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } +} diff --git a/src/main/java/io/github/easybill/Interceptors/RequestResponseLoggingInterceptor.java b/src/main/java/io/github/easybill/Interceptors/RequestResponseLoggingInterceptor.java new file mode 100644 index 0000000..771ec7f --- /dev/null +++ b/src/main/java/io/github/easybill/Interceptors/RequestResponseLoggingInterceptor.java @@ -0,0 +1,47 @@ +package io.github.easybill.Interceptors; + +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.ext.Provider; +import java.io.IOException; +import org.jboss.logging.Logger; + +@Provider +public class RequestResponseLoggingInterceptor + implements ContainerRequestFilter, ContainerResponseFilter { + + private static final Logger logger = Logger.getLogger( + RequestResponseLoggingInterceptor.class + ); + + @Override + public void filter(ContainerRequestContext containerRequestContext) + throws IOException { + String method = containerRequestContext.getMethod(); + String uri = containerRequestContext + .getUriInfo() + .getRequestUri() + .toString(); + String queryParams = containerRequestContext + .getUriInfo() + .getQueryParameters() + .toString(); + + logger.infof("Request received: %s %s, %s", method, uri, queryParams); + } + + @Override + public void filter( + ContainerRequestContext containerRequestContext, + ContainerResponseContext containerResponseContext + ) throws IOException { + int statusCode = containerResponseContext.getStatus(); + String status = containerResponseContext + .getStatusInfo() + .getReasonPhrase(); + + logger.infof("Response sent: %s - %s", statusCode, status); + } +} diff --git a/src/main/java/io/github/easybill/Services/HealthCheck/StatsHealthCheck.java b/src/main/java/io/github/easybill/Services/HealthCheck/ApplicationHealthCheck.java similarity index 72% rename from src/main/java/io/github/easybill/Services/HealthCheck/StatsHealthCheck.java rename to src/main/java/io/github/easybill/Services/HealthCheck/ApplicationHealthCheck.java index fd2ad78..69e93f5 100644 --- a/src/main/java/io/github/easybill/Services/HealthCheck/StatsHealthCheck.java +++ b/src/main/java/io/github/easybill/Services/HealthCheck/ApplicationHealthCheck.java @@ -2,6 +2,8 @@ import jakarta.enterprise.context.ApplicationScoped; import java.lang.management.ManagementFactory; +import java.util.Objects; +import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.health.HealthCheck; import org.eclipse.microprofile.health.HealthCheckResponse; import org.eclipse.microprofile.health.HealthCheckResponseBuilder; @@ -9,12 +11,18 @@ @Liveness @ApplicationScoped -public final class StatsHealthCheck implements HealthCheck { +public final class ApplicationHealthCheck implements HealthCheck { + + final Config config; + + public ApplicationHealthCheck(Config config) { + this.config = config; + } @Override public HealthCheckResponse call() { HealthCheckResponseBuilder response = HealthCheckResponse.named( - "stats" + "Application" ); var osBean = ManagementFactory.getOperatingSystemMXBean(); @@ -22,6 +30,12 @@ public HealthCheckResponse call() { return response .up() + .withData( + "version", + Objects.requireNonNull( + config.getConfigValue("application.version").getValue() + ) + ) .withData("osName", osBean.getName()) .withData("osArch", osBean.getArch()) .withData( diff --git a/src/main/java/io/github/easybill/Services/HealthCheck/ValidatorHealthCheck.java b/src/main/java/io/github/easybill/Services/HealthCheck/ValidatorHealthCheck.java index 5440139..c859350 100644 --- a/src/main/java/io/github/easybill/Services/HealthCheck/ValidatorHealthCheck.java +++ b/src/main/java/io/github/easybill/Services/HealthCheck/ValidatorHealthCheck.java @@ -2,6 +2,8 @@ import io.github.easybill.Contracts.IValidationService; import jakarta.enterprise.context.ApplicationScoped; +import java.util.Objects; +import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.health.HealthCheck; import org.eclipse.microprofile.health.HealthCheckResponse; import org.eclipse.microprofile.health.HealthCheckResponseBuilder; @@ -11,16 +13,28 @@ @ApplicationScoped public final class ValidatorHealthCheck implements HealthCheck { + final Config config; final IValidationService validationService; - public ValidatorHealthCheck(IValidationService validationService) { + public ValidatorHealthCheck( + IValidationService validationService, + Config config + ) { + this.config = config; this.validationService = validationService; } @Override public HealthCheckResponse call() { HealthCheckResponseBuilder response = HealthCheckResponse.named( - "schematron ready and ruleset is valid" + "Validator" + ); + + response.withData( + "artefactsVersion", + Objects.requireNonNull( + config.getConfigValue("en16931.artefacts.version").getValue() + ) ); if (this.validationService.isLoadedSchematronValid()) { diff --git a/src/main/java/io/github/easybill/Services/ValidationService.java b/src/main/java/io/github/easybill/Services/ValidationService.java index 2dc2b8e..2fa7845 100644 --- a/src/main/java/io/github/easybill/Services/ValidationService.java +++ b/src/main/java/io/github/easybill/Services/ValidationService.java @@ -3,11 +3,12 @@ import com.helger.commons.io.ByteArrayWrapper; import com.helger.commons.io.resource.ClassPathResource; import com.helger.schematron.sch.SchematronResourceSCH; -import com.helger.schematron.svrl.SVRLMarshaller; import com.helger.schematron.svrl.jaxb.FailedAssert; import com.helger.schematron.svrl.jaxb.SchematronOutputType; import io.github.easybill.Contracts.IValidationService; import io.github.easybill.Dtos.ValidationResult; +import io.github.easybill.Dtos.ValidationResultField; +import io.github.easybill.Dtos.ValidationResultMetaData; import io.github.easybill.Enums.XMLSyntaxType; import io.github.easybill.Exceptions.InvalidXmlException; import jakarta.inject.Singleton; @@ -18,23 +19,34 @@ import java.util.Optional; import java.util.regex.Pattern; import org.checkerframework.checker.nullness.qual.NonNull; +import org.eclipse.microprofile.config.Config; import org.mozilla.universalchardet.UniversalDetector; @Singleton public final class ValidationService implements IValidationService { + private final String artefactsVersion; private final SchematronResourceSCH ciiSchematron; private final SchematronResourceSCH ublSchematron; - ValidationService() { + ValidationService(Config config) { + artefactsVersion = + Objects.requireNonNull( + config.getConfigValue("en16931.artefacts.version").getValue() + ); + ciiSchematron = new SchematronResourceSCH( - new ClassPathResource("EN16931-CII-1.3.12.sch") + new ClassPathResource( + String.format("EN16931-CII-%s.sch", artefactsVersion) + ) ); ublSchematron = new SchematronResourceSCH( - new ClassPathResource("EN16931-UBL-1.3.12.sch") + new ClassPathResource( + String.format("EN16931-UBL-%s.sch", artefactsVersion) + ) ); if (!ciiSchematron.isValidSchematron()) { @@ -66,15 +78,8 @@ public final class ValidationService implements IValidationService { var report = innerValidateSchematron(xmlSyntaxType, bytesFromSteam) .orElseThrow(RuntimeException::new); - String reportXML = new SVRLMarshaller().getAsString(report); - - if (reportXML == null) { - throw new RuntimeException("validation failed unexpectedly"); - } - return new ValidationResult( - report, - reportXML, + new ValidationResultMetaData(xmlSyntaxType, artefactsVersion), getErrorsFromSchematronOutput(report), getWarningsFromSchematronOutput(report) ); @@ -88,7 +93,7 @@ public boolean isLoadedSchematronValid() { ); } - private List getErrorsFromSchematronOutput( + private List<@NonNull ValidationResultField> getErrorsFromSchematronOutput( @NonNull SchematronOutputType outputType ) { return outputType @@ -99,10 +104,11 @@ private List getErrorsFromSchematronOutput( Objects.equals(((FailedAssert) element).getFlag(), "fatal") ) .map(element -> (FailedAssert) element) + .map(ValidationResultField::fromFailedAssert) .toList(); } - private List getWarningsFromSchematronOutput( + private List<@NonNull ValidationResultField> getWarningsFromSchematronOutput( @NonNull SchematronOutputType outputType ) { return outputType @@ -113,6 +119,7 @@ private List getWarningsFromSchematronOutput( Objects.equals(((FailedAssert) element).getFlag(), "warning") ) .map(element -> (FailedAssert) element) + .map(ValidationResultField::fromFailedAssert) .toList(); } diff --git a/src/main/resources/EN16931-CII-1.3.12.sch b/src/main/resources/EN16931-CII-1.3.13.sch old mode 100644 new mode 100755 similarity index 97% rename from src/main/resources/EN16931-CII-1.3.12.sch rename to src/main/resources/EN16931-CII-1.3.13.sch index 2efd856..91a34f5 --- a/src/main/resources/EN16931-CII-1.3.12.sch +++ b/src/main/resources/EN16931-CII-1.3.13.sch @@ -4,7 +4,8 @@ Licensed under European Union Public Licence (EUPL) version 1.2. --> - + + EN16931 model bound to CII @@ -121,7 +122,7 @@ [BR-64]-The Item standard identifier (BT-157) shall have a Scheme identifier. [BR-CO-04]-Each Invoice line (BG-25) shall be categorized with an Invoiced item VAT category code (BT-151). [BR-CO-18]-An Invoice shall at least have one VAT breakdown group (BG-23). - [BR-DEC-23]-The allowed maximum number of decimals for the Invoice line net amount (BT-131) is 2. + [BR-DEC-23]-The allowed maximum number of decimals for the Invoice line net amount (BT-131) is 2. [BR-41]-Each Invoice line allowance (BG-27) shall have an Invoice line allowance amount (BT-136). @@ -216,7 +217,7 @@ [BR-AG-02]-An Invoice that contains an Invoice line (BG-25) where the Invoiced item VAT category code (BT-151) is "IPSI" shall contain the Seller VAT Identifier (BT-31), the Seller tax registration identifier (BT-32) and/or the Seller tax representative VAT identifier (BT-63). - [BR-AG-05]-In an Invoice line (BG-25) where the Invoiced item VAT category code (BT-151) is "IPSI" the Invoiced item VAT rate (BT-152) shall be 0 (zero) or greater than zero. + [BR-AG-05]-In an Invoice line (BG-25) where the Invoiced item VAT category code (BT-151) is "IPSI" the Invoiced item VAT rate (BT-152) shall be 0 (zero) or greater than zero. [BR-AG-03]-An Invoice that contains a Document level allowance (BG-20) where the Document level allowance VAT category code (BT-95) is "IPSI" shall contain the Seller VAT Identifier (BT-31), the Seller Tax registration identifier (BT-32) and/or the Seller tax representative VAT identifier (BT-63). @@ -489,7 +490,6 @@ [CII-SR-129] - TypeCode should not be present [CII-SR-130] - CategoryTradeTax should not be present [CII-SR-131] - ActualTradeCurrencyExchange should not be present - [CII-SR-440] - ActualAmount should exist maximum once [CII-SR-445] - IncludedTradeTax should not be present [CII-SR-132] - ValiditySpecifiedPeriod should not be present [CII-SR-133] - DeliveryTradeLocation should not be present @@ -513,6 +513,9 @@ [CII-SR-150] - IncludedSpecifiedMarketplace should not be present [CII-SR-447] - UltimateCustomerOrderReferencedDocument should not be present + + [CII-SR-440] - ActualAmount should exist maximum once + [CII-SR-151] - RequestedQuantity should not be present [CII-SR-152] - ReceivedQuantity should not be present @@ -680,8 +683,8 @@ [CII-SR-456] - DefinedTradeContact of BuyerTradeParty shall exist maximum once [CII-SR-457] - IssuerAssignedID with TypeCode 50 should exist maximum once [CII-SR-458] - IssuerAssignedID with TypeCode 130 should exist maximum once - [CII-SR-459] - SellerTradeParty URIUniversalCommunication should exist maximum once - [CII-SR-460] - BuyerTradeParty URIUniversalCommunication should exist maximum once + [CII-SR-459] - SellerTradeParty URIUniversalCommunication should exist maximum once + [CII-SR-460] - BuyerTradeParty URIUniversalCommunication should exist maximum once [CII-SR-308] - RelatedSupplyChainConsignment should not be present @@ -807,7 +810,7 @@ [CII-SR-452] - Only one SpecifiedTradePaymentTerms should be present [CII-SR-453] - Only one SpecifiedTradePaymentTerms Description should be present [CII-SR-461] - Only one TaxPointDate shall be present - [CII-SR-462] - Only one DueDateTypeCode shall be present + [CII-SR-462] - Only one DueDateTypeCode shall be present [CII-SR-411] - InformationAmount should not be present @@ -830,16 +833,20 @@ [CII-SR-004] - Value should not be present [CII-SR-005] - SpecifiedDocumentVersion should not be present - + + [CII-DT-001] - schemeName should not be present + [CII-DT-002] - schemeAgencyName should not be present + [CII-DT-003] - schemeDataURI should not be present + [CII-DT-004] - schemeURI should not be present [CII-DT-005] - schemeID should not be present [CII-DT-006] - schemeAgencyID should not be present [CII-DT-007] - schemeVersionID should not be present - [CII-DT-001] - schemeName should not be present - [CII-DT-002] - schemeAgencyName should not be present - [CII-DT-003] - schemeDataURI should not be present - [CII-DT-004] - schemeURI should not be present + [CII-DT-001] - schemeName should not be present + [CII-DT-002] - schemeAgencyName should not be present + [CII-DT-003] - schemeDataURI should not be present + [CII-DT-004] - schemeURI should not be present [CII-DT-008] - name should not be present @@ -955,13 +962,13 @@ [BR-CL-01]-The document type code MUST be coded by the invoice and credit note related code lists of UNTDID 1001. - [BR-CL-03]-currencyID MUST be coded using ISO code list 4217 alpha-3 + [BR-CL-03]-currencyID MUST be coded using ISO code list 4217 alpha-3 - [BR-CL-04]-Invoice currency code MUST be coded using ISO code list 4217 alpha-3 + [BR-CL-04]-Invoice currency code MUST be coded using ISO code list 4217 alpha-3 - [BR-CL-05]-Tax currency code MUST be coded using ISO code list 4217 alpha-3 + [BR-CL-05]-Tax currency code MUST be coded using ISO code list 4217 alpha-3 [BR-CL-06]-Value added tax point date code MUST be coded using a restriction of UNTDID 2475. @@ -973,10 +980,10 @@ [BR-CL-08]-Subject Code MUST be coded using a restriction of UNTDID 4451. - [BR-CL-10]-Any identifier identification scheme identifier MUST be coded using one of the ISO 6523 ICD list. + [BR-CL-10]-Any identifier identification scheme identifier MUST be coded using one of the ISO 6523 ICD list. - [BR-CL-11]-Any registration identifier identification scheme identifier MUST be coded using one of the ISO 6523 ICD list. + [BR-CL-11]-Any registration identifier identification scheme identifier MUST be coded using one of the ISO 6523 ICD list. [BR-CL-13]-Item classification identifier identification scheme identifier MUST be coded using one of the UNTDID 7143 list. @@ -1003,11 +1010,11 @@ [BR-CL-20]-Coded charge reasons MUST belong to the UNCL 7161 code list - [BR-CL-21]-Item standard identifier scheme identifier MUST belong to the ISO 6523 ICD + [BR-CL-21]-Item standard identifier scheme identifier MUST belong to the ISO 6523 ICD code list - [BR-CL-22]-Tax exemption reason code identifier scheme identifier MUST belong to the CEF VATEX code list + [BR-CL-22]-Tax exemption reason code identifier scheme identifier MUST belong to the CEF VATEX code list [BR-CL-23]-Unit code MUST be coded according to the UN/ECE Recommendation 20 with Rec 21 extension @@ -1016,10 +1023,10 @@ [BR-CL-24]-For Mime code in attribute use MIMEMediaType. - [BR-CL-25]-Endpoint identifier scheme identifier MUST belong to the CEF EAS code list + [BR-CL-25]-Endpoint identifier scheme identifier MUST belong to the CEF EAS code list - [BR-CL-26]-Delivery location identifier scheme identifier MUST belong to the ISO 6523 ICD + [BR-CL-26]-Delivery location identifier scheme identifier MUST belong to the ISO 6523 ICD code list diff --git a/src/main/resources/EN16931-UBL-1.3.12.sch b/src/main/resources/EN16931-UBL-1.3.13.sch old mode 100644 new mode 100755 similarity index 98% rename from src/main/resources/EN16931-UBL-1.3.12.sch rename to src/main/resources/EN16931-UBL-1.3.13.sch index 43b85cf..33a7bc7 --- a/src/main/resources/EN16931-UBL-1.3.12.sch +++ b/src/main/resources/EN16931-UBL-1.3.13.sch @@ -4,7 +4,8 @@ Licensed under European Union Public Licence (EUPL) version 1.2. --> - + + EN16931 model bound to UBL @@ -1149,13 +1150,13 @@ [BR-CL-01]-The document type code MUST be coded by the invoice and credit note related code lists of UNTDID 1001. - [BR-CL-03]-currencyID MUST be coded using ISO code list 4217 alpha-3 + [BR-CL-03]-currencyID MUST be coded using ISO code list 4217 alpha-3 - [BR-CL-04]-Invoice currency code MUST be coded using ISO code list 4217 alpha-3 + [BR-CL-04]-Invoice currency code MUST be coded using ISO code list 4217 alpha-3 - [BR-CL-05]-Tax currency code MUST be coded using ISO code list 4217 alpha-3 + [BR-CL-05]-Tax currency code MUST be coded using ISO code list 4217 alpha-3 [BR-CL-06]-Value added tax point date code MUST be coded using a restriction of UNTDID 2005. @@ -1164,10 +1165,10 @@ [BR-CL-07]-Object identifier identification scheme identifier MUST be coded using a restriction of UNTDID 1153. - [BR-CL-10]-Any identifier identification scheme identifier MUST be coded using one of the ISO 6523 ICD list. + [BR-CL-10]-Any identifier identification scheme identifier MUST be coded using one of the ISO 6523 ICD list. - [BR-CL-11]-Any registration identifier identification scheme identifier MUST be coded using one of the ISO 6523 ICD list. + [BR-CL-11]-Any registration identifier identification scheme identifier MUST be coded using one of the ISO 6523 ICD list. [BR-CL-13]-Item classification identifier identification scheme identifier MUST be @@ -1195,10 +1196,10 @@ [BR-CL-20]-Coded charge reasons MUST belong to the UNCL 7161 code list - [BR-CL-21]-Item standard identifier scheme identifier MUST belong to the ISO 6523 ICD code list + [BR-CL-21]-Item standard identifier scheme identifier MUST belong to the ISO 6523 ICD code list - [BR-CL-22]-Tax exemption reason code identifier scheme identifier MUST belong to the CEF VATEX code list + [BR-CL-22]-Tax exemption reason code identifier scheme identifier MUST belong to the CEF VATEX code list [BR-CL-23]-Unit code MUST be coded according to the UN/ECE Recommendation 20 with @@ -1208,10 +1209,10 @@ [BR-CL-24]-For Mime code in attribute use MIMEMediaType. - [BR-CL-25]-Endpoint identifier scheme identifier MUST belong to the CEF EAS code list + [BR-CL-25]-Endpoint identifier scheme identifier MUST belong to the CEF EAS code list - [BR-CL-26]-Delivery location identifier scheme identifier MUST belong to the ISO 6523 ICD code list + [BR-CL-26]-Delivery location identifier scheme identifier MUST belong to the ISO 6523 ICD code list diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 2227b16..8ae7f7f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,18 @@ +application.version=0.2.0 + +en16931.artefacts.version=1.3.13 + +quarkus.smallrye-openapi.info-title=EN16931 Validator API +quarkus.smallrye-openapi.info-version=${application.version} +quarkus.smallrye-openapi.info-description=A small service to validate XML against EN16931 rules +quarkus.smallrye-openapi.info-contact-email=dev@easybill.de +quarkus.smallrye-openapi.info-contact-name=easybill GmbH +quarkus.smallrye-openapi.info-contact-url=https://github.com/easybill +quarkus.smallrye-openapi.info-license-name=MIT +quarkus.smallrye-openapi.info-license-url=https://mit-license.org + quarkus.banner.enabled=false +quarkus.log.console.level=INFO quarkus.swagger-ui.always-include=true quarkus.swagger-ui.path=/swagger quarkus.smallrye-health.root-path=/health \ No newline at end of file diff --git a/src/native-test/java/io/github/easybill/IndexResourceIT.java b/src/native-test/java/io/github/easybill/ValidationResourceIT.java similarity index 72% rename from src/native-test/java/io/github/easybill/IndexResourceIT.java rename to src/native-test/java/io/github/easybill/ValidationResourceIT.java index 198ec24..8ea339b 100644 --- a/src/native-test/java/io/github/easybill/IndexResourceIT.java +++ b/src/native-test/java/io/github/easybill/ValidationResourceIT.java @@ -3,6 +3,6 @@ import io.quarkus.test.junit.QuarkusIntegrationTest; @QuarkusIntegrationTest -class IndexResourceIT extends IndexControllerTest { +class ValidationResourceIT extends ValidationControllerTest { // Execute the same tests but in packaged mode. } diff --git a/src/test/java/io/github/easybill/IndexControllerTest.java b/src/test/java/io/github/easybill/ValidationControllerTest.java similarity index 72% rename from src/test/java/io/github/easybill/IndexControllerTest.java rename to src/test/java/io/github/easybill/ValidationControllerTest.java index 395012e..0ff5ab8 100644 --- a/src/test/java/io/github/easybill/IndexControllerTest.java +++ b/src/test/java/io/github/easybill/ValidationControllerTest.java @@ -1,7 +1,9 @@ package io.github.easybill; import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.*; +import io.github.easybill.Enums.XMLSyntaxType; import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; import java.io.IOException; @@ -16,7 +18,7 @@ import org.junit.jupiter.params.provider.ValueSource; @QuarkusTest -class IndexControllerTest { +class ValidationControllerTest { @Test void testValidationEndpointWhenInvokedWithWrongMethod() { @@ -28,6 +30,17 @@ void testValidationEndpointWhenInvokedWithAnEmptyPayload() { given().when().post("/validation").then().statusCode(415); } + @Test + void testValidationEndpointWithEmptyPayload() throws IOException { + given() + .body(loadFixtureFileAsStream("Invalid/Invalid.xml")) + .contentType(ContentType.XML) + .when() + .post("/validation") + .then() + .statusCode(400); + } + @ParameterizedTest @ValueSource( strings = { @@ -42,7 +55,6 @@ void testValidationEndpointWhenInvokedWithAnEmptyPayload() { "UBL/guide-example3.xml", "UBL/issue116.xml", "UBL/sales-order-example.xml", - "UBL/sample-discount-price.xml", "UBL/ubl-tc434-creditnote1.xml", "UBL/ubl-tc434-example1.xml", "UBL/ubl-tc434-example2.xml", @@ -68,7 +80,14 @@ void testValidationEndpointWithValidUblDocuments( .when() .post("/validation") .then() - .statusCode(200); + .statusCode(200) + .contentType(ContentType.JSON) + .body("is_valid", equalTo(true)) + .body( + "meta.validation_profile", + equalTo(XMLSyntaxType.UBL.toString()) + ) + .body("errors", empty()); } @ParameterizedTest @@ -103,6 +122,7 @@ void testValidationEndpointWithValidUblDocuments( "CII/CII_ZUGFeRD_23_XRECHNUNG_Elektron.xml", "CII/CII_ZUGFeRD_23_XRECHNUNG_Reisekostenabrechnung.xml", "CII/XRechnung-O.xml", + "CII/CII_ZUGFeRD_23_EXTENDED_Rechnungskorrektur.xml", } ) void testValidationEndpointWithValidCIIDocuments( @@ -114,28 +134,40 @@ void testValidationEndpointWithValidCIIDocuments( .when() .post("/validation") .then() - .statusCode(200); + .statusCode(200) + .contentType(ContentType.JSON) + .body("is_valid", equalTo(true)) + .body( + "meta.validation_profile", + equalTo(XMLSyntaxType.CII.toString()) + ) + .body("errors", empty()); } - @ParameterizedTest - @ValueSource( - strings = { - "Invalid/CII_MissingExchangeDocumentContext.xml", - "Invalid/Empty.xml", + static Stream providerValuesValidationEndpointWithInvalidPayload() { + return Stream.of( + Arguments.of("Invalid/CII_MissingExchangeDocumentContext.xml"), + //Arguments.of("Invalid/Empty.xml"), + // Uses HRK as currency which is no longer supported in EN16931 + Arguments.of("UBL/sample-discount-price.xml"), // Profile BASIC WL is not EN16931 conform. WL = Without Lines. EN16931 requires at least 1 line. - "CII/CII_ZUGFeRD_23_BASIC-WL_Einfach.xml", + Arguments.of("CII/CII_ZUGFeRD_23_BASIC-WL_Einfach.xml"), // Profile MINIMUM is not EN16931 conform. - "CII/CII_ZUGFeRD_Minimum.xml", - "CII/CII_ZUGFeRD_23_MINIMUM_Buchungshilfe.xml", - "CII/CII_ZUGFeRD_23_MINIMUM_Rechnung.xml", + Arguments.of("CII/CII_ZUGFeRD_Minimum.xml"), + Arguments.of("CII/CII_ZUGFeRD_23_MINIMUM_Buchungshilfe.xml"), + Arguments.of("CII/CII_ZUGFeRD_23_MINIMUM_Rechnung.xml"), // Profile EXTENDED is EN16931 conform. However, those examples do have rounding issues. Which is valid // in EXTENDED Profile - "CII/CII_ZUGFeRD_23_EXTENDED_Kostenrechnung.xml", - "CII/CII_ZUGFeRD_23_EXTENDED_Projektabschlussrechnung.xml", - "CII/CII_ZUGFeRD_23_EXTENDED_Rechnungskorrektur.xml", - "CII/CII_ZUGFeRD_23_EXTENDED_Warenrechnung.xml", - } - ) + Arguments.of("CII/CII_ZUGFeRD_23_EXTENDED_Kostenrechnung.xml"), + Arguments.of( + "CII/CII_ZUGFeRD_23_EXTENDED_Projektabschlussrechnung.xml" + ), + Arguments.of("CII/CII_ZUGFeRD_23_EXTENDED_Warenrechnung.xml") + ); + } + + @ParameterizedTest + @MethodSource("providerValuesValidationEndpointWithInvalidPayload") void testValidationEndpointWithInvalidPayload( @NonNull String fixtureFileName ) throws IOException { @@ -145,7 +177,17 @@ void testValidationEndpointWithInvalidPayload( .when() .post("/validation") .then() - .statusCode(400); + .statusCode(400) + .contentType(ContentType.JSON) + .body("is_valid", equalTo(false)) + .body("errors", not(empty())); + } + + static Stream providerValuesForDifferentEncodings() { + return Stream.of( + Arguments.of("UBL/base-example-utf16be.xml"), + Arguments.of("UBL/base-example-utf16le.xml") + ); } @ParameterizedTest @@ -159,14 +201,10 @@ void testValidationEndpointWithDifferentEncodings( .when() .post("/validation") .then() - .statusCode(200); - } - - static Stream providerValuesForDifferentEncodings() { - return Stream.of( - Arguments.of("UBL/base-example-utf16be.xml"), - Arguments.of("UBL/base-example-utf16le.xml") - ); + .statusCode(200) + .contentType(ContentType.JSON) + .body("is_valid", equalTo(true)) + .body("errors", empty()); } InputStream loadFixtureFileAsStream(@NonNull String fixtureFileName) diff --git a/src/test/resources/Invalid/Empty.xml b/src/test/resources/Invalid/Empty.xml deleted file mode 100644 index e69de29..0000000 diff --git a/src/test/resources/Invalid/Invalid.xml b/src/test/resources/Invalid/Invalid.xml new file mode 100644 index 0000000..edccff3 --- /dev/null +++ b/src/test/resources/Invalid/Invalid.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file