using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Domain.Entities.Store.ValueObject; using Microsoft.EntityFrameworkCore; using SharedKernel.Results; namespace Application.Features.Admin.Store.Coupon.ExpireCodes; public sealed class Handler(IAppDbContext db) : ICommandHandler> { public async Task> Handle(Command request, CancellationToken ct) { if (request.CodeIDs is null || request.CodeIDs.Length == 0) { return Result.Failure(Error.Problem("CouponCode.NoIDs", "만료 처리할 코드를 선택해 주세요.")); } var targets = await db.CouponCode .Where(c => c.CouponID == request.CouponID && request.CodeIDs.Contains(c.ID)) .ToListAsync(ct); if (targets.Count == 0) { return Result.Failure(Error.NotFound("CouponCode.NotFound", "만료 처리할 코드를 찾을 수 없습니다.")); } var blocked = targets.Where(c => c.Status != CouponCodeStatus.Available && c.Status != CouponCodeStatus.Issued).ToList(); if (blocked.Count > 0) { var preview = string.Join(", ", blocked.Take(10).Select(c => $"#{c.ID}({StatusText(c.Status)})")); var more = blocked.Count > 10 ? $" 외 {blocked.Count - 10}건" : string.Empty; return Result.Failure(Error.Conflict( "CouponCode.NotExpirable", $"사용 가능(Available) 또는 발급됨(Issued) 상태 코드만 만료 처리할 수 있습니다. 대상 외 코드: {preview}{more}" )); } // Issued 출발 코드에 연결된 MemberInventory 를 같은 트랜잭션에서 즉시 만료 var issuedCodeIDs = targets.Where(c => c.Status == CouponCodeStatus.Issued).Select(c => c.ID).ToList(); if (issuedCodeIDs.Count > 0) { var inventories = await db.MemberInventory .Where(i => issuedCodeIDs.Contains(i.CouponCodeID) && i.UsedAt == null) .ToListAsync(ct); foreach (var inv in inventories) { inv.ForceExpire(); } } foreach (var code in targets) { code.Expire(); } await db.SaveChangesAsync(ct); return Result.Success(new Response(targets.Count)); } private static string StatusText(CouponCodeStatus status) => status switch { CouponCodeStatus.Available => "사용 가능", CouponCodeStatus.Reserved => "결제 진행 중", CouponCodeStatus.Issued => "발급됨", CouponCodeStatus.Used => "사용됨", CouponCodeStatus.Expired => "만료", _ => status.ToString() }; }