feat: Implement mfr truncation fix and enhance invoice handling
Playwright Tests / test (push) Canceled after 0s
Playwright Tests / test (push) Canceled after 0s
- Added a new SQL script to address the "String or binary data would be truncated" error in mfr sync by widening the Note fields in service objects and companies to NVARCHAR(MAX). - Updated the table types and associated stored procedures to accommodate the changes, ensuring compatibility with existing data. - Queued a re-sync for previously rejected rows due to truncation issues. - Created application logins for fuchs_fds and fuchs_web with placeholder passwords for initial setup. - Introduced functions for invoice backdating and invoice date retrieval to streamline invoice processing. - Developed a password generation function with customizable parameters for length and character types. - Added a random number generation function to support password creation.
This commit is contained in:
@@ -135,6 +135,12 @@ public class AzureBlobStorageService : IBlobStorageService
|
||||
Response<bool> response = await blobClient.ExistsAsync(cancellationToken);
|
||||
return response.Value;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Caller cancelled (e.g. app shutdown during the startup backfill) — not a storage failure;
|
||||
// reporting "not archived" here made the backfill go on to a doomed upload logged as an error.
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Blob existence check failed for {Container}/{Blob} — treating as not archived.",
|
||||
@@ -270,6 +276,11 @@ public class AzureBlobStorageService : IBlobStorageService
|
||||
category, documentId, containerName, blobName);
|
||||
return blobClient.Uri;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogDebug("Blob upload cancelled for {Category} {Id} (caller cancelled).", category, documentId);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FuchsTelemetry.BlobUploadsFailed.Add(1, new KeyValuePair<string, object?>("category", category));
|
||||
|
||||
@@ -259,13 +259,7 @@ public static class ERechnungMapper
|
||||
}
|
||||
|
||||
// ── helpers ────────────────────────────────────────────────────────────────
|
||||
private static DateOnly IssueDate(GenericObjectDictionary? reg)
|
||||
{
|
||||
foreach (var key in new[] { "DateFinalized", "DateCreated" })
|
||||
if (reg?.getString(key) is { Length: > 0 } s && DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt))
|
||||
return DateOnly.FromDateTime(dt);
|
||||
return DateOnly.FromDateTime(DateTime.Today);
|
||||
}
|
||||
private static DateOnly IssueDate(GenericObjectDictionary? reg) => InvoiceDates.IssueDate(reg);
|
||||
|
||||
/// <summary>Normalises a raw country value (ISO alpha-2, or a German/English name) to a country code.</summary>
|
||||
internal static CountryCode NormalizeCountry(string? raw)
|
||||
|
||||
@@ -12,7 +12,8 @@ public interface IBlobStorageService
|
||||
/// supplied (typically <c>FdsInvoiceData.InvoiceRegistration</c>), blob metadata is projected from
|
||||
/// it using <see cref="AzureBlobStorageSettings.MetadataFields"/> — see <see cref="DocumentMetadataBuilder"/>.
|
||||
/// Returns the blob URI, or <c>null</c> when storage is disabled/unconfigured
|
||||
/// or the upload failed — failures never break the primary DB-storage flow.
|
||||
/// or the upload failed — failures never break the primary DB-storage flow. Only a cancelled
|
||||
/// <paramref name="cancellationToken"/> surfaces, as <see cref="OperationCanceledException"/>.
|
||||
/// </summary>
|
||||
Task<Uri?> UploadInvoicePdfAsync(string invoiceId, string fileName, byte[] content,
|
||||
IReadOnlyDictionary<string, object?>? sourceRow = null, CancellationToken cancellationToken = default);
|
||||
@@ -50,7 +51,8 @@ public interface IBlobStorageService
|
||||
/// <summary>
|
||||
/// Returns <c>true</c> if a blob already exists for the given container/document/filename
|
||||
/// combination. Used by the startup backfill to skip documents that were already archived.
|
||||
/// Returns <c>false</c> (never throws) when storage is disabled/unconfigured or the check fails.
|
||||
/// Returns <c>false</c> when storage is disabled/unconfigured or the check fails; throws only
|
||||
/// <see cref="OperationCanceledException"/> when <paramref name="cancellationToken"/> is cancelled.
|
||||
/// </summary>
|
||||
Task<bool> ExistsAsync(string containerName, string documentId, string fileName,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -616,7 +616,8 @@ public sealed class InvoiceDraftEditService : IInvoiceDraftService
|
||||
["SendToAddressJson"] = InvoiceRecipientAddress.FromCustomValues(Str(session.New["CustomValues"]))?
|
||||
.ToJson().ToString(Newtonsoft.Json.Formatting.None) ?? "",
|
||||
["InvoiceOptions"] = BuildInvoiceOptions(session),
|
||||
["DateCreated"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)
|
||||
// UTC like the stored column — InvoiceDates.IssueDate converts it to the German day
|
||||
["DateCreated"] = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
int idx = 0;
|
||||
|
||||
@@ -205,6 +205,17 @@ public class InvoiceService : IInvoiceService
|
||||
/// The whole set is cleared and rewritten on every call — simpler and safer than diffing —
|
||||
/// mirroring the legacy <c>fds__invoice_data.RegisterInvoice_V1</c> (VB) behaviour.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Normalises an item's posted <c>ref</c> (the invoice a CanceledInvoice / IntInvoiceAllocation line
|
||||
/// points at) for <c>fds__invoice_items.reference_InvId</c> (varchar(20)); null when absent or not an id.
|
||||
/// </summary>
|
||||
internal static string? ItemReferenceInvId(string? raw)
|
||||
{
|
||||
string s = (raw ?? "").Trim();
|
||||
if (s.Length == 0 || s.Length > 20 || s.Equals("null", StringComparison.OrdinalIgnoreCase)) return null;
|
||||
return s.All(char.IsAsciiLetterOrDigit) ? s : null;
|
||||
}
|
||||
|
||||
private async Task PersistInvoiceLineItemsAsync(FdsInvoiceData invoice, string userAccountId, DatabaseSecurity dbSec)
|
||||
{
|
||||
string invoiceId = invoice.Id;
|
||||
@@ -269,6 +280,7 @@ public class InvoiceService : IInvoiceService
|
||||
float vs = 0;
|
||||
if (TryCastSingle(itmDic.getItem("vs"), ref vs)) row["value_service"] = (decimal)vs;
|
||||
row["det"] = itmDic.no("det", false) is true;
|
||||
if (ItemReferenceInvId(itmDic.nz("ref")) is { } refInvId) row["reference_InvId"] = refInvId;
|
||||
row["SortOrder"] = (byte)Math.Clamp(itemSort, 0, 255);
|
||||
itemsTable.Rows.Add(row);
|
||||
}
|
||||
@@ -302,7 +314,9 @@ public class InvoiceService : IInvoiceService
|
||||
: Array.Empty<string>(),
|
||||
AdminUser = reg?.getString("UserNameFinalized") ?? "",
|
||||
AdminUserEmail = reg?.getString("UserEmailFinalized") ?? "",
|
||||
AdminDatumValue = reg?.getString("DateCreated") is { Length: > 0 } dc ? DateTime.Parse(dc) : DateTime.Now
|
||||
// Rechnungsdatum = DateFinalized (incl. backdating), draft: DateCreated — same source as the
|
||||
// e-invoice issue date (ERechnungMapper), see InvoiceDates / ADR 0032.
|
||||
AdminDatumValue = InvoiceDates.IssueDate(reg).ToDateTime(TimeOnly.MinValue)
|
||||
};
|
||||
var inlineImages = await ResolveInlineImagesAsync(invoice);
|
||||
var doc = await _pdf.WriteLetterAsync(tb, draft);
|
||||
|
||||
@@ -271,7 +271,8 @@ public sealed class ReminderDraftEditService : IReminderDraftService
|
||||
["invoices"] = invoices,
|
||||
["CustomValues"] = Str(session.New["CustomValues"]),
|
||||
["IsFinal"] = false,
|
||||
["DateCreated"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)
|
||||
// UTC like the stored column — InvoiceDates.IssueDate converts it to the German day
|
||||
["DateCreated"] = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)
|
||||
};
|
||||
return new GenericObjectDictionary(d);
|
||||
}
|
||||
|
||||
@@ -111,7 +111,8 @@ public class ReminderService : IReminderService
|
||||
Address = reminder.InvoiceAddress,
|
||||
AdminUser = reminder.UserNameFinalized,
|
||||
AdminUserEmail = reminder.UserEmailFinalized,
|
||||
AdminDatumValue = reminder.DateCreated ?? DateTime.Now
|
||||
// Reminder date = DateFinalized, draft: DateCreated — same rule as the invoice date (ADR 0035)
|
||||
AdminDatumValue = InvoiceDates.IssueDate(reminder.ReminderRegistration).ToDateTime(TimeOnly.MinValue)
|
||||
};
|
||||
if (!string.IsNullOrEmpty(reminder.RawCustomValues))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user