356 lines
14 KiB
C#
356 lines
14 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.Transactions;
|
||
|
using System.Collections;
|
||
|
using System.Collections.Generic;
|
||
|
using System.Linq;
|
||
|
using System;
|
||
|
using Microsoft.EntityFrameworkCore;
|
||
|
using Internal;
|
||
|
using System.Collections.Immutable;
|
||
|
|
||
|
public interface ITransactionService
|
||
|
{
|
||
|
IEnumerable<Transaction> GetAll();
|
||
|
Transaction GetById(int id);
|
||
|
IEnumerable<Transaction> BulkCreate(List<TransactionCreate> model);
|
||
|
Transaction Create(TransactionCreate model, bool errorOnFail = true);
|
||
|
void Update(int id, TransactionUpdateRequest model);
|
||
|
void Delete(int id);
|
||
|
}
|
||
|
|
||
|
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,
|
||
|
IAutoclassService autoclassService)
|
||
|
{
|
||
|
_context = context;
|
||
|
_mapper = mapper;
|
||
|
_logger = logger;
|
||
|
_autoclassService = autoclassService;
|
||
|
}
|
||
|
|
||
|
public IEnumerable<Transaction> GetAll()
|
||
|
{
|
||
|
return _context.Transactions
|
||
|
.Include(t => t.CurrencyType)
|
||
|
.Include(t => t.DebitAccount)
|
||
|
.Include(t => t.CreditAccount)
|
||
|
.Include(t => t.DebitEnvelope)
|
||
|
.Include(t => t.CreditEnvelope)
|
||
|
.ToList();
|
||
|
}
|
||
|
|
||
|
public Transaction GetById(int id)
|
||
|
{
|
||
|
return getTransaction(id);
|
||
|
}
|
||
|
|
||
|
private Account prepareAccount(int? accountId)
|
||
|
{
|
||
|
if (accountId == null || accountId.Value == 0)
|
||
|
{
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
Account account = _context.Accounts.Find(accountId.Value);
|
||
|
if (account == null)
|
||
|
throw new AppException("Could not find account with ID of '" + accountId.Value + "'.");
|
||
|
|
||
|
return account;
|
||
|
}
|
||
|
|
||
|
private Envelope prepareEnvelope(int? envelopeId)
|
||
|
{
|
||
|
if (envelopeId == null || envelopeId.Value == 0)
|
||
|
{
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
Envelope envelope = _context.Envelopes
|
||
|
.Include(e => e.Account)
|
||
|
.FirstOrDefault(e => e.Id == envelopeId.Value);
|
||
|
|
||
|
if (envelope == null)
|
||
|
throw new AppException("Could not find envelope with ID of '" + envelopeId.Value + "'.");
|
||
|
|
||
|
return envelope;
|
||
|
}
|
||
|
|
||
|
public IEnumerable<Transaction> BulkCreate(List<TransactionCreate> model)
|
||
|
{
|
||
|
List<Transaction> transactions = new List<Transaction>();
|
||
|
|
||
|
foreach (TransactionCreate tr in model)
|
||
|
{
|
||
|
var tran = this.Create(tr, false);
|
||
|
if (tran != null)
|
||
|
transactions.Add(tran);
|
||
|
}
|
||
|
|
||
|
return transactions;
|
||
|
}
|
||
|
|
||
|
public Transaction Create(TransactionCreate model, bool errorOnFail = true)
|
||
|
{
|
||
|
Transaction transaction = new Transaction {
|
||
|
Description = model.Description,
|
||
|
Date = model.Date.Date.ToUniversalTime(),
|
||
|
CreatedOn = DateTime.UtcNow,
|
||
|
UpdatedOn = DateTime.UtcNow,
|
||
|
ExternalId = string.IsNullOrWhiteSpace(model.ExternalId) ? "" : model.ExternalId,
|
||
|
DebitAccount = prepareAccount(model.DebitAccount),
|
||
|
CreditAccount = prepareAccount(model.CreditAccount),
|
||
|
DebitEnvelope = prepareEnvelope(model.DebitEnvelope),
|
||
|
CreditEnvelope = prepareEnvelope(model.CreditEnvelope),
|
||
|
IsEnvelopeFundingTransaction = model.IsEnvelopeFundingTransaction == null ? false : model.IsEnvelopeFundingTransaction.Value,
|
||
|
Amount = Convert.ToDecimal(model.Amount),
|
||
|
CurrencyType = _context.CurrencyTypes.Find(model.CurrencyType),
|
||
|
Notes = model.Notes,
|
||
|
IsPending = model.IsPending,
|
||
|
Tags = model.Tags,
|
||
|
};
|
||
|
|
||
|
if (this.ValidateTransaction(transaction, errorOnFail) == false)
|
||
|
{
|
||
|
_logger.LogInformation($"Aborted adding transaction '{transaction.Description}'.");
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// 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;
|
||
|
}
|
||
|
|
||
|
public void Update(int id, TransactionUpdateRequest model)
|
||
|
{
|
||
|
Transaction transaction = getTransaction(id);
|
||
|
|
||
|
string temp = "";
|
||
|
|
||
|
// Transaction.Date
|
||
|
if (model.Date.HasValue)
|
||
|
transaction.Date = model.Date.Value;
|
||
|
|
||
|
// Transaction.ExternalId
|
||
|
if (model.ExternalId != null)
|
||
|
transaction.ExternalId = model.ExternalId;
|
||
|
|
||
|
// Transaction.Description
|
||
|
if (model.Description != null)
|
||
|
transaction.Description = model.Description;
|
||
|
|
||
|
// Transaction.DebitAccount
|
||
|
if (model.DebitAccount.HasValue)
|
||
|
transaction.DebitAccount = prepareAccount(model.DebitAccount);
|
||
|
|
||
|
// Transaction.CreditAccount
|
||
|
if (model.CreditAccount.HasValue)
|
||
|
transaction.CreditAccount = prepareAccount(model.CreditAccount.Value);
|
||
|
|
||
|
// Transaction.DebitEnvelope
|
||
|
if (model.DebitEnvelope.HasValue)
|
||
|
transaction.DebitEnvelope = prepareEnvelope(model.DebitEnvelope.Value);
|
||
|
|
||
|
// Transaction.CreditEnvelope
|
||
|
if (model.CreditEnvelope.HasValue)
|
||
|
transaction.CreditEnvelope = prepareEnvelope(model.CreditEnvelope.Value);
|
||
|
|
||
|
// Transaction.IsEnvelopeFundingTransaction
|
||
|
if (model.IsEnvelopeFundingTransaction.HasValue)
|
||
|
transaction.IsEnvelopeFundingTransaction = model.IsEnvelopeFundingTransaction.Value;
|
||
|
|
||
|
// Transaction.Amount
|
||
|
if (model.Amount.HasValue)
|
||
|
transaction.Amount = model.Amount.Value;
|
||
|
|
||
|
// Transaction.CurrencyType
|
||
|
if (model.CurrencyType.HasValue)
|
||
|
transaction.CurrencyType = _context.CurrencyTypes.Find(model.CurrencyType.Value);
|
||
|
|
||
|
// Transaction.Notes
|
||
|
if (model.Notes != null)
|
||
|
transaction.Notes = model.Notes;
|
||
|
|
||
|
// Transaction.IsPending
|
||
|
if (model.IsPending.HasValue)
|
||
|
transaction.IsPending = model.IsPending.Value;
|
||
|
|
||
|
// Tags
|
||
|
if (model.Tags != null)
|
||
|
transaction.Tags = model.Tags;
|
||
|
|
||
|
this.ValidateTransaction(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);
|
||
|
}
|
||
|
|
||
|
private bool ValidateTransaction(int transactionId)
|
||
|
{
|
||
|
return this.ValidateTransaction(getTransaction(transactionId));
|
||
|
}
|
||
|
|
||
|
private bool ErrorOrFalse(bool error, string errorMessage)
|
||
|
{
|
||
|
if (error)
|
||
|
throw new AppException(errorMessage);
|
||
|
|
||
|
_logger.LogWarning(errorMessage);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// FIX VALIDATETRANSACTION REFERENCES TO USE BOOL
|
||
|
private bool ValidateTransaction(Transaction transaction, bool errorOnFail = true)
|
||
|
{
|
||
|
// MUST!!! Classify intra-account transactions as NULL account ids, and only envelope Ids
|
||
|
// MUST!!! Classify inter-account transactions as correct account ids, even with envelope ids
|
||
|
|
||
|
// There has to be at least 1 specified account or envelope
|
||
|
if (transaction.DebitAccount == null
|
||
|
&& transaction.CreditAccount == null
|
||
|
&& transaction.DebitEnvelope == null
|
||
|
&& transaction.CreditEnvelope == null)
|
||
|
return ErrorOrFalse(errorOnFail, "There must be an envelope or account chosen for a transaction.");
|
||
|
|
||
|
// Envelopes must have associated parent account entities
|
||
|
if (transaction.CreditEnvelope != null && transaction.CreditEnvelope.Account == null)
|
||
|
return ErrorOrFalse(errorOnFail, $"The parent account of envelope '{transaction.CreditEnvelope.Name}' with id '{transaction.CreditEnvelope.Id}' could not be found.");
|
||
|
|
||
|
if (transaction.DebitEnvelope != null && transaction.DebitEnvelope.Account == null)
|
||
|
return ErrorOrFalse(errorOnFail, $"The parent account of envelope '{transaction.DebitEnvelope.Name}' with id '{transaction.DebitEnvelope.Id}' could not be found.");
|
||
|
|
||
|
// Accounts cannot be the same
|
||
|
if (transaction.DebitAccount != null && transaction.CreditAccount != null &&
|
||
|
transaction.DebitAccount.Id == transaction.CreditAccount.Id)
|
||
|
return ErrorOrFalse(errorOnFail, "The debit and credit accounts of a transaction cannot be the same.");
|
||
|
|
||
|
// Envelopes cannot be the same
|
||
|
if (transaction.DebitEnvelope != null && transaction.CreditEnvelope != null &&
|
||
|
transaction.DebitEnvelope.Id == transaction.CreditEnvelope.Id)
|
||
|
return ErrorOrFalse(errorOnFail, "The debit and credit envelopes of a transaction cannot be the same.");
|
||
|
|
||
|
// Envelopes must be child of underlying parent account
|
||
|
if (transaction.DebitEnvelope != null && transaction.DebitAccount != null &&
|
||
|
transaction.DebitEnvelope.Account.Id != transaction.DebitAccount.Id)
|
||
|
return ErrorOrFalse(errorOnFail, "The debit envelope of a transaction must be a child of the debit account.");
|
||
|
|
||
|
if (transaction.CreditEnvelope != null && transaction.CreditAccount != null &&
|
||
|
transaction.CreditEnvelope.Account.Id != transaction.CreditAccount.Id)
|
||
|
return ErrorOrFalse(errorOnFail, "The credit envelope of a transaction must be a child of the credit account.");
|
||
|
|
||
|
// If just envelopes are specified, they must belong to the same parent account, or add appropriate accounts
|
||
|
if (transaction.CreditEnvelope != null
|
||
|
&& transaction.DebitEnvelope != null
|
||
|
&& (transaction.DebitAccount != null || transaction.CreditAccount != null))
|
||
|
{
|
||
|
// There is an account specified when the envelopes are of the same account
|
||
|
if (transaction.CreditEnvelope.Account.Id == transaction.DebitEnvelope.Account.Id)
|
||
|
{
|
||
|
transaction.CreditAccount = null;
|
||
|
transaction.DebitAccount = null;
|
||
|
}
|
||
|
else
|
||
|
// There are envelopes from different accounts, without the underlying accounts specified
|
||
|
{
|
||
|
transaction.CreditAccount = transaction.CreditEnvelope.Account;
|
||
|
transaction.DebitAccount = transaction.DebitEnvelope.Account;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Transaction Duplication Check - External ID
|
||
|
if (!string.IsNullOrWhiteSpace(transaction.ExternalId) && _context.Transactions.Any(x => x.ExternalId == transaction.ExternalId))
|
||
|
return ErrorOrFalse(errorOnFail, "Transaction with the external ID '" + transaction.ExternalId + "' already exists");
|
||
|
|
||
|
// Transaction Duplication Check - All other fields
|
||
|
if (_context.Transactions.Any(x =>
|
||
|
x.Description == transaction.Description
|
||
|
&& x.Date == transaction.Date
|
||
|
&& x.DebitAccount == transaction.DebitAccount
|
||
|
&& x.CreditAccount == transaction.CreditAccount
|
||
|
&& x.DebitEnvelope == transaction.DebitEnvelope
|
||
|
&& x.CreditEnvelope == transaction.CreditEnvelope
|
||
|
&& x.Amount == transaction.Amount))
|
||
|
{
|
||
|
return ErrorOrFalse(errorOnFail, "Transaction with the same fields already exists");
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
private void UpdateAccountsAndEnvelopes(Transaction transaction, bool invert = false)
|
||
|
{
|
||
|
_logger.LogInformation($"Updating accounts and envelopes affected by transaction '{transaction.Id}'.");
|
||
|
|
||
|
decimal debitAmount = invert ? transaction.Amount : -transaction.Amount;
|
||
|
decimal creditAmount = invert ? -transaction.Amount : transaction.Amount;
|
||
|
|
||
|
if (transaction.DebitAccount != null)
|
||
|
_context.AddAmountToAccount(transaction.DebitAccount.Id, debitAmount);
|
||
|
|
||
|
if (transaction.CreditAccount != null)
|
||
|
_context.AddAmountToAccount(transaction.CreditAccount.Id, creditAmount);
|
||
|
|
||
|
if (transaction.DebitEnvelope != null)
|
||
|
_context.AddAmountToEnvelope(transaction.DebitEnvelope.Id, debitAmount);
|
||
|
|
||
|
if (transaction.CreditEnvelope != null)
|
||
|
_context.AddAmountToEnvelope(transaction.CreditEnvelope.Id, creditAmount);
|
||
|
}
|
||
|
|
||
|
public void Delete(int id)
|
||
|
{
|
||
|
var transaction = getTransaction(id);
|
||
|
_context.Transactions.Remove(transaction);
|
||
|
_context.SaveChanges();
|
||
|
|
||
|
UpdateAccountsAndEnvelopes(transaction, true);
|
||
|
}
|
||
|
|
||
|
private Transaction getTransaction(int id)
|
||
|
{
|
||
|
var 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 == id);
|
||
|
|
||
|
if (transaction == null)
|
||
|
throw new KeyNotFoundException("Transaction not found");
|
||
|
|
||
|
return transaction;
|
||
|
}
|
||
|
}
|