396 lines
14 KiB
C#
396 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.Envelopes;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System;
|
|
using Internal;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using AAIntegration.SimmonsBank.API.Models.Transactions;
|
|
|
|
public interface IEnvelopeService
|
|
{
|
|
IEnumerable<Envelope> GetAll(int? accountId = null);
|
|
Envelope GetById(int envelopeId);
|
|
void Create(EnvelopeCreateRequest model);
|
|
void Update(int envelopeId, EnvelopeUpdateRequest model);
|
|
//Envelope RefreshEnvelopeBalance(int envelopeId);
|
|
void Delete(int envelopeId);
|
|
bool HasBeenFundedThisMonth(int envelopeId);
|
|
EnvelopeHistorical GetEnvelopeHistorical(int envelopeId, int lookBack);
|
|
void FundEnvelopeForThisMonth(int envelopeId);
|
|
List<EnvelopeFundingMethodModes> GetEnvelopeFundingMethodModes();
|
|
}
|
|
|
|
public class EnvelopeService : IEnvelopeService
|
|
{
|
|
private DataContext _context;
|
|
private readonly IMapper _mapper;
|
|
private readonly ITransactionService _transactionService;
|
|
private readonly ILogger<EnvelopeService> _logger;
|
|
|
|
public EnvelopeService(
|
|
DataContext context,
|
|
IMapper mapper,
|
|
ITransactionService transactionService,
|
|
ILogger<EnvelopeService> logger)
|
|
{
|
|
_context = context;
|
|
_mapper = mapper;
|
|
_transactionService = transactionService;
|
|
_logger = logger;
|
|
}
|
|
|
|
public IEnumerable<Envelope> GetAll(int? accountId = null)
|
|
{
|
|
if (accountId != null)
|
|
return _context.Envelopes
|
|
.Include(e => e.Account)
|
|
.Include(e => e.FundingMethods)
|
|
.Where(e => e.Account.Id == accountId.Value);
|
|
|
|
return _context.Envelopes
|
|
.Include(e => e.FundingMethods);
|
|
}
|
|
|
|
public Envelope GetById(int envelopeId)
|
|
{
|
|
return getEnvelope(envelopeId);
|
|
}
|
|
|
|
public void Create(EnvelopeCreateRequest model)
|
|
{
|
|
Account account = _context.Accounts.Find(model.Account);
|
|
if (account == null)
|
|
throw new AppException("Could not create envelope. Account with id '" + model.Account + "' not found.");
|
|
|
|
// Check that envelope with same name or same external number doesn't exist
|
|
IEnumerable<Envelope> envelopesWithSameName = _context.Envelopes
|
|
.Include(e => e.Account)
|
|
.Where(e =>
|
|
e.Name.ToUpper() == model.Name.ToUpper()
|
|
&& e.Account.Id == model.Account);
|
|
|
|
if (envelopesWithSameName.Count() > 0)
|
|
throw new AppException("Envelope with name '" + model.Name + "' already exists for account '" + account.Name + "'.");
|
|
|
|
// map model to new envelope object
|
|
//var envelope = _mapper.Map<Envelope>(model);
|
|
|
|
Envelope envelope = new Envelope {
|
|
Name = model.Name,
|
|
Account = account,
|
|
InitialBalance = Convert.ToDecimal(model.InitialBalance),
|
|
Balance = Convert.ToDecimal(model.InitialBalance),
|
|
Enabled = model.Enabled,
|
|
IsPersistant = model.IsPersistant,
|
|
Priority = model.Priority,
|
|
LastTriggeredOn = null
|
|
};
|
|
|
|
_context.Envelopes.Add(envelope);
|
|
_context.SaveChanges();
|
|
|
|
foreach (EnvelopeFundingMethodCreateRequest methodReq in model.FundingMethods)
|
|
{
|
|
EnvelopeFundingMethod method = new EnvelopeFundingMethod {
|
|
Envelope = envelope,
|
|
Order = methodReq.Order,
|
|
Mode = methodReq.Mode,
|
|
Value = methodReq.Value,
|
|
PeriodsToLookback = methodReq.PeriodsToLookback
|
|
};
|
|
|
|
_context.EnvelopeFundingMethods.Add(method);
|
|
}
|
|
|
|
_context.SaveChanges();
|
|
}
|
|
|
|
public void Update(int envelopeId, EnvelopeUpdateRequest model)
|
|
{
|
|
Envelope envelope = getEnvelope(envelopeId);
|
|
|
|
// Name
|
|
if (!string.IsNullOrWhiteSpace(model.Name))
|
|
{
|
|
if (model.Name != envelope.Name && _context.Envelopes.Any(x => x.Name == model.Name))
|
|
throw new AppException("Envelope with the name '" + model.Name + "' already exists");
|
|
|
|
envelope.Name = model.Name;
|
|
}
|
|
|
|
// Account
|
|
if (model.Account.HasValue)
|
|
envelope.Account = _context.Accounts.Find(model.Account.Value);
|
|
|
|
// Enabled
|
|
if (model.Enabled.HasValue)
|
|
envelope.Enabled = model.Enabled.Value;
|
|
|
|
// Period
|
|
if (model.Period.HasValue)
|
|
envelope.Period = model.Period.Value;
|
|
|
|
// IsPersistant
|
|
if (model.IsPersistant.HasValue)
|
|
envelope.IsPersistant = model.IsPersistant.Value;
|
|
|
|
// Priority
|
|
if (model.Priority.HasValue)
|
|
envelope.Priority = model.Priority.Value;
|
|
|
|
// Last Triggered On
|
|
if (model.LastTriggeredOn.HasValue)
|
|
envelope.LastTriggeredOn = model.LastTriggeredOn;
|
|
|
|
// FundingMethods
|
|
if (model.FundingMethods != null)
|
|
{
|
|
// Remove funding methods defined on envelope
|
|
foreach (EnvelopeFundingMethod method in envelope.FundingMethods)
|
|
{
|
|
_context.EnvelopeFundingMethods.Remove(method);
|
|
}
|
|
|
|
// Add the methods from request
|
|
foreach (EnvelopeFundingMethodCreateRequest methodReq in model.FundingMethods)
|
|
{
|
|
EnvelopeFundingMethod method = new EnvelopeFundingMethod {
|
|
Envelope = envelope,
|
|
Order = methodReq.Order,
|
|
Mode = methodReq.Mode,
|
|
Value = methodReq.Value,
|
|
PeriodsToLookback = methodReq.PeriodsToLookback
|
|
};
|
|
|
|
_context.EnvelopeFundingMethods.Add(method);
|
|
}
|
|
}
|
|
|
|
// copy model to envelope and save
|
|
//_mapper.Map(model, envelope);
|
|
_context.Envelopes.Update(envelope);
|
|
_context.SaveChanges();
|
|
_context.RecalculateEnvelopeBalance(envelopeId);
|
|
}
|
|
/*
|
|
public Envelope RefreshEnvelopeBalance(int envelopeId)
|
|
{
|
|
_context.RecalculateEnvelopeBalance(envelopeId);
|
|
return _context.Envelopes.Find(envelopeId);
|
|
}*/
|
|
|
|
public void Delete(int envelopeId)
|
|
{
|
|
// 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
|
|
|
|
List<Transaction> delTransactions = _context.Transactions
|
|
.Include(t => t.CreditEnvelope)
|
|
.Include(t => t.DebitEnvelope)
|
|
.Include(t => t.CreditAccount)
|
|
.Include(t => t.DebitAccount)
|
|
.Where(t =>
|
|
(t.CreditEnvelope != null && t.CreditEnvelope.Id == envelopeId) ||
|
|
(t.DebitEnvelope != null && t.DebitEnvelope.Id == envelopeId))
|
|
.ToList();
|
|
|
|
foreach (Transaction tran in delTransactions)
|
|
{
|
|
if (tran.CreditAccount == null && tran.DebitAccount == null && (tran.CreditEnvelope == null || tran.DebitEnvelope == null))
|
|
{
|
|
// Delete transactions who only have credit or debit envelope of this envelope, and nothing else (funding type transactions)
|
|
_context.Transactions.Remove(tran);
|
|
}
|
|
else
|
|
{
|
|
// Change transactions that have a reference to this envelopeId
|
|
if (tran.DebitEnvelope?.Id == envelopeId)
|
|
tran.DebitEnvelope = null;
|
|
else
|
|
tran.CreditEnvelope = null;
|
|
}
|
|
}
|
|
|
|
var envelope = getEnvelope(envelopeId);
|
|
_context.Envelopes.Remove(envelope);
|
|
_context.SaveChanges();
|
|
}
|
|
|
|
public bool HasBeenFundedThisMonth(int envelopeId)
|
|
{
|
|
return _context.Transactions
|
|
.Include(t => t.CreditEnvelope)
|
|
.Where(t => t.CreditEnvelope != null && t.CreditEnvelope.Id == envelopeId && t.IsEnvelopeFundingTransaction == true)
|
|
.Any(t => t.Date.Year == DateTime.UtcNow.Year && t.Date.Month == DateTime.UtcNow.Month);
|
|
}
|
|
|
|
public EnvelopeHistorical GetEnvelopeHistorical(int envelopeId, int lookBack)
|
|
{
|
|
// Input validate lookBack
|
|
if (lookBack < 1)
|
|
throw new AppException($"Look Back value must be greater than 0.");
|
|
|
|
return _context.GetEnvelopeHistorical(envelopeId, lookBack);
|
|
}
|
|
|
|
public void FundEnvelopeForThisMonth(int envelopeId)
|
|
{
|
|
// !!!!!!
|
|
// Add notes in UI about how things are calculated
|
|
|
|
Envelope env = _context.Envelopes
|
|
.Include(e => e.FundingMethods)
|
|
.FirstOrDefault(e => e.Id == envelopeId);
|
|
|
|
if (env.Enabled == false)
|
|
{
|
|
_logger.LogInformation($"Envelope {env.GetLogString()} is disabled. No funding will occur.");
|
|
}
|
|
|
|
if (env.FundingMethods == null || env.FundingMethods.Any() == false)
|
|
{
|
|
_logger.LogInformation($"Envelope {env.GetLogString()} couldn't be funded. No funding methods found.");
|
|
return;
|
|
}
|
|
|
|
if (env.IsPersistant == false)
|
|
{
|
|
this.ZeroOutEnvelopeFunds(env);
|
|
}
|
|
|
|
DateTime firstOfThisMonth = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, 1);
|
|
|
|
decimal amount = this.CalculateFundingAmount(envelopeId);
|
|
|
|
TransactionCreate transaction = new TransactionCreate {
|
|
Description = $"Funding Envelope",
|
|
Date = firstOfThisMonth.ToUniversalTime(),
|
|
CreditEnvelope = envelopeId,
|
|
IsEnvelopeFundingTransaction = true,
|
|
Amount = amount,
|
|
CurrencyType = 1,// CHANGE TO DYNAMIC
|
|
IsPending = false,
|
|
Notes = "Automated Transaction. Envelope Funding Event."
|
|
};
|
|
|
|
_transactionService.Create(transaction);
|
|
}
|
|
|
|
public void ZeroOutEnvelopeFunds(int envelopeId)
|
|
{
|
|
ZeroOutEnvelopeFunds(getEnvelope(envelopeId));
|
|
}
|
|
|
|
public void ZeroOutEnvelopeFunds(Envelope envelope)
|
|
{
|
|
if (envelope.Balance == 0)
|
|
{
|
|
_logger.LogInformation($"Envelope {envelope.GetLogString()} is already at zero.");
|
|
return;
|
|
}
|
|
|
|
DateTime lastOfLastMonth = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, 1).AddDays(-1);
|
|
|
|
TransactionCreate transaction = new TransactionCreate {
|
|
Description = $"Envelope Balance Reset",
|
|
Date = lastOfLastMonth.ToUniversalTime(),
|
|
IsEnvelopeFundingTransaction = true,
|
|
Amount = envelope.Balance,
|
|
CurrencyType = 1,// CHANGE TO DYNAMIC
|
|
IsPending = false,
|
|
Notes = "Automated Transaction. Envelope Clearing Event."
|
|
};
|
|
|
|
if (envelope.Balance < 0)
|
|
transaction.CreditEnvelope = envelope.Id;
|
|
else
|
|
transaction.DebitEnvelope = envelope.Id;
|
|
|
|
_transactionService.Create(transaction);
|
|
_logger.LogInformation($"Envelope {envelope.GetLogString()} set to zero.");
|
|
}
|
|
|
|
private decimal CalculateFundingAmount(int envelopeId, DateTime? month = null)
|
|
{
|
|
DateTime dt = month.HasValue ? month.Value : DateTime.UtcNow;
|
|
|
|
Envelope envelope = _context.Envelopes
|
|
.Include(e => e.Account)
|
|
.Include(e => e.FundingMethods)
|
|
.FirstOrDefault(e => e.Id == envelopeId);
|
|
|
|
decimal virtualBalance = envelope.Account.VirtualBalance;
|
|
decimal fundingAmount = 0;
|
|
|
|
List<EnvelopeFundingMethod> fundingMethods = envelope.FundingMethods.OrderBy(f => f.Order).ToList();
|
|
|
|
foreach (EnvelopeFundingMethod method in fundingMethods)
|
|
{
|
|
decimal value = Convert.ToDecimal(method.Value);
|
|
decimal amount = 0;
|
|
|
|
switch (method.Mode)
|
|
{
|
|
case EnvelopeFundingMethodModes.PERCENTAGE:
|
|
amount = virtualBalance * value / 100m;
|
|
amount = amount < 0 ? 0 : amount;
|
|
break;
|
|
case EnvelopeFundingMethodModes.HISTORICAL_MEDIAN:
|
|
case EnvelopeFundingMethodModes.HISTORICAL_MEAN:
|
|
EnvelopeHistorical hist = this.GetEnvelopeHistorical(envelopeId, method.PeriodsToLookback);
|
|
|
|
if (hist.Debit == null || hist.Credit == null)
|
|
amount = value;
|
|
else if (method.Mode == EnvelopeFundingMethodModes.HISTORICAL_MEDIAN)
|
|
if (!hist.Debit.Median.HasValue || !hist.Credit.Median.HasValue)
|
|
amount = value;
|
|
else
|
|
amount = hist.Debit.Median.Value - hist.Credit.Median.Value;
|
|
else if (method.Mode == EnvelopeFundingMethodModes.HISTORICAL_MEAN)
|
|
if (!hist.Debit.Mean.HasValue || !hist.Credit.Mean.HasValue)
|
|
amount = value;
|
|
else
|
|
amount = hist.Debit.Mean.Value - hist.Credit.Mean.Value;
|
|
|
|
amount = amount > value ? amount : value;
|
|
break;
|
|
case EnvelopeFundingMethodModes.ABSOLUTE:
|
|
default:
|
|
amount = Convert.ToDecimal(method.Value);
|
|
break;
|
|
}
|
|
|
|
_logger.LogInformation($"FundMethod -> Order:{method.Order}, Mode:{method.Mode}, Value:{method.Value}, Amount:{amount}");
|
|
fundingAmount += amount;
|
|
}
|
|
|
|
_logger.LogInformation($"Calculated Fund Amount for envelope {envelopeId}: {fundingAmount}");
|
|
|
|
return fundingAmount;
|
|
}
|
|
|
|
public List<EnvelopeFundingMethodModes> GetEnvelopeFundingMethodModes()
|
|
{
|
|
return Enum.GetValues<EnvelopeFundingMethodModes>().ToList();
|
|
}
|
|
|
|
// helper methods
|
|
|
|
private Envelope getEnvelope(int id)
|
|
{
|
|
var envelope = _context.Envelopes
|
|
.Include(e => e.Account)
|
|
.Include(e => e.FundingMethods)
|
|
.FirstOrDefault(e => e.Id == id);
|
|
|
|
if (envelope == null) throw new KeyNotFoundException("Envelope not found");
|
|
return envelope;
|
|
}
|
|
} |