using System.Security.Claims; using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.DTOs; using SeaHaven.Services.Exceptions; using SeaHaven.Services.Helpers; using SeaHaven.Services.Interfaces; namespace SeaHaven.Services.Implementation { public class WorkOrderCompletionService : IWorkOrderCompletionService { private readonly IWorkOrderCompletionDataService _completionData; private readonly ICompletionDocTemplateDataService _templateData; private readonly IWorkOrderDetailDataService _detailData; private readonly IWorkOrderAuditService _auditService; private readonly IWorkOrderAccountResolver _accountResolver; public WorkOrderCompletionService( IWorkOrderCompletionDataService completionData, ICompletionDocTemplateDataService templateData, IWorkOrderDetailDataService detailData, IWorkOrderAuditService auditService, IWorkOrderAccountResolver accountResolver) { _completionData = completionData; _templateData = templateData; _detailData = detailData; _auditService = auditService; _accountResolver = accountResolver; } public async Task EnsureCanUploadCompletionDocAsync( int workOrderId, ClaimsPrincipal user, string? actorId, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(actorId) || user?.Identity?.IsAuthenticated != true) { throw new WorkOrderBoardValidationException( "Forbidden", "You are not allowed to upload completion documents."); } // AsNoTracking pre-check so UploadCompletionDocAsync load is not stale-cached. await GetMutableWorkOrderForAuthAsync(workOrderId, user, cancellationToken); } public async Task UploadCompletionDocAsync( int workOrderId, WorkOrderCompletionDocUploadDto request, string fileUrl, ClaimsPrincipal user, string? actorId, CancellationToken cancellationToken = default) { var accountId = _accountResolver.ResolveAccountFilter(user); var workOrder = await _completionData.GetTrackedWorkOrderAsync( workOrderId, accountId, cancellationToken); if (workOrder == null) throw new WorkOrderBoardValidationException("NotFound", "Work order not found."); if (WorkOrderBoardMutationRules.IsReadOnly(workOrder.LifecycleStatus)) throw new WorkOrderBoardValidationException("ReadOnly", "Work order is read-only in its current status."); var workOrderVersion = ParseRowVersion(request.WorkOrderVersion); if (workOrderVersion == null) throw new WorkOrderBoardValidationException("WorkOrderVersionRequired", "workOrderVersion is required."); if (workOrder.RowVersion == null || !workOrder.RowVersion.AsSpan().SequenceEqual(workOrderVersion)) throw new WorkOrderBoardValidationException("ConcurrencyConflict", "Work order was modified. Refresh and retry."); _completionData.SetExpectedWorkOrderVersion(workOrder, workOrderVersion); var oldDocStatus = workOrder.DocStatus?.ToString(); var oldAttachment = workOrder.SignOffAttachment; workOrder.SignOffAttachment = fileUrl; if (!string.IsNullOrWhiteSpace(request.SignOffName)) workOrder.SignOffName = request.SignOffName.Trim(); if (!string.IsNullOrWhiteSpace(request.SignOffSignature)) workOrder.SignOffSignature = request.SignOffSignature.Trim(); workOrder.DocStatus = DocStatus.Yes; await _auditService.StageFieldChangedAsync(workOrderId, "SignOffAttachment", oldAttachment, fileUrl, actorId); if (oldDocStatus != DocStatus.Yes.ToString()) await _auditService.StageFieldChangedAsync(workOrderId, "DocStatus", oldDocStatus, DocStatus.Yes.ToString(), actorId); await _completionData.SaveAsync(cancellationToken); var extended = await _detailData.GetExtendedFieldsAsync(workOrderId); var template = await _templateData.ResolveForWorkOrderAsync( workOrder.Trade, WorkOrderCatalogType.For(workOrder.WorkOrderType)); var signoffs = await _detailData.GetDispatchSignoffsAsync(workOrderId); return new WorkOrderCompletionDto { DocStatus = workOrder.DocStatus, Template = template == null ? null : WorkOrderDetailService.MapTemplate(template), SignOffName = extended?.SignOffName, SignOffAttachment = extended?.SignOffAttachment, SignOffSignature = extended?.SignOffSignature, DispatchSignoffs = signoffs.Select(s => new DispatchSignoffDto { DispatchId = s.DispatchId, SignoffType = s.SignoffType, Name = s.Name, SignedAt = s.SignedAt?.ToUniversalTime().ToString("o") }).ToList() }; } private async Task GetMutableWorkOrderForAuthAsync( int workOrderId, ClaimsPrincipal user, CancellationToken cancellationToken) { var accountId = _accountResolver.ResolveAccountFilter(user); var workOrder = await _completionData.GetWorkOrderForCompletionAuthAsync( workOrderId, accountId, cancellationToken); if (workOrder == null) throw new WorkOrderBoardValidationException("NotFound", "Work order not found."); if (WorkOrderBoardMutationRules.IsReadOnly(workOrder.LifecycleStatus)) throw new WorkOrderBoardValidationException("ReadOnly", "Work order is read-only in its current status."); return workOrder; } private static byte[]? ParseRowVersion(string? base64) { if (string.IsNullOrWhiteSpace(base64)) return null; try { return Convert.FromBase64String(base64); } catch (FormatException) { throw new WorkOrderBoardValidationException("InvalidRowVersion", "Invalid workOrderVersion format."); } } } }