|
|
|
@@ -1,5 +1,6 @@
|
|
|
|
|
using System.Data;
|
|
|
|
|
using System.Data;
|
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
using CAMTParser;
|
|
|
|
|
using Fuchs.Observability;
|
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
using programmersdigest.MT940Parser;
|
|
|
|
@@ -7,7 +8,9 @@ using programmersdigest.MT940Parser;
|
|
|
|
|
namespace Fuchs.Services;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// MT940 bank statement parsing service. Replaces the static <c>Banking</c> class.
|
|
|
|
|
/// Bank statement parsing service. Accepts both MT940 (SWIFT text) and
|
|
|
|
|
/// CAMT (ISO 20022 camt.052/053/054 XML) and maps either into the same
|
|
|
|
|
/// banking-transactions DataTable. The format is auto-detected from content.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class BankingService : IBankingService
|
|
|
|
|
{
|
|
|
|
@@ -20,23 +23,56 @@ public class BankingService : IBankingService
|
|
|
|
|
|
|
|
|
|
public string DebitCreditMarkAbb(DebitCreditMark mark) => mark switch
|
|
|
|
|
{
|
|
|
|
|
DebitCreditMark.Credit => "C",
|
|
|
|
|
DebitCreditMark.Debit => "D",
|
|
|
|
|
DebitCreditMark.Credit => "C",
|
|
|
|
|
DebitCreditMark.Debit => "D",
|
|
|
|
|
DebitCreditMark.ReverseCredit => "RC",
|
|
|
|
|
DebitCreditMark.ReverseDebit => "RD",
|
|
|
|
|
_ => ""
|
|
|
|
|
DebitCreditMark.ReverseDebit => "RD",
|
|
|
|
|
_ => ""
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
public DataTable ParseToDatatable(Stream stream, DataTable? schemaDatatable = null)
|
|
|
|
|
{
|
|
|
|
|
using var act = FuchsTelemetry.StartActivity("banking.mt940.parse");
|
|
|
|
|
using var act = FuchsTelemetry.StartActivity("banking.parse");
|
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
|
|
|
var tbl = schemaDatatable?.Clone() ?? BuildDefaultSchema();
|
|
|
|
|
|
|
|
|
|
// Buffer once so we can sniff the format and (re)parse from the bytes.
|
|
|
|
|
byte[] bytes;
|
|
|
|
|
using (var ms = new MemoryStream())
|
|
|
|
|
{
|
|
|
|
|
stream.CopyTo(ms);
|
|
|
|
|
bytes = ms.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string format;
|
|
|
|
|
if (CamtParser.LooksLikeXml(bytes))
|
|
|
|
|
{
|
|
|
|
|
format = "camt";
|
|
|
|
|
FillFromCamt(tbl, bytes);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
format = "mt940";
|
|
|
|
|
using var msMt = new MemoryStream(bytes);
|
|
|
|
|
FillFromMt940(tbl, msMt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tbl.AcceptChanges();
|
|
|
|
|
sw.Stop();
|
|
|
|
|
FuchsTelemetry.Mt940RowsParsed.Add(tbl.Rows.Count, new KeyValuePair<string, object?>("format", format));
|
|
|
|
|
act?.SetTag("fuchs.banking.format", format);
|
|
|
|
|
act?.SetTag("fuchs.banking.rows", tbl.Rows.Count);
|
|
|
|
|
_logger.LogInformation("Bank statement parsed: format={Format} rows={Rows} in {Ms} ms",
|
|
|
|
|
format, tbl.Rows.Count, sw.ElapsedMilliseconds);
|
|
|
|
|
return tbl;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── MT940 ─────────────────────────────────────────────────────────────────
|
|
|
|
|
private void FillFromMt940(DataTable tbl, Stream stream)
|
|
|
|
|
{
|
|
|
|
|
void SetNfo(DataRow nr, string key, object? value)
|
|
|
|
|
{
|
|
|
|
|
if (tbl.Columns.Contains(key) && value != null)
|
|
|
|
|
nr[key] = value;
|
|
|
|
|
if (tbl.Columns.Contains(key) && value != null) nr[key] = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using var ps = new Parser(stream: stream);
|
|
|
|
@@ -51,93 +87,130 @@ public class BankingService : IBankingService
|
|
|
|
|
{
|
|
|
|
|
var nr = tbl.NewRow();
|
|
|
|
|
SetNfo(nr, "AccountIdentification", statement.AccountIdentification);
|
|
|
|
|
if (line.Amount.HasValue) SetNfo(nr, "Amount", line.Amount);
|
|
|
|
|
if (line.EntryDate.HasValue) SetNfo(nr, "EntryDate", line.EntryDate);
|
|
|
|
|
if (line.FundsCode.HasValue) SetNfo(nr, "FundsCode", line.FundsCode.ToString());
|
|
|
|
|
if (line.Amount.HasValue) SetNfo(nr, "Amount", line.Amount);
|
|
|
|
|
if (line.EntryDate.HasValue) SetNfo(nr, "EntryDate", line.EntryDate);
|
|
|
|
|
if (line.FundsCode.HasValue) SetNfo(nr, "FundsCode", line.FundsCode.ToString());
|
|
|
|
|
SetNfo(nr, "BankReference", line.BankReference);
|
|
|
|
|
|
|
|
|
|
var info = line.InformationToOwner;
|
|
|
|
|
SetNfo(nr, "AccountNumberOfPayer", info.AccountNumberOfPayer);
|
|
|
|
|
SetNfo(nr, "BankCodeOfPayer", info.BankCodeOfPayer);
|
|
|
|
|
SetNfo(nr, "CompensationAmount", info.CompensationAmount);
|
|
|
|
|
SetNfo(nr, "CreditorReference", info.CreditorReference);
|
|
|
|
|
SetNfo(nr, "CreditorsReferenceParty", info.CreditorsReferenceParty);
|
|
|
|
|
SetNfo(nr, "CustomerReference", info.CustomerReference);
|
|
|
|
|
SetNfo(nr, "EndToEndReference", info.EndToEndReference);
|
|
|
|
|
SetNfo(nr, "JournalNumber", info.JournalNumber);
|
|
|
|
|
SetNfo(nr, "MandateReference", info.MandateReference);
|
|
|
|
|
SetNfo(nr, "NameOfPayer", info.NameOfPayer);
|
|
|
|
|
SetNfo(nr, "OriginalAmount", info.OriginalAmount);
|
|
|
|
|
SetNfo(nr, "OriginatorsIdentificationCode", info.OriginatorsIdentificationCode);
|
|
|
|
|
SetNfo(nr, "PayersReferenceParty", info.PayersReferenceParty);
|
|
|
|
|
SetNfo(nr, "PostingText", info.PostingText);
|
|
|
|
|
SetNfo(nr, "SepaRemittanceInformation", info.SepaRemittanceInformation);
|
|
|
|
|
SetNfo(nr, "AccountNumberOfPayer", info.AccountNumberOfPayer);
|
|
|
|
|
SetNfo(nr, "BankCodeOfPayer", info.BankCodeOfPayer);
|
|
|
|
|
SetNfo(nr, "CompensationAmount", info.CompensationAmount);
|
|
|
|
|
SetNfo(nr, "CreditorReference", info.CreditorReference);
|
|
|
|
|
SetNfo(nr, "CreditorsReferenceParty", info.CreditorsReferenceParty);
|
|
|
|
|
SetNfo(nr, "CustomerReference", info.CustomerReference);
|
|
|
|
|
SetNfo(nr, "EndToEndReference", info.EndToEndReference);
|
|
|
|
|
SetNfo(nr, "JournalNumber", info.JournalNumber);
|
|
|
|
|
SetNfo(nr, "MandateReference", info.MandateReference);
|
|
|
|
|
SetNfo(nr, "NameOfPayer", info.NameOfPayer);
|
|
|
|
|
SetNfo(nr, "OriginalAmount", info.OriginalAmount);
|
|
|
|
|
SetNfo(nr, "OriginatorsIdentificationCode", info.OriginatorsIdentificationCode);
|
|
|
|
|
SetNfo(nr, "PayersReferenceParty", info.PayersReferenceParty);
|
|
|
|
|
SetNfo(nr, "PostingText", info.PostingText);
|
|
|
|
|
SetNfo(nr, "SepaRemittanceInformation", info.SepaRemittanceInformation);
|
|
|
|
|
if (info.TextKeyAddition.HasValue) SetNfo(nr, "TextKeyAddition", info.TextKeyAddition);
|
|
|
|
|
SetNfo(nr, "TransactionCode", info.TransactionCode);
|
|
|
|
|
SetNfo(nr, "IsUnstructuredData", info.IsUnstructuredData);
|
|
|
|
|
SetNfo(nr, "UnstructuredData", info.UnstructuredData);
|
|
|
|
|
SetNfo(nr, "UnstructuredRemittanceInformation", info.UnstructuredRemittanceInformation);
|
|
|
|
|
SetNfo(nr, "TransactionCode", info.TransactionCode);
|
|
|
|
|
SetNfo(nr, "IsUnstructuredData", info.IsUnstructuredData);
|
|
|
|
|
SetNfo(nr, "UnstructuredData", info.UnstructuredData);
|
|
|
|
|
SetNfo(nr, "UnstructuredRemittanceInformation",info.UnstructuredRemittanceInformation);
|
|
|
|
|
|
|
|
|
|
SetNfo(nr, "DebitCreditMark", DebitCreditMarkAbb(line.Mark));
|
|
|
|
|
SetNfo(nr, "DebitCreditMark", DebitCreditMarkAbb(line.Mark));
|
|
|
|
|
SetNfo(nr, "SupplementaryDetails", line.SupplementaryDetails);
|
|
|
|
|
SetNfo(nr, "TransactionTypeIdCode", line.TransactionTypeIdCode);
|
|
|
|
|
SetNfo(nr, "ValueDate", line.ValueDate);
|
|
|
|
|
SetNfo(nr, "TransactionTypeIdCode",line.TransactionTypeIdCode);
|
|
|
|
|
SetNfo(nr, "ValueDate", line.ValueDate);
|
|
|
|
|
|
|
|
|
|
tbl.Rows.Add(nr);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogWarning(ex, "MT940 line parse error — account={Account}", statement.AccountIdentification);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex) { _logger.LogWarning(ex, "MT940 line parse error — account={Account}", statement.AccountIdentification); }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
catch (Exception ex) { _logger.LogError(ex, "MT940 statement parse failed."); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── CAMT (ISO 20022) ───────────────────────────────────────────────────────
|
|
|
|
|
private void FillFromCamt(DataTable tbl, byte[] bytes)
|
|
|
|
|
{
|
|
|
|
|
void SetNfo(DataRow nr, string key, object? value)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogError(ex, "MT940 statement parse failed.");
|
|
|
|
|
if (tbl.Columns.Contains(key) && value != null) nr[key] = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tbl.AcceptChanges();
|
|
|
|
|
sw.Stop();
|
|
|
|
|
FuchsTelemetry.Mt940RowsParsed.Add(tbl.Rows.Count);
|
|
|
|
|
act?.SetTag("fuchs.banking.rows", tbl.Rows.Count);
|
|
|
|
|
_logger.LogInformation("MT940 parsed {Rows} transaction lines in {Ms} ms", tbl.Rows.Count, sw.ElapsedMilliseconds);
|
|
|
|
|
return tbl;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var statements = new CamtParser().Parse(bytes);
|
|
|
|
|
foreach (var stmt in statements)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(stmt.AccountIdentification)) continue;
|
|
|
|
|
foreach (var e in stmt.Entries)
|
|
|
|
|
{
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
{
|
|
|
|
|
var t = new DataTable();
|
|
|
|
|
var cols = t.Columns;
|
|
|
|
|
cols.Add("AccountIdentification", typeof(string));
|
|
|
|
|
cols.Add("Amount", typeof(decimal));
|
|
|
|
|
cols.Add("BankReference", typeof(string));
|
|
|
|
|
cols.Add("EntryDate", typeof(DateTime));
|
|
|
|
|
cols.Add("FundsCode", typeof(string));
|
|
|
|
|
cols.Add("AccountNumberOfPayer", typeof(string));
|
|
|
|
|
cols.Add("BankCodeOfPayer", typeof(string));
|
|
|
|
|
cols.Add("CompensationAmount", typeof(string));
|
|
|
|
|
cols.Add("CreditorReference", typeof(string));
|
|
|
|
|
cols.Add("CreditorsReferenceParty", typeof(string));
|
|
|
|
|
cols.Add("CustomerReference", typeof(string));
|
|
|
|
|
cols.Add("EndToEndReference", typeof(string));
|
|
|
|
|
cols.Add("JournalNumber", typeof(string));
|
|
|
|
|
cols.Add("MandateReference", typeof(string));
|
|
|
|
|
cols.Add("NameOfPayer", typeof(string));
|
|
|
|
|
cols.Add("OriginalAmount", typeof(string));
|
|
|
|
|
cols.Add("OriginatorsIdentificationCode", typeof(string));
|
|
|
|
|
cols.Add("PayersReferenceParty", typeof(string));
|
|
|
|
|
cols.Add("PostingText", typeof(string));
|
|
|
|
|
cols.Add("SepaRemittanceInformation", typeof(string));
|
|
|
|
|
cols.Add("TextKeyAddition", typeof(int));
|
|
|
|
|
cols.Add("TransactionCode", typeof(int));
|
|
|
|
|
cols.Add("IsUnstructuredData", typeof(bool));
|
|
|
|
|
cols.Add("UnstructuredData", typeof(string));
|
|
|
|
|
cols.Add("AccountIdentification", typeof(string));
|
|
|
|
|
cols.Add("Amount", typeof(decimal));
|
|
|
|
|
cols.Add("BankReference", typeof(string));
|
|
|
|
|
cols.Add("EntryDate", typeof(DateTime));
|
|
|
|
|
cols.Add("FundsCode", typeof(string));
|
|
|
|
|
cols.Add("AccountNumberOfPayer", typeof(string));
|
|
|
|
|
cols.Add("BankCodeOfPayer", typeof(string));
|
|
|
|
|
cols.Add("CompensationAmount", typeof(string));
|
|
|
|
|
cols.Add("CreditorReference", typeof(string));
|
|
|
|
|
cols.Add("CreditorsReferenceParty", typeof(string));
|
|
|
|
|
cols.Add("CustomerReference", typeof(string));
|
|
|
|
|
cols.Add("EndToEndReference", typeof(string));
|
|
|
|
|
cols.Add("JournalNumber", typeof(string));
|
|
|
|
|
cols.Add("MandateReference", typeof(string));
|
|
|
|
|
cols.Add("NameOfPayer", typeof(string));
|
|
|
|
|
cols.Add("OriginalAmount", typeof(string));
|
|
|
|
|
cols.Add("OriginatorsIdentificationCode", typeof(string));
|
|
|
|
|
cols.Add("PayersReferenceParty", typeof(string));
|
|
|
|
|
cols.Add("PostingText", typeof(string));
|
|
|
|
|
cols.Add("SepaRemittanceInformation", typeof(string));
|
|
|
|
|
cols.Add("TextKeyAddition", typeof(int));
|
|
|
|
|
cols.Add("TransactionCode", typeof(int));
|
|
|
|
|
cols.Add("IsUnstructuredData", typeof(bool));
|
|
|
|
|
cols.Add("UnstructuredData", typeof(string));
|
|
|
|
|
cols.Add("UnstructuredRemittanceInformation", typeof(string));
|
|
|
|
|
cols.Add("DebitCreditMark", typeof(string));
|
|
|
|
|
cols.Add("SupplementaryDetails", typeof(string));
|
|
|
|
|
cols.Add("TransactionTypeIdCode", typeof(string));
|
|
|
|
|
cols.Add("ValueDate", typeof(DateTime));
|
|
|
|
|
cols.Add("DebitCreditMark", typeof(string));
|
|
|
|
|
cols.Add("SupplementaryDetails", typeof(string));
|
|
|
|
|
cols.Add("TransactionTypeIdCode", typeof(string));
|
|
|
|
|
cols.Add("ValueDate", typeof(DateTime));
|
|
|
|
|
return t;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|