diff --git a/src/bnhtrade.Core/Data/Database/Repository/Implementation/StockJournalRepository.cs b/src/bnhtrade.Core/Data/Database/Repository/Implementation/StockJournalRepository.cs index 8fc6ff5..9ba1261 100644 --- a/src/bnhtrade.Core/Data/Database/Repository/Implementation/StockJournalRepository.cs +++ b/src/bnhtrade.Core/Data/Database/Repository/Implementation/StockJournalRepository.cs @@ -1,5 +1,6 @@ using bnhtrade.Core.Data.Database._BoilerPlate; using bnhtrade.Core.Data.Database.Repository.Interface; +using bnhtrade.Core.Model.Account; using Microsoft.Data.SqlClient; using System; using System.Collections.Generic; @@ -9,6 +10,7 @@ using System.Text; using System.Threading.Tasks; using System.Transactions; using static bnhtrade.Core.Data.Database.Constants; +using static bnhtrade.Core.Model.Account.Journal; namespace bnhtrade.Core.Data.Database.Repository.Implementation { @@ -22,84 +24,13 @@ namespace bnhtrade.Core.Data.Database.Repository.Implementation // Create // - // only to be assessable via code, use stock relocate to move stock bewtween status' - public int StockJournalInsert(int journalTypeId, int stockId, List<(int statusId, int quantity)> journalPosts, - DateTime entryDate, bool isNewStock = false) + public int InsertStockJournalHeader(int stockId, int journalTypeId, DateTime entryDate, bool isLocked) { - /* - * TODO: currently the consistancy check checks the journal after the entry has been inserted to the db, if the check fails - * the transaction scope is disposed, and the ne journal entries roll back. However, if this is done within a higher - * level transaction scope, this nested dispose() also rolls back the all transacopes scopes it is a child of. - * - * Therefore, a consistancy check needs to be simulated in code to negate the need to rollback/dispose of a db transaction. - * This would also have some slight performance benefits. - * - * Once you've done this, fix the SkuTransactionReconcile class: Currently it's transactionscope only covers updates. - * Need to set the scope to cover the intial table read (to lock the records). The issue above restricts this. - */ - - - // balance and status IsCredit checks made by post insert function - - // create the journal entry - int stockJournalId; - //consitancy check is required? - bool consistencyRequired = true; - // get date of most recent debit for status' that I will be crediting - if (isNewStock == false) + if (entryDate.Kind != DateTimeKind.Utc) { - // build sql string - string stringSql = @" - SELECT - tblStockJournal.EntryDate - FROM - tblStockJournal - INNER JOIN tblStockJournalPost - ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID - WHERE - tblStockJournal.StockID=@stockId - AND EntryDate>=@entryDate - AND tblStockJournalPost.Quantity>0 - AND ("; - - bool firstDone = false; - foreach (var item in journalPosts) - { - if (item.quantity < 0) - { - if (firstDone) - { - stringSql = stringSql + " OR "; - } - stringSql = stringSql + "tblStockJournalPost.StockStatusID=" + item.statusId; - firstDone = true; - } - } - stringSql = stringSql + ");"; - - using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) - { - cmd.CommandText = stringSql; - cmd.Transaction = _transaction as SqlTransaction; - - cmd.Parameters.AddWithValue("@stockId", stockId); - cmd.Parameters.AddWithValue("@entryDate", entryDate.ToUniversalTime()); - - using (var reader = cmd.ExecuteReader()) - { - if (reader.HasRows) - { - consistencyRequired = true; - } - else - { - consistencyRequired = false; - } - } - } + throw new ArgumentException("Entry date must be in UTC format.", nameof(entryDate)); } - // create journal entry using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) { cmd.Transaction = _transaction as SqlTransaction; @@ -111,43 +42,33 @@ namespace bnhtrade.Core.Data.Database.Repository.Implementation cmd.Parameters.AddWithValue("@stockID", stockId); cmd.Parameters.AddWithValue("@journalTypeId", journalTypeId); cmd.Parameters.AddWithValue("@EntryDate", entryDate.ToUniversalTime()); - cmd.Parameters.AddWithValue("@isLocked", isNewStock); + cmd.Parameters.AddWithValue("@isLocked", isLocked); //execute - stockJournalId = (int)cmd.ExecuteScalar(); + return (int)cmd.ExecuteScalar(); } + } - // insert journal posts into database - //new Data.Database.Stock - StockJournalPostInsert(stockId, stockJournalId, journalPosts, isNewStock); - - // consistency check - bool consistencyResult = true; - if (consistencyRequired) + public int InsertStockJournalPost(int stockJournalId, int stockStatusId, int quantity) + { + if (quantity == 0) { - consistencyResult = false; - // build list of effected status' - var statusIdEffected = new List(); - foreach (var item in journalPosts) - { - if (item.quantity < 0) - { - statusIdEffected.Add(item.statusId); - } - } - // run check - consistencyResult = Stock.StockJournal.WIP_StockJournalConsistencyCheck(sqlConnectionString, stockId, statusIdEffected); + throw new ArgumentException("Quantity must be non-zero.", nameof(quantity)); } + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = @" + INSERT INTO tblStockJournalPost ( StockJournalID, StockStatusID, Quantity ) + OUTPUT INSERTED.StockJournalPostID + VALUES ( @StockJournalId, @stockStatudId, @quantity );"; - if (consistencyResult) - { - // commit - scope.Complete(); - return stockJournalId; - } - else - { - throw new Exception("Unable to insert stock journal entry, consistancy check failed."); + cmd.Parameters.AddWithValue("@StockJournalId", stockJournalId); + cmd.Parameters.AddWithValue("@stockStatudId", stockStatusId); + cmd.Parameters.AddWithValue("@quantity", quantity); + + // execute + return cmd.ExecuteNonQuery(); } } @@ -155,6 +76,32 @@ namespace bnhtrade.Core.Data.Database.Repository.Implementation // Read // + public (int, DateTime) ReadJournalStockIdAndEntryDate(int stockJournalId) + { + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = @" + SELECT tblStockJournal.EntryDate, StockID + FROM tblStockJournal + WHERE (((tblStockJournal.StockJournalID)=@stockJournalId));"; + + cmd.Parameters.AddWithValue("@stockJournalId", stockJournalId); + + using (var reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + return (reader.GetInt32(1), DateTime.SpecifyKind(reader.GetDateTime(0), DateTimeKind.Utc)); + } + else + { + throw new Exception("StockJournalID=" + stockJournalId + " does not exist!"); + } + } + } + } + public int ReadJournalTypeIdByStockId(int stockId) { using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) @@ -322,6 +269,54 @@ namespace bnhtrade.Core.Data.Database.Repository.Implementation } + public DateTime? ReadMostRecentEntryDateForStatusDebit(int stockId, List stockStatusIdList) + { + if (stockId <= 0) + { + throw new ArgumentException("Stock ID must be greater than zero.", nameof(stockId)); + } + if (stockStatusIdList == null || stockStatusIdList.Count == 0) + { + throw new ArgumentException("Stock status ID list cannot be null or empty.", nameof(stockStatusIdList)); + } + + var sqlWhere = new SqlWhereBuilder(); + + // build sql string + string stringSql = @" + MAX (tblStockJournal.EntryDate) AS MostRecentEntryDate + FROM + tblStockJournal + INNER JOIN tblStockJournalPost + ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID + WHERE + tblStockJournal.StockID=@stockId + AND tblStockJournalPost.Quantity>0 "; + + sqlWhere.In("tblStockJournalPost.StockStatusID", stockStatusIdList, "AND"); + + stringSql = stringSql + sqlWhere.SqlWhereString; + + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.CommandText = stringSql; + cmd.Transaction = _transaction as SqlTransaction; + + cmd.Parameters.AddWithValue("@stockId", stockId); + sqlWhere.AddParametersToSqlCommand(cmd); + + object obj = cmd.ExecuteScalar(); + if (obj == null || obj == DBNull.Value) + { + return null; + } + else + { + return DateTime.SpecifyKind((DateTime)obj, DateTimeKind.Utc); + } + } + } + // // update // @@ -331,454 +326,6 @@ namespace bnhtrade.Core.Data.Database.Repository.Implementation // Delete // - public void StockJournalDelete(int stockJournalId) - { - // get date for journal entry - DateTime entryDate; - int stockId; - using (SqlCommand cmd = new SqlCommand(@" - SELECT tblStockJournal.EntryDate, StockID - FROM tblStockJournal - WHERE (((tblStockJournal.StockJournalID)=@stockJournalId)); - ", conn)) - { - // add parameters - cmd.Parameters.AddWithValue("@stockJournalId", stockJournalId); - using (var reader = cmd.ExecuteReader()) - { - if (reader.Read()) - { - entryDate = DateTime.SpecifyKind(reader.GetDateTime(0), DateTimeKind.Utc); - stockId = reader.GetInt32(1); - } - else - { - throw new Exception("StockJournalID=" + stockJournalId + " does not exist!"); - } - } - - } - - // is consistancy check required - bool consistancyCheck; - // build list of debits that are to be deleted - var debitList = new List(); - using (SqlCommand cmd = new SqlCommand(@" - SELECT tblStockJournalPost.StockStatusID - FROM tblStockJournalPost - WHERE (((tblStockJournalPost.StockJournalID)=@stockJournalId) AND ((tblStockJournalPost.Quantity)>0)); - ", conn)) - { - // add parameters - cmd.Parameters.AddWithValue("@stockJournalId", stockJournalId); - - using (var reader = cmd.ExecuteReader()) - { - if (reader.HasRows) - { - while (reader.Read()) - { - debitList.Add(reader.GetInt32(0)); - } - } - else - { - throw new Exception("StockJournalID=" + stockJournalId + " has no debits with quantity greater than zero!"); - } - } - } - - // check no credits for stockId & debit combination have been made since delete entry - string stringSql = @" - SELECT - tblStockJournal.EntryDate - FROM - tblStockJournal - INNER JOIN tblStockJournalPost - ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID - WHERE - tblStockJournal.StockID=@stockId - AND tblStockJournalPost.Quantity<0 - AND EntryDate>=@entryDate - AND ("; - - bool firstDone = false; - foreach (var item in debitList) - { - if (firstDone) - { - stringSql = stringSql + " OR "; - } - stringSql = stringSql + "tblStockJournalPost.StockStatusID=" + item; - firstDone = true; - } - stringSql = stringSql + ");"; - - using (SqlCommand cmd = new SqlCommand(stringSql, conn)) - { - cmd.Parameters.AddWithValue("@stockId", stockId); - cmd.Parameters.AddWithValue("@entryDate", entryDate.ToUniversalTime()); - - using (var reader = cmd.ExecuteReader()) - { - if (reader.HasRows) - { - consistancyCheck = true; - } - else - { - consistancyCheck = false; - } - } - } - - // delete the posts - StockJournalPostDelete(conn, stockJournalId); - - // delete journal entry - using (SqlCommand cmd = new SqlCommand(@" - DELETE FROM tblStockJournal - WHERE StockJournalID=@stockJournalId; - ", conn)) - { - // add parameters - cmd.Parameters.AddWithValue("@stockJournalId", stockJournalId); - - int count = cmd.ExecuteNonQuery(); - - if (count != 1) - { - throw new Exception("Failed to delete stock journal header."); - } - } - - // consistanct check - bool consistencyResult = true; - if (consistancyCheck) - { - // run check - consistencyResult = Stock.StockJournal.WIP_StockJournalConsistencyCheck(sqlConnectionString, stockId, debitList); - } - - if (consistencyResult) - { - // commit - scope.Complete(); - } - else - { - throw new Exception("Unable to delete stock journal entry, consistancy check failed."); - } - } - - private void StockJournalPostInsert(int stockId, int stockJournalId, - List<(int statusId, int quantity)> journalPosts, bool isNewStock = false) - { - //checks - if (journalPosts.Count > 2) - { - // I have purposely made the code to accept split transaction incase of future requirements, however, db design is simpler this way. - throw new Exception("Stock journal does not currently support split transactions (greater than two posts)." + journalPosts.Count + " number posts attempted."); - } - else if (journalPosts.Count < 2) - { - // list not long enough - throw new Exception("Stock journal entry requires minium of two posts, entry of " + journalPosts.Count + " number posts attempted."); - } - - if (journalPosts.Sum(item => item.quantity) != 0) - { - // credits and debits do not match - throw new Exception("Sum of credits and debits do not resolve to zero."); - } - - // group credits and debits by status - var dicStatusQty = new Dictionary(); - foreach (var post in journalPosts) - { - if (dicStatusQty.ContainsKey(post.statusId) == false) - { - dicStatusQty.Add(post.statusId, post.quantity); - } - else - { - dicStatusQty[post.statusId] = dicStatusQty[post.statusId] + post.quantity; - } - } - - // get isCreditOnly for each status - var dicStatusIsCreditOnly = new Dictionary(); - foreach (var item in dicStatusQty) - { - using (SqlCommand cmd = new SqlCommand(@" - SELECT IsCreditOnly FROM tblStockStatus WHERE StockStatusID=@statusId; - ", conn)) - { - cmd.Parameters.AddWithValue("@statusId", item.Key); - - dicStatusIsCreditOnly.Add(item.Key, (bool)cmd.ExecuteScalar()); - } - } - - // check there is only one IsCreditOnly in list and it is allowed in this instance - int isCreditOnlyCount = 0; - foreach (var item in dicStatusIsCreditOnly) - { - if (item.Value) - { - isCreditOnlyCount = isCreditOnlyCount + 1; - } - } - if (isNewStock == false && isCreditOnlyCount > 0) - { - throw new Exception("Attempted credit or debit to 'Is Credit Only' status not allowed, in this instance."); - } - if (isNewStock == true && isCreditOnlyCount != 1) - { - throw new Exception("StockID=" + stockId + ", each stock line must have only have one IsCreditOnly=True status assigned to it."); - } - - // ensure debit (or zero credit) isn't made to credit only status - // need to do this check via original post list (i.e. journalPosts) - foreach (var post in journalPosts) - { - // debit check - if (post.quantity >= 0) - { - if (dicStatusIsCreditOnly[post.statusId] == true) - { - throw new Exception("Cannot make debit, or zero quantity credit, to credit only status. StatusID=" + post.statusId); - } - } - } - - // balance check for any credits (that are not isCreditOnly=true) - foreach (var item in dicStatusQty) - { - if (item.Value < 0 && dicStatusIsCreditOnly[item.Key] == false) - { - int quantity = new Data.Database.Stock.ReadStatusBalance().ReadStatusBalanceByStockId(stockId, item.Key); - - if (quantity + item.Value < 0) - { - throw new Exception("Credit status balance check failed. Available balance " + quantity + ", attempted credit " + item.Value * -1 + "."); - } - } - } - - // get this far... - // insert journal posts into database - foreach (var post in journalPosts) - { - using (SqlCommand cmd = new SqlCommand(@" - INSERT INTO tblStockJournalPost ( StockJournalID, StockStatusID, Quantity ) - VALUES ( @StockJournalId, @stockStatudId, @quantity ); - ", conn)) - { - cmd.Parameters.AddWithValue("@StockJournalId", stockJournalId); - cmd.Parameters.AddWithValue("@stockStatudId", post.statusId); - cmd.Parameters.AddWithValue("@quantity", post.quantity); - - // execute - cmd.ExecuteNonQuery(); - } - } - } - - private void StockJournalPostDelete(int stockJournalId) - { - using (SqlCommand cmd = new SqlCommand(@" - DELETE FROM tblStockJournalPost - WHERE StockJournalID=@stockJournalId - ", conn)) - { - cmd.Parameters.AddWithValue("@StockJournalId", stockJournalId); - - // execute - cmd.ExecuteNonQuery(); - - // the calling method must compete any transaction-scope on the connection - } - } - - public bool WIP_StockJournalConsistencyCheck( int stockId, List statusIdEffected = null) - { - if (statusIdEffected == null) - { - statusIdEffected = new List(); - } - - // if no list supplied, build list of all used status' for stockId - if (statusIdEffected.Count == 0) - { - using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) - { - cmd.Transaction = _transaction as SqlTransaction; - cmd.CommandText = @" - SELECT - tblStockJournalPost.StockStatusID - FROM - tblStockJournal - INNER JOIN tblStockJournalPost - ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID - WHERE - tblStockJournal.StockID=@stockId - GROUP BY - tblStockJournalPost.StockStatusID;"; - - cmd.Parameters.AddWithValue("@stockId", stockId); - - using (var reader = cmd.ExecuteReader()) - { - if (reader.HasRows) - { - while (reader.Read()) - { - statusIdEffected.Add(reader.GetInt32(0)); - } - } - else - { - throw new Exception("No stock journal entries exist for StockID=" + stockId); - } - } - } - - // build the sql string to build creditCreate bool - var dicStatusCreditOnly = new Dictionary(); - string sqlString = @" - SELECT - tblStockStatus.StockStatusID, tblStockStatus.IsCreditOnly - FROM - tblStockStatus "; - - for (var i = 0; i < statusIdEffected.Count; i++) - { - if (i == 0) - { - sqlString = sqlString + " WHERE tblStockStatus.StockStatusID=" + statusIdEffected[i]; - } - else - { - sqlString = sqlString + " OR tblStockStatus.StockStatusID=" + statusIdEffected[i]; - } - - //if (i == (statusIdEffected.Count - 1)) - //{ - // sqlString = sqlString + ";"; - //} - } - sqlString = sqlString + " GROUP BY tblStockStatus.StockStatusID, tblStockStatus.IsCreditOnly;"; - - // run query & build dictionaries - using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) - { - cmd.Transaction = _transaction as SqlTransaction; - cmd.CommandText = sqlString; - cmd.Parameters.AddWithValue("@stockId", stockId); - - using (SqlDataReader reader = cmd.ExecuteReader()) - { - if (reader.Read()) - { - dicStatusCreditOnly.Add(reader.GetInt32(0), reader.GetBoolean(1)); - - while (reader.Read()) - { - dicStatusCreditOnly.Add(reader.GetInt32(0), reader.GetBoolean(1)); - } - } - else - { - throw new Exception("Error, no journal entries found for StockID=" + stockId); - } - } - } - - // check integrity of supplied statusIds - foreach (int statusId in statusIdEffected) - { - if (!dicStatusCreditOnly.ContainsKey(statusId)) - { - throw new Exception("Supplied StatusId (" + statusId + ") doesn't exist for StockId=" + stockId); - } - } - - // loop through each statudId and check integrity, if createdEnabled=false - foreach (int statudId in statusIdEffected) - { - if (dicStatusCreditOnly[statudId] == false) - { - using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) - { - cmd.Transaction = _transaction as SqlTransaction; - cmd.CommandText = @" - SELECT - tblStockJournal.EntryDate, tblStockJournalPost.Quantity - FROM - tblStockJournal - INNER JOIN tblStockJournalPost - ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID - WHERE - tblStockJournal.StockID=@stockId - AND tblStockJournalPost.StockStatusID=@statudId - ORDER BY - tblStockJournal.EntryDate;"; - - cmd.Parameters.AddWithValue("@stockId", stockId); - cmd.Parameters.AddWithValue("@statudId", statudId); - - using (SqlDataReader reader = cmd.ExecuteReader()) - { - // read first line into variables - reader.Read(); - int quantity = reader.GetInt32(1); - DateTime entryDate = DateTime.SpecifyKind(reader.GetDateTime(0), DateTimeKind.Utc); - - while (true) - { - // compare to next values - if (reader.Read()) - { - DateTime nextEntryDate = DateTime.SpecifyKind(reader.GetDateTime(0), DateTimeKind.Utc); - // don't check quantites for transactions on same date - if (entryDate == nextEntryDate) - { - entryDate = nextEntryDate; - quantity = quantity + reader.GetInt32(1); - } - // check if dates are different - else - { - if (quantity < 0) - { - return false; - } - else - { - entryDate = nextEntryDate; - quantity = quantity + reader.GetInt32(1); - } - } - } - // end if no records, check quantity - else - { - if (quantity < 0) - { - return false; - } - break; - } - } - } - } - } - } - } - // get this far, all good - return true; - } } } diff --git a/src/bnhtrade.Core/Data/Database/Repository/Implementation/StockStatusRepository.cs b/src/bnhtrade.Core/Data/Database/Repository/Implementation/StockStatusRepository.cs index 246f0ca..c2ad7c8 100644 --- a/src/bnhtrade.Core/Data/Database/Repository/Implementation/StockStatusRepository.cs +++ b/src/bnhtrade.Core/Data/Database/Repository/Implementation/StockStatusRepository.cs @@ -16,10 +16,10 @@ namespace bnhtrade.Core.Data.Database.Repository.Implementation { } - public List ReadStatus(List statusIds = null, List statusTypeIds = null) + public Dictionary ReadStatus(List statusIds = null, List statusTypeIds = null) { var sqlBuilder = new SqlWhereBuilder(); - var returnList = new List(); + var returnList = new Dictionary(); //build sql query string sql = @" @@ -91,7 +91,7 @@ namespace bnhtrade.Core.Data.Database.Repository.Implementation , recordCreated ); - returnList.Add(newItem); + returnList.Add(statusId, newItem); } } } diff --git a/src/bnhtrade.Core/Data/Database/Repository/Interface/IStockJournalRepository.cs b/src/bnhtrade.Core/Data/Database/Repository/Interface/IStockJournalRepository.cs index 3d19b63..beacbcc 100644 --- a/src/bnhtrade.Core/Data/Database/Repository/Interface/IStockJournalRepository.cs +++ b/src/bnhtrade.Core/Data/Database/Repository/Interface/IStockJournalRepository.cs @@ -8,8 +8,9 @@ namespace bnhtrade.Core.Data.Database.Repository.Interface { internal interface IStockJournalRepository { - public int StockJournalInsert(int journalTypeId, int stockId, List<(int statusId, int quantity)> journalPosts, - DateTime entryDate, bool isNewStock = false); + int InsertStockJournalHeader(int stockId, int journalTypeId, DateTime entryDate, bool isLocked); + int InsertStockJournalPost(int stockJournalId, int stockStatusId, int quantity); + (int, DateTime) ReadJournalStockIdAndEntryDate(int stockJournalId); int ReadJournalTypeIdByStockId(int stockId); int ReadStatusBalanceBySku(string sku, int statusId); int ReadStatusBalanceByStockNumber(int stockNumber, int statusId); @@ -17,9 +18,6 @@ namespace bnhtrade.Core.Data.Database.Repository.Interface int ReadJournalEntryCountByStockId(int stockId); int? ReadTypeIdStatusCreditId(int stockJournalTypeId); Dictionary ReadStatusBalanceByStatusId(int statusId); - void StockJournalDelete(int stockJournalId); - void StockJournalPostInsert(int stockId, int stockJournalId, List<(int statusId, int quantity)> journalPosts, bool isNewStock = false); - void StockJournalPostDelete(int stockJournalId); - bool WIP_StockJournalConsistencyCheck(int stockId, List statusIdEffected = null); + DateTime? ReadMostRecentEntryDateForStatusDebit(int stockId, List stockStatusIdList); } } diff --git a/src/bnhtrade.Core/Data/Database/Repository/Interface/IStockStatusRepository.cs b/src/bnhtrade.Core/Data/Database/Repository/Interface/IStockStatusRepository.cs index 7f06bcd..c3d2a47 100644 --- a/src/bnhtrade.Core/Data/Database/Repository/Interface/IStockStatusRepository.cs +++ b/src/bnhtrade.Core/Data/Database/Repository/Interface/IStockStatusRepository.cs @@ -8,7 +8,7 @@ namespace bnhtrade.Core.Data.Database.Repository.Interface { internal interface IStockStatusRepository { - List ReadStatus(List statusIds = null, List statusTypeIds = null); + Dictionary ReadStatus(List statusIds = null, List statusTypeIds = null); Dictionary ReadStatusType(); } } diff --git a/src/bnhtrade.Core/Logic/Inventory/StockJournalService.cs b/src/bnhtrade.Core/Logic/Inventory/StockJournalService.cs index 1e34602..3586fea 100644 --- a/src/bnhtrade.Core/Logic/Inventory/StockJournalService.cs +++ b/src/bnhtrade.Core/Logic/Inventory/StockJournalService.cs @@ -1,9 +1,11 @@ -using bnhtrade.Core.Data.Database.UnitOfWork; +using Amazon.Runtime.Internal.Transform; +using bnhtrade.Core.Data.Database.UnitOfWork; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using static System.Formats.Asn1.AsnWriter; namespace bnhtrade.Core.Logic.Inventory { @@ -13,32 +15,507 @@ namespace bnhtrade.Core.Logic.Inventory internal StockJournalService(IUnitOfWork unitOfWork) : base(unitOfWork) { } + public int StockJournalInsert(int journalTypeId, int stockId, List<(int statusId, int quantity)> journalPosts, + DateTime entryDate, bool isNewStock = false) + { + /* + * TODO: currently the consistancy check checks the journal after the entry has been inserted to the db, if the check fails + * the transaction scope is disposed, and the ne journal entries roll back. However, if this is done within a higher + * level transaction scope, this nested dispose() also rolls back the all transacopes scopes it is a child of. + * + * Therefore, a consistancy check needs to be simulated in code to negate the need to rollback/dispose of a db transaction. + * This would also have some slight performance benefits. + * + * Once you've done this, fix the SkuTransactionReconcile class: Currently it's transactionscope only covers updates. + * Need to set the scope to cover the intial table read (to lock the records). The issue above restricts this. + * + * Future me here, refactored the code to use a UnitOfWork pattern, wil need to check that this is still the case. + */ + + + // balance and status IsCredit checks made by post insert function + + //consitancy check is required? + bool consistencyRequired = true; + + return WithUnitOfWork(uow => + { + // get date of most recent debit for status' that I will be crediting + if (isNewStock == false) + { + var debitStatusIds = new List(); + foreach (var post in journalPosts) + { + if (post.quantity > 0) + { + debitStatusIds.Add(post.statusId); + } + } + var mostRecentDebitDate = uow.StockJournalRepository.ReadMostRecentEntryDateForStatusDebit(stockId, debitStatusIds); + if (mostRecentDebitDate.HasValue && mostRecentDebitDate >= entryDate) + { + consistencyRequired = true; + } + else + { + consistencyRequired = false; + } + } + + // create journal entry + int stockJournalId = uow.StockJournalRepository.InsertStockJournalHeader(stockId, journalTypeId, entryDate, isNewStock); + + // insert journal posts into database + //new Data.Database.Stock + var insertCount = StockJournalPostInsert(uow, stockId, stockJournalId, journalPosts, isNewStock); + + // consistency check + bool consistencyResult = true; + if (consistencyRequired) + { + consistencyResult = false; + // build list of effected status' + var statusIdEffected = new List(); + foreach (var item in journalPosts) + { + if (item.quantity < 0) + { + statusIdEffected.Add(item.statusId); + } + } + // run check + consistencyResult = WIP_StockJournalConsistencyCheck(stockId, statusIdEffected); + } + + if (consistencyResult) + { + // commit + CommitIfOwned(uow); + return stockJournalId; + } + else + { + throw new Exception("Unable to insert stock journal entry, consistancy check failed."); + } + }); + } + + private int StockJournalPostInsert(IUnitOfWork uow, int stockId, int stockJournalId, List<(int statusId, int quantity)> journalPosts, bool isNewStock = false) + { + //checks + if (journalPosts.Count > 2) + { + // I have purposely made the code to accept split transaction incase of future requirements, however for now, it's simpler this way + throw new Exception("Stock journal does not currently support split transactions (greater than two posts)." + journalPosts.Count + " number posts attempted."); + } + else if (journalPosts.Count < 2) + { + // list not long enough + throw new Exception("Stock journal entry requires minium of two posts, entry of " + journalPosts.Count + " number posts attempted."); + } + + if (journalPosts.Sum(item => item.quantity) != 0) + { + // credits and debits do not match + throw new Exception("Sum of credits and debits do not resolve to zero."); + } + + // group credits and debits by status + var dicStatusQty = new Dictionary(); + foreach (var post in journalPosts) + { + if (dicStatusQty.ContainsKey(post.statusId) == false) + { + dicStatusQty.Add(post.statusId, post.quantity); + } + else + { + dicStatusQty[post.statusId] = dicStatusQty[post.statusId] + post.quantity; + } + } + // get isCreditOnly for each status + var statusList = new StockStatusService(uow).GetStatus(dicStatusQty.Keys.ToList()); + var dicStatusIsCreditOnly = new Dictionary(); + foreach (var status in statusList.Values.ToList()) + { + dicStatusIsCreditOnly.Add(status.StatusId, status.IsCreditOnly); + } + + // check there is only one IsCreditOnly in list and it is allowed in this instance + int isCreditOnlyCount = 0; + foreach (var item in dicStatusIsCreditOnly) + { + if (item.Value) + { + isCreditOnlyCount = isCreditOnlyCount + 1; + } + } + if (isNewStock == false && isCreditOnlyCount > 0) + { + throw new Exception("Attempted credit or debit to 'Is Credit Only' status not allowed, in this instance."); + } + if (isNewStock == true && isCreditOnlyCount != 1) + { + throw new Exception("StockID=" + stockId + ", each stock line must have only have one IsCreditOnly=True status assigned to it."); + } + + // ensure debit (or zero credit) isn't made to credit only status + // need to do this check via original post list (i.e. journalPosts) + foreach (var post in journalPosts) + { + // debit check + if (post.quantity >= 0) + { + if (dicStatusIsCreditOnly[post.statusId] == true) + { + throw new Exception("Cannot make debit, or zero quantity credit, to credit only status. StatusID=" + post.statusId); + } + } + } + + // balance check for any credits (that are not isCreditOnly=true) + foreach (var item in dicStatusQty) + { + if (item.Value < 0 && dicStatusIsCreditOnly[item.Key] == false) + { + int quantity = new Data.Database.Stock.ReadStatusBalance().ReadStatusBalanceByStockId(stockId, item.Key); + + if (quantity + item.Value < 0) + { + throw new Exception("Credit status balance check failed. Available balance " + quantity + ", attempted credit " + item.Value * -1 + "."); + } + } + } + + // get this far... + // insert journal posts into database + var postIdList = new List() + foreach (var post in journalPosts) + { + postIdList.Add(uow.StockJournalRepository.InsertStockJournalPost(stockJournalId, post.statusId, post.quantity)); + } + return postIdList.Count(); + } + public void StockJournalDelete(int stockJournalId) { WithUnitOfWork(uow => { - if (stockJournalId <= 0) + // get date for journal entry + var idAndDate = uow.StockJournalRepository.ReadJournalStockIdAndEntryDate(stockJournalId); + DateTime entryDate = idAndDate.Item2; + int stockId = idAndDate.Item1; + + // is consistancy check required + bool consistancyCheck; + // build list of debits that are to be deleted + var debitList = new List(); + using (SqlCommand cmd = new SqlCommand(@" + SELECT tblStockJournalPost.StockStatusID + FROM tblStockJournalPost + WHERE (((tblStockJournalPost.StockJournalID)=@stockJournalId) AND ((tblStockJournalPost.Quantity)>0)); + ", conn)) { - throw new ArgumentException("Stock journal ID must be greater than zero", nameof(stockJournalId)); + // add parameters + cmd.Parameters.AddWithValue("@stockJournalId", stockJournalId); + + using (var reader = cmd.ExecuteReader()) + { + if (reader.HasRows) + { + while (reader.Read()) + { + debitList.Add(reader.GetInt32(0)); + } + } + else + { + throw new Exception("StockJournalID=" + stockJournalId + " has no debits with quantity greater than zero!"); + } + } + } + + // check no credits for stockId & debit combination have been made since delete entry + string stringSql = @" + SELECT + tblStockJournal.EntryDate + FROM + tblStockJournal + INNER JOIN tblStockJournalPost + ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID + WHERE + tblStockJournal.StockID=@stockId + AND tblStockJournalPost.Quantity<0 + AND EntryDate>=@entryDate + AND ("; + + bool firstDone = false; + foreach (var item in debitList) + { + if (firstDone) + { + stringSql = stringSql + " OR "; + } + stringSql = stringSql + "tblStockJournalPost.StockStatusID=" + item; + firstDone = true; + } + stringSql = stringSql + ");"; + + using (SqlCommand cmd = new SqlCommand(stringSql, conn)) + { + cmd.Parameters.AddWithValue("@stockId", stockId); + cmd.Parameters.AddWithValue("@entryDate", entryDate.ToUniversalTime()); + + using (var reader = cmd.ExecuteReader()) + { + if (reader.HasRows) + { + consistancyCheck = true; + } + else + { + consistancyCheck = false; + } + } + } + + // delete the posts + StockJournalPostDelete(conn, stockJournalId); + + // delete journal entry + using (SqlCommand cmd = new SqlCommand(@" + DELETE FROM tblStockJournal + WHERE StockJournalID=@stockJournalId; + ", conn)) + { + // add parameters + cmd.Parameters.AddWithValue("@stockJournalId", stockJournalId); + + int count = cmd.ExecuteNonQuery(); + + if (count != 1) + { + throw new Exception("Failed to delete stock journal header."); + } + } + + // consistanct check + bool consistencyResult = true; + if (consistancyCheck) + { + // run check + consistencyResult = Stock.StockJournal.WIP_StockJournalConsistencyCheck(sqlConnectionString, stockId, debitList); + } + + if (consistencyResult) + { + // commit + scope.Complete(); + } + else + { + throw new Exception("Unable to delete stock journal entry, consistancy check failed."); } - uow.StockJournalRepository.StockJournalDelete(stockJournalId); - CommitIfOwned(uow); }); } + private void StockJournalPostDelete(int stockJournalId) + { + using (SqlCommand cmd = new SqlCommand(@" + DELETE FROM tblStockJournalPost + WHERE StockJournalID=@stockJournalId + ", conn)) + { + cmd.Parameters.AddWithValue("@StockJournalId", stockJournalId); + + // execute + cmd.ExecuteNonQuery(); + + // the calling method must compete any transaction-scope on the connection + } + } + + // can be used before commiting an sql insert, update or delete to the stock journal to ensure a status does not fall below 0 // (unless the status is enabled to do so) // set empty list or statusIdEffected to null to check entier stock entries for consistency public bool WIP_StockJournalConsistencyCheck(int stockId, List statusIdEffected = null) { - return WithUnitOfWork(uow => + if (statusIdEffected == null) { - if (stockId <= 0) + statusIdEffected = new List(); + } + + // if no list supplied, build list of all used status' for stockId + if (statusIdEffected.Count == 0) + { + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) { - throw new ArgumentException("Stock ID must be greater than zero", nameof(stockId)); + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = @" + SELECT + tblStockJournalPost.StockStatusID + FROM + tblStockJournal + INNER JOIN tblStockJournalPost + ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID + WHERE + tblStockJournal.StockID=@stockId + GROUP BY + tblStockJournalPost.StockStatusID;"; + + cmd.Parameters.AddWithValue("@stockId", stockId); + + using (var reader = cmd.ExecuteReader()) + { + if (reader.HasRows) + { + while (reader.Read()) + { + statusIdEffected.Add(reader.GetInt32(0)); + } + } + else + { + throw new Exception("No stock journal entries exist for StockID=" + stockId); + } + } } - return uow.StockJournalRepository.WIP_StockJournalConsistencyCheck(stockId, statusIdEffected); - }); + + // build the sql string to build creditCreate bool + var dicStatusCreditOnly = new Dictionary(); + string sqlString = @" + SELECT + tblStockStatus.StockStatusID, tblStockStatus.IsCreditOnly + FROM + tblStockStatus "; + + for (var i = 0; i < statusIdEffected.Count; i++) + { + if (i == 0) + { + sqlString = sqlString + " WHERE tblStockStatus.StockStatusID=" + statusIdEffected[i]; + } + else + { + sqlString = sqlString + " OR tblStockStatus.StockStatusID=" + statusIdEffected[i]; + } + + //if (i == (statusIdEffected.Count - 1)) + //{ + // sqlString = sqlString + ";"; + //} + } + sqlString = sqlString + " GROUP BY tblStockStatus.StockStatusID, tblStockStatus.IsCreditOnly;"; + + // run query & build dictionaries + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = sqlString; + cmd.Parameters.AddWithValue("@stockId", stockId); + + using (SqlDataReader reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + dicStatusCreditOnly.Add(reader.GetInt32(0), reader.GetBoolean(1)); + + while (reader.Read()) + { + dicStatusCreditOnly.Add(reader.GetInt32(0), reader.GetBoolean(1)); + } + } + else + { + throw new Exception("Error, no journal entries found for StockID=" + stockId); + } + } + } + + // check integrity of supplied statusIds + foreach (int statusId in statusIdEffected) + { + if (!dicStatusCreditOnly.ContainsKey(statusId)) + { + throw new Exception("Supplied StatusId (" + statusId + ") doesn't exist for StockId=" + stockId); + } + } + + // loop through each statudId and check integrity, if createdEnabled=false + foreach (int statudId in statusIdEffected) + { + if (dicStatusCreditOnly[statudId] == false) + { + using (SqlCommand cmd = _connection.CreateCommand() as SqlCommand) + { + cmd.Transaction = _transaction as SqlTransaction; + cmd.CommandText = @" + SELECT + tblStockJournal.EntryDate, tblStockJournalPost.Quantity + FROM + tblStockJournal + INNER JOIN tblStockJournalPost + ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID + WHERE + tblStockJournal.StockID=@stockId + AND tblStockJournalPost.StockStatusID=@statudId + ORDER BY + tblStockJournal.EntryDate;"; + + cmd.Parameters.AddWithValue("@stockId", stockId); + cmd.Parameters.AddWithValue("@statudId", statudId); + + using (SqlDataReader reader = cmd.ExecuteReader()) + { + // read first line into variables + reader.Read(); + int quantity = reader.GetInt32(1); + DateTime entryDate = DateTime.SpecifyKind(reader.GetDateTime(0), DateTimeKind.Utc); + + while (true) + { + // compare to next values + if (reader.Read()) + { + DateTime nextEntryDate = DateTime.SpecifyKind(reader.GetDateTime(0), DateTimeKind.Utc); + // don't check quantites for transactions on same date + if (entryDate == nextEntryDate) + { + entryDate = nextEntryDate; + quantity = quantity + reader.GetInt32(1); + } + // check if dates are different + else + { + if (quantity < 0) + { + return false; + } + else + { + entryDate = nextEntryDate; + quantity = quantity + reader.GetInt32(1); + } + } + } + // end if no records, check quantity + else + { + if (quantity < 0) + { + return false; + } + break; + } + } + } + } + } + } + } + // get this far, all good + return true; } } } diff --git a/src/bnhtrade.Core/Logic/Inventory/StockService.cs b/src/bnhtrade.Core/Logic/Inventory/StockService.cs index f8bd73d..16669e9 100644 --- a/src/bnhtrade.Core/Logic/Inventory/StockService.cs +++ b/src/bnhtrade.Core/Logic/Inventory/StockService.cs @@ -100,7 +100,7 @@ namespace bnhtrade.Core.Logic.Inventory var journalPosts = new List<(int statusId, int quantity)>(); journalPosts.Add((statusDebitId, quantity)); journalPosts.Add((statusCreditId.Value, (quantity * -1))); - int stockJournalId = uow.StockJournalRepository.StockJournalInsert(stockJournalTypeId, stockId, journalPosts, stockJournalEntryDate, true); + int stockJournalId = new StockJournalService(uow).StockJournalInsert(stockJournalTypeId, stockId, journalPosts, stockJournalEntryDate, true); // update the stock table count = uow.StockRepository.UpdateStockJournalId(stockId, stockJournalId); @@ -191,7 +191,7 @@ namespace bnhtrade.Core.Logic.Inventory } // delete stock journal entry - uow.StockJournalRepository.StockJournalDelete(stockJournalId); + new StockJournalService(uow).StockJournalDelete(stockJournalId); // delete stock table entry count = uow.StockRepository.DeleteStock(stockId); diff --git a/src/bnhtrade.Core/Logic/Inventory/StockStatusService.cs b/src/bnhtrade.Core/Logic/Inventory/StockStatusService.cs index 55f8bb4..1519838 100644 --- a/src/bnhtrade.Core/Logic/Inventory/StockStatusService.cs +++ b/src/bnhtrade.Core/Logic/Inventory/StockStatusService.cs @@ -13,6 +13,14 @@ namespace bnhtrade.Core.Logic.Inventory internal StockStatusService(IUnitOfWork unitOfWork) : base(unitOfWork) { } + public Dictionary GetStatus(List statusIds = null, List statusTypeIds = null) + { + return WithUnitOfWork(uow => + { + return uow.StockStatusRepository.ReadStatus(statusIds, statusTypeIds); + }); + } + /// /// Return the avaliable balance of a status. Uses a more efficent sql/code. However, balance requests should /// generally also involve a date/time (i.e. the system does allow a stock transaction in the future, therefore diff --git a/src/bnhtrade.Core/Logic/Stock/StatusBalance.cs b/src/bnhtrade.Core/Logic/Stock/StatusBalance.cs index 8d41852..8c53f8e 100644 --- a/src/bnhtrade.Core/Logic/Stock/StatusBalance.cs +++ b/src/bnhtrade.Core/Logic/Stock/StatusBalance.cs @@ -100,7 +100,7 @@ namespace bnhtrade.Core.Logic.Stock var sku = readSku.BySkuNumber(statusTransaction.SkuNumber); // get the status obj - var status = uow.StockStatusRepository.ReadStatus(new List { statusTransaction.StockStatusId })[0]; + var status = new Logic.Inventory.StockStatusService(uow).GetStatus(new List { statusTransaction.StockStatusId }).Values.First(); return new Model.Stock.StatusBalance(status, sku, entryList); }