| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- using Application.Helpers;
- using Domain.Entities.Stocks;
- using Domain.Entities.Stocks.ValueObject;
- using Infrastructure.StockData;
- using Microsoft.EntityFrameworkCore;
- namespace Application.Tests;
- /// <summary>
- /// SeibroBondSyncService 의 DB 반영 로직(SeibroBondImport) + rolling 대상 선정 통합 검증 —
- /// HTTP 는 파서 입력을 fixture/합성 XML 로 대체(SeibroCorpActionSyncTests 방식 미러링). 실제 SQL Server(LocalDB).
- /// 발행 upsert / 마스터 rolling(KRX BondDailyTrade ISIN 합집합 대상) / 이자 / 조기상환 window / 단기발행 스탬핑 / CD·CP·ESTB 매핑(SECN_TPCD 분기) / Bond 예산 상한.
- /// </summary>
- [TestClass]
- public sealed class SeibroBondSyncTests
- {
- [TestMethod]
- public async Task UpsertBondIssuances_FromSampleXml_ThenIdempotent()
- {
- // 기대 행위: getBondIssuInfo(1행) upsert — UQ Isin. 재실행 무변경.
- var rows = SeibroBondIssuParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getBondIssuInfo.xml")));
- using (var db = TestDb.Create())
- {
- var (inserted, _) = await SeibroBondImport.UpsertBondIssuancesAsync(db, rows, CancellationToken.None);
- Assert.AreEqual(1, inserted);
- }
- using (var db = TestDb.Create())
- {
- var (inserted, updated) = await SeibroBondImport.UpsertBondIssuancesAsync(db, rows, CancellationToken.None);
- Assert.AreEqual(0, inserted, "재실행 idempotent");
- Assert.AreEqual(0, updated, "무변경 no-op");
- var saved = await db.BondIssuance.AsNoTracking().Where(c => c.Isin == "KR6268761881").SingleAsync();
- Assert.AreEqual(new DateOnly(2018, 8, 3), saved.IssuDt);
- Assert.AreEqual(29998, saved.IssucoCustno);
- }
- }
- [TestMethod]
- public async Task UpsertBondMaster_FromSampleXml_StampsIsin_AndCreditGrades()
- {
- // 기대 행위: getBondStatInfo — 응답에 ISIN 없어 요청 ISIN 스탬핑, PK Isin upsert, 신용등급 4사 저장.
- var rows = SeibroBondStatParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getBondStatInfo.xml")));
- const string isin = "KR6268761881";
- using (var db = TestDb.Create())
- {
- var ok = await SeibroBondImport.UpsertBondMasterAsync(db, isin, rows, CancellationToken.None);
- Assert.IsTrue(ok);
- }
- using (var db = TestDb.Create())
- {
- var saved = await db.BondMaster.AsNoTracking().Where(c => c.Isin == isin).SingleAsync();
- Assert.AreEqual("SK해운 61", saved.KorSecnNm);
- Assert.AreEqual(new DateOnly(2020, 2, 3), saved.XpirDt, "만기");
- Assert.AreEqual(4.485m, saved.CouponRate, "표면이자율");
- Assert.AreEqual("2", saved.GrtyTpcd, "무보증");
- Assert.AreEqual("133", saved.KisValatGrdCd);
- Assert.AreEqual("133", saved.KrValatGrdCd);
- Assert.IsNull(saved.NiceValatGrdCd, "빈칸 → null");
- }
- }
- [TestMethod]
- public async Task BondMasterTarget_UnionsBondIssuanceAndKrxBondDailyTrade()
- {
- // 기대 행위(rolling 대상 선정): BondMaster 대상 = BondIssuance.Isin ∪ KRX BondDailyTrade.Code 중복 제거.
- var issuedOnly = $"KRA{Guid.NewGuid().ToString("N")[..9]}";
- var krxOnly = $"KRB{Guid.NewGuid().ToString("N")[..9]}";
- var both = $"KRC{Guid.NewGuid().ToString("N")[..9]}";
- using (var db = TestDb.Create())
- {
- await db.BondIssuance.AddAsync(BondIssuance.Create(new DateOnly(2018, 8, 3), 1, "발행사", issuedOnly, "발행전용"), CancellationToken.None);
- await db.BondIssuance.AddAsync(BondIssuance.Create(new DateOnly(2018, 8, 3), 1, "발행사", both, "양쪽"), CancellationToken.None);
- await db.BondDailyTrade.AddAsync(NewTrade(krxOnly), CancellationToken.None);
- await db.BondDailyTrade.AddAsync(NewTrade(both), CancellationToken.None);
- await db.SaveChangesAsync(CancellationToken.None);
- }
- using (var db = TestDb.Create())
- {
- var issued = await db.BondIssuance.AsNoTracking().Select(c => c.Isin).Distinct().ToListAsync();
- var krx = await db.BondDailyTrade.AsNoTracking().Select(c => c.Code).Distinct().ToListAsync();
- var universe = issued.Union(krx, StringComparer.Ordinal).ToHashSet();
- Assert.IsTrue(universe.Contains(issuedOnly), "발행만");
- Assert.IsTrue(universe.Contains(krxOnly), "KRX만");
- Assert.IsTrue(universe.Contains(both), "양쪽 포함");
- Assert.AreEqual(1, universe.Count(c => c == both), "중복 제거로 both 는 1회");
- }
- }
- [TestMethod]
- public async Task UpsertInterestPayment_SyntheticXml_StampsIsin_SnapshotUpdate()
- {
- // 빈 fixture 대응 — 합성 XML 로 PK Isin 스냅샷 upsert + 재실행 갱신 검증.
- const string xml = """
- <vector result="1">
- <data vectorkey="0" type="Document"><result>
- <COUPON_RATE value="4.485"/><INT_PAY_WAY_TPCD value="1"/><INT_PAY_CYCLE_TERMS value="3"/>
- <INT_PAY_CYCLE_TPCD value="23"/><INT_PAY_TIMS_TPCD value="01"/>
- <BEFORE_DATE value="20180803"/><AFTER_DATE value="20181103"/>
- </result></data>
- </vector>
- """;
- var rows = SeibroIntPayParser.Parse(SeibroXml.Parse(xml));
- const string isin = "KR6268761881";
- using (var db = TestDb.Create())
- {
- Assert.IsTrue(await SeibroBondImport.UpsertInterestPaymentAsync(db, isin, rows, CancellationToken.None));
- }
- using (var db = TestDb.Create())
- {
- Assert.IsTrue(await SeibroBondImport.UpsertInterestPaymentAsync(db, isin, rows, CancellationToken.None), "재수집 idempotent upsert");
- var saved = await db.BondInterestPayment.AsNoTracking().Where(c => c.Isin == isin).SingleAsync();
- Assert.AreEqual("23", saved.IntPayCycleTpcd, "개월 주기");
- Assert.AreEqual(new DateOnly(2018, 11, 3), saved.AfterDate);
- }
- }
- [TestMethod]
- public async Task ReplaceEarlyRedemptionWindow_FromSampleXml_ThenReconstructs()
- {
- // 기대 행위: getBondOptionXrcInfo — 조기상환일 window delete+insert (UQ Isin+ErlyRedDt). 재실행 창 재구성.
- var rows = SeibroBondOptionXrcParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getBondOptionXrcInfo.xml")));
- var day = new DateOnly(2018, 8, 24);
- using (var db = TestDb.Create())
- {
- var (inserted, deleted) = await SeibroBondImport.ReplaceEarlyRedemptionWindowAsync(db, day, day, rows, CancellationToken.None);
- Assert.AreEqual(1, inserted);
- Assert.AreEqual(0, deleted);
- }
- using (var db = TestDb.Create())
- {
- var (inserted, deleted) = await SeibroBondImport.ReplaceEarlyRedemptionWindowAsync(db, day, day, rows, CancellationToken.None);
- Assert.AreEqual(1, inserted, "창 재구성");
- Assert.AreEqual(1, deleted, "기존 창 삭제");
- var saved = await db.BondEarlyRedemption.AsNoTracking().Where(c => c.Isin == "KR6225693573" && c.ErlyRedDt == day).SingleAsync();
- Assert.AreEqual("9401", saved.OptionTpcd, "CALL");
- Assert.AreEqual(2000000000m, saved.ErlyRedamtVal);
- }
- }
- [TestMethod]
- public async Task UpsertShortTermIssuances_FromSampleXml_StampsSecnTpcdAndIssuDt()
- {
- // 기대 행위: getShortmIssuInfo(다수행) — 응답에 없는 SECN_TPCD·ISSU_DT 를 요청값 스탬핑 upsert (UQ Isin).
- var rows = SeibroShortmIssuParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getShortmIssuInfo.xml")));
- var issuDt = new DateOnly(2018, 8, 1);
- using (var db = TestDb.Create())
- {
- var (inserted, _) = await SeibroBondImport.UpsertShortTermIssuancesAsync(db, "12", issuDt, rows, CancellationToken.None);
- Assert.IsTrue(inserted > 1, "다수 종목 insert");
- }
- using (var db = TestDb.Create())
- {
- var (inserted, updated) = await SeibroBondImport.UpsertShortTermIssuancesAsync(db, "12", issuDt, rows, CancellationToken.None);
- Assert.AreEqual(0, inserted, "재실행 idempotent");
- Assert.AreEqual(0, updated);
- var saved = await db.ShortTermIssuance.AsNoTracking().Where(c => c.Isin == "KRZF00405288").SingleAsync();
- Assert.AreEqual("12", saved.SecnTpcd, "구분 스탬핑");
- Assert.AreEqual(issuDt, saved.IssuDt, "발행일 스탬핑");
- Assert.AreEqual(405, saved.IssucoCustno);
- }
- }
- [TestMethod]
- public async Task ShortTermMasterTarget_BranchesBySecnTpcd()
- {
- // 기대 행위(SECN_TPCD 분기): 13→CD, 12→CP, 14→ESTB 대상 분리. 각 마스터 매핑 검증.
- var cdIsin = $"KRD{Guid.NewGuid().ToString("N")[..9]}";
- var cpIsin = $"KRE{Guid.NewGuid().ToString("N")[..9]}";
- var estbIsin = $"KRF{Guid.NewGuid().ToString("N")[..9]}";
- var issuDt = new DateOnly(2018, 8, 1);
- using (var db = TestDb.Create())
- {
- await db.ShortTermIssuance.AddAsync(ShortTermIssuance.Create(cdIsin, "CD종목", 1, "발행", "13", issuDt), CancellationToken.None);
- await db.ShortTermIssuance.AddAsync(ShortTermIssuance.Create(cpIsin, "CP종목", 2, "발행", "12", issuDt), CancellationToken.None);
- await db.ShortTermIssuance.AddAsync(ShortTermIssuance.Create(estbIsin, "전단채", 3, "발행", "14", issuDt), CancellationToken.None);
- await db.SaveChangesAsync(CancellationToken.None);
- }
- using (var db = TestDb.Create())
- {
- var cdTargets = await db.ShortTermIssuance.AsNoTracking().Where(c => c.SecnTpcd == "13" && !db.CdMaster.Any(m => m.Isin == c.Isin)).Select(c => c.Isin).ToListAsync();
- var cpTargets = await db.ShortTermIssuance.AsNoTracking().Where(c => c.SecnTpcd == "12" && !db.CpMaster.Any(m => m.Isin == c.Isin)).Select(c => c.Isin).ToListAsync();
- var estbTargets = await db.ShortTermIssuance.AsNoTracking().Where(c => c.SecnTpcd == "14" && !db.EstbMaster.Any(m => m.Isin == c.Isin)).Select(c => c.Isin).ToListAsync();
- CollectionAssert.Contains(cdTargets, cdIsin);
- CollectionAssert.DoesNotContain(cdTargets, cpIsin, "CP 는 CD 대상 아님");
- CollectionAssert.Contains(cpTargets, cpIsin);
- CollectionAssert.Contains(estbTargets, estbIsin);
- // CP 마스터 매핑 (fixture)
- var cpRows = SeibroCPParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getCPInfo.xml")));
- Assert.IsTrue(await SeibroBondImport.UpsertCpMasterAsync(db, cpIsin, cpRows, CancellationToken.None));
- var cp = await db.CpMaster.AsNoTracking().Where(c => c.Isin == cpIsin).SingleAsync();
- Assert.AreEqual("112", cp.KisValatGrdCd);
- Assert.AreEqual("교보증권", cp.DiscnOrdNm);
- // ESTB 마스터 매핑 (fixture)
- var estbRows = SeibroESTBParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getESTBInfo.xml")));
- Assert.IsTrue(await SeibroBondImport.UpsertEstbMasterAsync(db, estbIsin, estbRows, CancellationToken.None));
- var estb = await db.EstbMaster.AsNoTracking().Where(c => c.Isin == estbIsin).SingleAsync();
- Assert.AreEqual("3", estb.EstbKacd, "PF AB");
- Assert.AreEqual("112", estb.NiceValatGrdCd);
- // CD 마스터 매핑 (합성 — 빈 fixture)
- const string cdXml = """
- <vector result="1"><data vectorkey="0" type="Document"><result>
- <ISSUCO_CUSTNO value="3061"/><FIRST_ISSU_AMT value="10000000000"/><DISCN_RATE value="1.85"/>
- <INT_PAY_MANN_TPCD value="1"/><ISSU_DT value="20180801"/><XPIR_DT value="20181031"/>
- </result></data></vector>
- """;
- var cdRows = SeibroCDParser.Parse(SeibroXml.Parse(cdXml));
- Assert.IsTrue(await SeibroBondImport.UpsertCdMasterAsync(db, cdIsin, cdRows, CancellationToken.None));
- var cd = await db.CdMaster.AsNoTracking().Where(c => c.Isin == cdIsin).SingleAsync();
- Assert.AreEqual(1.85m, cd.DiscnRate);
- Assert.AreEqual("1", cd.IntPayMannTpcd);
- }
- }
- [TestMethod]
- public void Quota_BondBudget_CapsConsumption()
- {
- // 기대 행위: 채권 예산 상한 — 소진 후 추가 소비 거부, 다른 카테고리 무영향.
- var quota = new SeibroQuota(() => new DateOnly(2026, 7, 7));
- Assert.IsTrue(quota.TryConsume(SeibroCategory.Bond, 8000, 8000), "예산 전량 소비");
- Assert.IsFalse(quota.TryConsume(SeibroCategory.Bond, 1, 8000), "채권 예산 소진");
- Assert.AreEqual(0, quota.Remaining(SeibroCategory.Bond, 8000));
- Assert.AreEqual(40000, quota.Remaining(SeibroCategory.Corp, 40000), "기업 예산 무영향");
- }
- private static BondDailyTrade NewTrade(string isin)
- {
- return BondDailyTrade.Create(BondMarket.General, isin, "채권매매", new DateOnly(2024, 1, 2), 10000m, 10000m, 10010m, 9990m, 5m, 3.5m, null, null, null, 100, 1000000);
- }
- }
|