407 lines
15 KiB
C#

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<AutoclassRule> GetAll(int? account = null);
AutoclassRule GetById(int id);
void Create(AutoclassRuleCreateRequest model);
void Update(int autoclassId, AutoclassRuleUpdateRequest model);
void Delete(int id);
List<AutoclassFieldMetaDTO> GetAllFieldInfo();
List<AutoclassTypeMetaDTO> 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<AutoclassService> _logger;
private List<AutoclassTypeMetaDTO> typeMetaData;
private List<AutoclassFieldMetaDTO> fieldMetaData;
public AutoclassService(
DataContext context,
IMapper mapper,
ILogger<AutoclassService> logger)
{
_context = context;
_mapper = mapper;
_logger = logger;
this.CreateFieldMetaData();
this.CreateTypeMetaData();
}
public IEnumerable<AutoclassRule> 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<IEnumerable<AutoclassExpression>>(model.Expressions),
Changes = _mapper.Map<IEnumerable<AutoclassChange>>(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<IEnumerable<AutoclassExpression>>(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<IEnumerable<AutoclassChange>>(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<AutoclassFieldMetaDTO> fields = new List<AutoclassFieldMetaDTO>();
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<AutoclassFieldMetaDTO> GetAllFieldInfo()
{
return this.fieldMetaData;
}
private void CreateTypeMetaData()
{
List<AutoclassTypeMetaDTO> types = new List<AutoclassTypeMetaDTO>();
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<AutoclassTypeMetaDTO> 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<AutoclassRule> 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<AutoclassRule> 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<Transaction> 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;
}
}