namespace AAIntegration.SimmonsBank.API.Services; using AutoMapper; using BCrypt.Net; using AAIntegration.SimmonsBank.API.Entities; using AAIntegration.SimmonsBank.API.Config; using AAIntegration.SimmonsBank.API.Models.Autoclass; using System.Collections; using System.Collections.Generic; using System.Linq; using System; using Microsoft.EntityFrameworkCore; using Internal; public interface IAutoclassService { IEnumerable GetAll(int? account = null); AutoclassRule GetById(int id); void Create(AutoclassRuleCreateRequest model); void Update(int autoclassId, AutoclassRuleUpdateRequest model); 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, ILogger logger) { _context = context; _mapper = mapper; _logger = logger; this.CreateFieldMetaData(); this.CreateTypeMetaData(); } public IEnumerable GetAll(int? account = null) { if (account.HasValue) { return _context.AutoclassRules .Include(t => t.Account) .Include(t => t.Expressions) .Include(t => t.Changes) .ToList() .Where(a => a.Account.Id == account.Value); } return _context.AutoclassRules .Include(t => t.Expressions) .Include(t => t.Changes) .ToList(); } public AutoclassRule GetById(int id) { return getAutoclass(id); } public void Create(AutoclassRuleCreateRequest model) { AutoclassRule rule = new AutoclassRule { Name = model.Name, Account = _context.Accounts.Find(model.AccountId), Expressions = _mapper.Map>(model.Expressions), Changes = _mapper.Map>(model.Changes), Enabled = model.Enabled }; if (rule.Account == null) throw new AppException($"Could not find the account with id '{model.AccountId}'."); if (rule.Account.Id == 0) throw new AppException($"Can not add rules to account with id '0'."); if (_context.AutoclassRules.Any(x => x.Name.ToUpper() == rule.Name.ToUpper() && x.Account.Id == rule.Account.Id)) { throw new AppException("AutoclassRule with the same name already exists for this account."); } _context.AutoclassRules.Add(rule); _context.SaveChanges(); if (model.TriggerOnCreate.HasValue && model.TriggerOnCreate.Value) this.ApplyAutoclassRule(rule.Id); } public void Update(int autoclassId, AutoclassRuleUpdateRequest model) { AutoclassRule rule = getAutoclass(autoclassId); // Account if (model.AccountId.HasValue) rule.Account = _context.Accounts.Find(model.AccountId.Value); // Name if (!string.IsNullOrWhiteSpace(model.Name)) { if (model.Name != rule.Name && _context.AutoclassRules.Any(x => x.Name.ToUpper() == model.Name.ToUpper() && x.Account.Id == rule.Account.Id)) { throw new AppException("AutoclassRule with the same name already exists for this account."); } rule.Name = model.Name; } // Enabled if (model.Enabled.HasValue) rule.Enabled = model.Enabled.Value; // Expressions if (model.Expressions != null) { // Remove expression defined on autoclass rule foreach (AutoclassExpression expression in rule.Expressions) _context.AutoclassExpressions.Remove(expression); // Add the expressions from request rule.Expressions = _mapper.Map>(model.Expressions); } // Changes if (model.Changes != null) { // Remove changes defined on autoclass rule foreach (AutoclassChange changes in rule.Changes) _context.AutoclassChanges.Remove(changes); // Add the expressions from request rule.Changes = _mapper.Map>(model.Changes); } _context.AutoclassRules.Update(rule); _context.SaveChanges(); // TriggerOnUpdate if (model.TriggerOnUpdate.HasValue && model.TriggerOnUpdate.Value) this.ApplyAutoclassRule(rule.Id); } public void Delete(int id) { var autoclassRule = getAutoclass(id); _context.AutoclassRules.Remove(autoclassRule); _context.SaveChanges(); } private AutoclassRule getAutoclass(int id) { var autoclassRule = _context.AutoclassRules .Include(t => t.Account) .Include(t => t.Expressions) .Include(t => t.Changes) .FirstOrDefault(o => o.Id == id); //_context.Entry(autoclassRule).Reference(t => t.Expressions).Load(); //_context.Entry(autoclassRule).Reference(t => t.Changes).Load(); if (autoclassRule == null) throw new KeyNotFoundException("AutoclassRule not found"); return autoclassRule; } private void CreateFieldMetaData() { List fields = new List(); fields.Add(new AutoclassFieldMetaDTO(AutoclassTransactionField.AMOUNT)); fields.Add(new AutoclassFieldMetaDTO(AutoclassTransactionField.CREDIT_ACCOUNT)); fields.Add(new AutoclassFieldMetaDTO(AutoclassTransactionField.CREDIT_ENVELOPE)); fields.Add(new AutoclassFieldMetaDTO(AutoclassTransactionField.CURRENCY_TYPE)); fields.Add(new AutoclassFieldMetaDTO(AutoclassTransactionField.DATE)); fields.Add(new AutoclassFieldMetaDTO(AutoclassTransactionField.DEBIT_ACCOUNT)); fields.Add(new AutoclassFieldMetaDTO(AutoclassTransactionField.DEBIT_ENVELOPE)); fields.Add(new AutoclassFieldMetaDTO(AutoclassTransactionField.DESCRIPTION)); fields.Add(new AutoclassFieldMetaDTO(AutoclassTransactionField.EXTERNAL_ID)); fields.Add(new AutoclassFieldMetaDTO(AutoclassTransactionField.IS_PENDING)); fields.Add(new AutoclassFieldMetaDTO(AutoclassTransactionField.TAGS)); this.fieldMetaData = fields; } public List GetAllFieldInfo() { return this.fieldMetaData; } private void CreateTypeMetaData() { List types = new List(); types.Add(new AutoclassTypeMetaDTO(AutoclassType.DATETIME)); types.Add(new AutoclassTypeMetaDTO(AutoclassType.STRING)); types.Add(new AutoclassTypeMetaDTO(AutoclassType.DECIMAL)); types.Add(new AutoclassTypeMetaDTO(AutoclassType.BOOLEAN)); types.Add(new AutoclassTypeMetaDTO(AutoclassType.ACCOUNT)); types.Add(new AutoclassTypeMetaDTO(AutoclassType.ENVELOPE)); types.Add(new AutoclassTypeMetaDTO(AutoclassType.CURRENCYTYPE)); types.Add(new AutoclassTypeMetaDTO(AutoclassType.STRING_ARRAY)); this.typeMetaData = types; } public List GetAllTypeOperatorInfo() { 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; } }