using Application.Helpers; using Domain.Entities.Stocks; using Infrastructure.StockData; using Microsoft.EntityFrameworkCore; namespace Application.Tests; /// /// SeibroForeignSyncService 의 DB 반영 로직(SeibroForeignImport) + 종목별 대기(ForeignIsins 빈) 검증 — /// HTTP 는 파서 입력을 fixture 로 대체(SeibroDerivSyncTests 방식 미러링). 실제 SQL Server(LocalDB). /// 국가별 보관/결제 날짜×국가 스윕 upsert + 종목별 보관/결제 upsert(시드 ISIN 시) + 종목별은 빈 리스트 시 콜 0 + Foreign 예산 상한. /// 공유 DB 오염 방지 위해 고유 국가코드·ISIN·날짜를 쓴다. /// [TestClass] public sealed class SeibroForeignSyncTests { [TestMethod] public async Task UpsertCustodyNation_FromSampleXml_StampsStdDtAndNation_ThenIdempotent() { // 기대 행위: getNationFrsecCusInfo(4행) — 응답에 없는 STD_DT·NATION_CD 요청값 스탬핑, UQ(StdDt,NationCd,SecnTpcd) upsert. var rows = SeibroNationFrsecCusParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getNationFrsecCusInfo.xml"))); var stdDt = new DateOnly(2018, 8, 6); var nation = $"T{Guid.NewGuid().ToString("N")[..1]}"; // 2자 고유 국가코드 using (var db = TestDb.Create()) { var (inserted, _) = await SeibroForeignImport.UpsertCustodyNationAsync(db, stdDt, nation, rows, CancellationToken.None); Assert.AreEqual(4, inserted, "종목구분 4행 insert"); } using (var db = TestDb.Create()) { var (inserted, updated) = await SeibroForeignImport.UpsertCustodyNationAsync(db, stdDt, nation, rows, CancellationToken.None); Assert.AreEqual(0, inserted, "재실행 idempotent"); Assert.AreEqual(0, updated, "무변경 no-op"); var stock = await db.ForeignCustodyNation.AsNoTracking().Where(c => c.StdDt == stdDt && c.NationCd == nation && c.SecnTpcd == "71").SingleAsync(); Assert.AreEqual(5509745879.73m, stock.FrsecCusAmt, "외화주식 보관금액 USD"); } } [TestMethod] public async Task UpsertSettlementNation_FromSampleXml_StampsSetlDtAndNation() { // 기대 행위: getNationFrsecSetlInfo(7행) — SETL_DT·NATION_CD 스탬핑, UQ(SetlDt,NationCd,IntlBizCacd,SecnTpcd) upsert. var rows = SeibroNationFrsecSetlParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getNationFrsecSetlInfo.xml"))); var setlDt = new DateOnly(2018, 8, 6); var nation = $"U{Guid.NewGuid().ToString("N")[..1]}"; using (var db = TestDb.Create()) { var (inserted, _) = await SeibroForeignImport.UpsertSettlementNationAsync(db, setlDt, nation, rows, CancellationToken.None); Assert.AreEqual(7, inserted, "매수·매도 7행 insert"); } using (var db = TestDb.Create()) { var (inserted, _) = await SeibroForeignImport.UpsertSettlementNationAsync(db, setlDt, nation, rows, CancellationToken.None); Assert.AreEqual(0, inserted, "재실행 idempotent"); var buyStock = await db.ForeignSettlementNation.AsNoTracking().Where(c => c.SetlDt == setlDt && c.NationCd == nation && c.IntlBizCacd == "1110" && c.SecnTpcd == "71").SingleAsync(); Assert.AreEqual(1591L, buyStock.SetlCount, "매수 결제건수"); Assert.AreEqual(37289660.18m, buyStock.FrsecSetlAmt, "결제금액 USD"); } } [TestMethod] public async Task UpsertCustodySecurity_FromSampleXml_StampsStdDtAndIsin_AllNationRows() { // 기대 행위 (리뷰 결함 #3 해소): getSecnFrsecCusInfo(2행 DE+US) — STD_DT·ISIN 스탬핑, UQ(StdDt,Isin,NationCd,SecnTpcd) 전 국가별 행 반영. // 기존 (StdDt,Isin) UQ 는 첫 행(DE) 만 반영해 US 를 무음 병합했다. var rows = SeibroSecnFrsecCusParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getSecnFrsecCusInfo.xml"))); var stdDt = new DateOnly(2018, 8, 6); var isin = $"US{Guid.NewGuid().ToString("N")[..10]}"; using (var db = TestDb.Create()) { Assert.IsTrue(await SeibroForeignImport.UpsertCustodySecurityAsync(db, stdDt, isin, rows, CancellationToken.None)); } using (var db = TestDb.Create()) { Assert.IsTrue(await SeibroForeignImport.UpsertCustodySecurityAsync(db, stdDt, isin, rows, CancellationToken.None), "재수집 idempotent upsert"); var saved = await db.ForeignCustodySecurity.AsNoTracking().Where(c => c.StdDt == stdDt && c.Isin == isin).ToListAsync(); Assert.AreEqual(2, saved.Count, "DE+US 국가별 2행 모두 저장 (첫 행만 반영하던 결함 해소)"); var de = saved.Single(c => c.NationCd == "DE"); Assert.AreEqual(10812m, de.FrsecTotHoldAmt); Assert.AreEqual(6m, de.FrsecTotHoldQty); var us = saved.Single(c => c.NationCd == "US"); Assert.AreEqual(737881960m, us.FrsecTotHoldAmt, "US 행도 별도 저장 (무음 병합 안 됨)"); Assert.AreEqual(404778m, us.FrsecTotHoldQty); } } [TestMethod] public async Task UpsertSettlementSecurity_FromSampleXml_StampsProcDtAndIsin_BuySellRows() { // 기대 행위: getSecnFrsecSetlInfo(2행) — PROC_DT·ISIN 스탬핑, UQ(ProcDt,Isin,IntlBizCacd) 매수·매도 2행. var rows = SeibroSecnFrsecSetlParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getSecnFrsecSetlInfo.xml"))); var procDt = new DateOnly(2018, 8, 6); var isin = $"US{Guid.NewGuid().ToString("N")[..10]}"; using (var db = TestDb.Create()) { var (inserted, _) = await SeibroForeignImport.UpsertSettlementSecurityAsync(db, procDt, isin, rows, CancellationToken.None); Assert.AreEqual(2, inserted, "매수·매도 2행"); } using (var db = TestDb.Create()) { var (inserted, _) = await SeibroForeignImport.UpsertSettlementSecurityAsync(db, procDt, isin, rows, CancellationToken.None); Assert.AreEqual(0, inserted, "재실행 idempotent"); var buy = await db.ForeignSettlementSecurity.AsNoTracking().Where(c => c.ProcDt == procDt && c.Isin == isin && c.IntlBizCacd == "1110").SingleAsync(); Assert.AreEqual(1571m, buy.SetlAskQty); Assert.AreEqual(2804130m, buy.SetlAmt, "매수 결제금액 USD"); } } [TestMethod] public async Task EmptyRows_ForeignImport_IsNoOp() { // 기대 행위: ForeignIsins 빈 → 종목별 콜 0. 파서가 빈결과일 때 Import 도 no-op(0행) 이어야 배치가 정상 통과. var stdDt = new DateOnly(2018, 8, 6); var isin = $"US{Guid.NewGuid().ToString("N")[..10]}"; using var db = TestDb.Create(); var (custInserted, _) = await SeibroForeignImport.UpsertCustodyNationAsync(db, stdDt, "ZZ", [], CancellationToken.None); Assert.AreEqual(0, custInserted, "빈 결과 no-op"); Assert.IsFalse(await SeibroForeignImport.UpsertCustodySecurityAsync(db, stdDt, isin, [], CancellationToken.None), "종목별 보관 빈결과 no-op"); var (setlInserted, _) = await SeibroForeignImport.UpsertSettlementSecurityAsync(db, stdDt, isin, [], CancellationToken.None); Assert.AreEqual(0, setlInserted, "종목별 결제 빈결과 no-op"); } [TestMethod] public void Quota_ForeignBudget_CapsConsumption() { // 기대 행위: 외화 예산 상한 — 소진 후 추가 소비 거부, 다른 카테고리 무영향. var quota = new SeibroQuota(() => new DateOnly(2026, 7, 7)); Assert.IsTrue(quota.TryConsume(SeibroCategory.Foreign, 15000, 15000), "예산 전량 소비"); Assert.IsFalse(quota.TryConsume(SeibroCategory.Foreign, 1, 15000), "외화 예산 소진"); Assert.AreEqual(0, quota.Remaining(SeibroCategory.Foreign, 15000)); Assert.AreEqual(8000, quota.Remaining(SeibroCategory.Deriv, 8000), "파생 예산 무영향"); } }