namespace SeaHaven.Services.Helpers { /// /// Server-side Add-On cutoff: Saturday 23:59:59 UTC of the week prior to the schedule week. /// SH-184: recalculated whenever scheduling is set, changed, or cleared; audited on change. /// At creation the WO arrives when it is created; on a reschedule it arrives when it is moved. /// Create may accept a client hint when unscheduled; schedule present ignores override. /// public static class WorkOrderAddonCutoff { /// /// Cutoff = Monday-of(anchor) − 2 days, end of day UTC (prior-week Saturday 23:59:59). /// public static DateTime GetAddonCutoffUtc(DateTime anchor) { var monday = WorkOrderBoardMutationRules.GetWeekStartMonday(anchor); var saturday = monday.Date.AddDays(-2); return DateTime.SpecifyKind( saturday.AddHours(23).AddMinutes(59).AddSeconds(59), DateTimeKind.Utc); } public static DateTime GetAddonCutoffUtc(DateOnly anchor) => GetAddonCutoffUtc(anchor.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc)); /// /// With schedule (ScheduledDate or TargetWeek): IsAddOn when createdAtUtc is after cutoff. /// Without schedule: client (default false). /// public static bool ComputeIsAddOnAtCreation( DateTime createdAtUtc, DateTime? scheduledDate, DateOnly? targetWeek, bool? manualOverride) { return ComputeFromScheduleAnchor(createdAtUtc, scheduledDate, targetWeek, manualOverride); } /// /// SH-412: a reschedule into another week makes the WO arrive in that week at the moment /// of the reschedule, so Add-On compares (not the /// creation time) with the new week's cutoff. A move within the same week keeps the /// current value; a cleared schedule yields false. Matches the prototype's reschedule rule. /// public static bool RecalculateIsAddOnOnReschedule( bool currentIsAddOn, DateTime? previousScheduledDate, DateOnly? previousTargetWeek, DateTime? scheduledDate, DateOnly? targetWeek, DateTime rescheduledAtUtc) { var anchor = ScheduleAnchor(scheduledDate, targetWeek); if (!anchor.HasValue) return false; var previousAnchor = ScheduleAnchor(previousScheduledDate, previousTargetWeek); if (previousAnchor.HasValue && WorkOrderBoardMutationRules.GetWeekStartMonday(previousAnchor.Value) == WorkOrderBoardMutationRules.GetWeekStartMonday(anchor.Value)) return currentIsAddOn; return NormalizeUtc(rescheduledAtUtc) > GetAddonCutoffUtc(anchor.Value); } private static DateTime? ScheduleAnchor(DateTime? scheduledDate, DateOnly? targetWeek) { if (scheduledDate.HasValue) return scheduledDate.Value; if (targetWeek.HasValue) return targetWeek.Value.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc); return null; } private static bool ComputeFromScheduleAnchor( DateTime createdAtUtc, DateTime? scheduledDate, DateOnly? targetWeek, bool? manualOverride) { var anchor = ScheduleAnchor(scheduledDate, targetWeek); if (!anchor.HasValue) return manualOverride ?? false; var cutoff = GetAddonCutoffUtc(anchor.Value); var created = NormalizeUtc(createdAtUtc); return created > cutoff; } private static DateTime NormalizeUtc(DateTime value) { if (value.Kind == DateTimeKind.Utc) return value; if (value.Kind == DateTimeKind.Local) return value.ToUniversalTime(); return DateTime.SpecifyKind(value, DateTimeKind.Utc); } } }