mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 06:03:12 +00:00
fix(work-orders): keep SH placeholder WO numbers and block downgrades (#123)
Some checks are pending
Validate and deploy / Validate deployable source bundle (push) Waiting to run
Validate and deploy / Deploy shoc-backend-dev through Terraform (push) Blocked by required conditions
Validate and deploy / Deploy shoc-backend-staging to Elastic Beanstalk (push) Blocked by required conditions
Some checks are pending
Validate and deploy / Validate deployable source bundle (push) Waiting to run
Validate and deploy / Deploy shoc-backend-dev through Terraform (push) Blocked by required conditions
Validate and deploy / Deploy shoc-backend-staging to Elastic Beanstalk (push) Blocked by required conditions
SH-320: the WO number normalizer stripped every non-digit, so a manually entered SH placeholder (e.g. SH00001) was saved as 00000000001 on both create and patch. Keep the SH prefix, and reject replacing a saved real APM number with an SH placeholder.
This commit is contained in:
parent
776c8be5cb
commit
4b772c8db3
5 changed files with 135 additions and 1 deletions
|
|
@ -5,6 +5,8 @@ namespace SeaHaven.Services.Helpers
|
|||
public static class WorkOrderNumberNormalizer
|
||||
{
|
||||
private const int RequiredLength = 11;
|
||||
private const string ProvisionalPrefix = "SH";
|
||||
private const int MinimumProvisionalDigits = 5;
|
||||
|
||||
public static bool TryNormalize(string? input, out string normalized, out string? error)
|
||||
{
|
||||
|
|
@ -17,7 +19,11 @@ namespace SeaHaven.Services.Helpers
|
|||
return false;
|
||||
}
|
||||
|
||||
var digits = Regex.Replace(input.Trim(), @"\D", "");
|
||||
var trimmed = input.Trim();
|
||||
if (WorkOrderNumberRules.IsProvisional(trimmed))
|
||||
return TryNormalizeProvisional(trimmed, out normalized, out error);
|
||||
|
||||
var digits = Regex.Replace(trimmed, @"\D", "");
|
||||
if (digits.Length == 0)
|
||||
{
|
||||
error = "WO number must contain digits.";
|
||||
|
|
@ -33,5 +39,30 @@ namespace SeaHaven.Services.Helpers
|
|||
normalized = digits.PadLeft(RequiredLength, '0');
|
||||
return true;
|
||||
}
|
||||
|
||||
// SH-320: an SH-prefixed number is the documented placeholder for a WO still
|
||||
// awaiting its real APM number; keep the prefix instead of stripping it to digits.
|
||||
private static bool TryNormalizeProvisional(string trimmed, out string normalized, out string? error)
|
||||
{
|
||||
normalized = "";
|
||||
error = null;
|
||||
|
||||
var digits = Regex.Replace(trimmed[ProvisionalPrefix.Length..], @"\D", "");
|
||||
if (digits.Length == 0)
|
||||
{
|
||||
error = "WO number must contain digits.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var candidate = ProvisionalPrefix + digits.PadLeft(MinimumProvisionalDigits, '0');
|
||||
if (candidate.Length > RequiredLength)
|
||||
{
|
||||
error = $"WO number cannot exceed {RequiredLength} characters.";
|
||||
return false;
|
||||
}
|
||||
|
||||
normalized = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -334,6 +334,14 @@ namespace SeaHaven.Services.Implementation
|
|||
if (!WorkOrderNumberNormalizer.TryNormalize(value, out var normalized, out var error))
|
||||
throw new WorkOrderBoardValidationException("InvalidWoNumber", error ?? "Invalid WO number.");
|
||||
|
||||
// SH-320: a saved real APM number can never be downgraded to an SH placeholder.
|
||||
if (WorkOrderNumberRules.IsProvisional(normalized)
|
||||
&& !string.IsNullOrWhiteSpace(workOrder.InternalWONumber)
|
||||
&& !WorkOrderNumberRules.IsProvisional(workOrder.InternalWONumber))
|
||||
throw new WorkOrderBoardValidationException(
|
||||
"WoNumberDowngrade",
|
||||
"Cannot replace an APM work order number with an internal number.");
|
||||
|
||||
if (await _boardDataService.InternalWoNumberExistsAsync(normalized, workOrder.Id, cancellationToken))
|
||||
throw new WorkOrderBoardValidationException("DuplicateWoNumber", "WO number already exists.");
|
||||
|
||||
|
|
|
|||
|
|
@ -87,6 +87,24 @@ public class WorkOrderBoardCreateServiceTests
|
|||
Assert.Equal("00000012345", result.WoNumber);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("SH00001", "SH00001")]
|
||||
[InlineData("sh00042", "SH00042")]
|
||||
public async Task Create_WithManualShPlaceholderWoNumber_KeepsShPrefix(string requested, string expected)
|
||||
{
|
||||
var (_, service) = CreateSut();
|
||||
|
||||
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
||||
{
|
||||
WoNumber = requested,
|
||||
WorkOrderType = WorkOrderType.PM,
|
||||
SiteCode = "DAL",
|
||||
LocationId = 1
|
||||
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
||||
|
||||
Assert.Equal(expected, result.WoNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_DuplicateWoNumber_Throws()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1158,6 +1158,57 @@ public class WorkOrderBoardUpdateServiceTests
|
|||
Assert.Equal("00000012345", result.WoNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PatchField_ShPlaceholderReplacesShPlaceholder()
|
||||
{
|
||||
var (context, service) = CreateSut();
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
InternalWONumber = "SH00001",
|
||||
LifecycleStatus = LifecycleStatus.Incomplete,
|
||||
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||
};
|
||||
context.workOrders.Add(wo);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
||||
{
|
||||
Field = WorkOrderBoardFieldNames.WoNumber,
|
||||
Value = "SH00007",
|
||||
WorkOrderVersion = ToVersion(wo)
|
||||
}, "actor-1");
|
||||
|
||||
Assert.Equal("SH00007", result.WoNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PatchField_RejectsReplacingRealWoNumberWithShPlaceholder()
|
||||
{
|
||||
var (context, service) = CreateSut();
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
InternalWONumber = "00000012345",
|
||||
LifecycleStatus = LifecycleStatus.Incomplete,
|
||||
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||
};
|
||||
context.workOrders.Add(wo);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
||||
service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
||||
{
|
||||
Field = WorkOrderBoardFieldNames.WoNumber,
|
||||
Value = "SH00001",
|
||||
WorkOrderVersion = ToVersion(wo)
|
||||
}, "actor-1"));
|
||||
|
||||
Assert.Equal("WoNumberDowngrade", ex.Code);
|
||||
var stored = await context.workOrders.SingleAsync(w => w.Id == 1);
|
||||
Assert.Equal("00000012345", stored.InternalWONumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PatchField_RejectsDuplicateWoNumber()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -40,4 +40,30 @@ public class WorkOrderNumberNormalizerTests
|
|||
Assert.False(ok);
|
||||
Assert.Contains("11", error);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("SH00001", "SH00001")]
|
||||
[InlineData(" sh00001 ", "SH00001")]
|
||||
[InlineData("SH-00001", "SH00001")]
|
||||
[InlineData("SH1", "SH00001")]
|
||||
[InlineData("SH123456789", "SH123456789")]
|
||||
public void TryNormalize_KeepsShPlaceholderPrefix(string input, string expected)
|
||||
{
|
||||
var ok = WorkOrderNumberNormalizer.TryNormalize(input, out var normalized, out var error);
|
||||
|
||||
Assert.True(ok);
|
||||
Assert.Null(error);
|
||||
Assert.Equal(expected, normalized);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("SH")]
|
||||
[InlineData("SH1234567890")]
|
||||
public void TryNormalize_RejectsInvalidShPlaceholder(string input)
|
||||
{
|
||||
var ok = WorkOrderNumberNormalizer.TryNormalize(input, out _, out var error);
|
||||
|
||||
Assert.False(ok);
|
||||
Assert.NotNull(error);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue