using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Domain.Entities.Paper.ValueObject; using Microsoft.EntityFrameworkCore; using SharedKernel.Results; namespace Application.Features.Api.Paper.CancelOrder; /// /// 모의투자 주문 취소 — 본인 계좌의 Pending 주문 && now < CancelableUntil 만 취소 (d4 §③/§⑤). /// 매수는 예약금 환원, 매도는 예약 수량 환원. /// internal sealed class Handler(IAppDbContext db) : ICommandHandler { public async Task Handle(Command request, CancellationToken ct) { var order = await db.PaperOrder .Include(c => c.Account) .FirstOrDefaultAsync(c => c.ID == request.OrderID, ct); if (order is null || order.Account.MemberID != request.MemberID) { return Result.Failure(Error.NotFound("Paper.OrderNotFound", "주문을 찾을 수 없습니다.")); } if (order.Status != PaperOrderStatus.Pending) { return Result.Failure(Error.Conflict("Paper.NotCancelable", "체결 대기 상태의 주문만 취소할 수 있습니다.")); } if (DateTime.UtcNow >= order.CancelableUntil) { return Result.Failure(Error.Conflict("Paper.CancelWindowClosed", "취소 가능 시한이 지났습니다.")); } if (!order.Cancel()) { return Result.Failure(Error.Conflict("Paper.NotCancelable", "주문을 취소할 수 없습니다.")); } if (order.Side == PaperOrderSide.Buy) { if (order.ReservedAmount > 0) { order.Account.ReleaseBuyReserve(order.ReservedAmount); } } else { var position = await db.PaperPosition.FirstOrDefaultAsync(c => c.AccountID == order.AccountID && c.StockCode == order.StockCode, ct); position?.ReleaseSellReserve(order.Quantity); } await db.SaveChangesAsync(ct); return Result.Success(); } }