using Application.Helpers; using Domain.Entities.Stocks; using Infrastructure.StockData; using Microsoft.EntityFrameworkCore; namespace Application.Tests; /// /// SeibroDerivSyncService 의 DB 반영 로직(SeibroDerivImport) + rolling 대상 선정 통합 검증 — /// HTTP 는 파서 입력을 fixture/합성 XML 로 대체(SeibroBondSyncTests 방식 미러링). 실제 SQL Server(LocalDB). /// 발견체인(발행→마스터 rolling) / 기초자산·행사·상환조건 upsert / 상환종목 window(종류별) / 미상환규모 13×2 스탬핑 / Deriv 예산 상한. /// [TestClass] public sealed class SeibroDerivSyncTests { [TestMethod] public async Task UpsertDerivIssuances_FromSampleXml_StampsSecnKacdAndIssuDt_ThenIdempotent() { // 기대 행위: getDerivCombiIssuInfo(다수행) — 응답에 없는 SECN_KACD·ISSU_DT 를 요청값 스탬핑 upsert (UQ Isin). var rows = SeibroDerivCombiIssuParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getDerivCombiIssuInfo.xml"))); var issuDt = new DateOnly(2018, 7, 2); using (var db = TestDb.Create()) { var (inserted, _) = await SeibroDerivImport.UpsertDerivIssuancesAsync(db, "4101", issuDt, rows, CancellationToken.None); Assert.IsTrue(inserted > 1, "다수 발행종목 insert"); } using (var db = TestDb.Create()) { var (inserted, updated) = await SeibroDerivImport.UpsertDerivIssuancesAsync(db, "4101", issuDt, rows, CancellationToken.None); Assert.AreEqual(0, inserted, "재실행 idempotent"); Assert.AreEqual(0, updated, "무변경 no-op"); var saved = await db.DerivIssuance.AsNoTracking().Where(c => c.Isin == "KR6533308872").SingleAsync(); Assert.AreEqual("4101", saved.SecnKacd, "종류 스탬핑"); Assert.AreEqual(issuDt, saved.IssuDt, "발행일 스탬핑"); Assert.AreEqual(1636, saved.IssucoCustno); } } [TestMethod] public async Task DerivMasterTarget_ComesFromDerivIssuance_AndUpsertStampsIsin() { // 발견체인: DerivMaster rolling 대상 = DerivIssuance.Isin. getDerivCombiIsinInfo 응답엔 ISIN 없어 요청값 스탬핑, PK Isin upsert. var isin = $"KR{Guid.NewGuid().ToString("N")[..10]}"; using (var db = TestDb.Create()) { await db.DerivIssuance.AddAsync(DerivIssuance.Create(isin, "테스트ELS", 1636, "4101", new DateOnly(2018, 7, 2)), CancellationToken.None); await db.SaveChangesAsync(CancellationToken.None); } using (var db = TestDb.Create()) { // rolling 대상 = DerivIssuance ∖ DerivMaster (미수집 우선) var issued = await db.DerivIssuance.AsNoTracking().Select(c => c.Isin).ToListAsync(); CollectionAssert.Contains(issued, isin); var rows = SeibroDerivCombiIsinParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getDerivCombiIsinInfo.xml"))); Assert.IsTrue(await SeibroDerivImport.UpsertDerivMasterAsync(db, isin, rows, CancellationToken.None)); } using (var db = TestDb.Create()) { Assert.IsTrue(await SeibroDerivImport.UpsertDerivMasterAsync(db, isin, SeibroDerivCombiIsinParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getDerivCombiIsinInfo.xml"))), CancellationToken.None), "재수집 idempotent upsert"); var saved = await db.DerivMaster.AsNoTracking().Where(c => c.Isin == isin).SingleAsync(); Assert.AreEqual("ELS", saved.SecnKanm); Assert.AreEqual(new DateOnly(2021, 7, 2), saved.XpirDt, "만기"); Assert.AreEqual("3", saved.PrcpPrsvTpcd, "원금비보장"); Assert.AreEqual(3, saved.BassetCnt); Assert.IsTrue(saved.MidredPossYn); } } [TestMethod] public async Task UpsertDerivAssets_FromSampleXml_StampsIsin_PreservesRawBassetCode() { // 기대 행위: getAssetInfo(3행) — ISIN 요청값 스탬핑, UQ(Isin,AssetSeq) upsert, StndBassetCd 반환 원문 보존. var rows = SeibroAssetParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getAssetInfo.xml"))); var isin = $"KR{Guid.NewGuid().ToString("N")[..10]}"; using (var db = TestDb.Create()) { var (inserted, _) = await SeibroDerivImport.UpsertDerivAssetsAsync(db, isin, rows, CancellationToken.None); Assert.AreEqual(3, inserted, "기초자산 3종"); } using (var db = TestDb.Create()) { var (inserted, updated) = await SeibroDerivImport.UpsertDerivAssetsAsync(db, isin, rows, CancellationToken.None); Assert.AreEqual(0, inserted, "재실행 idempotent"); Assert.AreEqual(0, updated); var hscei = await db.DerivAsset.AsNoTracking().Where(c => c.Isin == isin && c.AssetSeq == 1).SingleAsync(); Assert.AreEqual("KSD310000306", hscei.StndBassetCd, "반환 코드(KSD31*) 원문 저장"); Assert.AreEqual(11073m, hscei.XrcStdprc); } } [TestMethod] public async Task UpsertDerivAssetExercises_FromSampleXml_StampsIsin() { // 기대 행위: getAssetXrcInfo(8행) — ISIN 요청값 스탬핑, UQ(Isin,AssetSeq,XrcStdSeq) upsert. var rows = SeibroAssetXrcParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getAssetXrcInfo.xml"))); var isin = $"KR{Guid.NewGuid().ToString("N")[..10]}"; using (var db = TestDb.Create()) { var (inserted, _) = await SeibroDerivImport.UpsertDerivAssetExercisesAsync(db, isin, rows, CancellationToken.None); Assert.AreEqual(8, inserted); } using (var db = TestDb.Create()) { var (inserted, _) = await SeibroDerivImport.UpsertDerivAssetExercisesAsync(db, isin, rows, CancellationToken.None); Assert.AreEqual(0, inserted, "재실행 idempotent"); var last = await db.DerivAssetExercise.AsNoTracking().Where(c => c.Isin == isin && c.XrcStdSeq == 8).SingleAsync(); Assert.AreEqual(65m, last.XrcStdRatio); Assert.AreEqual(181.2395m, last.XrcPrice); } } [TestMethod] public async Task UpsertRedemptionConditions_FromSampleXml_StampsIsin_1000CharText() { // 기대 행위: getRedCondiInfo(10행) — ISIN 요청값 스탬핑, UQ(Isin,RedCondiTpcd,ValatNtimesSeq) upsert, 1000자 텍스트 보존. var rows = SeibroRedCondiParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getRedCondiInfo.xml"))); var isin = $"KR{Guid.NewGuid().ToString("N")[..10]}"; using (var db = TestDb.Create()) { var (inserted, _) = await SeibroDerivImport.UpsertDerivRedemptionConditionsAsync(db, isin, rows, CancellationToken.None); Assert.AreEqual(10, inserted, "중간 8 + 만기 2"); } using (var db = TestDb.Create()) { var (inserted, _) = await SeibroDerivImport.UpsertDerivRedemptionConditionsAsync(db, isin, rows, CancellationToken.None); Assert.AreEqual(0, inserted, "재실행 idempotent"); var expiryLoss = await db.DerivRedemptionCondition.AsNoTracking().Where(c => c.Isin == isin && c.RedCondiTpcd == "2" && c.ValatNtimesSeq == 2).SingleAsync(); StringAssert.Contains(expiryLoss.RedCondiContent, "65% 미만"); Assert.AreEqual("액면금액 * (만기평가가격/최초기준가격)", expiryLoss.RedFormulaContent); } } [TestMethod] public async Task ReplaceRedemptionWindow_FromSampleXml_StampsRedDtAndSecnKacd_ThenReconstructs() { // 기대 행위: getRedIsinInfo — 상환일×종류 window delete+insert (UQ Isin+RedDt+DerisecExerTpcd, RED_DT·SECN_KACD 스탬핑). 재실행 창 재구성. var rows = SeibroRedIsinParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getRedIsinInfo.xml"))); var day = new DateOnly(2018, 8, 6); using (var db = TestDb.Create()) { var (inserted, deleted) = await SeibroDerivImport.ReplaceRedemptionWindowAsync(db, "4101", day, day, rows, CancellationToken.None); Assert.AreEqual(12, inserted); Assert.AreEqual(0, deleted); } using (var db = TestDb.Create()) { var (inserted, deleted) = await SeibroDerivImport.ReplaceRedemptionWindowAsync(db, "4101", day, day, rows, CancellationToken.None); Assert.AreEqual(12, inserted, "창 재구성"); Assert.AreEqual(12, deleted, "기존 창 삭제"); var saved = await db.DerivRedemption.AsNoTracking().Where(c => c.Isin == "KR6552327845" && c.RedDt == day).SingleAsync(); Assert.AreEqual("4101", saved.SecnKacd, "종류 스탬핑"); Assert.AreEqual("3", saved.DerisecExerTpcd, "조기"); Assert.AreEqual(132000000m, saved.RedQty); } } [TestMethod] public async Task UpsertUnredeemedScale_13x2_StampsAllKeys_EmptyRemaTolerated() { // 기대 행위: getBassetUnredScale — STD_DT·STND_BASSET_CD·SECN_TPCD 3종 스탬핑, UQ upsert. 실샘플 UNRED_REMA 빈값 → null 허용. var rows = SeibroBassetUnredScaleParser.Parse(SeibroXml.Parse(SeibroFixtures.Read("getBassetUnredScale.xml"))); // 공유 DB 라 고정 기준일이 타 테스트와 충돌하지 않도록 이 테스트 전용 기초자산코드 프리픽스를 쓴다. var stdDt = new DateOnly(2018, 7, 31); var cdA = $"TSTA{Guid.NewGuid().ToString("N")[..8]}"; var cdB = $"TSTB{Guid.NewGuid().ToString("N")[..8]}"; using (var db = TestDb.Create()) { // 같은 기준일에 서로 다른 기초자산×종목구분 조합이 각각 별 row 로 들어감 (13×2 스윕의 축소판) Assert.IsTrue(await SeibroDerivImport.UpsertUnredeemedScaleAsync(db, stdDt, cdA, "41", rows, CancellationToken.None)); Assert.IsTrue(await SeibroDerivImport.UpsertUnredeemedScaleAsync(db, stdDt, cdA, "43", rows, CancellationToken.None), "같은 기초자산 다른 SECN_TPCD 는 별 row"); Assert.IsTrue(await SeibroDerivImport.UpsertUnredeemedScaleAsync(db, stdDt, cdB, "41", rows, CancellationToken.None), "다른 기초자산"); } using (var db = TestDb.Create()) { var mine = await db.ElsUnredeemedScale.AsNoTracking().Where(c => c.StdDt == stdDt && (c.StndBassetCd == cdA || c.StndBassetCd == cdB)).ToListAsync(); Assert.AreEqual(3, mine.Count, "13×2 조합 중 3건 스탬핑 (본 테스트 기초자산만)"); var kospi41 = mine.Single(c => c.StndBassetCd == cdA && c.SecnTpcd == "41"); Assert.AreEqual("EURO STOXX 50", kospi41.BassetContent, "응답 기초자산내용 그대로"); Assert.AreEqual(2, kospi41.UnredSecncnt); Assert.IsNull(kospi41.UnredRema, "빈 잔액 → null"); // 재실행 idempotent Assert.IsTrue(await SeibroDerivImport.UpsertUnredeemedScaleAsync(db, stdDt, cdA, "41", rows, CancellationToken.None)); var again = await db.ElsUnredeemedScale.AsNoTracking().Where(c => c.StdDt == stdDt && (c.StndBassetCd == cdA || c.StndBassetCd == cdB)).CountAsync(); Assert.AreEqual(3, again, "재실행 무증가 (upsert)"); } } [TestMethod] public void Quota_DerivBudget_CapsConsumption() { // 기대 행위: 파생 예산 상한 — 소진 후 추가 소비 거부, 다른 카테고리 무영향. var quota = new SeibroQuota(() => new DateOnly(2026, 7, 7)); Assert.IsTrue(quota.TryConsume(SeibroCategory.Deriv, 8000, 8000), "예산 전량 소비"); Assert.IsFalse(quota.TryConsume(SeibroCategory.Deriv, 1, 8000), "파생 예산 소진"); Assert.AreEqual(0, quota.Remaining(SeibroCategory.Deriv, 8000)); Assert.AreEqual(8000, quota.Remaining(SeibroCategory.Bond, 8000), "채권 예산 무영향"); } }