namespace New.Domain.ValueObjects; /// /// The decision on an application. Named AssessmentOutcome - never the /// bare word "Status" - to keep it distinct from the case-framework's own /// ProcessStatus and from the legacy `stat_cd` / `beoordRes` codes. /// Also stands in for what the case-framework calls a "Decision" document. /// public enum AssessmentOutcome { Approved, Rejected, } /// /// A recorded assessment of a . Only ever /// constructed through , which enforces every invariant /// so the same validation applies whether the assessment is entered natively /// through the owned path or reconstructed from legacy data during adoption. /// public sealed record Assessment { private const int DefaultMinimumMotivationLength = 20; private const int OtherCategoryMinimumMotivationLength = 50; public AssessmentOutcome Outcome { get; } public string Motivation { get; } public IReadOnlyList VerifiedItems { get; } public string? ExceptionReason { get; } public string? RejectionCategory { get; } public DateOnly DecidedOn { get; } private Assessment( AssessmentOutcome outcome, string motivation, IReadOnlyList verifiedItems, string? exceptionReason, string? rejectionCategory, DateOnly decidedOn) { Outcome = outcome; Motivation = motivation; VerifiedItems = verifiedItems; ExceptionReason = exceptionReason; RejectionCategory = rejectionCategory; DecidedOn = decidedOn; } public static Assessment Create( AssessmentOutcome outcome, string motivation, IReadOnlyList? verifiedItems, string? exceptionReason, string? rejectionCategory, DateOnly decidedOn) { var items = verifiedItems ?? Array.Empty(); // Recording an assessment requires either every diploma evidence item // to have been verified, or a recorded reason why verification was // skipped - never neither. if (items.Count == 0 && string.IsNullOrWhiteSpace(exceptionReason)) { throw new DomainInvariantViolationException( "Assessment.VerificationRequired", "Recording an assessment requires either at least one verified item or a recorded exception reason."); } // rejectionCategory only makes sense - and is only carried - alongside // a Rejected outcome. var normalizedRejectionCategory = outcome == AssessmentOutcome.Rejected ? rejectionCategory : null; if (outcome == AssessmentOutcome.Rejected && string.IsNullOrWhiteSpace(normalizedRejectionCategory)) { throw new DomainInvariantViolationException( "Assessment.RejectionCategoryRequired", "A rejection category is required when the outcome is Rejected."); } var minimumLength = IsOtherCategory(normalizedRejectionCategory) ? OtherCategoryMinimumMotivationLength : DefaultMinimumMotivationLength; if (string.IsNullOrWhiteSpace(motivation) || motivation.Trim().Length < minimumLength) { throw new DomainInvariantViolationException( "Assessment.MotivationTooShort", $"The motivation must be at least {minimumLength} characters long" + (IsOtherCategory(normalizedRejectionCategory) ? " when the rejection category is 'Other'." : ".")); } return new Assessment(outcome, motivation, items, exceptionReason, normalizedRejectionCategory, decidedOn); } private static bool IsOtherCategory(string? rejectionCategory) => rejectionCategory is not null && (string.Equals(rejectionCategory, "Other", StringComparison.OrdinalIgnoreCase) || string.Equals(rejectionCategory, "anders", StringComparison.OrdinalIgnoreCase)); }