namespace KArtSell.Integration.Tests.PortfolioReconciliation; using System; using System.Collections.Generic; using System.Threading.Tasks; using KArtSell.Modules.ModelOperations.PortfolioReconciliation; using Xunit; public class ReconciliationEngineTests { private readonly CostBasisCalculator _costCalc; private readonly MismatchDetector _mismatchDetector; public ReconciliationEngineTests() { _costCalc = new CostBasisCalculator(); _mismatchDetector = new MismatchDetector(); } [Fact] public void CalculateWeightedAverageCost_BuyFirst_Success() { // Arrange int previousQuantity = 0; decimal previousCostBasis = 0m; int buyQuantity = 100; decimal buyPrice = 150m; // Act var result = _costCalc.CalculateWeightedAverageCost( previousQuantity, previousCostBasis, buyQuantity, buyPrice); // Assert Assert.Equal(150m, result); } [Fact] public void CalculateWeightedAverageCost_SecondBuy_Success() { // Arrange int previousQuantity = 100; decimal previousCostBasis = 15000m; // 100 * 150 int buyQuantity = 50; decimal buyPrice = 160m; // Act var result = _costCalc.CalculateWeightedAverageCost( previousQuantity, previousCostBasis, buyQuantity, buyPrice); // Assert var expected = (15000m + (50 * 160m)) / 150m; // (15000 + 8000) / 150 = 153.33 Assert.Equal(expected, result, 2); } [Fact] public void CalculateRealizedGainLoss_Profit_Success() { // Arrange int sellQuantity = 100; decimal sellPrice = 160m; decimal weightedAverageCost = 150m; // Act var result = _costCalc.CalculateRealizedGainLoss( sellQuantity, sellPrice, weightedAverageCost); // Assert Assert.Equal(1000m, result); // (160 - 150) * 100 = 1000 } [Fact] public void CalculateRealizedGainLoss_Loss_Success() { // Arrange int sellQuantity = 100; decimal sellPrice = 140m; decimal weightedAverageCost = 150m; // Act var result = _costCalc.CalculateRealizedGainLoss( sellQuantity, sellPrice, weightedAverageCost); // Assert Assert.Equal(-1000m, result); // (140 - 150) * 100 = -1000 } [Fact] public void CalculateUnrealizedGainLoss_Profit_Success() { // Arrange decimal marketValue = 18000m; decimal totalCostBasis = 15000m; // Act var result = _costCalc.CalculateUnrealizedGainLoss(marketValue, totalCostBasis); // Assert Assert.Equal(3000m, result); } [Fact] public void CalculateUnrealizedGainLoss_Loss_Success() { // Arrange decimal marketValue = 12000m; decimal totalCostBasis = 15000m; // Act var result = _costCalc.CalculateUnrealizedGainLoss(marketValue, totalCostBasis); // Assert Assert.Equal(-3000m, result); } [Fact] public void AllocateLotsFifo_Success() { // Arrange var lots = new List { new Lot { Id = Guid.NewGuid(), Quantity = 50, UnitCost = 100m, FifoOrder = 1 }, new Lot { Id = Guid.NewGuid(), Quantity = 100, UnitCost = 110m, FifoOrder = 2 } }; // Act var result = _costCalc.AllocateLotsFifo(lots, 120); // Assert Assert.Equal(2, result.Count); Assert.Equal(50, result[0].Quantity); Assert.Equal(70, result[1].Quantity); } [Fact] public void AllocateLotsFifo_InsufficientQuantity_Throws() { // Arrange var lots = new List { new Lot { Id = Guid.NewGuid(), Quantity = 50, UnitCost = 100m, FifoOrder = 1 } }; // Act & Assert Assert.Throws(() => _costCalc.AllocateLotsFifo(lots, 100)); } [Fact] public void DetectQuantityVariance_NoVariance_ReturnsNull() { // Arrange var mismatches = _mismatchDetector.DetectMismatches( approvedQuantity: 100, executedQuantity: 100, approvedPrice: 150m, executedPrice: 150m, tradeDate: DateTime.UtcNow, expectedSettlementDate: DateTime.UtcNow.AddDays(2), actualSettlementDate: DateTime.UtcNow.AddDays(2), ledgerCostBasis: 15000m, calculatedCostBasis: 15000m, now: DateTime.UtcNow); // Assert Assert.Empty(mismatches); } [Fact] public void DetectQuantityVariance_VarianceDetected_ReturnsMismatch() { // Arrange // 100 -> 99 = 1% variance (exceeds 0.1% threshold) var mismatches = _mismatchDetector.DetectMismatches( approvedQuantity: 100, executedQuantity: 99, approvedPrice: 150m, executedPrice: 150m, tradeDate: DateTime.UtcNow, expectedSettlementDate: DateTime.UtcNow.AddDays(2), actualSettlementDate: DateTime.UtcNow.AddDays(2), ledgerCostBasis: 15000m, calculatedCostBasis: 14850m, now: DateTime.UtcNow); // Assert Assert.NotEmpty(mismatches); var quantityMismatch = mismatches.FirstOrDefault(m => m.Type == MismatchType.QuantityVariance); Assert.NotNull(quantityMismatch); Assert.Equal(MismatchSeverity.High, quantityMismatch.Severity); } [Fact] public void DetectPriceVariance_VarianceDetected_ReturnsMismatch() { // Arrange // 150 -> 153 = 2% variance (exceeds 2% threshold = at boundary) // Actually 150 -> 153.1 = 2.07% (exceeds) var mismatches = _mismatchDetector.DetectMismatches( approvedQuantity: 100, executedQuantity: 100, approvedPrice: 150m, executedPrice: 153.1m, // 2.07% tradeDate: DateTime.UtcNow, expectedSettlementDate: DateTime.UtcNow.AddDays(2), actualSettlementDate: DateTime.UtcNow.AddDays(2), ledgerCostBasis: 15000m, calculatedCostBasis: 15310m, now: DateTime.UtcNow); // Assert Assert.NotEmpty(mismatches); var priceMismatch = mismatches.FirstOrDefault(m => m.Type == MismatchType.PriceVariance); Assert.NotNull(priceMismatch); Assert.Equal(MismatchSeverity.Medium, priceMismatch.Severity); } [Fact] public void DetectSettlementDelay_DelayDetected_ReturnsMismatch() { // Arrange var expectedDate = DateTime.UtcNow.AddDays(-1); var actualDate = DateTime.UtcNow.AddDays(2); // 3 days late var mismatches = _mismatchDetector.DetectMismatches( approvedQuantity: 100, executedQuantity: 100, approvedPrice: 150m, executedPrice: 150m, tradeDate: DateTime.UtcNow.AddDays(-5), expectedSettlementDate: expectedDate, actualSettlementDate: actualDate, ledgerCostBasis: 15000m, calculatedCostBasis: 15000m, now: DateTime.UtcNow); // Assert Assert.NotEmpty(mismatches); var timingMismatch = mismatches.FirstOrDefault(m => m.Type == MismatchType.SettlementDelay); Assert.NotNull(timingMismatch); } [Fact] public void DetectCostBasisMismatch_MismatchDetected_ReturnsMismatch() { // Arrange var mismatches = _mismatchDetector.DetectMismatches( approvedQuantity: 100, executedQuantity: 100, approvedPrice: 150m, executedPrice: 150m, tradeDate: DateTime.UtcNow, expectedSettlementDate: DateTime.UtcNow.AddDays(2), actualSettlementDate: DateTime.UtcNow.AddDays(2), ledgerCostBasis: 15000.00m, calculatedCostBasis: 14999.50m, // $0.50 delta now: DateTime.UtcNow); // Assert Assert.NotEmpty(mismatches); var costMismatch = mismatches.FirstOrDefault(m => m.Type == MismatchType.CostBasisMismatch); Assert.NotNull(costMismatch); } [Fact] public void RequiresEscalation_HighSeverity_ReturnsTrue() { // Arrange var mismatches = new List { new Mismatch { Severity = MismatchSeverity.High } }; // Act var result = _mismatchDetector.RequiresEscalation(mismatches); // Assert Assert.True(result); } [Fact] public void RequiresEscalation_MediumOnly_ReturnsFalse() { // Arrange var mismatches = new List { new Mismatch { Severity = MismatchSeverity.Medium } }; // Act var result = _mismatchDetector.RequiresEscalation(mismatches); // Assert Assert.False(result); } [Fact] public void VerifyCostBasis_Correct_ReturnsTrue() { // Arrange decimal calculated = 15000.00m; decimal expected = 15000.01m; // Act var result = _costCalc.VerifyCostBasis(calculated, expected, tolerance: 0.05m); // Assert Assert.True(result); } [Fact] public void VerifyCostBasis_OutOfTolerance_ReturnsFalse() { // Arrange decimal calculated = 15000.00m; decimal expected = 14999.50m; // Act var result = _costCalc.VerifyCostBasis(calculated, expected, tolerance: 0.1m); // Assert Assert.False(result); } }