Compare commits
3
Commits
master
..
ceb8ec6b7d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ceb8ec6b7d | ||
|
|
4d42f769cc | ||
|
|
80baa11357 |
@@ -1,150 +0,0 @@
|
|||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using MoneyMap.Controllers;
|
|
||||||
using MoneyMap.Models;
|
|
||||||
using MoneyMap.Services;
|
|
||||||
using MoneyMap.Tests.TestHelpers;
|
|
||||||
|
|
||||||
namespace MoneyMap.Tests.Controllers;
|
|
||||||
|
|
||||||
public class ImportsControllerTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public async Task ImportCsv_AutoMode_SkipsDuplicates_AssignsCardAndCategory()
|
|
||||||
{
|
|
||||||
using var context = DbContextHelper.CreateInMemoryContext();
|
|
||||||
SeedAutoImportData(context);
|
|
||||||
|
|
||||||
var controller = CreateController(context);
|
|
||||||
var csv = CreateCsvFormFile(
|
|
||||||
"transactions.csv",
|
|
||||||
"Date,Transaction,Name,Memo,Amount\n" +
|
|
||||||
"2025-01-15,DEBIT,AMAZON MKTPL,existing usbank.com.1234,-50.00\n" +
|
|
||||||
"2025-01-16,DEBIT,AMAZON MKTPL,new purchase usbank.com.1234,-25.00\n");
|
|
||||||
|
|
||||||
var result = await controller.ImportCsv(new CsvImportRequest
|
|
||||||
{
|
|
||||||
Csv = csv,
|
|
||||||
PaymentMode = MoneyMap.Models.Import.PaymentSelectMode.Auto
|
|
||||||
});
|
|
||||||
|
|
||||||
var ok = Assert.IsType<OkObjectResult>(result);
|
|
||||||
using var json = JsonDocument.Parse(JsonSerializer.Serialize(ok.Value));
|
|
||||||
|
|
||||||
Assert.Equal(2, json.RootElement.GetProperty("TotalRows").GetInt32());
|
|
||||||
Assert.Equal(1, json.RootElement.GetProperty("Inserted").GetInt32());
|
|
||||||
Assert.Equal(1, json.RootElement.GetProperty("SkippedDuplicates").GetInt32());
|
|
||||||
Assert.Equal(2, json.RootElement.GetProperty("AutoCategorized").GetInt32());
|
|
||||||
|
|
||||||
Assert.Equal(2, context.Transactions.Count());
|
|
||||||
|
|
||||||
var imported = context.Transactions.Single(t => t.Memo == "new purchase usbank.com.1234");
|
|
||||||
Assert.Equal(1, imported.AccountId);
|
|
||||||
Assert.Equal(1, imported.CardId);
|
|
||||||
Assert.Equal("Shopping", imported.Category);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ImportCsv_AccountMode_ImportsWhenAutoDetectionIsUnavailable()
|
|
||||||
{
|
|
||||||
using var context = DbContextHelper.CreateInMemoryContext();
|
|
||||||
context.Accounts.Add(new Account
|
|
||||||
{
|
|
||||||
Id = 7,
|
|
||||||
Institution = "Manual Bank",
|
|
||||||
Last4 = "4321",
|
|
||||||
Owner = "Test Owner",
|
|
||||||
AccountType = AccountType.Checking
|
|
||||||
});
|
|
||||||
await context.SaveChangesAsync();
|
|
||||||
|
|
||||||
var controller = CreateController(context);
|
|
||||||
var csv = CreateCsvFormFile(
|
|
||||||
"manual-import.csv",
|
|
||||||
"Date,Transaction,Name,Memo,Amount\n" +
|
|
||||||
"2025-02-01,DEBIT,LOCAL STORE,no card info here,-10.00\n");
|
|
||||||
|
|
||||||
var result = await controller.ImportCsv(new CsvImportRequest
|
|
||||||
{
|
|
||||||
Csv = csv,
|
|
||||||
PaymentMode = MoneyMap.Models.Import.PaymentSelectMode.Account,
|
|
||||||
SelectedAccountId = 7
|
|
||||||
});
|
|
||||||
|
|
||||||
var ok = Assert.IsType<OkObjectResult>(result);
|
|
||||||
using var json = JsonDocument.Parse(JsonSerializer.Serialize(ok.Value));
|
|
||||||
|
|
||||||
Assert.Equal(1, json.RootElement.GetProperty("Inserted").GetInt32());
|
|
||||||
Assert.Equal(0, json.RootElement.GetProperty("SkippedDuplicates").GetInt32());
|
|
||||||
|
|
||||||
var imported = Assert.Single(context.Transactions);
|
|
||||||
Assert.Equal(7, imported.AccountId);
|
|
||||||
Assert.Null(imported.CardId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ImportsController CreateController(MoneyMap.Data.MoneyMapContext context)
|
|
||||||
{
|
|
||||||
var cardResolver = new CardResolver(context);
|
|
||||||
var importer = new TransactionImporter(context, cardResolver);
|
|
||||||
var categorizer = new TransactionCategorizer(context, new MemoryCache(new MemoryCacheOptions()));
|
|
||||||
|
|
||||||
return new ImportsController(context, importer, categorizer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void SeedAutoImportData(MoneyMap.Data.MoneyMapContext context)
|
|
||||||
{
|
|
||||||
context.Accounts.Add(new Account
|
|
||||||
{
|
|
||||||
Id = 1,
|
|
||||||
Institution = "Test Bank",
|
|
||||||
Last4 = "1234",
|
|
||||||
Owner = "Test Owner",
|
|
||||||
AccountType = AccountType.Checking
|
|
||||||
});
|
|
||||||
|
|
||||||
context.Cards.Add(new Card
|
|
||||||
{
|
|
||||||
Id = 1,
|
|
||||||
Issuer = "Visa",
|
|
||||||
Last4 = "1234",
|
|
||||||
Owner = "Test Owner",
|
|
||||||
AccountId = 1
|
|
||||||
});
|
|
||||||
|
|
||||||
context.CategoryMappings.Add(new CategoryMapping
|
|
||||||
{
|
|
||||||
Category = "Shopping",
|
|
||||||
Pattern = "AMAZON",
|
|
||||||
Priority = 10
|
|
||||||
});
|
|
||||||
|
|
||||||
context.Transactions.Add(new Transaction
|
|
||||||
{
|
|
||||||
Date = new DateTime(2025, 1, 15),
|
|
||||||
TransactionType = "DEBIT",
|
|
||||||
Name = "AMAZON MKTPL",
|
|
||||||
Memo = "existing usbank.com.1234",
|
|
||||||
Amount = -50.00m,
|
|
||||||
Category = "Shopping",
|
|
||||||
AccountId = 1,
|
|
||||||
CardId = 1,
|
|
||||||
Last4 = "1234"
|
|
||||||
});
|
|
||||||
|
|
||||||
context.SaveChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IFormFile CreateCsvFormFile(string fileName, string content)
|
|
||||||
{
|
|
||||||
var bytes = Encoding.UTF8.GetBytes(content);
|
|
||||||
var stream = new MemoryStream(bytes);
|
|
||||||
return new FormFile(stream, 0, bytes.Length, "csv", fileName)
|
|
||||||
{
|
|
||||||
Headers = new HeaderDictionary(),
|
|
||||||
ContentType = "text/csv"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using MoneyMap.Data;
|
|
||||||
using MoneyMap.Models.Import;
|
|
||||||
using MoneyMap.Services;
|
|
||||||
|
|
||||||
namespace MoneyMap.Controllers;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
public class ImportsController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly MoneyMapContext _db;
|
|
||||||
private readonly ITransactionImporter _importer;
|
|
||||||
private readonly ITransactionCategorizer _categorizer;
|
|
||||||
|
|
||||||
public ImportsController(
|
|
||||||
MoneyMapContext db,
|
|
||||||
ITransactionImporter importer,
|
|
||||||
ITransactionCategorizer categorizer)
|
|
||||||
{
|
|
||||||
_db = db;
|
|
||||||
_importer = importer;
|
|
||||||
_categorizer = categorizer;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("csv")]
|
|
||||||
[Consumes("multipart/form-data")]
|
|
||||||
[RequestFormLimits(MultipartBodyLengthLimit = 50_000_000)]
|
|
||||||
[RequestSizeLimit(50_000_000)]
|
|
||||||
public async Task<IActionResult> ImportCsv([FromForm] CsvImportRequest request)
|
|
||||||
{
|
|
||||||
if (request.Csv is null || request.Csv.Length == 0)
|
|
||||||
return BadRequest(new { message = "Please choose a CSV file." });
|
|
||||||
|
|
||||||
var cards = await _db.Cards
|
|
||||||
.Include(c => c.Account)
|
|
||||||
.OrderBy(c => c.Owner)
|
|
||||||
.ThenBy(c => c.Last4)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var accounts = await _db.Accounts
|
|
||||||
.OrderBy(a => a.Institution)
|
|
||||||
.ThenBy(a => a.Last4)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var importContext = new ImportContext
|
|
||||||
{
|
|
||||||
PaymentMode = request.PaymentMode,
|
|
||||||
SelectedCardId = request.SelectedCardId,
|
|
||||||
SelectedAccountId = request.SelectedAccountId,
|
|
||||||
AvailableCards = cards,
|
|
||||||
AvailableAccounts = accounts,
|
|
||||||
FileName = request.Csv.FileName
|
|
||||||
};
|
|
||||||
|
|
||||||
await using var csvStream = request.Csv.OpenReadStream();
|
|
||||||
var previewResult = await _importer.PreviewAsync(csvStream, importContext);
|
|
||||||
|
|
||||||
if (!previewResult.IsSuccess)
|
|
||||||
return BadRequest(new { message = previewResult.ErrorMessage });
|
|
||||||
|
|
||||||
var previewTransactions = previewResult.Data ?? new List<TransactionPreview>();
|
|
||||||
var autoCategorizedCount = 0;
|
|
||||||
|
|
||||||
foreach (var preview in previewTransactions)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(preview.Transaction.Category))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var categorizationResult = await _categorizer.CategorizeAsync(preview.Transaction.Name, preview.Transaction.Amount);
|
|
||||||
if (string.IsNullOrWhiteSpace(categorizationResult.Category))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
preview.Transaction.Category = categorizationResult.Category;
|
|
||||||
preview.Transaction.MerchantId = categorizationResult.MerchantId;
|
|
||||||
preview.SuggestedCategory = categorizationResult.Category;
|
|
||||||
autoCategorizedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
var transactionsToImport = previewTransactions
|
|
||||||
.Where(p => !p.IsDuplicate)
|
|
||||||
.Select(p => p.Transaction)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var duplicateCount = previewTransactions.Count - transactionsToImport.Count;
|
|
||||||
|
|
||||||
if (transactionsToImport.Count > 0)
|
|
||||||
{
|
|
||||||
var importResult = await _importer.ImportAsync(transactionsToImport);
|
|
||||||
if (!importResult.IsSuccess)
|
|
||||||
return BadRequest(new { message = importResult.ErrorMessage });
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(new
|
|
||||||
{
|
|
||||||
FileName = request.Csv.FileName,
|
|
||||||
request.PaymentMode,
|
|
||||||
request.SelectedCardId,
|
|
||||||
request.SelectedAccountId,
|
|
||||||
TotalRows = previewTransactions.Count,
|
|
||||||
Inserted = transactionsToImport.Count,
|
|
||||||
SkippedDuplicates = duplicateCount,
|
|
||||||
AutoCategorized = autoCategorizedCount,
|
|
||||||
Transactions = previewTransactions.Select(p => new
|
|
||||||
{
|
|
||||||
p.Transaction.Date,
|
|
||||||
p.Transaction.Name,
|
|
||||||
p.Transaction.Memo,
|
|
||||||
p.Transaction.Amount,
|
|
||||||
p.Transaction.Category,
|
|
||||||
p.Transaction.AccountId,
|
|
||||||
p.Transaction.CardId,
|
|
||||||
p.IsDuplicate,
|
|
||||||
p.PaymentMethodLabel
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CsvImportRequest
|
|
||||||
{
|
|
||||||
public IFormFile? Csv { get; set; }
|
|
||||||
public PaymentSelectMode PaymentMode { get; set; } = PaymentSelectMode.Auto;
|
|
||||||
public int? SelectedCardId { get; set; }
|
|
||||||
public int? SelectedAccountId { get; set; }
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user