mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 20:03:11 +00:00
Reconcile migration/docs deletions from phase 2, adopt async audit staging and ApptTime lock contracts, and keep phase 3 create/cancel transactional behavior.
227 lines
11 KiB
C#
227 lines
11 KiB
C#
using Data.SeaHavenIndustries;
|
|
using Data.SeaHavenIndustries.Enums;
|
|
using FluentValidation;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
using SeaHaven.Services.DTOs;
|
|
using SeaHaven.Services.Exceptions;
|
|
using SeaHaven.Services.Helpers;
|
|
using SeaHaven.Services.Interfaces;
|
|
using SeaHaven.Services.Validation;
|
|
|
|
namespace SeaHaven.Services.Implementation
|
|
{
|
|
public class WorkOrderBoardCreateService : IWorkOrderBoardCreateService
|
|
{
|
|
private readonly ApplicationDbContext _context;
|
|
private readonly IWorkOrderBoardDataService _boardDataService;
|
|
private readonly IWorkOrderBoardService _boardService;
|
|
private readonly IWorkOrderAuditService _auditService;
|
|
private readonly IWorkOrderBoardCreateValidation _validator;
|
|
|
|
public WorkOrderBoardCreateService(
|
|
ApplicationDbContext context,
|
|
IWorkOrderBoardDataService boardDataService,
|
|
IWorkOrderBoardService boardService,
|
|
IWorkOrderAuditService auditService,
|
|
IWorkOrderBoardCreateValidation validator)
|
|
{
|
|
_context = context;
|
|
_boardDataService = boardDataService;
|
|
_boardService = boardService;
|
|
_auditService = auditService;
|
|
_validator = validator;
|
|
}
|
|
|
|
public async Task<WorkOrderBoardRowDto> CreateAsync(WorkOrderBoardCreateRequestDto request, string? actorId)
|
|
{
|
|
var validationResult = await _validator.ValidateAsync(request);
|
|
if (!validationResult.IsValid)
|
|
throw new ValidationException(validationResult.Errors);
|
|
|
|
var woNumber = await ResolveWoNumberAsync(request.WoNumber);
|
|
var siteCode = request.SiteCode!.Trim();
|
|
|
|
var workOrder = new WorkOrder
|
|
{
|
|
InternalWONumber = woNumber,
|
|
WorkerOrderNumber = woNumber,
|
|
WorkOrderType = request.WorkOrderType,
|
|
SiteCode = siteCode,
|
|
Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim(),
|
|
Trade = string.IsNullOrWhiteSpace(request.Trade) ? null : request.Trade.Trim(),
|
|
LocationId = request.LocationId,
|
|
DueDate = request.DueDate?.Date,
|
|
AssignTo = string.IsNullOrWhiteSpace(request.AssignTo) ? null : request.AssignTo.Trim(),
|
|
TargetWeek = request.TargetWeek,
|
|
ScheduleWeekOnly = request.ScheduleWeekOnly,
|
|
DocStatus = request.DocStatus,
|
|
createdby = actorId,
|
|
CreatedDate = DateTime.UtcNow,
|
|
istemplate = false
|
|
};
|
|
|
|
WorkOrderBoardFieldMutations.SetInitialLifecycleStatus(workOrder);
|
|
|
|
var changes = new List<WorkOrderBoardFieldMutations.BoardFieldChange>
|
|
{
|
|
WorkOrderBoardFieldMutations.BoardFieldChange.Changed("InternalWONumber", "", woNumber),
|
|
WorkOrderBoardFieldMutations.BoardFieldChange.Changed("WorkOrderType", "", request.WorkOrderType.ToString()),
|
|
WorkOrderBoardFieldMutations.BoardFieldChange.Changed("SiteCode", "", siteCode),
|
|
WorkOrderBoardFieldMutations.BoardFieldChange.Changed("LifecycleStatus", "", LifecycleStatus.Incomplete.ToString())
|
|
};
|
|
|
|
if (workOrder.Description != null)
|
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("Description", "", workOrder.Description));
|
|
if (workOrder.Trade != null)
|
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("Trade", "", workOrder.Trade));
|
|
if (workOrder.LocationId.HasValue)
|
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("LocationId", "", workOrder.LocationId.Value.ToString()));
|
|
if (workOrder.DueDate.HasValue)
|
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("DueDate", "", workOrder.DueDate.Value.ToString("yyyy-MM-dd")));
|
|
if (workOrder.TargetWeek.HasValue)
|
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("TargetWeek", "", workOrder.TargetWeek.Value.ToString("yyyy-MM-dd")));
|
|
if (workOrder.ScheduleWeekOnly.HasValue)
|
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("ScheduleWeekOnly", "", workOrder.ScheduleWeekOnly.Value.ToString()));
|
|
if (workOrder.DocStatus.HasValue)
|
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("DocStatus", "", workOrder.DocStatus.Value.ToString()));
|
|
|
|
if (!string.IsNullOrWhiteSpace(request.AssignTo))
|
|
changes.AddRange(WorkOrderBoardFieldMutations.ApplyAssignTo(workOrder, request.AssignTo));
|
|
else if (!string.IsNullOrWhiteSpace(workOrder.AssignTo))
|
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.AssignmentChanged("AssignTo", "", workOrder.AssignTo));
|
|
|
|
if (request.ScheduledDate.HasValue)
|
|
changes.AddRange(WorkOrderBoardFieldMutations.ApplyScheduledDate(workOrder, request.ScheduledDate.Value.Date));
|
|
|
|
Dispatch? dispatch = null;
|
|
if (request.VendorId.HasValue || request.ApptDate.HasValue || !string.IsNullOrWhiteSpace(request.ApptTime))
|
|
{
|
|
if (!request.VendorId.HasValue || request.VendorId.Value <= 0)
|
|
throw new WorkOrderBoardValidationException("DispatchRequired", "vendorId is required when setting appointment fields.");
|
|
|
|
dispatch = new Dispatch
|
|
{
|
|
VendorId = request.VendorId.Value,
|
|
Status = "Pending",
|
|
CreatedDate = DateTime.UtcNow,
|
|
ScheduledDate = request.ApptDate?.Date
|
|
};
|
|
_context.Dispatches.Add(dispatch);
|
|
workOrder.PrimaryDispatch = dispatch;
|
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("VendorId", "", request.VendorId.Value.ToString()));
|
|
|
|
if (request.ApptDate.HasValue)
|
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("ApptDate", "", request.ApptDate.Value.ToString("yyyy-MM-dd")));
|
|
|
|
if (!string.IsNullOrWhiteSpace(request.ApptTime))
|
|
{
|
|
var apptChange = WorkOrderBoardFieldMutations.ApplyApptTime(workOrder, dispatch, request.ApptTime);
|
|
if (apptChange.HasChanged)
|
|
changes.Add(apptChange);
|
|
}
|
|
}
|
|
|
|
// Two SaveChanges are required so generated WO/dispatch IDs can backfill FKs and
|
|
// audit/lock/contact rows. A transaction keeps the create atomic across both saves.
|
|
await using var transaction = _context.Database.IsRelational()
|
|
? await _context.Database.BeginTransactionAsync()
|
|
: null;
|
|
|
|
try
|
|
{
|
|
_context.workOrders.Add(workOrder);
|
|
await _context.SaveChangesAsync();
|
|
|
|
if (dispatch != null && workOrder.PrimaryDispatchId == null)
|
|
{
|
|
workOrder.PrimaryDispatchId = dispatch.Id;
|
|
dispatch.WorkOrderId = workOrder.Id;
|
|
}
|
|
|
|
await _auditService.StageCreatedAsync(workOrder.Id, actorId, woNumber);
|
|
await StageChangesAsync(workOrder.Id, changes, actorId, dispatch);
|
|
|
|
if (request.PocContactId.HasValue)
|
|
{
|
|
_context.WorkOrderContacts.Add(new WorkOrderContacts
|
|
{
|
|
WorkorderId = workOrder.Id,
|
|
ContactId = request.PocContactId,
|
|
Notes = string.IsNullOrWhiteSpace(request.PocNotes) ? null : request.PocNotes.Trim()
|
|
});
|
|
}
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
if (transaction is not null)
|
|
await transaction.CommitAsync();
|
|
}
|
|
catch
|
|
{
|
|
if (transaction is not null)
|
|
await transaction.RollbackAsync();
|
|
throw;
|
|
}
|
|
|
|
var row = await _boardService.GetBoardRowAsync(workOrder.Id);
|
|
return row ?? throw new WorkOrderBoardValidationException("NotFound", "Work order was created but could not be loaded.");
|
|
}
|
|
|
|
private async Task<string> ResolveWoNumberAsync(string? requested)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(requested))
|
|
{
|
|
if (!WorkOrderNumberNormalizer.TryNormalize(requested, out var normalized, out var error))
|
|
throw new WorkOrderBoardValidationException("InvalidWoNumber", error ?? "Invalid WO number.");
|
|
|
|
if (await _boardDataService.InternalWoNumberExistsAsync(normalized, 0))
|
|
throw new WorkOrderBoardValidationException("DuplicateWoNumber", "WO number already exists.");
|
|
|
|
return normalized;
|
|
}
|
|
|
|
for (var attempt = 0; attempt < 20; attempt++)
|
|
{
|
|
var candidate = await GenerateNextSequentialWoNumberAsync();
|
|
if (!await _boardDataService.InternalWoNumberExistsAsync(candidate, 0))
|
|
return candidate;
|
|
}
|
|
|
|
throw new WorkOrderBoardValidationException("WoNumberGenerationFailed", "Could not generate a unique WO number.");
|
|
}
|
|
|
|
private async Task<string> GenerateNextSequentialWoNumberAsync()
|
|
{
|
|
var maxId = await _context.workOrders.MaxAsync(w => (int?)w.Id) ?? 0;
|
|
var seed = Math.Max(maxId + 1, 1);
|
|
if (!WorkOrderNumberNormalizer.TryNormalize(seed.ToString(), out var normalized, out _))
|
|
normalized = seed.ToString().PadLeft(11, '0');
|
|
return normalized;
|
|
}
|
|
|
|
private async Task StageChangesAsync(
|
|
int workOrderId,
|
|
IEnumerable<WorkOrderBoardFieldMutations.BoardFieldChange> changes,
|
|
string? actorId,
|
|
Dispatch? dispatch)
|
|
{
|
|
foreach (var change in changes.Where(c => c.HasChanged))
|
|
{
|
|
var dispatchId = change.DispatchId ?? (dispatch?.Id > 0 ? dispatch.Id : null);
|
|
switch (change.Action)
|
|
{
|
|
case AuditActionType.StatusChanged:
|
|
await _auditService.StageStatusChangedAsync(workOrderId, change.OldValue, change.NewValue, actorId);
|
|
break;
|
|
case AuditActionType.AssignmentChanged:
|
|
await _auditService.StageAssignmentChangedAsync(workOrderId, change.OldValue, change.NewValue, actorId);
|
|
break;
|
|
default:
|
|
await _auditService.StageFieldChangedAsync(workOrderId, change.FieldName, change.OldValue ?? "", change.NewValue, actorId, dispatchId);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|