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);
}
}
}