using System; using System.IO; using System.Linq; using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using DigitalData.Modules.Interfaces; using static DigitalData.Modules.Interfaces.Exceptions; using static DigitalData.Modules.Interfaces.ZUGFeRDInterface; using static DigitalData.Modules.Interfaces.PropertyValues; using System.Data.SqlClient; using Microsoft.Extensions.Configuration; using System.Xml.Linq; using Newtonsoft.Json.Linq; namespace ZUGFeRDRESTService.Controllers { [Route("api/[controller]")] [ApiController] public class ValidationController : ControllerBase { public const string RESPONSE_OK = "OK"; public const string RESPONSE_ERROR = "ERROR"; public const string ADDED_WHO = "ZUGFeRD REST Service"; public const string MESSAGEID_DOMAIN = "test.wisag.de"; private const int MAX_FILE_SIZE_DEFAULT = 25; private readonly ZUGFeRDInterface _zugferd; private readonly IDatabase _database; private readonly DigitalData.Modules.Logging.LogConfig _logConfig; private readonly DigitalData.Modules.Logging.Logger _logger; private readonly DigitalData.Modules.Filesystem.File _file; private readonly PropertyValues _props; private readonly Dictionary _propertyMap = new Dictionary(); private int _MaxFileSizeInMegabytes; private bool _AllowFacturX; private bool _AllowXRechnung; private bool _AllowZugferd2x; private bool _AllowZugferd10; private bool _AllowPeppolBISBill3x; private string _GDPictureVersion; public ValidationController(ILogging logging, IDatabase database, IConfiguration Config) { _logConfig = logging.LogConfig; _logger = _logConfig.GetLogger(); _file = new DigitalData.Modules.Filesystem.File(_logConfig); _logger.Debug("Validation Controller initializing"); // Read config file and assign all option flags related to // - Zugferd files // - Filesizes ParseConfig(Config); _database = database; var oGDPictureKey = database.GetGDPictureKey(); var oPropertyMap = database.GetPropertyMap(); _zugferd = new ZUGFeRDInterface(_logConfig, oGDPictureKey, new ZugferdOptions() { AllowFacturX_Filename = _AllowFacturX, AllowXRechnung_Filename = _AllowXRechnung, AllowZugferd_1_0_Schema = _AllowZugferd10, AllowZugferd_2_x_Schema = _AllowZugferd2x, AllowPeppol_3_x_Schema = _AllowPeppolBISBill3x }); _props = new PropertyValues(_logConfig); _logger.Debug("Property Map initial: [{0}] entries found.", oPropertyMap.Count); if (_AllowZugferd10 == true) _propertyMap = oPropertyMap. Where(kv => kv.Value.Specification == ZUGFERD_SPEC_10 || kv.Value.Specification == ZUGFERD_SPEC_DEFAULT). Concat(_propertyMap). ToDictionary(kv => kv.Key, kv => kv.Value); if (_AllowZugferd2x == true) _propertyMap = oPropertyMap. Where(kv => kv.Value.Specification == ZUGFERD_SPEC_2x). Concat(_propertyMap). ToDictionary(kv => kv.Key, kv => kv.Value); if (_AllowPeppolBISBill3x == true) _propertyMap = oPropertyMap. Where(kv => kv.Value.Specification == UBL_SPEC_21). Concat(_propertyMap). ToDictionary(kv => kv.Key, kv => kv.Value); _logger.Debug("Property Map filtered: [{0}] entries found.", _propertyMap.Count); _logger.Debug("Validation Controller initialized!"); } private void ParseConfig(IConfiguration Config) { var oAppConfig = Config.GetSection("Config"); var oZugferdConfig = oAppConfig.GetSection("Zugferd"); if (!bool.TryParse(oZugferdConfig["AllowFacturX"], out _AllowFacturX)) { _logger.Info("Configuration AllowFacturX was not set. Using default value [{0}]", false); _AllowFacturX = false; } if (!bool.TryParse(oZugferdConfig["AllowXRechnung"], out _AllowXRechnung)) { _logger.Info("Configuration AllowXRechnung was not set. Using default value [{0}]", false); _AllowXRechnung = false; } if (!bool.TryParse(oZugferdConfig["AllowZugferd2x"], out _AllowZugferd2x)) { _logger.Info("Configuration Zugferd2x was not set. Using default value [{0}]", false); _AllowZugferd2x = false; } if (!bool.TryParse(oZugferdConfig["AllowZugferd10"], out _AllowZugferd10)) { _logger.Info("Configuration Zugferd10 was not set. Using default value [{0}]", true); _AllowZugferd10 = true; } if (!bool.TryParse(oZugferdConfig["AllowPeppolBISBill3x"], out _AllowPeppolBISBill3x)) { _logger.Info("Configuration AllowPeppolBISBill3x was not set. Using default value [{0}]", false); _AllowPeppolBISBill3x = false; } _GDPictureVersion = oAppConfig["GDPictureVersion"]; if (string.IsNullOrEmpty(_GDPictureVersion)) { _logger.Info("Configuration GDPictureVersion was not set. Using default value [string.Empty]"); _GDPictureVersion = string.Empty; } if (!int.TryParse(oAppConfig["MaxFileSizeInMegabytes"], out _MaxFileSizeInMegabytes)) { _logger.Info("Configuration MaxFileSizeInMegabytes was not set. Using default value [{0}]", MAX_FILE_SIZE_DEFAULT); _MaxFileSizeInMegabytes = MAX_FILE_SIZE_DEFAULT; } } /// /// POST: /api/validation /// /// This parameter's name needs to correspond to the html form's file-input name /// This is the email address which the user supplied [HttpPost] public ValidationResponse Post(IFormFile file, string user_id) { _logger.Info("Start processing request to ValidationController"); ZugferdResult oZugferdResult = null; CheckPropertyValuesResult oPropertyResult = new CheckPropertyValuesResult(); try { using Stream oStream = file.OpenReadStream(); { _logger.Info("Checking Filesize of file [{0}]", file.FileName); long maxFileSize = _MaxFileSizeInMegabytes * 1024 * 1024; bool oFileSizeIsOK = file.Length < maxFileSize; if (oFileSizeIsOK == false) { throw new ZUGFeRDExecption(ErrorType.FileTooBig, "FileTooBig"); } _logger.Info("Extracting ZUGFeRD Data from file [{0}]", file.FileName); if (file.FileName.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)) { oZugferdResult = _zugferd.ExtractZUGFeRDFileWithGDPicture(oStream); } else if (file.FileName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) { // Für die Verarbeitung von XML-Belegen var oResult = new ZugferdResult() { DataFileName = file.FileName, XElementObject = XElement.Load(oStream) }; oZugferdResult = _zugferd.SerializeZUGFeRDDocument(oResult); } _logger.Info("Detected Specification was: [{0}]", oZugferdResult.Specification); var oFilteredPropertyMap = _zugferd.FilterPropertyMap(_propertyMap, oZugferdResult.Specification); if (oFilteredPropertyMap.Count == 0) { _logger.Warn("No properties found in property map for specification [{0}]", oZugferdResult.Specification); throw new ZUGFeRDExecption(ErrorType.UnsupportedFormat, "Unsupported Format"); } else { _logger.Debug("Property map contains [{0}] entries for specification [{1}]", oFilteredPropertyMap.Count, oZugferdResult.Specification); } _logger.Info("Starting structural check against the database."); oPropertyResult = _props.CheckPropertyValues(oZugferdResult.SchemaObject, oFilteredPropertyMap, "MESSAGEID"); var oRequiredProperties = oFilteredPropertyMap. Where(prop => { return prop.Value.IsRequired == true; }). Select(prop => { return prop.Value.Description; }). ToList(); _logger.Debug("Found [{0}] required properties", oRequiredProperties.Count); _logger.Debug(string.Join(",", oRequiredProperties.ToArray())); _logger.Info("Result of checking against the database: {0} valid properties, {1} missing properties", oPropertyResult.ValidProperties.Count, oPropertyResult.MissingProperties.Count); if (oPropertyResult.MissingProperties.Count > 0) { throw new ZUGFeRDExecption(ErrorType.MissingProperties, "Missing Properties"); } Tuple oValidateResult = ValidateBuyerOrderReference(oPropertyResult.ValidProperties); if (oValidateResult.Item1 == false) { throw new ZUGFeRDExecption(ErrorType.UnknownError, "Unknown Error"); } string oValidateResultString = oValidateResult.Item2; if (oValidateResultString == "ALL REFERENCES CHECKED POSITIVE") { string oMessage = "Die hochgeladene Datei ist eine gültige-ZUGFeRD Rechnung"; _logger.Info($"Responding with message: [{oMessage}]"); return new ValidationResponse() { status = RESPONSE_OK, message = oMessage }; } else { string oMessage = oValidateResultString; _logger.Info($"Responding with message: [{oMessage}]"); return new ValidationResponse() { status = RESPONSE_ERROR, message = oMessage }; } }; } catch (ZUGFeRDExecption ex) { _logger.Error(ex); // Determine which message should be sent in the response string oMessage = ex.ErrorType switch { ErrorType.NoValidFile => "Die hochgeladene Datei ist keine gültige Datei.", ErrorType.NoZugferd => "Die hochgeladene Datei ist keine ZUGFeRD-Rechnung.", ErrorType.NoValidZugferd => "Die hochgeladene Datei ist keine gültige ZUGFeRD-Rechnung.", ErrorType.MissingProperties => "Die hochgeladene Datei ist keine gültige ZUGFeRD-Rechnung, es fehlen einige Metadaten.", ErrorType.FileTooBig => string.Format("Die hochgeladene Datei überschreitet die zulässige Dateigröße [{0}].", _MaxFileSizeInMegabytes), ErrorType.UnsupportedFormat => "Die hochgeladene Datei enthält ein falsches oder nicht unterstütztes ZUGFeRD Format.", _ => "Die hochgeladene Datei kann nicht validiert werden.", }; // Determine if any errors should be sent in the response var oErrors = new List(); switch (ex.ErrorType) { case ErrorType.MissingProperties: oErrors.AddRange(from item in oPropertyResult.MissingProperties select item.Description); break; default: break; } _logger.Info($"Responding with message: [{oMessage}]"); return new ValidationResponse() { status = RESPONSE_ERROR, message = oMessage, errors = oErrors }; } catch (ValidationException ex) { string oMessage = "Die hochgeladene Datei enthält ungültige Werte."; List oErrors = ex.ValidationErrors.Select(e => { return $"Element '{e.ElementName}' mit Wert '{e.ElementValue}': {e.ErrorMessage}"; }).ToList(); return new ValidationResponse() { status = RESPONSE_ERROR, message = oMessage, errors = oErrors }; } catch (Exception ex) { _logger.Error(ex); string oMessage = "Die hochgeladene Datei kann nicht validiert werden, weil ein unbekannter Fehler aufgetreten ist."; _logger.Info($"Responding with message: [{oMessage}]"); return new ValidationResponse() { status = RESPONSE_ERROR, message = oMessage }; } } private Tuple ValidateBuyerOrderReference(List pProperties) { var oMessageId = GetMessageId(); _logger.Debug("Created new MessageId: [{0}]", oMessageId); _logger.Debug("Inserting properties into database."); foreach (var oItem in pProperties) { var oResult = InsertPropertyMap(oItem, oMessageId); if (oResult == false) { _logger.Warn("Error while inserting the Property [{0}] into the Database!", oItem.Description); } } _logger.Debug("Calling validation prodecure."); try { using SqlCommand oCommand = new SqlCommand("PRCUST_INV_CHECK_FROM_PORTAL"); oCommand.CommandType = System.Data.CommandType.StoredProcedure; oCommand.Parameters.Clear(); oCommand.Parameters.Add("@REF_GUID", System.Data.SqlDbType.VarChar, 250).Value = oMessageId; oCommand.Parameters.Add("@MSG_OUTPUT", System.Data.SqlDbType.VarChar, 500).Direction = System.Data.ParameterDirection.Output; if (_database.MSSQL.ExecuteNonQuery(oCommand)) { string oReturnValue = (string)oCommand.Parameters["@MSG_OUTPUT"].Value; return new Tuple(true, oReturnValue); } else { return new Tuple(false, string.Empty); } } catch (Exception e) { _logger.Error(e); return new Tuple(false, string.Empty); } } public string GetMessageId() { return $"{Guid.NewGuid()}@{MESSAGEID_DOMAIN}"; } public bool InsertPropertyMap(ValidProperty pProperty, string pMessageId) { try { if (pProperty.ItemType == 3) { // Wir speichern keine Attachment-Werte in die DB return true; } if (pProperty.ItemType == 0 && string.IsNullOrEmpty(pProperty.Value.ToString())) { // Leere Texte speichern wir nicht in der DB return true; } var oSql = $"INSERT INTO {pProperty.TableName} " + "(REFERENCE_GUID, ITEM_DESCRIPTION, ITEM_VALUE, CREATEDWHO, SPEC_NAME, GROUP_COUNTER, IS_REQUIRED) VALUES " + "(@REFERENCE_GUID, @ITEM_DESCRIPTION, @ITEM_VALUE, @CREATEDWHO, @SPEC_NAME, @GROUP_COUNTER, @IS_REQUIRED)"; string itemValue = string.Empty; if (pProperty.Value.Length > 900) { itemValue = pProperty.Value.Substring(1, 899); _logger.Warn("Value for field [{0}] is longer than 900 characters, will be truncated!", pProperty.TableColumn); } else { itemValue = pProperty.Value; } var oParams = new SqlParameter[] { new SqlParameter("@REFERENCE_GUID", pMessageId), new SqlParameter("@ITEM_DESCRIPTION", pProperty.Description), new SqlParameter("@ITEM_VALUE", itemValue.Replace("'", "''")), new SqlParameter("@CREATEDWHO", ADDED_WHO), new SqlParameter("@GROUP_COUNTER", pProperty.GroupCounter), new SqlParameter("@SPEC_NAME", pProperty.TableColumn), new SqlParameter("@IS_REQUIRED", pProperty.IsRequired) }; var oCommand = new SqlCommand(oSql); oCommand.Parameters.AddRange(oParams); return _database.MSSQL.ExecuteNonQuery(oCommand); } catch (Exception ex) { _logger.Error(ex); return false; } } } }