mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-03 16:43:28 +00:00
feat(work-orders): persist additionalContacts on create, board GET and PATCH (SH-218)
Add JSON column, DTO/mapper, create + PATCH field, board projection, FluentValidation, and regression tests for additional POC contacts round-trip.
This commit is contained in:
parent
5f614fe893
commit
36ef0b00f5
16 changed files with 290 additions and 0 deletions
|
|
@ -0,0 +1,33 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.SeaHavenIndustries.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260813160000_SH218_WorkOrderAdditionalContacts")]
|
||||||
|
public partial class SH218_WorkOrderAdditionalContacts : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "AdditionalContacts",
|
||||||
|
table: "workOrders",
|
||||||
|
type: "nvarchar(4000)",
|
||||||
|
maxLength: 4000,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AdditionalContacts",
|
||||||
|
table: "workOrders");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2496,6 +2496,10 @@ namespace Data.SeaHavenIndustries.Migrations
|
||||||
.HasMaxLength(2000)
|
.HasMaxLength(2000)
|
||||||
.HasColumnType("nvarchar(2000)");
|
.HasColumnType("nvarchar(2000)");
|
||||||
|
|
||||||
|
b.Property<string>("AdditionalContacts")
|
||||||
|
.HasMaxLength(4000)
|
||||||
|
.HasColumnType("nvarchar(4000)");
|
||||||
|
|
||||||
b.Property<string>("PocPhone")
|
b.Property<string>("PocPhone")
|
||||||
.HasMaxLength(50)
|
.HasMaxLength(50)
|
||||||
.HasColumnType("nvarchar(50)");
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,9 @@ namespace Data.SeaHavenIndustries
|
||||||
public string? PocPhone { get; set; }
|
public string? PocPhone { get; set; }
|
||||||
[MaxLength(2000)]
|
[MaxLength(2000)]
|
||||||
public string? PocNotes { get; set; }
|
public string? PocNotes { get; set; }
|
||||||
|
/// <summary>JSON array of additional POC contacts beyond primary (name, phone, notes?).</summary>
|
||||||
|
[MaxLength(4000)]
|
||||||
|
public string? AdditionalContacts { get; set; }
|
||||||
/// <summary>JSON array of extra service labels beyond primary (Trade).</summary>
|
/// <summary>JSON array of extra service labels beyond primary (Trade).</summary>
|
||||||
[MaxLength(2000)]
|
[MaxLength(2000)]
|
||||||
public string? ExtraServices { get; set; }
|
public string? ExtraServices { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
w.Problem,
|
w.Problem,
|
||||||
w.ServiceNotes,
|
w.ServiceNotes,
|
||||||
w.ExtraServices,
|
w.ExtraServices,
|
||||||
|
w.AdditionalContacts,
|
||||||
w.DocStatus,
|
w.DocStatus,
|
||||||
w.CompletedDate,
|
w.CompletedDate,
|
||||||
w.FlagColor,
|
w.FlagColor,
|
||||||
|
|
@ -127,6 +128,7 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
w.Problem,
|
w.Problem,
|
||||||
w.ServiceNotes,
|
w.ServiceNotes,
|
||||||
w.ExtraServices,
|
w.ExtraServices,
|
||||||
|
w.AdditionalContacts,
|
||||||
w.DocStatus,
|
w.DocStatus,
|
||||||
w.CompletedDate,
|
w.CompletedDate,
|
||||||
w.FlagColor,
|
w.FlagColor,
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
string? Problem,
|
string? Problem,
|
||||||
string? ServiceNotes,
|
string? ServiceNotes,
|
||||||
string? ExtraServices,
|
string? ExtraServices,
|
||||||
|
string? AdditionalContacts,
|
||||||
DocStatus? DocStatus,
|
DocStatus? DocStatus,
|
||||||
DateTime? CompletedDate,
|
DateTime? CompletedDate,
|
||||||
string? FlagColor,
|
string? FlagColor,
|
||||||
|
|
|
||||||
4
SeaHaven.Services/DTOs/WorkOrderAdditionalContactDto.cs
Normal file
4
SeaHaven.Services/DTOs/WorkOrderAdditionalContactDto.cs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
namespace SeaHaven.Services.DTOs
|
||||||
|
{
|
||||||
|
public record WorkOrderAdditionalContactDto(string Name, string Phone, string? Notes);
|
||||||
|
}
|
||||||
|
|
@ -34,6 +34,7 @@ namespace SeaHaven.Services.DTOs
|
||||||
public string? PocName { get; set; }
|
public string? PocName { get; set; }
|
||||||
public string? PocPhone { get; set; }
|
public string? PocPhone { get; set; }
|
||||||
public string? PocNotes { get; set; }
|
public string? PocNotes { get; set; }
|
||||||
|
public List<WorkOrderAdditionalContactDto>? AdditionalContacts { get; set; }
|
||||||
public LifecycleStatus? LifecycleStatus { get; set; }
|
public LifecycleStatus? LifecycleStatus { get; set; }
|
||||||
public string? LifecycleStatusLabel { get; set; }
|
public string? LifecycleStatusLabel { get; set; }
|
||||||
public string? LegacyStatus { get; set; }
|
public string? LegacyStatus { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ namespace SeaHaven.Services.DTOs
|
||||||
public string? PocName { get; set; }
|
public string? PocName { get; set; }
|
||||||
public string? PocPhone { get; set; }
|
public string? PocPhone { get; set; }
|
||||||
public string? PocNotes { get; set; }
|
public string? PocNotes { get; set; }
|
||||||
|
public List<WorkOrderAdditionalContactDto>? AdditionalContacts { get; set; }
|
||||||
public string? TechPhone { get; set; }
|
public string? TechPhone { get; set; }
|
||||||
public string? VendorNotes { get; set; }
|
public string? VendorNotes { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
120
SeaHaven.Services/Helpers/WorkOrderAdditionalContactsMapper.cs
Normal file
120
SeaHaven.Services/Helpers/WorkOrderAdditionalContactsMapper.cs
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
|
using SeaHaven.Services.Exceptions;
|
||||||
|
|
||||||
|
namespace SeaHaven.Services.Helpers
|
||||||
|
{
|
||||||
|
public static class WorkOrderAdditionalContactsMapper
|
||||||
|
{
|
||||||
|
private const int MaxNameLength = 200;
|
||||||
|
private const int MaxPhoneLength = 50;
|
||||||
|
private const int MaxNotesLength = 2000;
|
||||||
|
private const int MaxSerializedLength = 4000;
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
|
};
|
||||||
|
|
||||||
|
public static List<WorkOrderAdditionalContactDto>? ParseJson(string? json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parsed = JsonSerializer.Deserialize<List<WorkOrderAdditionalContactDto>>(json, JsonOptions);
|
||||||
|
return parsed is { Count: > 0 } ? parsed : null;
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? SerializeForStorage(IReadOnlyList<WorkOrderAdditionalContactDto>? contacts)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeList(contacts);
|
||||||
|
if (normalized.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(normalized, JsonOptions);
|
||||||
|
if (json.Length > MaxSerializedLength)
|
||||||
|
{
|
||||||
|
throw new WorkOrderBoardValidationException(
|
||||||
|
"InvalidValue",
|
||||||
|
"additionalContacts must serialize to at most 4000 characters.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? SerializeForPatch(string? jsonValue)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(jsonValue))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
List<WorkOrderAdditionalContactDto>? parsed;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
parsed = JsonSerializer.Deserialize<List<WorkOrderAdditionalContactDto>>(jsonValue.Trim(), JsonOptions);
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
throw new WorkOrderBoardValidationException(
|
||||||
|
"InvalidValue",
|
||||||
|
"additionalContacts must be a JSON array of { name, phone, notes? }.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return SerializeForStorage(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<WorkOrderAdditionalContactDto> NormalizeList(IReadOnlyList<WorkOrderAdditionalContactDto>? contacts)
|
||||||
|
{
|
||||||
|
var normalized = new List<WorkOrderAdditionalContactDto>();
|
||||||
|
if (contacts == null || contacts.Count == 0)
|
||||||
|
return normalized;
|
||||||
|
|
||||||
|
foreach (var raw in contacts)
|
||||||
|
{
|
||||||
|
var name = raw.Name?.Trim() ?? "";
|
||||||
|
var phone = raw.Phone?.Trim() ?? "";
|
||||||
|
var notes = raw.Notes?.Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(name) && string.IsNullOrEmpty(phone))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (name.Length > MaxNameLength)
|
||||||
|
{
|
||||||
|
throw new WorkOrderBoardValidationException(
|
||||||
|
"InvalidValue",
|
||||||
|
"Each additional contact name must be at most 200 characters.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phone.Length > MaxPhoneLength)
|
||||||
|
{
|
||||||
|
throw new WorkOrderBoardValidationException(
|
||||||
|
"InvalidValue",
|
||||||
|
"Each additional contact phone must be at most 50 characters.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notes != null && notes.Length > MaxNotesLength)
|
||||||
|
{
|
||||||
|
throw new WorkOrderBoardValidationException(
|
||||||
|
"InvalidValue",
|
||||||
|
"Each additional contact notes must be at most 2000 characters.");
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized.Add(new WorkOrderAdditionalContactDto(
|
||||||
|
name,
|
||||||
|
phone,
|
||||||
|
string.IsNullOrEmpty(notes) ? null : notes));
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ namespace SeaHaven.Services.Helpers
|
||||||
public const string Pm = "pm";
|
public const string Pm = "pm";
|
||||||
public const string ServiceNotes = "serviceNotes";
|
public const string ServiceNotes = "serviceNotes";
|
||||||
public const string ExtraServices = "extraServices";
|
public const string ExtraServices = "extraServices";
|
||||||
|
public const string AdditionalContacts = "additionalContacts";
|
||||||
public const string FlagColor = "flagColor";
|
public const string FlagColor = "flagColor";
|
||||||
public const string RescheduleCount = "RescheduleCount";
|
public const string RescheduleCount = "RescheduleCount";
|
||||||
|
|
||||||
|
|
@ -48,6 +49,7 @@ namespace SeaHaven.Services.Helpers
|
||||||
Pm,
|
Pm,
|
||||||
ServiceNotes,
|
ServiceNotes,
|
||||||
ExtraServices,
|
ExtraServices,
|
||||||
|
AdditionalContacts,
|
||||||
FlagColor
|
FlagColor
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -89,6 +91,7 @@ namespace SeaHaven.Services.Helpers
|
||||||
Pm => "Trade",
|
Pm => "Trade",
|
||||||
ServiceNotes => "ServiceNotes",
|
ServiceNotes => "ServiceNotes",
|
||||||
ExtraServices => "ExtraServices",
|
ExtraServices => "ExtraServices",
|
||||||
|
AdditionalContacts => "AdditionalContacts",
|
||||||
FlagColor => "FlagColor",
|
FlagColor => "FlagColor",
|
||||||
_ => canonical
|
_ => canonical
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
var pocName = TrimOrNull(request.PocName);
|
var pocName = TrimOrNull(request.PocName);
|
||||||
var pocPhone = TrimOrNull(request.PocPhone);
|
var pocPhone = TrimOrNull(request.PocPhone);
|
||||||
var pocNotes = TrimOrNull(request.PocNotes);
|
var pocNotes = TrimOrNull(request.PocNotes);
|
||||||
|
var additionalContactsJson = WorkOrderAdditionalContactsMapper.SerializeForStorage(request.AdditionalContacts);
|
||||||
var techPhone = TrimOrNull(request.TechPhone);
|
var techPhone = TrimOrNull(request.TechPhone);
|
||||||
var vendorNotes = TrimOrNull(request.VendorNotes);
|
var vendorNotes = TrimOrNull(request.VendorNotes);
|
||||||
var customer = TrimOrNull(request.Customer);
|
var customer = TrimOrNull(request.Customer);
|
||||||
|
|
@ -87,6 +88,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
PocName = pocName,
|
PocName = pocName,
|
||||||
PocPhone = pocPhone,
|
PocPhone = pocPhone,
|
||||||
PocNotes = pocNotes,
|
PocNotes = pocNotes,
|
||||||
|
AdditionalContacts = additionalContactsJson,
|
||||||
LocationId = request.LocationId,
|
LocationId = request.LocationId,
|
||||||
DueDate = request.DueDate?.Date,
|
DueDate = request.DueDate?.Date,
|
||||||
AssignTo = string.IsNullOrWhiteSpace(request.AssignTo) ? null : request.AssignTo.Trim(),
|
AssignTo = string.IsNullOrWhiteSpace(request.AssignTo) ? null : request.AssignTo.Trim(),
|
||||||
|
|
@ -128,6 +130,8 @@ namespace SeaHaven.Services.Implementation
|
||||||
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("PocPhone", "", workOrder.PocPhone));
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("PocPhone", "", workOrder.PocPhone));
|
||||||
if (workOrder.PocNotes != null)
|
if (workOrder.PocNotes != null)
|
||||||
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("PocNotes", "", workOrder.PocNotes));
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("PocNotes", "", workOrder.PocNotes));
|
||||||
|
if (workOrder.AdditionalContacts != null)
|
||||||
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("AdditionalContacts", "", workOrder.AdditionalContacts));
|
||||||
if (workOrder.TechPhone != null)
|
if (workOrder.TechPhone != null)
|
||||||
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("TechPhone", "", workOrder.TechPhone));
|
changes.Add(WorkOrderBoardFieldMutations.BoardFieldChange.Changed("TechPhone", "", workOrder.TechPhone));
|
||||||
if (workOrder.VendorNotes != null)
|
if (workOrder.VendorNotes != null)
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
Pm = row.Trade ?? row.Problem,
|
Pm = row.Trade ?? row.Problem,
|
||||||
ServiceNotes = row.ServiceNotes,
|
ServiceNotes = row.ServiceNotes,
|
||||||
ExtraServices = ParseExtraServices(row.ExtraServices),
|
ExtraServices = ParseExtraServices(row.ExtraServices),
|
||||||
|
AdditionalContacts = WorkOrderAdditionalContactsMapper.ParseJson(row.AdditionalContacts),
|
||||||
DocStatus = row.DocStatus,
|
DocStatus = row.DocStatus,
|
||||||
CompletedDate = row.CompletedDate,
|
CompletedDate = row.CompletedDate,
|
||||||
FlagColor = row.FlagColor,
|
FlagColor = row.FlagColor,
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
WorkOrderBoardFieldNames.Pm => new List<FieldChange> { ApplyStringField(value, auditField, v => workOrder.Trade = v, () => workOrder.Trade) },
|
WorkOrderBoardFieldNames.Pm => new List<FieldChange> { ApplyStringField(value, auditField, v => workOrder.Trade = v, () => workOrder.Trade) },
|
||||||
WorkOrderBoardFieldNames.ServiceNotes => new List<FieldChange> { ApplyServiceNotes(workOrder, value, auditField) },
|
WorkOrderBoardFieldNames.ServiceNotes => new List<FieldChange> { ApplyServiceNotes(workOrder, value, auditField) },
|
||||||
WorkOrderBoardFieldNames.ExtraServices => new List<FieldChange> { ApplyExtraServices(workOrder, value, auditField) },
|
WorkOrderBoardFieldNames.ExtraServices => new List<FieldChange> { ApplyExtraServices(workOrder, value, auditField) },
|
||||||
|
WorkOrderBoardFieldNames.AdditionalContacts => new List<FieldChange> { ApplyAdditionalContacts(workOrder, value, auditField) },
|
||||||
WorkOrderBoardFieldNames.FlagColor => new List<FieldChange> { ApplyFlagColor(workOrder, value, auditField) },
|
WorkOrderBoardFieldNames.FlagColor => new List<FieldChange> { ApplyFlagColor(workOrder, value, auditField) },
|
||||||
_ => throw new WorkOrderBoardValidationException("InvalidField", $"Field '{field}' is not editable.")
|
_ => throw new WorkOrderBoardValidationException("InvalidField", $"Field '{field}' is not editable.")
|
||||||
};
|
};
|
||||||
|
|
@ -321,6 +322,18 @@ namespace SeaHaven.Services.Implementation
|
||||||
return FieldChange.ForField(auditField, old, normalizedJson);
|
return FieldChange.ForField(auditField, old, normalizedJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static FieldChange ApplyAdditionalContacts(WorkOrder workOrder, string? value, string auditField)
|
||||||
|
{
|
||||||
|
var normalizedJson = WorkOrderAdditionalContactsMapper.SerializeForPatch(value);
|
||||||
|
|
||||||
|
var old = workOrder.AdditionalContacts;
|
||||||
|
if (old == normalizedJson)
|
||||||
|
return FieldChange.Unchanged(auditField);
|
||||||
|
|
||||||
|
workOrder.AdditionalContacts = normalizedJson;
|
||||||
|
return FieldChange.ForField(auditField, old, normalizedJson);
|
||||||
|
}
|
||||||
|
|
||||||
private static List<string> NormalizeExtraServiceList(List<string>? extras)
|
private static List<string> NormalizeExtraServiceList(List<string>? extras)
|
||||||
{
|
{
|
||||||
var normalized = new List<string>();
|
var normalized = new List<string>();
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,24 @@ namespace SeaHaven.Services.Validation
|
||||||
RuleForEach(x => x.ExtraServices)
|
RuleForEach(x => x.ExtraServices)
|
||||||
.MaximumLength(128)
|
.MaximumLength(128)
|
||||||
.When(x => x.ExtraServices != null);
|
.When(x => x.ExtraServices != null);
|
||||||
|
|
||||||
|
RuleForEach(x => x.AdditionalContacts)
|
||||||
|
.ChildRules(contact =>
|
||||||
|
{
|
||||||
|
contact.RuleFor(c => c.Name)
|
||||||
|
.MaximumLength(200)
|
||||||
|
.When(c => !string.IsNullOrEmpty(c.Name));
|
||||||
|
contact.RuleFor(c => c.Phone)
|
||||||
|
.MaximumLength(50)
|
||||||
|
.When(c => !string.IsNullOrEmpty(c.Phone));
|
||||||
|
contact.RuleFor(c => c.Notes)
|
||||||
|
.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.");
|
||||||
|
})
|
||||||
|
.When(x => x.AdditionalContacts != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool HasPrimaryWhenExtras(WorkOrderBoardCreateRequestDto request)
|
private static bool HasPrimaryWhenExtras(WorkOrderBoardCreateRequestDto request)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ using Microsoft.EntityFrameworkCore;
|
||||||
using SeaHaven.DataServices.Implementation;
|
using SeaHaven.DataServices.Implementation;
|
||||||
using SeaHaven.Services.DTOs;
|
using SeaHaven.Services.DTOs;
|
||||||
using SeaHaven.Services.Exceptions;
|
using SeaHaven.Services.Exceptions;
|
||||||
|
using SeaHaven.Services.Helpers;
|
||||||
using SeaHaven.Services.Implementation;
|
using SeaHaven.Services.Implementation;
|
||||||
|
using SeaHaven.Services.Interfaces;
|
||||||
using SeaHaven.Services.Validation;
|
using SeaHaven.Services.Validation;
|
||||||
|
|
||||||
namespace SeaHavenIndustries.Tests;
|
namespace SeaHavenIndustries.Tests;
|
||||||
|
|
@ -351,4 +353,34 @@ public class WorkOrderBoardCreateServiceTests
|
||||||
|
|
||||||
Assert.Equal("VendorNotFound", ex.Code);
|
Assert.Equal("VendorNotFound", ex.Code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Create_AdditionalContacts_PersistsAndReturnsOnBoardRow()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
|
||||||
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
||||||
|
{
|
||||||
|
WorkOrderType = WorkOrderType.Reactive,
|
||||||
|
SiteCode = "BK5",
|
||||||
|
PocName = "Primary Lead",
|
||||||
|
AdditionalContacts = new List<WorkOrderAdditionalContactDto>
|
||||||
|
{
|
||||||
|
new("Backup Lead", "+1 555-0101", "After hours"),
|
||||||
|
new("Security Desk", "+1 555-0102", null)
|
||||||
|
}
|
||||||
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
||||||
|
|
||||||
|
Assert.Equal("Primary Lead", result.PocName);
|
||||||
|
Assert.NotNull(result.AdditionalContacts);
|
||||||
|
Assert.Equal(2, result.AdditionalContacts!.Count);
|
||||||
|
Assert.Equal("Backup Lead", result.AdditionalContacts[0].Name);
|
||||||
|
Assert.Equal("+1 555-0101", result.AdditionalContacts[0].Phone);
|
||||||
|
Assert.Equal("After hours", result.AdditionalContacts[0].Notes);
|
||||||
|
|
||||||
|
var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id);
|
||||||
|
Assert.NotNull(wo.AdditionalContacts);
|
||||||
|
Assert.Contains("Backup Lead", wo.AdditionalContacts);
|
||||||
|
Assert.Contains("Security Desk", wo.AdditionalContacts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -354,6 +354,56 @@ public class WorkOrderBoardUpdateServiceTests
|
||||||
Assert.Equal("InvalidValue", ex.Code);
|
Assert.Equal("InvalidValue", ex.Code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PatchField_UpdatesAdditionalContacts()
|
||||||
|
{
|
||||||
|
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\":\"Jane Backup\",\"phone\":\"+1 555-0101\",\"notes\":\"Call first\"}]",
|
||||||
|
WorkOrderVersion = ToVersion(wo)
|
||||||
|
}, "actor-1");
|
||||||
|
|
||||||
|
Assert.NotNull(result.AdditionalContacts);
|
||||||
|
Assert.Single(result.AdditionalContacts!);
|
||||||
|
Assert.Equal("Jane Backup", result.AdditionalContacts![0].Name);
|
||||||
|
Assert.Contains("Jane Backup", (await context.workOrders.FindAsync(1))!.AdditionalContacts);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PatchField_InvalidAdditionalContactsJson_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 = "not-json",
|
||||||
|
WorkOrderVersion = ToVersion(wo)
|
||||||
|
}, "actor-1"));
|
||||||
|
|
||||||
|
Assert.Equal("InvalidValue", ex.Code);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PatchField_SetsFlagColor_CanonicalUppercase()
|
public async Task PatchField_SetsFlagColor_CanonicalUppercase()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue