mirror of
https://github.com/stokebob/bnhtrade.git
synced 2026-05-18 19:48:23 +00:00
603 lines
25 KiB
C#
603 lines
25 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Microsoft.Data.SqlClient;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.Transactions;
|
|
|
|
namespace bnhtrade.Core.Data.Database.Stock
|
|
{
|
|
public class JournalCrud : Connection
|
|
{
|
|
// 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)
|
|
{
|
|
/*
|
|
* 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;
|
|
using (TransactionScope scope = new TransactionScope())
|
|
using (SqlConnection conn = new SqlConnection(SqlConnectionString))
|
|
{
|
|
conn.Open();
|
|
|
|
//consitancy check is required?
|
|
bool consistencyRequired = true;
|
|
// get date of most recent debit for status' that I will be crediting
|
|
if (isNewStock == false)
|
|
{
|
|
// 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 = new SqlCommand(stringSql, conn))
|
|
{
|
|
cmd.Parameters.AddWithValue("@stockId", stockId);
|
|
cmd.Parameters.AddWithValue("@entryDate", entryDate.ToUniversalTime());
|
|
|
|
using (var reader = cmd.ExecuteReader())
|
|
{
|
|
if (reader.HasRows)
|
|
{
|
|
consistencyRequired = true;
|
|
}
|
|
else
|
|
{
|
|
consistencyRequired = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// create journal entry
|
|
using (SqlCommand cmd = new SqlCommand(@"
|
|
INSERT INTO tblStockJournal ( EntryDate, StockJournalTypeID, StockID, IsLocked )
|
|
OUTPUT INSERTED.StockJournalID
|
|
VALUES ( @EntryDate, @journalTypeId, @stockID, @isLocked );
|
|
", conn))
|
|
{
|
|
// add parameters
|
|
cmd.Parameters.AddWithValue("@stockID", stockId);
|
|
cmd.Parameters.AddWithValue("@journalTypeId", journalTypeId);
|
|
cmd.Parameters.AddWithValue("@EntryDate", entryDate.ToUniversalTime());
|
|
cmd.Parameters.AddWithValue("@isLocked", isNewStock);
|
|
|
|
//execute
|
|
stockJournalId = (int)cmd.ExecuteScalar();
|
|
}
|
|
|
|
// insert journal posts into database
|
|
StockJournalPostInsert(conn, stockId, stockJournalId, journalPosts, isNewStock);
|
|
|
|
// consistency check
|
|
bool consistencyResult = true;
|
|
if (consistencyRequired)
|
|
{
|
|
consistencyResult = false;
|
|
// build list of effected status'
|
|
var statusIdEffected = new List<int>();
|
|
foreach (var item in journalPosts)
|
|
{
|
|
if (item.quantity < 0)
|
|
{
|
|
statusIdEffected.Add(item.statusId);
|
|
}
|
|
}
|
|
// run check
|
|
consistencyResult = StockJournalConsistencyCheck(stockId, statusIdEffected);
|
|
}
|
|
|
|
if (consistencyResult)
|
|
{
|
|
// commit
|
|
scope.Complete();
|
|
return stockJournalId;
|
|
}
|
|
else
|
|
{
|
|
throw new Exception("Unable to insert stock journal entry, consistancy check failed.");
|
|
}
|
|
}
|
|
}
|
|
|
|
public void StockJournalDelete(int stockJournalId)
|
|
{
|
|
using (TransactionScope scope = new TransactionScope())
|
|
using (SqlConnection conn = new SqlConnection(SqlConnectionString))
|
|
{
|
|
conn.Open();
|
|
|
|
// 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<int>();
|
|
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 = StockJournalConsistencyCheck(stockId, debitList);
|
|
}
|
|
|
|
if (consistencyResult)
|
|
{
|
|
// commit
|
|
scope.Complete();
|
|
}
|
|
else
|
|
{
|
|
throw new Exception("Unable to delete stock journal entry, consistancy check failed.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void StockJournalPostInsert(SqlConnection conn, 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<int, int>();
|
|
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<int, bool>();
|
|
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().ByStockId(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(SqlConnection conn, 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 StockJournalConsistencyCheck(int stockId, List<int> statusIdEffected = null)
|
|
{
|
|
if (statusIdEffected == null)
|
|
{
|
|
statusIdEffected = new List<int>();
|
|
}
|
|
|
|
|
|
using (SqlConnection conn = new SqlConnection(SqlConnectionString))
|
|
{
|
|
conn.Open();
|
|
|
|
// if no list supplied, build list of all used status' for stockId
|
|
if (statusIdEffected.Count == 0)
|
|
{
|
|
using (SqlCommand cmd = new SqlCommand(@"
|
|
SELECT
|
|
tblStockJournalPost.StockStatusID
|
|
FROM
|
|
tblStockJournal
|
|
INNER JOIN tblStockJournalPost
|
|
ON tblStockJournal.StockJournalID = tblStockJournalPost.StockJournalID
|
|
WHERE
|
|
tblStockJournal.StockID=@stockId
|
|
GROUP BY
|
|
tblStockJournalPost.StockStatusID;
|
|
", conn))
|
|
{
|
|
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<int, bool>();
|
|
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 = new SqlCommand(sqlString, conn))
|
|
{
|
|
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 = new SqlCommand(@"
|
|
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;
|
|
", conn))
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
}
|