Files
MoneyMap/MoneyMap.Tests/Services/ReceiptMatchingServiceTests.cs
AJ af4a638b81 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>
2025-10-26 00:01:28 -04:00

331 lines
9.1 KiB
C#

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);
}
}