using Application.Abstractions.Data;
using Application.Abstractions.Messaging;
using Microsoft.EntityFrameworkCore;
using SharedKernel.Results;
namespace Application.Features.Api.Stocks.Watch.Toggle;
///
/// 관심종목 토글 — 회원×종목코드가 있으면 해제, 없으면 등록. Stock.Code 활성 존재를 AnyAsync 로 사전 검증(FK 없이 denorm). (MemberID,StockCode) UNIQUE.
///
public sealed class Handler(IAppDbContext db) : ICommandHandler>
{
public async Task> Handle(Command request, CancellationToken ct)
{
var code = request.StockCode?.Trim() ?? "";
var existing = await db.StockWatch.FirstOrDefaultAsync(c => c.MemberID == request.MemberID && c.StockCode == code, ct);
bool isWatched;
if (existing is not null)
{
// 이미 관심 등록된 종목은 상장폐지(IsActive=false)돼도 항상 해제 가능해야 하므로 존재검증을 건너뛴다
db.StockWatch.Remove(existing);
isWatched = false;
}
else
{
// 신규 등록만 종목 존재(활성) 검증 — FK 없이 denorm 이라 사전 AnyAsync
var exists = await db.Stock.AsNoTracking().AnyAsync(c => c.Code == code && c.IsActive, ct);
if (!exists)
{
return Result.Failure(Error.NotFound("StockWatch.StockNotFound", "종목을 찾을 수 없습니다."));
}
var watch = Domain.Entities.Stocks.StockWatch.Create(request.MemberID, code);
await db.StockWatch.AddAsync(watch, ct);
isWatched = true;
}
try
{
await db.SaveChangesAsync(ct);
}
catch (DbUpdateConcurrencyException) when (existing is not null)
{
// 동시 해제 경합(대상 행이 이미 삭제됨) — idempotent 성공 처리
return new Response(false);
}
catch (DbUpdateException) when (existing is null)
{
// 동시 등록 경합(UNIQUE 위반) — 다른 요청이 이미 등록 완료 — idempotent 성공 처리
return new Response(true);
}
return new Response(isWatched);
}
}