2026-07-13 13:22:11 -03:00
|
|
|
using Data.SeaHavenIndustries;
|
2026-07-24 17:35:34 -03:00
|
|
|
using SeaHaven.DataServices.Interfaces;
|
2026-07-13 13:22:11 -03:00
|
|
|
using SeaHaven.Services.DTOs;
|
|
|
|
|
using SeaHaven.Services.Helpers;
|
|
|
|
|
using SeaHaven.Services.Interfaces;
|
|
|
|
|
|
|
|
|
|
namespace SeaHaven.Services.Implementation
|
|
|
|
|
{
|
|
|
|
|
public class WorkOrderIngestService : IWorkOrderIngestService
|
|
|
|
|
{
|
2026-07-24 17:35:34 -03:00
|
|
|
private readonly IWorkOrderIngestDataService _ingestData;
|
2026-07-13 13:22:11 -03:00
|
|
|
private readonly ISyncFieldMergePolicy _mergePolicy;
|
|
|
|
|
private readonly IWorkOrderFieldLockService _fieldLocks;
|
|
|
|
|
private readonly IWorkOrderAuditService _audit;
|
|
|
|
|
|
|
|
|
|
public WorkOrderIngestService(
|
2026-07-24 17:35:34 -03:00
|
|
|
IWorkOrderIngestDataService ingestData,
|
2026-07-13 13:22:11 -03:00
|
|
|
ISyncFieldMergePolicy mergePolicy,
|
|
|
|
|
IWorkOrderFieldLockService fieldLocks,
|
|
|
|
|
IWorkOrderAuditService audit)
|
|
|
|
|
{
|
2026-07-24 17:35:34 -03:00
|
|
|
_ingestData = ingestData;
|
2026-07-13 13:22:11 -03:00
|
|
|
_mergePolicy = mergePolicy;
|
|
|
|
|
_fieldLocks = fieldLocks;
|
|
|
|
|
_audit = audit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<WorkOrderIngestBatchResultDto> UpsertBatchAsync(
|
|
|
|
|
IReadOnlyList<WorkOrderIngestPayloadDto> items,
|
|
|
|
|
CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var result = new WorkOrderIngestBatchResultDto();
|
|
|
|
|
if (items.Count == 0)
|
|
|
|
|
return result;
|
|
|
|
|
|
2026-07-24 17:35:34 -03:00
|
|
|
await _ingestData.ExecuteTransactionalAsync(async cancellationToken =>
|
2026-07-13 13:22:11 -03:00
|
|
|
{
|
|
|
|
|
var nextSeed = await AllocateNextWoSeedAsync(cancellationToken);
|
|
|
|
|
|
|
|
|
|
foreach (var item in items)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(item.ExternalWorkOrderId))
|
|
|
|
|
{
|
|
|
|
|
result.Skipped++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (upsert, updatedSeed) = await UpsertOneAsync(item, nextSeed, cancellationToken);
|
|
|
|
|
nextSeed = updatedSeed;
|
|
|
|
|
result.Results.Add(upsert);
|
|
|
|
|
if (upsert.Created)
|
|
|
|
|
result.Created++;
|
|
|
|
|
else
|
|
|
|
|
result.Updated++;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-24 17:35:34 -03:00
|
|
|
await _ingestData.SaveAsync(cancellationToken);
|
|
|
|
|
}, cancellationToken);
|
2026-07-13 13:22:11 -03:00
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<(WorkOrderIngestItemResultDto Result, int NextSeed)> UpsertOneAsync(
|
|
|
|
|
WorkOrderIngestPayloadDto item,
|
|
|
|
|
int nextSeed,
|
|
|
|
|
CancellationToken cancellationToken)
|
|
|
|
|
{
|
2026-07-24 17:35:34 -03:00
|
|
|
var existing = await _ingestData.GetTrackedByExternalIdAsync(item.ExternalWorkOrderId, cancellationToken);
|
2026-07-13 13:22:11 -03:00
|
|
|
|
|
|
|
|
var locationId = await ResolveLocationIdAsync(
|
|
|
|
|
item.SiteCode, item.Building, item.Address, cancellationToken);
|
|
|
|
|
|
|
|
|
|
if (existing == null)
|
|
|
|
|
{
|
|
|
|
|
var (woNumber, updatedSeed) = AllocateInternalWoNumber(nextSeed);
|
|
|
|
|
var wo = new WorkOrder
|
|
|
|
|
{
|
|
|
|
|
InternalWONumber = woNumber,
|
|
|
|
|
ExternalWorkOrderId = item.ExternalWorkOrderId,
|
|
|
|
|
WorkerOrderNumber = item.ExternalWorkOrderId,
|
|
|
|
|
WorkerOrderTitle = item.Description,
|
|
|
|
|
Description = item.Description,
|
|
|
|
|
Status = WorkOrderIngestFieldMapper.MapStatus(item.WoStatus) ?? "Open",
|
|
|
|
|
Priority = WorkOrderIngestFieldMapper.MapSeverityToPriority(item.Severity),
|
|
|
|
|
Severity = item.Severity,
|
|
|
|
|
Customer = item.Customer,
|
|
|
|
|
SiteCode = item.SiteCode,
|
|
|
|
|
Building = item.Building,
|
|
|
|
|
LocationId = locationId,
|
|
|
|
|
DueDate = item.DueDate,
|
|
|
|
|
DateReported = item.DateReported,
|
|
|
|
|
ScheduledStart = item.ScheduledStart,
|
|
|
|
|
SourceEmailS3Key = item.SourceEmailS3Key,
|
|
|
|
|
CreatedDate = item.CreatedAt ?? DateTime.UtcNow,
|
|
|
|
|
istemplate = false
|
|
|
|
|
};
|
2026-07-24 17:35:34 -03:00
|
|
|
_ingestData.TrackWorkOrder(wo);
|
|
|
|
|
await _ingestData.SaveAsync(cancellationToken);
|
2026-07-13 13:22:11 -03:00
|
|
|
|
|
|
|
|
return (new WorkOrderIngestItemResultDto
|
|
|
|
|
{
|
|
|
|
|
ExternalWorkOrderId = item.ExternalWorkOrderId,
|
|
|
|
|
WorkOrderId = wo.Id,
|
|
|
|
|
Created = true
|
|
|
|
|
}, updatedSeed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var syncContext = new WorkOrderSyncContext
|
|
|
|
|
{
|
|
|
|
|
WorkOrder = existing,
|
|
|
|
|
FieldLocks = _fieldLocks,
|
|
|
|
|
Audit = _audit
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (item.Description != null)
|
|
|
|
|
{
|
|
|
|
|
await _mergePolicy.TryApplyAsync(syncContext, "Description", item.Description);
|
|
|
|
|
await _mergePolicy.TryApplyAsync(syncContext, "WorkerOrderTitle", item.Description);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var mappedStatus = WorkOrderIngestFieldMapper.MapStatus(item.WoStatus);
|
|
|
|
|
if (mappedStatus != null)
|
|
|
|
|
await _mergePolicy.TryApplyAsync(syncContext, "Status", mappedStatus);
|
|
|
|
|
|
|
|
|
|
var priority = WorkOrderIngestFieldMapper.MapSeverityToPriority(item.Severity);
|
|
|
|
|
if (priority != null)
|
|
|
|
|
await _mergePolicy.TryApplyAsync(syncContext, "Priority", priority);
|
|
|
|
|
|
|
|
|
|
if (item.Severity != null)
|
|
|
|
|
await _mergePolicy.TryApplyAsync(syncContext, "Severity", item.Severity);
|
|
|
|
|
|
|
|
|
|
if (item.SiteCode != null)
|
|
|
|
|
await _mergePolicy.TryApplyAsync(syncContext, "SiteCode", item.SiteCode);
|
|
|
|
|
|
|
|
|
|
if (item.Building != null)
|
|
|
|
|
await _mergePolicy.TryApplyAsync(syncContext, "Building", item.Building);
|
|
|
|
|
|
|
|
|
|
if (locationId.HasValue)
|
|
|
|
|
await _mergePolicy.TryApplyAsync(syncContext, "LocationId", locationId.Value);
|
|
|
|
|
|
|
|
|
|
if (item.DueDate.HasValue)
|
|
|
|
|
await _mergePolicy.TryApplyAsync(syncContext, "DueDate", item.DueDate.Value);
|
|
|
|
|
|
|
|
|
|
if (item.Customer != null)
|
|
|
|
|
existing.Customer = item.Customer;
|
|
|
|
|
|
|
|
|
|
// Sync-owned metadata (not in ShocOwnedFieldSet)
|
|
|
|
|
existing.DateReported = item.DateReported ?? existing.DateReported;
|
|
|
|
|
existing.ScheduledStart = item.ScheduledStart ?? existing.ScheduledStart;
|
|
|
|
|
existing.SourceEmailS3Key = item.SourceEmailS3Key ?? existing.SourceEmailS3Key;
|
|
|
|
|
|
|
|
|
|
return (new WorkOrderIngestItemResultDto
|
|
|
|
|
{
|
|
|
|
|
ExternalWorkOrderId = item.ExternalWorkOrderId,
|
|
|
|
|
WorkOrderId = existing.Id,
|
|
|
|
|
Created = false
|
|
|
|
|
}, nextSeed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<int> AllocateNextWoSeedAsync(CancellationToken cancellationToken)
|
|
|
|
|
{
|
2026-07-24 17:35:34 -03:00
|
|
|
var maxId = await _ingestData.GetMaxWorkOrderIdAsync(cancellationToken);
|
2026-07-13 13:22:11 -03:00
|
|
|
return Math.Max(maxId + 1, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static (string Number, int NextSeed) AllocateInternalWoNumber(int seed)
|
|
|
|
|
{
|
|
|
|
|
if (!WorkOrderNumberNormalizer.TryNormalize(seed.ToString(), out var normalized, out _))
|
|
|
|
|
normalized = seed.ToString().PadLeft(11, '0');
|
|
|
|
|
return (normalized, seed + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<int?> ResolveLocationIdAsync(
|
|
|
|
|
string? siteCode,
|
|
|
|
|
string? building,
|
|
|
|
|
string? address,
|
|
|
|
|
CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(siteCode) && string.IsNullOrWhiteSpace(building))
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
var matchCode = siteCode ?? building;
|
2026-07-24 17:35:34 -03:00
|
|
|
var existing = await _ingestData.FindLocationAsync(matchCode, cancellationToken);
|
2026-07-13 13:22:11 -03:00
|
|
|
|
|
|
|
|
if (existing != null)
|
|
|
|
|
return existing.Id;
|
|
|
|
|
|
|
|
|
|
var location = new Locations
|
|
|
|
|
{
|
|
|
|
|
Name = matchCode,
|
|
|
|
|
Title = building,
|
|
|
|
|
Address1 = address,
|
|
|
|
|
Status = "Active"
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-24 17:35:34 -03:00
|
|
|
await _ingestData.AddAndSaveLocationAsync(location, cancellationToken);
|
2026-07-13 13:22:11 -03:00
|
|
|
return location.Id;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|