using Application.Abstractions.Data;
using Domain.Entities.Stocks;
using Microsoft.EntityFrameworkCore;
namespace Infrastructure.StockData;
///
/// SeibroCorpActionSyncService 의 DB 반영 로직 분리 (테스트 가능) — HTTP 없이 파서 출력(fixture)으로 검증한다
/// (SeibroSupplyImport 패턴 미러링). 일곱 엔티티(총회·안건·상호변경·대금·단주·CB대상·CB행사) upsert / window delete+insert 담당.
///
public static class SeibroCorpActionImport
{
///
/// getGmeetInfo 결과를 GeneralMeeting 로 upsert — UQ (IssucoCustno, RgtStdDt, GmetKacd).
/// rows 는 (회사, 기준일) per 콜 결과. 같은 콜 내 자연키 중복은 첫 행 채택.
///
public static async Task<(int Inserted, int Updated)> UpsertGeneralMeetingsAsync(IAppDbContext db, IReadOnlyList 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);
}
///
/// getGmeetMeasureInfo 결과를 MeetingAgenda 로 upsert — UQ (IssucoCustno, RgtStdDt, AgendaSeq).
/// rows 는 (회사, 기준일) per 콜 결과(여러 안건).
///
public static async Task<(int Inserted, int Updated)> UpsertMeetingAgendasAsync(IAppDbContext db, IReadOnlyList 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);
}
///
/// getFmnmAltInfo 결과를 CorpNameChange 로 upsert — UQ (IssucoCustno, RgtStdDt).
/// rows 는 (회사, 기준일) per 콜 결과(보통 1행).
///
public static async Task<(int Inserted, int Updated)> UpsertNameChangesAsync(IAppDbContext db, IReadOnlyList 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);
}
///
/// getCostPaySchedul 결과를 PaymentSchedule 로 window(지급일+구분) delete+insert 반영.
/// 날짜 스윕(TH1_PAY_TERM_BEGIN_DT × PAY_COST_TPCD)이라 자연키 대신 (지급일, 구분) 창 재구성. 응답에 없는 지급일/구분은 요청값 스탬핑.
/// 같은 창 내 중복 (회사, ISIN, 권리기준일) 방어.
///
public static async Task<(int Inserted, int Deleted)> ReplacePaymentScheduleWindowAsync(IAppDbContext db, DateOnly payDate, string payCostTpcd, IReadOnlyList 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);
}
///
/// getOddLotInfo 결과를 OddLotPayment 로 upsert — UQ (IssucoCustno, PayDate, RgtStdDt).
/// 응답에 없는 회사번호·지급일은 요청값을 스탬핑. rows 는 (회사, 지급일) per 콜 결과.
///
public static async Task<(int Inserted, int Updated)> UpsertOddLotsAsync(IAppDbContext db, int issucoCustno, DateOnly payDate, IReadOnlyList 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();
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);
}
///
/// getXrcStkStatInfo 결과를 ConvertibleBondTarget 로 upsert — UQ (BondIsin, XrcStkIsin). rolling per-채권ISIN 1콜 결과.
///
public static async Task<(int Inserted, int Updated)> UpsertBondTargetsAsync(IAppDbContext db, IReadOnlyList 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);
}
///
/// getXrcStkOptionXrcInfo 결과를 ConvertibleExercise 로 window(권리행사일 begin~end) delete+insert 반영.
/// 날짜 스윕(RGT_STD_DT=권리행사일)이라 창 재구성으로 삭제분 반영. begin/end 는 이번 스윕 창.
///
public static async Task<(int Inserted, int Deleted)> ReplaceExerciseWindowAsync(IAppDbContext db, DateOnly begin, DateOnly end, IReadOnlyList 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);
}
}