From 3035ef1719befddab14f5441fef0a56e62cefcd5 Mon Sep 17 00:00:00 2001 From: Stefan Date: Wed, 1 Jul 2026 22:06:29 +0200 Subject: [PATCH] Add ZIP-wrapped CAMT parsing and real bank file tests - Support parsing camt.052 ZIP archives in CamtParser and BankingService - Add CamtZipParserTests for ZIP parsing (single/multi/malformed/empty cases) - Add CamtRealFileTests: integration tests for real bank exports (ZIP, XML, CAMT, MT940, STA) in git-ignored test data dir - Update .gitignore and add README.md for test data usage - Copy test data files in Fuchs.Tests.csproj; add .gitkeep - Refactor BankingService CAMT mapping logic - Overhaul connection string config: use {username}/{password} tokens, resolve from secrets at runtime, update appsettings and Key Vault keys --- .gitignore | 10 +- CAMTParser/CamtParser.cs | 44 ++++++- Fuchs.Tests/CamtParserTests.cs | 159 +++++++++++++++++++++++- Fuchs.Tests/CamtRealFileTests.cs | 162 +++++++++++++++++++++++++ Fuchs.Tests/Fuchs.Tests.csproj | 9 ++ Fuchs.Tests/TestData/Banking/.gitkeep | 1 + Fuchs.Tests/TestData/Banking/README.md | 32 +++++ Fuchs/Program.cs | 39 ++++++ Fuchs/Services/BankingService.cs | 76 ++++++------ Fuchs/appsettings.Development.json | 6 +- Fuchs/appsettings.json | 14 ++- 11 files changed, 507 insertions(+), 45 deletions(-) create mode 100644 Fuchs.Tests/CamtRealFileTests.cs create mode 100644 Fuchs.Tests/TestData/Banking/.gitkeep create mode 100644 Fuchs.Tests/TestData/Banking/README.md diff --git a/.gitignore b/.gitignore index 9d8d9f9..d7baaa0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# Visual Studio +# Visual Studio .vs/ *.user *.suo @@ -38,3 +38,11 @@ secrets.cache # Secret values file (never commit) Scripts/secrets.json + +# Banking test data (sensitive real bank export files — never commit) +Fuchs.Tests/TestData/Banking/*.zip +Fuchs.Tests/TestData/Banking/*.xml +Fuchs.Tests/TestData/Banking/*.camt +Fuchs.Tests/TestData/Banking/*.sta +Fuchs.Tests/TestData/Banking/*.mt940 +Fuchs.Tests/TestData/Banking/*.txt diff --git a/CAMTParser/CamtParser.cs b/CAMTParser/CamtParser.cs index 00727b7..0adf713 100644 --- a/CAMTParser/CamtParser.cs +++ b/CAMTParser/CamtParser.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System.Globalization; +using System.IO.Compression; using System.Text; using System.Xml.Linq; @@ -32,6 +33,14 @@ public sealed class CamtParser xml.Contains("camt.05", StringComparison.OrdinalIgnoreCase) || xml.Contains("BkToCstmr", StringComparison.OrdinalIgnoreCase); + /// + /// Cheap content sniff: is this payload a ZIP archive? Some banks deliver + /// camt.052 intraday reports wrapped in a ZIP containing one or more XML files. + /// Checks for the universal PK magic bytes (0x50 0x4B). + /// + public static bool LooksLikeZip(ReadOnlySpan bytes) => + bytes.Length >= 2 && bytes[0] == 0x50 && bytes[1] == 0x4B; + public List Parse(Stream stream) { using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, leaveOpen: true); @@ -40,6 +49,39 @@ public sealed class CamtParser public List Parse(byte[] bytes) => Parse(Encoding.UTF8.GetString(bytes)); + /// + /// Parses all CAMT XML files found inside a ZIP archive and returns + /// the combined list of statements. Non-XML entries and malformed XML + /// entries are silently skipped. Used for camt.052 deliveries where the + /// bank wraps one or more intraday reports in a single ZIP file. + /// + public List ParseZip(byte[] bytes) + { + using var ms = new MemoryStream(bytes); + return ParseZip(ms); + } + + /// + public List ParseZip(Stream stream) + { + var result = new List(); + using var archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: true); + foreach (var entry in archive.Entries) + { + if (!entry.Name.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) + continue; + using var entryStream = entry.Open(); + using var buffer = new MemoryStream(); + entryStream.CopyTo(buffer); + var entryBytes = buffer.ToArray(); + if (!LooksLikeXml(entryBytes)) + continue; + try { result.AddRange(Parse(entryBytes)); } + catch (FormatException) { /* skip malformed XML entries */ } + } + return result; + } + public List Parse(string xml) { var result = new List(); diff --git a/Fuchs.Tests/CamtParserTests.cs b/Fuchs.Tests/CamtParserTests.cs index 0bf3476..d893db4 100644 --- a/Fuchs.Tests/CamtParserTests.cs +++ b/Fuchs.Tests/CamtParserTests.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Data; using System.IO; +using System.IO.Compression; using System.Linq; using System.Text; using CAMTParser; @@ -156,6 +157,120 @@ public class CamtParserTests => Assert.Empty(new CamtParser().Parse("bar")); } +/// Tests for ZIP-wrapped camt.052 parsing. +public class CamtZipParserTests +{ + // Minimal camt.052 with one credit entry. + private const string Camt052Xml = """ + + + + RPT12023-01-31T08:00:00 + + RPT-1 + DE12345678901234567890EUR + + 100.00 + CRDT +
2023-01-15
+
2023-01-15
+ + Test Sender + Test Zahlung + +
+
+
+
+ """; + + private static byte[] BuildZip(params (string name, string content)[] entries) + { + using var ms = new MemoryStream(); + using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true)) + { + foreach (var (name, content) in entries) + { + var entry = archive.CreateEntry(name); + using var s = entry.Open(); + var b = Encoding.UTF8.GetBytes(content); + s.Write(b, 0, b.Length); + } + } + return ms.ToArray(); + } + + // ── LooksLikeZip ────────────────────────────────────────────────────────── + [Fact] + public void LooksLikeZip_ZipBytes_True() + { + var zip = BuildZip(("dummy.xml", Camt052Xml)); + Assert.True(CamtParser.LooksLikeZip(zip)); + } + + [Fact] + public void LooksLikeZip_XmlBytes_False() + => Assert.False(CamtParser.LooksLikeZip(Encoding.UTF8.GetBytes(Camt052Xml))); + + [Fact] + public void LooksLikeZip_Empty_False() + => Assert.False(CamtParser.LooksLikeZip(Array.Empty())); + + // ── ParseZip ────────────────────────────────────────────────────────────── + [Fact] + public void ParseZip_SingleXmlEntry_ReturnsCamt052Statement() + { + var zip = BuildZip(("camt052.xml", Camt052Xml)); + var statements = new CamtParser().ParseZip(zip); + var stmt = Assert.Single(statements); + Assert.Equal(CamtDocumentType.Camt052, stmt.DocumentType); + Assert.Equal("DE12345678901234567890", stmt.AccountIdentification); + Assert.Single(stmt.Entries); + Assert.Equal(100m, stmt.Entries[0].Amount); + Assert.Equal(CamtDebitCreditMark.Credit, stmt.Entries[0].Mark); + Assert.Equal("Test Sender", stmt.Entries[0].CounterpartyName); + } + + [Fact] + public void ParseZip_MultipleXmlEntries_MergesAllStatements() + { + var zip = BuildZip( + ("report_a.xml", Camt052Xml), + ("report_b.xml", Camt052Xml)); + var statements = new CamtParser().ParseZip(zip); + Assert.Equal(2, statements.Count); + Assert.All(statements, s => Assert.Equal(CamtDocumentType.Camt052, s.DocumentType)); + } + + [Fact] + public void ParseZip_NonXmlEntriesSkipped_NoException() + { + var zip = BuildZip( + ("readme.txt", "This is not XML"), + ("camt052.xml", Camt052Xml)); + var statements = new CamtParser().ParseZip(zip); + Assert.Single(statements); + } + + [Fact] + public void ParseZip_EmptyZip_ReturnsEmpty() + { + var zip = BuildZip(); + Assert.Empty(new CamtParser().ParseZip(zip)); + } + + [Fact] + public void ParseZip_MalformedXmlEntry_SkippedGracefully() + { + var zip = BuildZip( + ("broken.xml", "BankingService routing: CAMT and MT940 both land in the same DataTable. public class BankingDualFormatTests { @@ -220,4 +335,46 @@ public class BankingDualFormatTests Assert.Equal(2, t.Columns.Count); Assert.Equal(1, t.Rows.Count); } + + [Fact] + public void ParseToDatatable_CamtZip_RoutesToCamtZipAndMapsColumns() + { + const string camt052Xml = """ + + + + RPT12023-01-31T08:00:00 + + DE12345678901234567890EUR + + 250.00CRDT +
2023-01-15
+ + ZIP Sender + ZIP Zahlung + +
+
+
+
+ """; + + using var ms = new MemoryStream(); + using (var archive = new System.IO.Compression.ZipArchive(ms, System.IO.Compression.ZipArchiveMode.Create, leaveOpen: true)) + { + var entry = archive.CreateEntry("camt052.xml"); + using var es = entry.Open(); + var b = Encoding.UTF8.GetBytes(camt052Xml); + es.Write(b, 0, b.Length); + } + ms.Position = 0; + + var t = Svc.ParseToDatatable(ms); + Assert.Equal(1, t.Rows.Count); + Assert.Equal("DE12345678901234567890", t.Rows[0]["AccountIdentification"]); + Assert.Equal(250.00m, t.Rows[0]["Amount"]); + Assert.Equal("C", t.Rows[0]["DebitCreditMark"]); + Assert.Equal("ZIP Sender", t.Rows[0]["NameOfPayer"]); + Assert.Equal("ZIP Zahlung", t.Rows[0]["SepaRemittanceInformation"]); + } } diff --git a/Fuchs.Tests/CamtRealFileTests.cs b/Fuchs.Tests/CamtRealFileTests.cs new file mode 100644 index 0000000..e0b5326 --- /dev/null +++ b/Fuchs.Tests/CamtRealFileTests.cs @@ -0,0 +1,162 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using CAMTParser; +using Fuchs.Services; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Fuchs.Tests; + +/// +/// Smoke tests against real bank export files placed under TestData/Banking/. +/// The files are git-ignored (sensitive financial data — see README.md there). +/// +/// Placement: +/// .zip → camt.052 archive from the bank (contains one or more XML reports) +/// .xml → camt.053 (or camt.052/054) plain XML +/// +/// When no files are present the tests pass trivially (loop body never runs). +/// Once a file is placed and the project is rebuilt it is automatically tested. +/// +/// Note: xUnit 2.x treats [Theory] with no data cases as a failure, so these +/// tests use [Fact] + a loop over the file discovery results instead. +/// +public class CamtRealFileTests +{ + private static readonly BankingService Svc = new(NullLogger.Instance); + private const string TestDataDir = "TestData/Banking"; + + // ── File discovery ──────────────────────────────────────────────────────── + + private static IEnumerable ZipFiles() + => FilesWithExtension(".zip"); + + private static IEnumerable XmlFiles() + => FilesWithExtension(".xml", ".camt"); + + private static IEnumerable AllBankingFiles() + => FilesWithExtension(".zip", ".xml", ".camt", ".sta", ".mt940"); + + private static IEnumerable FilesWithExtension(params string[] extensions) + { + if (!Directory.Exists(TestDataDir)) + return Enumerable.Empty(); + return Directory.EnumerateFiles(TestDataDir) + .Where(f => extensions.Any(ext => + f.EndsWith(ext, System.StringComparison.OrdinalIgnoreCase))); + } + + // ── ZIP files (camt.052 bank reports wrapped in a ZIP) ─────────────────── + + [Fact] + public void ParseZipFiles_EachReturnsAtLeastOneStatement() + { + foreach (var filePath in ZipFiles()) + { + var bytes = File.ReadAllBytes(filePath); + Assert.True(CamtParser.LooksLikeZip(bytes), + $"{filePath}: expected ZIP magic bytes (PK)."); + + var statements = new CamtParser().ParseZip(bytes); + Assert.True(statements.Count > 0, + $"{filePath}: no CAMT statements found in ZIP."); + + foreach (var stmt in statements) + AssertValidStatement(stmt, filePath); + } + } + + [Fact] + public void ParseZipFiles_BankingService_EachReturnsRows() + { + foreach (var filePath in ZipFiles()) + { + using var stream = File.OpenRead(filePath); + var table = Svc.ParseToDatatable(stream); + Assert.True(table.Rows.Count > 0, + $"{filePath}: BankingService.ParseToDatatable returned no rows."); + } + } + + // ── XML files (camt.053 / camt.052 plain / camt.054) ───────────────────── + + [Fact] + public void ParseXmlFiles_EachReturnsAtLeastOneStatement() + { + foreach (var filePath in XmlFiles()) + { + var bytes = File.ReadAllBytes(filePath); + Assert.True(CamtParser.LooksLikeXml(bytes), + $"{filePath}: expected XML content."); + Assert.True(CamtParser.LooksLikeCamt(Encoding.UTF8.GetString(bytes)), + $"{filePath}: XML does not look like a CAMT document (missing 'camt.05' or 'BkToCstmr')."); + + var statements = new CamtParser().Parse(bytes); + Assert.True(statements.Count > 0, + $"{filePath}: no CAMT statements found."); + + foreach (var stmt in statements) + AssertValidStatement(stmt, filePath); + } + } + + [Fact] + public void ParseXmlFiles_BankingService_EachReturnsRows() + { + foreach (var filePath in XmlFiles()) + { + using var stream = File.OpenRead(filePath); + var table = Svc.ParseToDatatable(stream); + Assert.True(table.Rows.Count > 0, + $"{filePath}: BankingService.ParseToDatatable returned no rows."); + } + } + + // ── All files: DocumentType recognised ─────────────────────────────────── + + [Fact] + public void ParseAllBankingFiles_DocumentTypeIsKnown() + { + foreach (var filePath in AllBankingFiles()) + { + var bytes = File.ReadAllBytes(filePath); + List statements; + if (CamtParser.LooksLikeZip(bytes)) + statements = new CamtParser().ParseZip(bytes); + else if (CamtParser.LooksLikeXml(bytes)) + statements = new CamtParser().Parse(bytes); + else + continue; // MT940 / other format — not a CAMT file + + Assert.NotEmpty(statements); + Assert.All(statements, stmt => + Assert.NotEqual(CamtDocumentType.Unknown, stmt.DocumentType)); + } + } + + // ── Shared assertions ───────────────────────────────────────────────────── + + private static void AssertValidStatement(CamtStatement stmt, string filePath) + { + Assert.False(string.IsNullOrWhiteSpace(stmt.AccountIdentification), + $"{filePath}: AccountIdentification must not be blank."); + Assert.True(stmt.Entries.Count > 0, + $"{filePath}: statement for account '{stmt.AccountIdentification}' has no entries."); + + Assert.All(stmt.Entries, e => + { + Assert.True(e.Amount is > 0, + $"{filePath}: entry amount must be positive, got {e.Amount}."); + Assert.True(e.EntryDate.HasValue || e.ValueDate.HasValue, + $"{filePath}: entry must have at least one date (EntryDate or ValueDate)."); + Assert.True( + e.Mark is CamtDebitCreditMark.Credit + or CamtDebitCreditMark.Debit + or CamtDebitCreditMark.ReverseCredit + or CamtDebitCreditMark.ReverseDebit, + $"{filePath}: entry has invalid DebitCreditMark '{e.Mark}'."); + }); + } +} diff --git a/Fuchs.Tests/Fuchs.Tests.csproj b/Fuchs.Tests/Fuchs.Tests.csproj index adb96fb..9121014 100644 --- a/Fuchs.Tests/Fuchs.Tests.csproj +++ b/Fuchs.Tests/Fuchs.Tests.csproj @@ -30,4 +30,13 @@ + + + + PreserveNewest + + + diff --git a/Fuchs.Tests/TestData/Banking/.gitkeep b/Fuchs.Tests/TestData/Banking/.gitkeep new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/Fuchs.Tests/TestData/Banking/.gitkeep @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Fuchs.Tests/TestData/Banking/README.md b/Fuchs.Tests/TestData/Banking/README.md new file mode 100644 index 0000000..4802ad6 --- /dev/null +++ b/Fuchs.Tests/TestData/Banking/README.md @@ -0,0 +1,32 @@ +# Banking Test Samples + +Lege echte Bankexport-Dateien hier ab, um die Integrations-Smoketests in +`CamtRealFileTests` zu aktivieren. + +> **Diese Dateien werden nie committed** (sensible Finanzdaten — gilt für alle +> `.zip`, `.xml`, `.camt`, `.sta` und `.mt940` in diesem Verzeichnis). + +## Wo ablegen + +| Dateiformat | Erwarteter Inhalt | Beispieldateiname | +|-------------|------------------|-------------------| +| `.zip` | camt.052-ZIP-Archiv der Bank (enthält XML-Dateien) | `camt052_jan2025.zip` | +| `.xml` | camt.053 (oder camt.052 / camt.054) als plain XML | `camt053_jan2025.xml` | +| `.camt` | alternativer Dateiname für CAMT-XML | `umsaetze.camt` | + +Jede `.zip`- und `.xml`/`.camt`-Datei wird **automatisch** gefunden und getestet — +keine Anpassung am Code nötig. + +## Was wird geprüft + +- ZIP-Dateien: `CamtParser.ParseZip` liefert ≥ 1 Statement mit IBAN und Einträgen +- XML-Dateien: `CamtParser.Parse` erkennt das Dokument als CAMT und liefert valide Einträge +- Beide: `BankingService.ParseToDatatable` liefert ≥ 1 Zeile in der DataTable +- Dokumenttyp ist bekannt (nicht `Unknown`) +- Jeder Eintrag hat Betrag > 0 und mindestens ein Datum + +## Hinweis + +Sind keine Dateien vorhanden, generiert xUnit 0 Testfälle (die Tests tauchen +nicht im Runner auf). Sobald du eine Datei ablegst und den Build neu startest, +erscheinen die Tests automatisch. diff --git a/Fuchs/Program.cs b/Fuchs/Program.cs index 1f109a5..d457563 100644 --- a/Fuchs/Program.cs +++ b/Fuchs/Program.cs @@ -36,6 +36,9 @@ public class Program // Key Vault + DPAPI secret management (must run before FuchsOcmsIntranet.Initialize) builder.AddSecretManagement(); + // Assemble connection strings from templates + resolved credentials + AssembleConnectionStrings(builder.Configuration); + // Initialize the Fuchs intranet singleton with configuration FuchsOcmsIntranet.Initialize(builder.Configuration); @@ -163,4 +166,40 @@ public class Program // One-time application start Fuchs_intranet.SetPdfLicense(); } + + /// + /// Replaces {username} and {password} tokens in connection string templates with the + /// resolved credential secrets, then overrides the config entries in-place. + /// When appsettings.Development.json supplies a complete connection string (no tokens), + /// the replace is a no-op and the original value is preserved. + /// + private static void AssembleConnectionStrings(ConfigurationManager config) + { + const string userToken = "{username}"; + const string passToken = "{password}"; + + (string csName, string userKey, string passKey)[] pairs = + [ + ("ocms_ConnectionString", "ConnectionStrings:ocms_username", "ConnectionStrings:ocms_password"), + ("fuchs_fds_ConnectionString", "ConnectionStrings:fuchs_fds_username", "ConnectionStrings:fuchs_fds_password"), + ]; + + var overrides = new Dictionary(); + foreach (var (csName, userKey, passKey) in pairs) + { + var template = config.GetConnectionString(csName); + if (string.IsNullOrEmpty(template)) continue; + if (!template.Contains(userToken, StringComparison.Ordinal) && + !template.Contains(passToken, StringComparison.Ordinal)) continue; + + var user = config[userKey] ?? ""; + var pass = config[passKey] ?? ""; + overrides[$"ConnectionStrings:{csName}"] = template + .Replace(userToken, user, StringComparison.Ordinal) + .Replace(passToken, pass, StringComparison.Ordinal); + } + + if (overrides.Count > 0) + config.AddInMemoryCollection(overrides); + } } diff --git a/Fuchs/Services/BankingService.cs b/Fuchs/Services/BankingService.cs index 5e2e903..88da3ea 100644 --- a/Fuchs/Services/BankingService.cs +++ b/Fuchs/Services/BankingService.cs @@ -1,4 +1,4 @@ -using System.Data; +using System.Data; using System.Diagnostics; using CAMTParser; using Fuchs.Observability; @@ -45,10 +45,17 @@ public class BankingService : IBankingService } string format; - if (CamtParser.LooksLikeXml(bytes)) + if (CamtParser.LooksLikeZip(bytes)) + { + format = "camt.zip"; + try { MapCamtEntries(tbl, new CamtParser().ParseZip(bytes)); } + catch (Exception ex) { _logger.LogError(ex, "CAMT ZIP statement parse failed."); } + } + else if (CamtParser.LooksLikeXml(bytes)) { format = "camt"; - FillFromCamt(tbl, bytes); + try { MapCamtEntries(tbl, new CamtParser().Parse(bytes)); } + catch (Exception ex) { _logger.LogError(ex, "CAMT statement parse failed."); } } else { @@ -129,53 +136,48 @@ public class BankingService : IBankingService } // ── CAMT (ISO 20022) ─────────────────────────────────────────────────────── - private void FillFromCamt(DataTable tbl, byte[] bytes) + private void MapCamtEntries(DataTable tbl, List statements) { void SetNfo(DataRow nr, string key, object? value) { if (tbl.Columns.Contains(key) && value != null) nr[key] = value; } - try + foreach (var stmt in statements) { - var statements = new CamtParser().Parse(bytes); - foreach (var stmt in statements) + if (string.IsNullOrEmpty(stmt.AccountIdentification)) continue; + foreach (var e in stmt.Entries) { - if (string.IsNullOrEmpty(stmt.AccountIdentification)) continue; - foreach (var e in stmt.Entries) + try { - try - { - var nr = tbl.NewRow(); - SetNfo(nr, "AccountIdentification", stmt.AccountIdentification); - if (e.Amount.HasValue) SetNfo(nr, "Amount", e.Amount); - if (e.EntryDate.HasValue) SetNfo(nr, "EntryDate", e.EntryDate); - if (e.ValueDate.HasValue) SetNfo(nr, "ValueDate", e.ValueDate); - SetNfo(nr, "FundsCode", e.Currency); - SetNfo(nr, "DebitCreditMark", e.MarkAbbreviation); - SetNfo(nr, "BankReference", e.BankReference); - SetNfo(nr, "EndToEndReference", e.EndToEndReference); - SetNfo(nr, "MandateReference", e.MandateReference); - SetNfo(nr, "CustomerReference", e.CustomerReference); - SetNfo(nr, "CreditorReference", e.CreditorReference); - SetNfo(nr, "AccountNumberOfPayer", e.CounterpartyIban); - SetNfo(nr, "BankCodeOfPayer", e.CounterpartyBic); - SetNfo(nr, "NameOfPayer", e.CounterpartyName); - SetNfo(nr, "PostingText", e.AdditionalInfo); - SetNfo(nr, "TransactionTypeIdCode",e.BankTransactionCode); - SetNfo(nr, "SepaRemittanceInformation", - string.IsNullOrEmpty(e.RemittanceUnstructured) ? e.RemittanceStructured : e.RemittanceUnstructured); - SetNfo(nr, "UnstructuredRemittanceInformation", e.RemittanceUnstructured); - SetNfo(nr, "UnstructuredData", e.RemittanceUnstructured); - SetNfo(nr, "IsUnstructuredData", e.IsUnstructuredData); + var nr = tbl.NewRow(); + SetNfo(nr, "AccountIdentification", stmt.AccountIdentification); + if (e.Amount.HasValue) SetNfo(nr, "Amount", e.Amount); + if (e.EntryDate.HasValue) SetNfo(nr, "EntryDate", e.EntryDate); + if (e.ValueDate.HasValue) SetNfo(nr, "ValueDate", e.ValueDate); + SetNfo(nr, "FundsCode", e.Currency); + SetNfo(nr, "DebitCreditMark", e.MarkAbbreviation); + SetNfo(nr, "BankReference", e.BankReference); + SetNfo(nr, "EndToEndReference", e.EndToEndReference); + SetNfo(nr, "MandateReference", e.MandateReference); + SetNfo(nr, "CustomerReference", e.CustomerReference); + SetNfo(nr, "CreditorReference", e.CreditorReference); + SetNfo(nr, "AccountNumberOfPayer", e.CounterpartyIban); + SetNfo(nr, "BankCodeOfPayer", e.CounterpartyBic); + SetNfo(nr, "NameOfPayer", e.CounterpartyName); + SetNfo(nr, "PostingText", e.AdditionalInfo); + SetNfo(nr, "TransactionTypeIdCode",e.BankTransactionCode); + SetNfo(nr, "SepaRemittanceInformation", + string.IsNullOrEmpty(e.RemittanceUnstructured) ? e.RemittanceStructured : e.RemittanceUnstructured); + SetNfo(nr, "UnstructuredRemittanceInformation", e.RemittanceUnstructured); + SetNfo(nr, "UnstructuredData", e.RemittanceUnstructured); + SetNfo(nr, "IsUnstructuredData", e.IsUnstructuredData); - tbl.Rows.Add(nr); - } - catch (Exception ex) { _logger.LogWarning(ex, "CAMT entry parse error — account={Account}", stmt.AccountIdentification); } + tbl.Rows.Add(nr); } + catch (Exception ex) { _logger.LogWarning(ex, "CAMT entry parse error — account={Account}", stmt.AccountIdentification); } } } - catch (Exception ex) { _logger.LogError(ex, "CAMT statement parse failed."); } } private static DataTable BuildDefaultSchema() diff --git a/Fuchs/appsettings.Development.json b/Fuchs/appsettings.Development.json index a45cc86..76ca4d1 100644 --- a/Fuchs/appsettings.Development.json +++ b/Fuchs/appsettings.Development.json @@ -1,4 +1,8 @@ -{ +{ + "ConnectionStrings": { + "ocms_ConnectionString": "Server=localhost;Database=ocms;User Id=DEV_USERNAME;Password=DEV_PASSWORD;TrustServerCertificate=True;", + "fuchs_fds_ConnectionString": "Server=localhost;Database=fuchs_fds;User Id=DEV_USERNAME;Password=DEV_PASSWORD;TrustServerCertificate=True;" + }, "Logging": { "LogLevel": { "Default": "Debug", diff --git a/Fuchs/appsettings.json b/Fuchs/appsettings.json index 479fbf9..d773025 100644 --- a/Fuchs/appsettings.json +++ b/Fuchs/appsettings.json @@ -5,8 +5,10 @@ "CacheFilePath": "secrets.cache", "SyncIntervalHours": 6, "ManagedSecretKeys": [ - "ConnectionStrings--ocms-ConnectionString", - "ConnectionStrings--fuchs-fds-ConnectionString", + "ConnectionStrings--ocms-username", + "ConnectionStrings--ocms-password", + "ConnectionStrings--fuchs-fds-username", + "ConnectionStrings--fuchs-fds-password", "Fuchs--SMS-APIKey", "Fuchs--Mailer--Token", "Fuchs--fuchs-captcha-TOTP", @@ -21,8 +23,12 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "ocms_ConnectionString": "MANAGED_BY_KEYVAULT", - "fuchs_fds_ConnectionString": "MANAGED_BY_KEYVAULT" + "ocms_ConnectionString": "Server=DB_SERVER;Database=ocms;User Id={username};Password={password};TrustServerCertificate=True;", + "fuchs_fds_ConnectionString": "Server=DB_SERVER;Database=fuchs_fds;User Id={username};Password={password};TrustServerCertificate=True;", + "ocms_username": "MANAGED_BY_KEYVAULT", + "ocms_password": "MANAGED_BY_KEYVAULT", + "fuchs_fds_username": "MANAGED_BY_KEYVAULT", + "fuchs_fds_password": "MANAGED_BY_KEYVAULT" }, "Fuchs": { "ocms_guid": "00094b8f-a822-4e9c-b627-87802f93fca8",