-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathRequestCardReplacementCommandHandler.cs
More file actions
107 lines (91 loc) · 4.03 KB
/
RequestCardReplacementCommandHandler.cs
File metadata and controls
107 lines (91 loc) · 4.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
using Microsoft.Extensions.Logging;
using SEBT.Portal.Core.Models;
using SEBT.Portal.Core.Models.Auth;
using SEBT.Portal.Core.Repositories;
using SEBT.Portal.Core.Services;
using SEBT.Portal.Kernel;
using SEBT.Portal.Kernel.Results;
namespace SEBT.Portal.UseCases.Household;
/// <summary>
/// Handles card replacement requests for an authenticated user's household.
/// Validates input, resolves household identity, enforces 2-week cooldown, and returns success.
/// State connector call is stubbed — actual card replacement is a future integration.
/// </summary>
public class RequestCardReplacementCommandHandler(
IValidator<RequestCardReplacementCommand> validator,
IHouseholdIdentifierResolver resolver,
IHouseholdRepository repository,
ILogger<RequestCardReplacementCommandHandler> logger)
: ICommandHandler<RequestCardReplacementCommand>
{
private static readonly TimeSpan CooldownPeriod = TimeSpan.FromDays(14);
public async Task<Result> Handle(
RequestCardReplacementCommand command,
CancellationToken cancellationToken = default)
{
var validationResult = await validator.Validate(command, cancellationToken);
if (validationResult is ValidationFailedResult validationFailed)
{
logger.LogWarning("Card replacement validation failed");
return Result.ValidationFailed(validationFailed.Errors);
}
var identifier = await resolver.ResolveAsync(command.User, cancellationToken);
if (identifier == null)
{
logger.LogWarning(
"Card replacement attempted but no household identifier could be resolved from claims");
return Result.Unauthorized("Unable to identify user from token.");
}
var household = await repository.GetHouseholdByIdentifierAsync(
identifier,
new PiiVisibility(IncludeAddress: false, IncludeEmail: false, IncludePhone: false),
UserIalLevel.None,
cancellationToken);
if (household == null)
{
logger.LogWarning("Card replacement attempted but household data not found");
return Result.PreconditionFailed(PreconditionFailedReason.NotFound, "Household data not found.");
}
var cooldownErrors = CheckCooldown(command.ApplicationNumbers, household);
if (cooldownErrors.Count > 0)
{
logger.LogInformation(
"Card replacement rejected: {Count} application(s) within cooldown period",
cooldownErrors.Count);
return Result.ValidationFailed(cooldownErrors);
}
var identifierKind = identifier.Type.ToString();
logger.LogInformation(
"Card replacement request received for household identifier kind {Kind}, {Count} application(s)",
identifierKind,
command.ApplicationNumbers.Count);
// TODO: Call state connector to process card replacement.
// Stubbed — returns success without calling the state system.
logger.LogInformation(
"Card replacement completed for household identifier kind {Kind}",
identifierKind);
return Result.Success();
}
private static List<ValidationError> CheckCooldown(
List<string> requestedApplicationNumbers,
Core.Models.Household.HouseholdData household)
{
var errors = new List<ValidationError>();
var now = DateTime.UtcNow;
foreach (var appNumber in requestedApplicationNumbers)
{
var application = household.Applications
.FirstOrDefault(a => a.ApplicationNumber == appNumber);
if (application?.CardRequestedAt == null)
continue;
var elapsed = now - application.CardRequestedAt.Value;
if (elapsed < CooldownPeriod)
{
errors.Add(new ValidationError(
"ApplicationNumbers",
$"Application {appNumber} was requested within the last 14 days."));
}
}
return errors;
}
}