using Dapper; namespace SQLInsight.Repository; /// Values of insight.SupportCases.Status. public static class SupportCaseStatus { /// Filed in SQLInsight Web; the agent has not had a case number back from the hub yet. public const string Pending = "Pending"; public const string Open = "Open"; public const string Closed = "Closed"; /// The hub refused the request (e.g. no support in the license); see LastError. public const string Failed = "Failed"; } public sealed record SupportCaseRow { public long CaseId { get; init; } public Guid? ClientRequestId { get; init; } public string? Reference { get; init; } public string Title { get; init; } = string.Empty; public string? Description { get; init; } public string? ContactName { get; init; } public string? ContactEmail { get; init; } public string? ContactPhone { get; init; } public long? FindingId { get; init; } public string? InstanceName { get; init; } public string? RuleId { get; init; } public string? Subject { get; init; } public string? RequestedBy { get; init; } public string Source { get; init; } = "Customer"; public string Status { get; init; } = SupportCaseStatus.Pending; public string? LastError { get; init; } public int Attempts { get; init; } public DateTime CreatedAt { get; init; } public DateTime? SentAt { get; init; } public DateTime? ClosedAt { get; init; } } /// A request filed in SQLInsight Web. public sealed record NewSupportRequest( Guid ClientRequestId, string Title, string? Description, string ContactName, string ContactEmail, string? ContactPhone, string? RequestedBy, long? FindingId = null, string? InstanceName = null, string? RuleId = null, string? Subject = null); /// A case as the hub reports it. public sealed record HubSupportCase(string Reference, string Title, bool IsOpen, DateTime CreatedAt, DateTime? ClosedAt, string Source, Guid? ClientRequestId); public sealed partial class InsightRepository { /// Files a support request (SQLInsight Web: needs only INSERT on this table, see 0005_support_cases.sql). public async Task AddSupportRequestAsync(NewSupportRequest request, DateTimeOffset now, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); await using var connection = await OpenAsync(cancellationToken).ConfigureAwait(false); return await connection.ExecuteScalarAsync(""" INSERT insight.SupportCases (ClientRequestId, Title, Description, ContactName, ContactEmail, ContactPhone, RequestedBy, FindingId, InstanceName, RuleId, Subject, Source, Status, NextAttemptAt, CreatedAt, UpdatedAt) OUTPUT INSERTED.CaseId VALUES (@ClientRequestId, @Title, @Description, @ContactName, @ContactEmail, @ContactPhone, @RequestedBy, @FindingId, @InstanceName, @RuleId, @Subject, 'Customer', 'Pending', @now, @now, @now); """, new { request.ClientRequestId, request.Title, request.Description, request.ContactName, request.ContactEmail, request.ContactPhone, request.RequestedBy, request.FindingId, request.InstanceName, request.RuleId, request.Subject, now = Utc(now), }).ConfigureAwait(false); } /// The customer's support cases, newest first. public async Task> GetSupportCasesAsync(int max = 200, CancellationToken cancellationToken = default) { await using var connection = await OpenAsync(cancellationToken).ConfigureAwait(false); var rows = await connection.QueryAsync( "SELECT TOP (@max) * FROM insight.SupportCases ORDER BY CreatedAt DESC, CaseId DESC;", new { max }).ConfigureAwait(false); return rows.ToList(); } public async Task> GetDueSupportRequestsAsync(DateTimeOffset now, int max = 10, CancellationToken cancellationToken = default) { await using var connection = await OpenAsync(cancellationToken).ConfigureAwait(false); var rows = await connection.QueryAsync(""" SELECT TOP (@max) * FROM insight.SupportCases WHERE Status = 'Pending' AND ClientRequestId IS NOT NULL AND NextAttemptAt <= @now ORDER BY NextAttemptAt; """, new { max, now = Utc(now) }).ConfigureAwait(false); return rows.ToList(); } /// The hub created (or already had) the case: store its number. public async Task MarkSupportRequestSentAsync(long caseId, string reference, bool isOpen, DateTime? closedAt, DateTimeOffset now, CancellationToken cancellationToken = default) { await using var connection = await OpenAsync(cancellationToken).ConfigureAwait(false); await connection.ExecuteAsync(""" UPDATE insight.SupportCases SET Reference = @reference, Status = CASE WHEN @isOpen = 1 THEN 'Open' ELSE 'Closed' END, ClosedAt = @closedAt, SentAt = @now, Attempts = Attempts + 1, LastError = NULL, UpdatedAt = @now WHERE CaseId = @caseId; """, new { caseId, reference, isOpen, closedAt, now = Utc(now) }).ConfigureAwait(false); } /// A failed attempt. With null the request is given up (Status Failed). public async Task MarkSupportRequestFailedAsync(long caseId, string error, DateTimeOffset now, DateTimeOffset? nextAttemptAt, CancellationToken cancellationToken = default) { await using var connection = await OpenAsync(cancellationToken).ConfigureAwait(false); await connection.ExecuteAsync(""" UPDATE insight.SupportCases SET Attempts = Attempts + 1, LastError = LEFT(@error, 1000), UpdatedAt = @now, NextAttemptAt = COALESCE(@next, NextAttemptAt), Status = CASE WHEN @next IS NULL THEN 'Failed' ELSE Status END WHERE CaseId = @caseId; """, new { caseId, error, now = Utc(now), next = nextAttemptAt is { } n ? Utc(n) : (DateTime?)null }).ConfigureAwait(false); } /// /// Brings the local list in step with the hub: status and number of the customer's own requests, and the cases /// SEOSoft created for the customer. /// public async Task SyncSupportCasesAsync(IEnumerable cases, DateTimeOffset now, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(cases); await using var connection = await OpenAsync(cancellationToken).ConfigureAwait(false); foreach (var c in cases) { await connection.ExecuteAsync(""" UPDATE insight.SupportCases SET Reference = @Reference, Status = CASE WHEN @IsOpen = 1 THEN 'Open' ELSE 'Closed' END, ClosedAt = @ClosedAt, Title = CASE WHEN ClientRequestId IS NULL THEN @Title ELSE Title END, LastError = NULL, UpdatedAt = @now WHERE (@ClientRequestId IS NOT NULL AND ClientRequestId = @ClientRequestId) OR Reference = @Reference; IF @@ROWCOUNT = 0 INSERT insight.SupportCases (ClientRequestId, Reference, Title, Source, Status, CreatedAt, ClosedAt, SentAt, UpdatedAt) VALUES (@ClientRequestId, @Reference, @Title, @Source, CASE WHEN @IsOpen = 1 THEN 'Open' ELSE 'Closed' END, @CreatedAt, @ClosedAt, @now, @now); """, new { c.Reference, c.Title, c.IsOpen, c.CreatedAt, c.ClosedAt, c.Source, c.ClientRequestId, now = Utc(now) }).ConfigureAwait(false); } } }