using bnhtrade.Core.Data.Database; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Transactions; namespace bnhtrade.Core.Logic.Stock { public class SkuTransactionReconcile { private Data.Database.AmazonShipment.ReadShipmentInfo readShipmentInfo; private Logic.Stock.SkuTransactionCrud dbSkuTransaction; private Logic.Validate.SkuTransaction validateSkuTrans; private Logic.Stock.StatusReallocate stockReallocate; private Logic.Log.LogEvent log; private string err = "Reconcile Sku Transaction Exception: "; public SkuTransactionReconcile() { Innit(); dbSkuTransaction = new SkuTransactionCrud(); readShipmentInfo = new Data.Database.AmazonShipment.ReadShipmentInfo(); validateSkuTrans = new Validate.SkuTransaction(); stockReallocate = new Logic.Stock.StatusReallocate(); log = new Log.LogEvent(); } public int ItemsCompleted { get; private set; } public int ItemsRemaining { get; private set; } public DateTime LastItemDateTime { get; private set; } public string ProgressMessage { get; private set; } public bool ReconciliationComplete { get; private set; } public int CurrentTransactionId { get; private set; } public string CurrentTransactionTypeCode { get; private set; } public Model.Stock.SkuTransaction CurrentSkuTransaction { get; private set; } private void Innit() { ItemsCompleted = 0; ItemsRemaining = 0; LastItemDateTime = default(DateTime); ProgressMessage = null; ReconciliationComplete = false; CurrentTransactionId = 0; CurrentTransactionTypeCode = null; } /// /// Iterates through the stock transaction table and creates stock journal entries, if required /// N.B. This function does not make allowances for status' that can create stock (i.e. if a status does not have stock available, /// this function will halt processing rows) /// /// Process Amazon reports before starting process /// Download reports from Amazon before starting process /// public void ReconcileStockTransactions(bool updateTransactions, bool downloadAmazon = false) { Innit(); string currentMethodName = nameof(ReconcileStockTransactions); // ensure import table have been processed into transaction table without exception if (updateTransactions == true) { try { if (downloadAmazon) { new bnhtrade.Core.Logic.Import.AmazonFbaReimbursement().SyncDatabaseWithAmazon(); new bnhtrade.Core.Logic.Import.AmazonFbaInventoryLedgerDetail().SyncDatabaseWithAmazon(); } new bnhtrade.Core.Logic.Stock.SkuTransactionImport().ImportAll(); } catch (Exception ex) { ProgressMessage = "Precheck failed: " + ex.Message; return; } } log.LogInformation("Starting ReconcileStockTransactions()"); int recordSkip = 0; ReconcileFoundAndLost(); // get list of sku transactions to reconcile dbSkuTransaction.Init(); dbSkuTransaction.IsReconciled = false; var transList = dbSkuTransaction.Read(); var shipmentInfoDic = new Dictionary(); ItemsRemaining = transList.Count; ItemsCompleted = 0; // get list of sku transaction types var codeList = new List(); foreach(var item in transList) { codeList.Add(item.SkuTransactionTypeCode); } var transTypeDict = new Logic.Stock.SkuTransactionTypeCrud().GetByTypeCode(codeList); try { // loop through transaction list //for (int i = 0; i < transList.Count; i++) foreach (var transaction in transList) { using (var scope = new TransactionScope()) { Console.Write("\rProcessing record: {0} ({1} skipped)", (ItemsCompleted + 1 + recordSkip), recordSkip); // setup return values CurrentSkuTransaction = transaction; CurrentTransactionId = transaction.SkuTransactionId; CurrentTransactionTypeCode = transaction.SkuTransactionTypeCode; LastItemDateTime = transaction.TransactionDate; // setup variables var transactionType = transTypeDict[transaction.SkuTransactionTypeCode].Clone(); // stop if a new transactiontype is encountered if (transactionType.IsNewReviewRequired) { ProgressMessage = "New 'Transaction-Type' encountered"; //Console.Write("\r"); //new Logic.Log.LogEvent().EventLogInsert(errMessage, 1); break; } else if (transactionType.StockJournalEntryEnabled == false) { //transList[i].IsProcessed = true; ReconcileTransaction(transaction.SkuTransactionId); } // stock journal entry is enabled else { // check debit/credits if (transactionType.DebitStockStatusId.GetValueOrDefault() == 0 || transactionType.CreditStockStatusId.GetValueOrDefault() == 0) { // special case FBA Shipment Receipt +ve if (transactionType.StockJournalTypeId == (int)Constants.StockJournalType.SkuReconciliationFbaReceipt) //transactionType.TypeCode == "<+ve>" //|| transactionType.TypeCode == "<-ve>") { Model.AmazonFba.ShipmentInfo shipmentInfo = null; if (!shipmentInfoDic.ContainsKey(transaction.Reference)) { shipmentInfo = readShipmentInfo.HeaderByFbaShipmentId(transaction.Reference); if (shipmentInfo == null) { throw new Exception("Unable to retrive shipment info for reference '" + transaction.Reference + "'."); } else { shipmentInfoDic.Add(transaction.Reference, shipmentInfo); } } if (shipmentInfo.IsSetShipmentStockStatusId()) { // +ve shipment receipt if (transactionType.CreditStockStatusId == null && transactionType.DebitStockStatusId > 0) { transactionType.CreditStockStatusId = shipmentInfo.ShipmentStockStatusId; } // -ve shipment receipt else if (transactionType.DebitStockStatusId == null && transactionType.CreditStockStatusId > 0) { transactionType.DebitStockStatusId = shipmentInfo.ShipmentStockStatusId; } // something went wrong, raise error else { ProgressMessage = "Unable to retrive FBA shipment location/status from tblAmazonShipment for Amazon shipment '" + shipmentInfo.FbaShipmentId + "'."; recordSkip = recordSkip + 1; break; } } else { throw new Exception("Unable to retrive shipment info."); } } // manual entry else { ProgressMessage = "Transaction-Type debit or credit is not set, is this a manual entry?"; recordSkip = recordSkip + 1; break; } } // make the journal entries var journalList = new List<(int StockJournalId, int Quantity)>(); if (transactionType.FilterStockOnDateTime) { journalList = stockReallocate.BySkuNumber( transaction.TransactionDate, transactionType.StockJournalTypeId, transaction.SkuNumber, transaction.Quantity, transactionType.DebitStockStatusId.GetValueOrDefault(), transactionType.CreditStockStatusId.GetValueOrDefault(), transactionType.FirstInFirstOut, true); } else { journalList = stockReallocate.BySkuNumber( DateTime.UtcNow, transactionType.StockJournalTypeId, transaction.SkuNumber, transaction.Quantity, transactionType.DebitStockStatusId.GetValueOrDefault(), transactionType.CreditStockStatusId.GetValueOrDefault(), transactionType.FirstInFirstOut, true); } // insufficient balance available if (!journalList.Any()) { // in special case (found inventory), continue if (transactionType.TypeCode.Contains("") || transactionType.TypeCode.Contains("")) { ItemsCompleted++; ItemsRemaining--; continue; } else { ProgressMessage = "Insurficent quantity at status/location to relocate stock"; recordSkip = recordSkip + 1; break; } } // fail safe int qtyUnallocated = journalList.Sum(c => c.Quantity); if (qtyUnallocated > transaction.Quantity) { throw new Exception( currentMethodName + ": StockReallocateBySkuId() returned greater quantity than passed to function" + transaction.SkuTransactionId); } // update sku transaction table var newRecordList = new List(); for (int j = 0; j < journalList.Count; j++) { // update existing record to reconciled if (j == 0) { dbSkuTransaction.UpdateQuanitity(transaction.SkuTransactionId, journalList[j].Quantity); dbSkuTransaction.UpdateIsProcessed(transaction.SkuTransactionId, true, journalList[j].StockJournalId); } // create new reconciled record else { var newTransaction = new Model.Stock.SkuTransactionCreate( transaction.TransactionDate , transaction.SkuTransactionTypeCode , transaction.ForeignKey , transaction.Reference , transaction.Detail , transaction.SkuNumber , journalList[j].Quantity ); int newId = dbSkuTransaction.Create(newTransaction); dbSkuTransaction.UpdateIsProcessed(newId, true, journalList[j].StockJournalId); } qtyUnallocated = qtyUnallocated - journalList[j].Quantity; } // create new record for unreconciled quantity if (qtyUnallocated > 0) { var newTransaction = new Model.Stock.SkuTransactionCreate( transaction.TransactionDate , transaction.SkuTransactionTypeCode , transaction.ForeignKey , transaction.Reference , transaction.Detail , transaction.SkuNumber , qtyUnallocated ); int newId = dbSkuTransaction.Create(newTransaction); ProgressMessage = "Transaction could not be fully reconcoiled. Unallocated quanity remaing:" + qtyUnallocated; recordSkip = recordSkip + 1; break; } } ItemsCompleted++; ItemsRemaining--; scope.Complete(); } // end of scope } // end of loop } catch (Exception ex) { Console.Write("\r"); ProgressMessage = ex.Message; return; } if (ItemsRemaining == 0) { ReconciliationComplete = true; ProgressMessage = "Operation complete."; } //Stop: Console.Write("\r"); UI.Console.WriteLine("ProcessStockTransactions() compete. " + ItemsCompleted + " total records processed, " + recordSkip + " rows uncompllete due to insurficent stock."); UI.Console.WriteLine("ProcessStockTransactions(), " + recordSkip + " rows skipped due to insurficent stock."); return; } /// /// /// public void ReconcileFoundAndLost() { /* Amazon can find a item before they lost it. In this senario the found item will not reconcile. Even when the lost transaction comes * though, it will be dated after the found transaction, so it also will not reconcile. * * This method tackles this. Instead of journal entries for found before lost, just cancel then out (set IsProcessed=true) for * both transactions. * * In the main reconcile method, if the porcdure hits an unreconciable found transaction, it will ignore it and carry on. Everything will still * tally at my end, even if Amazon thinks i've got more than I actually have */ using (var scope = new TransactionScope()) { try { var lostQueryString = new List(); lostQueryString.Add("<-ve>"); var foundQueryString = new List(); foundQueryString.Add("<+ve>"); dbSkuTransaction.Init(); dbSkuTransaction.IsReconciled = false; dbSkuTransaction.StockTransactionTypeCode = lostQueryString; var lostList = dbSkuTransaction.Read(); dbSkuTransaction.Init(); dbSkuTransaction.IsReconciled = false; dbSkuTransaction.StockTransactionTypeCode = foundQueryString; var foundList = dbSkuTransaction.Read(); ItemsRemaining = foundList.Count; ItemsCompleted = 0; for (int i = 0; i < foundList.Count; i++) { for (int j = 0; j < lostList.Count; j++) { if (foundList[i].SkuNumber == lostList[j].SkuNumber) { // notes. isProcessed items need to be removed from the 'lost' list as we'll be looping over this multiple times, the // 'found' list we only loop though once // // when an sku match is found there is only 3 possible routes // 1. Lost == Found if (foundList[i].Quantity == lostList[j].Quantity) { // mark reconciled transaction as isProcessed dbSkuTransaction.UpdateIsProcessed(foundList[i].SkuTransactionId, true, null); dbSkuTransaction.UpdateIsProcessed(lostList[j].SkuTransactionId, true, null); // delete reconciled from 'lost' list lostList.RemoveAt(j); ItemsCompleted++; break; } // 2. Lost < Found else if (foundList[i].Quantity < lostList[j].Quantity) { // split the lost transaction int newTransId = dbSkuTransaction.SplitTransaction(lostList[j].SkuTransactionId, foundList[i].Quantity); // mark reconciled transaction as isProcessed dbSkuTransaction.UpdateIsProcessed(foundList[i].SkuTransactionId, true, null); dbSkuTransaction.UpdateIsProcessed(lostList[j].SkuTransactionId, true, null); // delete reconciled from 'lost' list lostList.RemoveAt(j); // retrieve the new split transaction var newTransactionList = dbSkuTransaction.Read(new List { newTransId }); if (!newTransactionList.Any()) throw new Exception("Something went wrong, this should not happen."); var newTransaction = newTransactionList[0]; // insert new split 'lost' record into the list lostList.Insert(j, newTransaction); ItemsCompleted++; break; } // 3. Lost > Found else if (foundList[i].Quantity > lostList[j].Quantity) { // split the found transaction int newTransId = dbSkuTransaction.SplitTransaction(lostList[i].SkuTransactionId, foundList[j].Quantity); // mark reconciled transaction as isProcessed dbSkuTransaction.UpdateIsProcessed(foundList[i].SkuTransactionId, true, null); dbSkuTransaction.UpdateIsProcessed(lostList[j].SkuTransactionId, true, null); // delete reconciled from 'lost' list lostList.RemoveAt(j); // retrive the new split transaction var newTransactionList = dbSkuTransaction.Read(new List { newTransId }); if (!newTransactionList.Any()) throw new Exception("Something went wrong, this should not happen."); var newTransaction = newTransactionList[0]; // // insert new split 'found' record into the list, in the next position foundList.Insert((i + 1), newTransaction); ItemsCompleted++; ItemsRemaining++; break; } else { throw new Exception("Something went wrong, this should not happen."); } } } } scope.Complete(); } catch (Exception ex) { scope.Dispose(); Console.Write("\r"); ProgressMessage = ex.Message; return; } } } /// /// Marks the transaction as reconciled/isprocessed with no stock journal entry required /// /// public void ReconcileTransaction(int skuTransactionId) { new Data.Database.Stock.UpdateSkuTransaction().UpdateIsProcessed(skuTransactionId, true, null); } public void UnReconcileTransaction(int skuTransactionId) { new Data.Database.Stock.UpdateSkuTransaction().UpdateIsProcessed(skuTransactionId, false, null, true); } } }