- Case numbers SUP-000001, SUP-000002 … per customer, unique per customer (new index; a migration step renames any old duplicates first). The portal suggests the next number on the finding page and under the new "Ny sag" on the customer page; an empty field gets the next number, a typed one that is taken is refused. Concurrent creates retry. - SupportCase gains Source (Staff/Customer), description, contact name, e-mail, phone, the customer's Windows user and ClientRequestId. - POST/GET /api/v1/cases for the customer's agent (license with priority support): creates a case idempotently on ClientRequestId, links the finding it came from, lists the customer's cases incl. staff-created. Customer/license creation is shared with the findings endpoint. - Optional mail to support for customer cases (MailKit; prefers AUTH LOGIN as Microsoft 365 needs). A failed mail is logged, never fails the call. deploy-hub.yml writes optional SUPPORT_SMTP_* secrets to .env. - Supportrapport: created, closed, open now, average/median resolution time, oldest open case and customer-created share per customer for a period (default 90 days), plus the cases; CSV for Danish Excel. - Cases page: newest first, customer cases marked, contact/description. - Customer page: findings can be filtered to one instance (?instans=id, ignored for another customer's instance); active findings per instance by severity. - dotnet-ef as a local tool (dotnet-tools.json) for the migration. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
35 lines
1.3 KiB
C#
35 lines
1.3 KiB
C#
using System.Globalization;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace SQLInsight.Hub.Support;
|
|
|
|
/// <summary>Case numbers SUP-000001, SUP-000002 … running per customer.</summary>
|
|
public static partial class CaseNumbers
|
|
{
|
|
public const string Prefix = "SUP-";
|
|
|
|
/// <summary>
|
|
/// The number after the highest SUP-<digits> among <paramref name="existing"/>. Other references (typed by hand,
|
|
/// e.g. from an older system) are ignored, so they never block or reset the sequence.
|
|
/// </summary>
|
|
public static string Next(IEnumerable<string> existing)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(existing);
|
|
long highest = 0;
|
|
foreach (var reference in existing)
|
|
{
|
|
var match = Number().Match(reference ?? string.Empty);
|
|
if (match.Success && long.TryParse(match.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out var n) && n > highest)
|
|
{
|
|
highest = n;
|
|
}
|
|
}
|
|
|
|
return Format(highest + 1);
|
|
}
|
|
|
|
public static string Format(long number) => Prefix + number.ToString("D6", CultureInfo.InvariantCulture);
|
|
|
|
[GeneratedRegex(@"^\s*SUP-(\d{1,18})\s*$", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
|
private static partial Regex Number();
|
|
}
|