diff --git a/ActiveAllocator.API/ActiveAllocator.API.csproj b/ActiveAllocator.API/ActiveAllocator.API.csproj index 7b1ce9c..41c06f9 100644 --- a/ActiveAllocator.API/ActiveAllocator.API.csproj +++ b/ActiveAllocator.API/ActiveAllocator.API.csproj @@ -25,7 +25,6 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/ActiveAllocator.API/ClientApp/src/components/accounts/AccountDashboard.js b/ActiveAllocator.API/ClientApp/src/components/accounts/AccountDashboard.js index 3275107..7cd1ade 100644 --- a/ActiveAllocator.API/ClientApp/src/components/accounts/AccountDashboard.js +++ b/ActiveAllocator.API/ClientApp/src/components/accounts/AccountDashboard.js @@ -49,6 +49,8 @@ export default function AccountDashboard() { const [envelopes, setEnvelopes] = useState([]); const [autoclassRules, setAutoclassRules] = useState([]); const [accountHistorical, setAccountHistorical] = useState({}); + + const [autoclassRuleTriggerMessage, setAutoclassRuleTriggerMessage] = useState(""); const [account, setAccount] = useState({ name: '', @@ -142,6 +144,17 @@ export default function AccountDashboard() { ); } + function triggerAutoclassRule(id) { + getData(EndPoints.AUTOCLASS_TRIGGERAUTOCLASSRULE + "/" + id).then( + (result) => { + if (result) { + setAutoclassRuleTriggerMessage(result.message); + setLoading(false); + } + } + ); + } + function findAccountName(id) { let name = accounts?.find(x => x.id === id)?.name; console.log("Id: " + id + ", Name: " + name); @@ -229,6 +242,11 @@ export default function AccountDashboard() { className="btn btn-sm btn-outline-danger" text="Delete" onClick={() => navigate(Routes.AUTOCLASS_DELETE + "?id=" + rule.id)} /> + triggerAutoclassRule(rule.id)} /> + {autoclassRuleTriggerMessage}
diff --git a/ActiveAllocator.API/ClientApp/src/components/services/AccessAPI.js b/ActiveAllocator.API/ClientApp/src/components/services/AccessAPI.js index e22139a..ba951a4 100644 --- a/ActiveAllocator.API/ClientApp/src/components/services/AccessAPI.js +++ b/ActiveAllocator.API/ClientApp/src/components/services/AccessAPI.js @@ -15,6 +15,8 @@ export const EndPoints = { AUTOCLASS: "Autoclass", AUTOCLASS_TYPEOPERATORINFO: "Autoclass/TypeOperatorInfo", AUTOCLASS_FIELDINFO: "Autoclass/FieldInfo", + AUTOCLASS_TRIGGERAUTOCLASSRULE: "Autoclass/TriggerAutoclassRule", + AUTOCLASS_TRIGGERTRANSACTIONRULES: "Autoclass/TriggerTransactionRules", } export function getData(endPoint) { diff --git a/ActiveAllocator.API/Controllers/AutoclassController.cs b/ActiveAllocator.API/Controllers/AutoclassController.cs index a0d99c9..0dfd2b5 100644 --- a/ActiveAllocator.API/Controllers/AutoclassController.cs +++ b/ActiveAllocator.API/Controllers/AutoclassController.cs @@ -86,4 +86,18 @@ public class AutoclassController : ControllerBase { return Ok(_autoclassService.GetAllFieldInfo()); } + + [HttpGet("TriggerAutoclassRule/{id}")] + public IActionResult TriggerAutoclassRule(int id) + { + int affectedTransactions = _autoclassService.ApplyAutoclassRule(id); + return Ok(new { message = $"AutoclassRule triggered and updated {affectedTransactions} transaction(s)." }); + } + + [HttpGet("TriggerTransactionRules/{id}")] + public IActionResult TriggerAutoclassRulesForTransaction(int id) + { + int triggeredRules = _autoclassService.ApplyRulesForTransaction(id); + return Ok(new { message = $"Transaction triggered {triggeredRules} autoclass rule(s)." }); + } } \ No newline at end of file diff --git a/ActiveAllocator.API/Entities/Transaction.cs b/ActiveAllocator.API/Entities/Transaction.cs index 46026cd..b6fd0b1 100644 --- a/ActiveAllocator.API/Entities/Transaction.cs +++ b/ActiveAllocator.API/Entities/Transaction.cs @@ -23,6 +23,39 @@ public class Transaction public List? Tags { get; set; } } +public static class TransactionExtensions +{ + public static bool HasTag(this Transaction transaction, string tag) + { + return transaction.Tags != null && transaction.Tags.Any(t => t.ToUpper() == tag.ToUpper()); + } + + public static Transaction AddTag(this Transaction transaction, string tag) + { + if (transaction.Tags == null) + transaction.Tags = new List() { tag }; + else if (transaction.Tags.Any(t => t.ToUpper() == tag.ToUpper()) == false) + transaction.Tags.Add(tag); + + return transaction; + } + + public static Transaction RemoveTag(this Transaction transaction, string tag) + { + if (transaction.Tags != null) + { + string? actualTag = transaction.Tags + .Where(t => t.ToUpper() == tag.ToUpper()) + .FirstOrDefault(); + + if (actualTag != null) + transaction.Tags.Remove(actualTag); + } + + return transaction; + } +} + public enum AutoclassTransactionField { DATE, diff --git a/ActiveAllocator.API/Models/Transactions/TransactionCreate.cs b/ActiveAllocator.API/Models/Transactions/TransactionCreate.cs index 10a519e..f435096 100644 --- a/ActiveAllocator.API/Models/Transactions/TransactionCreate.cs +++ b/ActiveAllocator.API/Models/Transactions/TransactionCreate.cs @@ -19,6 +19,7 @@ public class TransactionCreate public string Notes { get; set; } public bool IsPending { get; set; } public List? Tags { get; set; } = null; + public bool? TriggerAutoclassRules { get; set; } = null; } /* diff --git a/ActiveAllocator.API/Services/AutoclassService.cs b/ActiveAllocator.API/Services/AutoclassService.cs index 959db1c..d392dfa 100644 --- a/ActiveAllocator.API/Services/AutoclassService.cs +++ b/ActiveAllocator.API/Services/AutoclassService.cs @@ -21,21 +21,28 @@ public interface IAutoclassService void Delete(int id); List GetAllFieldInfo(); List GetAllTypeOperatorInfo(); + Transaction ProcessRule(Transaction transaction, AutoclassRule rule, out bool ruleFired); + int ApplyRulesForTransaction(int transactionId); + int ApplyAutoclassRule(int autoclassRule); + int ApplyAutoclassRule(AutoclassRule autoclassRule); } public class AutoclassService : IAutoclassService { private DataContext _context; private readonly IMapper _mapper; + private readonly ILogger _logger; private List typeMetaData; private List fieldMetaData; public AutoclassService( DataContext context, - IMapper mapper) + IMapper mapper, + ILogger logger) { _context = context; _mapper = mapper; + _logger = logger; this.CreateFieldMetaData(); this.CreateTypeMetaData(); @@ -249,4 +256,185 @@ public class AutoclassService : IAutoclassService { return this.typeMetaData; } + + public int ApplyRulesForTransaction(int transactionId) + { + Transaction? transaction = _context.Transactions + .Include(t => t.DebitEnvelope) + .Include(t => t.DebitAccount) + .Include(t => t.CreditAccount) + .Include(t => t.CreditEnvelope) + .Include(t => t.CurrencyType) + .FirstOrDefault(t => t.Id == transactionId); + + if (transaction == null) + throw new AppException($"Could not find transaction with id {transactionId}."); + + int rulesFired = 0; + + if (transaction.DebitAccount != null) + { + IEnumerable debitAccountRules = _context.AutoclassRules + .Include(r => r.Account) + .Where(r => r.Account.Id == transaction.DebitAccount.Id) + .Include(r => r.Expressions) + .Include(r => r.Changes) + .ToList(); + + + _logger.LogInformation($"Processing Autoclass Rules from DebitAccount '{transaction.DebitAccount.Id}' for new transaction '{transaction.Description}'."); + foreach (AutoclassRule rule in debitAccountRules) + { + transaction = ProcessRule(transaction, rule, out bool ruleFired); + + if (ruleFired) + rulesFired++; + } + + _logger.LogInformation($"...DebitAccount '{transaction.DebitAccount.Id}' rule processing complete."); + } + + if (transaction.CreditAccount != null) + { + IEnumerable creditAccountRules = _context.AutoclassRules + .Include(r => r.Account) + .Where(r => r.Account.Id == transaction.CreditAccount.Id) + .Include(r => r.Expressions) + .Include(r => r.Changes) + .ToList(); + + _logger.LogInformation($"Processing Autoclass Rules from CreditAccount '{transaction.CreditAccount.Id}' for new transaction '{transaction.Description}'."); + foreach (AutoclassRule rule in creditAccountRules) + { + transaction = ProcessRule(transaction, rule, out bool ruleFired); + + if (ruleFired) + rulesFired++; + } + + _logger.LogInformation($"...CreditAccount '{transaction.CreditAccount.Id}' rule processing complete."); + } + + _context.Transactions.Update(transaction); + _context.SaveChanges(); + + return rulesFired; + } + + public int ApplyAutoclassRule(int autoclassRuleId) + { + AutoclassRule? autoclassRule = _context.AutoclassRules + .Include(r => r.Account) + .Include(r => r.Expressions) + .Include(r => r.Changes) + .FirstOrDefault(r => r.Id == autoclassRuleId); + + if (autoclassRule == null) + throw new AppException($"AutoclassRule with id {autoclassRuleId} not found."); + + return this.ApplyAutoclassRule(autoclassRule); + } + + public int ApplyAutoclassRule(AutoclassRule autoclassRule) + { + if (autoclassRule.Account == null) + throw new AppException($"System Error: AutoclassRule with id {autoclassRule} lacks an associated account."); + + // Get Transactions + List transactions = _context.Transactions + .Include(t => t.DebitEnvelope) + .Include(t => t.DebitAccount) + .Include(t => t.CreditAccount) + .Include(t => t.CreditEnvelope) + .Include(t => t.CurrencyType) + .Where(t => (t.DebitAccount != null && t.DebitAccount.Id == autoclassRule.Account.Id) + || (t.CreditAccount != null && t.CreditAccount.Id == autoclassRule.Account.Id)) + .ToList(); + + int affectedTransactionsCount = 0; + + // Process Each Transaction for this rule + for (int i = 0; i < transactions.Count; i++) + { + transactions[i] = ProcessRule(transactions[i], autoclassRule, out bool ruleFired); + _context.Transactions.Update(transactions[i]); + + if (ruleFired) + affectedTransactionsCount++; + } + + _context.SaveChanges(); + + return affectedTransactionsCount; + } + + public Transaction ProcessRule(Transaction transaction, AutoclassRule rule, out bool ruleFired) + { + bool triggerRule = true; + + foreach (AutoclassExpression exp in rule.Expressions) + { + triggerRule &= exp.Evaluate(transaction); + } + + if (triggerRule) + { + _logger.LogInformation($"Rule '{rule.Name}' was triggered."); + foreach (AutoclassChange chg in rule.Changes) + { + _logger.LogInformation($"\tField {chg.Field} will be set to '{chg.Value}'."); + + switch (chg.Field) + { + case AutoclassTransactionField.DATE: + transaction.Date = Convert.ToDateTime(chg.Value); + break; + + case AutoclassTransactionField.EXTERNAL_ID: + transaction.ExternalId = chg.Value; + break; + + case AutoclassTransactionField.DESCRIPTION: + transaction.Description = chg.Value; + break; + + case AutoclassTransactionField.AMOUNT: + transaction.Amount = Convert.ToDecimal(chg.Value); + break; + + case AutoclassTransactionField.IS_PENDING: + transaction.IsPending = Convert.ToBoolean(chg.Value); + break; + + case AutoclassTransactionField.DEBIT_ACCOUNT: + transaction.DebitAccount = _context.Accounts.Find(Convert.ToInt32(chg.Value)); + break; + + case AutoclassTransactionField.CREDIT_ACCOUNT: + transaction.CreditAccount = _context.Accounts.Find(Convert.ToInt32(chg.Value)); + break; + + case AutoclassTransactionField.DEBIT_ENVELOPE: + transaction.DebitEnvelope = _context.Envelopes.Find(Convert.ToInt32(chg.Value)); + break; + + case AutoclassTransactionField.CREDIT_ENVELOPE: + transaction.CreditEnvelope = _context.Envelopes.Find(Convert.ToInt32(chg.Value)); + break; + + case AutoclassTransactionField.CURRENCY_TYPE: + transaction.CurrencyType = _context.CurrencyTypes.Find(Convert.ToInt32(chg.Value)); + break; + + case AutoclassTransactionField.TAGS: + transaction = transaction.AddTag(chg.Value); + break; + } + } + } + + ruleFired = triggerRule; + + return transaction; + } } \ No newline at end of file diff --git a/ActiveAllocator.API/Services/TransactionService.cs b/ActiveAllocator.API/Services/TransactionService.cs index 1fde882..2a6975d 100644 --- a/ActiveAllocator.API/Services/TransactionService.cs +++ b/ActiveAllocator.API/Services/TransactionService.cs @@ -28,15 +28,18 @@ public class TransactionService : ITransactionService private DataContext _context; private readonly IMapper _mapper; private readonly ILogger _logger; + private readonly IAutoclassService _autoclassService; public TransactionService( DataContext context, IMapper mapper, - ILogger logger) + ILogger logger, + IAutoclassService autoclassService) { _context = context; _mapper = mapper; _logger = logger; + _autoclassService = autoclassService; } public IEnumerable GetAll() @@ -125,15 +128,18 @@ public class TransactionService : ITransactionService _logger.LogInformation($"Aborted adding transaction '{transaction.Description}'."); return null; } - - // At this point transaction itself is valid - transaction = this.ApplyAutoclassRules(transaction); + // At this point transaction itself is valid + _context.Transactions.Add(transaction); _context.SaveChanges(); _logger.LogInformation("New transaction successfully created."); + if (!model.TriggerAutoclassRules.HasValue || model.TriggerAutoclassRules.Value) + _autoclassService.ApplyRulesForTransaction(transaction.Id); + //transaction = this.ApplyAutoclassRules(transaction); + this.UpdateAccountsAndEnvelopes(transaction); return transaction; @@ -198,15 +204,16 @@ public class TransactionService : ITransactionService transaction.Tags = model.Tags; this.ValidateTransaction(transaction); - - if (model.TriggerAutoclassRules.HasValue && model.TriggerAutoclassRules == true) - transaction = this.ApplyAutoclassRules(transaction); _context.Transactions.Update(transaction); _context.SaveChanges(); _logger.LogInformation($"Transaction '{id}' successfully updated."); + if (model.TriggerAutoclassRules.HasValue && model.TriggerAutoclassRules == true) + _autoclassService.ApplyRulesForTransaction(transaction.Id); + //transaction = this.ApplyAutoclassRules(transaction); + this.UpdateAccountsAndEnvelopes(transaction); } @@ -302,112 +309,6 @@ public class TransactionService : ITransactionService return true; } - private Transaction ApplyAutoclassRules(Transaction transaction) - { - if (transaction.DebitAccount != null) - { - IEnumerable debitAccountRules = _context.AutoclassRules - .Include(r => r.Account) - .Where(r => r.Account.Id == transaction.DebitAccount.Id) - .Include(r => r.Expressions) - .Include(r => r.Changes) - .ToList(); - - - _logger.LogInformation($"Processing Autoclass Rules from DebitAccount '{transaction.DebitAccount.Id}' for new transaction '{transaction.Description}'."); - foreach (AutoclassRule rule in debitAccountRules) - transaction = ProcessRule(transaction, rule); - - _logger.LogInformation($"...DebitAccount '{transaction.DebitAccount.Id}' rule processing complete."); - } - - if (transaction.CreditAccount != null) - { - IEnumerable creditAccountRules = _context.AutoclassRules - .Include(r => r.Account) - .Where(r => r.Account.Id == transaction.CreditAccount.Id) - .Include(r => r.Expressions) - .Include(r => r.Changes) - .ToList(); - - _logger.LogInformation($"Processing Autoclass Rules from CreditAccount '{transaction.CreditAccount.Id}' for new transaction '{transaction.Description}'."); - foreach (AutoclassRule rule in creditAccountRules) - transaction = ProcessRule(transaction, rule); - - _logger.LogInformation($"...CreditAccount '{transaction.CreditAccount.Id}' rule processing complete."); - } - - return transaction; - } - - private Transaction ProcessRule(Transaction transaction, AutoclassRule rule) - { - bool triggerRule = true; - - foreach (AutoclassExpression exp in rule.Expressions) - { - triggerRule &= exp.Evaluate(transaction); - } - - if (triggerRule) - { - _logger.LogInformation($"Rule '{rule.Name}' was triggered."); - foreach (AutoclassChange chg in rule.Changes) - { - _logger.LogInformation($"\tField {chg.Field} will be set to '{chg.Value}'."); - - switch (chg.Field) - { - case AutoclassTransactionField.DATE: - transaction.Date = Convert.ToDateTime(chg.Value); - break; - - case AutoclassTransactionField.EXTERNAL_ID: - transaction.ExternalId = chg.Value; - break; - - case AutoclassTransactionField.DESCRIPTION: - transaction.Description = chg.Value; - break; - - case AutoclassTransactionField.AMOUNT: - transaction.Amount = Convert.ToDecimal(chg.Value); - break; - - case AutoclassTransactionField.IS_PENDING: - transaction.IsPending = Convert.ToBoolean(chg.Value); - break; - - case AutoclassTransactionField.DEBIT_ACCOUNT: - transaction.DebitAccount = _context.Accounts.Find(Convert.ToInt32(chg.Value)); - break; - - case AutoclassTransactionField.CREDIT_ACCOUNT: - transaction.CreditAccount = _context.Accounts.Find(Convert.ToInt32(chg.Value)); - break; - - case AutoclassTransactionField.DEBIT_ENVELOPE: - transaction.DebitEnvelope = _context.Envelopes.Find(Convert.ToInt32(chg.Value)); - break; - - case AutoclassTransactionField.CREDIT_ENVELOPE: - transaction.CreditEnvelope = _context.Envelopes.Find(Convert.ToInt32(chg.Value)); - break; - - case AutoclassTransactionField.CURRENCY_TYPE: - transaction.CurrencyType = _context.CurrencyTypes.Find(Convert.ToInt32(chg.Value)); - break; - - case AutoclassTransactionField.TAGS: - transaction = AddTag(transaction, chg.Value); - break; - } - } - } - - return transaction; - } - private void UpdateAccountsAndEnvelopes(Transaction transaction, bool invert = false) { _logger.LogInformation($"Updating accounts and envelopes affected by transaction '{transaction.Id}'."); @@ -428,36 +329,6 @@ public class TransactionService : ITransactionService _context.AddAmountToEnvelope(transaction.CreditEnvelope.Id, creditAmount); } - public bool HasTag(Transaction transaction, string tag) - { - return transaction.Tags == null ? false : transaction.Tags.Any(t => t.ToUpper() == tag.ToUpper()); - } - - private Transaction AddTag(Transaction transaction, string tag) - { - if (transaction.Tags == null) - transaction.Tags = new List() { tag }; - else if (transaction.Tags.Any(t => t.ToUpper() == tag.ToUpper()) == false) - transaction.Tags.Add(tag); - - return transaction; - } - - private Transaction RemoveTag(Transaction transaction, string tag) - { - if (transaction.Tags != null) - { - string? actualTag = transaction.Tags - .Where(t => t.ToUpper() == tag.ToUpper()) - .FirstOrDefault(); - - if (actualTag != null) - transaction.Tags.Remove(actualTag); - } - - return transaction; - } - public void Delete(int id) { var transaction = getTransaction(id); diff --git a/Build.sh b/Build.sh new file mode 100755 index 0000000..c8c6c11 --- /dev/null +++ b/Build.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +dotnet build \ No newline at end of file