shoc-backend/SeaHaven.Services/Helpers/WorkOrderPocSiteContact.cs
Alexandre Brandizzi 582af8fb2c fix(workorders): compare manual POC against the followed contact, not any site contact
The manual POC "follow the site" clear-rule compared the edit against every
live Site contact. A work order only ever displays one of them, so editing to
a different live contact (Site has Alice primary and Bob; WO shows Alice; edit
to Bob) matched, cleared the override, and left the row showing Alice with no
audit row written — the SH-379 symptom on a different input. A work order with
a linked WorkOrderContacts POC hit the same bug when the dispatcher typed the
Site's primary: the override cleared and the linked contact showed instead.

Compare the edit against the single contact the work order actually follows —
the linked WorkOrderContacts POC, or else the Site primary (ResolvePrimary) —
matching the board projection's override -> linked -> site precedence, and
store the override whenever the edit differs from it. The create path in
WorkOrderBoardCreateService had the same any-contact rule and gets the same
fix; a supplied PocContactId that is not a live Site contact leaves no follow
target, so the typed POC is stored.

Replaces MatchesAnySiteContact with Matches(name, phone, contact); comment and
PR-body wording updated to state the followed-contact rule. Adds tests for the
second-site-contact edit, the linked-contact-differs edit, and the
second-site-contact create case.
2026-09-18 18:56:29 -03:00

51 lines
2.3 KiB
C#

using Data.SeaHavenIndustries;
namespace SeaHaven.Services.Helpers
{
/// <summary>
/// SH-379 / SH-190: single resolution for "the Site's live contact" used by the
/// manual POC override. The same rules back the board projection fallback, the
/// completion snapshot fallback, the create-time baseline, and the
/// "changing away from the contact the work order follows" lock comparison.
/// </summary>
public static class WorkOrderPocSiteContact
{
/// <summary>
/// The Site's primary live contact: first non-deleted contact ordered by
/// SiteContactOrder then Id. Null when the site has no live contacts.
/// </summary>
public static Contacts? ResolvePrimary(IEnumerable<Contacts>? contacts)
=> contacts?
.Where(c => c.IsDeleted != true)
.OrderBy(c => c.SiteContactOrder)
.ThenBy(c => c.Id)
.FirstOrDefault();
/// <summary>Display name matching the board projection: trimmed First + Last.</summary>
public static string? DisplayName(Contacts? contact)
=> contact == null
? null
: Normalize(((contact.FirstName ?? "") + " " + (contact.LastName ?? "")).Trim());
/// <summary>
/// SH-190: true when a manual POC (trimmed name + phone) equals the single
/// contact the work order currently follows — the linked WorkOrderContacts
/// POC, or else the Site primary. Such an edit is not a change away from
/// that contact, so it must not lock or store an override. Comparing against
/// only the followed contact (rather than any live Site contact) means an
/// edit to a different live contact is still stored, since the work order
/// only ever displays the one it follows.
/// </summary>
public static bool Matches(string? name, string? phone, Contacts? contact)
{
if (contact == null)
return false;
return string.Equals(DisplayName(contact), Normalize(name), StringComparison.Ordinal)
&& string.Equals(Normalize(contact.PhoneNumber), Normalize(phone), StringComparison.Ordinal);
}
private static string? Normalize(string? value)
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
}