using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit; using ZB.MOM.WW.ScadaBridge.Commons.Types; using Timestamp = Google.Protobuf.WellKnownTypes.Timestamp; namespace ZB.MOM.WW.ScadaBridge.Communication.Grpc; /// /// Canonical bridge for Site Call Audit operational rows between the /// wire-format exchanged on the /// CachedCallTelemetry packet and the in-process /// persistence entity central writes into the SiteCalls table. /// /// /// /// This mapper lives in ZB.MOM.WW.ScadaBridge.Communication (which owns the generated /// and references Commons for /// ) so both SiteStreamGrpcServer and /// ZB.MOM.WW.ScadaBridge.AuditLog can share one implementation without the /// project-reference cycle that would result from hosting it in /// ZB.MOM.WW.ScadaBridge.AuditLog (AuditLog → Communication, never the reverse). /// Mirrors the sibling . /// /// /// Two directions are provided. rehydrates the central /// entity central writes into the SiteCalls table. /// projects a site-local /// onto the wire — used by the Site Call Audit PullSiteCalls /// reconciliation handler (the central→site self-heal pull). The /// entity itself is never mapped back onto the wire: /// sites emit operational state from , never /// from the central , so a SiteCall→DTO method /// would be dead code. /// /// /// String nullability convention: proto3 scalar strings cannot be absent, so the /// optional rehydrates from an empty string back /// to null. The optional HttpStatus and TerminalAtUtc use proto /// wrappers so they preserve true null semantics. /// /// public static class SiteCallDtoMapper { /// /// Reconstructs a persistence entity from its /// wire-format DTO. An empty LastError rehydrates as null; absent /// HttpStatus/TerminalAtUtc wrappers stay null. /// /// /// is stamped here as a placeholder /// (); the central ingest actor overwrites it /// inside the dual-write transaction so the AuditLog and SiteCalls rows /// share one instant. The value sent on the wire is informational only. /// /// The wire-format site call DTO to map. /// A entity populated from the DTO fields. public static SiteCall FromDto(SiteCallOperationalDto dto) { ArgumentNullException.ThrowIfNull(dto); return new SiteCall { TrackedOperationId = TrackedOperationId.Parse(dto.TrackedOperationId), Channel = dto.Channel, Target = dto.Target, SourceSite = dto.SourceSite, SourceNode = string.IsNullOrEmpty(dto.SourceNode) ? null : dto.SourceNode, Status = dto.Status, RetryCount = dto.RetryCount, LastError = string.IsNullOrEmpty(dto.LastError) ? null : dto.LastError, HttpStatus = dto.HttpStatus, CreatedAtUtc = DateTime.SpecifyKind(dto.CreatedAtUtc.ToDateTime(), DateTimeKind.Utc), UpdatedAtUtc = DateTime.SpecifyKind(dto.UpdatedAtUtc.ToDateTime(), DateTimeKind.Utc), TerminalAtUtc = dto.TerminalAtUtc is null ? null : DateTime.SpecifyKind(dto.TerminalAtUtc.ToDateTime(), DateTimeKind.Utc), IngestedAtUtc = DateTime.UtcNow, // overwritten by AuditLogIngestActor }; } /// /// Projects a site-local onto its /// wire-format DTO for the Site Call Audit PullSiteCalls /// reconciliation RPC. The inverse of ; null /// / /// collapse to empty strings (proto3 scalar strings cannot be absent), while /// the nullable HttpStatus and TerminalAtUtc stay unset on the /// wire so true-null semantics survive the round-trip back through /// . /// /// The site-local operational state to project to wire format. /// A populated ready for transmission. public static SiteCallOperationalDto ToDto(SiteCallOperational operational) { ArgumentNullException.ThrowIfNull(operational); var dto = new SiteCallOperationalDto { TrackedOperationId = operational.TrackedOperationId.ToString(), Channel = operational.Channel, Target = operational.Target, SourceSite = operational.SourceSite, SourceNode = operational.SourceNode ?? string.Empty, Status = operational.Status, RetryCount = operational.RetryCount, LastError = operational.LastError ?? string.Empty, CreatedAtUtc = Timestamp.FromDateTime(EnsureUtc(operational.CreatedAtUtc)), UpdatedAtUtc = Timestamp.FromDateTime(EnsureUtc(operational.UpdatedAtUtc)), }; if (operational.HttpStatus.HasValue) { dto.HttpStatus = operational.HttpStatus.Value; } if (operational.TerminalAtUtc.HasValue) { dto.TerminalAtUtc = Timestamp.FromDateTime(EnsureUtc(operational.TerminalAtUtc.Value)); } return dto; } // All ScadaBridge timestamps are UTC by invariant; Timestamp.FromDateTime // requires UTC kind. Specify (never convert) so a row read back from SQLite // with Kind=Utc passes through and a defensively-unspecified value is // treated as the UTC it already is. Mirrors AuditEventDtoMapper.EnsureUtc. private static DateTime EnsureUtc(DateTime value) => value.Kind == DateTimeKind.Utc ? value : DateTime.SpecifyKind(value, DateTimeKind.Utc); }