mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 07:13:12 +00:00
fix(work-orders): map additionalContacts on detail GET (SH-218)
Copy contacts into MapInfo so slide-over round-trips create/PATCH, and require name plus phone on retained entries while dropping blank placeholders.
This commit is contained in:
parent
36ef0b00f5
commit
3609365939
6 changed files with 291 additions and 2 deletions
|
|
@ -87,6 +87,13 @@ namespace SeaHaven.Services.Helpers
|
||||||
if (string.IsNullOrEmpty(name) && string.IsNullOrEmpty(phone))
|
if (string.IsNullOrEmpty(name) && string.IsNullOrEmpty(phone))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(phone))
|
||||||
|
{
|
||||||
|
throw new WorkOrderBoardValidationException(
|
||||||
|
"InvalidValue",
|
||||||
|
"Each additional contact must include a name and phone.");
|
||||||
|
}
|
||||||
|
|
||||||
if (name.Length > MaxNameLength)
|
if (name.Length > MaxNameLength)
|
||||||
{
|
{
|
||||||
throw new WorkOrderBoardValidationException(
|
throw new WorkOrderBoardValidationException(
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
PocName = row.PocName,
|
PocName = row.PocName,
|
||||||
PocPhone = row.PocPhone,
|
PocPhone = row.PocPhone,
|
||||||
PocNotes = row.PocNotes,
|
PocNotes = row.PocNotes,
|
||||||
|
AdditionalContacts = row.AdditionalContacts,
|
||||||
LifecycleStatus = row.LifecycleStatus,
|
LifecycleStatus = row.LifecycleStatus,
|
||||||
LifecycleStatusLabel = row.LifecycleStatusLabel,
|
LifecycleStatusLabel = row.LifecycleStatusLabel,
|
||||||
LegacyStatus = row.LegacyStatus,
|
LegacyStatus = row.LegacyStatus,
|
||||||
|
|
|
||||||
|
|
@ -92,8 +92,8 @@ namespace SeaHaven.Services.Validation
|
||||||
.MaximumLength(2000)
|
.MaximumLength(2000)
|
||||||
.When(c => !string.IsNullOrEmpty(c.Notes));
|
.When(c => !string.IsNullOrEmpty(c.Notes));
|
||||||
contact.RuleFor(c => c)
|
contact.RuleFor(c => c)
|
||||||
.Must(c => !string.IsNullOrWhiteSpace(c.Name) || !string.IsNullOrWhiteSpace(c.Phone))
|
.Must(HasNameAndPhoneWhenPresent)
|
||||||
.WithMessage("Each additional contact must include a name or phone.");
|
.WithMessage("Each additional contact must include a name and phone.");
|
||||||
})
|
})
|
||||||
.When(x => x.AdditionalContacts != null);
|
.When(x => x.AdditionalContacts != null);
|
||||||
}
|
}
|
||||||
|
|
@ -108,5 +108,15 @@ namespace SeaHaven.Services.Validation
|
||||||
return !string.IsNullOrWhiteSpace(request.PrimaryService)
|
return !string.IsNullOrWhiteSpace(request.PrimaryService)
|
||||||
|| !string.IsNullOrWhiteSpace(request.Trade);
|
|| !string.IsNullOrWhiteSpace(request.Trade);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool HasNameAndPhoneWhenPresent(WorkOrderAdditionalContactDto contact)
|
||||||
|
{
|
||||||
|
var hasName = !string.IsNullOrWhiteSpace(contact.Name);
|
||||||
|
var hasPhone = !string.IsNullOrWhiteSpace(contact.Phone);
|
||||||
|
if (!hasName && !hasPhone)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return hasName && hasPhone;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -383,4 +383,112 @@ public class WorkOrderBoardCreateServiceTests
|
||||||
Assert.Contains("Backup Lead", wo.AdditionalContacts);
|
Assert.Contains("Backup Lead", wo.AdditionalContacts);
|
||||||
Assert.Contains("Security Desk", wo.AdditionalContacts);
|
Assert.Contains("Security Desk", wo.AdditionalContacts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Create_AdditionalContacts_EmptyList_SucceedsWithoutContacts()
|
||||||
|
{
|
||||||
|
var (_, service) = CreateSut();
|
||||||
|
|
||||||
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
||||||
|
{
|
||||||
|
WorkOrderType = WorkOrderType.PM,
|
||||||
|
SiteCode = "BK5",
|
||||||
|
AdditionalContacts = new List<WorkOrderAdditionalContactDto>()
|
||||||
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
||||||
|
|
||||||
|
Assert.Null(result.AdditionalContacts);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Create_AdditionalContacts_DropsBlankRows_PreservesOrder()
|
||||||
|
{
|
||||||
|
var (_, service) = CreateSut();
|
||||||
|
|
||||||
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
||||||
|
{
|
||||||
|
WorkOrderType = WorkOrderType.Reactive,
|
||||||
|
SiteCode = "BK5",
|
||||||
|
AdditionalContacts = new List<WorkOrderAdditionalContactDto>
|
||||||
|
{
|
||||||
|
new("", "", null),
|
||||||
|
new("Backup Lead", "+1 555-0101", "After hours"),
|
||||||
|
new(" ", " ", null),
|
||||||
|
new("Security Desk", "+1 555-0102", null)
|
||||||
|
}
|
||||||
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
||||||
|
|
||||||
|
Assert.NotNull(result.AdditionalContacts);
|
||||||
|
Assert.Equal(2, result.AdditionalContacts!.Count);
|
||||||
|
Assert.Equal("Backup Lead", result.AdditionalContacts[0].Name);
|
||||||
|
Assert.Equal("Security Desk", result.AdditionalContacts[1].Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Create_AdditionalContacts_NameOnly_Throws()
|
||||||
|
{
|
||||||
|
var (_, service) = CreateSut();
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<FluentValidation.ValidationException>(() =>
|
||||||
|
service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
||||||
|
{
|
||||||
|
WorkOrderType = WorkOrderType.PM,
|
||||||
|
SiteCode = "BK5",
|
||||||
|
AdditionalContacts = new List<WorkOrderAdditionalContactDto>
|
||||||
|
{
|
||||||
|
new("Backup Lead", "", null)
|
||||||
|
}
|
||||||
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"));
|
||||||
|
|
||||||
|
Assert.Contains("name and phone", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Create_AdditionalContacts_PhoneOnly_Throws()
|
||||||
|
{
|
||||||
|
var (_, service) = CreateSut();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<FluentValidation.ValidationException>(() =>
|
||||||
|
service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
||||||
|
{
|
||||||
|
WorkOrderType = WorkOrderType.PM,
|
||||||
|
SiteCode = "BK5",
|
||||||
|
AdditionalContacts = new List<WorkOrderAdditionalContactDto>
|
||||||
|
{
|
||||||
|
new("", "+1 555-0101", null)
|
||||||
|
}
|
||||||
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Create_AdditionalContacts_PersistsAndReturnsOnDetail()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
var created = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
||||||
|
{
|
||||||
|
WorkOrderType = WorkOrderType.Reactive,
|
||||||
|
SiteCode = "BK5",
|
||||||
|
AdditionalContacts = new List<WorkOrderAdditionalContactDto>
|
||||||
|
{
|
||||||
|
new("Backup Lead", "+1 555-0101", "After hours"),
|
||||||
|
new("Security Desk", "+1 555-0102", null)
|
||||||
|
}
|
||||||
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
||||||
|
|
||||||
|
var resolver = WorkOrderAccountTestHelpers.Resolver(context);
|
||||||
|
var detail = new WorkOrderDetailService(
|
||||||
|
new WorkOrderBoardService(new WorkOrderBoardDataService(context), resolver),
|
||||||
|
new WorkOrderDetailDataService(context),
|
||||||
|
new CompletionDocTemplateDataService(context),
|
||||||
|
new UserDataService(context),
|
||||||
|
resolver);
|
||||||
|
|
||||||
|
var result = await detail.GetDetailAsync(created.Id, WorkOrderAccountTestHelpers.AccountUser());
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.NotNull(result!.Info.AdditionalContacts);
|
||||||
|
Assert.Equal(2, result.Info.AdditionalContacts!.Count);
|
||||||
|
Assert.Equal("Backup Lead", result.Info.AdditionalContacts[0].Name);
|
||||||
|
Assert.Equal("+1 555-0101", result.Info.AdditionalContacts[0].Phone);
|
||||||
|
Assert.Equal("Security Desk", result.Info.AdditionalContacts[1].Name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -404,6 +404,104 @@ public class WorkOrderBoardUpdateServiceTests
|
||||||
Assert.Equal("InvalidValue", ex.Code);
|
Assert.Equal("InvalidValue", ex.Code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PatchField_AdditionalContacts_NameOnly_Throws()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
var wo = new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
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.AdditionalContacts,
|
||||||
|
Value = "[{\"name\":\"Jane Backup\",\"phone\":\"\"}]",
|
||||||
|
WorkOrderVersion = ToVersion(wo)
|
||||||
|
}, "actor-1"));
|
||||||
|
|
||||||
|
Assert.Equal("InvalidValue", ex.Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PatchField_AdditionalContacts_PhoneOnly_Throws()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
var wo = new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
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.AdditionalContacts,
|
||||||
|
Value = "[{\"name\":\"\",\"phone\":\"+1 555-0101\"}]",
|
||||||
|
WorkOrderVersion = ToVersion(wo)
|
||||||
|
}, "actor-1"));
|
||||||
|
|
||||||
|
Assert.Equal("InvalidValue", ex.Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PatchField_AdditionalContacts_DropsBlankRows_PreservesOrder()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
var wo = new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
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.AdditionalContacts,
|
||||||
|
Value = "[{\"name\":\"\",\"phone\":\"\"},{\"name\":\"Jane Backup\",\"phone\":\"+1 555-0101\"},{\"name\":\"Security Desk\",\"phone\":\"+1 555-0102\"}]",
|
||||||
|
WorkOrderVersion = ToVersion(wo)
|
||||||
|
}, "actor-1");
|
||||||
|
|
||||||
|
Assert.Equal(2, result.AdditionalContacts!.Count);
|
||||||
|
Assert.Equal("Jane Backup", result.AdditionalContacts[0].Name);
|
||||||
|
Assert.Equal("Security Desk", result.AdditionalContacts[1].Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PatchField_AdditionalContacts_EmptyArray_Clears()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
var wo = new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
LifecycleStatus = LifecycleStatus.Incomplete,
|
||||||
|
AdditionalContacts = "[{\"name\":\"Jane Backup\",\"phone\":\"+1 555-0101\"}]",
|
||||||
|
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.AdditionalContacts,
|
||||||
|
Value = "[]",
|
||||||
|
WorkOrderVersion = ToVersion(wo)
|
||||||
|
}, "actor-1");
|
||||||
|
|
||||||
|
Assert.Null(result.AdditionalContacts);
|
||||||
|
Assert.Null((await context.workOrders.FindAsync(1))!.AdditionalContacts);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PatchField_SetsFlagColor_CanonicalUppercase()
|
public async Task PatchField_SetsFlagColor_CanonicalUppercase()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,71 @@ public class WorkOrderDetailServiceTests
|
||||||
Assert.Equal(1, detail!.Info.PendingUpliftCount);
|
Assert.Equal(1, detail!.Info.PendingUpliftCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDetail_ReturnsAdditionalContactsFromBoardRow()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
context.workOrders.Add(new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "10000000218",
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
|
ScheduledDate = DateTime.UtcNow.AddDays(1),
|
||||||
|
AdditionalContacts =
|
||||||
|
"[{\"name\":\"Backup Lead\",\"phone\":\"+1 555-0101\",\"notes\":\"After hours\"},{\"name\":\"Security Desk\",\"phone\":\"+1 555-0102\"}]",
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var detail = await service.GetDetailAsync(1, WorkOrderAccountTestHelpers.OrgWideAdmin());
|
||||||
|
|
||||||
|
Assert.NotNull(detail);
|
||||||
|
Assert.NotNull(detail!.Info.AdditionalContacts);
|
||||||
|
Assert.Equal(2, detail.Info.AdditionalContacts!.Count);
|
||||||
|
Assert.Equal("Backup Lead", detail.Info.AdditionalContacts[0].Name);
|
||||||
|
Assert.Equal("+1 555-0101", detail.Info.AdditionalContacts[0].Phone);
|
||||||
|
Assert.Equal("After hours", detail.Info.AdditionalContacts[0].Notes);
|
||||||
|
Assert.Equal("Security Desk", detail.Info.AdditionalContacts[1].Name);
|
||||||
|
Assert.Equal("+1 555-0102", detail.Info.AdditionalContacts[1].Phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDetail_ReflectsPatchedAdditionalContacts()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
var wo = new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "10000000219",
|
||||||
|
LifecycleStatus = LifecycleStatus.Incomplete,
|
||||||
|
AdditionalContacts = "[{\"name\":\"Old Backup\",\"phone\":\"+1 555-0000\"}]",
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||||
|
};
|
||||||
|
context.workOrders.Add(wo);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var mutationData = new WorkOrderBoardMutationDataService(context);
|
||||||
|
var boardData = new WorkOrderBoardDataService(context);
|
||||||
|
var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context));
|
||||||
|
var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks);
|
||||||
|
var update = new WorkOrderBoardUpdateService(boardData, mutationData, audit);
|
||||||
|
|
||||||
|
await update.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
||||||
|
{
|
||||||
|
Field = WorkOrderBoardFieldNames.AdditionalContacts,
|
||||||
|
Value = "[{\"name\":\"Jane Backup\",\"phone\":\"+1 555-0101\",\"notes\":\"Call first\"}]",
|
||||||
|
WorkOrderVersion = Convert.ToBase64String(wo.RowVersion ?? new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 })
|
||||||
|
}, "actor-1");
|
||||||
|
|
||||||
|
var detail = await service.GetDetailAsync(1, WorkOrderAccountTestHelpers.OrgWideAdmin());
|
||||||
|
|
||||||
|
Assert.NotNull(detail);
|
||||||
|
Assert.Single(detail!.Info.AdditionalContacts!);
|
||||||
|
Assert.Equal("Jane Backup", detail.Info.AdditionalContacts![0].Name);
|
||||||
|
Assert.Equal("+1 555-0101", detail.Info.AdditionalContacts[0].Phone);
|
||||||
|
Assert.Equal("Call first", detail.Info.AdditionalContacts[0].Notes);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetAudit_ResolvesAssignToUserIdsToDisplayNames()
|
public async Task GetAudit_ResolvesAssignToUserIdsToDisplayNames()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue