Initial commit: Purrse personal finance app
Self-hosted, plugin-extensible personal finance manager built with ASP.NET Core 9.0, Vue 3 + Vuetify 3, and PostgreSQL 17. Backend (8 .NET projects): - Core: 19 domain models, 6 enums, 14 DTOs, 12 service interfaces - Data: EF Core DbContext, 16 entity configurations, category seeder - API: 14 controllers, 15 services, JWT auth, SignalR, middleware - Plugins: Abstractions + OFX/CSV/QIF file parsers - Tests: 28 xUnit tests (fingerprinting, duplicate detection, parsers) Frontend (Vue 3 + Vuetify 3 + TypeScript): - 13 views, Pinia stores, Axios API services with JWT interceptors - Dashboard, accounts, transactions, categories, imports, and more Deployment: - Docker Compose (PostgreSQL 17 + .NET API + nginx frontend) - Auto-migration on startup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Purrse.Core.DTOs;
|
||||
using Purrse.Core.Enums;
|
||||
using Purrse.Core.Models;
|
||||
using Purrse.Data;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Purrse.Api.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ReconciliationController : ControllerBase
|
||||
{
|
||||
private readonly PurrseDbContext _db;
|
||||
private Guid UserId => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||
|
||||
public ReconciliationController(PurrseDbContext db) => _db = db;
|
||||
|
||||
[HttpPost("{accountId}/start")]
|
||||
public async Task<ActionResult<ReconciliationResponse>> Start(Guid accountId, StartReconciliationRequest request)
|
||||
{
|
||||
var account = await _db.Accounts.FirstOrDefaultAsync(a => a.Id == accountId && a.UserId == UserId)
|
||||
?? throw new KeyNotFoundException("Account not found");
|
||||
|
||||
var reconciliation = new Reconciliation
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
AccountId = accountId,
|
||||
StatementDate = request.StatementDate,
|
||||
StatementBalance = request.StatementBalance
|
||||
};
|
||||
|
||||
_db.Reconciliations.Add(reconciliation);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
var uncleared = await _db.Transactions
|
||||
.Include(t => t.Category)
|
||||
.Where(t => t.AccountId == accountId && t.Status != TransactionStatus.Reconciled && !t.IsVoid)
|
||||
.OrderByDescending(t => t.Date)
|
||||
.Select(t => new TransactionResponse(t.Id, t.AccountId, null, t.Date, t.Amount,
|
||||
t.PayeeId, t.PayeeName, t.CategoryId, t.Category != null ? t.Category.Name : null,
|
||||
t.Memo, t.ReferenceNumber, t.CheckNumber, t.Type, t.Status, t.IsVoid,
|
||||
t.TransferTransactionId, t.ImportBatchId, null, t.CreatedAt))
|
||||
.ToListAsync();
|
||||
|
||||
var clearedBalance = await _db.Transactions
|
||||
.Where(t => t.AccountId == accountId && t.Status == TransactionStatus.Cleared && !t.IsVoid)
|
||||
.SumAsync(t => t.Amount);
|
||||
|
||||
return Ok(new ReconciliationResponse(reconciliation.Id, accountId, request.StatementDate,
|
||||
request.StatementBalance, clearedBalance, request.StatementBalance - clearedBalance, false, uncleared));
|
||||
}
|
||||
|
||||
[HttpPost("{accountId}/complete")]
|
||||
public async Task<ActionResult> Complete(Guid accountId, [FromQuery] Guid reconciliationId)
|
||||
{
|
||||
var rec = await _db.Reconciliations.FirstOrDefaultAsync(r => r.Id == reconciliationId && r.AccountId == accountId)
|
||||
?? throw new KeyNotFoundException("Reconciliation not found");
|
||||
|
||||
var clearedTxns = await _db.Transactions
|
||||
.Where(t => t.AccountId == accountId && t.Status == TransactionStatus.Cleared && !t.IsVoid)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var txn in clearedTxns)
|
||||
{
|
||||
txn.Status = TransactionStatus.Reconciled;
|
||||
txn.ReconciliationId = reconciliationId;
|
||||
}
|
||||
|
||||
rec.IsCompleted = true;
|
||||
rec.CompletedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{accountId}/cancel")]
|
||||
public async Task<ActionResult> Cancel(Guid accountId, [FromQuery] Guid reconciliationId)
|
||||
{
|
||||
var rec = await _db.Reconciliations.FirstOrDefaultAsync(r => r.Id == reconciliationId && r.AccountId == accountId && !r.IsCompleted)
|
||||
?? throw new KeyNotFoundException("Reconciliation not found");
|
||||
|
||||
_db.Reconciliations.Remove(rec);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user