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>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<Compile Include="ZUGFeRDInterface\Validator.vb" />
<Compile Include="ZUGFeRDInterface\Version1.0\CrossIndustryDocumentType.vb" />
<Compile Include="ZUGFeRDInterface.vb" />
<Compile Include="ZUGFeRDInterface\FileGroups.vb" />

View File

@ -15,6 +15,7 @@ Public Class ZUGFeRDInterface
Private ReadOnly _logConfig As LogConfig
Private ReadOnly _logger As Logger
Private ReadOnly _Options As ZugferdOptions
Private ReadOnly _Validator As Validator
' These constants define the specification markers for the different
' zugferd document schema versions. These markers need to be used to
@ -39,7 +40,6 @@ Public Class ZUGFeRDInterface
UnsupportedFormat
FileTooBig
UnknownError
ValidationFailed
End Enum
Public ReadOnly Property FileGroup As FileGroups
@ -54,7 +54,6 @@ Public Class ZUGFeRDInterface
Public Class ZugferdResult
Public Property DataFileName As String
Public Property XPathObject As XPathDocument
Public Property XElementObject As XElement
Public Property SchemaObject As Object
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)
_logConfig = pLogConfig
_logger = _logConfig.GetLogger()
_Validator = New Validator(_logConfig)
If pOptions Is Nothing Then
_Options = New ZugferdOptions()
@ -138,9 +138,11 @@ Public Class ZUGFeRDInterface
Dim oResult = ValidateZUGFeRDFileWithGDPicture(Path)
oResult = ValidateZUGFeRDDocument(oResult)
'If IsNothing(oResult.SchemaObject) Then
Throw New ZUGFeRDExecption(ErrorType.NoZugferd, "Datei ist keine ZUGFeRD Datei.")
'End If
If oResult.ValidationErrors.Any() Then
Throw New ValidationException() With {
.ValidationErrors = oResult.ValidationErrors
}
End If
Return SerializeZUGFeRDDocument(oResult)
End Function
@ -154,9 +156,16 @@ Public Class ZUGFeRDInterface
Dim oResult = ValidateZUGFeRDFileWithGDPicture(Stream)
oResult = ValidateZUGFeRDDocument(oResult)
'If IsNothing(oResult.SchemaObject) Then
' Throw New ZUGFeRDExecption(ErrorType.NoZugferd, "Datei ist keine ZUGFeRD Datei.")
'End If
If oResult.ValidationErrors.Any() Then
_logger.Info("Validation found [{0}] errors", oResult.ValidationErrors.Count)
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)
End Function
@ -248,22 +257,13 @@ Public Class ZUGFeRDInterface
End If
Try
Dim oXPathObject As XPathDocument = Nothing
Using oStream As New MemoryStream(oAllowedResult.FileContents)
oXPathObject = New XPathDocument(oStream)
Return New ZugferdResult With {
.DataFileName = oAllowedResult.FileName,
.XElementObject = XElement.Load(oStream)
}
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
' 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.
@ -281,93 +281,11 @@ Public Class ZUGFeRDInterface
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")
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
Return _Validator.ValidateZUGFeRDDocument(pResult)
End Function
Public Function SerializeZUGFeRDDocument(pResult As ZugferdResult) As ZugferdResult
Try
Dim oNavigator As XPathNavigator = pResult.XPathObject.CreateNavigator()
Dim oReader As XmlReader
Dim oObject As Object = Nothing
@ -405,8 +323,7 @@ Public Class ZUGFeRDInterface
_logger.Debug("Trying Type [{0}]", oTypeName)
Try
oReader = oNavigator.ReadSubtree()
oReader = pResult.XElementObject.CreateReader()
oObject = oSerializer.Deserialize(oReader)
oSpecification = oType.Specification

View File

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

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)
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

View File

@ -255,6 +255,9 @@ Public Class ImportZUGFeRDFiles
Try
oDocument = _zugferd.ExtractZUGFeRDFileWithGDPicture(oFile.FullName)
Catch ex As ValidationException
Throw ex
Catch ex As ZUGFeRDExecption
Select Case ex.ErrorType
Case ZUGFeRDInterface.ErrorType.NoZugferd
@ -267,7 +270,7 @@ Public Class ImportZUGFeRDFiles
Throw New UnsupportedFerdException(ex.XmlFile)
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()
Case Else
@ -276,17 +279,6 @@ 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`.