using Application.Abstractions.Data; using Domain.Entities.Stocks; using Microsoft.EntityFrameworkCore; namespace Infrastructure.StockData; /// /// SeibroDerivSyncService 의 DB 반영 로직 분리 (테스트 가능) — HTTP 없이 파서 출력(fixture)으로 검증한다 /// (SeibroBondImport 패턴 미러링). 일곱 엔티티(발행·마스터·기초자산·기초자산행사·상환조건·상환종목·미상환규모) upsert 담당. /// getDerivCombiIsinInfo/getAssetXrcInfo/getRedCondiInfo 응답에는 ISIN 이 없어(요청 파라미터로만 받음) 배치가 요청 ISIN 을 스탬핑한다. /// getDerivCombiIssuInfo/getRedIsinInfo 는 SECN_KACD·(ISSU_DT|RED_DT) 요청값을, getBassetUnredScale 는 STD_DT·STND_BASSET_CD·SECN_TPCD 를 스탬핑한다. /// public static class SeibroDerivImport { /// getDerivCombiIssuInfo 결과를 DerivIssuance 로 upsert — UQ (Isin). SECN_KACD·ISSU_DT 요청값 스탬핑. rows 는 발행일×종류 스윕 per 콜 결과(다수 종목). public static async Task<(int Inserted, int Updated)> UpsertDerivIssuancesAsync(IAppDbContext db, string secnKacd, DateOnly issuDt, IReadOnlyList rows, CancellationToken ct) { if (rows.Count == 0) { return (0, 0); } var isins = rows.Select(c => c.Isin).Distinct().ToList(); var existing = await db.DerivIssuance.Where(c => isins.Contains(c.Isin)).ToListAsync(ct); var byKey = existing.ToDictionary(c => c.Isin); var seen = new HashSet(); var inserted = 0; var updated = 0; foreach (var row in rows) { if (!seen.Add(row.Isin)) { continue; } if (byKey.TryGetValue(row.Isin, out var entity)) { var was = entity.UpdatedAt; entity.Update(row.KorSecnNm, row.IssucoCustno, secnKacd, issuDt); if (entity.UpdatedAt != was) { updated++; } } else { var created = DerivIssuance.Create(row.Isin, row.KorSecnNm, row.IssucoCustno, secnKacd, issuDt); await db.DerivIssuance.AddAsync(created, ct); byKey[row.Isin] = created; inserted++; } } await db.SaveChangesAsync(ct); return (inserted, updated); } /// getDerivCombiIsinInfo 결과를 DerivMaster 로 upsert — PK Isin(요청값 스탬핑). rolling per-ISIN 1콜 결과(첫 행만). 빈결과면 no-op. public static async Task UpsertDerivMasterAsync(IAppDbContext db, string isin, IReadOnlyList rows, CancellationToken ct) { if (rows.Count == 0) { return false; } var fields = rows[0]; var entity = await db.DerivMaster.FirstOrDefaultAsync(c => c.Isin == isin, ct); if (entity is null) { await db.DerivMaster.AddAsync(DerivMaster.Create(isin, fields), ct); } else { entity.Update(fields); } await db.SaveChangesAsync(ct); return true; } /// getAssetInfo 결과를 DerivAsset 으로 upsert — UQ (Isin, AssetSeq). ISIN 요청값 스탬핑. rolling per-ISIN 1콜(다수 기초자산). public static async Task<(int Inserted, int Updated)> UpsertDerivAssetsAsync(IAppDbContext db, string isin, IReadOnlyList rows, CancellationToken ct) { if (rows.Count == 0) { return (0, 0); } var existing = await db.DerivAsset.Where(c => c.Isin == isin).ToListAsync(ct); var byKey = existing.ToDictionary(c => c.AssetSeq); var seen = new HashSet(); var inserted = 0; var updated = 0; foreach (var row in rows) { if (!seen.Add(row.AssetSeq)) { continue; } if (byKey.TryGetValue(row.AssetSeq, out var entity)) { var was = entity.UpdatedAt; entity.Update(row.Fields); if (entity.UpdatedAt != was) { updated++; } } else { var created = DerivAsset.Create(isin, row.AssetSeq, row.Fields); await db.DerivAsset.AddAsync(created, ct); byKey[row.AssetSeq] = created; inserted++; } } await db.SaveChangesAsync(ct); return (inserted, updated); } /// getAssetXrcInfo 결과를 DerivAssetExercise 로 upsert — UQ (Isin, AssetSeq, XrcStdSeq). ISIN 요청값 스탬핑. rolling per-ISIN 1콜(다수 행). public static async Task<(int Inserted, int Updated)> UpsertDerivAssetExercisesAsync(IAppDbContext db, string isin, IReadOnlyList rows, CancellationToken ct) { if (rows.Count == 0) { return (0, 0); } var existing = await db.DerivAssetExercise.Where(c => c.Isin == isin).ToListAsync(ct); var byKey = existing.ToDictionary(c => (c.AssetSeq, c.XrcStdSeq)); var seen = new HashSet<(int, int)>(); var inserted = 0; var updated = 0; foreach (var row in rows) { var key = (row.AssetSeq, row.XrcStdSeq); if (!seen.Add(key)) { continue; } if (byKey.TryGetValue(key, out var entity)) { var was = entity.UpdatedAt; entity.Update(row.XrcStdRatio, row.XrcPrice); if (entity.UpdatedAt != was) { updated++; } } else { var created = DerivAssetExercise.Create(isin, row.AssetSeq, row.XrcStdSeq, row.XrcStdRatio, row.XrcPrice); await db.DerivAssetExercise.AddAsync(created, ct); byKey[key] = created; inserted++; } } await db.SaveChangesAsync(ct); return (inserted, updated); } /// getRedCondiInfo 결과를 DerivRedemptionCondition 으로 upsert — UQ (Isin, RedCondiTpcd, ValatNtimesSeq). ISIN 요청값 스탬핑. rolling per-ISIN 1콜(다수 조건). public static async Task<(int Inserted, int Updated)> UpsertDerivRedemptionConditionsAsync(IAppDbContext db, string isin, IReadOnlyList rows, CancellationToken ct) { if (rows.Count == 0) { return (0, 0); } var existing = await db.DerivRedemptionCondition.Where(c => c.Isin == isin).ToListAsync(ct); var byKey = existing.ToDictionary(c => (c.RedCondiTpcd, c.ValatNtimesSeq)); var seen = new HashSet<(string, int)>(); var inserted = 0; var updated = 0; foreach (var row in rows) { var key = (row.RedCondiTpcd, row.ValatNtimesSeq); if (!seen.Add(key)) { continue; } if (byKey.TryGetValue(key, out var entity)) { var was = entity.UpdatedAt; entity.Update(row.MidValatPayDt, row.MidValatBeginDt, row.MidValatExpryDt, row.RedCondiContent, row.RedFormulaContent); if (entity.UpdatedAt != was) { updated++; } } else { var created = DerivRedemptionCondition.Create(isin, row.RedCondiTpcd, row.ValatNtimesSeq, row.MidValatPayDt, row.MidValatBeginDt, row.MidValatExpryDt, row.RedCondiContent, row.RedFormulaContent); await db.DerivRedemptionCondition.AddAsync(created, ct); byKey[key] = created; inserted++; } } await db.SaveChangesAsync(ct); return (inserted, updated); } /// /// getRedIsinInfo 결과를 DerivRedemption 으로 window(상환일 begin~end)×종류 delete+insert 반영. /// 날짜 스윕(RED_DT)이라 창 재구성으로 삭제분 반영. RED_DT·SECN_KACD 요청값 스탬핑. 같은 창·종류 내 (Isin, RedDt, DerisecExerTpcd) 중복 방어. /// public static async Task<(int Inserted, int Deleted)> ReplaceRedemptionWindowAsync(IAppDbContext db, string secnKacd, DateOnly begin, DateOnly end, IReadOnlyList rows, CancellationToken ct) { var deleted = await db.DerivRedemption.Where(c => c.SecnKacd == secnKacd && c.RedDt >= begin && c.RedDt <= end).ExecuteDeleteAsync(ct); var seen = new HashSet<(string, DateOnly, string)>(); var inserted = 0; foreach (var row in rows) { // 날짜 스윕은 begin=end 하루라 RedDt 는 begin 으로 스탬핑 var key = (row.Isin, begin, row.DerisecExerTpcd); if (!seen.Add(key)) { continue; } var created = DerivRedemption.Create(row.Isin, row.DerisecExerTpcd, row.RedQty, begin, secnKacd); await db.DerivRedemption.AddAsync(created, ct); inserted++; } await db.SaveChangesAsync(ct); return (inserted, deleted); } /// getBassetUnredScale 결과를 ElsUnredeemedScale 로 upsert — UQ (StdDt, StndBassetCd, SecnTpcd). 요청값 3종 스탬핑. 종목당 1행(첫 행만). 빈결과면 no-op. public static async Task UpsertUnredeemedScaleAsync(IAppDbContext db, DateOnly stdDt, string stndBassetCd, string secnTpcd, IReadOnlyList rows, CancellationToken ct) { if (rows.Count == 0) { return false; } var row = rows[0]; var entity = await db.ElsUnredeemedScale.FirstOrDefaultAsync(c => c.StdDt == stdDt && c.StndBassetCd == stndBassetCd && c.SecnTpcd == secnTpcd, ct); if (entity is null) { await db.ElsUnredeemedScale.AddAsync(ElsUnredeemedScale.Create(stdDt, stndBassetCd, secnTpcd, row.BassetContent, row.UnredSecncnt, row.UnredRema), ct); } else { entity.Update(row.BassetContent, row.UnredSecncnt, row.UnredRema); } await db.SaveChangesAsync(ct); return true; } }