Test: add comprehensive unit test project for services
Add MoneyMap.Tests project with xUnit tests for all new services: - AccountServiceTests: Account retrieval, stats, validation, deletion - CardServiceTests: Card retrieval, stats, validation, deletion - MerchantServiceTests: Merchant CRUD operations - ReferenceDataServiceTests: Reference data retrieval - TransactionServiceTests: Duplicate detection, retrieval, deletion - TransactionStatisticsServiceTests: Statistics calculations - ReceiptMatchingServiceTests: Receipt-to-transaction matching logic - DbContextHelper: In-memory database context factory for test isolation Uses xUnit, Moq, and EF Core InMemory database. Solution file updated to include test project. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
330
MoneyMap.Tests/Services/ReceiptMatchingServiceTests.cs
Normal file
330
MoneyMap.Tests/Services/ReceiptMatchingServiceTests.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
using MoneyMap.Models;
|
||||
using MoneyMap.Services;
|
||||
using MoneyMap.Tests.TestHelpers;
|
||||
using Xunit;
|
||||
|
||||
namespace MoneyMap.Tests.Services;
|
||||
|
||||
public class ReceiptMatchingServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetTransactionIdsWithReceiptsAsync_ReturnsTransactionIds()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new ReceiptMatchingService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var transaction1 = new Transaction
|
||||
{
|
||||
Id = 1,
|
||||
Date = DateTime.Now,
|
||||
Amount = -50.00m,
|
||||
Name = "Store A",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
var transaction2 = new Transaction
|
||||
{
|
||||
Id = 2,
|
||||
Date = DateTime.Now,
|
||||
Amount = -30.00m,
|
||||
Name = "Store B",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
context.Transactions.AddRange(transaction1, transaction2);
|
||||
|
||||
var receipt = new Receipt
|
||||
{
|
||||
Id = 1,
|
||||
TransactionId = 1,
|
||||
FileName = "receipt.pdf",
|
||||
ContentType = "application/pdf",
|
||||
StoragePath = "/receipts/receipt.pdf",
|
||||
FileSizeBytes = 1024,
|
||||
FileHashSha256 = "hash123",
|
||||
UploadedAtUtc = DateTime.UtcNow
|
||||
};
|
||||
context.Receipts.Add(receipt);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await service.GetTransactionIdsWithReceiptsAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Contains(1L, result);
|
||||
Assert.DoesNotContain(2L, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindMatchingTransactionsAsync_FiltersByDateRange()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new ReceiptMatchingService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var receiptDate = new DateTime(2025, 1, 15);
|
||||
|
||||
var withinRange = new Transaction
|
||||
{
|
||||
Id = 1,
|
||||
Date = receiptDate.AddDays(2), // Within +/- 3 days
|
||||
Amount = -50.00m,
|
||||
Name = "Store A",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
var outsideRange = new Transaction
|
||||
{
|
||||
Id = 2,
|
||||
Date = receiptDate.AddDays(5), // Outside +/- 3 days
|
||||
Amount = -50.00m,
|
||||
Name = "Store B",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
context.Transactions.AddRange(withinRange, outsideRange);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var criteria = new ReceiptMatchCriteria
|
||||
{
|
||||
ReceiptDate = receiptDate,
|
||||
ExcludeTransactionIds = new HashSet<long>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await service.FindMatchingTransactionsAsync(criteria);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(1, result[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindMatchingTransactionsAsync_FiltersByAmountTolerance()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new ReceiptMatchingService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var receiptDate = DateTime.Now;
|
||||
var receiptTotal = 100.00m;
|
||||
|
||||
var withinTolerance = new Transaction
|
||||
{
|
||||
Id = 1,
|
||||
Date = receiptDate,
|
||||
Amount = -105.00m, // Within 10% tolerance
|
||||
Name = "Store A",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
var outsideTolerance = new Transaction
|
||||
{
|
||||
Id = 2,
|
||||
Date = receiptDate,
|
||||
Amount = -150.00m, // Outside 10% tolerance
|
||||
Name = "Store B",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
context.Transactions.AddRange(withinTolerance, outsideTolerance);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var criteria = new ReceiptMatchCriteria
|
||||
{
|
||||
ReceiptDate = receiptDate,
|
||||
Total = receiptTotal,
|
||||
ExcludeTransactionIds = new HashSet<long>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await service.FindMatchingTransactionsAsync(criteria);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(1, result[0].Id);
|
||||
Assert.True(result[0].IsCloseAmount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindMatchingTransactionsAsync_MarksExactAmountMatch()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new ReceiptMatchingService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var receiptDate = DateTime.Now;
|
||||
var receiptTotal = 100.00m;
|
||||
|
||||
var exactMatch = new Transaction
|
||||
{
|
||||
Id = 1,
|
||||
Date = receiptDate,
|
||||
Amount = -100.00m, // Exact match
|
||||
Name = "Store A",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
context.Transactions.Add(exactMatch);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var criteria = new ReceiptMatchCriteria
|
||||
{
|
||||
ReceiptDate = receiptDate,
|
||||
Total = receiptTotal,
|
||||
ExcludeTransactionIds = new HashSet<long>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await service.FindMatchingTransactionsAsync(criteria);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.True(result[0].IsExactAmount);
|
||||
Assert.False(result[0].IsCloseAmount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindMatchingTransactionsAsync_ExcludesTransactionsWithReceipts()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new ReceiptMatchingService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var receiptDate = DateTime.Now;
|
||||
|
||||
var transaction1 = new Transaction
|
||||
{
|
||||
Id = 1,
|
||||
Date = receiptDate,
|
||||
Amount = -50.00m,
|
||||
Name = "Store A",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
var transaction2 = new Transaction
|
||||
{
|
||||
Id = 2,
|
||||
Date = receiptDate,
|
||||
Amount = -50.00m,
|
||||
Name = "Store B",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
context.Transactions.AddRange(transaction1, transaction2);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var criteria = new ReceiptMatchCriteria
|
||||
{
|
||||
ReceiptDate = receiptDate,
|
||||
ExcludeTransactionIds = new HashSet<long> { 1 } // Exclude transaction 1
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await service.FindMatchingTransactionsAsync(criteria);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(2, result[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FindMatchingTransactionsAsync_UsesDueDateForBills()
|
||||
{
|
||||
// Arrange
|
||||
using var context = DbContextHelper.CreateInMemoryContext();
|
||||
var service = new ReceiptMatchingService(context);
|
||||
|
||||
var account = new Account
|
||||
{
|
||||
Id = 1,
|
||||
Institution = "Test Bank",
|
||||
AccountType = AccountType.Checking,
|
||||
Last4 = "1234",
|
||||
Owner = "Test Owner"
|
||||
};
|
||||
context.Accounts.Add(account);
|
||||
|
||||
var receiptDate = new DateTime(2025, 1, 1);
|
||||
var dueDate = new DateTime(2025, 1, 15);
|
||||
|
||||
// Transaction on due date + 3 days (should match for bills)
|
||||
var transaction = new Transaction
|
||||
{
|
||||
Id = 1,
|
||||
Date = dueDate.AddDays(3),
|
||||
Amount = -50.00m,
|
||||
Name = "Utility Company",
|
||||
Memo = "Test",
|
||||
AccountId = 1
|
||||
};
|
||||
context.Transactions.Add(transaction);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var criteria = new ReceiptMatchCriteria
|
||||
{
|
||||
ReceiptDate = receiptDate,
|
||||
DueDate = dueDate, // Bill with due date
|
||||
ExcludeTransactionIds = new HashSet<long>()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await service.FindMatchingTransactionsAsync(criteria);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
Assert.Equal(1, result[0].Id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user