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