using CostingDAL.DTO.Biztransactionkeys; using CostingDAL.Query.Biztransaction; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using System; using System.Collections.Generic; using System.Data.Common; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CostingDAL.CustomCode.VnoGeneration { /// /// GB5 VNO Generation Service. /// Migrated from GB4 VnoGenerationDLL (AdminDAL.CustomeCode.VnoGeneration). /// /// GB4 had 3 methods — GetNextVno, GetNextVnoWithSameSession, GetMultiVno. /// GetNextVno was dead code (immediately delegated to GetNextVnoWithSameSession). /// GB5 consolidates into 2 clean async methods: /// - GetNextVnoAsync → single VNO (was GetNextVnoWithSameSession) /// - GetMultiVnoAsync → N VNOs in one DB hit (was GetMultiVno) /// /// Both always require a DbTransaction — callers (BLL) own the transaction scope. /// public class VnoGenerationService : IVnoGenerationService { private readonly IQueryExecutor _qe; private readonly AutoNumber _autoNumber; public VnoGenerationService(IQueryExecutor qe, AutoNumber autoNumber) { _qe = qe; _autoNumber = autoNumber; } // ════════════════════════════════════════════════════════════════ // GET NEXT VNO — single document number // GB4 parity: GetNextVnoWithSameSession // ════════════════════════════════════════════════════════════════ public async Task GetNextVnoAsync( int bizTransactionTypeId, DateTime vdate, LoginDTO login, DbTransaction tx, CancellationToken ct = default) { var biz = await LoadBizTransactionType(bizTransactionTypeId, login, tx, ct); // GB4 parity: NOGenerationType==1 means manual — throw, do not generate if (biz.NOGenerationType == 1) throw new InvalidOperationException( $"BizTransactionType [{bizTransactionTypeId}] is set to Manual number generation. " + "Auto-generation is not permitted."); var (lastNo, _) = await IncrementKeys( count: 1, bizTransactionTypeId, vdate, biz, login, tx, ct); // lastNo returned is the NEW number assigned to this document return FormatVno(lastNo, vdate, biz); } // ════════════════════════════════════════════════════════════════ // GET MULTI VNO — N sequential numbers, one DB round-trip // GB4 parity: GetMultiVno // ════════════════════════════════════════════════════════════════ public async Task> GetMultiVnoAsync( int count, int bizTransactionTypeId, DateTime vdate, LoginDTO login, DbTransaction tx, CancellationToken ct = default) { if (count <= 0) throw new ArgumentOutOfRangeException(nameof(count), "VNO count must be greater than zero."); var biz = await LoadBizTransactionType(bizTransactionTypeId, login, tx, ct); if (biz.NOGenerationType == 1) throw new InvalidOperationException( $"BizTransactionType [{bizTransactionTypeId}] is set to Manual number generation. " + "Auto-generation is not permitted."); // GB4 parity: GetMultiVno increments LastNo by N in one update, // then loops from (oldLastNo+1) to (oldLastNo+N) to build formatted numbers. var (baseLastNo, _) = await IncrementKeys( count, bizTransactionTypeId, vdate, biz, login, tx, ct); // baseLastNo is the LAST number in the range assigned. // First number = baseLastNo - count + 1. // GB4 parity: loop nextvno from 1 to N, number = lastNoBeforeIncrement + nextvno. var result = new List(count); int firstNo = baseLastNo - count + 1; for (int i = 0; i < count; i++) { result.Add(FormatVno(firstNo + i, vdate, biz)); } return result; } // ════════════════════════════════════════════════════════════════ // CORE: INCREMENT KEYS TABLE // GB4 parity: reads MBIZTRANSACTIONKEYS, updates or inserts, // returns the assigned LastNo value (single) or end-of-range (multi). // ════════════════════════════════════════════════════════════════ private async Task<(int assignedLastNo, int keysId)> IncrementKeys( int count, int bizTransactionTypeId, DateTime vdate, BizTransactionTypeVnoDTO biz, LoginDTO login, DbTransaction tx, CancellationToken ct) { // Build the SELECT query based on period type — same as GB4 switch var (sql, param) = BuildKeysSelectQuery( biz.NOGenerationPeriodType, bizTransactionTypeId, vdate); var existing = await _qe.QuerySingleAsync( login, sql, param); int lastNo; int keysId; if (existing != null) { // ── EXISTING ROW: UPDATE LastNo += count ───────────────── // GB4 parity: GetNextVno does LastNo+1, GetMultiVno does LastNo+count lastNo = existing.LastNo + count; keysId = existing.BIZTransactionKeysId; await _qe.ExecuteAsync( login, BizTransactionQB.UPDATE_BIZTRANSACTIONKEYS_LASTNO, new { LastNo = lastNo, BIZTransactionKeysId = keysId }, tx, ct); // For single VNO: assigned = lastNo (was LastNo+1) // For multi VNO: range is [existing.LastNo+1 .. lastNo] } else { // ── NO ROW YET: INSERT first key row ───────────────────── // GB4 parity: LastNo=1 for single, LastNo=count for multi var auto = await _autoNumber.GetNumberAsync(1, "BIZTRANSACTIONKEYS", login); keysId = auto.StartNumber; lastNo = count; // first number assigned is 1 (single) or count (multi end) var insertParam = BuildKeysInsertParam( keysId, bizTransactionTypeId, biz, vdate, lastNo); await _qe.ExecuteAsync( login, BizTransactionQB.INSERT_BIZTRANSACTIONKEYS, insertParam, tx, ct); } return (lastNo, keysId); } // ════════════════════════════════════════════════════════════════ // FORMAT VNO // GB4 parity: FinalSystemPrefix + Prefix + ZeroPad(lastNo) + Suffix + FinalSystemSuffix // SystemPrefix/SystemSuffix are DateTime format strings ("yy/MM" etc.) // Zero-padding fills to NumberSize total length. // Throws if formatted length exceeds NumberSize. // ════════════════════════════════════════════════════════════════ private static string FormatVno( int lastNo, DateTime vdate, BizTransactionTypeVnoDTO biz) { string finalSystemPrefix = FormatDatePart(vdate, biz.SystemPrefix, "SystemPrefix"); string finalSystemSuffix = FormatDatePart(vdate, biz.SystemSuffix, "SystemSuffix"); string prefix = biz.Prefix ?? ""; string suffix = biz.Suffix ?? ""; string number = lastNo.ToString(); int fixedLength = finalSystemPrefix.Length + prefix.Length + suffix.Length + finalSystemSuffix.Length; int numberLength = number.Length; int totalLength = fixedLength + numberLength; if (totalLength == biz.NumberSize) { // Exact fit — no padding needed return finalSystemPrefix + prefix + number + suffix + finalSystemSuffix; } else if (totalLength < biz.NumberSize) { // Pad the number with leading zeros to fill NumberSize int padCount = biz.NumberSize - fixedLength; string paddedNumber = number.PadLeft(padCount, '0'); return finalSystemPrefix + prefix + paddedNumber + suffix + finalSystemSuffix; } else { // Generated length exceeds configured NumberSize — same exception as GB4 throw new InvalidOperationException( $"Generated VNO exceeds the configured NumberSize [{biz.NumberSize}] for " + $"BizTransactionType. Increase NumberSize or reduce Prefix/Suffix length."); } } // ════════════════════════════════════════════════════════════════ // BUILD SELECT QUERY FOR KEYS TABLE // GB4 parity: 4 period types // 0 = Yearly → Year=-2 (all years, no year filter) // 1 = Monthly → filter by Year + Month // 2 = Daily → filter by Year + Month + Day // 3 = Continuously → Year=-1 (no date filter at all) // ════════════════════════════════════════════════════════════════ private static (string sql, object param) BuildKeysSelectQuery( int periodType, int bizTransactionTypeId, DateTime vdate) { return periodType switch { 0 => ( // Yearly — GB4 Year=-2 sentinel BizTransactionQB.GET_BIZTRANSACTIONKEYS_YEARLY, (object)new { BIZTransactionTypeId = bizTransactionTypeId }), 1 => ( // Monthly BizTransactionQB.GET_BIZTRANSACTIONKEYS_MONTHLY, (object)new { BIZTransactionTypeId = bizTransactionTypeId, Year = vdate.Year, Month = vdate.Month }), 2 => ( // Daily BizTransactionQB.GET_BIZTRANSACTIONKEYS_DAILY, (object)new { BIZTransactionTypeId = bizTransactionTypeId, Year = vdate.Year, Month = vdate.Month, Day = vdate.Day }), 3 => ( // Continuously — no date filter BizTransactionQB.GET_BIZTRANSACTIONKEYS_CONTINUOUS, (object)new { BIZTransactionTypeId = bizTransactionTypeId }), _ => throw new InvalidOperationException( $"Unknown NOGenerationPeriodType [{periodType}].") }; } // ════════════════════════════════════════════════════════════════ // BUILD INSERT PARAM FOR NEW KEYS ROW // GB4 parity: Year/Month/Day sentinel values per period type // Yearly: Year=-2, Month=0, Day=0 // Monthly: Year=actual, Month=actual, Day=0 // Daily: Year=actual, Month=actual, Day=actual // Continuously: Year=-1, Month=0, Day=0 // ════════════════════════════════════════════════════════════════ private static object BuildKeysInsertParam( int keysId, int bizTransactionTypeId, BizTransactionTypeVnoDTO biz, DateTime vdate, int lastNo) { (short year, byte month, byte day) = biz.NOGenerationPeriodType switch { 0 => ((short)-2, (byte)0, (byte)0), 1 => ((short)vdate.Year, (byte)vdate.Month, (byte)0), 2 => ((short)vdate.Year, (byte)vdate.Month, (byte)vdate.Day), 3 => ((short)-1, (byte)0, (byte)0), _ => throw new InvalidOperationException( $"Unknown NOGenerationPeriodType [{biz.NOGenerationPeriodType}].") }; return new { BIZTransactionKeysId = keysId, BIZTransactionTypeId = bizTransactionTypeId, Year = year, Month = month, Day = day, Prefix = biz.Prefix ?? "", Suffix = biz.Suffix ?? "", SystemPrefix = biz.SystemPrefix ?? "", LastNo = lastNo // NOTE: SystemSuffix is NOT stored in MBIZTRANSACTIONKEYS table. // It is read from MBIZTRANSACTIONTYPE and used only in FormatVno(). }; } // ════════════════════════════════════════════════════════════════ // LOAD BIZ TRANSACTION TYPE // GB4 parity: throws on not-found or duplicate — same messages. // ════════════════════════════════════════════════════════════════ private async Task LoadBizTransactionType( int bizTransactionTypeId, LoginDTO login, DbTransaction tx, CancellationToken ct) { var result = await _qe.QuerySingleAsync( login, BizTransactionQB.GET_BIZTRANSACTIONTYPE_FOR_VNO, new { BIZTransactionTypeId = bizTransactionTypeId }); if (result == null) throw new KeyNotFoundException( $"BizTransactionType not found for Id [{bizTransactionTypeId}]. " + "Contact Administrator."); return result; } // ════════════════════════════════════════════════════════════════ // FORMAT DATE PART (SystemPrefix / SystemSuffix) // GB4 parity: applies Vdate.ToString(format, en-US culture). // Empty/null → returns "". // Invalid format → throws with same message as GB4. // ════════════════════════════════════════════════════════════════ private static string FormatDatePart(DateTime vdate, string? format, string fieldName) { if (string.IsNullOrEmpty(format)) return ""; try { return vdate.ToString(format, CultureInfo.GetCultureInfo("en-US")); } catch (Exception ex) { throw new InvalidOperationException( $"Please check {fieldName} — format is not correct. " + $"Check BizTransactionType entry. Format: [{format}].", ex); } } } }