SeibroForeignSyncTests.cs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. using Application.Helpers;
  2. using Domain.Entities.Stocks;
  3. using Infrastructure.StockData;
  4. using Microsoft.EntityFrameworkCore;
  5. namespace Application.Tests;
  6. /// <summary>
  7. /// SeibroForeignSyncService 의 DB 반영 로직(SeibroForeignImport) + 종목별 대기(ForeignIsins 빈) 검증 —
  8. /// HTTP 는 파서 입력을 fixture 로 대체(SeibroDerivSyncTests 방식 미러링). 실제 SQL Server(LocalDB).
  9. /// 국가별 보관/결제 날짜×국가 스윕 upsert + 종목별 보관/결제 upsert(시드 ISIN 시) + 종목별은 빈 리스트 시 콜 0 + Foreign 예산 상한.
  10. /// 공유 DB 오염 방지 위해 고유 국가코드·ISIN·날짜를 쓴다.
  11. /// </summary>
  12. [TestClass]
  13. public sealed class SeibroForeignSyncTests
  14. {
  15. [TestMethod]
  16. public async Task UpsertCustodyNation_FromSampleXml_StampsStdDtAndNation_ThenIdempotent()
  17. {
  18. // 기대 행위: getNationFrsecCusInfo(4행) — 응답에 없는 STD_DT·NATION_CD 요청값 스탬핑, UQ(StdDt,NationCd,SecnTpcd) upsert.
  19. var rows = SeibroNationFrsecCusParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getNationFrsecCusInfo.xml")));
  20. var stdDt = new DateOnly(2018, 8, 6);
  21. var nation = $"T{Guid.NewGuid().ToString("N")[..1]}"; // 2자 고유 국가코드
  22. using (var db = TestDb.Create())
  23. {
  24. var (inserted, _) = await SeibroForeignImport.UpsertCustodyNationAsync(db, stdDt, nation, rows, CancellationToken.None);
  25. Assert.AreEqual(4, inserted, "종목구분 4행 insert");
  26. }
  27. using (var db = TestDb.Create())
  28. {
  29. var (inserted, updated) = await SeibroForeignImport.UpsertCustodyNationAsync(db, stdDt, nation, rows, CancellationToken.None);
  30. Assert.AreEqual(0, inserted, "재실행 idempotent");
  31. Assert.AreEqual(0, updated, "무변경 no-op");
  32. var stock = await db.ForeignCustodyNation.AsNoTracking().Where(c => c.StdDt == stdDt && c.NationCd == nation && c.SecnTpcd == "71").SingleAsync();
  33. Assert.AreEqual(5509745879.73m, stock.FrsecCusAmt, "외화주식 보관금액 USD");
  34. }
  35. }
  36. [TestMethod]
  37. public async Task UpsertSettlementNation_FromSampleXml_StampsSetlDtAndNation()
  38. {
  39. // 기대 행위: getNationFrsecSetlInfo(7행) — SETL_DT·NATION_CD 스탬핑, UQ(SetlDt,NationCd,IntlBizCacd,SecnTpcd) upsert.
  40. var rows = SeibroNationFrsecSetlParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getNationFrsecSetlInfo.xml")));
  41. var setlDt = new DateOnly(2018, 8, 6);
  42. var nation = $"U{Guid.NewGuid().ToString("N")[..1]}";
  43. using (var db = TestDb.Create())
  44. {
  45. var (inserted, _) = await SeibroForeignImport.UpsertSettlementNationAsync(db, setlDt, nation, rows, CancellationToken.None);
  46. Assert.AreEqual(7, inserted, "매수·매도 7행 insert");
  47. }
  48. using (var db = TestDb.Create())
  49. {
  50. var (inserted, _) = await SeibroForeignImport.UpsertSettlementNationAsync(db, setlDt, nation, rows, CancellationToken.None);
  51. Assert.AreEqual(0, inserted, "재실행 idempotent");
  52. var buyStock = await db.ForeignSettlementNation.AsNoTracking().Where(c => c.SetlDt == setlDt && c.NationCd == nation && c.IntlBizCacd == "1110" && c.SecnTpcd == "71").SingleAsync();
  53. Assert.AreEqual(1591L, buyStock.SetlCount, "매수 결제건수");
  54. Assert.AreEqual(37289660.18m, buyStock.FrsecSetlAmt, "결제금액 USD");
  55. }
  56. }
  57. [TestMethod]
  58. public async Task UpsertCustodySecurity_FromSampleXml_StampsStdDtAndIsin_AllNationRows()
  59. {
  60. // 기대 행위 (리뷰 결함 #3 해소): getSecnFrsecCusInfo(2행 DE+US) — STD_DT·ISIN 스탬핑, UQ(StdDt,Isin,NationCd,SecnTpcd) 전 국가별 행 반영.
  61. // 기존 (StdDt,Isin) UQ 는 첫 행(DE) 만 반영해 US 를 무음 병합했다.
  62. var rows = SeibroSecnFrsecCusParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getSecnFrsecCusInfo.xml")));
  63. var stdDt = new DateOnly(2018, 8, 6);
  64. var isin = $"US{Guid.NewGuid().ToString("N")[..10]}";
  65. using (var db = TestDb.Create())
  66. {
  67. Assert.IsTrue(await SeibroForeignImport.UpsertCustodySecurityAsync(db, stdDt, isin, rows, CancellationToken.None));
  68. }
  69. using (var db = TestDb.Create())
  70. {
  71. Assert.IsTrue(await SeibroForeignImport.UpsertCustodySecurityAsync(db, stdDt, isin, rows, CancellationToken.None), "재수집 idempotent upsert");
  72. var saved = await db.ForeignCustodySecurity.AsNoTracking().Where(c => c.StdDt == stdDt && c.Isin == isin).ToListAsync();
  73. Assert.AreEqual(2, saved.Count, "DE+US 국가별 2행 모두 저장 (첫 행만 반영하던 결함 해소)");
  74. var de = saved.Single(c => c.NationCd == "DE");
  75. Assert.AreEqual(10812m, de.FrsecTotHoldAmt);
  76. Assert.AreEqual(6m, de.FrsecTotHoldQty);
  77. var us = saved.Single(c => c.NationCd == "US");
  78. Assert.AreEqual(737881960m, us.FrsecTotHoldAmt, "US 행도 별도 저장 (무음 병합 안 됨)");
  79. Assert.AreEqual(404778m, us.FrsecTotHoldQty);
  80. }
  81. }
  82. [TestMethod]
  83. public async Task UpsertSettlementSecurity_FromSampleXml_StampsProcDtAndIsin_BuySellRows()
  84. {
  85. // 기대 행위: getSecnFrsecSetlInfo(2행) — PROC_DT·ISIN 스탬핑, UQ(ProcDt,Isin,IntlBizCacd) 매수·매도 2행.
  86. var rows = SeibroSecnFrsecSetlParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getSecnFrsecSetlInfo.xml")));
  87. var procDt = new DateOnly(2018, 8, 6);
  88. var isin = $"US{Guid.NewGuid().ToString("N")[..10]}";
  89. using (var db = TestDb.Create())
  90. {
  91. var (inserted, _) = await SeibroForeignImport.UpsertSettlementSecurityAsync(db, procDt, isin, rows, CancellationToken.None);
  92. Assert.AreEqual(2, inserted, "매수·매도 2행");
  93. }
  94. using (var db = TestDb.Create())
  95. {
  96. var (inserted, _) = await SeibroForeignImport.UpsertSettlementSecurityAsync(db, procDt, isin, rows, CancellationToken.None);
  97. Assert.AreEqual(0, inserted, "재실행 idempotent");
  98. var buy = await db.ForeignSettlementSecurity.AsNoTracking().Where(c => c.ProcDt == procDt && c.Isin == isin && c.IntlBizCacd == "1110").SingleAsync();
  99. Assert.AreEqual(1571m, buy.SetlAskQty);
  100. Assert.AreEqual(2804130m, buy.SetlAmt, "매수 결제금액 USD");
  101. }
  102. }
  103. [TestMethod]
  104. public async Task EmptyRows_ForeignImport_IsNoOp()
  105. {
  106. // 기대 행위: ForeignIsins 빈 → 종목별 콜 0. 파서가 빈결과일 때 Import 도 no-op(0행) 이어야 배치가 정상 통과.
  107. var stdDt = new DateOnly(2018, 8, 6);
  108. var isin = $"US{Guid.NewGuid().ToString("N")[..10]}";
  109. using var db = TestDb.Create();
  110. var (custInserted, _) = await SeibroForeignImport.UpsertCustodyNationAsync(db, stdDt, "ZZ", [], CancellationToken.None);
  111. Assert.AreEqual(0, custInserted, "빈 결과 no-op");
  112. Assert.IsFalse(await SeibroForeignImport.UpsertCustodySecurityAsync(db, stdDt, isin, [], CancellationToken.None), "종목별 보관 빈결과 no-op");
  113. var (setlInserted, _) = await SeibroForeignImport.UpsertSettlementSecurityAsync(db, stdDt, isin, [], CancellationToken.None);
  114. Assert.AreEqual(0, setlInserted, "종목별 결제 빈결과 no-op");
  115. }
  116. [TestMethod]
  117. public void Quota_ForeignBudget_CapsConsumption()
  118. {
  119. // 기대 행위: 외화 예산 상한 — 소진 후 추가 소비 거부, 다른 카테고리 무영향.
  120. var quota = new SeibroQuota(() => new DateOnly(2026, 7, 7));
  121. Assert.IsTrue(quota.TryConsume(SeibroCategory.Foreign, 15000, 15000), "예산 전량 소비");
  122. Assert.IsFalse(quota.TryConsume(SeibroCategory.Foreign, 1, 15000), "외화 예산 소진");
  123. Assert.AreEqual(0, quota.Remaining(SeibroCategory.Foreign, 15000));
  124. Assert.AreEqual(8000, quota.Remaining(SeibroCategory.Deriv, 8000), "파생 예산 무영향");
  125. }
  126. }