using Domain.Entities.Stocks; using Domain.Entities.Stocks.ValueObject; using LendingHandler = Application.Features.Api.Stocks.GetLendingHistory.Handler; using LendingQuery = Application.Features.Api.Stocks.GetLendingHistory.Query; using LockupHandler = Application.Features.Api.Stocks.GetLockupCalendar.Handler; using LockupQuery = Application.Features.Api.Stocks.GetLockupCalendar.Query; namespace Application.Tests; [TestClass] public sealed class SeibroSupplyQueryHandlerTests { [TestMethod] public async Task GetLendingHistory_ByShortCode_FiltersPeriod_OrdersDesc() { // 기대 행위: 단축코드 → Stock.ISIN 로 SecuritiesLending 조회 + from~to 필터 + 최신순 var code = $"72{Random.Shared.Next(1000, 9999)}"; var isin = $"KR7{code}003"; using var db = TestDb.Create(); var stock = Stock.Create(code, "대차종목", StockMarket.KOSPI, new DateOnly(2020, 1, 2), isin: isin); await db.Stock.AddAsync(stock); await db.SecuritiesLending.AddAsync(SecuritiesLending.Create(isin, new DateOnly(2025, 3, 3), 1000L, 100L, 50L, 10L, 500L, 300L, 1.5m, 100L, 0.3m, 400L, 1.2m, 90L, 0.2m)); await db.SecuritiesLending.AddAsync(SecuritiesLending.Create(isin, new DateOnly(2025, 3, 4), 1000L, 120L, 60L, 20L, 540L, 320L, 1.6m, 110L, 0.35m, 410L, 1.25m, 95L, 0.22m)); await db.SecuritiesLending.AddAsync(SecuritiesLending.Create(isin, new DateOnly(2025, 1, 2), 1000L, 80L, 40L, 5L, 480L, 280L, 1.4m, 90L, 0.28m, 380L, 1.1m, 85L, 0.18m)); await db.SaveChangesAsync(default); var handler = new LendingHandler(db); var result = await handler.Handle(new LendingQuery(code, new DateOnly(2025, 3, 1), new DateOnly(2025, 3, 31), 1, 100), default); Assert.IsTrue(result.IsSuccess); Assert.AreEqual(isin, result.Value.Isin); Assert.AreEqual(2, result.Value.Total, "1월 행은 범위 밖 → 제외"); Assert.AreEqual(new DateOnly(2025, 3, 4), result.Value.List[0].StdDt, "최신순"); Assert.AreEqual(1.6m, result.Value.List[0].ForeignLendRatio); Assert.AreEqual(540L, result.Value.List[0].LendingBalanceQty); } [TestMethod] public async Task GetLendingHistory_ByIsin_WhenNoStockRow() { // 기대 행위: 6자리 단축코드가 Stock 에 없으면 입력을 ISIN 으로 간주 var isin = $"KR7{Guid.NewGuid().ToString("N")[..9]}"; using var db = TestDb.Create(); await db.SecuritiesLending.AddAsync(SecuritiesLending.Create(isin, new DateOnly(2024, 6, 6), 2000L, 200L, 100L, 30L, 900L, 600L, 2.1m, 200L, 0.5m, 700L, 1.8m, 180L, 0.4m)); await db.SaveChangesAsync(default); var handler = new LendingHandler(db); var result = await handler.Handle(new LendingQuery(isin, null, null, 1, 100), default); Assert.IsTrue(result.IsSuccess); Assert.AreEqual(isin, result.Value.Isin); Assert.AreEqual(1, result.Value.Total); Assert.AreEqual(2.1m, result.Value.List[0].ForeignLendRatio); } [TestMethod] public async Task GetLockupCalendar_FiltersByReturnOrSafedpDate() { // 기대 행위: ReturnDt 또는 SafedpDt 가 기간 안이면 포함. 범위 밖 예수 이벤트는 제외. var custno = 800100 + Random.Shared.Next(1, 800); var code = $"73{Random.Shared.Next(1000, 9999)}"; using var db = TestDb.Create(); // 반환일이 기간 안 (락업 해제) await db.LockupEvent.AddAsync(LockupEvent.Create(custno, "락업종목", "보통주", code, "11", "2", new DateOnly(2024, 3, 1), 100000L, "03", new DateOnly(2025, 3, 1), 100000L, 5000000L)); // 예수일이 기간 안 await db.LockupEvent.AddAsync(LockupEvent.Create(custno, "락업종목", "보통주", code, "11", "1", new DateOnly(2025, 5, 15), 50000L, "01", null, null, 5000000L)); // 둘 다 기간 밖 await db.LockupEvent.AddAsync(LockupEvent.Create(custno, "락업종목", "보통주", code, "11", "1", new DateOnly(2023, 1, 1), 30000L, "02", null, null, 5000000L)); await db.SaveChangesAsync(default); var handler = new LockupHandler(db); var result = await handler.Handle(new LockupQuery(new DateOnly(2025, 1, 1), new DateOnly(2025, 12, 31), 1, 50), default); Assert.IsTrue(result.IsSuccess); var mine = result.Value.List.Where(c => c.IssucoCustno == custno).ToList(); Assert.AreEqual(2, mine.Count, "반환일(2025-03) + 예수일(2025-05) 만. 2023 예수는 제외"); Assert.IsTrue(mine.Any(c => c.ReturnDt == new DateOnly(2025, 3, 1) && c.OccrSeq == "2")); Assert.IsTrue(mine.Any(c => c.SafedpDt == new DateOnly(2025, 5, 15) && c.OccrSeq == "1")); } [TestMethod] public async Task GetLockupCalendar_SafedpBeforeFrom_ReturnAfterTo_IsExcluded() { // 회귀 (리뷰 결함 #2): SafedpDtto 인 행은 대표일(ReturnDt) 이 범위 밖이라 제외돼야 한다. // 기존 코드는 from/to 를 각각 (Safedp OR Return) 로 걸어 서로 다른 필드가 각 절을 따로 만족 → 범위 밖 이벤트 누출. var custno = 850100 + Random.Shared.Next(1, 800); var code = $"74{Random.Shared.Next(1000, 9999)}"; using var db = TestDb.Create(); // Safedp=2019(< from 2025) & Return=2030(> to 2025) — 대표일 2030 은 범위 밖 → 제외돼야 함 await db.LockupEvent.AddAsync(LockupEvent.Create(custno, "누출종목", "보통주", code, "11", "1", new DateOnly(2019, 1, 1), 10000L, "01", new DateOnly(2030, 1, 1), 10000L, 5000000L)); // 대조군: 대표일(Return=2025-06) 이 범위 안 → 포함 await db.LockupEvent.AddAsync(LockupEvent.Create(custno, "정상종목", "보통주", code, "11", "2", new DateOnly(2024, 1, 1), 20000L, "03", new DateOnly(2025, 6, 1), 20000L, 5000000L)); await db.SaveChangesAsync(default); var handler = new LockupHandler(db); var result = await handler.Handle(new LockupQuery(new DateOnly(2025, 1, 1), new DateOnly(2025, 12, 31), 1, 50), default); Assert.IsTrue(result.IsSuccess); var mine = result.Value.List.Where(c => c.IssucoCustno == custno).ToList(); Assert.AreEqual(1, mine.Count, "대표일 범위 밖(2019~2030 걸침) 은 제외, 대표일 범위 안(2025-06) 만 포함"); Assert.AreEqual(new DateOnly(2025, 6, 1), mine[0].ReturnDt); } }