| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- using Application.Helpers;
- using Domain.Entities.Stocks;
- using Infrastructure.StockData;
- using Microsoft.EntityFrameworkCore;
- namespace Application.Tests;
- /// <summary>
- /// SeibroForeignSyncService 의 DB 반영 로직(SeibroForeignImport) + 종목별 대기(ForeignIsins 빈) 검증 —
- /// HTTP 는 파서 입력을 fixture 로 대체(SeibroDerivSyncTests 방식 미러링). 실제 SQL Server(LocalDB).
- /// 국가별 보관/결제 날짜×국가 스윕 upsert + 종목별 보관/결제 upsert(시드 ISIN 시) + 종목별은 빈 리스트 시 콜 0 + Foreign 예산 상한.
- /// 공유 DB 오염 방지 위해 고유 국가코드·ISIN·날짜를 쓴다.
- /// </summary>
- [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), "파생 예산 무영향");
- }
- }
|