356 lines
23 KiB
C#
356 lines
23 KiB
C#
using YLErp.BLL;
|
||
using YLErp.DBModels;
|
||
using YLErp.Enums;
|
||
using YLErp.Modules.MarginModule;
|
||
using YLErp.Modules.TradeModule;
|
||
using YLErp.Helpers;
|
||
|
||
namespace YLErp.Modules.SwapModule.Margin
|
||
{
|
||
/// <summary>
|
||
/// R3 阶段四 §4.1:合约维度(MarginWatchRule==0)规则15 交易日终结算产生"追加保证金"。
|
||
/// 交易维度追加保证金 = 维持保证金(阶段三引擎 trade_span 产出)− 已缴保证金净额
|
||
/// (应付预付金现金净收额 + 初始授信占用净额——
|
||
/// 2026-08-27 修正:授信垫付的初始预付金不产生应付预付金流水,此前未计入已缴导致每个结算日按维持全额重复开追加);
|
||
/// 需求上升只增不减、回落不返还。
|
||
/// 腿化改造(2026-08-27):增量>0 时生成一条与手工追加预付金同形态的交易腿(InterestMode=追加预付金、
|
||
/// OptName="EOD追保" 打标),并复用手工确认链路的 SwapFundTagService.ApplyMarginFundTags 完成授信/现金簿记
|
||
/// (资金来源走腿 fund_tag→交易级 margin_fund_source 回退链;授信部分写授信出入表占用绑腿、不产生流水;
|
||
/// 现金部分出 Action=系统操作-追加保证金 流水、Deal=现金腿 id——Action 与手工腿簿记的"应付预付金"区隔,
|
||
/// 保住 EQD-6952 资金通知书聚合口径);平仓返还(ReleaseMarginByTag)与交易回退清理由既有链路自动获得。
|
||
/// 幂等:重跑先清(本结算日起 EOD 旧追保腿及其簿记)后建;eod_swap_position 当日行显式补写(方案A——
|
||
/// 腿生成晚于当日 SwapPositionCompose,快照须补齐,见 SwapEodPositionService.SaveEodAdditionalMarginPosition)。
|
||
/// 由 EOD 在客户资金计算之前调用:当日新记录计入当日出入金窗口并翻"已结算"。
|
||
/// 客户维度(MarginWatchRule=1/NULL)不产生资金记录(§0 占用口径),不在本服务范围。
|
||
/// </summary>
|
||
public class SwapAdditionalMarginService : YLBaseService
|
||
{
|
||
private static readonly IYcLogger logger = LogFactory.GetLogger("EOD追保");
|
||
|
||
/// <summary>
|
||
/// EOD 追保腿打标(OptName):与手工追加预付金腿(OptName=操作员实名)区分,
|
||
/// 幂等清理、RemoveByTrade 保护与时间轴回退清理均以此识别。拆单现金腿落库时被打服务身份,簿记后回打本标识。
|
||
/// </summary>
|
||
public const string EodOptName = "EOD追保";
|
||
|
||
public SwapAdditionalMarginService(OptUserInfo userInfo) : base(userInfo)
|
||
{
|
||
}
|
||
|
||
public SwapAdditionalMarginService(YLBaseService baseService) : base(baseService)
|
||
{
|
||
}
|
||
|
||
/// <summary>
|
||
/// EOD 追保腿识别(InterestMode=追加预付金 + OptName 打标);
|
||
/// happenDateFrom 限定重跑/回退窗口起点,防误删更早历史日已归属腿。
|
||
/// </summary>
|
||
public static bool IsEodMarginLeg(swap_position leg, DateTime? happenDateFrom = null)
|
||
{
|
||
return leg != null
|
||
&& leg.InterestMode == (int)InterestModeEnum.追加预付金
|
||
&& leg.OptName == EodOptName
|
||
&& (happenDateFrom == null || leg.HappenDate >= happenDateFrom);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构造 EOD 追保腿(纯函数,便于单测;落库与 PosiNumber 回填由调用方完成):
|
||
/// 与手工追加预付金腿同形态(PositionType=0 资金腿、PosiDirection=0、IsInitial、InterestMode=追加预付金、
|
||
/// 收取方向、fix=增量两位舍入),FundTag=null 回退交易级资金来源,OptName 打标 EOD 追保。
|
||
/// </summary>
|
||
public static swap_position BuildEodMarginLeg(trade td, double increment, DateTime settleDate, int optId)
|
||
{
|
||
return new swap_position
|
||
{
|
||
SwapTradeId = td.id,
|
||
PositionType = 0,
|
||
PosiDirection = 0,
|
||
IsInitial = true,
|
||
InterestMode = (int)InterestModeEnum.追加预付金,
|
||
InterestDirection = (int)SwapDirectionEnum.收取,
|
||
InterestPrincipalFix = Math.Round(Convert.ToDecimal(increment), 2, MidpointRounding.AwayFromZero),
|
||
HappenDate = settleDate,
|
||
PosiStartDate = td.StartDate ?? settleDate,
|
||
PosiMatuirityDate = td.ExerciseDate,
|
||
Currency = td.SettlementCurrency ?? ConsGlobal.Currency.CNY,
|
||
FundTag = null,
|
||
OptId = optId,
|
||
OptName = EodOptName,
|
||
OptTime = DateTime.Now
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 结算日逐客户逐交易产生追加保证金(clientFilter 为部分结算的客户过滤,与 EOD 请求一致)。
|
||
/// 顶层兜异常日志(Error 级)后原样抛出,避免异常栈被 EOD 框架层吞掉无从定位。
|
||
/// </summary>
|
||
public void SettleAdditionalMargin(DateTime settleDate, List<int> clientFilter = null)
|
||
{
|
||
try
|
||
{
|
||
SettleAdditionalMarginCore(settleDate, clientFilter);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var filterDesc = clientFilter != null && clientFilter.Any() ? string.Join(",", clientFilter) : "全部";
|
||
logger.Error($"EOD追保:结算日{settleDate:yyyy-MM-dd}(客户范围:{filterDesc})追保腿生成异常", ex);
|
||
throw;
|
||
}
|
||
}
|
||
|
||
private void SettleAdditionalMarginCore(DateTime settleDate, List<int> clientFilter)
|
||
{
|
||
//合约维度盯市客户
|
||
var watchClientIds = DbContextFactory.GetClientDbContext(OptUser).client.AsNoTracking()
|
||
.Where(t => t.ProcessStatus != "未提交" && t.MarginWatchRule == 0)
|
||
.Select(t => t.id)
|
||
.ToList();
|
||
if (clientFilter != null && clientFilter.Any())
|
||
{
|
||
watchClientIds = watchClientIds.Where(t => clientFilter.Contains(t)).ToList();
|
||
}
|
||
if (watchClientIds.Count == 0)
|
||
{
|
||
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 无合约维度盯市(MarginWatchRule=0)客户,结束");
|
||
return;
|
||
}
|
||
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 合约维度盯市客户{watchClientIds.Count}个({string.Join(",", watchClientIds)})");
|
||
|
||
//存续中的互换交易(状态口径与 eodSwapQuery 一致,含当日已了结)
|
||
var tradeStatuses = ConsTrade.TradeStatusAfterConfirmed;
|
||
var trades = DbContext.trade.AsNoTracking()
|
||
.Where(t => tradeStatuses.Contains(t.TradeStatus)
|
||
&& t.ValidState != ConsGlobal.InValid
|
||
&& t.TradeType == "收益互换"
|
||
&& t.TradeDate <= settleDate
|
||
&& watchClientIds.Contains(t.ClientId))
|
||
.ToList();
|
||
if (trades.Count == 0)
|
||
{
|
||
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 盯市客户名下无存续收益互换交易,结束");
|
||
return;
|
||
}
|
||
|
||
//开始日门槛:未到交易开始日(未起息)的不参与追保——维持保证金引擎从成交日就产出 trade_span,
|
||
//初始预付金流水却到开始日才入账,不过滤会在 成交日~开始日 之间把未到期的初始预付金
|
||
//误判成缺口、按维持全额追加(2026-08-28 交易2571实证:07-31成交/08-03起息,重刷07-31误追500000)
|
||
var notStartedTrades = trades.Where(t => (t.StartDate ?? t.TradeDate) > settleDate).ToList();
|
||
if (notStartedTrades.Count > 0)
|
||
{
|
||
logger.Info($"EOD追保:{notStartedTrades.Count}笔交易未到开始日(id=[{string.Join(",", notStartedTrades.Select(t => t.id))}]),不参与追保结算");
|
||
trades = trades.Except(notStartedTrades).ToList();
|
||
if (trades.Count == 0)
|
||
{
|
||
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 交易均未到开始日,结束");
|
||
return;
|
||
}
|
||
}
|
||
|
||
//规则15(区间追保结构)交易:R1 三层级解析(BUG-02 修正,与引擎/确认书同口径)——
|
||
//交易绑定→客户默认→全局默认 找到即停,只配客户/全局默认模板的交易同样纳入追保结算
|
||
var candidateCount = trades.Count;
|
||
var templatesByTrade = MarginTemplateV2RateHelper.ResolveTieredTemplates(trades, settleDate, DbContext);
|
||
trades = trades.Where(t => templatesByTrade.TryGetValue(t.id, out var tpl)
|
||
&& tpl.RuleType == (int)MarginRuleTypeEnum.区间追保结构).ToList();
|
||
if (candidateCount > trades.Count)
|
||
{
|
||
logger.Info($"EOD追保:{candidateCount - trades.Count}笔交易模板非区间追保结构(或三级未命中),不参与追保结算");
|
||
}
|
||
var tradeIds = trades.Select(t => t.id).ToList();
|
||
if (tradeIds.Count == 0)
|
||
{
|
||
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 规则15交易为0,结束");
|
||
return;
|
||
}
|
||
|
||
//当日维持保证金(引擎产出,我方净收取为正),按交易合计(与 SwapSpanBalanceQueryService 缺口口径一致)
|
||
var maintenanceByTrade = DbContext.trade_span.AsNoTracking()
|
||
.Where(x => x.ValueDate == settleDate && tradeIds.Contains(x.TradeId) && x.Spv != null)
|
||
.GroupBy(x => x.TradeId)
|
||
.Select(g => new { TradeId = g.Key, Spv = g.Sum(x => x.Spv ?? 0d) })
|
||
.ToDictionary(x => x.TradeId, x => x.Spv);
|
||
|
||
//应付预付金净收额(客户付钱记负 → 取反为正;平仓返还自动冲减;口径与 EOD canonical 一致:非作废+已确认/已结算)
|
||
var payableNetByTrade = DbContext.ClientCashInCashOut.AsNoTracking()
|
||
.Where(x => x.TradeId != null && tradeIds.Contains(x.TradeId ?? 0)
|
||
&& x.Action == ClientCashInCashOut.系统操作_应付预付金
|
||
&& x.HappenDate <= settleDate
|
||
&& x.ValidState != ConsGlobal.InValid
|
||
&& (x.State == ClientCashInCashOut.已确认 || x.State == ClientCashInCashOut.已结算)
|
||
&& x.Money != null)
|
||
.GroupBy(x => x.TradeId)
|
||
.Select(g => new { TradeId = g.Key ?? 0, Sum = -g.Sum(x => x.Money ?? 0d) })
|
||
.ToDictionary(x => x.TradeId, x => x.Sum);
|
||
|
||
//追加保证金资金记录累计值(逐日增量记录求和即累计,BUG-03;口径与 EOD canonical 一致:非作废+已确认/已结算。
|
||
//腿化改造后现金流水 Deal=现金腿 id(原 Deal==0 条件去除):该 Action 只有 EOD 会写,手工链路写应付预付金,语义天然隔离)
|
||
var addRecordByTrade = DbContext.ClientCashInCashOut.AsNoTracking()
|
||
.Where(x => x.TradeId != null && tradeIds.Contains(x.TradeId ?? 0)
|
||
&& x.Action == ClientCashInCashOut.系统操作_追加保证金
|
||
&& x.ValidState != ConsGlobal.InValid
|
||
&& (x.State == ClientCashInCashOut.已确认 || x.State == ClientCashInCashOut.已结算)
|
||
&& x.Money != null)
|
||
.GroupBy(x => x.TradeId)
|
||
.Select(g => new { TradeId = g.Key ?? 0, Funded = -g.Sum(x => x.Money ?? 0d) })
|
||
.ToDictionary(x => x.TradeId, x => x.Funded);
|
||
|
||
//追加保证金授信占用累计(amount 占用记正 → 直接求和(BUG-01 修正口径);remark 前缀标识,见 ClientCreditInoutService)
|
||
var addCreditByTrade = DbContext.client_credit_inout.AsNoTracking()
|
||
.Where(x => x.trade_id != null && tradeIds.Contains(x.trade_id ?? 0)
|
||
&& x.remark.StartsWith(ClientCreditInoutService.AdditionalMarginRemark))
|
||
.GroupBy(x => x.trade_id)
|
||
.Select(g => new { TradeId = g.Key ?? 0, Funded = g.Sum(x => x.amount) })
|
||
.ToDictionary(x => x.TradeId, x => x.Funded);
|
||
|
||
//初始预付金的授信占用净额(非"追加保证金"前缀:簿记初始占用 + 平仓释放取负,Σ(amount) 即净已缴):
|
||
//授信腿不产生应付预付金流水,目标追加里只扣现金净收额会把授信垫付的初始预付金漏掉——
|
||
//每个结算日都按维持保证金全额重复开追加(BUG:多收授信占用/现金,2026-08-27 交易2538实证:初始授信200万未扣、首日全额追加280.8万)
|
||
var initCreditByTrade = DbContext.client_credit_inout.AsNoTracking()
|
||
.Where(x => x.trade_id != null && tradeIds.Contains(x.trade_id ?? 0)
|
||
&& (x.remark == null || !x.remark.StartsWith(ClientCreditInoutService.AdditionalMarginRemark)))
|
||
.GroupBy(x => x.trade_id)
|
||
.Select(g => new { TradeId = g.Key ?? 0, Funded = g.Sum(x => x.amount) })
|
||
.ToDictionary(x => x.TradeId, x => x.Funded);
|
||
|
||
var fundTagService = new SwapFundTagService(this);
|
||
var cashService = new ClientCashInCashOutService(this);
|
||
var eodPositionService = new SwapEodPositionService(this);
|
||
var flowEventService = new SwapFlowEventService(this);
|
||
|
||
var settledTrades = 0;
|
||
var settledLegs = 0;
|
||
var totalIncrement = 0d;
|
||
|
||
foreach (var clientGroup in trades.GroupBy(t => t.ClientId).OrderBy(g => g.Key))
|
||
{
|
||
foreach (var td in clientGroup.OrderBy(t => t.id))
|
||
{
|
||
if (!maintenanceByTrade.TryGetValue(td.id, out var maintenance) || maintenance <= 0)
|
||
{
|
||
logger.Info($"EOD追保:交易{td.id}(客户{td.ClientId})跳过——当日无trade_span维持保证金或维持≤0(maintenance={maintenance:0.00})");
|
||
continue;
|
||
}
|
||
//目标追加 = 维持保证金 − 已缴初始保证金净额(现金应付预付金净收额 + 授信初始占用净额,
|
||
//授信垫付与现金垫付同等对待,杜绝授信初始预付金被重复追加)
|
||
var payable = payableNetByTrade.TryGetValue(td.id, out var p) ? p : 0;
|
||
var initCredit = initCreditByTrade.TryGetValue(td.id, out var ic) ? ic : 0;
|
||
var payableNet = payable + initCredit;
|
||
var target = SwapAdditionalMarginCalc.CalcTarget(maintenance, payableNet);
|
||
if (target <= 0)
|
||
{
|
||
logger.Info($"EOD追保:交易{td.id}(客户{td.ClientId})跳过——目标追加≤0:维持={maintenance:0.00},已缴={payableNet:0.00}(应付净额{payable:0.00}+初始授信占用{initCredit:0.00})已覆盖");
|
||
continue;
|
||
}
|
||
var fundedCash = addRecordByTrade.TryGetValue(td.id, out var cash) ? cash : 0;
|
||
var fundedCredit = addCreditByTrade.TryGetValue(td.id, out var credit) ? credit : 0;
|
||
var increment = Math.Round(target - fundedCash - fundedCredit, 2, MidpointRounding.AwayFromZero);
|
||
if (increment <= 0)
|
||
{
|
||
//已补足;追保回落(目标下降)不返还——负缺口在可用资金公式(Σ维持−累计)体现
|
||
logger.Info($"EOD追保:交易{td.id}(客户{td.ClientId})跳过——增量≤0:目标={target:0.00},已补足(现金{fundedCash:0.00}+授信{fundedCredit:0.00}),追保回落不返还");
|
||
continue;
|
||
}
|
||
logger.Info($"EOD追保:交易{td.id}(客户{td.ClientId})需追加:维持={maintenance:0.00},已缴={payableNet:0.00}(应付净额{payable:0.00}+初始授信占用{initCredit:0.00}),目标={target:0.00},已补足(现金{fundedCash:0.00}+授信{fundedCredit:0.00}),本次增量={increment:0.00}");
|
||
|
||
//幂等清理:先删本结算日起 EOD 旧追保腿及其簿记(腿/流水/占用/快照同生共死),再按最新增量重建;
|
||
//手工追加预付金腿(OptName≠EOD追保)不受影响
|
||
CleanupEodMarginLegs(td, settleDate);
|
||
|
||
//建腿:与手工追加预付金同形态,FundTag=null 回退交易级资金来源
|
||
var legIdsBefore = DbContext.swap_position.Where(x => x.SwapTradeId == td.id).Select(x => x.id).ToList();
|
||
var leg = BuildEodMarginLeg(td, increment, settleDate, UserId);
|
||
DbContext.swap_position.Add(leg);
|
||
DbContext.SaveChanges();
|
||
leg.PosiNumber = $"{td.TradeNumber}-{leg.id}";
|
||
|
||
//簿记:复用手工确认链路的标签分配(额度内授信占账、跨界拆单、现金出流水),
|
||
//Action 独立为 追加保证金(EQD-6952 口径)、占用 remark 带"追加保证金"前缀(累计/清理链路硬性识别点);
|
||
//resetExistingBookings=false——本交易初始预付金占用不在此重写(EOD 只簿记本次新腿,旧追保簿记上方已清)
|
||
fundTagService.ApplyMarginFundTags(td, new List<swap_position> { leg }, cashService, ignoreMoneyCheck: false,
|
||
cashAction: ClientCashInCashOut.系统操作_追加保证金,
|
||
occupyRemark: ClientCreditInoutService.AdditionalMarginRemark + "占用",
|
||
splitOccupyRemark: ClientCreditInoutService.AdditionalMarginRemark + "拆单授信部分",
|
||
bookingDate: settleDate,
|
||
resetExistingBookings: false);
|
||
|
||
//本次新腿 = 追保腿 + 可能的拆单现金腿(SplitLeg 落库时打了服务身份 OptName,回打 EOD 标识保证幂等清理覆盖完整)
|
||
var newLegs = DbContext.swap_position.Where(x => x.SwapTradeId == td.id && x.IsInitial
|
||
&& x.InterestMode == (int)InterestModeEnum.追加预付金 && !legIdsBefore.Contains(x.id))
|
||
.ToList();
|
||
newLegs.ForEach(x => x.OptName = EodOptName);
|
||
DbContext.SaveChanges();
|
||
logger.Info($"EOD追保:交易{td.id} 追保腿落库完成——新腿{newLegs.Count}条(id=[{string.Join(",", newLegs.Select(x => x.id))}],含拆单现金腿)");
|
||
settledTrades++;
|
||
settledLegs += newLegs.Count;
|
||
totalIncrement += increment;
|
||
|
||
//实时持仓克隆 + 开仓事件(参照 TradeConfirmService 簿记后动作,但只针对本次新腿——
|
||
//整交易 InitialPosition 会把浮动腿实时持仓重置回开仓态、AddPositionEvent 会为全部腿重复建开仓事件,EOD 场景不可用);
|
||
//克隆继承定稿标签,供平仓返还分流与后续快照链使用
|
||
foreach (var newLeg in newLegs)
|
||
{
|
||
var clone = newLeg.Clone();
|
||
clone.id = 0;
|
||
clone.PositionId = newLeg.id;
|
||
clone.IsInitial = false;
|
||
DbContext.swap_position.Add(clone);
|
||
}
|
||
DbContext.SaveChanges();
|
||
flowEventService.InitEvent(newLegs, td, EodOptName);
|
||
DbContext.SaveChanges();
|
||
|
||
//方案A:腿生成晚于当日 SwapPositionCompose(当日 trade_span 结算后才可算增量),显式补写当日 eod_swap_position 行,
|
||
//使当日报表明细(PostionMarginGain 等)不漏计;下一结算日快照由 Compose 先清后建正常接管
|
||
foreach (var newLeg in newLegs)
|
||
{
|
||
eodPositionService.SaveEodAdditionalMarginPosition(td, newLeg, settleDate);
|
||
}
|
||
}
|
||
}
|
||
|
||
logger.Info($"EOD追保:结算日{settleDate:yyyy-MM-dd} 完成——规则15交易{tradeIds.Count}笔,{settledTrades}笔产生追保(腿{settledLegs}条,合计增量{totalIncrement:0.00})");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 幂等清理:删除本交易 本结算日起 EOD 生成的旧追保腿(含其实时克隆)及其簿记——
|
||
/// 现金流水(Action=追加保证金,Deal=腿id,按现行 DeleteTradeCashInCashOut 习惯硬删)、
|
||
/// 授信占用(position_id=腿id)、开仓事件、方案A 补写的当日及以后 eod_swap_position 行。
|
||
/// </summary>
|
||
private void CleanupEodMarginLegs(trade td, DateTime settleDate)
|
||
{
|
||
var oldLegs = DbContext.swap_position.Where(x => x.SwapTradeId == td.id
|
||
&& x.InterestMode == (int)InterestModeEnum.追加预付金
|
||
&& x.OptName == EodOptName
|
||
&& x.HappenDate >= settleDate)
|
||
.ToList();
|
||
if (oldLegs.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
var oldLegIds = oldLegs.Select(x => x.id).ToList();
|
||
//簿记(流水 Deal/占用 position_id/快照 PositionId/事件 PositionId)均绑期初腿(拆单现金腿也是 IsInitial 的独立腿)
|
||
var oldInitialIds = oldLegs.Where(x => x.IsInitial).Select(x => x.id).ToList();
|
||
var oldCashRecords = DbContext.ClientCashInCashOut.Where(x => x.TradeId == td.id
|
||
&& x.Action == ClientCashInCashOut.系统操作_追加保证金
|
||
&& oldInitialIds.Contains(x.Deal))
|
||
.ToList();
|
||
DbContext.ClientCashInCashOut.RemoveRange(oldCashRecords);
|
||
var oldCreditRecords = DbContext.client_credit_inout.Where(x => x.trade_id == td.id
|
||
&& x.position_id != null && oldInitialIds.Contains(x.position_id.Value))
|
||
.ToList();
|
||
DbContext.client_credit_inout.RemoveRange(oldCreditRecords);
|
||
var oldEvents = DbContext.swap_flow_event.Where(x => x.SwapTradeId == td.id
|
||
&& x.EventType == (int)SwapFlowEventTypeEnum.开仓 && oldInitialIds.Contains(x.PositionId))
|
||
.ToList();
|
||
DbContext.swap_flow_event.RemoveRange(oldEvents);
|
||
var oldEodRows = DbContext.eod_swap_position.Where(x => x.SwapTradeId == td.id
|
||
&& x.ValueDate >= settleDate && oldInitialIds.Contains(x.PositionId))
|
||
.ToList();
|
||
logger.Info($"EOD追保:交易{td.id} 幂等清理(重跑)——删EOD追保腿{oldLegs.Count}条(id=[{string.Join(",", oldLegs.Select(x => x.id))}])及其簿记:现金流水{oldCashRecords.Count}条、授信占用{oldCreditRecords.Count}条、开仓事件{oldEvents.Count}条、eod快照{oldEodRows.Count}行");
|
||
DbContext.eod_swap_position.RemoveRange(oldEodRows);
|
||
DbContext.swap_position.RemoveRange(oldLegs);
|
||
DbContext.SaveChanges();
|
||
}
|
||
}
|
||
}
|