using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Login; using GB5Shared.EntityHandler; using GB5Shared.GB5Exception; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using GB5Shared.Validation; using PayRollDAL.CustomeCode.Punch; using PayRollDAL.DTO.Punch; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using static GB5Shared.GB5Constant.Constant; namespace PayRollBLL.Punch { public class PunchBLL : IPunchBLL { // Client whose replicated devicelog table is named DEVICELOG (singular) instead of // DEVICELOGS - matches the GB4 PunchBLL.MissingPunchInsertNew client-id check. private const int EFL_CLIENT_ID = -1399999812; private readonly IPunchDAL _PunchDAL; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; private readonly IQueryExecutor _QueryExecutor; private readonly BaseEntityAppService _baseEntityAppService; private readonly VoucherNumberService _voucherNumberService; public PunchBLL(IPunchDAL PunchDAL, AutoNumber AutoNumber, IValidation validation, IQueryExecutor IQueryExecutor, BaseEntityAppService baseEntityAppService, VoucherNumberService voucherNumberService) { _PunchDAL = PunchDAL; _AutoNumber = AutoNumber; _Validation = validation; _QueryExecutor = IQueryExecutor; _baseEntityAppService = baseEntityAppService; _voucherNumberService = voucherNumberService; } public async Task SaveMissedPunch(PunchDTO dto, LoginDTO login) { if (dto == null) throw new ArgumentNullException(nameof(dto)); ValidatePunch(dto); bool isNew = dto.PunchId == 0; dto.PunchPunchDate = ConvertToDate(dto.PunchPunchDate); var autoNumber = isNew ? await _AutoNumber.GetNumberAsync(1, "PUNCH", login) : null; var trans = await _QueryExecutor.BeginTransactionAsync(login); try { /* ================= SAVE ================= */ await _baseEntityAppService.ExecuteSaveAsync( EntityConstant.PUNCH, EventTypeConstant.SAVEPUNCHEVENTTYPEID, dto, login, async tx => { if (isNew) { dto.PunchId = autoNumber!.StartNumber; } return isNew ? await _PunchDAL.SaveMissedPunch(dto, login, tx) : await _PunchDAL.UpdateMissedPunch(dto, login, tx); }, new Dictionary(), BIZTRANSACTIONCLASSCONSTANT.PUNCH, BizTransactionConstant.PUNCH, trans, isNewEntity: isNew ); await _QueryExecutor.CommitAsync(trans); return isNew ? $"{dto.EmployeeName} Status is: IN" : $"{dto.EmployeeName} Punch Details updated successfully "; } catch { await _QueryExecutor.RollbackAsync(trans); if (autoNumber != null && dto.PunchId > 0) { await _AutoNumber.RollbackAutoNumber("PUNCH", dto.PunchId, login); } throw; } } private static void ValidatePunch(PunchDTO dto) { if (dto.EmployeeId == 0) throw new ArgumentException("Employee is required"); if (dto.PunchPunchDate == default) throw new ArgumentException("Punch Date is required"); } private static DateOnly ConvertToDate(object date) { if (date == null) return DateOnly.FromDateTime(DateTime.UtcNow); string str = date.ToString()!.Trim(); if (str.StartsWith("/Date(")) { int start = str.IndexOf('(') + 1; int end = str.IndexOfAny(new[] { '+', '-', ')' }, start); long ms = long.Parse(str.Substring(start, end - start)); return DateOnly.FromDateTime(DateTimeOffset.FromUnixTimeMilliseconds(ms).UtcDateTime); } return DateOnly.FromDateTime(Convert.ToDateTime(str)); } #region MissingPunchInsert / MissingPunchInsertNew public async Task MissingPunchInsert(int employeeId, string fromDate, string toDate, LoginDTO login, CancellationToken ct = default) { ValidateDateRange(fromDate, toDate); toDate = NormalizeToDate(toDate); string? employeeCode = employeeId != -1 ? await _PunchDAL.GetEmployeeTemporaryCardNumberAsync(employeeId, login, ct) : null; // When ConnectionDatabaseName is supplied, the punch devicelogs table lives in a // separate download DB (GB4 behaviour — read from Gb4SolutionCommonweb.config). // When it is absent, punches are in the same database as the main application — // reuse the caller's existing LoginDTO directly without building a second connection. string? punchDownloadDbName = login.ConnectionDatabaseName; bool separatePunchDb = !string.IsNullOrEmpty(punchDownloadDbName); var punchDbLogin = separatePunchDb ? new LoginDTO { ClientId = login.ClientId, DatabaseName = punchDownloadDbName, DatabaseType = login.DatabaseType, ServerConfigId = login.ServerConfigId, IsValidationRequired = 1, UserId = login.UserId } : login; var deviceLogs = (await _PunchDAL.GetDeviceLogsFromPunchDbAsync(fromDate, toDate, employeeCode, punchDbLogin, ct)).ToList(); if (deviceLogs.Count == 0) throw new MethodNotAllowedException("There is no record found to insert.."); if (separatePunchDb) { // Phase 1: delete fetched rows from the separate Punch Download DB var punchDbTrans = await _QueryExecutor.BeginTransactionAsync(punchDbLogin); try { foreach (var row in deviceLogs) await _PunchDAL.DeleteDeviceLogFromPunchDbAsync(row.Id, punchDbLogin, punchDbTrans, ct); await _QueryExecutor.CommitAsync(punchDbTrans); } catch { await _QueryExecutor.RollbackAsync(punchDbTrans); throw; } } // Phase 2: write into the main DB (delete old TPUNCH rows, insert device logs) var trans = await _QueryExecutor.BeginTransactionAsync(login); try { await _PunchDAL.DeleteTPunchBeforeInsertingAsync(employeeId, fromDate, toDate, login, trans, ct); foreach (var row in deviceLogs) await _PunchDAL.InsertDeviceLogFromPunchDbAsync(row, login, trans, ct); await _QueryExecutor.CommitAsync(trans); } catch { await _QueryExecutor.RollbackAsync(trans); throw; } return "Missing Punch Insert Success.."; } public async Task MissingPunchInsertNew(int employeeId, string fromDate, string toDate, LoginDTO login, CancellationToken ct = default) { ValidateDateRange(fromDate, toDate); toDate = NormalizeToDate(toDate); string tableName = login.ClientId == EFL_CLIENT_ID ? "DEVICELOG" : "DEVICELOGS"; var connectionNames = await _PunchDAL.FindPunchServerConnectionNamesAsync(tableName, login, ct); if (connectionNames.Count == 0) throw new MethodNotAllowedException("Replicate is not configured for devicelogs table.."); if (connectionNames.Count > 1) throw new MethodNotAllowedException("Replicate is configured more than one for devicelogs table.."); /* ===== Phase 1: pull rows missing locally from the replicated linked-server table ===== */ var trans1 = await _QueryExecutor.BeginTransactionAsync(login); try { await _PunchDAL.InsertMissingDeviceLogsFromLinkedServerAsync( employeeId, connectionNames[0], fromDate, toDate, tableName, login, trans1, ct); await _QueryExecutor.CommitAsync(trans1); } catch { await _QueryExecutor.RollbackAsync(trans1); throw; } /* ===== Phase 2: stage punches still missing from TPUNCH into devicelogs2 ===== */ var trans2 = await _QueryExecutor.BeginTransactionAsync(login); try { await _PunchDAL.StageMissingPunchesToDeviceLogs2Async(employeeId, fromDate, toDate, tableName, login, trans2, ct); await _QueryExecutor.CommitAsync(trans2); } catch { await _QueryExecutor.RollbackAsync(trans2); throw; } return "Missing Punch Insert Success.."; } private static void ValidateDateRange(string fromDate, string toDate) { if (string.IsNullOrEmpty(fromDate)) throw new MethodNotAllowedException("FromDate must be selected.."); if (string.IsNullOrEmpty(toDate)) throw new MethodNotAllowedException("ToDate must be selected.."); } private static string NormalizeToDate(string toDate) { DateTime tempToDate = Convert.ToDateTime(toDate).AddMinutes(1439); return tempToDate.ToString("dd-MMM-yyyy HH:mm"); } #endregion } }