Files
ScadaBridge/src/ScadaLink.Commons/Interfaces/Repositories/ISiteCallAuditRepository.cs
T

90 lines
4.0 KiB
C#

using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Types;
using ScadaLink.Commons.Types.Audit;
namespace ScadaLink.Commons.Interfaces.Repositories;
/// <summary>
/// Operational-state data access for the central <c>SiteCalls</c> table
/// (Site Call Audit #22, Audit Log #23 M3 Bundle B). One row per
/// <see cref="TrackedOperationId"/>; sites remain the source of truth and this
/// table is an eventually-consistent mirror fed by best-effort gRPC telemetry
/// plus periodic reconciliation pulls.
/// </summary>
/// <remarks>
/// <para>
/// Unlike the partitioned append-only <c>AuditLog</c> (M1), this table holds
/// mutable operational state. <see cref="UpsertAsync"/> is insert-if-not-exists
/// then monotonic update — a status update with rank less than or equal to the
/// stored status is a silent no-op so out-of-order telemetry, duplicate gRPC
/// packets, and reconciliation pulls can all feed the same writer without
/// rolling state backward.
/// </para>
/// <para>
/// Status rank for monotonic comparison (lower wins): <c>Submitted=0,
/// Forwarded=1, Attempted=2, Skipped=2, Delivered=3, Failed=3, Parked=3,
/// Discarded=3</c>. Terminal statuses share rank 3 and are mutually exclusive
/// — an attempt to upsert e.g. <c>Delivered</c> over an existing <c>Parked</c>
/// row is a no-op.
/// </para>
/// </remarks>
public interface ISiteCallAuditRepository
{
/// <summary>
/// Inserts <paramref name="siteCall"/> if no row with the same
/// <see cref="SiteCall.TrackedOperationId"/> exists; otherwise updates the
/// existing row IF AND ONLY IF the incoming status' rank strictly exceeds
/// the stored status' rank. Out-of-order / duplicate updates are silently
/// dropped (monotonic forward-only progression).
/// </summary>
Task UpsertAsync(SiteCall siteCall, CancellationToken ct = default);
/// <summary>
/// Returns the row for the given id, or <c>null</c> if none exists.
/// </summary>
Task<SiteCall?> GetAsync(TrackedOperationId id, CancellationToken ct = default);
/// <summary>
/// Returns up to <see cref="SiteCallPaging.PageSize"/> rows matching
/// <paramref name="filter"/>, ordered by <c>(CreatedAtUtc DESC,
/// TrackedOperationId DESC)</c>. Use keyset paging via
/// <see cref="SiteCallPaging.AfterCreatedAtUtc"/> + <see cref="SiteCallPaging.AfterId"/>
/// to fetch subsequent pages.
/// </summary>
Task<IReadOnlyList<SiteCall>> QueryAsync(
SiteCallQueryFilter filter,
SiteCallPaging paging,
CancellationToken ct = default);
/// <summary>
/// Deletes terminal rows whose <see cref="SiteCall.TerminalAtUtc"/> is
/// strictly older than <paramref name="olderThanUtc"/>. Non-terminal rows
/// (TerminalAtUtc IS NULL) are NEVER purged. Returns the number of rows
/// deleted.
/// </summary>
Task<int> PurgeTerminalAsync(DateTime olderThanUtc, CancellationToken ct = default);
/// <summary>
/// Computes a point-in-time global <see cref="SiteCallKpiSnapshot"/> from the
/// <c>SiteCalls</c> table. Counts are aggregated server-side (no row
/// materialisation): <c>StuckCount</c> uses <paramref name="stuckCutoff"/>;
/// <c>FailedLastInterval</c> / <c>DeliveredLastInterval</c> use
/// <paramref name="intervalSince"/>; the current time for <c>OldestPendingAge</c>
/// is captured inside the method.
/// </summary>
Task<SiteCallKpiSnapshot> ComputeKpisAsync(
DateTime stuckCutoff,
DateTime intervalSince,
CancellationToken ct = default);
/// <summary>
/// Computes a point-in-time <see cref="SiteCallSiteKpiSnapshot"/> per source
/// site. Sites with no <c>SiteCalls</c> rows at all are omitted. The stuck
/// cutoff and interval bounds are interpreted as in <see cref="ComputeKpisAsync"/>.
/// </summary>
Task<IReadOnlyList<SiteCallSiteKpiSnapshot>> ComputePerSiteKpisAsync(
DateTime stuckCutoff,
DateTime intervalSince,
CancellationToken ct = default);
}