| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- using Application.Helpers;
- using Domain.Entities.Stocks;
- using Infrastructure.StockData;
- using Microsoft.EntityFrameworkCore;
- namespace Application.Tests;
- /// <summary>
- /// SeibroCorpActionSyncService 의 DB 반영 로직(SeibroCorpActionImport) + 이벤트 구동 트리거 체인 통합 검증 —
- /// HTTP 는 파서 입력을 fixture 로 대체(SeibroSupplySyncTests 방식 미러링). 실제 SQL Server(LocalDB)로 확인.
- /// 총회 upsert / 안건 upsert / 상호변경 upsert / 대금 window / 단주 upsert / CB행사 window / CB대상 발견체인 / 예산 상한.
- /// </summary>
- [TestClass]
- public sealed class SeibroCorpActionSyncTests
- {
- [TestMethod]
- public async Task UpsertGeneralMeetings_FromSampleXml_ThenIdempotent()
- {
- // 기대 행위: getGmeetInfo(1행) upsert — UQ (custno, RgtStdDt, GmetKacd). 재실행 무변경.
- var rows = SeibroGmeetParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getGmeetInfo.xml")));
- using (var db = TestDb.Create())
- {
- var (inserted, _) = await SeibroCorpActionImport.UpsertGeneralMeetingsAsync(db, rows, CancellationToken.None);
- Assert.AreEqual(1, inserted);
- }
- using (var db = TestDb.Create())
- {
- var (inserted, updated) = await SeibroCorpActionImport.UpsertGeneralMeetingsAsync(db, rows, CancellationToken.None);
- Assert.AreEqual(0, inserted, "재실행 idempotent");
- Assert.AreEqual(0, updated, "무변경 no-op");
- var saved = await db.GeneralMeeting.AsNoTracking().Where(c => c.IssucoCustno == 593 && c.RgtStdDt == new DateOnly(2017, 12, 31) && c.GmetKacd == "001").SingleAsync();
- Assert.AreEqual(new DateOnly(2018, 3, 23), saved.GmetDt);
- Assert.IsFalse(saved.EvoteYn);
- }
- }
- [TestMethod]
- public async Task UpsertMeetingAgendas_FromSampleXml_KeepsAllElevenAgendas()
- {
- // 기대 행위: getGmeetMeasureInfo(11행) upsert — AGENDA_SEQ 로 구분 11행 모두 저장. 재실행 idempotent.
- var rows = SeibroGmeetMeasureParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getGmeetMeasureInfo.xml")));
- using (var db = TestDb.Create())
- {
- var (inserted, _) = await SeibroCorpActionImport.UpsertMeetingAgendasAsync(db, rows, CancellationToken.None);
- Assert.AreEqual(11, inserted);
- }
- using (var db = TestDb.Create())
- {
- var (inserted, updated) = await SeibroCorpActionImport.UpsertMeetingAgendasAsync(db, rows, CancellationToken.None);
- Assert.AreEqual(0, inserted, "재실행 idempotent");
- Assert.AreEqual(0, updated);
- var agendas = await db.MeetingAgenda.AsNoTracking().Where(c => c.IssucoCustno == 593 && c.RgtStdDt == new DateOnly(2017, 12, 31)).ToListAsync();
- Assert.AreEqual(11, agendas.Count);
- }
- }
- [TestMethod]
- public async Task UpsertNameChanges_FromSampleXml_ThenIdempotent()
- {
- // 기대 행위: getFmnmAltInfo(1행) upsert — UQ (custno, RgtStdDt). 재실행 무변경.
- var rows = SeibroFmnmAltParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getFmnmAltInfo.xml")));
- using (var db = TestDb.Create())
- {
- var (inserted, _) = await SeibroCorpActionImport.UpsertNameChangesAsync(db, rows, CancellationToken.None);
- Assert.AreEqual(1, inserted);
- }
- using (var db = TestDb.Create())
- {
- var (inserted, updated) = await SeibroCorpActionImport.UpsertNameChangesAsync(db, rows, CancellationToken.None);
- Assert.AreEqual(0, inserted, "재실행 idempotent");
- Assert.AreEqual(0, updated);
- var saved = await db.CorpNameChange.AsNoTracking().Where(c => c.IssucoCustno == 3572).SingleAsync();
- Assert.AreEqual("(주)카카오", saved.FmnmAltKorNm);
- Assert.AreEqual(new DateOnly(2015, 10, 20), saved.UniCirclDt);
- }
- }
- [TestMethod]
- public async Task ReplacePaymentScheduleWindow_FromSampleXml_StampsDayAndTpcd_ThenReconstructs()
- {
- // 기대 행위: getCostPaySchedul — 지급일+구분 window delete+insert. 응답에 없는 지급일/구분을 요청값으로 스탬핑. 재실행 창 재구성.
- var rows = SeibroCostPaySchedulParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getCostPaySchedul.xml")));
- var payDate = new DateOnly(2018, 7, 20);
- int firstInserted;
- using (var db = TestDb.Create())
- {
- var (inserted, deleted) = await SeibroCorpActionImport.ReplacePaymentScheduleWindowAsync(db, payDate, "3", rows, CancellationToken.None);
- Assert.IsTrue(inserted > 0, "단주대금 다수 insert");
- Assert.AreEqual(0, deleted);
- firstInserted = inserted;
- }
- using (var db = TestDb.Create())
- {
- var (inserted, deleted) = await SeibroCorpActionImport.ReplacePaymentScheduleWindowAsync(db, payDate, "3", rows, CancellationToken.None);
- Assert.AreEqual(firstInserted, inserted, "동일 창 재구성");
- Assert.AreEqual(firstInserted, deleted, "기존 창 삭제 후 재삽입");
- var stamped = await db.PaymentSchedule.AsNoTracking().Where(c => c.Isin == "KR7066980004" && c.Th1PayTermBeginDt == payDate).SingleAsync();
- Assert.AreEqual("3", stamped.PayCostTpcd, "단주 구분 스탬핑");
- Assert.AreEqual(payDate, stamped.Th1PayTermBeginDt, "지급일 스탬핑");
- Assert.AreEqual("국내CB행사", stamped.RgtRacdNm);
- }
- }
- [TestMethod]
- public async Task UpsertOddLots_FromSampleXml_StampsCustnoAndPayDate()
- {
- // 기대 행위: getOddLotInfo(1행) — 응답에 없는 회사번호·지급일을 요청값으로 스탬핑 upsert (UQ custno+PayDate+RgtStdDt).
- var rows = SeibroOddLotParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getOddLotInfo.xml")));
- const int custno = 10364;
- var payDate = new DateOnly(2018, 7, 9);
- using (var db = TestDb.Create())
- {
- var (inserted, _) = await SeibroCorpActionImport.UpsertOddLotsAsync(db, custno, payDate, rows, CancellationToken.None);
- Assert.AreEqual(1, inserted);
- }
- using (var db = TestDb.Create())
- {
- var (inserted, updated) = await SeibroCorpActionImport.UpsertOddLotsAsync(db, custno, payDate, rows, CancellationToken.None);
- Assert.AreEqual(0, inserted, "재실행 idempotent");
- Assert.AreEqual(0, updated);
- var saved = await db.OddLotPayment.AsNoTracking().Where(c => c.IssucoCustno == custno && c.PayDate == payDate).SingleAsync();
- Assert.AreEqual(new DateOnly(2018, 6, 11), saved.RgtStdDt);
- Assert.AreEqual("무상증자", saved.RgtRacdNm);
- Assert.AreEqual(3175m, saved.EstmStdprc);
- }
- }
- [TestMethod]
- public async Task ReplaceExerciseWindow_SyntheticXml_ThenReconstructs_AndSeedsBondTarget()
- {
- // 빈 fixture 대응 — 합성 XML 로 CB행사 window delete+insert 및 (f) BondTarget 발견체인 검증.
- const string exerciseXml = """
- <vector result="1">
- <data vectorkey="0" type="Document"><result>
- <BOND_ISIN value="KR6019591561"/>
- <BOND_KOR_SECN_NM value="테스트5CB"/>
- <BOND_KIND_NM value="CB"/>
- <XRC_STK_ISIN value="KR7019590007"/>
- <STK_KOR_SECN_NM value="테스트"/>
- <RGT_STD_DT value="20180321"/>
- <XRC_QTY value="20000"/>
- <XRC_PRICE value="5000"/>
- </result></data>
- </vector>
- """;
- var exRows = SeibroXrcStkOptionXrcParser.Parse(SeibroXml.Parse(exerciseXml));
- var day = new DateOnly(2018, 3, 21);
- using (var db = TestDb.Create())
- {
- var (inserted, deleted) = await SeibroCorpActionImport.ReplaceExerciseWindowAsync(db, day, day, exRows, CancellationToken.None);
- Assert.AreEqual(1, inserted);
- Assert.AreEqual(0, deleted);
- }
- using (var db = TestDb.Create())
- {
- var (inserted, deleted) = await SeibroCorpActionImport.ReplaceExerciseWindowAsync(db, day, day, exRows, CancellationToken.None);
- Assert.AreEqual(1, inserted, "창 재구성");
- Assert.AreEqual(1, deleted, "기존 창 삭제");
- }
- // (f) 발견체인: ConvertibleExercise 의 DISTINCT BondIsin 이 BondTarget 수집 대상 시드가 된다
- using (var db = TestDb.Create())
- {
- var bondIsins = await db.ConvertibleExercise.AsNoTracking().Select(c => c.BondIsin).Distinct().ToListAsync();
- CollectionAssert.Contains(bondIsins, "KR6019591561", "행사내역이 CB대상 발견 소스");
- const string statXml = """
- <vector result="1">
- <data vectorkey="0" type="Document"><result>
- <BOND_ISIN value="KR6019591561"/>
- <BOND_KIND_NM value="CB"/>
- <XRC_STK_ISIN value="KR7019590007"/>
- <XRC_PRICE value="5000"/>
- </result></data>
- </vector>
- """;
- var statRows = SeibroXrcStkStatParser.Parse(SeibroXml.Parse(statXml));
- var (inserted, _) = await SeibroCorpActionImport.UpsertBondTargetsAsync(db, statRows, CancellationToken.None);
- Assert.AreEqual(1, inserted);
- var target = await db.ConvertibleBondTarget.AsNoTracking().Where(c => c.BondIsin == "KR6019591561").SingleAsync();
- Assert.AreEqual("KR7019590007", target.XrcStkIsin);
- Assert.AreEqual(5000m, target.XrcPrice);
- }
- }
- [TestMethod]
- public async Task MeetingTrigger_FromRightsBaseDate_SelectsMeetingRacdsOnly()
- {
- // 기대 행위(이벤트 구동): RightsBaseDate RGT_RACD∈{001,002,003,009} 만 총회 트리거, 204(상호)·기타는 제외.
- string[] meetingRacds = ["001", "002", "003", "009"];
- var custno = 800000 + Random.Shared.Next(1, 99999);
- var stdDt = new DateOnly(2019, 3, 31);
- using (var db = TestDb.Create())
- {
- await db.RightsBaseDate.AddAsync(RightsBaseDate.Create(custno, "테스트정기", stdDt, "001", "정기총회", "99"), CancellationToken.None);
- await db.RightsBaseDate.AddAsync(RightsBaseDate.Create(custno, "테스트상호", stdDt, "204", "상호변경", "99"), CancellationToken.None);
- await db.RightsBaseDate.AddAsync(RightsBaseDate.Create(custno, "테스트배당", stdDt, "103", "배당분배", "02"), CancellationToken.None);
- await db.SaveChangesAsync(CancellationToken.None);
- }
- using (var db = TestDb.Create())
- {
- var meetingTriggers = await db.RightsBaseDate.AsNoTracking()
- .Where(c => c.IssucoCustno == custno && meetingRacds.Contains(c.RgtRacd))
- .Select(c => new { c.IssucoCustno, c.RgtStdDt })
- .Distinct()
- .ToListAsync();
- Assert.AreEqual(1, meetingTriggers.Count, "총회계열(001) 1건만");
- var nameTriggers = await db.RightsBaseDate.AsNoTracking()
- .Where(c => c.IssucoCustno == custno && c.RgtRacd == "204")
- .Select(c => new { c.IssucoCustno, c.RgtStdDt })
- .Distinct()
- .ToListAsync();
- Assert.AreEqual(1, nameTriggers.Count, "상호변경(204) 1건");
- }
- }
- [TestMethod]
- public async Task OddLotTrigger_FromPaymentSchedule_SelectsTpcd3Only()
- {
- // 기대 행위(이벤트 구동): PaymentSchedule PAY_COST_TPCD=3(단주)만 단주대금 트리거, 1(배당금)은 제외.
- var custno = 900000 + Random.Shared.Next(1, 99999);
- var payDate = new DateOnly(2018, 7, 20);
- var isinA = $"KR7{Guid.NewGuid().ToString("N")[..9]}";
- var isinB = $"KR7{Guid.NewGuid().ToString("N")[..9]}";
- using (var db = TestDb.Create())
- {
- await db.PaymentSchedule.AddAsync(PaymentSchedule.Create(custno, isinA, "배당건", null, null, null, null, "1", payDate), CancellationToken.None);
- await db.PaymentSchedule.AddAsync(PaymentSchedule.Create(custno, isinB, "단주건", null, null, null, null, "3", payDate), CancellationToken.None);
- await db.SaveChangesAsync(CancellationToken.None);
- }
- using (var db = TestDb.Create())
- {
- var triggers = await db.PaymentSchedule.AsNoTracking()
- .Where(c => c.IssucoCustno == custno && c.PayCostTpcd == "3")
- .Select(c => new { c.IssucoCustno, c.Th1PayTermBeginDt })
- .Distinct()
- .ToListAsync();
- Assert.AreEqual(1, triggers.Count, "단주(구분3) 1건만 트리거");
- }
- }
- [TestMethod]
- public void Quota_CorpAndStockBudgets_AreSeparateBuckets()
- {
- // 기대 행위: 기업·주식 예산이 독립 버킷 — 한쪽 소진이 다른 쪽에 영향 없음.
- var quota = new SeibroQuota(() => new DateOnly(2026, 7, 7));
- for (var i = 0; i < 5; i++)
- {
- Assert.IsTrue(quota.TryConsume(SeibroCategory.Corp, 1, 5));
- }
- Assert.IsFalse(quota.TryConsume(SeibroCategory.Corp, 1, 5), "기업 예산(5) 소진");
- Assert.AreEqual(0, quota.Remaining(SeibroCategory.Corp, 5));
- Assert.AreEqual(8, quota.Remaining(SeibroCategory.Stock, 8), "주식 예산은 온전");
- Assert.IsTrue(quota.TryConsume(SeibroCategory.Stock, 8, 8), "주식은 별도로 소비 가능");
- }
- }
|