public async Task TestCreateMatched() { var files = Enumerable.Range(2010, 10).Select(o => $"output_{o}.se"); var roots = await TestingTools.ReadSIEFiles(files); var allAccountTypes = roots.SelectMany(o => o.Children).OfType <AccountRecord>().GroupBy(o => o.AccountId).ToDictionary(o => o.Key, o => string.Join(" | ", o.Select(o => o.AccountName).Distinct())); var allVouchers = roots.SelectMany(o => o.Children).OfType <VoucherRecord>(); var transactionsWithUndefinedAccounts = allVouchers.SelectMany(o => o.Transactions.Where(tx => !allAccountTypes.ContainsKey(tx.AccountId)).Select(tx => new { tx.CompanyName, tx.AccountId })); Assert.False(transactionsWithUndefinedAccounts.Any()); var matchResult = MatchSLRResult.MatchSLRVouchers(allVouchers, VoucherRecord.DefaultIgnoreVoucherTypes); //All must have a single (24400|15200) transaction var transactionsMissingRequired = matchResult.Matches.SelectMany(o => new[] { o.SLR, o.Other }) .Where(o => o.Transactions.Count(tx => TransactionMatched.RequiredAccountIds.Contains(tx.AccountId)) != 1); Assert.False(transactionsMissingRequired.Any()); Assert.Equal(0, matchResult.Matches.Count(o => o.Other.TransactionsNonAdminOrCorrections.Count() > 1)); var txs = TransactionMatched.FromVoucherMatches(matchResult, TransactionMatched.RequiredAccountIds); var dbg = string.Join("\n", txs.OrderBy(o => o.DateRegistered ?? LocalDate.MinIsoValue)); }
public async Task TestMethod1() { Func <int, bool> accountFilter = accountId => accountId.ToString().StartsWith("45"); //Load SBC invoices var fromSBC = (await Tools.LoadSBCInvoices(accountFilter)).Select(o => new SBCVariant { AccountId = o.AccountId, Amount = o.Amount, CompanyName = o.Supplier, DateRegistered = NodaTime.LocalDate.FromDateTime(o.RegisteredDate), DateFinalized = o.PaymentDate.HasValue ? NodaTime.LocalDate.FromDateTime(o.PaymentDate.Value) : (NodaTime.LocalDate?)null, Source = o, }).ToList(); //Load SIE vouchers List <TransactionMatched> fromSIE; { var files = Enumerable.Range(2010, 10).Select(o => $"output_{o}.se"); var sieDir = Tools.GetOutputFolder("SIE"); var roots = await SBCExtensions.ReadSIEFiles(files.Select(file => Path.Combine(sieDir, file))); var allVouchers = roots.SelectMany(o => o.Children).OfType <VoucherRecord>(); var matchResult = MatchSLRResult.MatchSLRVouchers(allVouchers, VoucherRecord.DefaultIgnoreVoucherTypes); fromSIE = TransactionMatched.FromVoucherMatches(matchResult, TransactionMatched.RequiredAccountIds).Where(o => accountFilter(o.AccountId)).ToList(); } var sbcByName = fromSBC.GroupBy(o => o.CompanyName).ToDictionary(o => o.Key, o => o.ToList()); var sieByName = fromSIE.GroupBy(o => o.CompanyName).ToDictionary(o => o.Key, o => o.ToList()); //Create name lookup (can be truncated in one source but not the other): Dictionary <string, string> nameLookup; { var(Intersection, OnlyInA, OnlyInB) = IntersectInfo(sbcByName.Keys, sieByName.Keys); nameLookup = Intersection.ToDictionary(o => o, o => o); AddLookups(OnlyInA, OnlyInB); AddLookups(OnlyInB, OnlyInA); void AddLookups(List <string> enumA, List <string> enumB) { for (int i = enumA.Count - 1; i >= 0; i--) { var itemA = enumA[i]; var itemB = enumB.FirstOrDefault(o => o.StartsWith(itemA)); if (itemB != null) { enumB.Remove(itemB); enumA.Remove(itemA); nameLookup.Add(itemB, itemA); nameLookup.Add(itemA, itemB); } } } //Non-matched: intersectInfo.OnlyInA and intersectInfo.OnlyInB } var matches = new List <(TransactionMatched, SBCVariant)>(); foreach (var(sieName, sieList) in sieByName) { if (nameLookup.ContainsKey(sieName)) { var inSbc = sbcByName[nameLookup[sieName]].GroupBy(o => o.Amount).ToDictionary(o => o.Key, o => o.ToList()); //Multiple passes of the following until no more matches while (true) { var newMatches = new List <(TransactionMatched, SBCVariant)>(); for (int sieIndex = sieList.Count - 1; sieIndex >= 0; sieIndex--) { var item = sieList[sieIndex]; if (inSbc.TryGetValue(item.Amount, out var sbcSameAmount)) { var sbcSameAmountAccount = sbcSameAmount.Where(o => o.AccountId == item.AccountId); //Find those with same register date (could be many) //If multiple or none, take those with closest payment date. //Remove match from inSbc so it can't be matched again var found = new List <SBCVariant>(); if (item.DateRegistered is NodaTime.LocalDate dateRegistered) { found = sbcSameAmountAccount.Where(o => (dateRegistered - item.DateRegistered.Value).Days <= 1).ToList(); } else { found = sbcSameAmountAccount.Where(o => (o.DateFinalized.HasValue && item.DateFinalized.HasValue) && (o.DateFinalized.Value - item.DateFinalized.Value).Days <= 1).ToList(); } if (found.Count > 1) { var orderByDateDiff = found .Where(o => (o.DateFinalized.HasValue && item.DateFinalized.HasValue)) .Select(o => new { #pragma warning disable CS8629 // Nullable value type may be null. Diff = Math.Abs((o.DateFinalized.Value - item.DateFinalized.Value).Days), #pragma warning restore CS8629 // Nullable value type may be null. Object = o }) .OrderBy(o => o.Diff); var minDiff = orderByDateDiff.First().Diff; if (orderByDateDiff.Count(o => o.Diff == minDiff) == 1) { found = orderByDateDiff.Take(1).Select(o => o.Object).ToList(); } } if (found.Count == 1) { newMatches.Add((item, found.Single())); sieList.RemoveAt(sieIndex); sbcSameAmount.Remove(found.First()); } } } if (!newMatches.Any()) { break; } matches.AddRange(newMatches); } } } var nonmatchedSBC = sbcByName.Values.SelectMany(o => o).Except(matches.Select(o => o.Item2)); //Remove cancelled-out pairs (same everything but opposite amount): var cancelling = nonmatchedSBC.GroupBy(o => $"{o.CompanyName} {Math.Abs(o.Amount)} {o.DateRegistered?.ToSimpleDateString()} {o.DateFinalized?.ToSimpleDateString()}") .Where(o => o.Count() == 2 && o.Sum(o => o.Amount) == 0).SelectMany(o => o); nonmatchedSBC = nonmatchedSBC.Except(cancelling); var nonmatched = nonmatchedSBC.Concat(sieByName.Values.SelectMany(o => o).Except(matches.Select(o => o.Item1))); var all = matches.Select(o => o.Item1).Concat(nonmatched); var sss = string.Join("\n", all .OrderBy(o => o.CompanyName).ThenBy(o => o.DateRegistered) .Select(o => $"{o.CompanyName}\t{o.Amount}\t{o.AccountId}\t{o.DateRegistered?.ToSimpleDateString()}\t{o.DateFinalized?.ToSimpleDateString()}\t{(nonmatched.Contains(o) ? "X" : "")}\t{((o is SBCVariant) ? "SBC" : "")}") ); }