diff --git a/AAIntegration.SimmonsBank.API/.env b/AAIntegration.SimmonsBank.API/.env
new file mode 100644
index 0000000..374562d
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/.env
@@ -0,0 +1,7 @@
+ActiveAllocator__Database__Host=database
+ActiveAllocator__Database__Port=5432
+
+
+POSTGRES_PASSWORD=nqA3UV3CliLLHpLL
+PGADMIN_DEFAULT_EMAIL=admin@admin.com
+PGADMIN_DEFAULT_PASSWORD=3254
diff --git a/AAIntegration.SimmonsBank.API/.gitignore b/AAIntegration.SimmonsBank.API/.gitignore
new file mode 100644
index 0000000..8f8b43b
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/.gitignore
@@ -0,0 +1,232 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+build/
+bld/
+bin/
+Bin/
+obj/
+Obj/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+/wwwroot/dist/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Microsoft Azure ApplicationInsights config file
+ApplicationInsights.config
+
+# Windows Store app package directory
+AppPackages/
+BundleArtifacts/
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+/node_modules
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+
+# FAKE - F# Make
+.fake/
diff --git a/AAIntegration.SimmonsBank.API/AAIntegration.SimmonsBank.API.csproj b/AAIntegration.SimmonsBank.API/AAIntegration.SimmonsBank.API.csproj
index ffe0227..6e5f1ae 100644
--- a/AAIntegration.SimmonsBank.API/AAIntegration.SimmonsBank.API.csproj
+++ b/AAIntegration.SimmonsBank.API/AAIntegration.SimmonsBank.API.csproj
@@ -3,20 +3,35 @@
net7.0
enable
+ AAIntegration.SimmonsBank.API
+ false
enable
+
+ ActiveAllocator-Integration-SimmonsBank
+ DefaultContainer
+ 1.0.0;latest
+ linux-x64
+
+
-
-
-
-
+
+
+
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
-
+
+
+
+
+
+
diff --git a/AAIntegration.SimmonsBank.API/Configs/ActiveAllocatorConfig.cs b/AAIntegration.SimmonsBank.API/Configs/ActiveAllocatorConfig.cs
new file mode 100644
index 0000000..47db1ec
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Configs/ActiveAllocatorConfig.cs
@@ -0,0 +1,8 @@
+namespace AAIntegration.SimmonsBank.API.Configs;
+
+public class ActiveAllocatorConfig
+{
+ DatabaseConfig Database { get; set; }
+ List AllowedHosts { get; set; }
+ string APIUrl { get; set; }
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Configs/AppSettings.cs b/AAIntegration.SimmonsBank.API/Configs/AppSettings.cs
index ad5138d..6b83fa6 100644
--- a/AAIntegration.SimmonsBank.API/Configs/AppSettings.cs
+++ b/AAIntegration.SimmonsBank.API/Configs/AppSettings.cs
@@ -1,4 +1,4 @@
-namespace AAIntegration.SimmonsBank.API.Configs;
+namespace AAIntegration.SimmonsBank.API.Config;
public class AppSettings
{
diff --git a/AAIntegration.SimmonsBank.API/Configs/DatabaseConfig.cs b/AAIntegration.SimmonsBank.API/Configs/DatabaseConfig.cs
index d44828d..5615352 100644
--- a/AAIntegration.SimmonsBank.API/Configs/DatabaseConfig.cs
+++ b/AAIntegration.SimmonsBank.API/Configs/DatabaseConfig.cs
@@ -10,6 +10,7 @@ public class DatabaseConfig
public string GetConnectionString()
{
+ //Server=localhost;Port=15432;Database=aadb;User Id=postgres;Password=nqA3UV3CliLLHpLL"
return $"Server={Host};Port={Port};Database={Name};User Id={User};Password={Password}";
}
}
diff --git a/AAIntegration.SimmonsBank.API/Configs/EnvelopeFundConfig.cs b/AAIntegration.SimmonsBank.API/Configs/EnvelopeFundConfig.cs
new file mode 100644
index 0000000..7fb0dd5
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Configs/EnvelopeFundConfig.cs
@@ -0,0 +1,6 @@
+namespace AAIntegration.SimmonsBank.API.Configs;
+
+public class EnvelopeFundConfig
+{
+ public int CheckIntervalInMinutes { get; set; }
+}
diff --git a/AAIntegration.SimmonsBank.API/Controllers/AccountController.cs b/AAIntegration.SimmonsBank.API/Controllers/AccountController.cs
new file mode 100644
index 0000000..ef9d6ba
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Controllers/AccountController.cs
@@ -0,0 +1,84 @@
+namespace AAIntegration.SimmonsBank.API.Controllers;
+
+using AutoMapper;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using AAIntegration.SimmonsBank.API.Models.Accounts;
+using AAIntegration.SimmonsBank.API.Services;
+using AAIntegration.SimmonsBank.API.Config;
+using System.Collections.Generic;
+using AAIntegration.SimmonsBank.API.Entities;
+using Microsoft.AspNetCore.Authorization;
+
+[Authorize]
+[ApiController]
+[Route("[controller]")]
+public class AccountsController : ControllerBase
+{
+ private IAccountService _accountService;
+ private IMapper _mapper;
+ private readonly AppSettings _appSettings;
+
+ public AccountsController(
+ IAccountService accountService,
+ IMapper mapper,
+ IOptions appSettings)
+ {
+ _accountService = accountService;
+ _mapper = mapper;
+ _appSettings = appSettings.Value;
+ }
+
+ [HttpGet]
+ public IActionResult GetAll()
+ {
+ List accountDtos = new List();
+
+ foreach (Account acc in _accountService.GetAll())
+ accountDtos.Add(_mapper.Map(acc));
+
+ return Ok(accountDtos);
+ }
+
+ [HttpGet("{id}")]
+ public IActionResult GetById(int id)
+ {
+ Account account = _accountService.GetById(id);
+ return Ok(_mapper.Map(account));
+ }
+
+ [HttpPost]
+ public IActionResult Create([FromBody]AccountCreateRequest model)
+ {
+ _accountService.Create(model);
+ return Ok(new { message = "account created" });
+ }
+
+ [HttpPut("{id}")]
+ public IActionResult Update(int id, [FromBody]AccountUpdateRequest model)
+ {
+ _accountService.Update(id, model);
+ return Ok(new { message = "account updated" });
+ }
+
+ [HttpDelete("{id}")]
+ public IActionResult Delete(int id)
+ {
+ _accountService.Delete(id);
+ return Ok(new { message = "account deleted" });
+ }
+
+ [HttpGet("RefreshBalance/{id}")]
+ public IActionResult RefreshBalance(int id)
+ {
+ Account account = _accountService.RefreshAccountBalance(id);
+ return Ok(_mapper.Map(account));
+ }
+
+ [HttpGet("Historical/{id}")]
+ public IActionResult Historical(int id, int lookBack)
+ {
+ AccountHistorical hist = _accountService.GetAccountHistorical(id, lookBack);
+ return Ok(_mapper.Map(hist));
+ }
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Controllers/ApiKeyController.cs b/AAIntegration.SimmonsBank.API/Controllers/ApiKeyController.cs
new file mode 100644
index 0000000..6134b9d
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Controllers/ApiKeyController.cs
@@ -0,0 +1,83 @@
+namespace AAIntegration.SimmonsBank.API.Controllers;
+
+using AutoMapper;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using AAIntegration.SimmonsBank.API.Models.Users;
+using AAIntegration.SimmonsBank.API.Services;
+using AAIntegration.SimmonsBank.API.Config;
+using System;
+using System.Security.Claims;
+using Microsoft.AspNetCore.Authorization;
+
+[Authorize]
+[ApiController]
+[Route("[controller]")]
+public class ApiKeyController : ControllerBase
+{
+ private IApiKeyService _apiKeyService;
+ private IMapper _mapper;
+ private readonly AppSettings _appSettings;
+ private readonly ILogger _logger;
+ private IUserService _userService;
+
+ public ApiKeyController(
+ IApiKeyService apiKeyService,
+ IMapper mapper,
+ IOptions appSettings,
+ ILogger logger,
+ IUserService userService)
+ {
+ _apiKeyService = apiKeyService;
+ _mapper = mapper;
+ _appSettings = appSettings.Value;
+ _logger = logger;
+ _userService = userService;
+ }
+
+ [HttpGet]
+ public IActionResult GetApiKey()
+ {
+ string apiKey = _userService.GetUserApiKey(this.GetCurrentUserId());
+ string[] apiKeys = { apiKey };
+
+ Console.WriteLine($"User API Key Got: " + apiKey);
+ return Ok(new { keys = apiKeys });
+ }
+
+ /*[HttpGet("{id}")]
+ public IActionResult GetById(int id)
+ {
+ var user = _apiKeyService.GetById(id);
+ return Ok(user);
+ }*/
+
+ [HttpGet("CreateNew")]
+ public IActionResult CreateNewApiKey()
+ {
+ string apiKey = _userService.CreateUserApiKey(this.GetCurrentUserId());
+ return Ok(new { message = "API Key created", key = apiKey });
+ }
+
+ [HttpDelete("{apiKey}")]
+ public IActionResult Delete(string apiKey)
+ {
+ _userService.InvalidateApiKey(apiKey);
+ return Ok(new { message = "API Key deleted" });
+ }
+
+ // Helpers
+
+ private int GetCurrentUserId()
+ {
+ string nameIdentifier = User.FindFirstValue(ClaimTypes.NameIdentifier);
+
+ if (nameIdentifier is null)
+ _logger.LogInformation($"Name Identifier: is null");
+
+ _logger.LogInformation($"Name Identifier: {nameIdentifier}");
+ Console.WriteLine($"User Id: " + nameIdentifier);
+
+ return Convert.ToInt32(nameIdentifier);
+ }
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Controllers/AppInfoController.cs b/AAIntegration.SimmonsBank.API/Controllers/AppInfoController.cs
new file mode 100644
index 0000000..47a431a
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Controllers/AppInfoController.cs
@@ -0,0 +1,32 @@
+namespace AAIntegration.SimmonsBank.API.Controllers;
+
+using AutoMapper;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using AAIntegration.SimmonsBank.API.Models.Users;
+using AAIntegration.SimmonsBank.API.Services;
+using AAIntegration.SimmonsBank.API.Config;
+using System;
+using System.Security.Claims;
+using Microsoft.AspNetCore.Authorization;
+
+[ApiController]
+[Route("[controller]")]
+public class AppInfoController : ControllerBase
+{
+ private IVersionService _versionService;
+
+ public AppInfoController(
+ IVersionService versionService)
+ {
+ _versionService = versionService;
+ }
+
+ [HttpGet("ApplicationVersion")]
+ public IActionResult GetApplicationVersion()
+ {
+ string appVersion = _versionService.GetVersion();
+
+ return Ok(new { version = appVersion });
+ }
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Controllers/AutoclassController.cs b/AAIntegration.SimmonsBank.API/Controllers/AutoclassController.cs
new file mode 100644
index 0000000..f3eaac5
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Controllers/AutoclassController.cs
@@ -0,0 +1,103 @@
+namespace AAIntegration.SimmonsBank.API.Controllers;
+
+using AutoMapper;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using AAIntegration.SimmonsBank.API.Models.Autoclass;
+using AAIntegration.SimmonsBank.API.Services;
+using AAIntegration.SimmonsBank.API.Config;
+using System.Collections.Generic;
+using AAIntegration.SimmonsBank.API.Entities;
+using Microsoft.AspNetCore.Authorization;
+
+[Authorize]
+[ApiController]
+[Route("[controller]")]
+public class AutoclassController : ControllerBase
+{
+ private IAutoclassService _autoclassService;
+ private IMapper _mapper;
+ private readonly AppSettings _appSettings;
+
+ public AutoclassController(
+ IAutoclassService autoclassService,
+ IMapper mapper,
+ IOptions appSettings)
+ {
+ _autoclassService = autoclassService;
+ _mapper = mapper;
+ _appSettings = appSettings.Value;
+ }
+
+ [HttpGet]
+ public IActionResult GetAll(int? account = null)
+ {
+ List autoclassDtos = new List();
+
+ foreach (AutoclassRule auto in _autoclassService.GetAll(account))
+ autoclassDtos.Add(_mapper.Map(auto));
+
+ return Ok(autoclassDtos);
+ }
+
+ [HttpGet("{id}")]
+ public IActionResult GetById(int id)
+ {
+ AutoclassRule auto = _autoclassService.GetById(id);
+ return Ok(_mapper.Map(auto));
+ }
+
+ [HttpPost]
+ public IActionResult Create([FromBody]AutoclassRuleCreateRequest model)
+ {
+ _autoclassService.Create(model);
+ return Ok(new { message = "AutoclassRule created" });
+ }
+
+ [HttpPut("{id}")]
+ public IActionResult Update(int id, [FromBody]AutoclassRuleUpdateRequest model)
+ {
+ _autoclassService.Update(id, model);
+ return Ok(new { message = "AutoclassRule updated" });
+ }
+
+ [HttpDelete("{id}")]
+ public IActionResult Delete(int id)
+ {
+ _autoclassService.Delete(id);
+ return Ok(new { message = "AutoclassRule deleted" });
+ }
+
+ /*[HttpGet("RefreshBalance/{id}")]
+ public IActionResult RefreshBalance(int id)
+ {
+ Account account = _accountService.RefreshAccountBalance(id);
+ return Ok(_mapper.Map(account));
+ }*/
+ // [HttpGet("RefreshBalance/{id}")]
+ [HttpGet("TypeOperatorInfo")]
+ public IActionResult GetAllTypeOperatorInfo()
+ {
+ return Ok(_autoclassService.GetAllTypeOperatorInfo());
+ }
+
+ [HttpGet("FieldInfo")]
+ public IActionResult GetAllFieldInfo()
+ {
+ 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)." });
+ }
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Controllers/CurrencyTypeController.cs b/AAIntegration.SimmonsBank.API/Controllers/CurrencyTypeController.cs
new file mode 100644
index 0000000..bb485e0
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Controllers/CurrencyTypeController.cs
@@ -0,0 +1,64 @@
+namespace AAIntegration.SimmonsBank.API.Controllers;
+
+using AutoMapper;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using AAIntegration.SimmonsBank.API.Models.CurrencyType;
+using AAIntegration.SimmonsBank.API.Services;
+using AAIntegration.SimmonsBank.API.Config;
+using Microsoft.AspNetCore.Authorization;
+
+[Authorize]
+[ApiController]
+[Route("[controller]")]
+public class CurrencyTypesController : ControllerBase
+{
+ private ICurrencyTypeService _currencyTypeService;
+ private IMapper _mapper;
+ private readonly AppSettings _appSettings;
+
+ public CurrencyTypesController(
+ ICurrencyTypeService currencyTypeService,
+ IMapper mapper,
+ IOptions appSettings)
+ {
+ _currencyTypeService = currencyTypeService;
+ _mapper = mapper;
+ _appSettings = appSettings.Value;
+ }
+
+ [HttpGet]
+ public IActionResult GetAll()
+ {
+ var currencyTypes = _currencyTypeService.GetAll();
+ return Ok(currencyTypes);
+ }
+
+ [HttpGet("{id}")]
+ public IActionResult GetById(int id)
+ {
+ var currencyType = _currencyTypeService.GetById(id);
+ return Ok(currencyType);
+ }
+
+ [HttpPost]
+ public IActionResult Create([FromBody]CurrencyTypeCreateRequest model)
+ {
+ _currencyTypeService.Create(model);
+ return Ok(new { message = "currencyType created" });
+ }
+
+ /*[HttpPut("{id}")]
+ public IActionResult Update(int id, [FromBody]AccountUpdateRequest model)
+ {
+ _accountService.Update(id, model);
+ return Ok(new { message = "account updated" });
+ }*/
+
+ [HttpDelete("{id}")]
+ public IActionResult Delete(int id)
+ {
+ _currencyTypeService.Delete(id);
+ return Ok(new { message = "currencyType deleted" });
+ }
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Controllers/EnvelopeController.cs b/AAIntegration.SimmonsBank.API/Controllers/EnvelopeController.cs
new file mode 100644
index 0000000..980324f
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Controllers/EnvelopeController.cs
@@ -0,0 +1,108 @@
+namespace AAIntegration.SimmonsBank.API.Controllers;
+
+using AutoMapper;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using AAIntegration.SimmonsBank.API.Models.Envelopes;
+using AAIntegration.SimmonsBank.API.Services;
+using AAIntegration.SimmonsBank.API.Config;
+using System.Collections.Generic;
+using AAIntegration.SimmonsBank.API.Entities;
+using Microsoft.AspNetCore.Authorization;
+
+[Authorize]
+[ApiController]
+[Route("[controller]")]
+public class EnvelopesController : ControllerBase
+{
+ private IEnvelopeService _envelopeService;
+ private IMapper _mapper;
+ private readonly AppSettings _appSettings;
+
+ public EnvelopesController(
+ IEnvelopeService envelopeService,
+ IMapper mapper,
+ IOptions appSettings)
+ {
+ _envelopeService = envelopeService;
+ _mapper = mapper;
+ _appSettings = appSettings.Value;
+ }
+
+ [HttpGet]
+ public IActionResult GetAll(int? accountId = null)
+ {
+ List envelopeDtos = new List();
+
+ foreach (Envelope env in _envelopeService.GetAll(accountId))
+ {
+ List methodDtos = new List();
+
+ foreach (EnvelopeFundingMethod method in env.FundingMethods)
+ {
+ methodDtos.Add(_mapper.Map(method));
+ }
+
+ EnvelopeDTO dto = new EnvelopeDTO {
+ Id = env.Id,
+ Name = env.Name,
+ AccountId = env.Account == null ? 0 : env.Account.Id,
+ Enabled = env.Enabled,
+ Period = env.Period,
+ IsPersistant = env.IsPersistant,
+ Priority = env.Priority,
+ Balance = env.Balance,
+ InitialBalance = env.InitialBalance,
+ LastTriggeredOn = env.LastTriggeredOn,
+ FundingMethods = methodDtos
+ };
+
+ envelopeDtos.Add(dto);
+ }
+ //envelopeDtos.Add(_mapper.Map(env));
+
+ return Ok(envelopeDtos);
+ }
+
+ [HttpGet("{id}")]
+ public IActionResult GetById(int id)
+ {
+ Envelope envelope = _envelopeService.GetById(id);
+ return Ok(_mapper.Map(envelope));
+ }
+
+ [HttpPost]
+ public IActionResult Create([FromBody]EnvelopeCreateRequest model)
+ {
+ _envelopeService.Create(model);
+ return Ok(new { message = "envelope created" });
+ }
+
+ [HttpPut("{id}")]
+ public IActionResult Update(int id, [FromBody]EnvelopeUpdateRequest model)
+ {
+ _envelopeService.Update(id, model);
+ return Ok(new { message = "envelope updated" });
+ }
+
+ [HttpDelete("{id}")]
+ public IActionResult Delete(int id)
+ {
+ _envelopeService.Delete(id);
+ return Ok(new { message = "envelope deleted" });
+ }
+
+ [HttpGet("Historical/{id}")]
+ public IActionResult Historical(int id, int lookBack)
+ {
+ EnvelopeHistorical hist = _envelopeService.GetEnvelopeHistorical(id, lookBack);
+ return Ok(_mapper.Map(hist));
+ }
+
+ [HttpGet("FundingMethodModes")]
+ public IActionResult FundingMethodModes()
+ {
+ List modes = _envelopeService.GetEnvelopeFundingMethodModes();
+ return Ok(modes);
+ }
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Controllers/TransactionController.cs b/AAIntegration.SimmonsBank.API/Controllers/TransactionController.cs
new file mode 100644
index 0000000..3ae5c74
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Controllers/TransactionController.cs
@@ -0,0 +1,88 @@
+namespace AAIntegration.SimmonsBank.API.Controllers;
+
+using AutoMapper;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using AAIntegration.SimmonsBank.API.Models.Transactions;
+using AAIntegration.SimmonsBank.API.Services;
+using AAIntegration.SimmonsBank.API.Config;
+using System.Runtime.InteropServices;
+using AAIntegration.SimmonsBank.API.Entities;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Authorization;
+
+[Authorize]
+[ApiController]
+[Route("[controller]")]
+public class TransactionsController : ControllerBase
+{
+ private ITransactionService _transactionService;
+ private IMapper _mapper;
+ private readonly AppSettings _appSettings;
+
+ public TransactionsController(
+ ITransactionService transactionService,
+ IMapper mapper,
+ IOptions appSettings)
+ {
+ _transactionService = transactionService;
+ _mapper = mapper;
+ _appSettings = appSettings.Value;
+ }
+
+ [HttpGet]
+ public IActionResult GetAll(int? accountId = null)
+ {
+ List transactionDtos = new List();
+
+ foreach (Transaction tran in _transactionService.GetAll())
+ {
+ if (accountId.HasValue
+ && (tran.DebitAccount == null || tran.DebitAccount.Id != accountId)
+ && (tran.CreditAccount == null || tran.CreditAccount.Id != accountId))
+ continue;
+
+ transactionDtos.Add(_mapper.Map(tran));
+ }
+
+ // Sort by Date
+ transactionDtos.Sort((t1, t2) => t2.Date.CompareTo(t1.Date));
+
+ return Ok(transactionDtos);
+ }
+
+ [HttpGet("{id}")]
+ public IActionResult GetById(int id)
+ {
+ Transaction tran = _transactionService.GetById(id);
+ return Ok(_mapper.Map(tran));
+ }
+
+ [HttpPost("BulkAdd")]
+ public IActionResult BulkCreate([FromBody]List model)
+ {
+ List trans = _transactionService.BulkCreate(model).ToList();
+ return Ok(new { message = $"{trans.Count()} transaction(s) created." });
+ }
+
+ [HttpPost]
+ public IActionResult Create([FromBody]TransactionCreate model)
+ {
+ Transaction tran = _transactionService.Create(model);
+ return Ok(new { message = $"transaction '{tran.Description}' created with id '{tran.Id}'." });
+ }
+
+ [HttpPut("{id}")]
+ public IActionResult Update(int id, [FromBody]TransactionUpdateRequest model)
+ {
+ _transactionService.Update(id, model);
+ return Ok(new { message = $"transaction with id '{id}' updated" });
+ }
+
+ [HttpDelete("{id}")]
+ public IActionResult Delete(int id)
+ {
+ _transactionService.Delete(id);
+ return Ok(new { message = "transaction deleted" });
+ }
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Controllers/UserController.cs b/AAIntegration.SimmonsBank.API/Controllers/UserController.cs
new file mode 100644
index 0000000..b4060cc
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Controllers/UserController.cs
@@ -0,0 +1,78 @@
+namespace AAIntegration.SimmonsBank.API.Controllers;
+
+using AutoMapper;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+using AAIntegration.SimmonsBank.API.Models.Users;
+using AAIntegration.SimmonsBank.API.Services;
+using AAIntegration.SimmonsBank.API.Config;
+using System;
+using Microsoft.AspNetCore.Authorization;
+
+[ApiController]
+[Route("[controller]")]
+public class UsersController : ControllerBase
+{
+ private IUserService _userService;
+ private IMapper _mapper;
+ private readonly AppSettings _appSettings;
+ private readonly ILogger _logger;
+
+ public UsersController(
+ IUserService userService,
+ IMapper mapper,
+ IOptions appSettings,
+ ILogger logger)
+ {
+ _userService = userService;
+ _mapper = mapper;
+ _appSettings = appSettings.Value;
+ _logger = logger;
+ }
+
+ [HttpPost("authenticate")]
+ public IActionResult Authenticate(AuthenticateRequest model)
+ {
+ var response = _userService.Authenticate(model);
+ return Ok(response);
+ }
+
+ [HttpPost("register")]
+ public IActionResult Register(RegisterRequest model)
+ {
+ _userService.Register(model);
+ return Ok(new { message = "Registration successful" });
+ }
+
+ [Authorize]
+ [HttpGet]
+ public IActionResult GetAll()
+ {
+ var users = _userService.GetAll();
+ return Ok(users);
+ }
+
+ [Authorize]
+ [HttpGet("{id}")]
+ public IActionResult GetById(int id)
+ {
+ var user = _userService.GetById(id);
+ return Ok(user);
+ }
+
+ [Authorize]
+ [HttpPut("{id}")]
+ public IActionResult Update(int id, [FromBody]UserUpdateRequest model)
+ {
+ _userService.Update(id, model);
+ return Ok(new { message = "User updated" });
+ }
+
+ [Authorize]
+ [HttpDelete("{id}")]
+ public IActionResult Delete(int id)
+ {
+ _userService.Delete(id);
+ return Ok(new { message = "User deleted" });
+ }
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Entities/Account.cs b/AAIntegration.SimmonsBank.API/Entities/Account.cs
new file mode 100644
index 0000000..afd347b
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Entities/Account.cs
@@ -0,0 +1,20 @@
+using System.Text.Json.Serialization;
+
+namespace AAIntegration.SimmonsBank.API.Entities;
+
+public class Account
+{
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public DateTime LastActivity { get; set; }
+ public decimal VirtualBalance { get; set; }
+ public decimal Balance { get; set; }
+ public DateTime CreatedOn { get; set; }
+ public decimal InitialBalance { get; set; }
+ public int CurrencyId { get; set; }
+ public CurrencyType Currency { get; set; }
+ public string ExternalAccountNumber { get; set; }
+ public ICollection Envelopes { get; set; }
+ //public ICollection Transactions { get; set; }
+ //public Institution institution { get; set; }
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Entities/AccountHistorical.cs b/AAIntegration.SimmonsBank.API/Entities/AccountHistorical.cs
new file mode 100644
index 0000000..b1512c5
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Entities/AccountHistorical.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Text.Json.Serialization;
+
+namespace AAIntegration.SimmonsBank.API.Entities;
+
+public class AccountHistorical
+{
+ //public int Id { get; set; }
+ public Account Account { get; set; }
+ public int LookBack { get; set; }
+ public Statistic Income { get; set; } = null;
+ public Statistic Credit { get; set; } = null;
+ public Statistic Debit { get; set; } = null;
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Entities/AutoclassChange.cs b/AAIntegration.SimmonsBank.API/Entities/AutoclassChange.cs
new file mode 100644
index 0000000..0cffe19
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Entities/AutoclassChange.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Text.Json.Serialization;
+
+namespace AAIntegration.SimmonsBank.API.Entities;
+
+public class AutoclassChange
+{
+ public int Id { get; set; }
+ public AutoclassTransactionField Field { get; set; }
+ public string Value { get; set; }
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Entities/AutoclassExpression.cs b/AAIntegration.SimmonsBank.API/Entities/AutoclassExpression.cs
new file mode 100644
index 0000000..3eced47
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Entities/AutoclassExpression.cs
@@ -0,0 +1,232 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Text.Json.Serialization;
+
+namespace AAIntegration.SimmonsBank.API.Entities;
+
+public class AutoclassExpression
+{
+ public int Id { get; set; }
+ public AutoclassTransactionField TransactionField { get; set; }
+ public AutoclassCompareOperator CompareOperator { get; set; }
+ public string Value { get; set; }
+
+ public bool Evaluate(Transaction transaction)
+ {
+ switch (TransactionField)
+ {
+ case AutoclassTransactionField.DATE:
+ return EvaluateDateType(transaction.Date);
+
+ case AutoclassTransactionField.EXTERNAL_ID:
+ return EvaluateStringType(transaction.ExternalId);
+
+ case AutoclassTransactionField.DESCRIPTION:
+ return EvaluateStringType(transaction.Description);
+
+ case AutoclassTransactionField.AMOUNT:
+ return EvaluateDecimalType(transaction.Amount);
+
+ case AutoclassTransactionField.IS_PENDING:
+ return EvaluateBoolType(transaction.IsPending);
+
+ case AutoclassTransactionField.DEBIT_ACCOUNT:
+ return EvaluateAccountType(transaction.DebitAccount);
+
+ case AutoclassTransactionField.CREDIT_ACCOUNT:
+ return EvaluateAccountType(transaction.CreditAccount);
+
+ case AutoclassTransactionField.DEBIT_ENVELOPE:
+ return EvaluateEnvelopeType(transaction.DebitEnvelope);
+
+ case AutoclassTransactionField.CREDIT_ENVELOPE:
+ return EvaluateEnvelopeType(transaction.CreditEnvelope);
+
+ case AutoclassTransactionField.CURRENCY_TYPE:
+ return EvaluateCurrencyType(transaction.CurrencyType);
+
+ case AutoclassTransactionField.TAGS:
+ return EvaluateStringArrayType(transaction.Tags);
+
+ default:
+ throw new Exception($"AutoclassExpression::Evaluate(Transaction) couldn't recognize the field '{TransactionField}'.");
+ }
+ }
+
+ private bool EvaluateDateType(DateTime currentValue)
+ {
+ /*DateTime expValue = Convert.ToDateTime(Value);
+ var dt = DateTime.ParseExact(Value , "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);*/
+ DateTime expValue = DateTime.Parse(Value, null, System.Globalization.DateTimeStyles.RoundtripKind);
+
+ switch (CompareOperator)
+ {
+ case AutoclassCompareOperator.EQUAL:
+ return currentValue.Date == expValue.Date;
+ case AutoclassCompareOperator.NOT_EQUAL:
+ return currentValue.Date != expValue.Date;
+ case AutoclassCompareOperator.GREATER_THAN:
+ return currentValue.Date > expValue.Date;
+ case AutoclassCompareOperator.GREATER_THAN_OR_EQUAL:
+ return currentValue.Date >= expValue.Date;
+ case AutoclassCompareOperator.LESS_THAN:
+ return currentValue.Date < expValue.Date;
+ case AutoclassCompareOperator.LESS_THAN_OR_EQUAL:
+ return currentValue.Date <= expValue.Date;
+ default:
+ throw new Exception($"Invalid compare operator '{CompareOperator}' for type 'DateTime'");
+ }
+ }
+
+ private bool EvaluateStringType(string currentValue)
+ {
+ switch (CompareOperator)
+ {
+ case AutoclassCompareOperator.EQUAL:
+ return currentValue == Value;
+ case AutoclassCompareOperator.NOT_EQUAL:
+ return currentValue != Value;
+ case AutoclassCompareOperator.CONTAINS:
+ return currentValue.Contains(Value);
+ case AutoclassCompareOperator.CONTAINS_INSENSITIVE:
+ return currentValue.ToUpper().Contains(Value.ToUpper());
+ default:
+ throw new Exception($"Invalid compare operator '{CompareOperator}' for type 'string'");
+ }
+ }
+
+ private bool EvaluateDecimalType(decimal currentValue)
+ {
+ decimal expValue = Convert.ToDecimal(Value);
+
+ switch (CompareOperator)
+ {
+ case AutoclassCompareOperator.EQUAL:
+ return currentValue == expValue;
+ case AutoclassCompareOperator.NOT_EQUAL:
+ return currentValue != expValue;
+ case AutoclassCompareOperator.GREATER_THAN:
+ return currentValue > expValue;
+ case AutoclassCompareOperator.GREATER_THAN_OR_EQUAL:
+ return currentValue >= expValue;
+ case AutoclassCompareOperator.LESS_THAN:
+ return currentValue < expValue;
+ case AutoclassCompareOperator.LESS_THAN_OR_EQUAL:
+ return currentValue <= expValue;
+ default:
+ throw new Exception($"Invalid compare operator '{CompareOperator}' for type 'decimal'");
+ }
+ }
+
+ private bool EvaluateBoolType(bool currentValue)
+ {
+ bool expValue = Convert.ToBoolean(Value);
+
+ switch (CompareOperator)
+ {
+ case AutoclassCompareOperator.EQUAL:
+ return currentValue == expValue;
+ case AutoclassCompareOperator.NOT_EQUAL:
+ return currentValue != expValue;
+ default:
+ throw new Exception($"Invalid compare operator '{CompareOperator}' for type 'boolean'");
+ }
+ }
+
+ private bool EvaluateAccountType(Account currentValue)
+ {
+ int expValue = Convert.ToInt32(Value);
+
+ switch (CompareOperator)
+ {
+ case AutoclassCompareOperator.EQUAL:
+ if (currentValue == null)
+ return expValue == 0;
+ else
+ return currentValue.Id == expValue;
+ case AutoclassCompareOperator.NOT_EQUAL:
+ if (currentValue == null)
+ return expValue != 0;
+ else
+ return currentValue.Id != expValue;
+ default:
+ throw new Exception($"Invalid compare operator '{CompareOperator}' for type 'Account'");
+ }
+ }
+
+ //evaluateEnvelopeType
+
+ private bool EvaluateEnvelopeType(Envelope currentValue)
+ {
+ int expValue = Convert.ToInt32(Value);
+
+ switch (CompareOperator)
+ {
+ case AutoclassCompareOperator.EQUAL:
+ if (currentValue == null)
+ return expValue == 0;
+ else
+ return currentValue.Id == expValue;
+ case AutoclassCompareOperator.NOT_EQUAL:
+ if (currentValue == null)
+ return expValue != 0;
+ else
+ return currentValue.Id != expValue;
+ default:
+ throw new Exception($"Invalid compare operator '{CompareOperator}' for type 'Envelope'");
+ }
+ }
+
+ private bool EvaluateCurrencyType(CurrencyType currentValue)
+ {
+ int expValue = Convert.ToInt32(Value);
+
+ switch (CompareOperator)
+ {
+ case AutoclassCompareOperator.EQUAL:
+ if (currentValue == null)
+ return expValue == 0;
+ else
+ return currentValue.Id == expValue;
+ case AutoclassCompareOperator.NOT_EQUAL:
+ if (currentValue == null)
+ return expValue != 0;
+ else
+ return currentValue.Id != expValue;
+ default:
+ throw new Exception($"Invalid compare operator '{CompareOperator}' for type 'CurrencyType'");
+ }
+ }
+
+ private bool EvaluateStringArrayType(List currentValue)
+ {
+ switch (CompareOperator)
+ {
+ case AutoclassCompareOperator.CONTAINS:
+ if (currentValue == null)
+ return false;
+ else
+ return currentValue.Any(s => s == Value);
+ case AutoclassCompareOperator.CONTAINS_INSENSITIVE:
+ if (currentValue == null)
+ return false;
+ else
+ return currentValue.Any(s => s.ToUpper() == Value.ToUpper());
+ default:
+ throw new Exception($"Invalid compare operator '{CompareOperator}' for type 'string[]'");
+ }
+ }
+}
+
+public enum AutoclassCompareOperator
+{
+ EQUAL,
+ NOT_EQUAL,
+ GREATER_THAN,
+ LESS_THAN,
+ GREATER_THAN_OR_EQUAL,
+ LESS_THAN_OR_EQUAL,
+ CONTAINS,
+ CONTAINS_INSENSITIVE,
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Entities/AutoclassRule.cs b/AAIntegration.SimmonsBank.API/Entities/AutoclassRule.cs
new file mode 100644
index 0000000..4b2a4c9
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Entities/AutoclassRule.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using System.Data;
+using System.Text.Json.Serialization;
+
+namespace AAIntegration.SimmonsBank.API.Entities;
+
+public class AutoclassRule
+{
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public Account Account { get; set; }
+ public IEnumerable Expressions { get; set; }
+ public IEnumerable Changes { get; set; }
+ public bool Enabled { get; set; }
+}
+
+public enum AutoclassType
+{
+ STRING,
+ BOOLEAN,
+ DECIMAL,
+ DATETIME,
+ ACCOUNT,
+ ENVELOPE,
+ CURRENCYTYPE,
+ STRING_ARRAY,
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Entities/CurrencyType.cs b/AAIntegration.SimmonsBank.API/Entities/CurrencyType.cs
new file mode 100644
index 0000000..64c7156
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Entities/CurrencyType.cs
@@ -0,0 +1,12 @@
+using System.Data;
+using System.Text.Json.Serialization;
+
+namespace AAIntegration.SimmonsBank.API.Entities;
+
+public class CurrencyType
+{
+ public int Id { get; set; }
+ public string Code { get; set; } // USD
+ public string Symbol { get; set; } // $
+ public int DecimalPlaces { get; set; } // 2
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Entities/Envelope.cs b/AAIntegration.SimmonsBank.API/Entities/Envelope.cs
new file mode 100644
index 0000000..a96beaf
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Entities/Envelope.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.Data;
+using System.Text.Json.Serialization;
+
+namespace AAIntegration.SimmonsBank.API.Entities;
+
+public class Envelope
+{
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public Account Account { get; set; }
+ public bool Enabled { get; set; }
+ public TimeSpan Period { get; set; }
+ public bool IsPersistant { get; set; }
+ public int Priority { get; set; }
+ public decimal Balance { get; set; }
+ public decimal InitialBalance { get; set; }
+ public DateTime? LastTriggeredOn { get; set; }
+ public IEnumerable FundingMethods { get; set; }
+
+ public string GetLogString()
+ {
+ return $"'{Name}' (id={Id})";
+ }
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Entities/EnvelopeFundingMethod.cs b/AAIntegration.SimmonsBank.API/Entities/EnvelopeFundingMethod.cs
new file mode 100644
index 0000000..a7be869
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Entities/EnvelopeFundingMethod.cs
@@ -0,0 +1,22 @@
+using System.Data;
+using System.Text.Json.Serialization;
+
+namespace AAIntegration.SimmonsBank.API.Entities;
+
+public class EnvelopeFundingMethod
+{
+ public int Id { get; set; }
+ public Envelope Envelope { get; set; }
+ public int Order { get; set; }
+ public EnvelopeFundingMethodModes Mode { get; set; }
+ public string Value { get; set; }
+ public int PeriodsToLookback { get; set; } = 0;
+}
+
+public enum EnvelopeFundingMethodModes
+{
+ ABSOLUTE,
+ PERCENTAGE,
+ HISTORICAL_MEDIAN,
+ HISTORICAL_MEAN,
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Entities/EnvelopeHistorical.cs b/AAIntegration.SimmonsBank.API/Entities/EnvelopeHistorical.cs
new file mode 100644
index 0000000..2ffd4ca
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Entities/EnvelopeHistorical.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Text.Json.Serialization;
+
+namespace AAIntegration.SimmonsBank.API.Entities;
+
+public class EnvelopeHistorical
+{
+ //public int Id { get; set; }
+ public Envelope Envelope { get; set; }
+ public int LookBack { get; set; }
+ public decimal? EnvelopeFunding { get; set; } = null;
+ public Statistic Credit { get; set; } = null;
+ public Statistic Debit { get; set; } = null;
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Entities/Operation.cs b/AAIntegration.SimmonsBank.API/Entities/Operation.cs
new file mode 100644
index 0000000..92c485c
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Entities/Operation.cs
@@ -0,0 +1,19 @@
+using System.Data;
+using System.Text.Json.Serialization;
+using AAIntegration.SimmonsBank.API.Enums;
+
+namespace AAIntegration.SimmonsBank.API.Entities;
+
+public class Operation
+{
+ public int Id { get; set; }
+ public int Order { get; set; }
+ public bool Enabled { get; set; }
+ public bool Negative { get; set; }
+ public OperationMode Mode { get; set; }
+ public TimeSpan HistoricPeriod { get; set; }
+ public int HistoricLookbackDepth { get; set; }
+ public HistoricStatistic HistoricStatistic { get; set; }
+ public decimal AbsoluteValue { get; set; }
+ public decimal Percentage { get; set; }
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Entities/Role.cs b/AAIntegration.SimmonsBank.API/Entities/Role.cs
new file mode 100644
index 0000000..ebbc879
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Entities/Role.cs
@@ -0,0 +1,7 @@
+namespace AAIntegration.SimmonsBank.API.Entities;
+
+public enum Role
+{
+ Admin,
+ User
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Entities/Statistic.cs b/AAIntegration.SimmonsBank.API/Entities/Statistic.cs
new file mode 100644
index 0000000..4d68ed7
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Entities/Statistic.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Text.Json.Serialization;
+
+namespace AAIntegration.SimmonsBank.API.Entities;
+
+public class Statistic
+{
+ //public int Id { get; set; }
+ public Dictionary? Values { get; set; } = null;
+ public decimal? Median { get; set; } = null;
+ public decimal? Mean { get; set; } = null;
+ public decimal? High { get; set; } = null;
+ public decimal? Low { get; set; } = null;
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Entities/Transaction.cs b/AAIntegration.SimmonsBank.API/Entities/Transaction.cs
new file mode 100644
index 0000000..1bdc18c
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Entities/Transaction.cs
@@ -0,0 +1,72 @@
+using System.Collections.Generic;
+using AAIntegration.SimmonsBank.API.Services;
+
+namespace AAIntegration.SimmonsBank.API.Entities;
+
+public class Transaction
+{
+ public int Id { get; set; }
+ public DateTime Date { get; set; }
+ public DateTime CreatedOn { get; set; }
+ public DateTime UpdatedOn { get; set; }
+ public string ExternalId { get; set; }
+ public string Description { get; set; }
+ public Account? DebitAccount { get; set; }
+ public Account? CreditAccount { get; set; }
+ public Envelope? DebitEnvelope { get; set; }
+ public Envelope? CreditEnvelope { get; set; }
+ public bool IsEnvelopeFundingTransaction { get; set; }
+ public decimal Amount { get; set; }
+ public CurrencyType CurrencyType { get; set; }
+ public string Notes { get; set; }
+ public bool IsPending { get; set; }
+ public List? 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() { 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,
+ EXTERNAL_ID,
+ DESCRIPTION,
+ DEBIT_ACCOUNT,
+ CREDIT_ACCOUNT,
+ DEBIT_ENVELOPE,
+ CREDIT_ENVELOPE,
+ AMOUNT,
+ CURRENCY_TYPE,
+ IS_PENDING,
+ TAGS
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Entities/User.cs b/AAIntegration.SimmonsBank.API/Entities/User.cs
index 3cfeb9a..9b401d2 100644
--- a/AAIntegration.SimmonsBank.API/Entities/User.cs
+++ b/AAIntegration.SimmonsBank.API/Entities/User.cs
@@ -1,10 +1,20 @@
+using System.Text.Json.Serialization;
+
namespace AAIntegration.SimmonsBank.API.Entities;
public class User
{
public int Id { get; set; }
- public string ApiKey { get; set; }
- public string SimmonsBankUsername { get; set; }
- public string SimmonsBankPassword { get; set; }
- public string MFAKey { get; set; }
+ public string Username { get; set; }
+ public string FirstName { get; set; }
+ public string LastName { get; set; }
+ public string Email { get; set; }
+ public Role Role { get; set; }
+ public ICollection Accounts { get; set; }
+
+ [JsonIgnore]
+ public string PasswordHash { get; set; }
+
+ [JsonIgnore]
+ public string? ApiKey { get; set; }
}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Enums/HistoricStatistic.cs b/AAIntegration.SimmonsBank.API/Enums/HistoricStatistic.cs
new file mode 100644
index 0000000..5d9f1a2
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Enums/HistoricStatistic.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+
+namespace AAIntegration.SimmonsBank.API.Enums;
+
+public enum HistoricStatistic
+{
+ Mean,
+ Median,
+ Mode,
+ Minimum,
+ Maximum,
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Enums/OperationMode.cs b/AAIntegration.SimmonsBank.API/Enums/OperationMode.cs
new file mode 100644
index 0000000..485fa4a
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Enums/OperationMode.cs
@@ -0,0 +1,10 @@
+using System.Text.Json.Serialization;
+
+namespace AAIntegration.SimmonsBank.API.Enums;
+
+public enum OperationMode
+{
+ Historic,
+ Absolute,
+ Percentage,
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Handlers/ApiKeyAuthenticationHandler.cs b/AAIntegration.SimmonsBank.API/Handlers/ApiKeyAuthenticationHandler.cs
index 05fa8a9..09a7c7e 100644
--- a/AAIntegration.SimmonsBank.API/Handlers/ApiKeyAuthenticationHandler.cs
+++ b/AAIntegration.SimmonsBank.API/Handlers/ApiKeyAuthenticationHandler.cs
@@ -2,10 +2,10 @@ namespace AAIntegration.SimmonsBank.API.Handlers;
using System.Security.Claims;
using System.Text.Encodings.Web;
+using AAIntegration.SimmonsBank.API.Configs;
+using AAIntegration.SimmonsBank.API.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
-using AAIntegration.SimmonsBank.API.Services;
-using AAIntegration.SimmonsBank.API.Configs;
public class ApiKeyAuthenticationHandler : AuthenticationHandler
{
diff --git a/AAIntegration.SimmonsBank.API/Helpers/AppException.cs b/AAIntegration.SimmonsBank.API/Helpers/AppException.cs
new file mode 100644
index 0000000..661abfd
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Helpers/AppException.cs
@@ -0,0 +1,17 @@
+namespace AAIntegration.SimmonsBank.API.Config;
+
+using System.Globalization;
+
+// custom exception class for throwing application specific exceptions (e.g. for validation)
+// that can be caught and handled within the application
+public class AppException : Exception
+{
+ public AppException() : base() {}
+
+ public AppException(string message) : base(message) { }
+
+ public AppException(string message, params object[] args)
+ : base(String.Format(CultureInfo.CurrentCulture, message, args))
+ {
+ }
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Helpers/AutoMapperProfile.cs b/AAIntegration.SimmonsBank.API/Helpers/AutoMapperProfile.cs
new file mode 100644
index 0000000..a2bec92
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Helpers/AutoMapperProfile.cs
@@ -0,0 +1,211 @@
+namespace AAIntegration.SimmonsBank.API.Config;
+
+using AutoMapper;
+using AAIntegration.SimmonsBank.API.Entities;
+using AAIntegration.SimmonsBank.API.Models.Users;
+using AAIntegration.SimmonsBank.API.Models.Accounts;
+using AAIntegration.SimmonsBank.API.Models.Envelopes;
+using AAIntegration.SimmonsBank.API.Models.CurrencyType;
+using AAIntegration.SimmonsBank.API.Services;
+using System.Runtime.Serialization;
+using AAIntegration.SimmonsBank.API.Models.Transactions;
+using AAIntegration.SimmonsBank.API.Models.Autoclass;
+
+public class AutoMapperProfile : Profile
+{
+
+ public AutoMapperProfile()
+ {
+ // User -> AuthenticateResponse
+ CreateMap();
+
+ // RegisterRequest -> User
+ CreateMap();
+
+ // UserUpdateRequest -> User
+ CreateMap()
+ .ForAllMembers(x => x.Condition(
+ (src, dest, prop) =>
+ {
+ // ignore both null & empty string properties
+ if (prop == null) return false;
+ if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
+
+ // ignore null role
+ if (x.DestinationMember.Name == "Role" && src.Role == null) return false;
+
+ // ignore null password
+ if (x.DestinationMember.Name == "Password" && src.Password == null) return false;
+
+ return true;
+ }
+ ));
+
+ // AccountUpdateRequest -> Account
+ CreateMap();
+
+ // AccountCreateRequest -> Account
+ CreateMap();
+ /*.ForMember(
+ dest => dest.OwnerId,
+ opt => opt.MapFrom(src => src.Owner)
+ );
+ /*.ForAllMembers(x => x.Condition(
+ (src, dest, prop) =>
+ {
+ // ignore both null & empty string properties
+ if (prop == null) return false;
+ if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
+
+ return true;
+ }
+ ))*/
+
+ // Account -> AccountGet
+ CreateMap()
+ .ForAllMembers(x => x.Condition(
+ (src, dest, prop) =>
+ {
+ // ignore both null & empty string properties
+ if (prop == null) return false;
+ if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
+
+ return true;
+ }
+ ));
+
+ // AccountHistorical -> AccountHistoricalDTO
+ CreateMap();
+
+ // EnvelopeHistorical -> EnvelopeHistoricalDTO
+ CreateMap();
+
+ // Envelope -> EnvelopeDTO
+ CreateMap()
+ .ForAllMembers(x => x.Condition(
+ (src, dest, prop) =>
+ {
+ // ignore both null & empty string properties
+ if (prop == null) return false;
+ if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
+
+ return true;
+ }
+ ));
+
+ // EnvelopeFundingMethod -> EnvelopeFundingMethodDTO
+ CreateMap()
+ .ForAllMembers(x => x.Condition(
+ (src, dest, prop) =>
+ {
+ // ignore both null & empty string properties
+ if (prop == null) return false;
+ if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
+
+ return true;
+ }
+ ));
+
+ // Transaction -> TransactionDto
+ CreateMap()
+ .ForMember(dest => dest.DebitAccountId, opt => opt.MapFrom(src => src.DebitAccount.Id))
+ .ForMember(dest => dest.CreditAccountId, opt => opt.MapFrom(src => src.CreditAccount.Id))
+ .ForMember(dest => dest.DebitEnvelopeId, opt => opt.MapFrom(src => src.DebitEnvelope.Id))
+ .ForMember(dest => dest.CreditEnvelopeId, opt => opt.MapFrom(src => src.CreditEnvelope.Id))
+ .ForAllMembers(x => x.Condition(
+ (src, dest, prop) =>
+ {
+ // ignore both null & empty string properties
+ if (prop == null) return false;
+ if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
+
+ return true;
+ }
+ ));
+
+ // CurrencyTypeCreateRequest -> CurrencyType
+ CreateMap()
+ .ForAllMembers(x => x.Condition(
+ (src, dest, prop) =>
+ {
+ // ignore both null & empty string properties
+ if (prop == null) return false;
+ if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
+
+ return true;
+ }
+ ));
+
+ // AutoclassExpression <-> AutoclassExpressionCreateRequest
+ CreateMap();
+ CreateMap()
+ .ForAllMembers(x => x.Condition(
+ (src, dest, prop) =>
+ {
+ // ignore both null & empty string properties
+ if (prop == null) return false;
+ if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
+
+ return true;
+ }
+ ));
+
+ CreateMap();
+ CreateMap();
+
+ // AutoclassChange <-> AutoclassChangeCreateRequest
+ CreateMap();
+ CreateMap()
+ .ForAllMembers(x => x.Condition(
+ (src, dest, prop) =>
+ {
+ // ignore both null & empty string properties
+ if (prop == null) return false;
+ if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
+
+ return true;
+ }
+ ));
+
+ // AutoclassRule <-> AutoclassRuleCreateRequest
+ CreateMap()
+ //.ForMember(d => d.Account, opt => opt.MapFrom(src => src.Account.Id))
+ .ReverseMap();/*
+ CreateMap()
+ .ForMember(d => d.Account, opt => opt.MapFrom(src => src))
+ .ForAllMembers(x => x.Condition(
+ (src, dest, prop) =>
+ {
+ if (dest.GetType() == typeof(Account))
+ {
+
+ }
+
+ // ignore both null & empty string properties
+ if (prop == null) return false;
+ if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
+
+ return true;
+ }
+ ));
+ CreateMap()
+ .ForMember(d => d.Id, opt => opt.MapFrom(src => src.Account))
+ .ForAllMembers(x => x.Condition(
+ (src, dest, prop) =>
+ {
+ src.UseDestinationValue();
+
+ // ignore both null & empty string properties
+ if (prop == null) return false;
+ if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;
+
+ return true;
+ }
+ ));*/
+
+ // AutoclassRule -> AutoclassRuleDTO
+ CreateMap()
+ .ForMember(dest => dest.AccountId, opt => opt.MapFrom(src => src.Account.Id));
+
+ }
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Helpers/DataContext.cs b/AAIntegration.SimmonsBank.API/Helpers/DataContext.cs
index 06248dd..3930376 100644
--- a/AAIntegration.SimmonsBank.API/Helpers/DataContext.cs
+++ b/AAIntegration.SimmonsBank.API/Helpers/DataContext.cs
@@ -1,10 +1,11 @@
-namespace AAIntegration.SimmonsBank.API.Helpers;
+namespace AAIntegration.SimmonsBank.API.Config;
using Microsoft.EntityFrameworkCore;
using AAIntegration.SimmonsBank.API.Entities;
using System.Diagnostics;
using System;
using System.Collections.Generic;
+using Internal;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Diagnostics.CodeAnalysis;
@@ -29,5 +30,428 @@ public class DataContext : DbContext
options.UseInMemoryDatabase("TestDb");
}*/
+ public void AddAmountToAccount(int accountId, decimal amount)
+ {
+ Account account = this.Accounts.Find(accountId);
+ if (account != null)
+ {
+ _logger.LogInformation($"\t Adding {amount} to account '{account.Name}' (id={accountId}).");
+ account.Balance += amount;
+ this.Accounts.Update(account);
+ this.SaveChanges();
+ }
+ }
+
+ public void AddAmountToEnvelope(int envelopeId, decimal amount)
+ {
+ Envelope envelope = this.Envelopes
+ .Include(e => e.Account)
+ .FirstOrDefault(e => e.Id == envelopeId);
+
+ if (envelope != null)
+ {
+ _logger.LogInformation($"\t Adding {amount} to envelope '{envelope.Name}' (id={envelopeId}).");
+ envelope.Balance += amount;
+ this.Envelopes.Update(envelope);
+ this.SaveChanges();
+
+ if (envelope.Account != null)
+ this.RecalculateAccountVirtualBalance(envelope.Account.Id);
+ }
+ }
+
+ public void RecalculateAccountBalance(int id)
+ {
+ Account account = this.Accounts
+ .FirstOrDefault(a => a.Id == id);
+
+ decimal amount = account.InitialBalance;
+
+ // Grab relevant transactions
+ List transactions = this.Transactions
+ .Include(t => t.DebitAccount)
+ .Include(t => t.CreditAccount)
+ .Where(t =>
+ (t.DebitAccount != null && t.DebitAccount.Id == id)
+ || (t.CreditAccount != null && t.CreditAccount.Id == id))
+ .ToList();
+
+ // Iterate and sum values
+ foreach (Transaction t in transactions)
+ {
+ if (t.DebitAccount?.Id == id) {
+ amount -= t.Amount;
+ } else if (t.CreditAccount?.Id == id) {
+ amount += t.Amount;
+ }
+ }
+
+ account.Balance = amount;
+ _logger.LogInformation($"Recalculating Account Balance for '{account.Name}' as '{amount}'.");
+
+ this.Accounts.Update(account);
+ this.SaveChanges();
+ }
+
+ public void RecalculateAccountVirtualBalance(Envelope env, int lookBack = 12)
+ {
+ Envelope envelope = this.Envelopes
+ .Include(e => e.Account)
+ .FirstOrDefault(e => e.Id == env.Id);
+
+ RecalculateAccountVirtualBalance(envelope.Account.Id, lookBack);
+ }
+
+ public void RecalculateAccountVirtualBalance(int accountId, int lookBack = 12)
+ {
+ AccountHistorical hist = this.GetAccountHistorical(accountId, lookBack);
+
+ Account account = this.Accounts.Find(accountId);
+
+ _logger.LogInformation($"Recalculating virtual balance for account '{account.Name}' (id={accountId}).");
+ decimal virtualBalance = account.Balance;
+
+ // Make this configurable?
+ // // 2nd half of CHECK BELOW IS TEMPORARY
+ if (hist.Income != null && hist.Income.Median.HasValue)
+ {
+ /*if (hist.Income.Median.Value == 0 && hist.Income.Mean.HasValue)
+ hist.Income.Median = hist.Income.Mean.Value;*/
+
+ _logger.LogInformation($"Using Median Income Value of {hist.Income.Median.Value}...");
+ virtualBalance = hist.Income.Median.Value;
+ }
+
+ // Determine Virtual Balance
+ List envelopes = this.Envelopes
+ .Include(e => e.Account)
+ .Where(e => e.Account.Id == accountId)
+ .ToList();
+
+ foreach (Envelope e in envelopes)
+ {
+ decimal? envelopeFunding = GetEnvelopeFunding(e.Id);
+ if (envelopeFunding.HasValue)
+ {
+ _logger.LogInformation($"Envelope {e.Name} had funding of {envelopeFunding.Value}.");
+ virtualBalance -= envelopeFunding.Value;
+ }
+ }
+
+ _logger.LogInformation($"VirtualBalance changed from {account.VirtualBalance} to {virtualBalance}.");
+ account.VirtualBalance = virtualBalance;
+ this.SaveChanges();
+ }
+
+ public AccountHistorical GetAccountHistorical(int id, int lookBack)
+ {
+ DateTime now = DateTime.UtcNow;
+ AccountHistorical hist = new AccountHistorical() {
+ Account = this.Accounts.Find(id),
+ LookBack = lookBack
+ };
+
+ DateTime firstOfThisMonth = new DateTime(now.Year, now.Month, 1);
+ DateTime lastOfLastMonth = firstOfThisMonth.AddDays(-1);
+ DateTime lookBackDate = firstOfThisMonth.AddMonths(-lookBack);
+
+ List transactions = this.Transactions
+ .Include(t => t.CreditAccount)
+ .Include(t => t.DebitAccount)
+ .Where(t => (t.CreditAccount != null && t.CreditAccount.Id == id) || (t.DebitAccount != null && t.DebitAccount.Id == id))
+ .Where(t => t.Date >= lookBackDate.ToUniversalTime() && t.Date <= lastOfLastMonth.ToUniversalTime())
+ .ToList();
+
+ // Sum variables
+ decimal sumDebit = 0;
+ decimal sumCredit = 0;
+ decimal sumIncome = 0;
+
+ // High variables
+ decimal highDebit = 0;
+ decimal highCredit = 0;
+ decimal highIncome = 0;
+
+ // Low variables
+ decimal lowDebit = 100000000;
+ decimal lowCredit = 100000000;
+ decimal lowIncome = 100000000;
+
+ // Values variables
+ Dictionary valuesDebit = new Dictionary();
+ Dictionary valuesCredit = new Dictionary();
+ Dictionary valuesIncome = new Dictionary();
+
+ bool monthsHaveData = false;
+ int monthsWithData = 0;
+
+ for (int i = 0; i < lookBack; i++)
+ {
+ DateTime currentLookBackMonth = lastOfLastMonth.AddMonths(-i);
+ List monthTransactions = this.GetTransactionsFromMonth(currentLookBackMonth, transactions);
+
+ decimal monthlySumDebit = 0;
+ decimal monthlySumCredit = 0;
+ decimal monthlySumIncome = 0;
+
+ foreach (Transaction tran in monthTransactions)
+ {
+ monthsHaveData = true;
+
+ if (tran.CreditAccount != null && tran.CreditAccount.Id == id)
+ {
+ monthlySumCredit += tran.Amount;
+
+ if (tran.Tags != null && tran.Tags.Any(t => t.ToUpper() == "INCOME"))
+ monthlySumIncome += tran.Amount;
+ }
+ else
+ monthlySumDebit += tran.Amount;
+
+ if (tran.Amount != 0)
+ monthsWithData = i + 1;
+ }
+
+ if (monthsWithData == i + 1)
+ {
+ valuesDebit.Add(currentLookBackMonth, monthlySumDebit);
+ valuesCredit.Add(currentLookBackMonth, monthlySumCredit);
+ valuesIncome.Add(currentLookBackMonth, monthlySumIncome);
+ }
+
+ sumDebit += monthlySumDebit;
+ sumCredit += monthlySumCredit;
+ sumIncome += monthlySumIncome;
+
+ highDebit = i == 0 || monthlySumDebit > highDebit ? monthlySumDebit : highDebit;
+ highCredit = i == 0 || monthlySumCredit > highCredit ? monthlySumCredit : highCredit;
+ highIncome = i == 0 || monthlySumIncome > highIncome ? monthlySumIncome : highIncome;
+
+ lowDebit = i == 0 || monthlySumDebit < lowDebit ? monthlySumDebit : lowDebit;
+ lowCredit = i == 0 || monthlySumCredit < lowCredit ? monthlySumCredit : lowCredit;
+ lowIncome = i == 0 || monthlySumIncome < lowIncome ? monthlySumIncome : lowIncome;
+ }
+
+ if (monthsHaveData == false)
+ return hist;
+
+
+ hist.Debit = new Statistic() {
+ Median = HistoricalMedian(valuesDebit),
+ Mean = sumDebit / (decimal)monthsWithData,
+ High = highDebit,
+ Low = lowDebit,
+ Values = valuesDebit,
+ };
+
+ hist.Credit = new Statistic() {
+ Median = HistoricalMedian(valuesCredit),
+ Mean = sumCredit / (decimal)monthsWithData,
+ High = highCredit,
+ Low = lowCredit,
+ Values = valuesCredit,
+ };
+
+ hist.Income = new Statistic() {
+ Median = HistoricalMedian(valuesIncome),
+ Mean = sumIncome / (decimal)monthsWithData,
+ High = highIncome,
+ Low = lowIncome,
+ Values = valuesIncome,
+ };
+
+ return hist;
+ }
+
+ /*private decimal? SetZeroToNull(decimal value)
+ {
+ return value == 0 ? null : (decimal?)value;
+ }
+
+ private decimal? SetZeroToNull(decimal? value)
+ {
+ return value.HasValue && value.Value == 0 ? null : value;
+ }*/
+
+ public EnvelopeHistorical GetEnvelopeHistorical(int id, int lookBack)
+ {
+ DateTime now = DateTime.UtcNow;
+ EnvelopeHistorical hist = new EnvelopeHistorical() {
+ Envelope = this.Envelopes.Find(id),
+ LookBack = lookBack
+ };
+
+ // EnvelopeFunding this month (period)
+ hist.EnvelopeFunding = this.GetEnvelopeFunding(id, now);
+
+ // Gather remaining statistics
+ DateTime firstOfThisMonth = new DateTime(now.Year, now.Month, 1);
+ DateTime lastOfLastMonth = firstOfThisMonth.AddDays(-1);
+ DateTime lookBackDate = firstOfThisMonth.AddMonths(-lookBack);
+
+ List transactions = this.Transactions
+ .Include(t => t.CreditEnvelope)
+ .Include(t => t.DebitEnvelope)
+ .Where(t => t.IsEnvelopeFundingTransaction == false)
+ .Where(t => (t.CreditEnvelope != null && t.CreditEnvelope.Id == id) || (t.DebitEnvelope != null && t.DebitEnvelope.Id == id))
+ .Where(t => t.Date >= lookBackDate.ToUniversalTime() && t.Date <= lastOfLastMonth.ToUniversalTime())
+ .ToList();
+
+ // Sum variables
+ decimal sumDebit = 0;
+ decimal sumCredit = 0;
+
+ // High variables
+ decimal highDebit = 0;
+ decimal highCredit = 0;
+
+ // Low variables
+ decimal lowDebit = 100000000;
+ decimal lowCredit = 100000000;
+
+ // Values variables
+ Dictionary valuesDebit = new Dictionary();
+ Dictionary valuesCredit = new Dictionary();
+
+ bool monthsHaveData = false;
+ int monthsWithData = 0;
+
+ for (int i = 0; i < lookBack; i++)
+ {
+ DateTime currentLookBackMonth = lastOfLastMonth.AddMonths(-i);
+ List monthTransactions = this.GetTransactionsFromMonth(currentLookBackMonth, transactions);
+
+ decimal monthlySumDebit = 0;
+ decimal monthlySumCredit = 0;
+ foreach (Transaction tran in monthTransactions)
+ {
+ monthsHaveData = true;
+
+ if (tran.CreditEnvelope != null && tran.CreditEnvelope.Id == id)
+ monthlySumCredit += tran.Amount;
+ else
+ monthlySumDebit += tran.Amount;
+
+ if (tran.Amount != 0)
+ monthsWithData = i + 1;
+ }
+
+ if (monthsWithData == i + 1)
+ {
+ valuesDebit.Add(currentLookBackMonth, monthlySumDebit);
+ valuesCredit.Add(currentLookBackMonth, monthlySumCredit);
+ }
+
+ sumDebit += monthlySumDebit;
+ sumCredit += monthlySumCredit;
+
+ highDebit = i == 0 || monthlySumDebit > highDebit ? monthlySumDebit : highDebit;
+ highCredit = i == 0 || monthlySumCredit > highCredit ? monthlySumCredit : highCredit;
+ lowDebit = i == 0 || monthlySumDebit < lowDebit ? monthlySumDebit : lowDebit;
+ lowCredit = i == 0 || monthlySumCredit < lowCredit ? monthlySumCredit : lowCredit;
+ }
+
+ if (monthsHaveData == false)
+ return hist;
+
+ hist.Credit = new Statistic() {
+ Median = HistoricalMedian(valuesCredit),
+ Mean = sumCredit / (decimal)monthsWithData,
+ High = highCredit,
+ Low = lowCredit,
+ Values = valuesCredit,
+ };
+
+ hist.Debit = new Statistic() {
+ Median = HistoricalMedian(valuesDebit),
+ Mean = sumDebit / (decimal)monthsWithData,
+ High = highDebit,
+ Low = lowDebit,
+ Values = valuesDebit,
+ };
+
+ return hist;
+ }
+
+ private List GetTransactionsFromMonth(DateTime? date = null, List transactions = null)
+ {
+ DateTime dt = date.HasValue ? date.Value : DateTime.UtcNow;
+
+ if (transactions == null)
+ return this.Transactions
+ .Where(t => t.Date.Year == dt.Year && t.Date.Month == dt.Month)
+ .ToList();
+
+ return transactions
+ .Where(t => t.Date.Year == dt.Year && t.Date.Month == dt.Month)
+ .ToList();
+ }
+
+ private decimal GetEnvelopeFunding(int id, DateTime? month = null)
+ {
+ DateTime dt = month.HasValue ? month.Value : DateTime.UtcNow;
+
+ Transaction envelopeFundTransaction = this.Transactions
+ .Include(t => t.CreditEnvelope)
+ .Where(t => t.IsEnvelopeFundingTransaction && t.CreditEnvelope != null && t.CreditEnvelope.Id == id)
+ .FirstOrDefault(t => t.Date.Year == dt.Year && t.Date.Month == dt.Month);
+
+ if (envelopeFundTransaction != null)
+ return envelopeFundTransaction.Amount;
+
+ return 0;
+ }
+
+ private decimal HistoricalMedian(Dictionary data)
+ {
+ int count = data.Count();
+
+ if (count % 2 == 0)
+ {
+ var sortedData = data.OrderBy(s => s.Value).ToArray();
+ return (sortedData[count / 2 - 1].Value + sortedData[count / 2].Value) / 2;
+ }
+ else
+ {
+ return data.OrderBy(s => s.Value).ElementAt(count / 2).Value;
+ }
+ }
+
+ public void RecalculateEnvelopeBalance(int id)
+ {
+ Envelope envelope = this.Envelopes.Find(id);
+ decimal amount = envelope.InitialBalance;
+
+ List transactions = this.Transactions
+ .Include(t => t.DebitEnvelope)
+ .Include(t => t.CreditEnvelope)
+ .Where(t =>
+ (t.DebitEnvelope != null && t.DebitEnvelope.Id == id)
+ || (t.CreditEnvelope != null && t.CreditEnvelope.Id == id))
+ .ToList();
+
+ foreach (Transaction t in transactions)
+ {
+ if (t.DebitEnvelope?.Id == id) {
+ amount -= t.Amount;
+ } else if (t.CreditEnvelope?.Id == id) {
+ amount += t.Amount;
+ }
+ }
+
+ envelope.Balance = amount;
+ this.Envelopes.Update(envelope);
+ this.SaveChanges();
+ }
+
public DbSet Users { get; set; }
+ public DbSet Accounts { get; set; }
+ public DbSet Envelopes { get; set; }
+ public DbSet EnvelopeFundingMethods { get; set; }
+ public DbSet CurrencyTypes { get; set; }
+ public DbSet Operations { get; set; }
+ public DbSet Transactions { get; set; }
+ public DbSet AutoclassRules { get; set; }
+ public DbSet AutoclassExpressions { get; set; }
+ public DbSet AutoclassChanges { get; set; }
}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Helpers/ErrorHandlerMiddleware.cs b/AAIntegration.SimmonsBank.API/Helpers/ErrorHandlerMiddleware.cs
new file mode 100644
index 0000000..4a04854
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Helpers/ErrorHandlerMiddleware.cs
@@ -0,0 +1,54 @@
+namespace AAIntegration.SimmonsBank.API.Config;
+
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+public class ErrorHandlerMiddleware
+{
+ private readonly RequestDelegate _next;
+ private readonly ILogger _logger;
+
+ public ErrorHandlerMiddleware(RequestDelegate next, ILogger logger)
+ {
+ _next = next;
+ _logger = logger;
+ }
+
+ public async Task Invoke(HttpContext context)
+ {
+ try
+ {
+ await _next(context);
+ }
+ catch (Exception error)
+ {
+ var response = context.Response;
+ response.ContentType = "application/json";
+
+ switch (error)
+ {
+ case AppException e:
+ // custom application error
+ response.StatusCode = (int)HttpStatusCode.BadRequest;
+ break;
+ case KeyNotFoundException e:
+ // not found error
+ response.StatusCode = (int)HttpStatusCode.NotFound;
+ break;
+ default:
+ // unhandled error
+ _logger.LogError(error, error.Message);
+ response.StatusCode = (int)HttpStatusCode.InternalServerError;
+ break;
+ }
+
+ var result = JsonSerializer.Serialize(new { message = error?.Message });
+ await response.WriteAsync(result);
+ }
+ }
+}
\ No newline at end of file
diff --git a/AAIntegration.SimmonsBank.API/Migrations/20231117033912_InitialCreate.Designer.cs b/AAIntegration.SimmonsBank.API/Migrations/20231117033912_InitialCreate.Designer.cs
new file mode 100644
index 0000000..8ad32e1
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Migrations/20231117033912_InitialCreate.Designer.cs
@@ -0,0 +1,65 @@
+//
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using AAIntegration.SimmonsBank.API.Config;
+
+#nullable disable
+
+namespace AAIntegration.SimmonsBank.API.Migrations
+{
+ [DbContext(typeof(DataContext))]
+ [Migration("20231117033912_InitialCreate")]
+ partial class InitialCreate
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.9")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Email")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("FirstName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("LastName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("PasswordHash")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Role")
+ .HasColumnType("integer");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/AAIntegration.SimmonsBank.API/Migrations/20231117033912_InitialCreate.cs b/AAIntegration.SimmonsBank.API/Migrations/20231117033912_InitialCreate.cs
new file mode 100644
index 0000000..e3da1c0
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Migrations/20231117033912_InitialCreate.cs
@@ -0,0 +1,40 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace AAIntegration.SimmonsBank.API.Migrations
+{
+ ///
+ public partial class InitialCreate : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Users",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Username = table.Column(type: "text", nullable: false),
+ FirstName = table.Column(type: "text", nullable: false),
+ LastName = table.Column(type: "text", nullable: false),
+ Email = table.Column(type: "text", nullable: false),
+ Role = table.Column(type: "integer", nullable: false),
+ PasswordHash = table.Column(type: "text", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Users", x => x.Id);
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Users");
+ }
+ }
+}
diff --git a/AAIntegration.SimmonsBank.API/Migrations/20231201042238_AccountsAllocsOperations.Designer.cs b/AAIntegration.SimmonsBank.API/Migrations/20231201042238_AccountsAllocsOperations.Designer.cs
new file mode 100644
index 0000000..d2b5312
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Migrations/20231201042238_AccountsAllocsOperations.Designer.cs
@@ -0,0 +1,270 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using AAIntegration.SimmonsBank.API.Config;
+
+#nullable disable
+
+namespace AAIntegration.SimmonsBank.API.Migrations
+{
+ [DbContext(typeof(DataContext))]
+ [Migration("20231201042238_AccountsAllocsOperations")]
+ partial class AccountsAllocsOperations
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.9")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.Account", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Balance")
+ .HasColumnType("numeric");
+
+ b.Property("CreatedOn")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CurrencyId")
+ .HasColumnType("integer");
+
+ b.Property("ExternalAccountNumber")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("InitialBalance")
+ .HasColumnType("numeric");
+
+ b.Property("LastActivity")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("OwnerId")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CurrencyId");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("Accounts");
+ });
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.Alloc", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AccountId")
+ .HasColumnType("integer");
+
+ b.Property("CurrentBalance")
+ .HasColumnType("numeric");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean");
+
+ b.Property("LastBalanceAdded")
+ .HasColumnType("numeric");
+
+ b.Property("LastTriggeredOn")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Period")
+ .HasColumnType("interval");
+
+ b.Property("PersistanceEnabled")
+ .HasColumnType("boolean");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccountId");
+
+ b.ToTable("Allocs");
+ });
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.CurrencyType", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("DecimalPlaces")
+ .HasColumnType("integer");
+
+ b.Property("Symbol")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("CurrencyTypes");
+ });
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.Operation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AbsoluteValue")
+ .HasColumnType("numeric");
+
+ b.Property("AllocId")
+ .HasColumnType("integer");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean");
+
+ b.Property("HistoricLookbackDepth")
+ .HasColumnType("integer");
+
+ b.Property("HistoricPeriod")
+ .HasColumnType("interval");
+
+ b.Property("HistoricStatistic")
+ .HasColumnType("integer");
+
+ b.Property("Mode")
+ .HasColumnType("integer");
+
+ b.Property("Negative")
+ .HasColumnType("boolean");
+
+ b.Property("Order")
+ .HasColumnType("integer");
+
+ b.Property("Percentage")
+ .HasColumnType("numeric");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AllocId");
+
+ b.ToTable("Operations");
+ });
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Email")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("FirstName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("LastName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("PasswordHash")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Role")
+ .HasColumnType("integer");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.Account", b =>
+ {
+ b.HasOne("AAIntegration.SimmonsBank.API.Entities.CurrencyType", "Currency")
+ .WithMany()
+ .HasForeignKey("CurrencyId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("AAIntegration.SimmonsBank.API.Entities.User", "Owner")
+ .WithMany("Accounts")
+ .HasForeignKey("OwnerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Currency");
+
+ b.Navigation("Owner");
+ });
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.Alloc", b =>
+ {
+ b.HasOne("AAIntegration.SimmonsBank.API.Entities.Account", "Account")
+ .WithMany("Allocs")
+ .HasForeignKey("AccountId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Account");
+ });
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.Operation", b =>
+ {
+ b.HasOne("AAIntegration.SimmonsBank.API.Entities.Alloc", null)
+ .WithMany("Operations")
+ .HasForeignKey("AllocId");
+ });
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.Account", b =>
+ {
+ b.Navigation("Allocs");
+ });
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.Alloc", b =>
+ {
+ b.Navigation("Operations");
+ });
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.User", b =>
+ {
+ b.Navigation("Accounts");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/AAIntegration.SimmonsBank.API/Migrations/20231201042238_AccountsAllocsOperations.cs b/AAIntegration.SimmonsBank.API/Migrations/20231201042238_AccountsAllocsOperations.cs
new file mode 100644
index 0000000..4d718ff
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Migrations/20231201042238_AccountsAllocsOperations.cs
@@ -0,0 +1,152 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace AAIntegration.SimmonsBank.API.Migrations
+{
+ ///
+ public partial class AccountsAllocsOperations : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "CurrencyTypes",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Code = table.Column(type: "text", nullable: false),
+ Symbol = table.Column(type: "text", nullable: false),
+ DecimalPlaces = table.Column(type: "integer", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_CurrencyTypes", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Accounts",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Name = table.Column(type: "text", nullable: false),
+ OwnerId = table.Column(type: "integer", nullable: false),
+ LastActivity = table.Column(type: "timestamp with time zone", nullable: false),
+ Balance = table.Column(type: "numeric", nullable: false),
+ CreatedOn = table.Column(type: "timestamp with time zone", nullable: false),
+ InitialBalance = table.Column(type: "numeric", nullable: false),
+ CurrencyId = table.Column(type: "integer", nullable: false),
+ ExternalAccountNumber = table.Column(type: "text", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Accounts", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Accounts_CurrencyTypes_CurrencyId",
+ column: x => x.CurrencyId,
+ principalTable: "CurrencyTypes",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_Accounts_Users_OwnerId",
+ column: x => x.OwnerId,
+ principalTable: "Users",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Allocs",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Name = table.Column(type: "text", nullable: false),
+ Enabled = table.Column(type: "boolean", nullable: false),
+ Period = table.Column(type: "interval", nullable: false),
+ AccountId = table.Column(type: "integer", nullable: false),
+ PersistanceEnabled = table.Column(type: "boolean", nullable: false),
+ CurrentBalance = table.Column(type: "numeric", nullable: false),
+ LastTriggeredOn = table.Column(type: "timestamp with time zone", nullable: false),
+ LastBalanceAdded = table.Column(type: "numeric", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Allocs", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Allocs_Accounts_AccountId",
+ column: x => x.AccountId,
+ principalTable: "Accounts",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Operations",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ Order = table.Column(type: "integer", nullable: false),
+ Enabled = table.Column(type: "boolean", nullable: false),
+ Negative = table.Column(type: "boolean", nullable: false),
+ Mode = table.Column(type: "integer", nullable: false),
+ HistoricPeriod = table.Column(type: "interval", nullable: false),
+ HistoricLookbackDepth = table.Column(type: "integer", nullable: false),
+ HistoricStatistic = table.Column(type: "integer", nullable: false),
+ AbsoluteValue = table.Column(type: "numeric", nullable: false),
+ Percentage = table.Column(type: "numeric", nullable: false),
+ AllocId = table.Column(type: "integer", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Operations", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Operations_Allocs_AllocId",
+ column: x => x.AllocId,
+ principalTable: "Allocs",
+ principalColumn: "Id");
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Accounts_CurrencyId",
+ table: "Accounts",
+ column: "CurrencyId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Accounts_OwnerId",
+ table: "Accounts",
+ column: "OwnerId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Allocs_AccountId",
+ table: "Allocs",
+ column: "AccountId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Operations_AllocId",
+ table: "Operations",
+ column: "AllocId");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Operations");
+
+ migrationBuilder.DropTable(
+ name: "Allocs");
+
+ migrationBuilder.DropTable(
+ name: "Accounts");
+
+ migrationBuilder.DropTable(
+ name: "CurrencyTypes");
+ }
+ }
+}
diff --git a/AAIntegration.SimmonsBank.API/Migrations/20231219215509_AddedTransactionEntity.Designer.cs b/AAIntegration.SimmonsBank.API/Migrations/20231219215509_AddedTransactionEntity.Designer.cs
new file mode 100644
index 0000000..74ad68b
--- /dev/null
+++ b/AAIntegration.SimmonsBank.API/Migrations/20231219215509_AddedTransactionEntity.Designer.cs
@@ -0,0 +1,420 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using AAIntegration.SimmonsBank.API.Config;
+
+#nullable disable
+
+namespace AAIntegration.SimmonsBank.API.Migrations
+{
+ [DbContext(typeof(DataContext))]
+ [Migration("20231219215509_AddedTransactionEntity")]
+ partial class AddedTransactionEntity
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.9")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.Account", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Balance")
+ .HasColumnType("numeric");
+
+ b.Property("CreatedOn")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CurrencyId")
+ .HasColumnType("integer");
+
+ b.Property("ExternalAccountNumber")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("InitialBalance")
+ .HasColumnType("numeric");
+
+ b.Property("LastActivity")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("OwnerId")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CurrencyId");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("Accounts");
+ });
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.Alloc", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AccountId")
+ .HasColumnType("integer");
+
+ b.Property("CurrentBalance")
+ .HasColumnType("numeric");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean");
+
+ b.Property("LastBalanceAdded")
+ .HasColumnType("numeric");
+
+ b.Property("LastTriggeredOn")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Period")
+ .HasColumnType("interval");
+
+ b.Property("PersistanceEnabled")
+ .HasColumnType("boolean");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccountId");
+
+ b.ToTable("Allocs");
+ });
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.CurrencyType", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("DecimalPlaces")
+ .HasColumnType("integer");
+
+ b.Property("Symbol")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("CurrencyTypes");
+ });
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.InnerTransaction", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Amount")
+ .HasColumnType("numeric");
+
+ b.Property("CreatedOn")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreditAccountId")
+ .HasColumnType("integer");
+
+ b.Property("CurrencyTypeId")
+ .HasColumnType("integer");
+
+ b.Property("DebitAccountId")
+ .HasColumnType("integer");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ExternalId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("InnerTransactionId")
+ .HasColumnType("integer");
+
+ b.Property("IsPending")
+ .HasColumnType("boolean");
+
+ b.Property("Notes")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("OuterTransactionId")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedOn")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreditAccountId");
+
+ b.HasIndex("CurrencyTypeId");
+
+ b.HasIndex("DebitAccountId");
+
+ b.HasIndex("InnerTransactionId");
+
+ b.HasIndex("OuterTransactionId");
+
+ b.ToTable("InnerTransactions");
+ });
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.Operation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AbsoluteValue")
+ .HasColumnType("numeric");
+
+ b.Property("AllocId")
+ .HasColumnType("integer");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean");
+
+ b.Property("HistoricLookbackDepth")
+ .HasColumnType("integer");
+
+ b.Property("HistoricPeriod")
+ .HasColumnType("interval");
+
+ b.Property("HistoricStatistic")
+ .HasColumnType("integer");
+
+ b.Property("Mode")
+ .HasColumnType("integer");
+
+ b.Property("Negative")
+ .HasColumnType("boolean");
+
+ b.Property("Order")
+ .HasColumnType("integer");
+
+ b.Property("Percentage")
+ .HasColumnType("numeric");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AllocId");
+
+ b.ToTable("Operations");
+ });
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.OuterTransaction", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedOn")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExternalId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("OwnerId")
+ .HasColumnType("integer");
+
+ b.Property("SplitDescription")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UpdatedOn")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId");
+
+ b.ToTable("OuterTransactions");
+ });
+
+ modelBuilder.Entity("AAIntegration.SimmonsBank.API.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property