using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using GB5Shared.Validation; using SkillManagementDAL.CustomCode.Skill; using SkillManagementDAL.DTO.Skill; using SkillManagementDAL.DTO.SkillGroup; using System.Linq; using System.Text.Json; namespace SkillManagementBLL.Skill { public class SkillBLL : ISkillBLL { private readonly ISkillDAL _dal; private readonly AutoNumber _autoNumber; private readonly IValidation _validation; public SkillBLL(ISkillDAL dal, AutoNumber autoNumber, IValidation validation) { _dal = dal; _autoNumber = autoNumber; _validation = validation; } public async Task GetList(int domainId, int groupId, LoginDTO login, CancellationToken ct) => await _dal.GetList(domainId, groupId, login, ct); public async Task GetById(int skillId, LoginDTO login, CancellationToken ct) => await _dal.GetById(skillId, login, ct); public async Task GetSelectList(CriteriaDTO criteriaDTO, LoginDTO login, CancellationToken ct) => await _dal.GetSelectList(criteriaDTO, login, ct); public async Task Save(SkillDTO dto, LoginDTO login, CancellationToken ct) { if (!string.IsNullOrEmpty(dto.SkillScopeText)) { dto.SkillScope = dto.SkillScopeText switch { "General" => 0, "ProductFamily" => 1, "Model" => 2, _ => throw new Exception("Invalid SkillScope") }; } await _validation.NotEmpty(dto.SkillCode, nameof(dto.SkillCode)); await _validation.NotEmpty(dto.SkillName, nameof(dto.SkillName)); if (dto.DomainId == 0) throw new ArgumentException("Invalid DomainId."); if (dto.GroupId == 0) throw new ArgumentException("Invalid GroupId."); if (dto.SkillScope < 0 || dto.SkillScope > 2) throw new ArgumentException("SkillScope must be 0, 1, or 2."); if (dto.LevelCount < 1 || dto.LevelCount > 4) throw new ArgumentException("LevelCount must be between 1 and 4."); if (dto.Levels == null || dto.Levels.Count == 0) throw new ArgumentException("At least one level is required."); var expected = Enumerable.Range(1, dto.LevelCount); var actual = dto.Levels.Select(x => (int)x.SlNo).OrderBy(x => x); if (dto.Levels.GroupBy(x => x.SlNo).Any(g => g.Count() > 1)) throw new ArgumentException("Duplicate SLNO values are not allowed."); var domainExists = await _dal.DomainExists(dto.DomainId, login); if (!domainExists) throw new Exception("Invalid DomainId"); var groupExists = await _dal.GroupExists(dto.GroupId, login); if (!groupExists) throw new Exception("Invalid GroupId"); var isMatch = await _dal.IsGroupInDomain(dto.GroupId, dto.DomainId, login); if (!isMatch) throw new Exception("Group does not belong to the given Domain"); var now = DateTime.UtcNow; dto.TenantId = login.ClientId; dto.ModifiedById = login.UserId; dto.ModifiedOn = now; bool isNew = dto.SkillId == 0; GB5Shared.DTO.Framework.AutoNumber.AutoNumberDTO? an = null; try { if (isNew) { // 1 ID for the skill itself an = await _autoNumber.GetAutoNumber(1, "MSKILL", login); if (an == null) throw new Exception("AutoNumber generation failed."); dto.SkillId = an.StartNumber; dto.CreatedById = login.UserId; dto.CreatedOn = now; } // Generate new SLevelId for any level that doesn't have one yet // (SLevelId == 0 means new — applies to both new and existing skills) foreach (var level in dto.Levels.Where(l => l.SLevelId == 0)) { var levelAn = await _autoNumber.GetAutoNumber(1, "MSKILLLEVEL", login); level.SLevelId = levelAn.StartNumber; } if (dto.SkillId == 0) throw new Exception("SkillId cannot be 0."); if (dto.Levels.Any(x => x.SLevelId == 0)) throw new Exception("LevelId cannot be 0."); await _dal.Save(dto, login, ct, isNew); return isNew ? string.Format(SuccessResponse.SaveSuccessWithCodeAndName, dto.SkillCode, dto.SkillName) : string.Format(SuccessResponse.UpdateSuccessWithCodeAndName, dto.SkillCode, dto.SkillName); } catch { // Only rollback if AutoNumber was allocated AND DAL save was actually attempted // If exception happened before DAL (logic checks), no rollback needed // because the number was never attempted to be written to DB // BUT rolling back is still safe due to the guard clause in RollbackAutoNumber // so we rollback in both cases to keep the counter clean if (an != null && isNew) await _autoNumber.RollbackAutoNumber("MSKILL", an.EndNumber, login); throw; } } public async Task DeleteSkill(int skillId, LoginDTO loginDTO) { var result = await _dal.DeleteSkill(skillId, loginDTO); if (result == ErrorResponse.DeleteNotFoundMessage) throw new Exception(result); return result; } public async Task GetSkillList(int firstNumber, int maxResult, CriteriaDTO criteriaDTO, LoginDTO login, CancellationToken ct = default) { // ── VALIDATION ─────────────────────────────────────────── if (firstNumber == -1 && maxResult != -1) throw new ArgumentException("MaxResult must also be -1 when FirstNumber is -1."); if (firstNumber != -1 && maxResult == -1) throw new ArgumentException("FirstNumber must also be -1 when MaxResult is -1."); if (firstNumber != -1 && firstNumber < 1) throw new ArgumentException($"FirstNumber must be >= 1 or -1. Got: {firstNumber}"); if (maxResult != -1 && maxResult < 1) throw new ArgumentException($"MaxResult must be >= 1 or -1. Got: {maxResult}"); // ── FETCH DATA ─────────────────────────────────────────── bool isAllData = firstNumber == -1 && maxResult == -1; List data = isAllData ? await _dal.GetAllList(login, ct) : await _dal.GetPagedList(firstNumber, maxResult, login, ct); // ── SERIALIZE (KEY CHANGE) ──────────────────────────────── var json = JsonSerializer.Serialize(data); return json; } } }