From befe6a753b44081a601f765e8b60ecd53f2f5dd1 Mon Sep 17 00:00:00 2001
From: Lorenz Stechauner <lorenz.stechauner@necronda.net>
Date: Wed, 31 Jan 2024 16:54:51 +0100
Subject: [PATCH] Export/Ebics: Add Tests to validate against schemas and fix
 issues

---
 Elwig/Helpers/Export/Ebics.cs               |  23 ++--
 Elwig/Models/Dtos/Transaction.cs            |   3 +-
 Elwig/Windows/PaymentVariantsWindow.xaml.cs |   8 +-
 Tests/HelperTests/EbicsTest.cs              | 113 ++++++++++++++++++++
 4 files changed, 133 insertions(+), 14 deletions(-)
 create mode 100644 Tests/HelperTests/EbicsTest.cs

diff --git a/Elwig/Helpers/Export/Ebics.cs b/Elwig/Helpers/Export/Ebics.cs
index 8abb5b5..eeb15c7 100644
--- a/Elwig/Helpers/Export/Ebics.cs
+++ b/Elwig/Helpers/Export/Ebics.cs
@@ -2,12 +2,13 @@ using Elwig.Models.Dtos;
 using Elwig.Models.Entities;
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Threading.Tasks;
 
 namespace Elwig.Helpers.Export {
-    public class Ebics(PaymentVar variant, string filename) : IBankingExporter {
+    public class Ebics(PaymentVar variant, string filename, int version) : IBankingExporter {
 
         public static string FileExtension => "xml";
 
@@ -16,6 +17,7 @@ namespace Elwig.Helpers.Export {
         private readonly int Year = variant.Year;
         private readonly string Name = variant.Name;
         private readonly int AvNr = variant.AvNr;
+        private readonly int Version = version;
 
         public void Dispose() {
             GC.SuppressFinalize(this);
@@ -32,6 +34,8 @@ namespace Elwig.Helpers.Export {
         }
 
         public async Task ExportAsync(IEnumerable<Transaction> transactions, IProgress<double>? progress = null) {
+            if (transactions.Any(tx => tx.Amount < 0))
+                throw new ArgumentException("Tranaction amount may not be negative");
             progress?.Report(0.0);
             var nbOfTxs = transactions.Count();
             int count = nbOfTxs + 2, i = 0;
@@ -41,9 +45,7 @@ namespace Elwig.Helpers.Export {
 
             await Writer.WriteLineAsync($"""
                 <?xml version="1.0" encoding="UTF-8"?>
-                <Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09"
-                          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-                          xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09 pain.001.001.09.xsd">
+                <Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.{Version:00}">
                  <CstmrCdtTrfInitn>
                   <GrpHdr>
                    <MsgId>{msgId}</MsgId>
@@ -57,10 +59,10 @@ namespace Elwig.Helpers.Export {
                    <PmtMtd>TRF</PmtMtd>
                    <NbOfTxs>{nbOfTxs}</NbOfTxs>
                    <CtrlSum>{Transaction.FormatAmount(ctrlSum)}</CtrlSum>
-                   <ReqdExctnDt><Dt>{Date:yyyy-MM-dd}</Dt></ReqdExctnDt>
+                   <ReqdExctnDt>{(Version >= 8 ? "<Dt>" : "")}{Date:yyyy-MM-dd}{(Version >= 8 ? "</Dt>" : "")}</ReqdExctnDt>
                    <Dbtr><Nm>{App.Client.NameFull}</Nm></Dbtr>
-                   <DbtrAcct><Id><IBAN>{App.Client.Iban?.Replace(" ", "")}</IBAN></Id></DbtrAcct>
-                   <DbtrAgt><FinInstnId><BICFI>{App.Client.Bic ?? "NOTPROVIDED"}</BICFI></FinInstnId></DbtrAgt>
+                   <DbtrAcct><Id><IBAN>{App.Client.Iban!.Replace(" ", "")}</IBAN></Id></DbtrAcct>
+                   <DbtrAgt><FinInstnId>{(Version >= 4 ? "<BICFI>" : "<BIC>")}{App.Client.Bic ?? "NOTPROVIDED"}{(Version >= 4 ? "</BICFI>" : "</BIC>")}</FinInstnId></DbtrAgt>
                 """);
             progress?.Report(100.0 * ++i / count);
 
@@ -74,15 +76,14 @@ namespace Elwig.Helpers.Export {
                         <PmtId><EndToEndId>{id}</EndToEndId></PmtId>
                         <Amt><InstdAmt Ccy="{tx.Currency}">{Transaction.FormatAmount(tx.Amount)}</InstdAmt></Amt>
                         <Cdtr>
-                         <Nm>{a.Name}</Nm>
+                         <Nm>{a.Name[..Math.Min(140, a.Name.Length)]}</Nm>
                          <PstlAdr>
-                          <StrtNm>{a1}</StrtNm><BldgNb>{a2}</BldgNb>
+                          <StrtNm>{a1?[..Math.Min(70, a1.Length)]}</StrtNm><BldgNb>{a2?[..Math.Min(16, a2.Length)]}</BldgNb>
                           <PstCd>{a.PostalDest.AtPlz?.Plz}</PstCd><TwnNm>{a.PostalDest.AtPlz?.Ort.Name}</TwnNm>
                           <Ctry>{a.PostalDest.Country.Alpha2}</Ctry>
                          </PstlAdr>
                         </Cdtr>
-                        <CdtrAcct><Id><IBAN>{tx.Member.Iban}</IBAN></Id></CdtrAcct>
-                        <CdtrAgt><FinInstnId><BICFI>{tx.Member.Bic ?? "NOTPROVIDED"}</BICFI></FinInstnId></CdtrAgt>
+                        <CdtrAcct><Id><IBAN>{tx.Member.Iban!}</IBAN></Id></CdtrAcct>
                         <RmtInf><Ustrd>{info}</Ustrd></RmtInf>
                        </CdtTrfTxInf>
                     """);
diff --git a/Elwig/Models/Dtos/Transaction.cs b/Elwig/Models/Dtos/Transaction.cs
index 860bc39..4cac4bf 100644
--- a/Elwig/Models/Dtos/Transaction.cs
+++ b/Elwig/Models/Dtos/Transaction.cs
@@ -1,4 +1,5 @@
 using Elwig.Models.Entities;
+using System;
 using System.Collections.Generic;
 using System.Linq;
 
@@ -19,7 +20,7 @@ namespace Elwig.Models.Dtos {
                 .ToList();
         }
 
-        public static string FormatAmountCent(long cents) => $"{cents / 100}.{cents % 100:00}";
+        public static string FormatAmountCent(long cents) => $"{cents / 100}.{Math.Abs(cents % 100):00}";
 
         public static string FormatAmount(decimal amount) => FormatAmountCent((int)(amount * 100));
     }
diff --git a/Elwig/Windows/PaymentVariantsWindow.xaml.cs b/Elwig/Windows/PaymentVariantsWindow.xaml.cs
index 50d7dd4..479a775 100644
--- a/Elwig/Windows/PaymentVariantsWindow.xaml.cs
+++ b/Elwig/Windows/PaymentVariantsWindow.xaml.cs
@@ -307,8 +307,12 @@ namespace Elwig.Windows {
             if (d.ShowDialog() == true) {
                 ExportButton.IsEnabled = false;
                 Mouse.OverrideCursor = Cursors.AppStarting;
-                using var e = new Ebics(v, d.FileName);
-                await e.ExportAsync(Transaction.FromPaymentVariant(v));
+                try {
+                    using var e = new Ebics(v, d.FileName, 9);
+                    await e.ExportAsync(Transaction.FromPaymentVariant(v));
+                } catch (Exception exc) {
+                    MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
+                }
                 Mouse.OverrideCursor = null;
                 ExportButton.IsEnabled = true;
             }
diff --git a/Tests/HelperTests/EbicsTest.cs b/Tests/HelperTests/EbicsTest.cs
new file mode 100644
index 0000000..d84280b
--- /dev/null
+++ b/Tests/HelperTests/EbicsTest.cs
@@ -0,0 +1,113 @@
+using Elwig.Helpers;
+using Elwig.Helpers.Export;
+using Elwig.Models.Dtos;
+using Elwig.Models.Entities;
+using System.Reflection;
+using System.Xml;
+
+namespace Tests.HelperTests {
+    // see https://www.iso20022.org/iso-20022-message-definitions
+    // and https://www.iso20022.org/catalogue-messages/iso-20022-messages-archive?search=pain
+    [TestFixture]
+    public class EbicsTest {
+
+        public static readonly string FileName = Path.Combine(Path.GetTempPath(), "test_ebics.xml");
+        public static readonly string Iban = "AT123456789012345678";
+
+        private static void ValidateSchema(string xmlPath, int version) {
+            XmlDocument xml = new();
+            xml.Load(xmlPath);
+            var schema = new XmlTextReader(Assembly.GetExecutingAssembly()
+                .GetManifestResourceStream($"Tests.Resources.Schemas.pain.001.001.{version:00}.xsd")!);
+            xml.Schemas.Add(null, schema);
+            xml.Validate(null);
+        }
+
+        private static async Task CreateXmlFile(int version) {
+            var v = new PaymentVar() {
+                Year = 2020,
+                AvNr = 1,
+                Name = "Endauszahlung",
+                TransferDate = new DateOnly(2021, 6, 15),
+            };
+            using var ctx = new AppDbContext();
+            var members = ctx.Members.ToList();
+            Assert.That(members, Has.Count.GreaterThan(0));
+            using var exporter = new Ebics(v, FileName, version);
+            await exporter.ExportAsync(members.Select(m => new Transaction(m, 1234.56m, "EUR", m.MgNr % 100)));
+        }
+
+        [TearDown]
+        public static void RemoveXmlFile() {
+            File.Delete(FileName);
+        }
+
+        [Test]
+        [Ignore("Version has no need to be supported")]
+        public async Task Test_CustomerCreditTransferInitiationV01() {
+            await CreateXmlFile(1);
+            Assert.DoesNotThrow(() => ValidateSchema(FileName, 1));
+        }
+
+        [Test]
+        [Ignore("Version has no need to be supported")]
+        public async Task Test_CustomerCreditTransferInitiationV02() {
+            await CreateXmlFile(2);
+            Assert.DoesNotThrow(() => ValidateSchema(FileName, 2));
+        }
+
+        [Test]
+        public async Task Test_CustomerCreditTransferInitiationV03() {
+            await CreateXmlFile(3);
+            Assert.DoesNotThrow(() => ValidateSchema(FileName, 3));
+        }
+
+        [Test]
+        public async Task Test_CustomerCreditTransferInitiationV04() {
+            await CreateXmlFile(4);
+            Assert.DoesNotThrow(() => ValidateSchema(FileName, 4));
+        }
+
+        [Test]
+        public async Task Test_CustomerCreditTransferInitiationV05() {
+            await CreateXmlFile(5);
+            Assert.DoesNotThrow(() => ValidateSchema(FileName, 5));
+        }
+
+        [Test]
+        public async Task Test_CustomerCreditTransferInitiationV06() {
+            await CreateXmlFile(6);
+            Assert.DoesNotThrow(() => ValidateSchema(FileName, 6));
+        }
+
+        [Test]
+        public async Task Test_CustomerCreditTransferInitiationV07() {
+            await CreateXmlFile(7);
+            Assert.DoesNotThrow(() => ValidateSchema(FileName, 7));
+        }
+
+        [Test]
+        public async Task Test_CustomerCreditTransferInitiationV08() {
+            await CreateXmlFile(8);
+            Assert.DoesNotThrow(() => ValidateSchema(FileName, 8));
+        }
+
+        [Test]
+        public async Task Test_CustomerCreditTransferInitiationV09() {
+            await CreateXmlFile(9);
+            Assert.DoesNotThrow(() => ValidateSchema(FileName, 9));
+        }
+
+        [Test]
+        public async Task Test_CustomerCreditTransferInitiationV10() {
+            await CreateXmlFile(10);
+            Assert.DoesNotThrow(() => ValidateSchema(FileName, 10));
+        }
+
+        [Test]
+        public async Task Test_CustomerCreditTransferInitiationV11() {
+            await CreateXmlFile(11);
+            Assert.DoesNotThrow(() => ValidateSchema(FileName, 11));
+        }
+    }
+}