SeibroCorpActionImport.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. using Application.Abstractions.Data;
  2. using Domain.Entities.Stocks;
  3. using Microsoft.EntityFrameworkCore;
  4. namespace Infrastructure.StockData;
  5. /// <summary>
  6. /// SeibroCorpActionSyncService 의 DB 반영 로직 분리 (테스트 가능) — HTTP 없이 파서 출력(fixture)으로 검증한다
  7. /// (SeibroSupplyImport 패턴 미러링). 일곱 엔티티(총회·안건·상호변경·대금·단주·CB대상·CB행사) upsert / window delete+insert 담당.
  8. /// </summary>
  9. public static class SeibroCorpActionImport
  10. {
  11. /// <summary>
  12. /// getGmeetInfo 결과를 GeneralMeeting 로 upsert — UQ (IssucoCustno, RgtStdDt, GmetKacd).
  13. /// rows 는 (회사, 기준일) per 콜 결과. 같은 콜 내 자연키 중복은 첫 행 채택.
  14. /// </summary>
  15. public static async Task<(int Inserted, int Updated)> UpsertGeneralMeetingsAsync(IAppDbContext db, IReadOnlyList<SeibroGmeetParser.Row> rows, CancellationToken ct)
  16. {
  17. if (rows.Count == 0)
  18. {
  19. return (0, 0);
  20. }
  21. var custnos = rows.Select(c => c.IssucoCustno).Distinct().ToList();
  22. var stdDts = rows.Select(c => c.RgtStdDt).Distinct().ToList();
  23. var existing = await db.GeneralMeeting.Where(c => custnos.Contains(c.IssucoCustno) && stdDts.Contains(c.RgtStdDt)).ToListAsync(ct);
  24. var byKey = existing.ToDictionary(c => (c.IssucoCustno, c.RgtStdDt, c.GmetKacd));
  25. var seen = new HashSet<(int, DateOnly, string)>();
  26. var inserted = 0;
  27. var updated = 0;
  28. foreach (var row in rows)
  29. {
  30. var key = (row.IssucoCustno, row.RgtStdDt, row.GmetKacd);
  31. if (!seen.Add(key))
  32. {
  33. continue;
  34. }
  35. if (byKey.TryGetValue(key, out var entity))
  36. {
  37. var was = entity.UpdatedAt;
  38. 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);
  39. if (entity.UpdatedAt != was)
  40. {
  41. updated++;
  42. }
  43. }
  44. else
  45. {
  46. 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);
  47. await db.GeneralMeeting.AddAsync(created, ct);
  48. byKey[key] = created;
  49. inserted++;
  50. }
  51. }
  52. await db.SaveChangesAsync(ct);
  53. return (inserted, updated);
  54. }
  55. /// <summary>
  56. /// getGmeetMeasureInfo 결과를 MeetingAgenda 로 upsert — UQ (IssucoCustno, RgtStdDt, AgendaSeq).
  57. /// rows 는 (회사, 기준일) per 콜 결과(여러 안건).
  58. /// </summary>
  59. public static async Task<(int Inserted, int Updated)> UpsertMeetingAgendasAsync(IAppDbContext db, IReadOnlyList<SeibroGmeetMeasureParser.Row> rows, CancellationToken ct)
  60. {
  61. if (rows.Count == 0)
  62. {
  63. return (0, 0);
  64. }
  65. var custnos = rows.Select(c => c.IssucoCustno).Distinct().ToList();
  66. var stdDts = rows.Select(c => c.RgtStdDt).Distinct().ToList();
  67. var existing = await db.MeetingAgenda.Where(c => custnos.Contains(c.IssucoCustno) && stdDts.Contains(c.RgtStdDt)).ToListAsync(ct);
  68. var byKey = existing.ToDictionary(c => (c.IssucoCustno, c.RgtStdDt, c.AgendaSeq));
  69. var seen = new HashSet<(int, DateOnly, int)>();
  70. var inserted = 0;
  71. var updated = 0;
  72. foreach (var row in rows)
  73. {
  74. var key = (row.IssucoCustno, row.RgtStdDt, row.AgendaSeq);
  75. if (!seen.Add(key))
  76. {
  77. continue;
  78. }
  79. if (byKey.TryGetValue(key, out var entity))
  80. {
  81. var was = entity.UpdatedAt;
  82. 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);
  83. if (entity.UpdatedAt != was)
  84. {
  85. updated++;
  86. }
  87. }
  88. else
  89. {
  90. 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);
  91. await db.MeetingAgenda.AddAsync(created, ct);
  92. byKey[key] = created;
  93. inserted++;
  94. }
  95. }
  96. await db.SaveChangesAsync(ct);
  97. return (inserted, updated);
  98. }
  99. /// <summary>
  100. /// getFmnmAltInfo 결과를 CorpNameChange 로 upsert — UQ (IssucoCustno, RgtStdDt).
  101. /// rows 는 (회사, 기준일) per 콜 결과(보통 1행).
  102. /// </summary>
  103. public static async Task<(int Inserted, int Updated)> UpsertNameChangesAsync(IAppDbContext db, IReadOnlyList<SeibroFmnmAltParser.Row> rows, CancellationToken ct)
  104. {
  105. if (rows.Count == 0)
  106. {
  107. return (0, 0);
  108. }
  109. var custnos = rows.Select(c => c.IssucoCustno).Distinct().ToList();
  110. var stdDts = rows.Select(c => c.RgtStdDt).Distinct().ToList();
  111. var existing = await db.CorpNameChange.Where(c => custnos.Contains(c.IssucoCustno) && stdDts.Contains(c.RgtStdDt)).ToListAsync(ct);
  112. var byKey = existing.ToDictionary(c => (c.IssucoCustno, c.RgtStdDt));
  113. var seen = new HashSet<(int, DateOnly)>();
  114. var inserted = 0;
  115. var updated = 0;
  116. foreach (var row in rows)
  117. {
  118. var key = (row.IssucoCustno, row.RgtStdDt);
  119. if (!seen.Add(key))
  120. {
  121. continue;
  122. }
  123. if (byKey.TryGetValue(key, out var entity))
  124. {
  125. var was = entity.UpdatedAt;
  126. entity.Update(row.FmnmBfaltKorNm, row.FmnmBfaltEngNm, row.FmnmAltKorNm, row.FmnmAltEngNm, row.MixusCirclBeginDt, row.MixusCirclExpryDt, row.UniCirclDt);
  127. if (entity.UpdatedAt != was)
  128. {
  129. updated++;
  130. }
  131. }
  132. else
  133. {
  134. var created = CorpNameChange.Create(row.IssucoCustno, row.RgtStdDt, row.FmnmBfaltKorNm, row.FmnmBfaltEngNm, row.FmnmAltKorNm, row.FmnmAltEngNm, row.MixusCirclBeginDt, row.MixusCirclExpryDt, row.UniCirclDt);
  135. await db.CorpNameChange.AddAsync(created, ct);
  136. byKey[key] = created;
  137. inserted++;
  138. }
  139. }
  140. await db.SaveChangesAsync(ct);
  141. return (inserted, updated);
  142. }
  143. /// <summary>
  144. /// getCostPaySchedul 결과를 PaymentSchedule 로 window(지급일+구분) delete+insert 반영.
  145. /// 날짜 스윕(TH1_PAY_TERM_BEGIN_DT × PAY_COST_TPCD)이라 자연키 대신 (지급일, 구분) 창 재구성. 응답에 없는 지급일/구분은 요청값 스탬핑.
  146. /// 같은 창 내 중복 (회사, ISIN, 권리기준일) 방어.
  147. /// </summary>
  148. public static async Task<(int Inserted, int Deleted)> ReplacePaymentScheduleWindowAsync(IAppDbContext db, DateOnly payDate, string payCostTpcd, IReadOnlyList<SeibroCostPaySchedulParser.Row> rows, CancellationToken ct)
  149. {
  150. var deleted = await db.PaymentSchedule.Where(c => c.Th1PayTermBeginDt == payDate && c.PayCostTpcd == payCostTpcd).ExecuteDeleteAsync(ct);
  151. var seen = new HashSet<(int, string, DateOnly?)>();
  152. var inserted = 0;
  153. foreach (var row in rows)
  154. {
  155. var key = (row.IssucoCustno, row.Isin, row.RgtStdDt);
  156. if (!seen.Add(key))
  157. {
  158. continue;
  159. }
  160. var created = PaymentSchedule.Create(row.IssucoCustno, row.Isin, row.KorSecnNm, row.SecnKacd, row.RgtStdDt, row.RgtRacdNm, row.RgtRsnDetailSortCd, payCostTpcd, payDate);
  161. await db.PaymentSchedule.AddAsync(created, ct);
  162. inserted++;
  163. }
  164. await db.SaveChangesAsync(ct);
  165. return (inserted, deleted);
  166. }
  167. /// <summary>
  168. /// getOddLotInfo 결과를 OddLotPayment 로 upsert — UQ (IssucoCustno, PayDate, RgtStdDt).
  169. /// 응답에 없는 회사번호·지급일은 요청값을 스탬핑. rows 는 (회사, 지급일) per 콜 결과.
  170. /// </summary>
  171. public static async Task<(int Inserted, int Updated)> UpsertOddLotsAsync(IAppDbContext db, int issucoCustno, DateOnly payDate, IReadOnlyList<SeibroOddLotParser.Row> rows, CancellationToken ct)
  172. {
  173. if (rows.Count == 0)
  174. {
  175. return (0, 0);
  176. }
  177. var existing = await db.OddLotPayment.Where(c => c.IssucoCustno == issucoCustno && c.PayDate == payDate).ToListAsync(ct);
  178. var byKey = existing.ToDictionary(c => c.RgtStdDt);
  179. var seen = new HashSet<DateOnly>();
  180. var inserted = 0;
  181. var updated = 0;
  182. foreach (var row in rows)
  183. {
  184. if (!seen.Add(row.RgtStdDt))
  185. {
  186. continue;
  187. }
  188. if (byKey.TryGetValue(row.RgtStdDt, out var entity))
  189. {
  190. var was = entity.UpdatedAt;
  191. entity.Update(row.RgtRacd, row.RgtRacdNm, row.RgtRsnDetailSortCd, row.EstmStdTpcd, row.EstmStdDt, row.EstmStdprc, row.RostCloseBeginDt, row.RostCloseExpryDt, row.XrgtDt);
  192. if (entity.UpdatedAt != was)
  193. {
  194. updated++;
  195. }
  196. }
  197. else
  198. {
  199. 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);
  200. await db.OddLotPayment.AddAsync(created, ct);
  201. byKey[row.RgtStdDt] = created;
  202. inserted++;
  203. }
  204. }
  205. await db.SaveChangesAsync(ct);
  206. return (inserted, updated);
  207. }
  208. /// <summary>
  209. /// getXrcStkStatInfo 결과를 ConvertibleBondTarget 로 upsert — UQ (BondIsin, XrcStkIsin). rolling per-채권ISIN 1콜 결과.
  210. /// </summary>
  211. public static async Task<(int Inserted, int Updated)> UpsertBondTargetsAsync(IAppDbContext db, IReadOnlyList<SeibroXrcStkStatParser.Row> rows, CancellationToken ct)
  212. {
  213. if (rows.Count == 0)
  214. {
  215. return (0, 0);
  216. }
  217. var bondIsins = rows.Select(c => c.BondIsin).Distinct().ToList();
  218. var existing = await db.ConvertibleBondTarget.Where(c => bondIsins.Contains(c.BondIsin)).ToListAsync(ct);
  219. var byKey = existing.ToDictionary(c => (c.BondIsin, c.XrcStkIsin));
  220. var seen = new HashSet<(string, string)>();
  221. var inserted = 0;
  222. var updated = 0;
  223. foreach (var row in rows)
  224. {
  225. var key = (row.BondIsin, row.XrcStkIsin);
  226. if (!seen.Add(key))
  227. {
  228. continue;
  229. }
  230. if (byKey.TryGetValue(key, out var entity))
  231. {
  232. var was = entity.UpdatedAt;
  233. entity.Update(row.BondSecnNm, row.BondKindNm, row.StkSecnNm, row.WrtbIsin, row.WrtbSecnNm, row.XrcRatio, row.XrcPrice, row.SetaccEndtermLimitDays, row.NewstkAllocDdBfLimitDays, row.DmanLimitRsnContent);
  234. if (entity.UpdatedAt != was)
  235. {
  236. updated++;
  237. }
  238. }
  239. else
  240. {
  241. 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);
  242. await db.ConvertibleBondTarget.AddAsync(created, ct);
  243. byKey[key] = created;
  244. inserted++;
  245. }
  246. }
  247. await db.SaveChangesAsync(ct);
  248. return (inserted, updated);
  249. }
  250. /// <summary>
  251. /// getXrcStkOptionXrcInfo 결과를 ConvertibleExercise 로 window(권리행사일 begin~end) delete+insert 반영.
  252. /// 날짜 스윕(RGT_STD_DT=권리행사일)이라 창 재구성으로 삭제분 반영. begin/end 는 이번 스윕 창.
  253. /// </summary>
  254. public static async Task<(int Inserted, int Deleted)> ReplaceExerciseWindowAsync(IAppDbContext db, DateOnly begin, DateOnly end, IReadOnlyList<SeibroXrcStkOptionXrcParser.Row> rows, CancellationToken ct)
  255. {
  256. var deleted = await db.ConvertibleExercise.Where(c => c.RgtStdDt >= begin && c.RgtStdDt <= end).ExecuteDeleteAsync(ct);
  257. var inserted = 0;
  258. foreach (var row in rows)
  259. {
  260. if (row.RgtStdDt < begin || row.RgtStdDt > end)
  261. {
  262. continue; // 스윕 창 밖 — 다른 창이 담당
  263. }
  264. 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);
  265. await db.ConvertibleExercise.AddAsync(created, ct);
  266. inserted++;
  267. }
  268. await db.SaveChangesAsync(ct);
  269. return (inserted, deleted);
  270. }
  271. }