Skip to content

Commit

Permalink
Merge pull request #36 from DRJTechnology/Development
Browse files Browse the repository at this point in the history
Addition of bank statement upload
  • Loading branch information
DRJTechnology authored Dec 21, 2023
2 parents 3ec45ac + fa49678 commit b729128
Show file tree
Hide file tree
Showing 26 changed files with 475 additions and 9 deletions.
16 changes: 16 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Property Portfolio Manager

A website for managing a number of rental properties.

This project is work-in-porgress.
Expand All @@ -10,13 +11,28 @@ The C# aspnetcore application is intended for use by landlords to manage a portf
The User interface is written as a Blazor Webassembly application hosted in a C# Web API project.

## Data access

The Web API uses service and repository layers to access a SQL Server database.

## Authentication

TODO

## Document & Image storage - MS Graph

TODO

## Finance - Double entry bookkeeping

TODO

## Other packages used

### Importing bank statements

The importing of bank statement csv files is completed using [CsvHelper](https://github.com/JoshClose/CsvHelper).
The initial implementation during development uses hard-coded column mappings and CultureInfo. This will eventually be stored against the individual portfolios.

### Caching

Redis is used for caching and the [DRJTechnology.Cache](https://github.com/DRJTechnology/DRJTechnology.Cache) package is used to implement this.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using PropertyPortfolioManager.Models.Model.Document;

namespace PropertyPortfolioManager.Client.Interfaces
{
public interface IBankStatementService : IGenericDataService
{
Task UploadBankStatement(Stream content, string filename);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@page "/uploadstatement"

<SectionContent SectionName="top-bar">
<div class="row">
<div class="col">
<div>Upload bank statement</div>
</div>
</div>
</SectionContent>

<div class="page-content">
<InputFile OnChange="HandleFileSelection" />
</div>

@if (file != null)
{
<div class="page-content">
<p><strong>File Name:</strong> @file.Name</p>
<p><strong>File Size:</strong> @file.Size bytes</p>
</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using PropertyPortfolioManager.Client.Interfaces;

namespace PropertyPortfolioManager.Client.Pages
{
public partial class UploadBankStatement
{
[Inject]
public IBankStatementService bankStatementService { get; set; }

private IBrowserFile file;

private async Task HandleFileSelection(InputFileChangeEventArgs e)
{
file = e.File;
var stream = file.OpenReadStream();
await bankStatementService.UploadBankStatement(stream, file.Name);
}
}
}
19 changes: 10 additions & 9 deletions src/PropertyPortfolioManager.Client/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,27 @@
// Supply HttpClient instances that include access tokens when making requests to the server project
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("PropertyPortfolioManager.ServerAPI"));

builder.Services.AddScoped<IPortfolioDataService, PortfolioDataService>();
builder.Services.AddScoped<IUnitDataService, UnitDataService>();
builder.Services.AddScoped<IUnitTypeDataService, UnitTypeDataService>();
builder.Services.AddScoped<IAccountDataService, AccountDataService>();
builder.Services.AddScoped<IAccountTypeDataService, AccountTypeDataService>();
builder.Services.AddScoped<IBankStatementService, BankStatementService>();
builder.Services.AddScoped<IContactDataService, ContactDataService>();
builder.Services.AddScoped<IContactTypeDataService, ContactTypeDataService>();
builder.Services.AddScoped<IUserDataService, UserDataService>();
builder.Services.AddScoped<ITenancyTypeDataService, TenancyTypeDataService>();
builder.Services.AddScoped<IDocumentService, DocumentService>();
builder.Services.AddScoped<IPortfolioDataService, PortfolioDataService>();
builder.Services.AddScoped<ITenancyDataService, TenancyDataService>();
builder.Services.AddScoped<IAccountDataService, AccountDataService>();
builder.Services.AddScoped<IAccountTypeDataService, AccountTypeDataService>();
builder.Services.AddScoped<ITenancyTypeDataService, TenancyTypeDataService>();
builder.Services.AddScoped<ITransactionDetailDataService, TransactionDetailDataService>();
builder.Services.AddScoped<ITransactionTypeDataService, TransactionTypeDataService>();
builder.Services.AddScoped<IDocumentService, DocumentService>();
builder.Services.AddScoped<IUnitDataService, UnitDataService>();
builder.Services.AddScoped<IUnitTypeDataService, UnitTypeDataService>();
builder.Services.AddScoped<IUserDataService, UserDataService>();
builder.Services.AddScoped<ProfileState>();

builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("api://2956eaa1-1d62-448d-9ac3-58ea18e4f302/API.Access");
options.ProviderOptions.LoginMode = "Redirect";
options.ProviderOptions.LoginMode = "Redirect";
});

await builder.Build().RunAsync();
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using PropertyPortfolioManager.Client.Interfaces;

namespace PropertyPortfolioManager.Client.Services
{
public class BankStatementService : GenericDataService, IBankStatementService
{
public BankStatementService(HttpClient httpClient)
: base(httpClient)
{
ApiControllerName = "BankStatement";
}

public async Task UploadBankStatement(Stream content, string filename)
{
var formContent = new MultipartFormDataContent();
formContent.Add(new StreamContent(content), "File", filename);

var response = await httpClient.PostAsync($"api/BankStatement/UploadBankStatement", formContent);

if (response == null || !response.IsSuccessStatusCode)
{
throw new Exception($"Failed to upload bank statement.");
}
}
}
}
5 changes: 5 additions & 0 deletions src/PropertyPortfolioManager.Client/Shared/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@
Bank Accounts
</NavLink>
</li>
<li>
<NavLink class="nav-link" href="uploadstatement">
Upload Bank Statement
</NavLink>
</li>
<li>
<NavLink class="nav-link" href="account">
Accounts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,18 @@
<Folder Include="Stored Procedures\finance" />
<Folder Include="Snapshots" />
<Folder Include="Schemas" />
<Folder Include="Types" />
</ItemGroup>
<ItemGroup>
<Build Include="Scripts\Transactions with running balance.sql" />
<Build Include="Security\db_executor.sql" />
<Build Include="Security\ppm-user-dev.sql" />
<Build Include="Stored Procedures\dbo\InsertAppLog.sql" />
<Build Include="Stored Procedures\finance\Account_GetByType.sql" />
<Build Include="Stored Procedures\finance\BankStatement_Upload.sql" />
<Build Include="Tables\dbo\AppLog.sql" />
<Build Include="Tables\dbo\CombinedAccounts.sql" />
<Build Include="Tables\finance\BankAccountDetail.sql" />
<Build Include="Tables\general\Address.sql" />
<Build Include="Tables\profile\User.sql" />
<Build Include="Tables\property\Portfolio.sql" />
Expand Down Expand Up @@ -157,6 +161,7 @@
<Build Include="Tables\finance\TransactionDetail.sql" />
<Build Include="Stored Procedures\finance\TransactionDetail_Get.sql" />
<Build Include="Stored Procedures\finance\TransactionType_GetAll.sql" />
<Build Include="Types\StatementTableType.sql.sql" />
</ItemGroup>
<ItemGroup>
<None Include="Scripts\InitialiseData.sql" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@

DECLARE
@PortfolioId int,
@FromDate datetime = null,
@ToDate datetime = null,
@AccountId int = null,
@TransactionTypeId int = null

SET @PortfolioId = 2
SET @FromDate = '01 Sep 2023'
SET @ToDate = '01 Oct 2023'
SET @AccountId = 1020

DECLARE @StartingBalance MONEY
IF (@FromDate IS NOT NULL)
BEGIN
SELECT @StartingBalance = SUM(amount * direction)
FROM finance.TransactionDetail td
INNER JOIN finance.[Transaction] t ON td.TransactionId = t.Id
INNER JOIN finance.[TransactionType] tt ON t.TransactionTypeId = tt.Id
INNER JOIN finance.Account a on td.AccountId = a.Id
WHERE t.PortfolioId = @PortfolioId
AND (td.[Date] < @FromDate)
AND (ISNULL(@AccountId, 0) = 0 OR td.AccountId = @AccountId)
AND (ISNULL(@TransactionTypeId, 0) = 0 OR t.TransactionTypeId = @TransactionTypeId)
END


SELECT @StartingBalance AS StartingBalance

SELECT td.Id,
td.TransactionId,
-- tt.[Type],
td.[Date],
t.Reference,
-- td.AccountId,
-- a.[Name] AS Account,
td.[Description],
CASE WHEN direction = 1 THEN td.amount ELSE 0 END AS Debit,
CASE WHEN direction = -1 THEN td.amount ELSE 0 END AS Credit
, (SUM(td.amount * td.direction) OVER (ORDER BY td.[Date]) + @StartingBalance) * -1 AS running_total
FROM finance.TransactionDetail td
INNER JOIN finance.[Transaction] t ON td.TransactionId = t.Id
-- INNER JOIN finance.[TransactionType] tt ON t.TransactionTypeId = tt.Id
-- INNER JOIN finance.Account a on td.AccountId = a.Id
WHERE t.PortfolioId = @PortfolioId
AND (@FromDate IS NULL OR td.[Date] >= @FromDate)
AND (@ToDate IS NULL OR td.[Date] < DATEADD(DAY, 1, @ToDate))
AND (ISNULL(@AccountId, 0) = 0 OR td.AccountId = @AccountId)
AND (ISNULL(@TransactionTypeId, 0) = 0 OR t.TransactionTypeId = @TransactionTypeId)
ORDER BY td.[Date] --DESC--, td.TransactionId, a.[Name], Credit, Debit

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- ==========================================================
-- Author: Dave Brown
-- Create date: 12 Dec 2023
-- Description: Creates an account record
-- ==========================================================
CREATE PROCEDURE [finance].[BankStatement_Upload]
@AccountId INT,
@Statement finance.StatementTableType READONLY,
@CurrentUserId INT
AS
INSERT INTO [finance].[BankAccountDetail] (AccountId, Date, Amount, Description, TransactionType, Deleted, CreateUserId, CreateDate, AmendUserId, AmendDate)
SELECT @AccountId, Date, Amount, Description, TransactionType, 0, @CurrentUserId, SYSDATETIME(), @CurrentUserId, SYSDATETIME()
FROM @Statement

RETURN 0

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
CREATE TABLE [finance].[BankAccountDetail]
(
[Id] INT IDENTITY NOT NULL,
[AccountId] INT NOT NULL,
[Date] DATETIME NOT NULL,
[Amount] MONEY NOT NULL,
[Description] NVARCHAR(512) NOT NULL,
[TransactionType] NVARCHAR(512) NOT NULL,
[Deleted] BIT DEFAULT (0) NOT NULL,
[CreateUserId] INT NOT NULL,
[CreateDate] DATETIME CONSTRAINT [DF_BankAccountDetail_CreateDate] DEFAULT (getutcdate()) NOT NULL,
[AmendUserId] INT NOT NULL,
[AmendDate] DATETIME CONSTRAINT [DF_BankAccountDetail_AmendDate] DEFAULT (getutcdate()) NOT NULL,
CONSTRAINT [PK_BankAccountDetail] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_BankAccountDetail_Account] FOREIGN KEY ([AccountId]) REFERENCES [finance].[Account]([Id])
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- ==========================================================
-- Author: Dave Brown
-- Create date: 12 Dec 2023
-- Description: Creates a Statement Table Type
-- ==========================================================
CREATE TYPE finance.StatementTableType AS TABLE (
[Date] DATETIME NULL,
[Amount] MONEY NULL,
[Description] NVARCHAR(512) NULL,
[TransactionType] NVARCHAR(512) NULL
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

namespace PropertyPortfolioManager.Models.InternalObjects
{
public class UploadResult
{
public bool Uploaded { get; set; }
public string? FileName { get; set; }
public string? StoredFileName { get; set; }
public int ErrorCode { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using CsvHelper.Configuration;

namespace PropertyPortfolioManager.Models.Model.Finance
{
public class BankStatementMap : ClassMap<BankStatementModel>
{
public BankStatementMap()
{
Map(m => m.Date).Name("Date");
Map(m => m.Amount).Name("Amount");
Map(m => m.Description).Name("Memo");
Map(m => m.TransactionType).Name("Subcategory");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace PropertyPortfolioManager.Models.Model.Finance
{
public class BankStatementModel
{
public DateTime Date { get; set; }
public decimal Amount { get; set; }
public string Description { get; set; } = string.Empty;
public string TransactionType { get; set; } = string.Empty;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="Microsoft.Graph" Version="5.33.0" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using PropertyPortfolioManager.Models.Model.Finance;
using System.Data;

namespace PropertyPortfolioManager.Server.Repositories.Interfaces
{
public interface IBankStatementRepository
{
Task AddBankStatementRecords(int currentUserId, int accountId, DataTable recordList);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Dapper;
using Microsoft.Graph.Models;
using PropertyPortfolioManager.Models.Model.Finance;
using PropertyPortfolioManager.Server.Repositories.Interfaces;
using System.ComponentModel;
using System.Data;

namespace PropertyPortfolioManager.Server.Repositories
{
public class BankStatementRepository : IBankStatementRepository
{
private readonly IDbConnection dbConnection;

public BankStatementRepository(IDbConnection dbConnection)
{
this.dbConnection = dbConnection;
}

public async Task AddBankStatementRecords(int currentUserId, int accountId, DataTable recordList)
{
var parameters = new DynamicParameters();
parameters.Add("@AccountId", accountId);
parameters.Add("@Statement", recordList.AsTableValuedParameter("[finance].[StatementTableType]"));
parameters.Add("@CurrentUserId", currentUserId);

await this.dbConnection.ExecuteAsync("finance.BankStatement_Upload", parameters, commandType: CommandType.StoredProcedure);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using PropertyPortfolioManager.Models.Model.Document;

namespace PropertyPortfolioManager.Server.Services.Interfaces
{
public interface IBankStatementService
{
Task UploadBankStatement(int currentUserId, int portfolioId, Stream stream);
}
}
Loading

0 comments on commit b729128

Please sign in to comment.