SeibroForeignSyncTests.cs 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  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_FirstRowOnly()
  59. {
  60. // 기대 행위: getSecnFrsecCusInfo(2행) — STD_DT·ISIN 스탬핑, UQ(StdDt,Isin) 종목당 1행(첫 행만).
  61. var rows = SeibroSecnFrsecCusParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getSecnFrsecCusInfo.xml")));
  62. var stdDt = new DateOnly(2018, 8, 6);
  63. var isin = $"US{Guid.NewGuid().ToString("N")[..10]}";
  64. using (var db = TestDb.Create())
  65. {
  66. Assert.IsTrue(await SeibroForeignImport.UpsertCustodySecurityAsync(db, stdDt, isin, rows, CancellationToken.None));
  67. }
  68. using (var db = TestDb.Create())
  69. {
  70. Assert.IsTrue(await SeibroForeignImport.UpsertCustodySecurityAsync(db, stdDt, isin, rows, CancellationToken.None), "재수집 idempotent upsert");
  71. var saved = await db.ForeignCustodySecurity.AsNoTracking().Where(c => c.StdDt == stdDt && c.Isin == isin).SingleAsync();
  72. Assert.AreEqual("DE", saved.NationCd, "첫 행(DE) 반영");
  73. Assert.AreEqual(10812m, saved.FrsecTotHoldAmt);
  74. Assert.AreEqual(6m, saved.FrsecTotHoldQty);
  75. }
  76. }
  77. [TestMethod]
  78. public async Task UpsertSettlementSecurity_FromSampleXml_StampsProcDtAndIsin_BuySellRows()
  79. {
  80. // 기대 행위: getSecnFrsecSetlInfo(2행) — PROC_DT·ISIN 스탬핑, UQ(ProcDt,Isin,IntlBizCacd) 매수·매도 2행.
  81. var rows = SeibroSecnFrsecSetlParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getSecnFrsecSetlInfo.xml")));
  82. var procDt = new DateOnly(2018, 8, 6);
  83. var isin = $"US{Guid.NewGuid().ToString("N")[..10]}";
  84. using (var db = TestDb.Create())
  85. {
  86. var (inserted, _) = await SeibroForeignImport.UpsertSettlementSecurityAsync(db, procDt, isin, rows, CancellationToken.None);
  87. Assert.AreEqual(2, inserted, "매수·매도 2행");
  88. }
  89. using (var db = TestDb.Create())
  90. {
  91. var (inserted, _) = await SeibroForeignImport.UpsertSettlementSecurityAsync(db, procDt, isin, rows, CancellationToken.None);
  92. Assert.AreEqual(0, inserted, "재실행 idempotent");
  93. var buy = await db.ForeignSettlementSecurity.AsNoTracking().Where(c => c.ProcDt == procDt && c.Isin == isin && c.IntlBizCacd == "1110").SingleAsync();
  94. Assert.AreEqual(1571m, buy.SetlAskQty);
  95. Assert.AreEqual(2804130m, buy.SetlAmt, "매수 결제금액 USD");
  96. }
  97. }
  98. [TestMethod]
  99. public async Task EmptyRows_ForeignImport_IsNoOp()
  100. {
  101. // 기대 행위: ForeignIsins 빈 → 종목별 콜 0. 파서가 빈결과일 때 Import 도 no-op(0행) 이어야 배치가 정상 통과.
  102. var stdDt = new DateOnly(2018, 8, 6);
  103. var isin = $"US{Guid.NewGuid().ToString("N")[..10]}";
  104. using var db = TestDb.Create();
  105. var (custInserted, _) = await SeibroForeignImport.UpsertCustodyNationAsync(db, stdDt, "ZZ", [], CancellationToken.None);
  106. Assert.AreEqual(0, custInserted, "빈 결과 no-op");
  107. Assert.IsFalse(await SeibroForeignImport.UpsertCustodySecurityAsync(db, stdDt, isin, [], CancellationToken.None), "종목별 보관 빈결과 no-op");
  108. var (setlInserted, _) = await SeibroForeignImport.UpsertSettlementSecurityAsync(db, stdDt, isin, [], CancellationToken.None);
  109. Assert.AreEqual(0, setlInserted, "종목별 결제 빈결과 no-op");
  110. }
  111. [TestMethod]
  112. public void Quota_ForeignBudget_CapsConsumption()
  113. {
  114. // 기대 행위: 외화 예산 상한 — 소진 후 추가 소비 거부, 다른 카테고리 무영향.
  115. var quota = new SeibroQuota(() => new DateOnly(2026, 7, 7));
  116. Assert.IsTrue(quota.TryConsume(SeibroCategory.Foreign, 15000, 15000), "예산 전량 소비");
  117. Assert.IsFalse(quota.TryConsume(SeibroCategory.Foreign, 1, 15000), "외화 예산 소진");
  118. Assert.AreEqual(0, quota.Remaining(SeibroCategory.Foreign, 15000));
  119. Assert.AreEqual(8000, quota.Remaining(SeibroCategory.Deriv, 8000), "파생 예산 무영향");
  120. }
  121. }