Initial import into Gitea

This commit is contained in:
Grae Jones
2026-03-14 13:50:09 -07:00
parent 8e7e03702e
commit 34c1f09e01
154 changed files with 17666 additions and 1548 deletions

View File

@@ -0,0 +1,64 @@
namespace Registration.Data;
/// <summary>
/// Abstraction over registration data.
/// MockDataService for development, SqlDataService when DB is connected.
/// </summary>
public interface IRegistrationDataService
{
Task<RegistrationListResult> GetPendingAsync(CancellationToken ct = default);
Task<Applicant?> GetByIdAsync(string registrationId, CancellationToken ct = default);
Task<RegistrationResult> RegisterAsync(RegisterRequest request, CancellationToken ct = default);
Task<RegistrationResult> RejectAsync(string registrationId, string? reason, string? rejectedBy, CancellationToken ct = default);
Task<RegistrationResult> CompleteAsync(string registrationId, string? platformClientId, CancellationToken ct = default);
}
// ── Models ──
public sealed class Applicant
{
public string RegistrationId { get; set; } = "";
public string BusinessName { get; set; } = "";
public string? WebsiteUrl { get; set; }
public string? BusinessCategory { get; set; }
public string? BusinessDescription { get; set; }
public string? ContactName { get; set; }
public string? ContactEmail { get; set; }
public string? ContactPhone { get; set; }
public string? EntraSubjectId { get; set; }
public string? ClientCategory { get; set; } // General | Franchisee | Franchisor
public string Status { get; set; } = "Pending"; // Pending, Approved, Rejected
public bool PaymentVerified { get; set; }
public DateTime RegisteredUtc { get; set; }
public DateTime? ReviewedUtc { get; set; }
public string? ReviewedBy { get; set; }
public string? RejectionReason { get; set; }
public string? PlatformClientId { get; set; } // Set after approval
}
public sealed class RegisterRequest
{
public string? BusinessName { get; set; }
public string? WebsiteUrl { get; set; }
public string? BusinessCategory { get; set; }
public string? BusinessDescription { get; set; }
public string? ContactName { get; set; }
public string? ContactEmail { get; set; }
public string? ContactPhone { get; set; }
public string? EntraSubjectId { get; set; }
public string? ClientCategory { get; set; } // General | Franchisee | Franchisor
}
public sealed class RegistrationListResult
{
public bool Ok { get; set; }
public List<Applicant> Applicants { get; set; } = new();
public int TotalCount { get; set; }
}
public sealed class RegistrationResult
{
public bool Ok { get; set; }
public string? Error { get; set; }
public string? RegistrationId { get; set; }
}

View File

@@ -0,0 +1,74 @@
using System.Text.Json;
using Microsoft.Extensions.Logging;
namespace Registration.Data;
/// <summary>
/// Real data service backed by dbRegistration.
/// Calls dbo.spRegistration with the standard @action/@rqst/@resp pattern.
///
/// Activate by swapping DI registration in Program.cs:
/// services.AddSingleton&lt;IRegistrationDataService, SqlDataService&gt;();
/// </summary>
public class SqlDataService : IRegistrationDataService
{
private readonly SqlService _sql;
private readonly ILogger<SqlDataService> _log;
private const string Proc = "dbo.spRegistration";
public SqlDataService(SqlService sql, ILogger<SqlDataService> log)
{
_sql = sql;
_log = log;
}
public async Task<RegistrationListResult> GetPendingAsync(CancellationToken ct)
{
var resp = await _sql.ExecProcAsync(Proc, "pending", "{}", ct: ct);
return JsonSerializer.Deserialize<RegistrationListResult>(resp, JsonOpts)
?? new() { Ok = false };
}
public async Task<Applicant?> GetByIdAsync(string registrationId, CancellationToken ct)
{
var rqst = JsonSerializer.Serialize(new { registrationId });
var resp = await _sql.ExecProcAsync(Proc, "get", rqst, ct: ct);
using var doc = JsonDocument.Parse(resp);
if (doc.RootElement.TryGetProperty("applicant", out var app))
return JsonSerializer.Deserialize<Applicant>(app.GetRawText(), JsonOpts);
return null;
}
public async Task<RegistrationResult> RegisterAsync(RegisterRequest request, CancellationToken ct)
{
var rqst = JsonSerializer.Serialize(request, JsonOpts);
var resp = await _sql.ExecProcAsync(Proc, "register", rqst, ct: ct);
return JsonSerializer.Deserialize<RegistrationResult>(resp, JsonOpts)
?? new() { Ok = false, Error = "Deserialization failed" };
}
public async Task<RegistrationResult> RejectAsync(string registrationId, string? reason, string? rejectedBy, CancellationToken ct)
{
var rqst = JsonSerializer.Serialize(new { registrationId, reason, rejectedBy });
var resp = await _sql.ExecProcAsync(Proc, "reject", rqst, ct: ct);
return JsonSerializer.Deserialize<RegistrationResult>(resp, JsonOpts)
?? new() { Ok = false, Error = "Deserialization failed" };
}
public async Task<RegistrationResult> CompleteAsync(string registrationId, string? platformClientId, CancellationToken ct)
{
var rqst = JsonSerializer.Serialize(new { registrationId, platformClientId });
var resp = await _sql.ExecProcAsync(Proc, "complete", rqst, ct: ct);
return JsonSerializer.Deserialize<RegistrationResult>(resp, JsonOpts)
?? new() { Ok = false, Error = "Deserialization failed" };
}
private static readonly JsonSerializerOptions JsonOpts = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};
}

View File

@@ -0,0 +1,82 @@
using System.Data;
using System.Diagnostics;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Registration.Data;
/// <summary>
/// Stored procedure executor — same @action/@rqst/@resp OUTPUT pattern
/// used by Gateway and Management.
///
/// Uncomment registration in Program.cs when dbRegistration is ready.
/// Connection string: ConnectionStrings__Sql (env var) or ConnectionStrings:Sql (appsettings).
/// </summary>
public class SqlService
{
private readonly IConfiguration _config;
private readonly ILogger<SqlService> _logger;
public SqlService(IConfiguration config, ILogger<SqlService> logger)
{
_config = config;
_logger = logger;
}
private string GetConnectionString()
{
var cs = _config.GetConnectionString("Sql");
if (string.IsNullOrWhiteSpace(cs))
throw new InvalidOperationException("Missing ConnectionStrings:Sql");
return cs;
}
public async Task<string> ExecProcAsync(
string procName,
string action,
string rqstJson,
int commandTimeoutSeconds = 30,
CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(procName))
throw new ArgumentException("procName is required.", nameof(procName));
if (string.IsNullOrWhiteSpace(rqstJson))
rqstJson = "{}";
var sw = Stopwatch.StartNew();
try
{
await using var conn = new SqlConnection(GetConnectionString());
await conn.OpenAsync(ct);
await using var cmd = new SqlCommand(procName, conn)
{
CommandType = CommandType.StoredProcedure,
CommandTimeout = commandTimeoutSeconds
};
cmd.Parameters.Add(new SqlParameter("@action", SqlDbType.VarChar, 50) { Value = action });
cmd.Parameters.Add(new SqlParameter("@rqst", SqlDbType.NVarChar, -1) { Value = rqstJson });
var pResp = new SqlParameter("@resp", SqlDbType.NVarChar, -1) { Direction = ParameterDirection.Output };
cmd.Parameters.Add(pResp);
await cmd.ExecuteNonQueryAsync(ct);
var resp = pResp.Value as string ?? "";
sw.Stop();
_logger.LogInformation("SQL ok: {Proc}.{Action} ms={Ms}", procName, action, sw.ElapsedMilliseconds);
return resp;
}
catch (Exception ex)
{
sw.Stop();
_logger.LogError(ex, "SQL error: {Proc}.{Action} ms={Ms}", procName, action, sw.ElapsedMilliseconds);
throw;
}
}
}