mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 04:53:11 +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))
|
||||
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)
|
||||
{
|
||||
throw new WorkOrderBoardValidationException(
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ namespace SeaHaven.Services.Implementation
|
|||
PocName = row.PocName,
|
||||
PocPhone = row.PocPhone,
|
||||
PocNotes = row.PocNotes,
|
||||
AdditionalContacts = row.AdditionalContacts,
|
||||
LifecycleStatus = row.LifecycleStatus,
|
||||
LifecycleStatusLabel = row.LifecycleStatusLabel,
|
||||
LegacyStatus = row.LegacyStatus,
|
||||
|
|
|
|||
|
|
@ -92,8 +92,8 @@ namespace SeaHaven.Services.Validation
|
|||
.MaximumLength(2000)
|
||||
.When(c => !string.IsNullOrEmpty(c.Notes));
|
||||
contact.RuleFor(c => c)
|
||||
.Must(c => !string.IsNullOrWhiteSpace(c.Name) || !string.IsNullOrWhiteSpace(c.Phone))
|
||||
.WithMessage("Each additional contact must include a name or phone.");
|
||||
.Must(HasNameAndPhoneWhenPresent)
|
||||
.WithMessage("Each additional contact must include a name and phone.");
|
||||
})
|
||||
.When(x => x.AdditionalContacts != null);
|
||||
}
|
||||
|
|
@ -108,5 +108,15 @@ namespace SeaHaven.Services.Validation
|
|||
return !string.IsNullOrWhiteSpace(request.PrimaryService)
|
||||
|| !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("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);
|
||||
}
|
||||
|
||||
[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]
|
||||
public async Task PatchField_SetsFlagColor_CanonicalUppercase()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -210,6 +210,71 @@ public class WorkOrderDetailServiceTests
|
|||
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]
|
||||
public async Task GetAudit_ResolvesAssignToUserIdsToDisplayNames()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue