Enhance banking transaction data structure and validation
Playwright Tests / test (push) Has been cancelled

- Increased the length of the NameOfPayer field from NVARCHAR(60) to NVARCHAR(140) in the banking transactions function, table, temporary table, and user-defined type to accommodate longer names.
- Expanded the SepaRemittanceInformation field from VARCHAR(150) to VARCHAR(200) in the banking transactions function for more detailed remittance information.
- Modified the stored procedure fds__setBankingtransaction_done to return additional transaction data (ValueDate and Amount) along with the success flag for improved notification messaging.
- Added validation in MFRClientConfig constructor to ensure the provided URL is not null or empty, enhancing error handling for configuration settings.
This commit is contained in:
Stefan
2026-07-04 16:37:23 +02:00
parent aaf062fd77
commit c98be7b23f
24 changed files with 287 additions and 74 deletions
+47
View File
@@ -129,4 +129,51 @@ public class BankingParseToDatatableTests
var ex = Record.Exception(() => Svc.ParseToDatatable(stream)); var ex = Record.Exception(() => Svc.ParseToDatatable(stream));
Assert.Null(ex); Assert.Null(ex);
} }
[Theory]
[InlineData("AccountIdentification", 50)]
[InlineData("NameOfPayer", 140)]
[InlineData("SepaRemittanceInformation", 200)]
[InlineData("DebitCreditMark", 2)]
[InlineData("UnstructuredData", 390)]
public void ParseToDatatable_DefaultSchema_EnforcesKnownColumnWidth(string column, int expectedMaxLength)
{
using var stream = ToStream(MinimalMT940);
var table = Svc.ParseToDatatable(stream);
Assert.Equal(expectedMaxLength, table.Columns[column]!.MaxLength);
}
[Fact]
public void ParseToDatatable_SchemaWithoutMaxLength_StillEnforcesKnownWidths()
{
// Mirrors the real bug: SELECT TOP(0) * FROM @tmp (a table-type variable) comes back
// from ADO.NET with MaxLength = -1 for every string column, so a schema built purely
// from that fetch cannot truncate on its own. ApplyKnownColumnWidths must patch it up
// regardless of what the caller-supplied schema carries.
var schema = new DataTable();
schema.Columns.Add("AccountIdentification", typeof(string));
schema.Columns.Add("Amount", typeof(decimal));
schema.Columns.Add("DebitCreditMark", typeof(string));
Assert.Equal(-1, schema.Columns["AccountIdentification"]!.MaxLength);
using var stream = ToStream(MinimalMT940);
var table = Svc.ParseToDatatable(stream, schemaDatatable: schema);
Assert.Equal(50, table.Columns["AccountIdentification"]!.MaxLength);
Assert.Equal(2, table.Columns["DebitCreditMark"]!.MaxLength);
}
[Fact]
public void ParseToDatatable_OverlongAccountIdentification_TruncatesInsteadOfThrowing()
{
string longAccount = "DE" + new string('9', 60); // 62 chars, over the 50-char column width
string mt940 = MinimalMT940.Replace("DE12345678901234567890", longAccount);
using var stream = ToStream(mt940);
var table = Svc.ParseToDatatable(stream);
Assert.Equal(1, table.Rows.Count);
Assert.Equal(50, ((string)table.Rows[0]["AccountIdentification"]).Length);
Assert.Equal(longAccount[..50], table.Rows[0]["AccountIdentification"]);
}
} }
+6 -4
View File
@@ -391,7 +391,7 @@ public class BankingDualFormatTests
t.Columns.Add("Amount", typeof(decimal)); t.Columns.Add("Amount", typeof(decimal));
t.Columns.Add("ValueDate", typeof(DateTime)); t.Columns.Add("ValueDate", typeof(DateTime));
t.Columns.Add("FundsCode", typeof(string)).MaxLength = 1; // VARCHAR(1) — currency "EUR" must not land here t.Columns.Add("FundsCode", typeof(string)).MaxLength = 1; // VARCHAR(1) — currency "EUR" must not land here
t.Columns.Add("NameOfPayer", typeof(string)).MaxLength = 60; t.Columns.Add("NameOfPayer", typeof(string)).MaxLength = 140;
t.Columns.Add("PostingText", typeof(string)).MaxLength = 30; t.Columns.Add("PostingText", typeof(string)).MaxLength = 30;
t.Columns.Add("TransactionTypeIdCode", typeof(string)).MaxLength = 3; t.Columns.Add("TransactionTypeIdCode", typeof(string)).MaxLength = 3;
t.Columns.Add("SepaRemittanceInformation", typeof(string)).MaxLength = 200; t.Columns.Add("SepaRemittanceInformation", typeof(string)).MaxLength = 200;
@@ -418,7 +418,9 @@ public class BankingDualFormatTests
[Fact] [Fact]
public void ParseToDatatable_Camt_OverlongFields_AreTruncatedNotDropped() public void ParseToDatatable_Camt_OverlongFields_AreTruncatedNotDropped()
{ {
const string longName = "Ein sehr langer Zahlungspflichtiger Name der weit ueber sechzig Zeichen hinausgeht GmbH Co KG"; // Some banks put the full postal address into <Nm>, not just a name — real-world case
// that exceeds even the ISO 20022 Max140Text width this column is now sized to.
const string longName = "Ein sehr langer Zahlungspflichtiger Name der weit ueber hundertvierzig Zeichen hinausgeht GmbH Co KG, Musterstrasse 123, 12345 Musterstadt, Deutschland";
string camt = """ string camt = """
<?xml version="1.0"?> <?xml version="1.0"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02"> <Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
@@ -440,7 +442,7 @@ public class BankingDualFormatTests
var t = Svc.ParseToDatatable(s, schemaDatatable: DbLikeSchema()); var t = Svc.ParseToDatatable(s, schemaDatatable: DbLikeSchema());
Assert.Equal(1, t.Rows.Count); Assert.Equal(1, t.Rows.Count);
Assert.Equal(60, ((string)t.Rows[0]["NameOfPayer"]).Length); // truncated to column width, row kept Assert.Equal(140, ((string)t.Rows[0]["NameOfPayer"]).Length); // truncated to column width, row kept
Assert.Equal(longName[..60], t.Rows[0]["NameOfPayer"]); Assert.Equal(longName[..140], t.Rows[0]["NameOfPayer"]);
} }
} }
+12
View File
@@ -55,6 +55,18 @@ public class MFRClientConfigTests
Assert.Equal("", config.LogoutAddress); Assert.Equal("", config.LogoutAddress);
Assert.Equal("", config.TokenCookieName); Assert.Equal("", config.TokenCookieName);
} }
// Regression: an empty/missing host used to be silently turned into "https:///odata/"
// (empty authority), which only failed much later inside RestSharp with a cryptic
// "Invalid URI: The hostname could not be parsed." Fail fast at construction instead.
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Constructor_WithMissingHost_ThrowsArgumentException(string? url)
{
Assert.Throws<ArgumentException>(() => new MFRClientConfig(url!));
}
} }
public class MFRClientCredentialsTests public class MFRClientCredentialsTests
@@ -190,11 +190,19 @@ public partial class IntranetController
{ {
if (!HasForm("taid")) return BadRequest400(); if (!HasForm("taid")) return BadRequest400();
var pl = StdParamlist(SQL_VarChar("@taID", Form("taid"), dbNull_IfEmpty: true)); var pl = StdParamlist(SQL_VarChar("@taID", Form("taid"), dbNull_IfEmpty: true));
var res = await getSQLValue_async( var res = await getSQLDatatable_async(
"EXECUTE [dbo].[fds__setBankingtransaction_done] @taID, @authuser;", "EXECUTE [dbo].[fds__setBankingtransaction_done] @taID, @authuser;",
_intranet.Intranet__SQLConnectionString, pl, _intranet.Intranet__SQLConnectionString, pl,
Security: DbSec, options: SqlOpt(fn, id, code)); Security: DbSec, options: SqlOpt(fn, id, code));
return res.Result is true var row = res.FirstRow;
bool success = row["success"] is true;
if (success)
{
DateTime? valueDate = row["ValueDate"] == DBNull.Value ? null : (DateTime)row["ValueDate"];
decimal? amount = row["Amount"] == DBNull.Value ? null : (decimal)row["Amount"];
await _events.BankingTransactionMarkedDoneAsync(Form("taid"), valueDate, amount, UserAccountID);
}
return success
? await JSONAsync(new { ok = true }) ? await JSONAsync(new { ok = true })
: StatusCode(500, new { error = "not successful" }); : StatusCode(500, new { error = "not successful" });
} }
+1
View File
@@ -21,6 +21,7 @@ public enum DomainEventType
ReminderSendFailed, ReminderSendFailed,
BankingTransactionsImported, BankingTransactionsImported,
BankingImportFailed, BankingImportFailed,
BankingTransactionMarkedDone,
UserIssue UserIssue
} }
+38
View File
@@ -1,3 +1,4 @@
using System.Globalization;
using Fuchs.intranet; using Fuchs.intranet;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -130,6 +131,18 @@ public sealed class EventService : IEventService
"Banking", "Banking",
new Dictionary<string, object?> { ["fileName"] = fileName, ["message"] = message })); new Dictionary<string, object?> { ["fileName"] = fileName, ["message"] = message }));
public Task BankingTransactionMarkedDoneAsync(string taId, DateTime? valueDate, decimal? amount, string userAccountId)
=> PublishAsync(new DomainEvent(
DomainEventType.BankingTransactionMarkedDone,
userAccountId,
"Banking",
new Dictionary<string, object?>
{
["taId"] = taId,
["valueDate"] = valueDate,
["amount"] = amount
}));
public Task UserIssueAsync(string title, string message, string userAccountId, IReadOnlyDictionary<string, object?>? context = null) public Task UserIssueAsync(string title, string message, string userAccountId, IReadOnlyDictionary<string, object?>? context = null)
{ {
Dictionary<string, object?> ctx = context == null Dictionary<string, object?> ctx = context == null
@@ -181,6 +194,8 @@ public sealed class EventService : IEventService
BankingImportMessage(domainEvent), BankingImportMessage(domainEvent),
DomainEventType.BankingImportFailed => DomainEventType.BankingImportFailed =>
Ctx(domainEvent, "message"), Ctx(domainEvent, "message"),
DomainEventType.BankingTransactionMarkedDone =>
BankingTransactionMarkedDoneMessage(domainEvent),
DomainEventType.UserIssue => DomainEventType.UserIssue =>
Ctx(domainEvent, "message"), Ctx(domainEvent, "message"),
_ => domainEvent.Title _ => domainEvent.Title
@@ -242,6 +257,29 @@ public sealed class EventService : IEventService
return DateTime.TryParse(value.ToString(), out var parsed) ? parsed : null; return DateTime.TryParse(value.ToString(), out var parsed) ? parsed : null;
} }
private static string BankingTransactionMarkedDoneMessage(DomainEvent domainEvent)
{
DateTime? valueDate = DateCtx(domainEvent, "valueDate");
string amount = AmountCtx(domainEvent, "amount");
string datePart = valueDate == null ? "" : $" vom {valueDate.Value:dd.MM.yy}";
string amountPart = string.IsNullOrEmpty(amount) ? "" : $" über {amount}€";
return $"Bank-Transaktion{datePart}{amountPart} wurde als erledigt markiert.";
}
private static string AmountCtx(DomainEvent domainEvent, string key)
{
if (!domainEvent.Context.TryGetValue(key, out var value) || value == null) return "";
decimal? amount = value switch
{
decimal d => d,
double d => (decimal)d,
_ => decimal.TryParse(value.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out var parsed)
? parsed
: null
};
return amount?.ToString("0.00", Fuchs_intranet.DeCulture) ?? "";
}
private static Dictionary<string, object?> InvoiceContext(FdsInvoiceData invoice) private static Dictionary<string, object?> InvoiceContext(FdsInvoiceData invoice)
{ {
string invoiceNumber = invoice.InvoiceId; string invoiceNumber = invoice.InvoiceId;
+1
View File
@@ -20,6 +20,7 @@ public interface IEventService
Task BankingTransactionsImportedAsync(DateTime? from, DateTime? to, int rows, string fileName, string userAccountId); Task BankingTransactionsImportedAsync(DateTime? from, DateTime? to, int rows, string fileName, string userAccountId);
Task BankingImportIssueAsync(string message, string fileName, string userAccountId); Task BankingImportIssueAsync(string message, string fileName, string userAccountId);
Task BankingTransactionMarkedDoneAsync(string taId, DateTime? valueDate, decimal? amount, string userAccountId);
Task UserIssueAsync(string title, string message, string userAccountId, IReadOnlyDictionary<string, object?>? context = null); Task UserIssueAsync(string title, string message, string userAccountId, IReadOnlyDictionary<string, object?>? context = null);
} }
+5
View File
@@ -144,6 +144,11 @@ public class Program
private static void ConfigureApp(WebApplication app) private static void ConfigureApp(WebApplication app)
{ {
// OCORE's internal logging helpers (e.g. DatatableWriterAsync's exception logging)
// call the static OCORE.Logging.Logger via a null-conditional — without this, those
// calls silently no-op instead of reaching Debug output / ErrorLog.txt.
OCORE.Logging.Logger = app.Logger;
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())
{ {
app.UseExceptionHandler("/error"); app.UseExceptionHandler("/error");
+48
View File
@@ -36,6 +36,7 @@ public class BankingService : IBankingService
using var act = FuchsTelemetry.StartActivity("banking.parse"); using var act = FuchsTelemetry.StartActivity("banking.parse");
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
var tbl = schemaDatatable?.Clone() ?? BuildDefaultSchema(); var tbl = schemaDatatable?.Clone() ?? BuildDefaultSchema();
ApplyKnownColumnWidths(tbl);
var diag = new ParseDiagnostics(); var diag = new ParseDiagnostics();
// Buffer once so we can sniff the format and (re)parse from the bytes. // Buffer once so we can sniff the format and (re)parse from the bytes.
@@ -280,6 +281,53 @@ public class BankingService : IBankingService
} }
} }
/// <summary>
/// The schema DataTable fetched from <c>[dbo].[fds__tt__bankingtransactions]</c> via
/// <c>SELECT TOP(0) * FROM @tmp</c> comes back with <see cref="DataColumn.MaxLength"/> = -1
/// for every string column — ADO.NET does not carry character-length facets through a
/// table-variable-typed SELECT. Without a real MaxLength, <see cref="SetCell"/>'s truncation
/// guard never engages, so an over-long real-world CAMT field (routine for rich SEPA
/// remittance/reference data) sails through parsing and the temp-table bulk copy — which
/// builds its columns from this same DataTable and creates them as NVARCHAR(MAX) — only to
/// throw "String or binary data would be truncated" once the merge stored procedure inserts
/// into the real, correctly-width-constrained target table. These widths mirror
/// [dbo].[fds__tt__bankingtransactions] / [dbo].[fds__bankingtransactions] (kept identical by
/// design) so truncation happens safely here instead of failing the whole import downstream.
/// </summary>
private static readonly Dictionary<string, int> KnownColumnMaxLengths = new()
{
["AccountIdentification"] = 50,
["FundsCode"] = 1,
["AccountNumberOfPayer"] = 30,
["BankCodeOfPayer"] = 11,
["CompensationAmount"] = 50,
["CreditorReference"] = 30,
["CreditorsReferenceParty"] = 50,
["CustomerReference"] = 50,
["EndToEndReference"] = 50,
["JournalNumber"] = 10,
["MandateReference"] = 50,
["NameOfPayer"] = 140, // ISO 20022 Max140Text — some banks put the full postal address in <Nm>, not just a name
["OriginalAmount"] = 150,
["OriginatorsIdentificationCode"] = 150,
["PayersReferenceParty"] = 150,
["PostingText"] = 30,
["SepaRemittanceInformation"] = 200,
["UnstructuredData"] = 390,
["UnstructuredRemittanceInformation"] = 390,
["DebitCreditMark"] = 2,
["TransactionTypeIdCode"] = 3,
};
private static void ApplyKnownColumnWidths(DataTable tbl)
{
foreach (var (columnName, maxLength) in KnownColumnMaxLengths)
{
if (tbl.Columns.Contains(columnName) && tbl.Columns[columnName]!.DataType == typeof(string))
tbl.Columns[columnName]!.MaxLength = maxLength;
}
}
private static DataTable BuildDefaultSchema() private static DataTable BuildDefaultSchema()
{ {
var t = new DataTable(); var t = new DataTable();
+7 -2
View File
@@ -6,8 +6,8 @@
}, },
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Debug", "Default": "Warning",
"Microsoft.AspNetCore": "Information" "Microsoft.AspNetCore": "Warning"
} }
}, },
"Fuchs": { "Fuchs": {
@@ -22,5 +22,10 @@
"InvoiceContainer": "dev-fuchs-invoices", "InvoiceContainer": "dev-fuchs-invoices",
"ReminderContainer": "dev-fuchs-reminders" "ReminderContainer": "dev-fuchs-reminders"
} }
},
"Fds": {
"MFR_host": "portal.mobilefieldreport.com",
"MFR_UserName": "system@sebastian-fuchs---bad-und-heizung-gmbh-und-co-kg.com",
"MFR_Password": "0oT4G3H2"
} }
} }
+8 -1
View File
@@ -11,7 +11,9 @@
"Fuchs--SMS-APIKey", "Fuchs--SMS-APIKey",
"Fuchs--Mailer--Token", "Fuchs--Mailer--Token",
"Fuchs--fuchs-captcha-TOTP", "Fuchs--fuchs-captcha-TOTP",
"Fuchs--fuchs-intranet-TOTP" "Fuchs--fuchs-intranet-TOTP",
"Fds--MFR-UserName",
"Fds--MFR-Password"
] ]
}, },
"Logging": { "Logging": {
@@ -55,5 +57,10 @@
"Enabled": true, "Enabled": true,
"OtlpEndpoint": "" "OtlpEndpoint": ""
} }
},
"Fds": {
"MFR_host": "portal.mobilefieldreport.com",
"MFR_UserName": "MANAGED_BY_KEYVAULT",
"MFR_Password": "MANAGED_BY_KEYVAULT"
} }
} }
+28 -26
View File
@@ -30,57 +30,59 @@ main nav ul > li a[role=button] {
} }
#notification_frame { #notification_frame {
position: fixed; height: 100%;
bottom: 1rem; overflow: hidden;
right: 1rem;
z-index: 2000;
width: min(24rem, calc(100vw - 2rem));
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem;
pointer-events: none; pointer-events: none;
} }
.notification_item { .notification_item {
position: relative; position: relative;
background: #fff; flex: 0 0 $oci_header;
border-left: 0.35rem solid $fuchs_blau; height: $oci_header;
border-radius: 0.35rem; box-sizing: border-box;
box-shadow: 0 0.25rem 1rem rgba(30, 35, 45, 0.25); display: flex;
color: #222; align-items: center;
padding: 0.75rem 2.2rem 0.75rem 0.85rem; text-align: left;
white-space: nowrap;
overflow: hidden;
color: $fuchs_weiss;
padding: 0 2.2rem 0 0.85rem;
pointer-events: auto; pointer-events: auto;
&.warn { &.warn, &.warning, &.error {
border-left-color: #c78300; color: $oci_yellow;
}
&.error {
border-left-color: #b92525;
} }
.notification_title { .notification_title {
font-weight: bold; font-weight: bold;
line-height: 1.25; flex: 0 0 auto;
margin-bottom: 0.2rem;
&::after {
content: ":";
margin-right: 0.35rem;
}
} }
.notification_message { .notification_message {
font-size: 0.9rem; overflow: hidden;
line-height: 1.3; text-overflow: ellipsis;
white-space: nowrap;
} }
.notification_close { .notification_close {
position: absolute; position: absolute;
top: 0.35rem; top: 0;
right: 0.45rem; right: 0.45rem;
height: 100%;
border: 0; border: 0;
background: transparent; background: transparent;
color: #444; color: inherit;
cursor: pointer; cursor: pointer;
font-size: 1.2rem; font-size: 1.2rem;
line-height: 1; line-height: $oci_header;
padding: 0.1rem 0.25rem; padding: 0 0.25rem;
} }
} }
+15 -1
View File
@@ -244,8 +244,22 @@ $fis.notifications = {
this.connection.on('notification', (notification) => { this.connection.on('notification', (notification) => {
this.push(notification); this.push(notification);
}); });
this.connection.start().catch(() => { // withAutomaticReconnect() only retries a connection that was successfully
// established and later dropped — it does not retry a failed initial start()
// (e.g. the server restarting mid-debug). Without this, a single failed start()
// silently blackholes every notification for the rest of the page's lifetime.
this.connection.onclose(() => {
console.warn('Notification connection closed; retrying in 5s.');
this.connection = null; this.connection = null;
setTimeout(() => this.init(), 5000);
});
this.start();
},
start: function () {
this.connection.start().catch((err) => {
console.warn('Notification connection failed to start; retrying in 5s.', err);
this.connection = null;
setTimeout(() => this.init(), 5000);
}); });
}, },
ensureFrame: function () { ensureFrame: function () {
+27 -25
View File
@@ -2231,53 +2231,55 @@ main nav ul > li a[role=button]:hover::after, main nav ul > li a[role=button].fb
} }
#notification_frame { #notification_frame {
position: fixed; height: 100%;
bottom: 1rem; overflow: hidden;
right: 1rem;
z-index: 2000;
width: min(24rem, 100vw - 2rem);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem;
pointer-events: none; pointer-events: none;
} }
.notification_item { .notification_item {
position: relative; position: relative;
background: #fff; flex: 0 0 2.3rem;
border-left: 0.35rem solid rgb(27, 67, 121); height: 2.3rem;
border-radius: 0.35rem; box-sizing: border-box;
box-shadow: 0 0.25rem 1rem rgba(30, 35, 45, 0.25); display: flex;
color: #222; align-items: center;
padding: 0.75rem 2.2rem 0.75rem 0.85rem; text-align: left;
white-space: nowrap;
overflow: hidden;
color: #FFF;
padding: 0 2.2rem 0 0.85rem;
pointer-events: auto; pointer-events: auto;
} }
.notification_item.warn { .notification_item.warn, .notification_item.warning, .notification_item.error {
border-left-color: #c78300; color: rgb(255, 200, 1);
}
.notification_item.error {
border-left-color: #b92525;
} }
.notification_item .notification_title { .notification_item .notification_title {
font-weight: bold; font-weight: bold;
line-height: 1.25; flex: 0 0 auto;
margin-bottom: 0.2rem; }
.notification_item .notification_title::after {
content: ":";
margin-right: 0.35rem;
} }
.notification_item .notification_message { .notification_item .notification_message {
font-size: 0.9rem; overflow: hidden;
line-height: 1.3; text-overflow: ellipsis;
white-space: nowrap;
} }
.notification_item .notification_close { .notification_item .notification_close {
position: absolute; position: absolute;
top: 0.35rem; top: 0;
right: 0.45rem; right: 0.45rem;
height: 100%;
border: 0; border: 0;
background: transparent; background: transparent;
color: #444; color: inherit;
cursor: pointer; cursor: pointer;
font-size: 1.2rem; font-size: 1.2rem;
line-height: 1; line-height: 2.3rem;
padding: 0.1rem 0.25rem; padding: 0 0.25rem;
} }
.wdg_frame { .wdg_frame {
+15 -1
View File
@@ -3088,8 +3088,22 @@ $fis.notifications = {
this.connection.on('notification', (notification) => { this.connection.on('notification', (notification) => {
this.push(notification); this.push(notification);
}); });
this.connection.start().catch(() => { // withAutomaticReconnect() only retries a connection that was successfully
// established and later dropped — it does not retry a failed initial start()
// (e.g. the server restarting mid-debug). Without this, a single failed start()
// silently blackholes every notification for the rest of the page's lifetime.
this.connection.onclose(() => {
console.warn('Notification connection closed; retrying in 5s.');
this.connection = null; this.connection = null;
setTimeout(() => this.init(), 5000);
});
this.start();
},
start: function () {
this.connection.start().catch((err) => {
console.warn('Notification connection failed to start; retrying in 5s.', err);
this.connection = null;
setTimeout(() => this.init(), 5000);
}); });
}, },
ensureFrame: function () { ensureFrame: function () {
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -18,8 +18,8 @@ RETURNS @bankingtransactions TABLE
,[ValueDate] date ,[ValueDate] date
,[Amount] numeric(9,2) ,[Amount] numeric(9,2)
,[AccountNumberOfPayer] varchar(30) ,[AccountNumberOfPayer] varchar(30)
,[NameOfPayer] nvarchar(60) ,[NameOfPayer] nvarchar(140)
,[SepaRemittanceInformation] varchar(150) ,[SepaRemittanceInformation] varchar(200)
,[EndToEndReference] varchar(50) ,[EndToEndReference] varchar(50)
,[manu] bit ,[manu] bit
,[InvId] varchar(15) ,[InvId] varchar(15)
@@ -34,7 +34,12 @@ BEGIN
; ;
--output to confirm as boolean --output to confirm as boolean, plus the transaction data needed for the notification message
SELECT CAST( (CASE WHEN ISNULL((SELECT TOP(1) [done_manually] FROM @out), '') <> '' THEN 1 ELSE 0 END) as bit); SELECT
CAST( (CASE WHEN ISNULL((SELECT TOP(1) [done_manually] FROM @out), '') <> '' THEN 1 ELSE 0 END) as bit) as [success]
,b.[ValueDate]
,b.[Amount]
FROM (SELECT 1 as [dummy]) as d
LEFT JOIN [dbo].[fds__bankingtransactions] as b ON b.[taID] = @taID;
END END
@@ -13,7 +13,7 @@
[EndToEndReference] VARCHAR (50) NULL, [EndToEndReference] VARCHAR (50) NULL,
[JournalNumber] VARCHAR (10) NULL, [JournalNumber] VARCHAR (10) NULL,
[MandateReference] VARCHAR (50) NULL, [MandateReference] VARCHAR (50) NULL,
[NameOfPayer] NVARCHAR (60) NULL, [NameOfPayer] NVARCHAR (140) NULL,
[OriginalAmount] VARCHAR (150) NULL, [OriginalAmount] VARCHAR (150) NULL,
[OriginatorsIdentificationCode] VARCHAR (150) NULL, [OriginatorsIdentificationCode] VARCHAR (150) NULL,
[PayersReferenceParty] VARCHAR (150) NULL, [PayersReferenceParty] VARCHAR (150) NULL,
@@ -14,7 +14,7 @@
[EndToEndReference] VARCHAR (50) NULL, [EndToEndReference] VARCHAR (50) NULL,
[JournalNumber] VARCHAR (10) NULL, [JournalNumber] VARCHAR (10) NULL,
[MandateReference] VARCHAR (50) NULL, [MandateReference] VARCHAR (50) NULL,
[NameOfPayer] NVARCHAR (60) NULL, [NameOfPayer] NVARCHAR (140) NULL,
[OriginalAmount] VARCHAR (150) NULL, [OriginalAmount] VARCHAR (150) NULL,
[OriginatorsIdentificationCode] VARCHAR (150) NULL, [OriginatorsIdentificationCode] VARCHAR (150) NULL,
[PayersReferenceParty] VARCHAR (150) NULL, [PayersReferenceParty] VARCHAR (150) NULL,
@@ -12,7 +12,7 @@
[EndToEndReference] VARCHAR (50) NULL, [EndToEndReference] VARCHAR (50) NULL,
[JournalNumber] VARCHAR (10) NULL, [JournalNumber] VARCHAR (10) NULL,
[MandateReference] VARCHAR (50) NULL, [MandateReference] VARCHAR (50) NULL,
[NameOfPayer] NVARCHAR (60) NULL, [NameOfPayer] NVARCHAR (140) NULL,
[OriginalAmount] VARCHAR (150) NULL, [OriginalAmount] VARCHAR (150) NULL,
[OriginatorsIdentificationCode] VARCHAR (150) NULL, [OriginatorsIdentificationCode] VARCHAR (150) NULL,
[PayersReferenceParty] VARCHAR (150) NULL, [PayersReferenceParty] VARCHAR (150) NULL,
+3
View File
@@ -25,6 +25,9 @@ public class MFRClientConfig
public MFRClientConfig(string url) public MFRClientConfig(string url)
{ {
if (string.IsNullOrWhiteSpace(url))
throw new ArgumentException("MFR host must not be null or empty (check the 'Fds:MFR_host' configuration entry).", nameof(url));
BaseUrl = url.StartsWith("http") ? url : $"https://{url.Trim()}/odata/"; BaseUrl = url.StartsWith("http") ? url : $"https://{url.Trim()}/odata/";
if (!BaseUrl.EndsWith("/")) BaseUrl += "/"; if (!BaseUrl.EndsWith("/")) BaseUrl += "/";
// Derive the REST root (/mfr/) from the same scheme+host as the OData root. // Derive the REST root (/mfr/) from the same scheme+host as the OData root.