| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- using Application.Abstractions.Data;
- using Domain.Entities.Stocks;
- using Microsoft.EntityFrameworkCore;
- namespace Infrastructure.StockData;
- /// <summary>
- /// SeibroCorpActionSyncService 의 DB 반영 로직 분리 (테스트 가능) — HTTP 없이 파서 출력(fixture)으로 검증한다
- /// (SeibroSupplyImport 패턴 미러링). 일곱 엔티티(총회·안건·상호변경·대금·단주·CB대상·CB행사) upsert / window delete+insert 담당.
- /// </summary>
- public static class SeibroCorpActionImport
- {
- /// <summary>
- /// getGmeetInfo 결과를 GeneralMeeting 로 upsert — UQ (IssucoCustno, RgtStdDt, GmetKacd).
- /// rows 는 (회사, 기준일) per 콜 결과. 같은 콜 내 자연키 중복은 첫 행 채택.
- /// </summary>
- public static async Task<(int Inserted, int Updated)> UpsertGeneralMeetingsAsync(IAppDbContext db, IReadOnlyList<SeibroGmeetParser.Row> rows, CancellationToken ct)
- {
- if (rows.Count == 0)
- {
- return (0, 0);
- }
- var custnos = rows.Select(c => c.IssucoCustno).Distinct().ToList();
- var stdDts = rows.Select(c => c.RgtStdDt).Distinct().ToList();
- var existing = await db.GeneralMeeting.Where(c => custnos.Contains(c.IssucoCustno) && stdDts.Contains(c.RgtStdDt)).ToListAsync(ct);
- var byKey = existing.ToDictionary(c => (c.IssucoCustno, c.RgtStdDt, c.GmetKacd));
- var seen = new HashSet<(int, DateOnly, string)>();
- var inserted = 0;
- var updated = 0;
- foreach (var row in rows)
- {
- var key = (row.IssucoCustno, row.RgtStdDt, row.GmetKacd);
- if (!seen.Add(key))
- {
- continue;
- }
- if (byKey.TryGetValue(key, out var entity))
- {
- var was = entity.UpdatedAt;
- entity.Update(row.IssuinNo, row.RgtRsnDetailSortCd, row.RostCloseBeginDt, row.RostCloseExpryDt, row.GmetOmeetYn, row.GmetPlaceTpcd, row.CouncilDterminPubnDt, row.GmetPlace, row.StkBuyreqRlYn, row.GmetDt, row.GmetTtm, row.NotcDlineDt, row.DferYn, row.EvoteYn, row.IsElectronicSecurity);
- if (entity.UpdatedAt != was)
- {
- updated++;
- }
- }
- else
- {
- var created = GeneralMeeting.Create(row.IssucoCustno, row.IssuinNo, row.GmetKacd, row.RgtRsnDetailSortCd, row.RostCloseBeginDt, row.RostCloseExpryDt, row.GmetOmeetYn, row.GmetPlaceTpcd, row.CouncilDterminPubnDt, row.GmetPlace, row.StkBuyreqRlYn, row.RgtStdDt, row.GmetDt, row.GmetTtm, row.NotcDlineDt, row.DferYn, row.EvoteYn, row.IsElectronicSecurity);
- await db.GeneralMeeting.AddAsync(created, ct);
- byKey[key] = created;
- inserted++;
- }
- }
- await db.SaveChangesAsync(ct);
- return (inserted, updated);
- }
- /// <summary>
- /// getGmeetMeasureInfo 결과를 MeetingAgenda 로 upsert — UQ (IssucoCustno, RgtStdDt, AgendaSeq).
- /// rows 는 (회사, 기준일) per 콜 결과(여러 안건).
- /// </summary>
- public static async Task<(int Inserted, int Updated)> UpsertMeetingAgendasAsync(IAppDbContext db, IReadOnlyList<SeibroGmeetMeasureParser.Row> rows, CancellationToken ct)
- {
- if (rows.Count == 0)
- {
- return (0, 0);
- }
- var custnos = rows.Select(c => c.IssucoCustno).Distinct().ToList();
- var stdDts = rows.Select(c => c.RgtStdDt).Distinct().ToList();
- var existing = await db.MeetingAgenda.Where(c => custnos.Contains(c.IssucoCustno) && stdDts.Contains(c.RgtStdDt)).ToListAsync(ct);
- var byKey = existing.ToDictionary(c => (c.IssucoCustno, c.RgtStdDt, c.AgendaSeq));
- var seen = new HashSet<(int, DateOnly, int)>();
- var inserted = 0;
- var updated = 0;
- foreach (var row in rows)
- {
- var key = (row.IssucoCustno, row.RgtStdDt, row.AgendaSeq);
- if (!seen.Add(key))
- {
- continue;
- }
- if (byKey.TryGetValue(key, out var entity))
- {
- var was = entity.UpdatedAt;
- entity.Update(row.IssuinNo, row.GmetDt, row.GmetKacd, row.AgendaXpresNo, row.AgendaSchSeqo, row.AgendaNo, row.DtailAgendaNo, row.AgendaNm, row.EngAgendaNm, row.DtailAgendaNm, row.DtailEngAgendaNm, row.VoteMannTpcd, row.GmetResultTpcd);
- if (entity.UpdatedAt != was)
- {
- updated++;
- }
- }
- else
- {
- var created = MeetingAgenda.Create(row.IssucoCustno, row.IssuinNo, row.RgtStdDt, row.GmetDt, row.GmetKacd, row.AgendaSeq, row.AgendaXpresNo, row.AgendaSchSeqo, row.AgendaNo, row.DtailAgendaNo, row.AgendaNm, row.EngAgendaNm, row.DtailAgendaNm, row.DtailEngAgendaNm, row.VoteMannTpcd, row.GmetResultTpcd);
- await db.MeetingAgenda.AddAsync(created, ct);
- byKey[key] = created;
- inserted++;
- }
- }
- await db.SaveChangesAsync(ct);
- return (inserted, updated);
- }
- /// <summary>
- /// getFmnmAltInfo 결과를 CorpNameChange 로 upsert — UQ (IssucoCustno, RgtStdDt).
- /// rows 는 (회사, 기준일) per 콜 결과(보통 1행).
- /// </summary>
- public static async Task<(int Inserted, int Updated)> UpsertNameChangesAsync(IAppDbContext db, IReadOnlyList<SeibroFmnmAltParser.Row> rows, CancellationToken ct)
- {
- if (rows.Count == 0)
- {
- return (0, 0);
- }
- var custnos = rows.Select(c => c.IssucoCustno).Distinct().ToList();
- var stdDts = rows.Select(c => c.RgtStdDt).Distinct().ToList();
- var existing = await db.CorpNameChange.Where(c => custnos.Contains(c.IssucoCustno) && stdDts.Contains(c.RgtStdDt)).ToListAsync(ct);
- var byKey = existing.ToDictionary(c => (c.IssucoCustno, c.RgtStdDt));
- var seen = new HashSet<(int, DateOnly)>();
- var inserted = 0;
- var updated = 0;
- foreach (var row in rows)
- {
- var key = (row.IssucoCustno, row.RgtStdDt);
- if (!seen.Add(key))
- {
- continue;
- }
- if (byKey.TryGetValue(key, out var entity))
- {
- var was = entity.UpdatedAt;
- entity.Update(row.FmnmBfaltKorNm, row.FmnmBfaltEngNm, row.FmnmAltKorNm, row.FmnmAltEngNm, row.MixusCirclBeginDt, row.MixusCirclExpryDt, row.UniCirclDt);
- if (entity.UpdatedAt != was)
- {
- updated++;
- }
- }
- else
- {
- var created = CorpNameChange.Create(row.IssucoCustno, row.RgtStdDt, row.FmnmBfaltKorNm, row.FmnmBfaltEngNm, row.FmnmAltKorNm, row.FmnmAltEngNm, row.MixusCirclBeginDt, row.MixusCirclExpryDt, row.UniCirclDt);
- await db.CorpNameChange.AddAsync(created, ct);
- byKey[key] = created;
- inserted++;
- }
- }
- await db.SaveChangesAsync(ct);
- return (inserted, updated);
- }
- /// <summary>
- /// getCostPaySchedul 결과를 PaymentSchedule 로 window(지급일+구분) delete+insert 반영.
- /// 날짜 스윕(TH1_PAY_TERM_BEGIN_DT × PAY_COST_TPCD)이라 자연키 대신 (지급일, 구분) 창 재구성. 응답에 없는 지급일/구분은 요청값 스탬핑.
- /// 같은 창 내 중복 (회사, ISIN, 권리기준일) 방어.
- /// </summary>
- public static async Task<(int Inserted, int Deleted)> ReplacePaymentScheduleWindowAsync(IAppDbContext db, DateOnly payDate, string payCostTpcd, IReadOnlyList<SeibroCostPaySchedulParser.Row> rows, CancellationToken ct)
- {
- var deleted = await db.PaymentSchedule.Where(c => c.Th1PayTermBeginDt == payDate && c.PayCostTpcd == payCostTpcd).ExecuteDeleteAsync(ct);
- var seen = new HashSet<(int, string, DateOnly?)>();
- var inserted = 0;
- foreach (var row in rows)
- {
- var key = (row.IssucoCustno, row.Isin, row.RgtStdDt);
- if (!seen.Add(key))
- {
- continue;
- }
- var created = PaymentSchedule.Create(row.IssucoCustno, row.Isin, row.KorSecnNm, row.SecnKacd, row.RgtStdDt, row.RgtRacdNm, row.RgtRsnDetailSortCd, payCostTpcd, payDate);
- await db.PaymentSchedule.AddAsync(created, ct);
- inserted++;
- }
- await db.SaveChangesAsync(ct);
- return (inserted, deleted);
- }
- /// <summary>
- /// getOddLotInfo 결과를 OddLotPayment 로 upsert — UQ (IssucoCustno, PayDate, RgtStdDt).
- /// 응답에 없는 회사번호·지급일은 요청값을 스탬핑. rows 는 (회사, 지급일) per 콜 결과.
- /// </summary>
- public static async Task<(int Inserted, int Updated)> UpsertOddLotsAsync(IAppDbContext db, int issucoCustno, DateOnly payDate, IReadOnlyList<SeibroOddLotParser.Row> rows, CancellationToken ct)
- {
- if (rows.Count == 0)
- {
- return (0, 0);
- }
- var existing = await db.OddLotPayment.Where(c => c.IssucoCustno == issucoCustno && c.PayDate == payDate).ToListAsync(ct);
- var byKey = existing.ToDictionary(c => c.RgtStdDt);
- var seen = new HashSet<DateOnly>();
- var inserted = 0;
- var updated = 0;
- foreach (var row in rows)
- {
- if (!seen.Add(row.RgtStdDt))
- {
- continue;
- }
- if (byKey.TryGetValue(row.RgtStdDt, out var entity))
- {
- var was = entity.UpdatedAt;
- entity.Update(row.RgtRacd, row.RgtRacdNm, row.RgtRsnDetailSortCd, row.EstmStdTpcd, row.EstmStdDt, row.EstmStdprc, row.RostCloseBeginDt, row.RostCloseExpryDt, row.XrgtDt);
- if (entity.UpdatedAt != was)
- {
- updated++;
- }
- }
- else
- {
- var created = OddLotPayment.Create(issucoCustno, payDate, row.RgtStdDt, row.RgtRacd, row.RgtRacdNm, row.RgtRsnDetailSortCd, row.EstmStdTpcd, row.EstmStdDt, row.EstmStdprc, row.RostCloseBeginDt, row.RostCloseExpryDt, row.XrgtDt);
- await db.OddLotPayment.AddAsync(created, ct);
- byKey[row.RgtStdDt] = created;
- inserted++;
- }
- }
- await db.SaveChangesAsync(ct);
- return (inserted, updated);
- }
- /// <summary>
- /// getXrcStkStatInfo 결과를 ConvertibleBondTarget 로 upsert — UQ (BondIsin, XrcStkIsin). rolling per-채권ISIN 1콜 결과.
- /// </summary>
- public static async Task<(int Inserted, int Updated)> UpsertBondTargetsAsync(IAppDbContext db, IReadOnlyList<SeibroXrcStkStatParser.Row> rows, CancellationToken ct)
- {
- if (rows.Count == 0)
- {
- return (0, 0);
- }
- var bondIsins = rows.Select(c => c.BondIsin).Distinct().ToList();
- var existing = await db.ConvertibleBondTarget.Where(c => bondIsins.Contains(c.BondIsin)).ToListAsync(ct);
- var byKey = existing.ToDictionary(c => (c.BondIsin, c.XrcStkIsin));
- var seen = new HashSet<(string, string)>();
- var inserted = 0;
- var updated = 0;
- foreach (var row in rows)
- {
- var key = (row.BondIsin, row.XrcStkIsin);
- if (!seen.Add(key))
- {
- continue;
- }
- if (byKey.TryGetValue(key, out var entity))
- {
- var was = entity.UpdatedAt;
- entity.Update(row.BondSecnNm, row.BondKindNm, row.StkSecnNm, row.WrtbIsin, row.WrtbSecnNm, row.XrcRatio, row.XrcPrice, row.SetaccEndtermLimitDays, row.NewstkAllocDdBfLimitDays, row.DmanLimitRsnContent);
- if (entity.UpdatedAt != was)
- {
- updated++;
- }
- }
- else
- {
- var created = ConvertibleBondTarget.Create(row.BondIsin, row.BondSecnNm, row.BondKindNm, row.XrcStkIsin, row.StkSecnNm, row.WrtbIsin, row.WrtbSecnNm, row.XrcRatio, row.XrcPrice, row.SetaccEndtermLimitDays, row.NewstkAllocDdBfLimitDays, row.DmanLimitRsnContent);
- await db.ConvertibleBondTarget.AddAsync(created, ct);
- byKey[key] = created;
- inserted++;
- }
- }
- await db.SaveChangesAsync(ct);
- return (inserted, updated);
- }
- /// <summary>
- /// getXrcStkOptionXrcInfo 결과를 ConvertibleExercise 로 window(권리행사일 begin~end) delete+insert 반영.
- /// 날짜 스윕(RGT_STD_DT=권리행사일)이라 창 재구성으로 삭제분 반영. begin/end 는 이번 스윕 창.
- /// </summary>
- public static async Task<(int Inserted, int Deleted)> ReplaceExerciseWindowAsync(IAppDbContext db, DateOnly begin, DateOnly end, IReadOnlyList<SeibroXrcStkOptionXrcParser.Row> rows, CancellationToken ct)
- {
- var deleted = await db.ConvertibleExercise.Where(c => c.RgtStdDt >= begin && c.RgtStdDt <= end).ExecuteDeleteAsync(ct);
- var inserted = 0;
- foreach (var row in rows)
- {
- if (row.RgtStdDt < begin || row.RgtStdDt > end)
- {
- continue; // 스윕 창 밖 — 다른 창이 담당
- }
- var created = ConvertibleExercise.Create(row.BondIsin, row.BondKorSecnNm, row.BondKindNm, row.XrcStkIsin, row.StkKorSecnNm, row.RgtStdDt, row.XrcPossBeginDt, row.XrcPossExpryDt, row.XrcAmt, row.XrcQty, row.XrcPrice, row.ListDt);
- await db.ConvertibleExercise.AddAsync(created, ct);
- inserted++;
- }
- await db.SaveChangesAsync(ct);
- return (inserted, deleted);
- }
- }
|