hrms-api-backend/BMA.EHR.Application/Repositories/Leaves/LeaveRequests/LeaveBeginingRepository.cs
Suphonchai Phoonsawat 5d090fa7bd
All checks were successful
Build & Deploy Leave Service / build (push) Successful in 1m51s
#2551
2026-06-22 10:18:21 +07:00

581 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Amazon.S3.Model;
using BMA.EHR.Application.Common.Interfaces;
using BMA.EHR.Application.Messaging;
using BMA.EHR.Application.Responses.Profiles;
using BMA.EHR.Domain.Extensions;
using BMA.EHR.Domain.Models.Leave.Commons;
using BMA.EHR.Domain.Models.Leave.Requests;
using BMA.EHR.Domain.Shared;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System.Collections.Concurrent;
namespace BMA.EHR.Application.Repositories.Leaves.LeaveRequests
{
public class LeaveBeginningRepository : GenericLeaveRepository<Guid, LeaveBeginning>
{
#region " Fields "
private readonly ILeaveDbContext _dbContext;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly OrganizationCommonRepository _organizationCommonRepository;
private readonly UserProfileRepository _userProfileRepository;
private readonly IConfiguration _configuration;
private readonly EmailSenderService _emailSenderService;
/// <summary>
/// Keyed locks to serialize get-or-create for LeaveBeginning rows by (ProfileId, LeaveYear, LeaveTypeId).
/// Prevents duplicate inserts when concurrent requests (e.g. UI calling /user/check twice) hit the same key.
/// </summary>
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _getOrAddLocks = new();
#endregion
#region " Constructor and Destuctor "
public LeaveBeginningRepository(ILeaveDbContext dbContext,
IHttpContextAccessor httpContextAccessor,
OrganizationCommonRepository organizationCommonRepository,
UserProfileRepository userProfileRepository,
IConfiguration configuration,
EmailSenderService emailSenderService) : base(dbContext, httpContextAccessor)
{
_dbContext = dbContext;
_httpContextAccessor = httpContextAccessor;
_organizationCommonRepository = organizationCommonRepository;
_userProfileRepository = userProfileRepository;
_configuration = configuration;
_emailSenderService = emailSenderService;
}
#endregion
#region " Properties "
protected Guid UserOrganizationId
{
get
{
if (UserId != null || UserId != "")
return _userProfileRepository.GetUserOCId(Guid.Parse(UserId!), AccessToken);
else
return Guid.Empty;
}
}
#endregion
public async Task<List<LeaveBeginning>> GetAllByYearAsync(int year)
{
return await _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.Where(x => x.LeaveYear == year)
.ToListAsync();
}
public async Task<LeaveBeginning?> GetByYearAndTypeIdAsync(int year, Guid typeId)
{
var data = await _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.FirstOrDefaultAsync(x => x.LeaveYear == year && x.LeaveTypeId == typeId);
return data;
}
public async Task UpdateLeaveUsageAsync(int year, Guid typeId, Guid userId, double day)
{
// var pf = await _userProfileRepository.GetProfileByKeycloakIdAsync(userId, AccessToken);
var pf = await _userProfileRepository.GetProfileByKeycloakIdNew2Async(userId, AccessToken);
if (pf == null)
{
throw new Exception(GlobalMessages.DataNotFound);
}
var data = await _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.FirstOrDefaultAsync(x => x.LeaveYear == year && x.LeaveTypeId == typeId && x.ProfileId == pf.Id);
if (data == null)
{
throw new Exception(GlobalMessages.DataNotFound);
}
data.LeaveDaysUsed += day;
await _dbContext.SaveChangesAsync();
}
public async Task UpdateLeaveCountAsync(int year, Guid typeId, Guid userId, int count)
{
// var pf = await _userProfileRepository.GetProfileByKeycloakIdAsync(userId, AccessToken);
var pf = await _userProfileRepository.GetProfileByKeycloakIdNew2Async(userId, AccessToken);
if (pf == null)
{
throw new Exception(GlobalMessages.DataNotFound);
}
var data = await _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.FirstOrDefaultAsync(x => x.LeaveYear == year && x.LeaveTypeId == typeId && x.ProfileId == pf.Id);
if (data == null)
{
throw new Exception(GlobalMessages.DataNotFound);
}
data.LeaveCount += count;
await _dbContext.SaveChangesAsync();
}
public async Task ProcessEarlyLeaveRequest(int year)
{
// Get Early Leave Request (กรองตามปีงบประมาณ: 1 ต.ค. (year-1) 30 ก.ย. (year))
var fiscalStart = new DateTime(year - 1, 10, 1);
var fiscalEnd = new DateTime(year, 9, 30);
var leaveReq = await _dbContext.Set<LeaveRequest>()
.Include(x => x.Type)
.Where(x => x.LeaveStatus == "APPROVE")
.Where(x => x.LeaveStartDate.Date <= fiscalEnd && x.LeaveEndDate.Date >= fiscalStart)
.ToListAsync();
foreach (var leave in leaveReq)
{
await GetByYearAndTypeIdForUserWithUpdateAsync(year, leave.Type.Id, leave.KeycloakUserId);
}
}
public async Task ProcessEarlyLeaveRequestSchedule()
{
int year = DateTime.Now.Year;
await ProcessEarlyLeaveRequest(year);
}
public async Task<LeaveBeginning?> GetByYearAndTypeIdForUserAsync(int year, Guid typeId, Guid userId)
{
// var pf = await _userProfileRepository.GetProfileByKeycloakIdAsync(userId, AccessToken);
var pf = await _userProfileRepository.GetProfileByKeycloakIdNew2Async(userId, AccessToken);
if (pf == null)
{
throw new Exception(GlobalMessages.DataNotFound);
}
var govAge = (pf?.DateStart?.Date ?? DateTime.Now.Date).DiffDay(DateTime.Now.Date);
var leaveType = await _dbContext.Set<LeaveType>().FirstOrDefaultAsync(x => x.Id == typeId);
LeaveBeginning Factory()
{
var limit = 0.0;
var prev = _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.FirstOrDefault(x => x.LeaveYear == year - 1 && x.LeaveTypeId == typeId && x.ProfileId == pf.Id);
// คำนวณปีงบประมาณจาก startDate (ปีงบประมาณเริ่ม 1 ต.ค. และสิ้นสุด 30 ก.ย.)
var isCurrentYear = DateTime.Now.Year == year;
var prevRemain = 0.0;
if (prev != null)
{
prevRemain = isCurrentYear ? prev.LeaveDays - (prev.LeaveDaysUsed ?? 0.0) : 0.0;
}
if (govAge >= 180)
{
if (govAge >= 3650)
{
limit = 10 + prevRemain;
if (limit > 30) limit = 30;
}
else
{
limit = 10 + prevRemain;
if (limit > 20) limit = 20;
}
}
else
{
limit = 0.0;
}
return new LeaveBeginning
{
LeaveYear = year,
LeaveTypeId = typeId,
ProfileId = pf.Id,
Prefix = pf.Prefix,
FirstName = pf.FirstName,
LastName = pf.LastName,
LeaveDaysUsed = 0,
LeaveDays = leaveType?.Code == "LV-005" ? limit : 0,
RootDnaId = pf.RootDnaId,
Child1DnaId = pf.Child1DnaId,
Child2DnaId = pf.Child2DnaId,
Child3DnaId = pf.Child3DnaId,
Child4DnaId = pf.Child4DnaId
};
}
return await GetOrAddForUserAsync(year, typeId, pf.Id, Factory);
}
public async Task<LeaveBeginning?> GetByYearAndTypeIdForUserWithUpdateAsync(int year, Guid typeId, Guid userId)
{
// var pf = await _userProfileRepository.GetProfileByKeycloakIdAsync(userId, AccessToken);
var pf = await _userProfileRepository.GetProfileByKeycloakIdNew2Async(userId, AccessToken);
if (pf == null)
{
throw new Exception(GlobalMessages.DataNotFound);
}
var govAge = (pf?.DateStart?.Date ?? DateTime.Now.Date).DiffDay(DateTime.Now.Date);
var leaveType = await _dbContext.Set<LeaveType>().FirstOrDefaultAsync(x => x.Id == typeId);
var limit = 0.0;
var prev = _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.FirstOrDefault(x => x.LeaveYear == year - 1 && x.LeaveTypeId == typeId && x.ProfileId == pf.Id);
var prevRemain = 0.0;
if (prev != null)
{
prevRemain = prev.LeaveDays - (prev.LeaveDaysUsed ?? 0.0);
}
if (govAge >= 180)
{
if (govAge >= 3650)
{
limit = 10 + prevRemain;
if (limit > 30) limit = 30;
}
else
{
limit = 10 + prevRemain;
if (limit > 20) limit = 20;
}
}
else
{
limit = 0.0;
}
var data = await _dbContext.Set<LeaveBeginning>()
.Where(x => x.LeaveYear == year && x.LeaveTypeId == typeId && x.ProfileId == pf.Id)
.FirstOrDefaultAsync();
if (data != null)
{
data.LeaveDays = leaveType?.Code == "LV-005" ? limit : 0;
await _dbContext.SaveChangesAsync();
}
// return new LeaveBeginning
// {
// LeaveYear = year,
// LeaveTypeId = typeId,
// ProfileId = pf.Id,
// Prefix = pf.Prefix,
// FirstName = pf.FirstName,
// LastName = pf.LastName,
// LeaveDaysUsed = 0,
// LeaveDays = leaveType?.Code == "LV-005" ? limit : 0,
// RootDnaId = pf.RootDnaId,
// Child1DnaId = pf.Child1DnaId,
// Child2DnaId = pf.Child2DnaId,
// Child3DnaId = pf.Child3DnaId,
// Child4DnaId = pf.Child4DnaId
// };
return data;
}
public async Task<LeaveBeginning?> GetByYearAndTypeIdForUser(int year, Guid typeId, GetProfileByKeycloakIdDto? pf)
{
var govAge = (pf?.DateStart?.Date ?? DateTime.Now.Date).DiffDay(DateTime.Now.Date);
var leaveType = await _dbContext.Set<LeaveType>().FirstOrDefaultAsync(x => x.Id == typeId);
LeaveBeginning Factory()
{
var limit = 0.0;
var prev = _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.FirstOrDefault(x => x.LeaveYear == year - 1 && x.LeaveTypeId == typeId && x.ProfileId == pf.Id);
var prevRemain = 0.0;
if (prev != null)
{
prevRemain = prev.LeaveDays - (prev.LeaveDaysUsed ?? 0.0);
}
if (govAge >= 180)
{
if (govAge >= 3650)
{
limit = 10 + prevRemain;
if (limit > 30) limit = 30;
}
else
{
limit = 10 + prevRemain;
if (limit > 20) limit = 20;
}
}
else
{
limit = 0.0;
}
return new LeaveBeginning
{
LeaveYear = year,
LeaveTypeId = typeId,
ProfileId = pf.Id,
Prefix = pf.Prefix,
FirstName = pf.FirstName,
LastName = pf.LastName,
LeaveDaysUsed = 0,
LeaveDays = leaveType?.Code == "LV-005" ? limit : 0,
RootDnaId = pf.RootDnaId,
Child1DnaId = pf.Child1DnaId,
Child2DnaId = pf.Child2DnaId,
Child3DnaId = pf.Child3DnaId,
Child4DnaId = pf.Child4DnaId
};
}
return await GetOrAddForUserAsync(year, typeId, pf.Id, Factory);
}
public async Task<LeaveBeginning?> GetByYearAndTypeIdForUser2Async(int year, Guid typeId, Guid userId)
{
// var pf = await _userProfileRepository.GetProfileByKeycloakIdAsync(userId, AccessToken);
var pf = await _userProfileRepository.GetProfileByKeycloakIdNew2Async(userId, AccessToken);
if (pf == null)
{
return null;
}
var govAge = (pf?.DateStart?.Date ?? DateTime.Now.Date).DiffDay(DateTime.Now.Date);
var leaveType = await _dbContext.Set<LeaveType>().FirstOrDefaultAsync(x => x.Id == typeId);
LeaveBeginning Factory()
{
var limit = 0.0;
var prev = _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.FirstOrDefault(x => x.LeaveYear == year - 1 && x.LeaveTypeId == typeId && x.ProfileId == pf.Id);
var prevRemain = 0.0;
if (prev != null)
{
prevRemain = prev.LeaveDays - (prev.LeaveDaysUsed ?? 0.0);
}
if (govAge >= 180)
{
if (govAge >= 3650)
{
limit = 10 + prevRemain;
if (limit > 30) limit = 30;
}
else
{
limit = 10 + prevRemain;
if (limit > 20) limit = 20;
}
}
else
{
limit = 0.0;
}
return new LeaveBeginning
{
LeaveYear = year,
LeaveTypeId = typeId,
ProfileId = pf.Id,
Prefix = pf.Prefix,
FirstName = pf.FirstName,
LastName = pf.LastName,
LeaveDaysUsed = 0,
LeaveDays = leaveType?.Code == "LV-005" ? limit : 0,
RootDnaId = pf.RootDnaId,
Child1DnaId = pf.Child1DnaId,
Child2DnaId = pf.Child2DnaId,
Child3DnaId = pf.Child3DnaId,
Child4DnaId = pf.Child4DnaId
};
}
return await GetOrAddForUserAsync(year, typeId, pf.Id, Factory);
}
/// <summary>
/// Get-or-create a LeaveBeginning row for (ProfileId, LeaveYear, LeaveTypeId) with concurrency protection.
/// Uses a keyed SemaphoreSlim to serialize within-process requests, and re-queries after acquiring the lock.
/// If a cross-process insert wins (unique index violation), the duplicate key exception is caught and the row
/// created by the winner is returned.
/// </summary>
private async Task<LeaveBeginning?> GetOrAddForUserAsync(int year, Guid typeId, Guid profileId, Func<LeaveBeginning> factory)
{
var key = $"{profileId}_{year}_{typeId}";
var semaphore = _getOrAddLocks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
await semaphore.WaitAsync();
try
{
// Re-query inside the lock — another thread may have created it while we waited.
var existing = await _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.FirstOrDefaultAsync(x => x.LeaveYear == year && x.LeaveTypeId == typeId && x.ProfileId == profileId);
if (existing != null)
{
return existing;
}
var entity = factory();
_dbContext.Set<LeaveBeginning>().Add(entity);
try
{
await _dbContext.SaveChangesAsync();
return entity;
}
catch (DbUpdateException)
{
// Cross-process/cross-server race hit the unique index (IX_LeaveBeginnings_ProfileId_LeaveYear_LeaveTypeId).
// Detach the failed insert and return the row created by the winner.
_dbContext.Detach(entity);
var winner = await _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.FirstOrDefaultAsync(x => x.LeaveYear == year && x.LeaveTypeId == typeId && x.ProfileId == profileId);
return winner;
}
}
finally
{
semaphore.Release();
}
}
public async Task<List<LeaveBeginning>> GetAllByYearAndTypeAsync(int year, Guid typeId, List<ProfileData> userIdList)
{
var updateList = new List<LeaveBeginning>();
var result = new List<LeaveBeginning>();
var beginningList = await _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.Where(x => x.LeaveYear == year && x.LeaveTypeId == typeId)
.ToListAsync();
foreach (var pf in userIdList)
{
//var pf = await _userProfileRepository.GetProfileByKeycloakIdAsync(id, AccessToken);
//if (pf == null)
//{
// continue; // Goto Next Id
//}
var profile = await _userProfileRepository.GetProfileByProfileIdAsync(pf.Id, AccessToken);
if (profile == null)
{
return null;
}
var govAge = (pf?.DateStart?.Date ?? DateTime.Now.Date).DiffDay(DateTime.Now.Date);
var leaveType = await _dbContext.Set<LeaveType>().FirstOrDefaultAsync(x => x.Id == typeId);
var data = beginningList.FirstOrDefault(x => x.ProfileId == pf.Id);
if (data == null)
{
var limit = 0.0;
var prev = await _dbContext.Set<LeaveBeginning>()
.Include(x => x.LeaveType)
.FirstOrDefaultAsync(x => x.LeaveYear == year - 1 && x.LeaveTypeId == typeId && x.ProfileId == pf.Id);
var prevRemain = 0.0;
if (prev != null)
{
prevRemain = prev.LeaveDays - (prev.LeaveDaysUsed ?? 0.0);
}
if (govAge >= 180)
{
if (govAge >= 3650)
{
limit = 10 + prevRemain;
if (limit > 30) limit = 30;
}
else
{
limit = 10 + prevRemain;
if (limit > 20) limit = 20;
}
}
else
{
limit = 0.0;
}
data = new LeaveBeginning
{
LeaveYear = year,
LeaveTypeId = typeId,
ProfileId = pf.Id,
Prefix = pf.Prefix,
FirstName = pf.FirstName,
LastName = pf.LastName,
LeaveDaysUsed = 0,
LeaveDays = leaveType?.Code == "LV-005" ? limit : 0,
RootDnaId = profile.RootDnaId,
Child1DnaId = profile.Child1DnaId,
Child2DnaId = profile.Child2DnaId,
Child3DnaId = profile.Child3DnaId,
Child4DnaId = profile.Child4DnaId
};
updateList.Add(data);
}
result.Add(data);
}
if (!updateList.Any())
{
await _dbContext.Set<LeaveBeginning>().AddRangeAsync(updateList);
await _dbContext.SaveChangesAsync();
}
return result;
}
}
public class ProfileData
{
public Guid Id { get; set; } = Guid.Empty;
public string Prefix { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public DateTime? DateStart { get; set; } = null;
public DateTime? DateAppoint { get; set; } = null;
}
}