Added rule trigger endpoints

pull/33/head
William Lewis 3 months ago
parent 9408b98775
commit 5d1a4a1e4d

@ -25,7 +25,6 @@
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="BCrypt.Net" Version="0.1.0" />
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="7.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.9">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

@ -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)} />
<RowButton
className="btn btn-sm btn-outline-warning"
text="Trigger"
onClick={() => triggerAutoclassRule(rule.id)} />
<span>{autoclassRuleTriggerMessage}</span>
</div>
</div>
<div className="row">

@ -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) {

@ -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)." });
}
}

@ -23,6 +23,39 @@ public class Transaction
public List<string>? 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<string>() { 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,

@ -19,6 +19,7 @@ public class TransactionCreate
public string Notes { get; set; }
public bool IsPending { get; set; }
public List<string>? Tags { get; set; } = null;
public bool? TriggerAutoclassRules { get; set; } = null;
}
/*

@ -21,21 +21,28 @@ public interface IAutoclassService
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)
IMapper mapper,
ILogger<AutoclassService> 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<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;
}
}

@ -28,15 +28,18 @@ public class TransactionService : ITransactionService
private DataContext _context;
private readonly IMapper _mapper;
private readonly ILogger<TransactionService> _logger;
private readonly IAutoclassService _autoclassService;
public TransactionService(
DataContext context,
IMapper mapper,
ILogger<TransactionService> logger)
ILogger<TransactionService> logger,
IAutoclassService autoclassService)
{
_context = context;
_mapper = mapper;
_logger = logger;
_autoclassService = autoclassService;
}
public IEnumerable<Transaction> 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<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);
_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);
_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<string>() { 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);

@ -0,0 +1,3 @@
#!/bin/bash
dotnet build
Loading…
Cancel
Save