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;
}
}