using System;
using System.Threading;
using System.Threading.Tasks;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.QueryExecutor;
namespace GB5Shared.GenerateAutoNumber
{
///
/// Single shared service for all document number generation in GB5.
/// Covers standard documents (MMHead, Indent, etc.) and task-style entities.
///
/// Wraps VoucherNumberService (atomic MBIZTRANSACTIONKEYS counter) and adds
/// the document-type-specific rules on top.
///
/// NOGenerationType (MBIZTRANSACTIONTYPE.NOGENERATIONTYPE):
/// 0 = Auto — atomically increments MBIZTRANSACTIONKEYS counter
/// 1 = Manual — validates user-supplied number length ≤ NumberSize
/// 2 = Contract — formats as ContractDocumentNumber/RevisionNumber
///
/// Registration: services.AddScoped<DocumentNumberService>()
///
public class DocumentNumberService
{
private readonly VoucherNumberService _voucherNumberService;
private readonly IQueryExecutor _queryExecutor;
public DocumentNumberService(VoucherNumberService voucherNumberService, IQueryExecutor queryExecutor)
{
_voucherNumberService = voucherNumberService;
_queryExecutor = queryExecutor;
}
// ── Standard document number (MMHead, Indent, etc.) ───────────────────────────
///
/// Returns the document number to stamp on a new head document.
/// Only call for new documents (MMHeadId == 0); updates keep their existing number.
///
/// BizTransactionType PK — for counter lookup.
/// Document date — for period bucketing and date-format tokens.
/// User-supplied number (kept for Manual; ignored for Auto/Contract).
/// Contract reference number (NOGenerationType==2 only).
/// Revision identifier (NOGenerationType==2 only).
/// 0=Auto, 1=Manual, 2=Contract — MBIZTRANSACTIONTYPE.NOGENERATIONTYPE.
/// Max allowed length for manual validation — MBIZTRANSACTIONTYPE.NUMBERSIZE.
/// Current user session.
/// Cancellation token.
public async Task GenerateDocumentNumberAsync(
int bizTransactionTypeId,
DateOnly documentDate,
string? currentNumber,
string? contractDocumentNumber,
string? revisionNumber,
byte noGenerationType,
short numberSize,
LoginDTO login,
CancellationToken ct = default)
{
switch (noGenerationType)
{
case 0: // Auto — VoucherNumberService manages the MBIZTRANSACTIONKEYS counter
var generated = await _voucherNumberService
.GetNextVoucherNumberAsync(bizTransactionTypeId, documentDate, login, null, ct)
.ConfigureAwait(false);
// VoucherNumberService returns null only for NOGenerationType==1; cannot happen here.
return generated ?? throw new InvalidOperationException(
$"Auto-generation returned null for BizTransactionTypeId={bizTransactionTypeId}. Contact Administrator.");
case 1: // Manual — user-supplied number; validate presence and length
if (string.IsNullOrWhiteSpace(currentNumber))
throw new InvalidOperationException(
$"Document number is required for BizTransactionTypeId={bizTransactionTypeId} " +
"(manual number generation is configured).");
if (numberSize > 0 && currentNumber.Length > numberSize)
throw new InvalidOperationException(
$"Document number '{currentNumber}' (length {currentNumber.Length}) exceeds " +
$"the allowed size ({numberSize}) defined in BizTransactionType settings.");
return currentNumber;
case 2: // Contract — derive number from the linked contract reference
if (string.IsNullOrEmpty(contractDocumentNumber))
return currentNumber ?? string.Empty;
return string.IsNullOrEmpty(revisionNumber)
? contractDocumentNumber
: $"{contractDocumentNumber}/{revisionNumber}";
default:
return currentNumber ?? string.Empty;
}
}
// ── Task-style number (uniqueness loop + type-letter prefix) ─────────────────
///
/// Generates the next unique task number, or returns null when
/// NOGenerationType == 1 (Manual) — the caller keeps the user-supplied number.
///
/// Runs outside any transaction — LASTNO in MBIZTRANSACTIONKEYS always advances
/// in auto-commit regardless of the caller's outer transaction outcome.
///
/// The retry loop skips numbers already present in the target table — a self-heal
/// for orphaned rows from an earlier bug. In normal operation one iteration executes.
/// Mirrors GB4: Task.Number = DetailTypeLetter + GetNextVnoWithSameSession(...)
///
/// BizTransactionType PK.
/// Task date for period bucketing and date-format tokens.
///
/// Controls the number prefix letter:
/// 0=MileStone→M, 1=RollUp→R, 2=Heading→H, 3=Work→W, default→T
///
///
/// SQL returning COUNT(1) when @TaskNumber already exists in the caller's table.
/// Passed by the caller so this service has no dependency on table-specific queries.
/// Example: "SELECT COUNT(1) FROM TTASK WHERE TASKNUMBER = @TaskNumber"
///
/// Current user session.
/// Cancellation token.
public async Task GenerateUniqueTaskNumberAsync(
int bizTransactionTypeId,
DateOnly taskDate,
byte detailType,
string checkExistsSql,
LoginDTO login,
CancellationToken ct = default)
{
for (int attempt = 0; attempt < 20; attempt++)
{
string? vno = await _voucherNumberService.GetNextVoucherNumberAsync(
bizTransactionTypeId,
taskDate,
login,
transaction: null, // auto-commit — LASTNO never rolled back
ct: ct,
prefixOverride: string.Empty);
if (vno == null)
return null; // Manual mode — caller keeps user-supplied number
string candidate = BuildTaskNumber(detailType, vno);
int existing = await _queryExecutor.ExecuteScalarAsync(
login,
checkExistsSql,
new { TaskNumber = candidate });
if (existing == 0)
return candidate; // Number is free — use it
// Number already taken by an orphaned row — advance LASTNO and retry
}
throw new InvalidOperationException(
$"Could not generate a unique task number for BizTransactionTypeId=" +
$"{bizTransactionTypeId} after 20 attempts. Contact Administrator.");
}
// ── Private helpers ───────────────────────────────────────────────────────────
///
/// Prepends the DetailType prefix letter to the VNO string.
/// DetailType: 0=MileStone→M, 1=RollUp→R, 2=Heading→H, 3=Work→W, default→T
///
private static string BuildTaskNumber(byte detailType, string vno)
{
string prefix = detailType switch
{
0 => "M",
1 => "R",
2 => "H",
3 => "W",
_ => "T"
};
return prefix + vno;
}
}
}