2026-08-11 14:52:02 -03:00
|
|
|
using System.Security.Claims;
|
2026-07-13 13:13:00 -03:00
|
|
|
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
|
|
|
|
|
{
|
2026-07-24 17:35:34 -03:00
|
|
|
private readonly IWorkOrderCompletionDataService _completionData;
|
2026-07-13 13:13:00 -03:00
|
|
|
private readonly ICompletionDocTemplateDataService _templateData;
|
|
|
|
|
private readonly IWorkOrderDetailDataService _detailData;
|
|
|
|
|
private readonly IWorkOrderAuditService _auditService;
|
2026-08-11 14:52:02 -03:00
|
|
|
private readonly IWorkOrderAccountResolver _accountResolver;
|
2026-07-13 13:13:00 -03:00
|
|
|
|
|
|
|
|
public WorkOrderCompletionService(
|
2026-07-24 17:35:34 -03:00
|
|
|
IWorkOrderCompletionDataService completionData,
|
2026-07-13 13:13:00 -03:00
|
|
|
ICompletionDocTemplateDataService templateData,
|
|
|
|
|
IWorkOrderDetailDataService detailData,
|
2026-08-11 14:52:02 -03:00
|
|
|
IWorkOrderAuditService auditService,
|
|
|
|
|
IWorkOrderAccountResolver accountResolver)
|
2026-07-13 13:13:00 -03:00
|
|
|
{
|
2026-07-24 17:35:34 -03:00
|
|
|
_completionData = completionData;
|
2026-07-13 13:13:00 -03:00
|
|
|
_templateData = templateData;
|
|
|
|
|
_detailData = detailData;
|
|
|
|
|
_auditService = auditService;
|
2026-08-11 14:52:02 -03:00
|
|
|
_accountResolver = accountResolver;
|
2026-07-13 13:13:00 -03:00
|
|
|
}
|
|
|
|
|
|
2026-08-11 14:52:02 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-13 13:13:00 -03:00
|
|
|
public async Task<WorkOrderCompletionDto> UploadCompletionDocAsync(
|
|
|
|
|
int workOrderId,
|
|
|
|
|
WorkOrderCompletionDocUploadDto request,
|
|
|
|
|
string fileUrl,
|
2026-08-11 14:52:02 -03:00
|
|
|
ClaimsPrincipal user,
|
|
|
|
|
string? actorId,
|
|
|
|
|
CancellationToken cancellationToken = default)
|
2026-07-13 13:13:00 -03:00
|
|
|
{
|
2026-08-11 14:52:02 -03:00
|
|
|
var accountId = _accountResolver.ResolveAccountFilter(user);
|
|
|
|
|
var workOrder = await _completionData.GetTrackedWorkOrderAsync(
|
|
|
|
|
workOrderId, accountId, cancellationToken);
|
2026-07-13 13:13:00 -03:00
|
|
|
|
|
|
|
|
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.");
|
|
|
|
|
|
2026-07-24 17:35:34 -03:00
|
|
|
_completionData.SetExpectedWorkOrderVersion(workOrder, workOrderVersion);
|
2026-07-13 13:13:00 -03:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-07-24 14:20:16 -03:00
|
|
|
await _auditService.StageFieldChangedAsync(workOrderId, "SignOffAttachment", oldAttachment, fileUrl, actorId);
|
2026-07-13 13:13:00 -03:00
|
|
|
if (oldDocStatus != DocStatus.Yes.ToString())
|
2026-07-24 14:20:16 -03:00
|
|
|
await _auditService.StageFieldChangedAsync(workOrderId, "DocStatus", oldDocStatus, DocStatus.Yes.ToString(), actorId);
|
2026-07-13 13:13:00 -03:00
|
|
|
|
2026-08-11 14:52:02 -03:00
|
|
|
await _completionData.SaveAsync(cancellationToken);
|
2026-07-13 13:13:00 -03:00
|
|
|
|
|
|
|
|
var extended = await _detailData.GetExtendedFieldsAsync(workOrderId);
|
|
|
|
|
var template = await _templateData.ResolveForWorkOrderAsync(workOrder.Trade, 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()
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-11 14:52:02 -03:00
|
|
|
private async Task<WorkOrder> 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-13 13:13:00 -03:00
|
|
|
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.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|