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 MediatR;
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.EntityFrameworkCore;
@@ -18,17 +17,14 @@ public record ReadEnvelopeTypesQuery : IRequest<IEnumerable<EnvelopeTypeDto>>;
public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQuery, IEnumerable<EnvelopeTypeDto>>
{
private readonly IRepository<EnvelopeType> _repository;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="repository"></param>
/// <param name="mapper"></param>
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository, IMapper mapper)
public ReadEnvelopeTypesQueryHandler(IRepository<EnvelopeType> repository)
{
_repository = repository;
_mapper = mapper;
}
/// <summary>
@@ -39,7 +35,28 @@ public class ReadEnvelopeTypesQueryHandler : IRequestHandler<ReadEnvelopeTypesQu
/// <returns></returns>
public async Task<IEnumerable<EnvelopeTypeDto>> Handle(ReadEnvelopeTypesQuery request, CancellationToken cancellationToken)
{
var types = await _repository.Query.ToListAsync(cancellationToken);
return _mapper.Map<IEnumerable<EnvelopeTypeDto>>(types);
// TODO: Temporary manual projection to avoid null-materialization crashes from legacy nullable DB columns in EnvelopeType.
// 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; }
[Column("EXPIRES_DAYS")]
public int ExpiresDays { get; set; }
public int? ExpiresDays { get; set; }
[Column("CERTIFICATION_TYPE")]
public Constants.CertificationType CertificationType { get; set; } = Constants.CertificationType.AdvancedElectronicSignature;
public Constants.CertificationType? CertificationType { get; set; }
[Column("USE_ACCESS_CODE")]
public bool UseAccessCode { get; set; }
public bool? UseAccessCode { get; set; }
[Column("FINAL_EMAIL_TO_CREATOR")]
public Constants.FinalEmailType FinalEmailToCreator { get; set; }
public Constants.FinalEmailType? FinalEmailToCreator { get; set; }
[Column("FINAL_EMAIL_TO_RECEIVERS")]
public Constants.FinalEmailType FinalEmailToReceivers { get; set; }
public Constants.FinalEmailType? FinalEmailToReceivers { get; set; }
[Required]
[Column("ADDED_WHEN", TypeName = "datetime")]
public DateTime AddedWhen { get; set; }
[Column("CHANGED_WHEN", TypeName = "datetime")]
public DateTime ChangedWhen { get; set; }
public DateTime? ChangedWhen { get; set; }
[Column("EXPIRES_WARNING_DAYS")]
public int ExpiresWarningDays { get; set; }
public int? ExpiresWarningDays { get; set; }
[Column("SEND_REMINDER_EMAILS")]
public bool SendReminderEmails { get; set; } = false;
public bool? SendReminderEmails { get; set; }
[Column("FIRST_REMINDER_DAYS")]
public int FirstReminderDays { get; set; } = 0;
public int? FirstReminderDays { get; set; }
[Column("REMINDER_INTERVAL_DAYS")]
public int ReminderIntervalDays { get; set; } = 0;
public int? ReminderIntervalDays { get; set; }
[Column("CONTRACT_TYPE")]
public int ContractType { get; set; }
public int? ContractType { get; set; }
[NotMapped]
public bool TFA_Enabled { get; set; } = false;
@@ -70,14 +70,14 @@ namespace EnvelopeGenerator.Domain.Entities
[NotMapped]
public int ExpiresWhenDays
{
get => ExpiresDays;
get => ExpiresDays ?? 0;
set => ExpiresDays = value;
}
[NotMapped]
public int ExpiresWarningWhenDays
{
get => ExpiresWarningDays;
get => ExpiresWarningDays ?? 0;
set => ExpiresWarningDays = value;
}
@@ -86,4 +86,4 @@ namespace EnvelopeGenerator.Domain.Entities
return Title;
}
}
}
}

View File

@@ -20,6 +20,7 @@
@inject ReceiverService ReceiverService
@inject EnvelopeService EnvelopeService
@inject EnvelopeTypeService EnvelopeTypeService
@inject AuthService AuthService
@inject IStringLocalizer<Resource> UiLocalizer
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
@@ -696,6 +697,13 @@
protected override async Task OnInitializedAsync()
{
var hasAccess = await AuthService.CheckSenderAccessAsync();
if (!hasAccess)
{
NavigationManager.NavigateTo("/sender/login");
return;
}
try
{
_envelopeTypes = (await EnvelopeTypeService.GetAsync()).ToList();
@@ -768,12 +776,14 @@
_finalEmailToCreator = envelope.FinalEmailToCreator ?? (int)FinalEmailType.No;
_finalEmailToReceivers = envelope.FinalEmailToReceivers ?? (int)FinalEmailType.No;
_sendReminderEmails = envelope.EnvelopeType?.SendReminderEmails ?? false;
_firstReminderDays = envelope.EnvelopeType?.FirstReminderDays ?? 0;
_reminderIntervalDays = envelope.EnvelopeType?.ReminderIntervalDays ?? 0;
_sendReminderEmails = envelope.SendReminderEmails ?? envelope.EnvelopeType?.SendReminderEmails ?? false;
_firstReminderDays = envelope.FirstReminderDays ?? envelope.EnvelopeType?.FirstReminderDays ?? 0;
_reminderIntervalDays = envelope.ReminderIntervalDays ?? envelope.EnvelopeType?.ReminderIntervalDays ?? 0;
_expiresWhenDays = envelope.ExpiresWhenDays ?? envelope.EnvelopeType?.ExpiresDays ?? 0;
_expiresWarningWhenDays = envelope.ExpiresWarningWhenDays ?? envelope.EnvelopeType?.ExpiresWarningDays ?? 0;
NormalizeSettings();
var envelopeReceivers = envelope.EnvelopeReceivers?.ToList() ?? [];
_receivers = envelopeReceivers
.Select((er, index) => new ReceiverDraft(
@@ -962,7 +972,7 @@
void OnTfaChanged(ChangeEventArgs e)
{
_tfaEnabled = e.Value as bool? ?? false;
_tfaEnabled = ParseCheckboxValue(e);
if (_tfaEnabled)
_useAccessCode = true;
}
@@ -975,12 +985,20 @@
return;
}
_useAccessCode = e.Value as bool? ?? false;
_useAccessCode = ParseCheckboxValue(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 ──
@@ -1100,6 +1118,13 @@
async Task SaveDraftAsync()
{
if (!ValidateSettings(out var settingsError))
{
_saveErrorMessage = settingsError;
_savePopupVisible = true;
return;
}
_isSaving = true;
_saveErrorMessage = null;
await InvokeAsync(StateHasChanged);
@@ -1140,6 +1165,13 @@
// ── Send envelope ──
async Task SendAsync()
{
if (!ValidateSettings(out var settingsError))
{
_saveErrorMessage = settingsError;
_savePopupVisible = true;
return;
}
// ── Validation ──
_titleTouched = true;
if (!_pdfLoaded || _originalPdfBytes is null)
@@ -1227,6 +1259,8 @@
CreateEnvelopeCommand BuildCreateEnvelopeCommand(bool send)
{
NormalizeSettings();
var envelopeId = _editingEnvelopeId
?? EnvelopeId
?? _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()
{
try