using Microsoft.EntityFrameworkCore; using SQLInsight.Contracts; using SQLInsight.Hub.Data; namespace SQLInsight.Hub.Support; /// Creates support cases with a case number that is unique per customer. public static class SupportCaseStore { private const int Attempts = 5; /// The number the next case for the customer gets unless another is typed: SUP-000001, SUP-000002 … public static async Task NextReferenceAsync(HubDbContext db, int customerId, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(db); var references = await db.SupportCases.Where(c => c.CustomerId == customerId).Select(c => c.Reference).ToListAsync(cancellationToken).ConfigureAwait(false); return CaseNumbers.Next(references); } /// /// Adds and saves it. An empty gets the next free number; /// if someone else took it meanwhile, the next one is tried. A typed reference that already exists for the customer /// is refused. Inside a transaction EF Core rolls back to a savepoint on failure, so retrying is safe there too. /// /// The saved case, or the reason it was not saved. public static async Task<(SupportCase? Case, string? Error)> AddAsync(HubDbContext db, SupportCase draft, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(db); ArgumentNullException.ThrowIfNull(draft); draft.Reference = draft.Reference?.Trim() ?? string.Empty; var typed = draft.Reference.Length > 0; if (typed && draft.Reference.Length > 32) { return (null, "Sagsnummeret må højst være 32 tegn."); } for (var attempt = 0; attempt < Attempts; attempt++) { if (!typed) { draft.Reference = await NextReferenceAsync(db, draft.CustomerId, cancellationToken).ConfigureAwait(false); } else if (await db.SupportCases.AnyAsync(c => c.CustomerId == draft.CustomerId && c.Reference == draft.Reference, cancellationToken).ConfigureAwait(false)) { return (null, $"Sagsnummeret {draft.Reference} findes allerede for kunden."); } db.SupportCases.Add(draft); try { await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); return (draft, null); } catch (DbUpdateException ex) when (ex.InnerException is Npgsql.PostgresException { SqlState: Npgsql.PostgresErrorCodes.UniqueViolation }) { db.Entry(draft).State = EntityState.Detached; if (typed) { return (null, $"Sagsnummeret {draft.Reference} findes allerede for kunden."); } } } return (null, "Der kunne ikke tildeles et sagsnummer. Prøv igen."); } public static SupportCaseDto ToDto(SupportCase c) { ArgumentNullException.ThrowIfNull(c); return new SupportCaseDto(c.Reference, c.Title, c.IsOpen, Utc(c.CreatedAt), c.ClosedAt is { } closed ? Utc(closed) : null, c.Source, c.ClientRequestId); } private static DateTimeOffset Utc(DateTime value) => new(DateTime.SpecifyKind(value, DateTimeKind.Utc)); }