6520ebf221
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>
92 lines
3.5 KiB
C#
92 lines
3.5 KiB
C#
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();
|
|
}
|
|
}
|