Skip to content

Commit

Permalink
Merge pull request #3 from easybill/v0.2.0
Browse files Browse the repository at this point in the history
V0.2.0
  • Loading branch information
BolZer authored Oct 16, 2024
2 parents 3da6293 + 756b364 commit 6ebacf3
Show file tree
Hide file tree
Showing 18 changed files with 357 additions and 220 deletions.
92 changes: 22 additions & 70 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
<?xml version="1.0" encoding="UTF-8"?>
<svrl:schematron-output xmlns:svrl="http://purl.oclc.org/dsdl/svrl" title="" schemaVersion="">
<svrl:ns-prefix-in-attribute-values prefix="rsm" uri="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" />
<svrl:ns-prefix-in-attribute-values prefix="ccts" uri="urn:un:unece:uncefact:documentation:standard:CoreComponentsTechnicalSpecification:2" />
<svrl:ns-prefix-in-attribute-values prefix="udt" uri="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100" />
<svrl:ns-prefix-in-attribute-values prefix="qdt" uri="urn:un:unece:uncefact:data:standard:QualifiedDataType:100" />
<svrl:ns-prefix-in-attribute-values prefix="ram" uri="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" />
<svrl:ns-prefix-in-attribute-values prefix="xs" uri="http://www.w3.org/2001/XMLSchema" />
<svrl:active-pattern id="EN16931-CII-Model" name="EN16931-CII-Model" document="" />
<svrl:fired-rule context="/rsm:CrossIndustryInvoice" />
<svrl:failed-assert id="BR-10" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]" test="rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty/ram:PostalTradeAddress" flag="fatal">
<svrl:text>[BR-10]-An Invoice shall contain the Buyer postal address (BG-8).</svrl:text>
</svrl:failed-assert>
<svrl:failed-assert id="BR-11" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]" test="normalize-space(rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement/ram:BuyerTradeParty/ram:PostalTradeAddress/ram:CountryID) != ''" flag="fatal">
<svrl:text>[BR-11]-The Buyer postal address shall contain a Buyer country code (BT-55).</svrl:text>
</svrl:failed-assert>
<svrl:failed-assert id="BR-16" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]" test="//ram:IncludedSupplyChainTradeLineItem" flag="fatal">
<svrl:text>[BR-16]-An Invoice shall have at least one Invoice line (BG-25).</svrl:text>
</svrl:failed-assert>
<svrl:failed-assert id="BR-CO-25" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]" test="(number(//ram:DuePayableAmount) > 0 and ((//ram:SpecifiedTradePaymentTerms/ram:DueDateDateTime) or (//ram:SpecifiedTradePaymentTerms/ram:Description))) or not(number(//ram:DuePayableAmount)>0)" flag="fatal">
<svrl:text>[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.</svrl:text>
</svrl:failed-assert>
<svrl:fired-rule context="//ram:SellerTradeParty" />
<svrl:fired-rule context="//ram:SpecifiedTaxRegistration/ram:ID[@schemeID='VA']" />
<svrl:fired-rule context="/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeDelivery" />
<svrl:fired-rule context="//ram:SpecifiedTradeSettlementHeaderMonetarySummation" />
<svrl:failed-assert id="BR-12" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:ApplicableHeaderTradeSettlement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:SpecifiedTradeSettlementHeaderMonetarySummation[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]" test="(ram:LineTotalAmount)" flag="fatal">
<svrl:text>[BR-12]-An Invoice shall have the Sum of Invoice line net amount (BT-106). </svrl:text>
</svrl:failed-assert>
<svrl:failed-assert id="BR-CO-10" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:ApplicableHeaderTradeSettlement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:SpecifiedTradeSettlementHeaderMonetarySummation[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]" test="xs:decimal(ram:LineTotalAmount) = round(xs:decimal(sum(../../ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation/ram:LineTotalAmount)) * xs:decimal(100)) div xs:decimal(100)" flag="fatal">
<svrl:text>[BR-CO-10]-Sum of Invoice line net amount (BT-106) = Σ Invoice line net amount (BT-131).</svrl:text>
</svrl:failed-assert>
<svrl:failed-assert id="BR-CO-13" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:ApplicableHeaderTradeSettlement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:SpecifiedTradeSettlementHeaderMonetarySummation[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]" test="(xs:decimal(ram:TaxBasisTotalAmount) = round((xs:decimal(ram:LineTotalAmount) - xs:decimal(ram:AllowanceTotalAmount) + xs:decimal(ram:ChargeTotalAmount)) *10 * 10) div 100) or ((xs:decimal(ram:TaxBasisTotalAmount) = round((xs:decimal(ram:LineTotalAmount) - xs:decimal(ram:AllowanceTotalAmount)) *10 * 10) div 100) and not (ram:ChargeTotalAmount)) or ((xs:decimal(ram:TaxBasisTotalAmount) = round((xs:decimal(ram:LineTotalAmount) + xs:decimal(ram:ChargeTotalAmount)) *10 * 10) div 100) and not (ram:AllowanceTotalAmount)) or ((xs:decimal(ram:TaxBasisTotalAmount) = round((xs:decimal(ram:LineTotalAmount)) *10 * 10) div 100) and not (ram:ChargeTotalAmount) and not (ram:AllowanceTotalAmount))" flag="fatal">
<svrl:text>[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).</svrl:text>
</svrl:failed-assert>
<svrl:fired-rule context="//ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TaxTotalAmount[@currencyID=/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:InvoiceCurrencyCode]" />
<svrl:failed-assert id="BR-CO-14" location="/*:CrossIndustryInvoice[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:SupplyChainTradeTransaction[namespace-uri()='urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'][1]/*:ApplicableHeaderTradeSettlement[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:SpecifiedTradeSettlementHeaderMonetarySummation[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]/*:TaxTotalAmount[namespace-uri()='urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'][1]" test=". = (round(sum(/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax/ram:CalculatedAmount)*10*10)div 100)" flag="fatal">
<svrl:text>[BR-CO-14]-Invoice total VAT amount (BT-110) = Σ VAT category tax amount (BT-117).</svrl:text>
</svrl:failed-assert>
<svrl:active-pattern id="EN16931-CII-Syntax" name="EN16931-CII-Syntax" document="" />
<svrl:fired-rule context="/rsm:CrossIndustryInvoice" />
<svrl:fired-rule context="/rsm:CrossIndustryInvoice/rsm:ExchangedDocumentContext" />
<svrl:fired-rule context="//*[ends-with(name(), 'DocumentContextParameter')]" />
<svrl:fired-rule context="//ram:*[ends-with(name(), 'ID')]" />
<svrl:fired-rule context="/rsm:CrossIndustryInvoice/rsm:ExchangedDocument" />
<svrl:fired-rule context="//ram:*[ends-with(name(), 'ID')]" />
<svrl:fired-rule context="//ram:TypeCode" />
<svrl:fired-rule context="//udt:DateTimeString[@format = '102']" />
<svrl:fired-rule context="/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeAgreement" />
<svrl:fired-rule context="//ram:PostalTradeAddress" />
<svrl:fired-rule context="//ram:*[ends-with(name(), 'ID')]" />
<svrl:fired-rule context="//ram:*[ends-with(name(), 'ID')]" />
<svrl:fired-rule context="//ram:*[ends-with(name(), 'ID')]" />
<svrl:fired-rule context="/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeDelivery" />
<svrl:fired-rule context="/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement" />
<svrl:fired-rule context="/rsm:CrossIndustryInvoice/rsm:SupplyChainTradeTransaction/ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeSettlementHeaderMonetarySummation" />
<svrl:fired-rule context="//ram:*[ends-with(name(), 'Amount') and not (self::ram:TaxTotalAmount)]" />
<svrl:fired-rule context="//ram:*[ends-with(name(), 'Amount') and not (self::ram:TaxTotalAmount)]" />
<svrl:fired-rule context="//ram:*[ends-with(name(), 'Amount') and not (self::ram:TaxTotalAmount)]" />
<svrl:active-pattern id="EN16931-Codes" name="EN16931-Codes" document="" />
<svrl:fired-rule context="rsm:ExchangedDocument/ram:TypeCode" />
<svrl:fired-rule context="ram:CountryID" />
<svrl:fired-rule context="ram:InvoiceCurrencyCode" />
<svrl:fired-rule context="ram:TaxTotalAmount[@currencyID]" />
</svrl:schematron-output>
```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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -39,22 +39,23 @@ 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
);

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);
Expand Down
55 changes: 12 additions & 43 deletions src/main/java/io/github/easybill/Dtos/ValidationResult.java
Original file line number Diff line number Diff line change
@@ -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<FailedAssert> errors;

@NonNull
private final List<FailedAssert> warnings;

public ValidationResult(
@NonNull SchematronOutputType report,
@NonNull String xmlReport,
@NonNull List<FailedAssert> errors,
@NonNull List<FailedAssert> 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<FailedAssert> getErrors() {
return errors.stream().toList();
}

public @NonNull List<FailedAssert> getWarnings() {
return warnings.stream().toList();
return errors.isEmpty();
}
}
52 changes: 52 additions & 0 deletions src/main/java/io/github/easybill/Dtos/ValidationResultField.java
Original file line number Diff line number Diff line change
@@ -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
);
}
}
Original file line number Diff line number Diff line change
@@ -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
) {}
21 changes: 0 additions & 21 deletions src/main/java/io/github/easybill/EN16931ValidatorApplication.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<Throwable> {

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();
}
}
Loading

0 comments on commit 6ebacf3

Please sign in to comment.