Refactor EnvelopeType handling and improve validation

Removed AutoMapper dependency in `ReadEnvelopeTypesQueryHandler` and replaced it with manual projection to address null-materialization issues. Updated `EnvelopeType` entity properties to nullable types to align with the database schema. Adjusted computed properties to handle null values gracefully.

Enhanced `SenderEditorPage` with authentication checks, improved handling of reminder and expiry settings, and added validation for user input. Introduced `NormalizeSettings` and `ValidateSettings` methods to ensure consistent and valid configurations. Refactored checkbox value parsing for better reliability.

Injected `AuthService` and `IStringLocalizer` for authentication and localization support. Improved error messaging and localized validation errors. Performed minor code cleanup and refactoring for better readability and maintainability.
This commit is contained in:
2026-10-02 01:25:43 +02:00
parent 1e8e5c96c4
commit c7bc9fbcca
3 changed files with 121 additions and 28 deletions

View File

@@ -1,6 +1,5 @@
using EnvelopeGenerator.Application.Common.Dto; using EnvelopeGenerator.Application.Common.Dto;
using MediatR; using MediatR;
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository; using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities; using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -18,17 +17,14 @@ public record ReadEnvelopeTypesQuery : IRequest<IEnumerable<EnvelopeTypeDto>>;
public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>> public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>>
{ {
private readonly IRepository<EnvelopeType> _repository; private readonly IRepository<EnvelopeType> _repository;
private readonly IMapper _mapper;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
/// <param name="repository"></param> /// <param name="repository"></param>
/// <param name="mapper"></param> public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository)
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository, IMapper mapper)
{ {
_repository = repository; _repository = repository;
_mapper = mapper;
} }
/// <summary> /// <summary>
@@ -39,7 +35,28 @@ public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQu
/// <returns></returns> /// <returns></returns>
public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken) public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken)
{ {
var types = await _repository.Query.ToListAsync(cancellationToken); // TODO: Temporary manual projection to avoid null-materialization crashes from legacy nullable DB columns in EnvelopeType.
return _mapper.Map<IEnumerable<EnvelopeTypeDto>>(types); // Revisit and switch to AutoMapper query projection (ProjectTo) after schema nullability and mapping contracts are stabilized.
return await _repository.Query
.AsNoTracking()
.Select(t => new EnvelopeTypeDto
{
Id = t.Id,
Title = t.Title ?? string.Empty,
Language = string.IsNullOrWhiteSpace(t.Language) ? "de-DE" : t.Language,
ExpiresDays = t.ExpiresDays,
CertificationType = (int?)t.CertificationType,
UseAccessCode = t.UseAccessCode,
FinalEmailToCreator = (int?)t.FinalEmailToCreator,
FinalEmailToReceivers = (int?)t.FinalEmailToReceivers,
AddedWhen = t.AddedWhen,
ChangedWhen = t.ChangedWhen,
ExpiresWarningDays = t.ExpiresWarningDays,
SendReminderEmails = t.SendReminderEmails,
FirstReminderDays = t.FirstReminderDays,
ReminderIntervalDays = t.ReminderIntervalDays,
ContractType = t.ContractType
})
.ToListAsync(cancellationToken);
} }
} }

View File

@@ -22,41 +22,41 @@ namespace EnvelopeGenerator.Domain.Entities
public string Language { get; set; } public string Language { get; set; }
[Column("EXPIRES_DAYS")] [Column("EXPIRES_DAYS")]
public int ExpiresDays { get; set; } public int? ExpiresDays { get; set; }
[Column("CERTIFICATION_TYPE")] [Column("CERTIFICATION_TYPE")]
public Constants.CertificationType CertificationType { get; set; } = Constants.CertificationType.AdvancedElectronicSignature; public Constants.CertificationType? CertificationType { get; set; }
[Column("USE_ACCESS_CODE")] [Column("USE_ACCESS_CODE")]
public bool UseAccessCode { get; set; } public bool? UseAccessCode { get; set; }
[Column("FINAL_EMAIL_TO_CREATOR")] [Column("FINAL_EMAIL_TO_CREATOR")]
public Constants.FinalEmailType FinalEmailToCreator { get; set; } public Constants.FinalEmailType? FinalEmailToCreator { get; set; }
[Column("FINAL_EMAIL_TO_RECEIVERS")] [Column("FINAL_EMAIL_TO_RECEIVERS")]
public Constants.FinalEmailType FinalEmailToReceivers { get; set; } public Constants.FinalEmailType? FinalEmailToReceivers { get; set; }
[Required] [Required]
[Column("ADDED_WHEN", TypeName = "datetime")] [Column("ADDED_WHEN", TypeName = "datetime")]
public DateTime AddedWhen { get; set; } public DateTime AddedWhen { get; set; }
[Column("CHANGED_WHEN", TypeName = "datetime")] [Column("CHANGED_WHEN", TypeName = "datetime")]
public DateTime ChangedWhen { get; set; } public DateTime? ChangedWhen { get; set; }
[Column("EXPIRES_WARNING_DAYS")] [Column("EXPIRES_WARNING_DAYS")]
public int ExpiresWarningDays { get; set; } public int? ExpiresWarningDays { get; set; }
[Column("SEND_REMINDER_EMAILS")] [Column("SEND_REMINDER_EMAILS")]
public bool SendReminderEmails { get; set; } = false; public bool? SendReminderEmails { get; set; }
[Column("FIRST_REMINDER_DAYS")] [Column("FIRST_REMINDER_DAYS")]
public int FirstReminderDays { get; set; } = 0; public int? FirstReminderDays { get; set; }
[Column("REMINDER_INTERVAL_DAYS")] [Column("REMINDER_INTERVAL_DAYS")]
public int ReminderIntervalDays { get; set; } = 0; public int? ReminderIntervalDays { get; set; }
[Column("CONTRACT_TYPE")] [Column("CONTRACT_TYPE")]
public int ContractType { get; set; } public int? ContractType { get; set; }
[NotMapped] [NotMapped]
public bool TFA_Enabled { get; set; } = false; public bool TFA_Enabled { get; set; } = false;
@@ -70,14 +70,14 @@ namespace EnvelopeGenerator.Domain.Entities
[NotMapped] [NotMapped]
public int ExpiresWhenDays public int ExpiresWhenDays
{ {
get => ExpiresDays; get => ExpiresDays ?? 0;
set => ExpiresDays = value; set => ExpiresDays = value;
} }
[NotMapped] [NotMapped]
public int ExpiresWarningWhenDays public int ExpiresWarningWhenDays
{ {
get => ExpiresWarningDays; get => ExpiresWarningDays ?? 0;
set => ExpiresWarningDays = value; set => ExpiresWarningDays = value;
} }

View File

@@ -20,6 +20,7 @@
@inject ReceiverService ReceiverService @inject ReceiverService ReceiverService
@inject EnvelopeService EnvelopeService @inject EnvelopeService EnvelopeService
@inject EnvelopeTypeService EnvelopeTypeService @inject EnvelopeTypeService EnvelopeTypeService
@inject AuthService AuthService
@inject IStringLocalizer<Resource> UiLocalizer @inject IStringLocalizer<Resource> UiLocalizer
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" /> <link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
@@ -696,6 +697,13 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
var hasAccess = await AuthService.CheckSenderAccessAsync();
if (!hasAccess)
{
NavigationManager.NavigateTo("/sender/login");
return;
}
try try
{ {
_envelopeTypes = (await EnvelopeTypeService.GetAsync()).ToList(); _envelopeTypes = (await EnvelopeTypeService.GetAsync()).ToList();
@@ -768,12 +776,14 @@
_finalEmailToCreator = envelope.FinalEmailToCreator ?? (int)FinalEmailType.No; _finalEmailToCreator = envelope.FinalEmailToCreator ?? (int)FinalEmailType.No;
_finalEmailToReceivers = envelope.FinalEmailToReceivers ?? (int)FinalEmailType.No; _finalEmailToReceivers = envelope.FinalEmailToReceivers ?? (int)FinalEmailType.No;
_sendReminderEmails = envelope.EnvelopeType?.SendReminderEmails ?? false; _sendReminderEmails = envelope.SendReminderEmails ?? envelope.EnvelopeType?.SendReminderEmails ?? false;
_firstReminderDays = envelope.EnvelopeType?.FirstReminderDays ?? 0; _firstReminderDays = envelope.FirstReminderDays ?? envelope.EnvelopeType?.FirstReminderDays ?? 0;
_reminderIntervalDays = envelope.EnvelopeType?.ReminderIntervalDays ?? 0; _reminderIntervalDays = envelope.ReminderIntervalDays ?? envelope.EnvelopeType?.ReminderIntervalDays ?? 0;
_expiresWhenDays = envelope.ExpiresWhenDays ?? envelope.EnvelopeType?.ExpiresDays ?? 0; _expiresWhenDays = envelope.ExpiresWhenDays ?? envelope.EnvelopeType?.ExpiresDays ?? 0;
_expiresWarningWhenDays = envelope.ExpiresWarningWhenDays ?? envelope.EnvelopeType?.ExpiresWarningDays ?? 0; _expiresWarningWhenDays = envelope.ExpiresWarningWhenDays ?? envelope.EnvelopeType?.ExpiresWarningDays ?? 0;
NormalizeSettings();
var envelopeReceivers = envelope.EnvelopeReceivers?.ToList() ?? []; var envelopeReceivers = envelope.EnvelopeReceivers?.ToList() ?? [];
_receivers = envelopeReceivers _receivers = envelopeReceivers
.Select((er, index) => new ReceiverDraft( .Select((er, index) => new ReceiverDraft(
@@ -962,7 +972,7 @@
void OnTfaChanged(ChangeEventArgs e) void OnTfaChanged(ChangeEventArgs e)
{ {
_tfaEnabled = e.Value as bool? ?? false; _tfaEnabled = ParseCheckboxValue(e);
if (_tfaEnabled) if (_tfaEnabled)
_useAccessCode = true; _useAccessCode = true;
} }
@@ -975,12 +985,20 @@
return; return;
} }
_useAccessCode = e.Value as bool? ?? false; _useAccessCode = ParseCheckboxValue(e);
} }
void OnSendReminderEmailsChanged(ChangeEventArgs e) void OnSendReminderEmailsChanged(ChangeEventArgs e)
{ {
_sendReminderEmails = e.Value as bool? ?? false; _sendReminderEmails = ParseCheckboxValue(e);
}
static bool ParseCheckboxValue(ChangeEventArgs e)
{
if (e.Value is bool b)
return b;
return bool.TryParse(e.Value?.ToString(), out var parsed) && parsed;
} }
// ── PDF area click → place field ── // ── PDF area click → place field ──
@@ -1100,6 +1118,13 @@
async Task SaveDraftAsync() async Task SaveDraftAsync()
{ {
if (!ValidateSettings(out var settingsError))
{
_saveErrorMessage = settingsError;
_savePopupVisible = true;
return;
}
_isSaving = true; _isSaving = true;
_saveErrorMessage = null; _saveErrorMessage = null;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
@@ -1140,6 +1165,13 @@
// ── Send envelope ── // ── Send envelope ──
async Task SendAsync() async Task SendAsync()
{ {
if (!ValidateSettings(out var settingsError))
{
_saveErrorMessage = settingsError;
_savePopupVisible = true;
return;
}
// ── Validation ── // ── Validation ──
_titleTouched = true; _titleTouched = true;
if (!_pdfLoaded || _originalPdfBytes is null) if (!_pdfLoaded || _originalPdfBytes is null)
@@ -1227,6 +1259,8 @@
CreateEnvelopeCommand BuildCreateEnvelopeCommand(bool send) CreateEnvelopeCommand BuildCreateEnvelopeCommand(bool send)
{ {
NormalizeSettings();
var envelopeId = _editingEnvelopeId var envelopeId = _editingEnvelopeId
?? EnvelopeId ?? EnvelopeId
?? _loadedEnvelopeId ?? _loadedEnvelopeId
@@ -1279,6 +1313,48 @@
}; };
} }
void NormalizeSettings()
{
_language = string.IsNullOrWhiteSpace(_language) ? "de-DE" : _language.Trim();
if (_tfaEnabled)
_useAccessCode = true;
_firstReminderDays = Math.Max(0, _firstReminderDays);
_reminderIntervalDays = Math.Max(0, _reminderIntervalDays);
_expiresWhenDays = Math.Max(0, _expiresWhenDays);
_expiresWarningWhenDays = Math.Max(0, _expiresWarningWhenDays);
if (_expiresWhenDays > 0 && _expiresWarningWhenDays > _expiresWhenDays)
_expiresWarningWhenDays = _expiresWhenDays;
}
bool ValidateSettings(out string? message)
{
NormalizeSettings();
if (_sendReminderEmails && _reminderIntervalDays <= 0)
{
message = L("SenderEditor_Validation_ReminderInterval", "Bitte geben Sie ein Erinnerungsintervall groesser als 0 Tage ein.");
return false;
}
if (_sendReminderEmails && _firstReminderDays < 0)
{
message = L("SenderEditor_Validation_FirstReminder", "Bitte geben Sie gueltige Tage fuer die erste Erinnerung ein.");
return false;
}
if (_expiresWhenDays > 0 && _expiresWarningWhenDays > _expiresWhenDays)
{
message = L("SenderEditor_Validation_ExpiryWarning", "Die Ablaufwarnung darf nicht nach dem Ablaufdatum liegen.");
return false;
}
message = null;
return true;
}
int? GetEnvelopeIdFromCurrentUri() int? GetEnvelopeIdFromCurrentUri()
{ {
try try