Enhance banking transaction data structure and validation
Playwright Tests / test (push) Has been cancelled
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:
@@ -129,4 +129,51 @@ public class BankingParseToDatatableTests
|
||||
var ex = Record.Exception(() => Svc.ParseToDatatable(stream));
|
||||
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"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,7 +391,7 @@ public class BankingDualFormatTests
|
||||
t.Columns.Add("Amount", typeof(decimal));
|
||||
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("NameOfPayer", typeof(string)).MaxLength = 60;
|
||||
t.Columns.Add("NameOfPayer", typeof(string)).MaxLength = 140;
|
||||
t.Columns.Add("PostingText", typeof(string)).MaxLength = 30;
|
||||
t.Columns.Add("TransactionTypeIdCode", typeof(string)).MaxLength = 3;
|
||||
t.Columns.Add("SepaRemittanceInformation", typeof(string)).MaxLength = 200;
|
||||
@@ -418,7 +418,9 @@ public class BankingDualFormatTests
|
||||
[Fact]
|
||||
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 = """
|
||||
<?xml version="1.0"?>
|
||||
<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());
|
||||
|
||||
Assert.Equal(1, t.Rows.Count);
|
||||
Assert.Equal(60, ((string)t.Rows[0]["NameOfPayer"]).Length); // truncated to column width, row kept
|
||||
Assert.Equal(longName[..60], t.Rows[0]["NameOfPayer"]);
|
||||
Assert.Equal(140, ((string)t.Rows[0]["NameOfPayer"]).Length); // truncated to column width, row kept
|
||||
Assert.Equal(longName[..140], t.Rows[0]["NameOfPayer"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,18 @@ public class MFRClientConfigTests
|
||||
Assert.Equal("", config.LogoutAddress);
|
||||
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
|
||||
|
||||
@@ -190,11 +190,19 @@ public partial class IntranetController
|
||||
{
|
||||
if (!HasForm("taid")) return BadRequest400();
|
||||
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;",
|
||||
_intranet.Intranet__SQLConnectionString, pl,
|
||||
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 })
|
||||
: StatusCode(500, new { error = "not successful" });
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ public enum DomainEventType
|
||||
ReminderSendFailed,
|
||||
BankingTransactionsImported,
|
||||
BankingImportFailed,
|
||||
BankingTransactionMarkedDone,
|
||||
UserIssue
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using Fuchs.intranet;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -130,6 +131,18 @@ public sealed class EventService : IEventService
|
||||
"Banking",
|
||||
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)
|
||||
{
|
||||
Dictionary<string, object?> ctx = context == null
|
||||
@@ -181,6 +194,8 @@ public sealed class EventService : IEventService
|
||||
BankingImportMessage(domainEvent),
|
||||
DomainEventType.BankingImportFailed =>
|
||||
Ctx(domainEvent, "message"),
|
||||
DomainEventType.BankingTransactionMarkedDone =>
|
||||
BankingTransactionMarkedDoneMessage(domainEvent),
|
||||
DomainEventType.UserIssue =>
|
||||
Ctx(domainEvent, "message"),
|
||||
_ => domainEvent.Title
|
||||
@@ -242,6 +257,29 @@ public sealed class EventService : IEventService
|
||||
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)
|
||||
{
|
||||
string invoiceNumber = invoice.InvoiceId;
|
||||
|
||||
@@ -20,6 +20,7 @@ public interface IEventService
|
||||
|
||||
Task BankingTransactionsImportedAsync(DateTime? from, DateTime? to, int rows, 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);
|
||||
}
|
||||
|
||||
@@ -144,6 +144,11 @@ public class Program
|
||||
|
||||
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())
|
||||
{
|
||||
app.UseExceptionHandler("/error");
|
||||
|
||||
@@ -36,6 +36,7 @@ public class BankingService : IBankingService
|
||||
using var act = FuchsTelemetry.StartActivity("banking.parse");
|
||||
var sw = Stopwatch.StartNew();
|
||||
var tbl = schemaDatatable?.Clone() ?? BuildDefaultSchema();
|
||||
ApplyKnownColumnWidths(tbl);
|
||||
var diag = new ParseDiagnostics();
|
||||
|
||||
// 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()
|
||||
{
|
||||
var t = new DataTable();
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"Microsoft.AspNetCore": "Information"
|
||||
"Default": "Warning",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Fuchs": {
|
||||
@@ -22,5 +22,10 @@
|
||||
"InvoiceContainer": "dev-fuchs-invoices",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
"Fuchs--SMS-APIKey",
|
||||
"Fuchs--Mailer--Token",
|
||||
"Fuchs--fuchs-captcha-TOTP",
|
||||
"Fuchs--fuchs-intranet-TOTP"
|
||||
"Fuchs--fuchs-intranet-TOTP",
|
||||
"Fds--MFR-UserName",
|
||||
"Fds--MFR-Password"
|
||||
]
|
||||
},
|
||||
"Logging": {
|
||||
@@ -55,5 +57,10 @@
|
||||
"Enabled": true,
|
||||
"OtlpEndpoint": ""
|
||||
}
|
||||
},
|
||||
"Fds": {
|
||||
"MFR_host": "portal.mobilefieldreport.com",
|
||||
"MFR_UserName": "MANAGED_BY_KEYVAULT",
|
||||
"MFR_Password": "MANAGED_BY_KEYVAULT"
|
||||
}
|
||||
}
|
||||
@@ -30,57 +30,59 @@ main nav ul > li a[role=button] {
|
||||
}
|
||||
|
||||
#notification_frame {
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 2000;
|
||||
width: min(24rem, calc(100vw - 2rem));
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.notification_item {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-left: 0.35rem solid $fuchs_blau;
|
||||
border-radius: 0.35rem;
|
||||
box-shadow: 0 0.25rem 1rem rgba(30, 35, 45, 0.25);
|
||||
color: #222;
|
||||
padding: 0.75rem 2.2rem 0.75rem 0.85rem;
|
||||
flex: 0 0 $oci_header;
|
||||
height: $oci_header;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
color: $fuchs_weiss;
|
||||
padding: 0 2.2rem 0 0.85rem;
|
||||
pointer-events: auto;
|
||||
|
||||
&.warn {
|
||||
border-left-color: #c78300;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-left-color: #b92525;
|
||||
&.warn, &.warning, &.error {
|
||||
color: $oci_yellow;
|
||||
}
|
||||
|
||||
.notification_title {
|
||||
font-weight: bold;
|
||||
line-height: 1.25;
|
||||
margin-bottom: 0.2rem;
|
||||
flex: 0 0 auto;
|
||||
|
||||
&::after {
|
||||
content: ":";
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
}
|
||||
|
||||
.notification_message {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.notification_close {
|
||||
position: absolute;
|
||||
top: 0.35rem;
|
||||
top: 0;
|
||||
right: 0.45rem;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #444;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
padding: 0.1rem 0.25rem;
|
||||
line-height: $oci_header;
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -244,8 +244,22 @@ $fis.notifications = {
|
||||
this.connection.on('notification', (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;
|
||||
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 () {
|
||||
|
||||
+27
-25
@@ -2231,53 +2231,55 @@ main nav ul > li a[role=button]:hover::after, main nav ul > li a[role=button].fb
|
||||
}
|
||||
|
||||
#notification_frame {
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 2000;
|
||||
width: min(24rem, 100vw - 2rem);
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.notification_item {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-left: 0.35rem solid rgb(27, 67, 121);
|
||||
border-radius: 0.35rem;
|
||||
box-shadow: 0 0.25rem 1rem rgba(30, 35, 45, 0.25);
|
||||
color: #222;
|
||||
padding: 0.75rem 2.2rem 0.75rem 0.85rem;
|
||||
flex: 0 0 2.3rem;
|
||||
height: 2.3rem;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
color: #FFF;
|
||||
padding: 0 2.2rem 0 0.85rem;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.notification_item.warn {
|
||||
border-left-color: #c78300;
|
||||
}
|
||||
.notification_item.error {
|
||||
border-left-color: #b92525;
|
||||
.notification_item.warn, .notification_item.warning, .notification_item.error {
|
||||
color: rgb(255, 200, 1);
|
||||
}
|
||||
.notification_item .notification_title {
|
||||
font-weight: bold;
|
||||
line-height: 1.25;
|
||||
margin-bottom: 0.2rem;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.notification_item .notification_title::after {
|
||||
content: ":";
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
.notification_item .notification_message {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.notification_item .notification_close {
|
||||
position: absolute;
|
||||
top: 0.35rem;
|
||||
top: 0;
|
||||
right: 0.45rem;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #444;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
padding: 0.1rem 0.25rem;
|
||||
line-height: 2.3rem;
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
.wdg_frame {
|
||||
|
||||
@@ -3088,8 +3088,22 @@ $fis.notifications = {
|
||||
this.connection.on('notification', (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;
|
||||
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 () {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
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
|
||||
,[Amount] numeric(9,2)
|
||||
,[AccountNumberOfPayer] varchar(30)
|
||||
,[NameOfPayer] nvarchar(60)
|
||||
,[SepaRemittanceInformation] varchar(150)
|
||||
,[NameOfPayer] nvarchar(140)
|
||||
,[SepaRemittanceInformation] varchar(200)
|
||||
,[EndToEndReference] varchar(50)
|
||||
,[manu] bit
|
||||
,[InvId] varchar(15)
|
||||
|
||||
@@ -34,7 +34,12 @@ BEGIN
|
||||
;
|
||||
|
||||
|
||||
--output to confirm as boolean
|
||||
SELECT CAST( (CASE WHEN ISNULL((SELECT TOP(1) [done_manually] FROM @out), '') <> '' THEN 1 ELSE 0 END) as bit);
|
||||
--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) 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
|
||||
@@ -13,7 +13,7 @@
|
||||
[EndToEndReference] VARCHAR (50) NULL,
|
||||
[JournalNumber] VARCHAR (10) NULL,
|
||||
[MandateReference] VARCHAR (50) NULL,
|
||||
[NameOfPayer] NVARCHAR (60) NULL,
|
||||
[NameOfPayer] NVARCHAR (140) NULL,
|
||||
[OriginalAmount] VARCHAR (150) NULL,
|
||||
[OriginatorsIdentificationCode] VARCHAR (150) NULL,
|
||||
[PayersReferenceParty] VARCHAR (150) NULL,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
[EndToEndReference] VARCHAR (50) NULL,
|
||||
[JournalNumber] VARCHAR (10) NULL,
|
||||
[MandateReference] VARCHAR (50) NULL,
|
||||
[NameOfPayer] NVARCHAR (60) NULL,
|
||||
[NameOfPayer] NVARCHAR (140) NULL,
|
||||
[OriginalAmount] VARCHAR (150) NULL,
|
||||
[OriginatorsIdentificationCode] VARCHAR (150) NULL,
|
||||
[PayersReferenceParty] VARCHAR (150) NULL,
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
[EndToEndReference] VARCHAR (50) NULL,
|
||||
[JournalNumber] VARCHAR (10) NULL,
|
||||
[MandateReference] VARCHAR (50) NULL,
|
||||
[NameOfPayer] NVARCHAR (60) NULL,
|
||||
[NameOfPayer] NVARCHAR (140) NULL,
|
||||
[OriginalAmount] VARCHAR (150) NULL,
|
||||
[OriginatorsIdentificationCode] VARCHAR (150) NULL,
|
||||
[PayersReferenceParty] VARCHAR (150) NULL,
|
||||
|
||||
@@ -25,6 +25,9 @@ public class MFRClientConfig
|
||||
|
||||
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/";
|
||||
if (!BaseUrl.EndsWith("/")) BaseUrl += "/";
|
||||
// Derive the REST root (/mfr/) from the same scheme+host as the OData root.
|
||||
|
||||
Reference in New Issue
Block a user