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> GetTemplatesAsync(string? serviceKey, WorkOrderType? workOrderType) { var rows = await _templateData.GetActiveAsync(serviceKey, WorkOrderCatalogType.For(workOrderType)); return rows.Select(WorkOrderDetailService.MapTemplate).ToList(); } public async Task GetTemplateByIdAsync(int id) { var row = await _templateData.GetByIdAsync(id); return row == null ? null : WorkOrderDetailService.MapTemplate(row); } 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() }; } public async Task CreateTemplateAsync(CompletionDocTemplateCreateDto request) { ValidateTemplateRequest(request); var entity = new CompletionDocTemplate { Name = request.Name.Trim(), ServiceKey = request.ServiceKey.Trim(), WorkOrderType = request.WorkOrderType, TemplateUrl = request.TemplateUrl.Trim(), IsActive = request.IsActive }; var created = await _templateData.CreateAsync(entity); return WorkOrderDetailService.MapTemplate(new CompletionDocTemplateRow( created.Id, created.Name, created.ServiceKey, created.WorkOrderType, created.TemplateUrl, created.IsActive)); } public async Task UpdateTemplateAsync(int id, CompletionDocTemplateCreateDto request) { ValidateTemplateRequest(request); var entity = await _completionData.GetTrackedTemplateAsync(id, CancellationToken.None); if (entity == null) throw new WorkOrderBoardValidationException("NotFound", "Template not found."); entity.Name = request.Name.Trim(); entity.ServiceKey = request.ServiceKey.Trim(); entity.WorkOrderType = request.WorkOrderType; entity.TemplateUrl = request.TemplateUrl.Trim(); entity.IsActive = request.IsActive; await _templateData.UpdateAsync(entity); return WorkOrderDetailService.MapTemplate(new CompletionDocTemplateRow( entity.Id, entity.Name, entity.ServiceKey, entity.WorkOrderType, entity.TemplateUrl, entity.IsActive)); } public async Task DeleteTemplateAsync(int id) { if (!await _templateData.DeleteAsync(id)) throw new WorkOrderBoardValidationException("NotFound", "Template not found."); } 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 void ValidateTemplateRequest(CompletionDocTemplateCreateDto request) { if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.ServiceKey)) throw new WorkOrderBoardValidationException("InvalidValue", "Name and serviceKey are required."); if (string.IsNullOrWhiteSpace(request.TemplateUrl)) throw new WorkOrderBoardValidationException("InvalidValue", "TemplateUrl is required."); if (!Uri.TryCreate(request.TemplateUrl.Trim(), UriKind.Absolute, out var uri) || (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)) { throw new WorkOrderBoardValidationException("InvalidValue", "TemplateUrl must be an absolute http(s) URL."); } } 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."); } } } }