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