using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Domain.Entities.Store.ValueObject; using Microsoft.EntityFrameworkCore; using SharedKernel.Results; namespace Application.Features.Api.Store.Inventory.Use; internal sealed class Handler(IAppDbContext db) : ICommandHandler> { public async Task> Handle(Command request, CancellationToken ct) { var inv = await db.MemberInventory .Include(i => i.CouponCode).ThenInclude(c => c!.Coupon) .FirstOrDefaultAsync(i => i.ID == request.InventoryID, ct); if (inv is null) { return Result.Failure(Error.NotFound("Inventory.NotFound", "보관함 항목을 찾을 수 없습니다.")); } // 본인 소유 검증 + 소프트 삭제 검증 if (inv.MemberID != request.MemberID || inv.DeletedAt is not null) { return Result.Failure(Error.NotFound("Inventory.NotFound", "보관함 항목을 찾을 수 없습니다.")); } if (inv.UsedAt is not null) { return Result.Failure(Error.Conflict("Inventory.AlreadyUsed", "이미 사용한 쿠폰입니다.")); } if (inv.ExpiresAt is not null && inv.ExpiresAt.Value < DateTime.UtcNow) { return Result.Failure(Error.Problem("Inventory.Expired", "만료된 쿠폰입니다.")); } if (inv.CouponCode is null || inv.CouponCode.Coupon is null) { return Result.Failure(Error.Problem("Inventory.InvalidState", "쿠폰 코드 정보를 찾을 수 없습니다.")); } // 관리자에 의해 코드 풀에서 만료 처리된 코드는 사용 차단 (Defense in depth — 통상 ExpiresAt 가드에서 걸림) if (inv.CouponCode.Status == CouponCodeStatus.Expired) { return Result.Failure(Error.Problem("Inventory.Expired", "만료된 쿠폰입니다.")); } inv.MarkUsed(); inv.CouponCode.MarkUsed(); inv.CouponCode.Coupon.IncreaseUsedCount(); await db.SaveChangesAsync(ct); return Result.Success(new Response(inv.CouponCode.Code, inv.UsedAt!.Value)); } }