diff --git a/Interfaces/ZUGFeRDInterface.vb b/Interfaces/ZUGFeRDInterface.vb index ab62b7e4..8221ad5d 100644 --- a/Interfaces/ZUGFeRDInterface.vb +++ b/Interfaces/ZUGFeRDInterface.vb @@ -1,4 +1,6 @@ -Imports System.IO +Imports System.Collections.Generic +Imports System.IO +Imports System.Reflection.Emit Imports System.Xml Imports System.Xml.Serialization Imports System.Xml.XPath @@ -54,6 +56,13 @@ Public Class ZUGFeRDInterface Public Property XPathObject As XPathDocument Public Property SchemaObject As Object Public Property Specification As String + Public Property ValidationErrors As New List(Of ZugferdValidationError) + End Class + + Public Class ZugferdValidationError + Public ElementName As String + Public ElementValue As String + Public ErrorMessage As String End Class ''' @@ -125,9 +134,10 @@ Public Class ZUGFeRDInterface ''' Public Function ExtractZUGFeRDFileWithGDPicture(Path As String) As ZugferdResult Dim oResult = ValidateZUGFeRDFileWithGDPicture(Path) + oResult = ValidateZUGFeRDDocument(oResult) 'If IsNothing(oResult.SchemaObject) Then - ' Throw New ZUGFeRDExecption(ErrorType.NoZugferd, "Datei ist keine ZUGFeRD Datei.") + Throw New ZUGFeRDExecption(ErrorType.NoZugferd, "Datei ist keine ZUGFeRD Datei.") 'End If Return SerializeZUGFeRDDocument(oResult) @@ -140,6 +150,7 @@ Public Class ZUGFeRDInterface ''' Public Function ExtractZUGFeRDFileWithGDPicture(Stream As Stream) As ZugferdResult Dim oResult = ValidateZUGFeRDFileWithGDPicture(Stream) + oResult = ValidateZUGFeRDDocument(oResult) 'If IsNothing(oResult.SchemaObject) Then ' Throw New ZUGFeRDExecption(ErrorType.NoZugferd, "Datei ist keine ZUGFeRD Datei.") @@ -256,6 +267,72 @@ Public Class ZUGFeRDInterface Public Specification As String End Class + Public Function ValidateZUGFeRDDocument(pResult As ZugferdResult) As ZugferdResult + Dim oNavigator As XPathNavigator = pResult.XPathObject.CreateNavigator() + Dim oNamespaceManager As New XmlNamespaceManager(oNavigator.NameTable) + + oNamespaceManager.AddNamespace("ram", "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:12") + oNamespaceManager.AddNamespace("rsm", "urn:ferd:CrossIndustryDocument:invoice:1p0") + + ' CurrencyCode Nodes + Try + Dim oCurrencyCodeIterator As XPathNodeIterator = oNavigator. + Select("//ram:InvoiceCurrencyCode | //ram:TaxCurrencyCode | //ram:TaxCurrencyCode | //ram:SourceCurrencyCode", oNamespaceManager) + + While oCurrencyCodeIterator.MoveNext() + Dim oNode As XPathNavigator = oCurrencyCodeIterator.Current + Dim oValid = ValidateCurrencyCode(oNode.Value) + If oValid = False Then + pResult.ValidationErrors.Add(New ZugferdValidationError() With { + .ElementName = oNode.Name, + .ElementValue = oNode.Value, + .ErrorMessage = "Invalid CurrencyCode. Only 3-Character codes are allowed." + }) + End If + End While + Catch ex As Exception + _logger.Error(ex) + End Try + + ' currencyID + Try + Dim oCurrencyIDIterator As XPathNodeIterator = oNavigator.Select("//*[@currencyID]") + + While oCurrencyIDIterator.MoveNext() + Dim oNode As XPathNavigator = oCurrencyIDIterator.Current + Dim oCurrencyID As String = oNode.GetAttribute("currencyID", "") + + ' CurrencyID is optional per spec + If String.IsNullOrWhiteSpace(oCurrencyID) Then + Continue While + End If + + Dim oValid = ValidateCurrencyCode(oCurrencyID) + If oValid = False Then + pResult.ValidationErrors.Add(New ZugferdValidationError() With { + .ElementName = oNode.Name, + .ElementValue = oCurrencyID, + .ErrorMessage = "Invalid currencyID. Only 3-Character codes or empty values are allowed." + }) + End If + End While + Catch ex As Exception + _logger.Error(ex) + End Try + + Return pResult + End Function + + Private Function ValidateCurrencyCode(pValue As String) As Boolean + Dim oValueRegex As New Text.RegularExpressions.Regex("[A-Z]{3}") + + If oValueRegex.IsMatch(pValue) = False Then + Return False + End If + + Return True + End Function + Public Function SerializeZUGFeRDDocument(pResult As ZugferdResult) As ZugferdResult Try Dim oNavigator As XPathNavigator = pResult.XPathObject.CreateNavigator() diff --git a/Jobs/Exceptions.vb b/Jobs/Exceptions.vb index 36400b9f..13ba4b2b 100644 --- a/Jobs/Exceptions.vb +++ b/Jobs/Exceptions.vb @@ -1,4 +1,6 @@ -Imports System.IO +Imports System.Collections.Generic +Imports System.IO +Imports DigitalData.Modules.Interfaces.ZUGFeRDInterface Public Class Exceptions Public Class MissingValueException @@ -75,4 +77,14 @@ Public Class Exceptions MyBase.New(pInfo) End Sub End Class + + Public Class ValidationException + Inherits ApplicationException + + Public ValidationErrors As List(Of ZugferdValidationError) + + Public Sub New() + MyBase.New("ZUGFeRD document found but validation failed!") + End Sub + End Class End Class diff --git a/Jobs/ZUGFeRD/EmailStrings.vb b/Jobs/ZUGFeRD/EmailStrings.vb index fbecd5bb..034510b0 100644 --- a/Jobs/ZUGFeRD/EmailStrings.vb +++ b/Jobs/ZUGFeRD/EmailStrings.vb @@ -20,6 +20,10 @@ Public Const EMAIL_MD5_ERROR = "

Die von Ihnen gesendete Rechnung wurde bereits von unserem System verarbeitet.

" + Public Const EMAIL_VALIDATION_ERROR = " +

Die von Ihnen gesendete Rechnung hat die ZUGFeRD Validierung nicht bestanden.

+

Die folgenden Felder sind nicht korrekt:

" + Public Const EMAIL_TOO_MUCH_FERDS = "

In Ihrer Email ({0}) sind mehr als ein ZUGFeRD Dokument enthalten. Bitte prüfen Sie Rechnung an Anhänge. Nur eine Rechnung darf das ZUGFeRD-Format enthalten

" Public Const EMAIL_NO_FERDS = "

Ihre Email ({0}) enthielt keine ZUGFeRD-Dokumente.

" diff --git a/Jobs/ZUGFeRD/ImportZUGFeRDFiles.vb b/Jobs/ZUGFeRD/ImportZUGFeRDFiles.vb index 5ffe0cb9..2fb8dee2 100644 --- a/Jobs/ZUGFeRD/ImportZUGFeRDFiles.vb +++ b/Jobs/ZUGFeRD/ImportZUGFeRDFiles.vb @@ -254,6 +254,7 @@ Public Class ImportZUGFeRDFiles Try oDocument = _zugferd.ExtractZUGFeRDFileWithGDPicture(oFile.FullName) + Catch ex As ZUGFeRDExecption Select Case ex.ErrorType Case ZUGFeRDInterface.ErrorType.NoZugferd @@ -275,6 +276,17 @@ Public Class ImportZUGFeRDFiles End Select End Try + ' These validation errors check the document according to the specification. + ' Things like this will be checked: + ' - Currency Codes + ' - Country Codes + ' - DateTime Formats + If oDocument.ValidationErrors.Any() Then + Throw New ValidationException() With { + .ValidationErrors = oDocument.ValidationErrors + } + End If + ' Extract all attachments with the extensions specified in `AllowedExtensions`. ' If you need to extract and use embedded xml files, you need to filter out the zugferd-invoice.xml yourself. ' Right now the zugferd-invoice.xml is filtered out because `AllowedExtensions` does not contain `xml`. @@ -375,6 +387,24 @@ Public Class ImportZUGFeRDFiles oIsSuccess = True oMoveDirectory = oArgs.SuccessDirectory + Catch ex As ValidationException + _logger.Error(ex) + + Dim oErrors = ex.ValidationErrors + Dim oMessage = "REJECTED - ZUGFeRD yes but formal validation failed!" + Update_HistoryEntry(oMessageId, oMD5CheckSum, oMessage, oFBTransaction) + + Dim oErrorList As String = "" + For Each oError In oErrors + oErrorList += $"
  • Element '{oError.ElementName}' mit Wert '{oError.ElementValue}': {oError.ErrorMessage}
  • " + Next + + Dim oBody = String.Format(EmailStrings.EMAIL_VALIDATION_ERROR, oErrorList) + Dim oEmailData = MoveAndRenameEmailToRejected(oArgs, oMessageId) + + _email.AddToEmailQueueMSSQL(oMessageId, oBody, oEmailData, "ValidationException", _EmailOutAccountId, oArgs.NamePortal) + AddRejectedState(oMessageId, "ValidationException", "Die Rechnungsvalidierung ist fehlgeschlagen!", "", oSQLTransaction) + Catch ex As MD5HashException _logger.Error(ex)