shoc-backend/SeaHaven.Services/Helpers/WorkOrderAdditionalContactsMapper.cs
Arthur Bassi 36ef0b00f5 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.
2026-08-13 16:11:36 -03:00

120 lines
4.1 KiB
C#

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;
}
}
}