[CLAUDE] App+Api: S35 Plan G-H2 Task 3 — HrmConfig BE CRUD 16 endpoint cookie-cutter
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m43s

Pattern 12-bis cumulative 3× (S29 Plan B + Chunk C + S35 G-H2). Mega scaffold mirror
Master/Catalogs/CatalogsFeatures.cs 334 LOC pattern → SolutionErp.Application/Hrm/
HrmConfigFeatures.cs 439 LOC. 4 region cookie-cutter cho 4 HRM catalog (LeaveTypes +
Holidays + ShiftPatterns + OtPolicies). 16 endpoint qua HrmConfigsController.cs 137 LOC.

## Scope (Implementer Case 2 ~25K spawn)
- HrmConfigFeatures.cs 4 region × (DTO + List + Create cmd/validator/handler + Update cmd/validator/handler + Delete cmd/handler) = 4 DTO + 4 List query + 4 Create + 4 Update + 4 Delete handler
- HrmConfigsController.cs URL `/api/hrm-configs/{kind}` × {leave-types, holidays, shifts, ot-policies} × 4 verb = 16 endpoint
- Authorization: Class `[Authorize]` + write `[Authorize(Roles="Admin")]` 12 attribute (4 × 3 write verb) mirror Catalogs precedent
- Conflict check: 8 AnyAsync IsDeleted (4 Create + 4 Update với composite Year+Date Holiday exclude-self via `x.Id != req.Id`)
- Validator: 8 AbstractValidator + 44 RuleFor — MaxLength match EF source-of-truth (Smart Friend Implementer caught spec drift, aligned)
- HRM entities NO HasQueryFilter (4 vs 9 file Master have it) — explicit `.Where(!IsDeleted)` in List/Conflict-check queries

## Build + Test verify
- dotnet build PASS 0 err (2 pre-existing DocxRenderer warning unrelated)
- dotnet test 130/130 PASS preserve (BE-only, no FE/test touch)
- FE files NOT touched (defer Task 4 G-H2 FE Admin next chunk)
- EF Config + DbInitializer + IApplicationDbContext: S34 done, no re-touch

## Pre-commit Reviewer Smart Friend 8× clean cumulative
S22+S25+S29×2+S33×2+S35×2 (FE forms + BE CRUD)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-28 09:51:18 +07:00
parent c3cd343bae
commit 909655c40d
2 changed files with 576 additions and 0 deletions

View File

@ -0,0 +1,439 @@
using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Domain.Hrm;
namespace SolutionErp.Application.Hrm;
// CRUD CQRS cho 4 HRM catalog (LeaveTypes/Holidays/Shifts/OtPolicies).
// Pattern 12-bis mega mirror Master/Catalogs/CatalogsFeatures.cs (S29 cumulative 2×).
// Endpoints expose qua HrmConfigsController:
// GET /api/hrm-configs/{kind} — list (filter q + isActive / year)
// POST /api/hrm-configs/{kind} — create
// PUT /api/hrm-configs/{kind}/{id} — update
// DELETE /api/hrm-configs/{kind}/{id} — soft delete via AuditingInterceptor
// kind ∈ {leave-types, holidays, shifts, ot-policies}
//
// MaxLength validator MATCH EF config exact (LeaveTypeConfiguration etc) — avoid runtime
// truncation. HRM entities KHÔNG có global HasQueryFilter (khác Master/Catalogs) → list
// query phải Where(!IsDeleted) thủ công.
// ===== DTOs =====
public record LeaveTypeDto(Guid Id, string Code, string Name, decimal DaysPerYear, bool IsPaid, bool RequiresAttachment, bool IsActive, string? Description);
public record HolidayDto(Guid Id, int Year, DateOnly Date, string Name, bool IsRecurring, bool IsPaid, bool IsActive, string? Description);
public record ShiftPatternDto(Guid Id, string Code, string Name, TimeOnly StartTime, TimeOnly EndTime, int BreakMinutes, string WorkDays, bool IsActive, string? Description);
public record OtPolicyDto(Guid Id, string Code, string Name, decimal MultiplierWeekday, decimal MultiplierWeekend, decimal MultiplierHoliday, int MaxHoursPerDay, int MaxHoursPerMonth, int MaxHoursPerYear, bool IsActive, string? Description);
// ===== Region 1: LeaveType =====
public record ListLeaveTypesQuery(string? Q = null, bool? IsActive = null) : IRequest<List<LeaveTypeDto>>;
public class ListLeaveTypesHandler(IApplicationDbContext db) : IRequestHandler<ListLeaveTypesQuery, List<LeaveTypeDto>>
{
public async Task<List<LeaveTypeDto>> Handle(ListLeaveTypesQuery req, CancellationToken ct)
{
var q = db.LeaveTypes.AsNoTracking().Where(x => !x.IsDeleted);
if (!string.IsNullOrWhiteSpace(req.Q))
{
var s = req.Q.ToLower();
q = q.Where(x => x.Code.ToLower().Contains(s) || x.Name.ToLower().Contains(s));
}
if (req.IsActive.HasValue) q = q.Where(x => x.IsActive == req.IsActive.Value);
return await q.OrderBy(x => x.Code)
.Select(x => new LeaveTypeDto(x.Id, x.Code, x.Name, x.DaysPerYear, x.IsPaid, x.RequiresAttachment, x.IsActive, x.Description))
.ToListAsync(ct);
}
}
public record CreateLeaveTypeCommand(string Code, string Name, decimal DaysPerYear, bool IsPaid, bool RequiresAttachment, string? Description) : IRequest<Guid>;
public class CreateLeaveTypeValidator : AbstractValidator<CreateLeaveTypeCommand>
{
public CreateLeaveTypeValidator()
{
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.DaysPerYear).GreaterThanOrEqualTo(0);
RuleFor(x => x.Description).MaximumLength(500);
}
}
public class CreateLeaveTypeHandler(IApplicationDbContext db) : IRequestHandler<CreateLeaveTypeCommand, Guid>
{
public async Task<Guid> Handle(CreateLeaveTypeCommand req, CancellationToken ct)
{
if (await db.LeaveTypes.AnyAsync(x => x.Code == req.Code && !x.IsDeleted, ct))
throw new ConflictException($"Mã loại phép '{req.Code}' đã tồn tại.");
var entity = new LeaveType
{
Code = req.Code,
Name = req.Name,
DaysPerYear = req.DaysPerYear,
IsPaid = req.IsPaid,
RequiresAttachment = req.RequiresAttachment,
Description = req.Description,
IsActive = true,
};
db.LeaveTypes.Add(entity);
await db.SaveChangesAsync(ct);
return entity.Id;
}
}
public record UpdateLeaveTypeCommand(Guid Id, string Code, string Name, decimal DaysPerYear, bool IsPaid, bool RequiresAttachment, bool IsActive, string? Description) : IRequest;
public class UpdateLeaveTypeValidator : AbstractValidator<UpdateLeaveTypeCommand>
{
public UpdateLeaveTypeValidator()
{
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.DaysPerYear).GreaterThanOrEqualTo(0);
RuleFor(x => x.Description).MaximumLength(500);
}
}
public class UpdateLeaveTypeHandler(IApplicationDbContext db) : IRequestHandler<UpdateLeaveTypeCommand>
{
public async Task Handle(UpdateLeaveTypeCommand req, CancellationToken ct)
{
var entity = await db.LeaveTypes.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct)
?? throw new NotFoundException("LeaveType", req.Id);
if (entity.Code != req.Code && await db.LeaveTypes.AnyAsync(x => x.Code == req.Code && !x.IsDeleted, ct))
throw new ConflictException($"Mã loại phép '{req.Code}' đã tồn tại.");
entity.Code = req.Code;
entity.Name = req.Name;
entity.DaysPerYear = req.DaysPerYear;
entity.IsPaid = req.IsPaid;
entity.RequiresAttachment = req.RequiresAttachment;
entity.IsActive = req.IsActive;
entity.Description = req.Description;
await db.SaveChangesAsync(ct);
}
}
public record DeleteLeaveTypeCommand(Guid Id) : IRequest;
public class DeleteLeaveTypeHandler(IApplicationDbContext db) : IRequestHandler<DeleteLeaveTypeCommand>
{
public async Task Handle(DeleteLeaveTypeCommand req, CancellationToken ct)
{
var entity = await db.LeaveTypes.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct)
?? throw new NotFoundException("LeaveType", req.Id);
db.LeaveTypes.Remove(entity); // Soft delete via AuditingInterceptor
await db.SaveChangesAsync(ct);
}
}
// ===== Region 2: Holiday =====
// UNIQUE composite (Year, Date) — Code field không có.
// List filter Year nullable: null = trả về all year, có value = chỉ year đó.
public record ListHolidaysQuery(string? Q = null, int? Year = null) : IRequest<List<HolidayDto>>;
public class ListHolidaysHandler(IApplicationDbContext db) : IRequestHandler<ListHolidaysQuery, List<HolidayDto>>
{
public async Task<List<HolidayDto>> Handle(ListHolidaysQuery req, CancellationToken ct)
{
var q = db.Holidays.AsNoTracking().Where(x => !x.IsDeleted);
if (req.Year.HasValue) q = q.Where(x => x.Year == req.Year.Value);
if (!string.IsNullOrWhiteSpace(req.Q))
{
var s = req.Q.ToLower();
q = q.Where(x => x.Name.ToLower().Contains(s));
}
return await q.OrderBy(x => x.Year).ThenBy(x => x.Date)
.Select(x => new HolidayDto(x.Id, x.Year, x.Date, x.Name, x.IsRecurring, x.IsPaid, x.IsActive, x.Description))
.ToListAsync(ct);
}
}
public record CreateHolidayCommand(int Year, DateOnly Date, string Name, bool IsRecurring, bool IsPaid, string? Description) : IRequest<Guid>;
public class CreateHolidayValidator : AbstractValidator<CreateHolidayCommand>
{
public CreateHolidayValidator()
{
RuleFor(x => x.Year).InclusiveBetween(2000, 2100);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.Description).MaximumLength(500);
}
}
public class CreateHolidayHandler(IApplicationDbContext db) : IRequestHandler<CreateHolidayCommand, Guid>
{
public async Task<Guid> Handle(CreateHolidayCommand req, CancellationToken ct)
{
if (await db.Holidays.AnyAsync(x => x.Year == req.Year && x.Date == req.Date && !x.IsDeleted, ct))
throw new ConflictException($"Ngày lễ năm {req.Year} ngày {req.Date:dd/MM/yyyy} đã tồn tại.");
var entity = new Holiday
{
Year = req.Year,
Date = req.Date,
Name = req.Name,
IsRecurring = req.IsRecurring,
IsPaid = req.IsPaid,
Description = req.Description,
IsActive = true,
};
db.Holidays.Add(entity);
await db.SaveChangesAsync(ct);
return entity.Id;
}
}
public record UpdateHolidayCommand(Guid Id, int Year, DateOnly Date, string Name, bool IsRecurring, bool IsPaid, bool IsActive, string? Description) : IRequest;
public class UpdateHolidayValidator : AbstractValidator<UpdateHolidayCommand>
{
public UpdateHolidayValidator()
{
RuleFor(x => x.Year).InclusiveBetween(2000, 2100);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.Description).MaximumLength(500);
}
}
public class UpdateHolidayHandler(IApplicationDbContext db) : IRequestHandler<UpdateHolidayCommand>
{
public async Task Handle(UpdateHolidayCommand req, CancellationToken ct)
{
var entity = await db.Holidays.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct)
?? throw new NotFoundException("Holiday", req.Id);
// Check conflict composite (Year, Date) khi 1 trong 2 đổi
if ((entity.Year != req.Year || entity.Date != req.Date)
&& await db.Holidays.AnyAsync(x => x.Year == req.Year && x.Date == req.Date && !x.IsDeleted && x.Id != req.Id, ct))
throw new ConflictException($"Ngày lễ năm {req.Year} ngày {req.Date:dd/MM/yyyy} đã tồn tại.");
entity.Year = req.Year;
entity.Date = req.Date;
entity.Name = req.Name;
entity.IsRecurring = req.IsRecurring;
entity.IsPaid = req.IsPaid;
entity.IsActive = req.IsActive;
entity.Description = req.Description;
await db.SaveChangesAsync(ct);
}
}
public record DeleteHolidayCommand(Guid Id) : IRequest;
public class DeleteHolidayHandler(IApplicationDbContext db) : IRequestHandler<DeleteHolidayCommand>
{
public async Task Handle(DeleteHolidayCommand req, CancellationToken ct)
{
var entity = await db.Holidays.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct)
?? throw new NotFoundException("Holiday", req.Id);
db.Holidays.Remove(entity);
await db.SaveChangesAsync(ct);
}
}
// ===== Region 3: ShiftPattern =====
// Code UNIQUE (entity MaxLen 20 — khác LeaveType/OtPolicy 50).
// Validator extra: StartTime != EndTime (denote ca làm việc khác giờ thật).
// WorkDays NotEmpty đủ — FE Designer multi-select sẽ format "Mon,Tue,..."
public record ListShiftsQuery(string? Q = null, bool? IsActive = null) : IRequest<List<ShiftPatternDto>>;
public class ListShiftsHandler(IApplicationDbContext db) : IRequestHandler<ListShiftsQuery, List<ShiftPatternDto>>
{
public async Task<List<ShiftPatternDto>> Handle(ListShiftsQuery req, CancellationToken ct)
{
var q = db.ShiftPatterns.AsNoTracking().Where(x => !x.IsDeleted);
if (!string.IsNullOrWhiteSpace(req.Q))
{
var s = req.Q.ToLower();
q = q.Where(x => x.Code.ToLower().Contains(s) || x.Name.ToLower().Contains(s));
}
if (req.IsActive.HasValue) q = q.Where(x => x.IsActive == req.IsActive.Value);
return await q.OrderBy(x => x.Code)
.Select(x => new ShiftPatternDto(x.Id, x.Code, x.Name, x.StartTime, x.EndTime, x.BreakMinutes, x.WorkDays, x.IsActive, x.Description))
.ToListAsync(ct);
}
}
public record CreateShiftCommand(string Code, string Name, TimeOnly StartTime, TimeOnly EndTime, int BreakMinutes, string WorkDays, string? Description) : IRequest<Guid>;
public class CreateShiftValidator : AbstractValidator<CreateShiftCommand>
{
public CreateShiftValidator()
{
RuleFor(x => x.Code).NotEmpty().MaximumLength(20);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.WorkDays).NotEmpty().MaximumLength(100);
RuleFor(x => x.BreakMinutes).GreaterThanOrEqualTo(0);
RuleFor(x => x.Description).MaximumLength(500);
RuleFor(x => x).Must(c => c.StartTime != c.EndTime)
.WithMessage("Giờ bắt đầu và giờ kết thúc không được trùng nhau.");
}
}
public class CreateShiftHandler(IApplicationDbContext db) : IRequestHandler<CreateShiftCommand, Guid>
{
public async Task<Guid> Handle(CreateShiftCommand req, CancellationToken ct)
{
if (await db.ShiftPatterns.AnyAsync(x => x.Code == req.Code && !x.IsDeleted, ct))
throw new ConflictException($"Mã ca làm việc '{req.Code}' đã tồn tại.");
var entity = new ShiftPattern
{
Code = req.Code,
Name = req.Name,
StartTime = req.StartTime,
EndTime = req.EndTime,
BreakMinutes = req.BreakMinutes,
WorkDays = req.WorkDays,
Description = req.Description,
IsActive = true,
};
db.ShiftPatterns.Add(entity);
await db.SaveChangesAsync(ct);
return entity.Id;
}
}
public record UpdateShiftCommand(Guid Id, string Code, string Name, TimeOnly StartTime, TimeOnly EndTime, int BreakMinutes, string WorkDays, bool IsActive, string? Description) : IRequest;
public class UpdateShiftValidator : AbstractValidator<UpdateShiftCommand>
{
public UpdateShiftValidator()
{
RuleFor(x => x.Code).NotEmpty().MaximumLength(20);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.WorkDays).NotEmpty().MaximumLength(100);
RuleFor(x => x.BreakMinutes).GreaterThanOrEqualTo(0);
RuleFor(x => x.Description).MaximumLength(500);
RuleFor(x => x).Must(c => c.StartTime != c.EndTime)
.WithMessage("Giờ bắt đầu và giờ kết thúc không được trùng nhau.");
}
}
public class UpdateShiftHandler(IApplicationDbContext db) : IRequestHandler<UpdateShiftCommand>
{
public async Task Handle(UpdateShiftCommand req, CancellationToken ct)
{
var entity = await db.ShiftPatterns.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct)
?? throw new NotFoundException("ShiftPattern", req.Id);
if (entity.Code != req.Code && await db.ShiftPatterns.AnyAsync(x => x.Code == req.Code && !x.IsDeleted, ct))
throw new ConflictException($"Mã ca làm việc '{req.Code}' đã tồn tại.");
entity.Code = req.Code;
entity.Name = req.Name;
entity.StartTime = req.StartTime;
entity.EndTime = req.EndTime;
entity.BreakMinutes = req.BreakMinutes;
entity.WorkDays = req.WorkDays;
entity.IsActive = req.IsActive;
entity.Description = req.Description;
await db.SaveChangesAsync(ct);
}
}
public record DeleteShiftCommand(Guid Id) : IRequest;
public class DeleteShiftHandler(IApplicationDbContext db) : IRequestHandler<DeleteShiftCommand>
{
public async Task Handle(DeleteShiftCommand req, CancellationToken ct)
{
var entity = await db.ShiftPatterns.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct)
?? throw new NotFoundException("ShiftPattern", req.Id);
db.ShiftPatterns.Remove(entity);
await db.SaveChangesAsync(ct);
}
}
// ===== Region 4: OtPolicy =====
// Code UNIQUE. KHÔNG auto-deactivate existing IsActive=true (admin manually toggle qua Update).
// Validator: Multipliers >= 1.0 (không thể nhỏ hơn giờ bình thường) + MaxHours > 0.
public record ListOtPoliciesQuery(string? Q = null, bool? IsActive = null) : IRequest<List<OtPolicyDto>>;
public class ListOtPoliciesHandler(IApplicationDbContext db) : IRequestHandler<ListOtPoliciesQuery, List<OtPolicyDto>>
{
public async Task<List<OtPolicyDto>> Handle(ListOtPoliciesQuery req, CancellationToken ct)
{
var q = db.OtPolicies.AsNoTracking().Where(x => !x.IsDeleted);
if (!string.IsNullOrWhiteSpace(req.Q))
{
var s = req.Q.ToLower();
q = q.Where(x => x.Code.ToLower().Contains(s) || x.Name.ToLower().Contains(s));
}
if (req.IsActive.HasValue) q = q.Where(x => x.IsActive == req.IsActive.Value);
return await q.OrderBy(x => x.Code)
.Select(x => new OtPolicyDto(x.Id, x.Code, x.Name, x.MultiplierWeekday, x.MultiplierWeekend, x.MultiplierHoliday, x.MaxHoursPerDay, x.MaxHoursPerMonth, x.MaxHoursPerYear, x.IsActive, x.Description))
.ToListAsync(ct);
}
}
public record CreateOtPolicyCommand(string Code, string Name, decimal MultiplierWeekday, decimal MultiplierWeekend, decimal MultiplierHoliday, int MaxHoursPerDay, int MaxHoursPerMonth, int MaxHoursPerYear, string? Description) : IRequest<Guid>;
public class CreateOtPolicyValidator : AbstractValidator<CreateOtPolicyCommand>
{
public CreateOtPolicyValidator()
{
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.MultiplierWeekday).GreaterThanOrEqualTo(1.0m);
RuleFor(x => x.MultiplierWeekend).GreaterThanOrEqualTo(1.0m);
RuleFor(x => x.MultiplierHoliday).GreaterThanOrEqualTo(1.0m);
RuleFor(x => x.MaxHoursPerDay).GreaterThan(0);
RuleFor(x => x.MaxHoursPerMonth).GreaterThan(0);
RuleFor(x => x.MaxHoursPerYear).GreaterThan(0);
RuleFor(x => x.Description).MaximumLength(500);
}
}
public class CreateOtPolicyHandler(IApplicationDbContext db) : IRequestHandler<CreateOtPolicyCommand, Guid>
{
public async Task<Guid> Handle(CreateOtPolicyCommand req, CancellationToken ct)
{
if (await db.OtPolicies.AnyAsync(x => x.Code == req.Code && !x.IsDeleted, ct))
throw new ConflictException($"Mã chính sách OT '{req.Code}' đã tồn tại.");
var entity = new OtPolicy
{
Code = req.Code,
Name = req.Name,
MultiplierWeekday = req.MultiplierWeekday,
MultiplierWeekend = req.MultiplierWeekend,
MultiplierHoliday = req.MultiplierHoliday,
MaxHoursPerDay = req.MaxHoursPerDay,
MaxHoursPerMonth = req.MaxHoursPerMonth,
MaxHoursPerYear = req.MaxHoursPerYear,
Description = req.Description,
IsActive = true,
};
db.OtPolicies.Add(entity);
await db.SaveChangesAsync(ct);
return entity.Id;
}
}
public record UpdateOtPolicyCommand(Guid Id, string Code, string Name, decimal MultiplierWeekday, decimal MultiplierWeekend, decimal MultiplierHoliday, int MaxHoursPerDay, int MaxHoursPerMonth, int MaxHoursPerYear, bool IsActive, string? Description) : IRequest;
public class UpdateOtPolicyValidator : AbstractValidator<UpdateOtPolicyCommand>
{
public UpdateOtPolicyValidator()
{
RuleFor(x => x.Code).NotEmpty().MaximumLength(50);
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.MultiplierWeekday).GreaterThanOrEqualTo(1.0m);
RuleFor(x => x.MultiplierWeekend).GreaterThanOrEqualTo(1.0m);
RuleFor(x => x.MultiplierHoliday).GreaterThanOrEqualTo(1.0m);
RuleFor(x => x.MaxHoursPerDay).GreaterThan(0);
RuleFor(x => x.MaxHoursPerMonth).GreaterThan(0);
RuleFor(x => x.MaxHoursPerYear).GreaterThan(0);
RuleFor(x => x.Description).MaximumLength(500);
}
}
public class UpdateOtPolicyHandler(IApplicationDbContext db) : IRequestHandler<UpdateOtPolicyCommand>
{
public async Task Handle(UpdateOtPolicyCommand req, CancellationToken ct)
{
var entity = await db.OtPolicies.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct)
?? throw new NotFoundException("OtPolicy", req.Id);
if (entity.Code != req.Code && await db.OtPolicies.AnyAsync(x => x.Code == req.Code && !x.IsDeleted, ct))
throw new ConflictException($"Mã chính sách OT '{req.Code}' đã tồn tại.");
entity.Code = req.Code;
entity.Name = req.Name;
entity.MultiplierWeekday = req.MultiplierWeekday;
entity.MultiplierWeekend = req.MultiplierWeekend;
entity.MultiplierHoliday = req.MultiplierHoliday;
entity.MaxHoursPerDay = req.MaxHoursPerDay;
entity.MaxHoursPerMonth = req.MaxHoursPerMonth;
entity.MaxHoursPerYear = req.MaxHoursPerYear;
entity.IsActive = req.IsActive;
entity.Description = req.Description;
await db.SaveChangesAsync(ct);
}
}
public record DeleteOtPolicyCommand(Guid Id) : IRequest;
public class DeleteOtPolicyHandler(IApplicationDbContext db) : IRequestHandler<DeleteOtPolicyCommand>
{
public async Task Handle(DeleteOtPolicyCommand req, CancellationToken ct)
{
var entity = await db.OtPolicies.FirstOrDefaultAsync(x => x.Id == req.Id && !x.IsDeleted, ct)
?? throw new NotFoundException("OtPolicy", req.Id);
db.OtPolicies.Remove(entity);
await db.SaveChangesAsync(ct);
}
}