mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 10:43:13 +00:00
Merge pull request #75 from Sea-Haven-Industries/fix/sh-252-provisional-wo-number
fix(work-orders): enforce provisional WO numbers (SH-252)
This commit is contained in:
commit
b69c2fe64e
5 changed files with 114 additions and 12 deletions
29
SeaHaven.Services/Helpers/WorkOrderNumberRules.cs
Normal file
29
SeaHaven.Services/Helpers/WorkOrderNumberRules.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using System.Globalization;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
public static class WorkOrderNumberRules
|
||||
{
|
||||
private const int MaximumLength = 11;
|
||||
private const int MinimumProvisionalDigits = 5;
|
||||
|
||||
public static bool IsProvisional(string? woNumber)
|
||||
=> !string.IsNullOrWhiteSpace(woNumber)
|
||||
&& woNumber.Trim().StartsWith("SH", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static string FormatProvisional(long seed)
|
||||
{
|
||||
if (seed < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(seed), "Provisional WO number seed must be positive.");
|
||||
|
||||
var digits = seed.ToString(CultureInfo.InvariantCulture)
|
||||
.PadLeft(MinimumProvisionalDigits, '0');
|
||||
var provisional = $"SH{digits}";
|
||||
|
||||
if (provisional.Length > MaximumLength)
|
||||
throw new ArgumentOutOfRangeException(nameof(seed), "Provisional WO number exceeds the supported length.");
|
||||
|
||||
return provisional;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -284,9 +284,19 @@ namespace SeaHaven.Services.Implementation
|
|||
return normalized;
|
||||
}
|
||||
|
||||
var maxId = await _mutationData.GetMaxWorkOrderIdAsync(CancellationToken.None);
|
||||
for (var attempt = 0; attempt < 20; attempt++)
|
||||
{
|
||||
var candidate = await GenerateNextSequentialWoNumberAsync();
|
||||
string candidate;
|
||||
try
|
||||
{
|
||||
candidate = WorkOrderNumberRules.FormatProvisional((long)maxId + 1 + attempt);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!await _boardDataService.InternalWoNumberExistsAsync(candidate, 0))
|
||||
return candidate;
|
||||
}
|
||||
|
|
@ -294,15 +304,6 @@ namespace SeaHaven.Services.Implementation
|
|||
throw new WorkOrderBoardValidationException("WoNumberGenerationFailed", "Could not generate a unique WO number.");
|
||||
}
|
||||
|
||||
private async Task<string> GenerateNextSequentialWoNumberAsync()
|
||||
{
|
||||
var maxId = await _mutationData.GetMaxWorkOrderIdAsync(CancellationToken.None);
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -362,6 +362,13 @@ namespace SeaHaven.Services.Implementation
|
|||
if (parsed == null)
|
||||
throw new WorkOrderBoardValidationException("InvalidValue", $"Invalid lifecycleStatus: {value}");
|
||||
|
||||
if (parsed == LifecycleStatus.Completed && WorkOrderNumberRules.IsProvisional(workOrder.InternalWONumber))
|
||||
{
|
||||
throw new WorkOrderBoardValidationException(
|
||||
"ProvisionalWoNumber",
|
||||
"Replace the provisional SH work order number before marking the work order Completed.");
|
||||
}
|
||||
|
||||
var old = workOrder.LifecycleStatus?.ToString() ?? workOrder.Status;
|
||||
if (workOrder.LifecycleStatus == parsed)
|
||||
return FieldChange.Unchanged(auditField);
|
||||
|
|
|
|||
|
|
@ -44,8 +44,28 @@ public class WorkOrderBoardCreateServiceTests
|
|||
Assert.Equal(LifecycleStatus.Incomplete, result.LifecycleStatus);
|
||||
Assert.Equal("BK5", result.SiteCode);
|
||||
Assert.Equal(WorkOrderType.PM, result.WorkOrderType);
|
||||
Assert.False(string.IsNullOrWhiteSpace(result.WoNumber));
|
||||
Assert.Equal(11, result.WoNumber!.Length);
|
||||
Assert.Equal("SH00001", result.WoNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_BlankWoNumber_PreservesExistingNumericPlaceholderAndGeneratesShNumber()
|
||||
{
|
||||
var (context, service) = CreateSut();
|
||||
context.workOrders.Add(new WorkOrder
|
||||
{
|
||||
InternalWONumber = "00000000123",
|
||||
WorkerOrderNumber = "00000000123"
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
||||
{
|
||||
WorkOrderType = WorkOrderType.PM,
|
||||
SiteCode = "BK5"
|
||||
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
||||
|
||||
Assert.Equal("SH00002", result.WoNumber);
|
||||
Assert.True(await context.workOrders.AnyAsync(w => w.InternalWONumber == "00000000123"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -153,6 +153,51 @@ public class WorkOrderBoardUpdateServiceTests
|
|||
Assert.False(afterStatus.IsPastDue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PatchField_ProvisionalWoNumber_BlocksCompletionUntilReplaced()
|
||||
{
|
||||
var (context, service) = CreateSut();
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
InternalWONumber = "SH00001",
|
||||
WorkerOrderNumber = "SH00001",
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
ScheduledDate = DateTime.UtcNow.Date.AddDays(1),
|
||||
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||
};
|
||||
context.workOrders.Add(wo);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var blocked = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
||||
service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
||||
{
|
||||
Field = WorkOrderBoardFieldNames.LifecycleStatus,
|
||||
Value = "Complete",
|
||||
WorkOrderVersion = ToVersion(wo)
|
||||
}, "actor-1"));
|
||||
|
||||
Assert.Equal("ProvisionalWoNumber", blocked.Code);
|
||||
|
||||
var renumbered = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
||||
{
|
||||
Field = WorkOrderBoardFieldNames.WoNumber,
|
||||
Value = "12345",
|
||||
WorkOrderVersion = ToVersion(wo)
|
||||
}, "actor-1");
|
||||
Assert.Equal("00000012345", renumbered.WoNumber);
|
||||
|
||||
wo = await context.workOrders.SingleAsync(w => w.Id == 1);
|
||||
var completed = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
||||
{
|
||||
Field = WorkOrderBoardFieldNames.LifecycleStatus,
|
||||
Value = "Complete",
|
||||
WorkOrderVersion = ToVersion(wo)
|
||||
}, "actor-1");
|
||||
|
||||
Assert.Equal(LifecycleStatus.Completed, completed.LifecycleStatus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PatchField_DueDateAlone_DoesNotClearPastDue()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue