ZUGFeRD: Validate errors in xml and throw ValidationException

This commit is contained in:
Jonathan Jenne 2023-06-22 10:51:43 +02:00
parent 0da1eb55a9
commit 524c429de4
6 changed files with 139 additions and 129 deletions

View File

@ -111,6 +111,7 @@
<DependentUpon>Settings.settings</DependentUpon> <DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput> <DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile> </Compile>
<Compile Include="ZUGFeRDInterface\Validator.vb" />
<Compile Include="ZUGFeRDInterface\Version1.0\CrossIndustryDocumentType.vb" /> <Compile Include="ZUGFeRDInterface\Version1.0\CrossIndustryDocumentType.vb" />
<Compile Include="ZUGFeRDInterface.vb" /> <Compile Include="ZUGFeRDInterface.vb" />
<Compile Include="ZUGFeRDInterface\FileGroups.vb" /> <Compile Include="ZUGFeRDInterface\FileGroups.vb" />

View File

@ -15,6 +15,7 @@ Public Class ZUGFeRDInterface
Private ReadOnly _logConfig As LogConfig Private ReadOnly _logConfig As LogConfig
Private ReadOnly _logger As Logger Private ReadOnly _logger As Logger
Private ReadOnly _Options As ZugferdOptions Private ReadOnly _Options As ZugferdOptions
Private ReadOnly _Validator As Validator
' These constants define the specification markers for the different ' These constants define the specification markers for the different
' zugferd document schema versions. These markers need to be used to ' zugferd document schema versions. These markers need to be used to
@ -39,7 +40,6 @@ Public Class ZUGFeRDInterface
UnsupportedFormat UnsupportedFormat
FileTooBig FileTooBig
UnknownError UnknownError
ValidationFailed
End Enum End Enum
Public ReadOnly Property FileGroup As FileGroups Public ReadOnly Property FileGroup As FileGroups
@ -54,7 +54,6 @@ Public Class ZUGFeRDInterface
Public Class ZugferdResult Public Class ZugferdResult
Public Property DataFileName As String Public Property DataFileName As String
Public Property XPathObject As XPathDocument
Public Property XElementObject As XElement Public Property XElementObject As XElement
Public Property SchemaObject As Object Public Property SchemaObject As Object
Public Property Specification As String Public Property Specification As String
@ -76,6 +75,7 @@ Public Class ZUGFeRDInterface
Public Sub New(pLogConfig As LogConfig, pGDPictureKey As String, Optional pOptions As ZugferdOptions = Nothing) Public Sub New(pLogConfig As LogConfig, pGDPictureKey As String, Optional pOptions As ZugferdOptions = Nothing)
_logConfig = pLogConfig _logConfig = pLogConfig
_logger = _logConfig.GetLogger() _logger = _logConfig.GetLogger()
_Validator = New Validator(_logConfig)
If pOptions Is Nothing Then If pOptions Is Nothing Then
_Options = New ZugferdOptions() _Options = New ZugferdOptions()
@ -138,9 +138,11 @@ Public Class ZUGFeRDInterface
Dim oResult = ValidateZUGFeRDFileWithGDPicture(Path) Dim oResult = ValidateZUGFeRDFileWithGDPicture(Path)
oResult = ValidateZUGFeRDDocument(oResult) oResult = ValidateZUGFeRDDocument(oResult)
'If IsNothing(oResult.SchemaObject) Then If oResult.ValidationErrors.Any() Then
Throw New ZUGFeRDExecption(ErrorType.NoZugferd, "Datei ist keine ZUGFeRD Datei.") Throw New ValidationException() With {
'End If .ValidationErrors = oResult.ValidationErrors
}
End If
Return SerializeZUGFeRDDocument(oResult) Return SerializeZUGFeRDDocument(oResult)
End Function End Function
@ -154,9 +156,16 @@ Public Class ZUGFeRDInterface
Dim oResult = ValidateZUGFeRDFileWithGDPicture(Stream) Dim oResult = ValidateZUGFeRDFileWithGDPicture(Stream)
oResult = ValidateZUGFeRDDocument(oResult) oResult = ValidateZUGFeRDDocument(oResult)
'If IsNothing(oResult.SchemaObject) Then If oResult.ValidationErrors.Any() Then
' Throw New ZUGFeRDExecption(ErrorType.NoZugferd, "Datei ist keine ZUGFeRD Datei.") _logger.Info("Validation found [{0}] errors", oResult.ValidationErrors.Count)
'End If oResult.ValidationErrors.ForEach(
Sub(e) _logger.Info("Field [{0}] with value [{1}] has error: [{2}]", e.ElementName, e.ElementValue, e.ErrorMessage)
)
Throw New ValidationException() With {
.ValidationErrors = oResult.ValidationErrors
}
End If
Return SerializeZUGFeRDDocument(oResult) Return SerializeZUGFeRDDocument(oResult)
End Function End Function
@ -248,22 +257,13 @@ Public Class ZUGFeRDInterface
End If End If
Try Try
Dim oXPathObject As XPathDocument = Nothing
Using oStream As New MemoryStream(oAllowedResult.FileContents) Using oStream As New MemoryStream(oAllowedResult.FileContents)
oXPathObject = New XPathDocument(oStream) Return New ZugferdResult With {
.DataFileName = oAllowedResult.FileName,
.XElementObject = XElement.Load(oStream)
}
End Using End Using
Dim oXElementObject As XElement = Nothing
Using oStream As New MemoryStream(oAllowedResult.FileContents)
oXElementObject = XElement.Load(oStream)
End Using
Return New ZugferdResult With {
.DataFileName = oAllowedResult.FileName,
.XElementObject = oXElementObject,
.XPathObject = oXPathObject
}
Catch ex As ZUGFeRDExecption Catch ex As ZUGFeRDExecption
' Don't log ZUGFeRD Exceptions here, they should be handled by the calling code. ' Don't log ZUGFeRD Exceptions here, they should be handled by the calling code.
' It also produces misleading error messages when checking if an attachment is a zugferd file. ' It also produces misleading error messages when checking if an attachment is a zugferd file.
@ -281,93 +281,11 @@ Public Class ZUGFeRDInterface
End Class End Class
Public Function ValidateZUGFeRDDocument(pResult As ZugferdResult) As ZugferdResult Public Function ValidateZUGFeRDDocument(pResult As ZugferdResult) As ZugferdResult
Dim oNavigator As XPathNavigator = pResult.XPathObject.CreateNavigator() Return _Validator.ValidateZUGFeRDDocument(pResult)
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")
Try
Dim oDecimalNodes = pResult.XElementObject.Descendants().
Where(Function(n) n.Name.ToString.EndsWith("Amount") Or n.Name.ToString.EndsWith("Percent"))
For Each oNode As XElement In oDecimalNodes
Dim oParsedValue As Decimal = 0.0
If Decimal.TryParse(oNode.Value, oParsedValue) = False Then
pResult.ValidationErrors.Add(New ZugferdValidationError() With {
.ElementName = oNode.Name.LocalName,
.ElementValue = oNode.Value,
.ErrorMessage = "Value could not be parsed as Decimal"
})
End If
Next
Catch ex As Exception
_logger.Error(ex)
End Try
' CurrencyCode Nodes
Try
Dim oCurrencyCodeNodes = pResult.XElementObject.Descendants().
Where(Function(n) n.Name.ToString.EndsWith("CurrencyCode"))
For Each oNode As XElement In oCurrencyCodeNodes
Dim oValid = ValidateCurrencyCode(oNode.Value)
If oValid = False Then
pResult.ValidationErrors.Add(New ZugferdValidationError() With {
.ElementName = oNode.Name.LocalName,
.ElementValue = oNode.Value,
.ErrorMessage = "Invalid CurrencyCode. Only 3-Character codes are allowed."
})
End If
Next
Catch ex As Exception
_logger.Error(ex)
End Try
' currencyID
Try
Dim oCurrencyIDNodes = pResult.XElementObject.Descendants().
Where(Function(n) n.Attributes.Any(Function(a) a.Name.LocalName = "currencyID"))
For Each oNode As XElement In oCurrencyIDNodes
Dim oCurrencyID As String = oNode.Attribute("currencyID")?.Value
' CurrencyID is optional per spec
If String.IsNullOrWhiteSpace(oCurrencyID) Then
Continue For
End If
Dim oValid = ValidateCurrencyCode(oCurrencyID)
If oValid = False Then
pResult.ValidationErrors.Add(New ZugferdValidationError() With {
.ElementName = oNode.Name.LocalName,
.ElementValue = oCurrencyID,
.ErrorMessage = "Invalid currencyID. Only 3-Character codes or empty values are allowed."
})
End If
Next
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 End Function
Public Function SerializeZUGFeRDDocument(pResult As ZugferdResult) As ZugferdResult Public Function SerializeZUGFeRDDocument(pResult As ZugferdResult) As ZugferdResult
Try Try
Dim oNavigator As XPathNavigator = pResult.XPathObject.CreateNavigator()
Dim oReader As XmlReader Dim oReader As XmlReader
Dim oObject As Object = Nothing Dim oObject As Object = Nothing
@ -405,8 +323,7 @@ Public Class ZUGFeRDInterface
_logger.Debug("Trying Type [{0}]", oTypeName) _logger.Debug("Trying Type [{0}]", oTypeName)
Try Try
oReader = oNavigator.ReadSubtree() oReader = pResult.XElementObject.CreateReader()
oObject = oSerializer.Deserialize(oReader) oObject = oSerializer.Deserialize(oReader)
oSpecification = oType.Specification oSpecification = oType.Specification

View File

@ -1,4 +1,6 @@
Public Class Exceptions Imports DigitalData.Modules.Interfaces.ZUGFeRDInterface
Public Class Exceptions
Public Class ZUGFeRDExecption Public Class ZUGFeRDExecption
Inherits ApplicationException Inherits ApplicationException
@ -23,4 +25,14 @@
_XmlFile = pXmlFileName _XmlFile = pXmlFileName
End Sub End Sub
End Class 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 End Class

View File

@ -0,0 +1,98 @@
Imports DigitalData.Modules.Interfaces.ZUGFeRDInterface
Imports DigitalData.Modules.Logging
Public Class Validator
Private ReadOnly _logConfig As LogConfig
Private ReadOnly _logger As Logger
Public Sub New(pLogConfig As LogConfig)
_logConfig = pLogConfig
_logger = pLogConfig.GetLogger()
End Sub
Public Function ValidateZUGFeRDDocument(pResult As ZugferdResult) As ZugferdResult
ValidateDecimalNodes(pResult)
ValidateCurrencyNodes(pResult)
Return pResult
End Function
Private Sub ValidateDecimalNodes(ByRef pResult As ZugferdResult)
Try
Dim oDecimalNodes = pResult.XElementObject.Descendants().
Where(Function(n) n.Name.ToString.EndsWith("Amount") Or n.Name.ToString.EndsWith("Percent"))
For Each oNode As XElement In oDecimalNodes
Dim oParsedValue As Decimal = 0.0
If Decimal.TryParse(oNode.Value, oParsedValue) = False Then
pResult.ValidationErrors.Add(New ZugferdValidationError() With {
.ElementName = oNode.Name.LocalName,
.ElementValue = oNode.Value,
.ErrorMessage = "Value could not be parsed as Decimal"
})
End If
Next
Catch ex As Exception
_logger.Error(ex)
End Try
End Sub
Private Sub ValidateCurrencyNodes(ByRef pResult As ZugferdResult)
' CurrencyCode Nodes
Try
Dim oCurrencyCodeNodes = pResult.XElementObject.Descendants().
Where(Function(n) n.Name.ToString.EndsWith("CurrencyCode"))
For Each oNode As XElement In oCurrencyCodeNodes
Dim oValid = ValidateCurrencyCode(oNode.Value)
If oValid = False Then
pResult.ValidationErrors.Add(New ZugferdValidationError() With {
.ElementName = oNode.Name.LocalName,
.ElementValue = oNode.Value,
.ErrorMessage = "Invalid CurrencyCode. Only 3-Character codes are allowed."
})
End If
Next
Catch ex As Exception
_logger.Error(ex)
End Try
' currencyID
Try
Dim oCurrencyIDNodes = pResult.XElementObject.Descendants().
Where(Function(n) n.Attributes.Any(Function(a) a.Name.LocalName = "currencyID"))
For Each oNode As XElement In oCurrencyIDNodes
Dim oCurrencyID As String = oNode.Attribute("currencyID")?.Value
' CurrencyID is optional per spec
If String.IsNullOrWhiteSpace(oCurrencyID) Then
Continue For
End If
Dim oValid = ValidateCurrencyCode(oCurrencyID)
If oValid = False Then
pResult.ValidationErrors.Add(New ZugferdValidationError() With {
.ElementName = oNode.Name.LocalName,
.ElementValue = oCurrencyID,
.ErrorMessage = "Invalid currencyID. Only 3-Character codes or empty values are allowed."
})
End If
Next
Catch ex As Exception
_logger.Error(ex)
End Try
End Sub
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
End Class

View File

@ -77,14 +77,4 @@ Public Class Exceptions
MyBase.New(pInfo) MyBase.New(pInfo)
End Sub End Sub
End Class 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 End Class

View File

@ -255,6 +255,9 @@ Public Class ImportZUGFeRDFiles
Try Try
oDocument = _zugferd.ExtractZUGFeRDFileWithGDPicture(oFile.FullName) oDocument = _zugferd.ExtractZUGFeRDFileWithGDPicture(oFile.FullName)
Catch ex As ValidationException
Throw ex
Catch ex As ZUGFeRDExecption Catch ex As ZUGFeRDExecption
Select Case ex.ErrorType Select Case ex.ErrorType
Case ZUGFeRDInterface.ErrorType.NoZugferd Case ZUGFeRDInterface.ErrorType.NoZugferd
@ -267,7 +270,7 @@ Public Class ImportZUGFeRDFiles
Throw New UnsupportedFerdException(ex.XmlFile) Throw New UnsupportedFerdException(ex.XmlFile)
Case ZUGFeRDInterface.ErrorType.NoValidZugferd Case ZUGFeRDInterface.ErrorType.NoValidZugferd
_logger.Warn("File [{0}] is an Incorrectly formatted ZUGFeRD document!", oFile.Name) _logger.Info("File [{0}] is an Incorrectly formatted ZUGFeRD document!", oFile.Name)
Throw New InvalidFerdException() Throw New InvalidFerdException()
Case Else Case Else
@ -276,17 +279,6 @@ Public Class ImportZUGFeRDFiles
End Select End Select
End Try 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`. ' 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. ' 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`. ' Right now the zugferd-invoice.xml is filtered out because `AllowedExtensions` does not contain `xml`.