mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-06 18:12:11 +00:00
feat(work-orders): capture Site/Vendor/POC snapshot on Completed
Freeze effective live values on first Completed transition and project them on GET.
This commit is contained in:
parent
6d58865253
commit
e0139d4601
14 changed files with 606 additions and 11 deletions
|
|
@ -0,0 +1,41 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Data.SeaHavenIndustries.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260910143000_WorkOrderCompletionSnapshot")]
|
||||||
|
public partial class WorkOrderCompletionSnapshot : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "FrozenCompany",
|
||||||
|
table: "workOrders",
|
||||||
|
type: "nvarchar(max)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "FrozenPoc",
|
||||||
|
table: "workOrders",
|
||||||
|
type: "nvarchar(max)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "FrozenSite",
|
||||||
|
table: "workOrders",
|
||||||
|
type: "nvarchar(max)",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(name: "FrozenCompany", table: "workOrders");
|
||||||
|
migrationBuilder.DropColumn(name: "FrozenPoc", table: "workOrders");
|
||||||
|
migrationBuilder.DropColumn(name: "FrozenSite", table: "workOrders");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2494,6 +2494,15 @@ namespace Data.SeaHavenIndustries.Migrations
|
||||||
.HasMaxLength(7)
|
.HasMaxLength(7)
|
||||||
.HasColumnType("nvarchar(7)");
|
.HasColumnType("nvarchar(7)");
|
||||||
|
|
||||||
|
b.Property<string>("FrozenCompany")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("FrozenPoc")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("FrozenSite")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b.Property<string>("InternalWONumber")
|
b.Property<string>("InternalWONumber")
|
||||||
.HasMaxLength(11)
|
.HasMaxLength(11)
|
||||||
.HasColumnType("nvarchar(11)");
|
.HasColumnType("nvarchar(11)");
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
namespace Data.SeaHavenIndustries
|
||||||
|
{
|
||||||
|
public sealed class WorkOrderFrozenSite
|
||||||
|
{
|
||||||
|
public string? Code { get; set; }
|
||||||
|
public string? Label { get; set; }
|
||||||
|
public string? Address { get; set; }
|
||||||
|
public string? City { get; set; }
|
||||||
|
public string? State { get; set; }
|
||||||
|
public string? Zip { get; set; }
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? Maps { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class WorkOrderFrozenCompany
|
||||||
|
{
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? ContactName { get; set; }
|
||||||
|
public string? Address { get; set; }
|
||||||
|
public string? City { get; set; }
|
||||||
|
public string? State { get; set; }
|
||||||
|
public string? Zip { get; set; }
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? Maps { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class WorkOrderFrozenPoc
|
||||||
|
{
|
||||||
|
public List<WorkOrderFrozenPocContact> Contacts { get; set; } = new();
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class WorkOrderFrozenPocContact
|
||||||
|
{
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -93,6 +93,12 @@ namespace Data.SeaHavenIndustries
|
||||||
|
|
||||||
// --- Completion slice ---
|
// --- Completion slice ---
|
||||||
public DocStatus? DocStatus { get; set; }
|
public DocStatus? DocStatus { get; set; }
|
||||||
|
/// <summary>JSON site snapshot captured on the first transition to Completed.</summary>
|
||||||
|
public string? FrozenSite { get; set; }
|
||||||
|
/// <summary>JSON live-vendor snapshot captured on the first transition to Completed.</summary>
|
||||||
|
public string? FrozenCompany { get; set; }
|
||||||
|
/// <summary>JSON effective POC snapshot captured on the first transition to Completed.</summary>
|
||||||
|
public string? FrozenPoc { get; set; }
|
||||||
|
|
||||||
/// <summary>Board flag color (#RRGGBB from fixed palette). Null = no flag.</summary>
|
/// <summary>Board flag color (#RRGGBB from fixed palette). Null = no flag.</summary>
|
||||||
[MaxLength(7)]
|
[MaxLength(7)]
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,15 @@ using Data.SeaHavenIndustries;
|
||||||
using Data.SeaHavenIndustries.Enums;
|
using Data.SeaHavenIndustries.Enums;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SeaHaven.DataServices.Interfaces;
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace SeaHaven.DataServices.Helpers
|
namespace SeaHaven.DataServices.Helpers
|
||||||
{
|
{
|
||||||
public static class WorkOrderBoardProjection
|
public static class WorkOrderBoardProjection
|
||||||
{
|
{
|
||||||
|
private static readonly JsonSerializerOptions SnapshotJsonOptions =
|
||||||
|
new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
public static async Task<IReadOnlyList<WorkOrderBoardRawRow>> ProjectRowsAsync(
|
public static async Task<IReadOnlyList<WorkOrderBoardRawRow>> ProjectRowsAsync(
|
||||||
ApplicationDbContext context,
|
ApplicationDbContext context,
|
||||||
IQueryable<WorkOrder> query,
|
IQueryable<WorkOrder> query,
|
||||||
|
|
@ -81,6 +85,9 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
w.AdditionalContacts,
|
w.AdditionalContacts,
|
||||||
w.DocStatus,
|
w.DocStatus,
|
||||||
w.CompletedDate,
|
w.CompletedDate,
|
||||||
|
w.FrozenSite,
|
||||||
|
w.FrozenCompany,
|
||||||
|
w.FrozenPoc,
|
||||||
w.FlagColor,
|
w.FlagColor,
|
||||||
w.PrimaryDispatchId,
|
w.PrimaryDispatchId,
|
||||||
w.RowVersion,
|
w.RowVersion,
|
||||||
|
|
@ -96,9 +103,22 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
|
|
||||||
return rows.Select(w =>
|
return rows.Select(w =>
|
||||||
{
|
{
|
||||||
var pocName = !string.IsNullOrWhiteSpace(w.WoPocName) ? w.WoPocName : w.ContactPoc?.Name;
|
var frozenSite = w.LifecycleStatus == LifecycleStatus.Completed
|
||||||
var pocPhone = !string.IsNullOrWhiteSpace(w.WoPocPhone) ? w.WoPocPhone : w.ContactPoc?.PhoneNumber;
|
? Deserialize<WorkOrderFrozenSite>(w.FrozenSite)
|
||||||
var pocNotes = !string.IsNullOrWhiteSpace(w.WoPocNotes) ? w.WoPocNotes : w.ContactPoc?.Notes;
|
: null;
|
||||||
|
var frozenCompany = w.LifecycleStatus == LifecycleStatus.Completed
|
||||||
|
? Deserialize<WorkOrderFrozenCompany>(w.FrozenCompany)
|
||||||
|
: null;
|
||||||
|
var frozenPoc = w.LifecycleStatus == LifecycleStatus.Completed
|
||||||
|
? Deserialize<WorkOrderFrozenPoc>(w.FrozenPoc)
|
||||||
|
: null;
|
||||||
|
var primaryFrozenPoc = frozenPoc?.Contacts.FirstOrDefault();
|
||||||
|
var pocName = primaryFrozenPoc?.Name
|
||||||
|
?? (!string.IsNullOrWhiteSpace(w.WoPocName) ? w.WoPocName : w.ContactPoc?.Name);
|
||||||
|
var pocPhone = primaryFrozenPoc?.Phone
|
||||||
|
?? (!string.IsNullOrWhiteSpace(w.WoPocPhone) ? w.WoPocPhone : w.ContactPoc?.PhoneNumber);
|
||||||
|
var pocNotes = frozenPoc?.Notes
|
||||||
|
?? (!string.IsNullOrWhiteSpace(w.WoPocNotes) ? w.WoPocNotes : w.ContactPoc?.Notes);
|
||||||
var techPhone = !string.IsNullOrWhiteSpace(w.DispatchTechPhone)
|
var techPhone = !string.IsNullOrWhiteSpace(w.DispatchTechPhone)
|
||||||
? w.DispatchTechPhone
|
? w.DispatchTechPhone
|
||||||
: (!string.IsNullOrWhiteSpace(w.WoTechPhone) ? w.WoTechPhone : w.VendorPhone);
|
: (!string.IsNullOrWhiteSpace(w.WoTechPhone) ? w.WoTechPhone : w.VendorPhone);
|
||||||
|
|
@ -106,6 +126,7 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
w.VendorId,
|
w.VendorId,
|
||||||
w.VendorName,
|
w.VendorName,
|
||||||
w.PrimaryDispatchStatus);
|
w.PrimaryDispatchStatus);
|
||||||
|
vendorName = frozenCompany?.Name ?? vendorName;
|
||||||
upliftByWorkOrder.TryGetValue(w.Id, out var uplift);
|
upliftByWorkOrder.TryGetValue(w.Id, out var uplift);
|
||||||
|
|
||||||
return new WorkOrderBoardRawRow(
|
return new WorkOrderBoardRawRow(
|
||||||
|
|
@ -115,8 +136,8 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
w.CarriedOver,
|
w.CarriedOver,
|
||||||
w.IsAddOn,
|
w.IsAddOn,
|
||||||
w.WorkOrderType,
|
w.WorkOrderType,
|
||||||
w.SiteCode,
|
frozenSite?.Code ?? w.SiteCode,
|
||||||
w.LocationName,
|
frozenSite?.Label ?? w.LocationName,
|
||||||
pocName,
|
pocName,
|
||||||
pocPhone,
|
pocPhone,
|
||||||
pocNotes,
|
pocNotes,
|
||||||
|
|
@ -135,8 +156,8 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
w.ScheduleWeekOnly,
|
w.ScheduleWeekOnly,
|
||||||
vendorId,
|
vendorId,
|
||||||
vendorName,
|
vendorName,
|
||||||
w.TechName,
|
frozenCompany?.ContactName ?? w.TechName,
|
||||||
techPhone,
|
frozenCompany?.Phone ?? techPhone,
|
||||||
w.DispatchApptDate,
|
w.DispatchApptDate,
|
||||||
w.Trade ?? w.Problem,
|
w.Trade ?? w.Problem,
|
||||||
w.Problem,
|
w.Problem,
|
||||||
|
|
@ -160,10 +181,28 @@ namespace SeaHaven.DataServices.Helpers
|
||||||
w.OriginalWeek,
|
w.OriginalWeek,
|
||||||
isUnscheduled,
|
isUnscheduled,
|
||||||
w.Severity,
|
w.Severity,
|
||||||
w.MediaCount);
|
w.MediaCount,
|
||||||
|
frozenSite,
|
||||||
|
frozenCompany,
|
||||||
|
frozenPoc);
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static T? Deserialize<T>(string? json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
return default;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<T>(json, SnapshotJsonOptions);
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SH-188: one round-trip for live uplifts on WOs in <paramref name="workOrders"/>
|
/// SH-188: one round-trip for live uplifts on WOs in <paramref name="workOrders"/>
|
||||||
/// (primary WorkOrderId and multi-WO DispatchWorkOrders links). Uses the same
|
/// (primary WorkOrderId and multi-WO DispatchWorkOrders links). Uses the same
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,13 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
=> await _context.workOrders.MaxAsync(w => (int?)w.Id, cancellationToken) ?? 0;
|
=> await _context.workOrders.MaxAsync(w => (int?)w.Id, cancellationToken) ?? 0;
|
||||||
|
|
||||||
public Task<WorkOrder?> GetTrackedWorkOrderAsync(int workOrderId, CancellationToken cancellationToken)
|
public Task<WorkOrder?> GetTrackedWorkOrderAsync(int workOrderId, CancellationToken cancellationToken)
|
||||||
=> _context.workOrders.FirstOrDefaultAsync(
|
=> _context.workOrders
|
||||||
|
.Include(workOrder => workOrder.Locations)
|
||||||
|
.Include(workOrder => workOrder.WorkOrderContacts!)
|
||||||
|
.ThenInclude(contact => contact.POC)
|
||||||
|
.Include(workOrder => workOrder.PrimaryDispatch!)
|
||||||
|
.ThenInclude(dispatch => dispatch.Vendor)
|
||||||
|
.FirstOrDefaultAsync(
|
||||||
w => w.Id == workOrderId && w.istemplate != true && (w.IsDeleted != true || w.IsDeleted == null),
|
w => w.Id == workOrderId && w.istemplate != true && (w.IsDeleted != true || w.IsDeleted == null),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
using Data.SeaHavenIndustries.Enums;
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
|
||||||
namespace SeaHaven.DataServices.Interfaces
|
namespace SeaHaven.DataServices.Interfaces
|
||||||
|
|
@ -65,7 +66,10 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
DateOnly? OriginalWeek,
|
DateOnly? OriginalWeek,
|
||||||
bool IsUnscheduled,
|
bool IsUnscheduled,
|
||||||
string? Severity = null,
|
string? Severity = null,
|
||||||
int MediaCount = 0);
|
int MediaCount = 0,
|
||||||
|
WorkOrderFrozenSite? FrozenSite = null,
|
||||||
|
WorkOrderFrozenCompany? FrozenCompany = null,
|
||||||
|
WorkOrderFrozenPoc? FrozenPoc = null);
|
||||||
|
|
||||||
public record WorkOrderBoardQueryResult(
|
public record WorkOrderBoardQueryResult(
|
||||||
IReadOnlyList<WorkOrderBoardRawRow> ScheduledRows,
|
IReadOnlyList<WorkOrderBoardRawRow> ScheduledRows,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,44 @@ namespace SeaHaven.Services.DTOs
|
||||||
public decimal? Amount { get; set; }
|
public decimal? Amount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class WorkOrderFrozenSiteDto
|
||||||
|
{
|
||||||
|
public string? Code { get; set; }
|
||||||
|
public string? Label { get; set; }
|
||||||
|
public string? Address { get; set; }
|
||||||
|
public string? City { get; set; }
|
||||||
|
public string? State { get; set; }
|
||||||
|
public string? Zip { get; set; }
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? Maps { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WorkOrderFrozenCompanyDto
|
||||||
|
{
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? ContactName { get; set; }
|
||||||
|
public string? Address { get; set; }
|
||||||
|
public string? City { get; set; }
|
||||||
|
public string? State { get; set; }
|
||||||
|
public string? Zip { get; set; }
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
public string? Email { get; set; }
|
||||||
|
public string? Maps { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WorkOrderFrozenPocDto
|
||||||
|
{
|
||||||
|
public List<WorkOrderFrozenPocContactDto> Contacts { get; set; } = new();
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WorkOrderFrozenPocContactDto
|
||||||
|
{
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class WorkOrderBoardRowDto
|
public class WorkOrderBoardRowDto
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
@ -53,6 +91,9 @@ namespace SeaHaven.Services.DTOs
|
||||||
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 List<WorkOrderAdditionalContactDto>? AdditionalContacts { get; set; }
|
||||||
|
public WorkOrderFrozenSiteDto? FrozenSite { get; set; }
|
||||||
|
public WorkOrderFrozenCompanyDto? FrozenCompany { get; set; }
|
||||||
|
public WorkOrderFrozenPocDto? FrozenPoc { 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; }
|
||||||
|
|
|
||||||
155
SeaHaven.Services/Helpers/WorkOrderCompletionSnapshotMapper.cs
Normal file
155
SeaHaven.Services/Helpers/WorkOrderCompletionSnapshotMapper.cs
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using SeaHaven.DataServices.Helpers;
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
|
using SeaHaven.Services.Exceptions;
|
||||||
|
|
||||||
|
namespace SeaHaven.Services.Helpers
|
||||||
|
{
|
||||||
|
public static class WorkOrderCompletionSnapshotMapper
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
public static void Capture(WorkOrder workOrder)
|
||||||
|
{
|
||||||
|
var location = workOrder.Locations
|
||||||
|
?? throw new WorkOrderBoardValidationException(
|
||||||
|
"CompletionSnapshotUnavailable",
|
||||||
|
"Site details are required before marking the work order Completed.");
|
||||||
|
|
||||||
|
var contact = workOrder.WorkOrderContacts?
|
||||||
|
.OrderBy(item => item.Id)
|
||||||
|
.FirstOrDefault();
|
||||||
|
var pocName = FirstNotBlank(
|
||||||
|
workOrder.PocName,
|
||||||
|
JoinName(contact?.POC?.FirstName, contact?.POC?.MiddleName, contact?.POC?.LastName));
|
||||||
|
var pocPhone = FirstNotBlank(workOrder.PocPhone, contact?.POC?.PhoneNumber);
|
||||||
|
var pocNotes = FirstNotBlank(workOrder.PocNotes, contact?.Notes);
|
||||||
|
|
||||||
|
var contacts = new List<WorkOrderFrozenPocContact>();
|
||||||
|
AddContact(contacts, pocName, pocPhone);
|
||||||
|
foreach (var additional in WorkOrderAdditionalContactsMapper.ParseJson(workOrder.AdditionalContacts)
|
||||||
|
?? Enumerable.Empty<WorkOrderAdditionalContactDto>())
|
||||||
|
{
|
||||||
|
AddContact(contacts, additional.Name, additional.Phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
workOrder.FrozenSite = JsonSerializer.Serialize(
|
||||||
|
new WorkOrderFrozenSite
|
||||||
|
{
|
||||||
|
Code = workOrder.SiteCode,
|
||||||
|
Label = FirstNotBlank(location.Name, location.Title, workOrder.SiteCode),
|
||||||
|
Address = JoinAddress(location.Address1, location.Address2),
|
||||||
|
City = location.City,
|
||||||
|
State = location.State,
|
||||||
|
Zip = location.Zip,
|
||||||
|
Phone = location.PhoneNumber,
|
||||||
|
Email = location.Email,
|
||||||
|
Maps = BuildMapsUrl(location)
|
||||||
|
},
|
||||||
|
JsonOptions);
|
||||||
|
|
||||||
|
var liveVendor = ResolveLiveVendor(workOrder.PrimaryDispatch);
|
||||||
|
workOrder.FrozenCompany = liveVendor == null
|
||||||
|
? null
|
||||||
|
: JsonSerializer.Serialize(
|
||||||
|
new WorkOrderFrozenCompany
|
||||||
|
{
|
||||||
|
Name = liveVendor.CompanyName,
|
||||||
|
ContactName = liveVendor.ContactName,
|
||||||
|
Address = liveVendor.Address,
|
||||||
|
City = liveVendor.City,
|
||||||
|
State = liveVendor.State,
|
||||||
|
Zip = liveVendor.Zip,
|
||||||
|
Phone = FirstNotBlank(liveVendor.CompanyPhone, liveVendor.Phone),
|
||||||
|
Email = liveVendor.Email,
|
||||||
|
Maps = liveVendor.GoogleMapsUrl
|
||||||
|
},
|
||||||
|
JsonOptions);
|
||||||
|
|
||||||
|
workOrder.FrozenPoc = JsonSerializer.Serialize(
|
||||||
|
new WorkOrderFrozenPoc
|
||||||
|
{
|
||||||
|
Contacts = contacts,
|
||||||
|
Notes = pocNotes
|
||||||
|
},
|
||||||
|
JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WorkOrderFrozenSite? ParseSite(string? json)
|
||||||
|
=> Deserialize<WorkOrderFrozenSite>(json);
|
||||||
|
|
||||||
|
public static WorkOrderFrozenCompany? ParseCompany(string? json)
|
||||||
|
=> Deserialize<WorkOrderFrozenCompany>(json);
|
||||||
|
|
||||||
|
public static WorkOrderFrozenPoc? ParsePoc(string? json)
|
||||||
|
=> Deserialize<WorkOrderFrozenPoc>(json);
|
||||||
|
|
||||||
|
private static Vendor? ResolveLiveVendor(Dispatch? dispatch)
|
||||||
|
{
|
||||||
|
var (vendorId, _) = WorkOrderBoardDispatchAssignment.LiveVendor(
|
||||||
|
dispatch?.VendorId,
|
||||||
|
dispatch?.Vendor?.CompanyName,
|
||||||
|
dispatch?.Status);
|
||||||
|
return vendorId.HasValue ? dispatch?.Vendor : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? BuildMapsUrl(Locations location)
|
||||||
|
{
|
||||||
|
if (!double.TryParse(location.Latitude, NumberStyles.Float, CultureInfo.InvariantCulture, out var latitude)
|
||||||
|
|| !double.TryParse(location.Longitude, NumberStyles.Float, CultureInfo.InvariantCulture, out var longitude))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FormattableString.Invariant(
|
||||||
|
$"https://www.google.com/maps/search/?api=1&query={latitude},{longitude}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? JoinAddress(params string?[] parts)
|
||||||
|
{
|
||||||
|
var value = string.Join(", ", parts.Where(part => !string.IsNullOrWhiteSpace(part)));
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? null : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? JoinName(params string?[] parts)
|
||||||
|
{
|
||||||
|
var value = string.Join(" ", parts.Where(part => !string.IsNullOrWhiteSpace(part)));
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? null : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FirstNotBlank(params string?[] values)
|
||||||
|
=> values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim();
|
||||||
|
|
||||||
|
private static void AddContact(
|
||||||
|
ICollection<WorkOrderFrozenPocContact> contacts,
|
||||||
|
string? name,
|
||||||
|
string? phone)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name) && string.IsNullOrWhiteSpace(phone))
|
||||||
|
return;
|
||||||
|
|
||||||
|
contacts.Add(new WorkOrderFrozenPocContact
|
||||||
|
{
|
||||||
|
Name = name?.Trim(),
|
||||||
|
Phone = phone?.Trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T? Deserialize<T>(string? json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
return default;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<T>(json, JsonOptions);
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
using Data.SeaHavenIndustries.Enums;
|
using Data.SeaHavenIndustries.Enums;
|
||||||
using SeaHaven.DataServices.Helpers;
|
using SeaHaven.DataServices.Helpers;
|
||||||
using SeaHaven.DataServices.Interfaces;
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
|
@ -113,6 +114,9 @@ namespace SeaHaven.Services.Implementation
|
||||||
PocName = row.PocName,
|
PocName = row.PocName,
|
||||||
PocPhone = row.PocPhone,
|
PocPhone = row.PocPhone,
|
||||||
PocNotes = row.PocNotes,
|
PocNotes = row.PocNotes,
|
||||||
|
FrozenSite = MapFrozenSite(row.FrozenSite),
|
||||||
|
FrozenCompany = MapFrozenCompany(row.FrozenCompany),
|
||||||
|
FrozenPoc = MapFrozenPoc(row.FrozenPoc),
|
||||||
LifecycleStatus = row.LifecycleStatus,
|
LifecycleStatus = row.LifecycleStatus,
|
||||||
LifecycleStatusLabel = WorkOrderDerivedFields.GetLifecycleStatusLabel(row.LifecycleStatus),
|
LifecycleStatusLabel = WorkOrderDerivedFields.GetLifecycleStatusLabel(row.LifecycleStatus),
|
||||||
LegacyStatus = row.LegacyStatus,
|
LegacyStatus = row.LegacyStatus,
|
||||||
|
|
@ -135,7 +139,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),
|
AdditionalContacts = MapAdditionalContacts(row),
|
||||||
DocStatus = row.DocStatus,
|
DocStatus = row.DocStatus,
|
||||||
MediaCount = row.MediaCount,
|
MediaCount = row.MediaCount,
|
||||||
CompletedDate = row.CompletedDate,
|
CompletedDate = row.CompletedDate,
|
||||||
|
|
@ -149,6 +153,71 @@ namespace SeaHaven.Services.Implementation
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<WorkOrderAdditionalContactDto>? MapAdditionalContacts(
|
||||||
|
WorkOrderBoardRawRow row)
|
||||||
|
{
|
||||||
|
if (row.FrozenPoc == null)
|
||||||
|
return WorkOrderAdditionalContactsMapper.ParseJson(row.AdditionalContacts);
|
||||||
|
|
||||||
|
var additional = row.FrozenPoc.Contacts
|
||||||
|
.Skip(1)
|
||||||
|
.Where(contact => !string.IsNullOrWhiteSpace(contact.Name)
|
||||||
|
|| !string.IsNullOrWhiteSpace(contact.Phone))
|
||||||
|
.Select(contact => new WorkOrderAdditionalContactDto(
|
||||||
|
contact.Name ?? "",
|
||||||
|
contact.Phone ?? "",
|
||||||
|
null))
|
||||||
|
.ToList();
|
||||||
|
return additional.Count > 0 ? additional : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorkOrderFrozenSiteDto? MapFrozenSite(WorkOrderFrozenSite? site)
|
||||||
|
=> site == null
|
||||||
|
? null
|
||||||
|
: new WorkOrderFrozenSiteDto
|
||||||
|
{
|
||||||
|
Code = site.Code,
|
||||||
|
Label = site.Label,
|
||||||
|
Address = site.Address,
|
||||||
|
City = site.City,
|
||||||
|
State = site.State,
|
||||||
|
Zip = site.Zip,
|
||||||
|
Phone = site.Phone,
|
||||||
|
Email = site.Email,
|
||||||
|
Maps = site.Maps
|
||||||
|
};
|
||||||
|
|
||||||
|
private static WorkOrderFrozenCompanyDto? MapFrozenCompany(WorkOrderFrozenCompany? company)
|
||||||
|
=> company == null
|
||||||
|
? null
|
||||||
|
: new WorkOrderFrozenCompanyDto
|
||||||
|
{
|
||||||
|
Name = company.Name,
|
||||||
|
ContactName = company.ContactName,
|
||||||
|
Address = company.Address,
|
||||||
|
City = company.City,
|
||||||
|
State = company.State,
|
||||||
|
Zip = company.Zip,
|
||||||
|
Phone = company.Phone,
|
||||||
|
Email = company.Email,
|
||||||
|
Maps = company.Maps
|
||||||
|
};
|
||||||
|
|
||||||
|
private static WorkOrderFrozenPocDto? MapFrozenPoc(WorkOrderFrozenPoc? poc)
|
||||||
|
=> poc == null
|
||||||
|
? null
|
||||||
|
: new WorkOrderFrozenPocDto
|
||||||
|
{
|
||||||
|
Notes = poc.Notes,
|
||||||
|
Contacts = poc.Contacts
|
||||||
|
.Select(contact => new WorkOrderFrozenPocContactDto
|
||||||
|
{
|
||||||
|
Name = contact.Name,
|
||||||
|
Phone = contact.Phone
|
||||||
|
})
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
|
||||||
private static bool HasClockTime(DateTime? value)
|
private static bool HasClockTime(DateTime? value)
|
||||||
=> value.HasValue && value.Value.TimeOfDay != TimeSpan.Zero;
|
=> value.HasValue && value.Value.TimeOfDay != TimeSpan.Zero;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -530,6 +530,9 @@ namespace SeaHaven.Services.Implementation
|
||||||
var old = workOrder.LifecycleStatus?.ToString() ?? workOrder.Status;
|
var old = workOrder.LifecycleStatus?.ToString() ?? workOrder.Status;
|
||||||
if (workOrder.LifecycleStatus != parsed)
|
if (workOrder.LifecycleStatus != parsed)
|
||||||
{
|
{
|
||||||
|
if (parsed == LifecycleStatus.Completed)
|
||||||
|
WorkOrderCompletionSnapshotMapper.Capture(workOrder);
|
||||||
|
|
||||||
workOrder.LifecycleStatus = parsed;
|
workOrder.LifecycleStatus = parsed;
|
||||||
workOrder.Status = WorkOrderDerivedFields.GetLifecycleStatusLabel(parsed);
|
workOrder.Status = WorkOrderDerivedFields.GetLifecycleStatusLabel(parsed);
|
||||||
if (workOrder.LegacyStatus == null && workOrder.Status != null)
|
if (workOrder.LegacyStatus == null && workOrder.Status != null)
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,9 @@ namespace SeaHaven.Services.Implementation
|
||||||
PocPhone = row.PocPhone,
|
PocPhone = row.PocPhone,
|
||||||
PocNotes = row.PocNotes,
|
PocNotes = row.PocNotes,
|
||||||
AdditionalContacts = row.AdditionalContacts,
|
AdditionalContacts = row.AdditionalContacts,
|
||||||
|
FrozenSite = row.FrozenSite,
|
||||||
|
FrozenCompany = row.FrozenCompany,
|
||||||
|
FrozenPoc = row.FrozenPoc,
|
||||||
LifecycleStatus = row.LifecycleStatus,
|
LifecycleStatus = row.LifecycleStatus,
|
||||||
LifecycleStatusLabel = row.LifecycleStatusLabel,
|
LifecycleStatusLabel = row.LifecycleStatusLabel,
|
||||||
LegacyStatus = row.LegacyStatus,
|
LegacyStatus = row.LegacyStatus,
|
||||||
|
|
|
||||||
|
|
@ -397,11 +397,13 @@ public class WorkOrderBoardUpdateServiceTests
|
||||||
var wo = new WorkOrder
|
var wo = new WorkOrder
|
||||||
{
|
{
|
||||||
Id = 1,
|
Id = 1,
|
||||||
|
LocationId = 10,
|
||||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
DueDate = DateTime.UtcNow.Date.AddDays(-3),
|
DueDate = DateTime.UtcNow.Date.AddDays(-3),
|
||||||
ScheduledDate = DateTime.UtcNow.Date.AddDays(-1),
|
ScheduledDate = DateTime.UtcNow.Date.AddDays(-1),
|
||||||
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||||
};
|
};
|
||||||
|
context.Locations.Add(new Locations { Id = 10, Name = "Test Site" });
|
||||||
context.workOrders.Add(wo);
|
context.workOrders.Add(wo);
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
@ -435,12 +437,14 @@ public class WorkOrderBoardUpdateServiceTests
|
||||||
var wo = new WorkOrder
|
var wo = new WorkOrder
|
||||||
{
|
{
|
||||||
Id = 1,
|
Id = 1,
|
||||||
|
LocationId = 10,
|
||||||
InternalWONumber = "SH00001",
|
InternalWONumber = "SH00001",
|
||||||
WorkerOrderNumber = "SH00001",
|
WorkerOrderNumber = "SH00001",
|
||||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
ScheduledDate = DateTime.UtcNow.Date.AddDays(1),
|
ScheduledDate = DateTime.UtcNow.Date.AddDays(1),
|
||||||
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||||
};
|
};
|
||||||
|
context.Locations.Add(new Locations { Id = 10, Name = "Test Site" });
|
||||||
context.workOrders.Add(wo);
|
context.workOrders.Add(wo);
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
|
||||||
175
SeaHavenIndustries.Tests/WorkOrderCompletionFreezeTests.cs
Normal file
175
SeaHavenIndustries.Tests/WorkOrderCompletionFreezeTests.cs
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SeaHaven.DataServices.Implementation;
|
||||||
|
using SeaHaven.Services.Constants;
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
|
using SeaHaven.Services.Helpers;
|
||||||
|
using SeaHaven.Services.Implementation;
|
||||||
|
|
||||||
|
namespace SeaHavenIndustries.Tests;
|
||||||
|
|
||||||
|
public class WorkOrderCompletionFreezeTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task FirstCompletedTransitionCapturesSnapshotAndLaterPatchKeepsIt()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var service = CreateService(context);
|
||||||
|
var location = new Locations
|
||||||
|
{
|
||||||
|
Id = 10,
|
||||||
|
Name = "BK5 Chicago",
|
||||||
|
Address1 = "100 Main St",
|
||||||
|
City = "Chicago",
|
||||||
|
State = "IL",
|
||||||
|
Zip = "60601",
|
||||||
|
PhoneNumber = "312-555-0100",
|
||||||
|
Email = "site@example.com"
|
||||||
|
};
|
||||||
|
var vendor = new Vendor
|
||||||
|
{
|
||||||
|
Id = 20,
|
||||||
|
CompanyName = "Frozen Vendor",
|
||||||
|
ContactName = "Frozen Tech",
|
||||||
|
Address = "200 Vendor Ave",
|
||||||
|
City = "Chicago",
|
||||||
|
State = "IL",
|
||||||
|
Zip = "60602",
|
||||||
|
CompanyPhone = "312-555-0200",
|
||||||
|
Email = "vendor@example.com"
|
||||||
|
};
|
||||||
|
var contact = new Contacts
|
||||||
|
{
|
||||||
|
Id = 30,
|
||||||
|
FirstName = "Frozen",
|
||||||
|
LastName = "POC",
|
||||||
|
PhoneNumber = "312-555-0300"
|
||||||
|
};
|
||||||
|
var dispatch = new Dispatch
|
||||||
|
{
|
||||||
|
Id = 40,
|
||||||
|
WorkOrderId = 1,
|
||||||
|
VendorId = vendor.Id,
|
||||||
|
Vendor = vendor,
|
||||||
|
Status = "Sent"
|
||||||
|
};
|
||||||
|
var workOrder = new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
InternalWONumber = "00000012345",
|
||||||
|
SiteCode = "BK5",
|
||||||
|
LocationId = location.Id,
|
||||||
|
Locations = location,
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
|
ScheduledDate = DateTime.UtcNow.Date.AddDays(1),
|
||||||
|
PrimaryDispatchId = dispatch.Id,
|
||||||
|
PrimaryDispatch = dispatch,
|
||||||
|
PocNotes = "Call first",
|
||||||
|
AdditionalContacts = """[{"name":"Backup","phone":"312-555-0400"}]""",
|
||||||
|
RowVersion = Version()
|
||||||
|
};
|
||||||
|
var workOrderContact = new WorkOrderContacts
|
||||||
|
{
|
||||||
|
Id = 50,
|
||||||
|
WorkorderId = workOrder.Id,
|
||||||
|
WorkOrder = workOrder,
|
||||||
|
ContactId = contact.Id,
|
||||||
|
POC = contact
|
||||||
|
};
|
||||||
|
workOrder.WorkOrderContacts = new[] { workOrderContact };
|
||||||
|
context.AddRange(location, vendor, contact, dispatch, workOrder, workOrderContact);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var completed = await service.PatchFieldAsync(
|
||||||
|
workOrder.Id,
|
||||||
|
Patch(WorkOrderBoardFieldNames.LifecycleStatus, "Complete", workOrder),
|
||||||
|
"actor-1");
|
||||||
|
var persisted = await context.workOrders.SingleAsync();
|
||||||
|
var frozenSiteJson = persisted.FrozenSite;
|
||||||
|
var frozenCompanyJson = persisted.FrozenCompany;
|
||||||
|
var frozenPocJson = persisted.FrozenPoc;
|
||||||
|
|
||||||
|
Assert.Equal(LifecycleStatus.Completed, completed.LifecycleStatus);
|
||||||
|
Assert.Equal("BK5 Chicago", completed.LocationName);
|
||||||
|
Assert.Equal("Frozen Vendor", completed.VendorName);
|
||||||
|
Assert.Equal("Frozen POC", completed.PocName);
|
||||||
|
Assert.Equal("Chicago", completed.FrozenSite?.City);
|
||||||
|
Assert.Equal("Frozen Vendor", completed.FrozenCompany?.Name);
|
||||||
|
Assert.Equal(2, completed.FrozenPoc?.Contacts.Count);
|
||||||
|
|
||||||
|
location.Name = "Changed Site";
|
||||||
|
location.City = "Milwaukee";
|
||||||
|
vendor.CompanyName = "Changed Vendor";
|
||||||
|
contact.FirstName = "Changed";
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var afterFlag = await service.PatchFieldAsync(
|
||||||
|
workOrder.Id,
|
||||||
|
Patch(WorkOrderBoardFieldNames.FlagColor, "#EF4444", persisted),
|
||||||
|
"actor-1");
|
||||||
|
|
||||||
|
Assert.Equal(frozenSiteJson, persisted.FrozenSite);
|
||||||
|
Assert.Equal(frozenCompanyJson, persisted.FrozenCompany);
|
||||||
|
Assert.Equal(frozenPocJson, persisted.FrozenPoc);
|
||||||
|
Assert.Equal("BK5 Chicago", afterFlag.LocationName);
|
||||||
|
Assert.Equal("Frozen Vendor", afterFlag.VendorName);
|
||||||
|
Assert.Equal("Frozen POC", afterFlag.PocName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task OpenWorkOrderProjectionKeepsUsingLiveLocation()
|
||||||
|
{
|
||||||
|
await using var context = CreateContext();
|
||||||
|
var location = new Locations { Id = 10, Name = "Live Site", City = "Chicago" };
|
||||||
|
var workOrder = new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
SiteCode = "BK5",
|
||||||
|
LocationId = location.Id,
|
||||||
|
Locations = location,
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
|
ScheduledDate = DateTime.UtcNow.Date.AddDays(1),
|
||||||
|
FrozenSite = """{"code":"BK5","label":"Frozen Site","city":"Old City"}""",
|
||||||
|
RowVersion = Version()
|
||||||
|
};
|
||||||
|
context.AddRange(location, workOrder);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var raw = await new WorkOrderBoardDataService(context).GetBoardRowByIdAsync(workOrder.Id);
|
||||||
|
var dto = WorkOrderBoardService.MapRawRow(Assert.IsType<SeaHaven.DataServices.Interfaces.WorkOrderBoardRawRow>(raw), DateTime.UtcNow);
|
||||||
|
|
||||||
|
Assert.Equal("Live Site", dto.LocationName);
|
||||||
|
Assert.Null(dto.FrozenSite);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ApplicationDbContext CreateContext()
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||||
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
return new ApplicationDbContext(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorkOrderBoardUpdateService CreateService(ApplicationDbContext context)
|
||||||
|
{
|
||||||
|
var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context));
|
||||||
|
return new WorkOrderBoardUpdateService(
|
||||||
|
new WorkOrderBoardDataService(context),
|
||||||
|
new WorkOrderBoardMutationDataService(context),
|
||||||
|
new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorkOrderBoardPatchRequestDto Patch(
|
||||||
|
string field,
|
||||||
|
string value,
|
||||||
|
WorkOrder workOrder)
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
Field = field,
|
||||||
|
Value = value,
|
||||||
|
WorkOrderVersion = Convert.ToBase64String(workOrder.RowVersion ?? Version())
|
||||||
|
};
|
||||||
|
|
||||||
|
private static byte[] Version() => new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 };
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue