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 string sqlConnectionString; private Data.Database.AmazonShipment.ReadShipmentInfo readShipmentInfo; private Logic.Stock.SkuTransactionPersistance dbSkuTransaction; private Logic.Stock.SkuTransactionTypePersistance dbSkuTransactionType; private Logic.Validate.SkuTransaction validateSkuTrans; private Logic.Stock.StatusReallocate stockReallocate; private Logic.Log.LogEvent logEvent; private string err = "Reconcile Sku Transaction Exception: "; public SkuTransactionReconcile(string sqlConnectionString) { Innit(); this.sqlConnectionString = sqlConnectionString; dbSkuTransaction = new SkuTransactionPersistance(sqlConnectionString); dbSkuTransactionType = new SkuTransactionTypePersistance(sqlConnectionString); readShipmentInfo = new Data.Database.AmazonShipment.ReadShipmentInfo(); validateSkuTrans = new Validate.SkuTransaction(); stockReallocate = new Logic.Stock.StatusReallocate(sqlConnectionString); logEvent = 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 inserts stock journal entries, where applicable /// 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) /// /// Download and process Amazon reports before starting process /// public void ReconcileStockTransactions(bool updateTransactions) { Innit(); string currentMethodName = nameof(ReconcileStockTransactions); // ensure import table have been processed into transaction table without exception if (updateTransactions == true) { try { var preCheck = new bnhtrade.Core.Stock.StockReconciliation(); preCheck.ProcessFbaStockImportData(sqlConnectionString); } catch (Exception ex) { ProgressMessage = "Precheck failed: " + ex.Message; return; } } logEvent.LogInformation("Starting ReconcileStockTransactions()"); int recordSkip = 0; ReconcileLostAndFound(); // 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; try { // loop through transaction list for (int i = 0; i < transList.Count; i++) { using (var scope = new TransactionScope()) { Console.Write("\rProcessing record: {0} ({1} skipped)", (i + 1 + recordSkip), recordSkip); // setup return values CurrentSkuTransaction = transList[i]; CurrentTransactionId = transList[i].SkuTransactionId; CurrentTransactionTypeCode = transList[i].SkuTransactionType.TypeCode; LastItemDateTime = transList[i].TransactionDate; // load type into variable //var transType = dbSkuTransactionType.GetByTypeName(transList[i].SkuTransactionTypeName); // stop if a new transactiontype is encountered if (transList[i].SkuTransactionType.IsNewReviewRequired) { ProgressMessage = "New 'Transaction-Type' encountered"; //Console.Write("\r"); //MiscFunction.EventLogInsert(errMessage, 1); break; } else if (transList[i].SkuTransactionType.StockJournalEntryEnabled == false) { transList[i].IsProcessed = true; dbSkuTransaction.Update(transList[i]); } // stock journal entry is enabled else { // check debit/credits if (transList[i].SkuTransactionType.DebitStockStatusId.GetValueOrDefault() == 0 || transList[i].SkuTransactionType.CreditStockStatusId.GetValueOrDefault() == 0) { // special case FBA Shipment Receipt +ve if (transList[i].SkuTransactionType.TypeCode == "<_GET_FBA_FULFILLMENT_INVENTORY_RECEIPTS_DATA_><+ve>" || transList[i].SkuTransactionType.TypeCode == "<_GET_FBA_FULFILLMENT_INVENTORY_RECEIPTS_DATA_><-ve>") { Model.AmazonFba.ShipmentInfo shipmentInfo = null; if (!shipmentInfoDic.ContainsKey(transList[i].Reference)) { shipmentInfo = readShipmentInfo.HeaderByFbaShipmentId(transList[i].Reference); if (shipmentInfo == null) { throw new Exception("Unable to retrive shipment info for reference '" + transList[i].Reference + "'."); } else { shipmentInfoDic.Add(transList[i].Reference, shipmentInfo); } } if (shipmentInfo.IsSetShipmentStockStatusId()) { // +ve shipment receipt if (transList[i].SkuTransactionType.CreditStockStatusId == null && transList[i].SkuTransactionType.DebitStockStatusId > 0) { transList[i].SkuTransactionType.CreditStockStatusId = shipmentInfo.ShipmentStockStatusId; } // -ve shipment receipt else if (transList[i].SkuTransactionType.DebitStockStatusId == null && transList[i].SkuTransactionType.CreditStockStatusId > 0) { transList[i].SkuTransactionType.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 (transList[i].SkuTransactionType.FilterStockOnDateTime) { journalList = stockReallocate.BySkuNumber( transList[i].TransactionDate, transList[i].SkuTransactionType.StockJournalTypeId, transList[i].SkuNumber, transList[i].Quantity, transList[i].SkuTransactionType.DebitStockStatusId.GetValueOrDefault(), transList[i].SkuTransactionType.CreditStockStatusId.GetValueOrDefault(), transList[i].SkuTransactionType.FirstInFirstOut, true); } else { journalList = stockReallocate.BySkuNumber( DateTime.UtcNow, transList[i].SkuTransactionType.StockJournalTypeId, transList[i].SkuNumber, transList[i].Quantity, transList[i].SkuTransactionType.DebitStockStatusId.GetValueOrDefault(), transList[i].SkuTransactionType.CreditStockStatusId.GetValueOrDefault(), transList[i].SkuTransactionType.FirstInFirstOut, true); } // insufficient balance available if (!journalList.Any()) { // in special case (found inventory), continue if (transList[i].SkuTransactionType.TypeCode.Contains("<_GET_FBA_FULFILLMENT_INVENTORY_ADJUSTMENTS_DATA_>")) { ItemsCompleted++; ItemsRemaining--; continue; } else { ProgressMessage = "Insurficent quantity at status/location to relocate stock"; recordSkip = recordSkip + 1; break; } } // fail safe int qtyAllocated = journalList.Sum(c => c.Quantity); if (qtyAllocated > transList[i].Quantity) { throw new Exception( currentMethodName + ": StockReallocateBySkuId() returned greater quantity than passed to function" + transList[i].SkuTransactionId); } // update sku transaction table int qtyRemain = qtyAllocated; var newRecordList = new List(); for (int j = 0; j < journalList.Count; j++) { // update existing record if (j == 0) { transList[i].Quantity = (short)journalList[j].Quantity; transList[i].StockJournalId = journalList[j].StockJournalId; transList[i].IsProcessed = true; dbSkuTransaction.Update(transList[i]); } // new record else { var newRecord = transList[i].Clone(); newRecord.Quantity = (short)journalList[j].Quantity; newRecord.IsProcessed = true; newRecord.StockJournalId = journalList[j].StockJournalId; newRecordList.Add(newRecord); } qtyRemain = qtyRemain - journalList[j].Quantity; } // new record for unallocated quantity if (qtyRemain > 0) { var newRecord = transList[i].Clone(); newRecord.Quantity = (short)qtyRemain; newRecord.IsProcessed = false; newRecordList.Add(newRecord); } // add new transactions to table for (int j = 0; j < newRecordList.Count; j++) { dbSkuTransaction.Create(newRecordList[j]); } } 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 ReconcileLostAndFound() { using (var scope = new TransactionScope()) { // need to loop though table and cancel out any found before they are lost (in reality they were never // lost, therefore should not be entered into journal as lost) string lost = "<_GET_FBA_FULFILLMENT_INVENTORY_ADJUSTMENTS_DATA_><-ve>"; string found = "<_GET_FBA_FULFILLMENT_INVENTORY_ADJUSTMENTS_DATA_><+ve>"; var codeList = new List(); codeList.Add(lost); codeList.Add(found); // get list of sku transactions to reconcile dbSkuTransaction.Init(); dbSkuTransaction.IsReconciled = false; dbSkuTransaction.StockTransactionTypeCode = codeList; var transList = dbSkuTransaction.Read(); ItemsRemaining = transList.Count; ItemsCompleted = 0; for (int i = 0; i < transList.Count; i++) { if (transList[i].SkuTransactionTypeCode == found && !transList[i].IsProcessed) { string sku = transList[i].SkuNumber; int foundQty = transList[i].Quantity; int foundQtyUnAllocated = foundQty; // loop though list and find matching missing for (int j = 0; j < transList.Count; j++) { // update the 'lost' transaction if (transList[j].SkuNumber == sku && transList[j].IsProcessed == false && transList[j].SkuTransactionTypeCode == lost) { // split 'lost' transaction if (foundQtyUnAllocated - transList[j].Quantity < 0) { // create 'reconciled' clone var clone = transList[j].Clone(); clone.IsProcessed = true; clone.Quantity = (short)foundQtyUnAllocated; // modifiy and validate existing record transList[j].IsProcessed = false; transList[j].Quantity = (short)(transList[j].Quantity - foundQtyUnAllocated); // fail safe check if (clone.IsProcessed) { foundQtyUnAllocated -= clone.Quantity; } if (transList[j].IsProcessed) { foundQtyUnAllocated -= transList[j].Quantity; } if (foundQtyUnAllocated != 0) { throw new Exception("Unallocated quantity should equal zero."); } // submitt to database dbSkuTransaction.Create(clone); dbSkuTransaction.Update(transList[j]); } // set as isprocessed and continue else { foundQtyUnAllocated = foundQtyUnAllocated - transList[j].Quantity; transList[j].IsProcessed = true; dbSkuTransaction.Update(transList[j]); } // break? if (foundQtyUnAllocated == 0) { break; } } } // drop out of the 'find lost' loop // update the 'found' record if (foundQty != foundQtyUnAllocated) { // set isprocess = true if (foundQtyUnAllocated == 0) { transList[i].IsProcessed = true; dbSkuTransaction.Update(transList[i]); } // split record else if (foundQtyUnAllocated > 0) { // create 'reconciled' clone var clone = transList[i].Clone(); clone.IsProcessed = true; clone.Quantity = (short)(clone.Quantity - foundQtyUnAllocated); // modifiy existing record transList[i].IsProcessed = false; transList[i].Quantity = (short)foundQtyUnAllocated; // submitt to database dbSkuTransaction.Create(clone); dbSkuTransaction.Update(transList[i]); } // this shouldn't happen else { throw new Exception("Quantity unallocated is negative number"); } } } } // drop out of the 'find found' loop scope.Complete(); } } public void UnReconcileTransaction(int skuTransactionId) { var trans = dbSkuTransaction.Read(new List { skuTransactionId }, false).FirstOrDefault(); if (trans == null) { return; } // test if journal entry needs deleting, or just set to isprocessed = false if (trans.IsProcessed == true && trans.IsSetStockJournalId) { dbSkuTransaction.DeleteJournalEntry(skuTransactionId); } else if (trans.IsProcessed == true) { new Data.Database.Stock.UpdateSkuTransaction().Update(skuTransactionId, false); } } } }