Add due date support for bills and utility receipts
Enhanced receipt parsing and mapping to support bills with due dates (like utility bills): - Updated OpenAI prompt to extract due date from bills - Added DueDate property to Receipt model with database migration - Modified auto-mapping logic to use bill date to due date range when both dates are present - For regular receipts without due dates, continues to use ±3 day window - Updated manual mapping UI to display due dates and adjust date range explanation - Receipt list now shows due dates in orange "Due:" text for clarity This allows electric bills and similar documents with 20+ day payment windows to be properly matched to their payment transactions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,9 @@
|
|||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(git add:*)",
|
"Bash(git add:*)",
|
||||||
"Bash(git commit:*)"
|
"Bash(git commit:*)",
|
||||||
|
"Bash(dotnet ef migrations:*)",
|
||||||
|
"Bash(dotnet ef database:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
609
MoneyMap/Migrations/20251012185017_AddReceiptDueDate.Designer.cs
generated
Normal file
609
MoneyMap/Migrations/20251012185017_AddReceiptDueDate.Designer.cs
generated
Normal file
@@ -0,0 +1,609 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using MoneyMap.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace MoneyMap.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(MoneyMapContext))]
|
||||||
|
[Migration("20251012185017_AddReceiptDueDate")]
|
||||||
|
partial class AddReceiptDueDate
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "9.0.9")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.Account", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("AccountType")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Institution")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Last4")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4)
|
||||||
|
.HasColumnType("nvarchar(4)");
|
||||||
|
|
||||||
|
b.Property<string>("Nickname")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Owner")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Institution", "Last4", "Owner");
|
||||||
|
|
||||||
|
b.ToTable("Accounts");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.Card", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int?>("AccountId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Issuer")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Last4")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(4)
|
||||||
|
.HasColumnType("nvarchar(4)");
|
||||||
|
|
||||||
|
b.Property<string>("Nickname")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("Owner")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId");
|
||||||
|
|
||||||
|
b.HasIndex("Issuer", "Last4", "Owner");
|
||||||
|
|
||||||
|
b.ToTable("Cards");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.Merchant", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Merchants");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.Receipt", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)")
|
||||||
|
.HasDefaultValue("application/octet-stream");
|
||||||
|
|
||||||
|
b.Property<string>("Currency")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("nvarchar(8)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DueDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("FileHashSha256")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(260)
|
||||||
|
.HasColumnType("nvarchar(260)");
|
||||||
|
|
||||||
|
b.Property<long>("FileSizeBytes")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("Merchant")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ReceiptDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("StoragePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("nvarchar(1024)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Subtotal")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Tax")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Total")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<long?>("TransactionId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UploadedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TransactionId", "FileHashSha256")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("[TransactionId] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("Receipts");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.ReceiptLineItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Category")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(300)
|
||||||
|
.HasColumnType("nvarchar(300)");
|
||||||
|
|
||||||
|
b.Property<int>("LineNumber")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal?>("LineTotal")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Quantity")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.Property<long>("ReceiptId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("Sku")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<string>("Unit")
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("nvarchar(16)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("UnitPrice")
|
||||||
|
.HasColumnType("decimal(18,4)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ReceiptId", "LineNumber");
|
||||||
|
|
||||||
|
b.ToTable("ReceiptLineItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.ReceiptParseLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime?>("CompletedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Confidence")
|
||||||
|
.HasColumnType("decimal(5,4)");
|
||||||
|
|
||||||
|
b.Property<string>("Error")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ExtractedTextPath")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("nvarchar(1024)");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Provider")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderJobId")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<string>("RawProviderPayloadJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<long>("ReceiptId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime>("StartedAtUtc")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<bool>("Success")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ReceiptId", "StartedAtUtc");
|
||||||
|
|
||||||
|
b.ToTable("ReceiptParseLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("AccountId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<int?>("CardId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Category")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Date")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Last4")
|
||||||
|
.HasMaxLength(4)
|
||||||
|
.HasColumnType("nvarchar(4)");
|
||||||
|
|
||||||
|
b.Property<string>("Memo")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)")
|
||||||
|
.HasDefaultValue("");
|
||||||
|
|
||||||
|
b.Property<int?>("MerchantId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("TransactionType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("nvarchar(20)");
|
||||||
|
|
||||||
|
b.Property<int?>("TransferToAccountId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AccountId");
|
||||||
|
|
||||||
|
b.HasIndex("Amount");
|
||||||
|
|
||||||
|
b.HasIndex("CardId");
|
||||||
|
|
||||||
|
b.HasIndex("Category");
|
||||||
|
|
||||||
|
b.HasIndex("Date");
|
||||||
|
|
||||||
|
b.HasIndex("MerchantId");
|
||||||
|
|
||||||
|
b.HasIndex("TransferToAccountId");
|
||||||
|
|
||||||
|
b.HasIndex("Date", "Amount", "Name", "Memo", "AccountId", "CardId")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("[CardId] IS NOT NULL");
|
||||||
|
|
||||||
|
b.ToTable("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.Transfer", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Date")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)");
|
||||||
|
|
||||||
|
b.Property<int?>("DestinationAccountId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Notes")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<long?>("OriginalTransactionId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<int?>("SourceAccountId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Date");
|
||||||
|
|
||||||
|
b.HasIndex("DestinationAccountId");
|
||||||
|
|
||||||
|
b.HasIndex("OriginalTransactionId");
|
||||||
|
|
||||||
|
b.HasIndex("SourceAccountId");
|
||||||
|
|
||||||
|
b.ToTable("Transfers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Services.CategoryMapping", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Category")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Confidence")
|
||||||
|
.HasColumnType("decimal(5,4)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)");
|
||||||
|
|
||||||
|
b.Property<int?>("MerchantId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Pattern")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
|
b.Property<int>("Priority")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MerchantId");
|
||||||
|
|
||||||
|
b.ToTable("CategoryMappings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.Card", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MoneyMap.Models.Account", "Account")
|
||||||
|
.WithMany("Cards")
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("Account");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.Receipt", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MoneyMap.Models.Transaction", "Transaction")
|
||||||
|
.WithMany("Receipts")
|
||||||
|
.HasForeignKey("TransactionId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.Navigation("Transaction");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.ReceiptLineItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MoneyMap.Models.Receipt", "Receipt")
|
||||||
|
.WithMany("LineItems")
|
||||||
|
.HasForeignKey("ReceiptId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Receipt");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.ReceiptParseLog", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MoneyMap.Models.Receipt", "Receipt")
|
||||||
|
.WithMany("ParseLogs")
|
||||||
|
.HasForeignKey("ReceiptId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Receipt");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MoneyMap.Models.Account", "Account")
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("AccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("MoneyMap.Models.Card", "Card")
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("CardId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("MoneyMap.Models.Merchant", "Merchant")
|
||||||
|
.WithMany("Transactions")
|
||||||
|
.HasForeignKey("MerchantId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("MoneyMap.Models.Account", "TransferToAccount")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("TransferToAccountId");
|
||||||
|
|
||||||
|
b.Navigation("Account");
|
||||||
|
|
||||||
|
b.Navigation("Card");
|
||||||
|
|
||||||
|
b.Navigation("Merchant");
|
||||||
|
|
||||||
|
b.Navigation("TransferToAccount");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.Transfer", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MoneyMap.Models.Account", "DestinationAccount")
|
||||||
|
.WithMany("DestinationTransfers")
|
||||||
|
.HasForeignKey("DestinationAccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("MoneyMap.Models.Transaction", "OriginalTransaction")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OriginalTransactionId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("MoneyMap.Models.Account", "SourceAccount")
|
||||||
|
.WithMany("SourceTransfers")
|
||||||
|
.HasForeignKey("SourceAccountId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("DestinationAccount");
|
||||||
|
|
||||||
|
b.Navigation("OriginalTransaction");
|
||||||
|
|
||||||
|
b.Navigation("SourceAccount");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Services.CategoryMapping", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("MoneyMap.Models.Merchant", "Merchant")
|
||||||
|
.WithMany("CategoryMappings")
|
||||||
|
.HasForeignKey("MerchantId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Merchant");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.Account", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Cards");
|
||||||
|
|
||||||
|
b.Navigation("DestinationTransfers");
|
||||||
|
|
||||||
|
b.Navigation("SourceTransfers");
|
||||||
|
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.Card", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.Merchant", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("CategoryMappings");
|
||||||
|
|
||||||
|
b.Navigation("Transactions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.Receipt", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("LineItems");
|
||||||
|
|
||||||
|
b.Navigation("ParseLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("MoneyMap.Models.Transaction", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Receipts");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
MoneyMap/Migrations/20251012185017_AddReceiptDueDate.cs
Normal file
29
MoneyMap/Migrations/20251012185017_AddReceiptDueDate.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace MoneyMap.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddReceiptDueDate : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "DueDate",
|
||||||
|
table: "Receipts",
|
||||||
|
type: "datetime2",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DueDate",
|
||||||
|
table: "Receipts");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -138,6 +138,9 @@ namespace MoneyMap.Migrations
|
|||||||
.HasMaxLength(8)
|
.HasMaxLength(8)
|
||||||
.HasColumnType("nvarchar(8)");
|
.HasColumnType("nvarchar(8)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("DueDate")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
b.Property<string>("FileHashSha256")
|
b.Property<string>("FileHashSha256")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ public class Receipt
|
|||||||
|
|
||||||
public DateTime? ReceiptDate { get; set; }
|
public DateTime? ReceiptDate { get; set; }
|
||||||
|
|
||||||
|
public DateTime? DueDate { get; set; } // For bills - payment due date
|
||||||
|
|
||||||
[Column(TypeName = "decimal(18,2)")]
|
[Column(TypeName = "decimal(18,2)")]
|
||||||
public decimal? Subtotal { get; set; }
|
public decimal? Subtotal { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -177,7 +177,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="small">
|
<td class="small">
|
||||||
@if (!string.IsNullOrWhiteSpace(r.Merchant) || r.ReceiptDate.HasValue || r.Total.HasValue)
|
@if (!string.IsNullOrWhiteSpace(r.Merchant) || r.ReceiptDate.HasValue || r.DueDate.HasValue || r.Total.HasValue)
|
||||||
{
|
{
|
||||||
<div>
|
<div>
|
||||||
@if (!string.IsNullOrWhiteSpace(r.Merchant))
|
@if (!string.IsNullOrWhiteSpace(r.Merchant))
|
||||||
@@ -188,6 +188,10 @@
|
|||||||
{
|
{
|
||||||
<div>@r.ReceiptDate.Value.ToString("yyyy-MM-dd")</div>
|
<div>@r.ReceiptDate.Value.ToString("yyyy-MM-dd")</div>
|
||||||
}
|
}
|
||||||
|
@if (r.DueDate.HasValue)
|
||||||
|
{
|
||||||
|
<div class="text-warning"><strong>Due:</strong> @r.DueDate.Value.ToString("yyyy-MM-dd")</div>
|
||||||
|
}
|
||||||
@if (r.Total.HasValue)
|
@if (r.Total.HasValue)
|
||||||
{
|
{
|
||||||
<div>@r.Total.Value.ToString("C")</div>
|
<div>@r.Total.Value.ToString("C")</div>
|
||||||
@@ -266,7 +270,7 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-bold">Receipt: @r.FileName</label>
|
<label class="form-label fw-bold">Receipt: @r.FileName</label>
|
||||||
@if (!string.IsNullOrWhiteSpace(r.Merchant) || r.ReceiptDate.HasValue || r.Total.HasValue)
|
@if (!string.IsNullOrWhiteSpace(r.Merchant) || r.ReceiptDate.HasValue || r.DueDate.HasValue || r.Total.HasValue)
|
||||||
{
|
{
|
||||||
<div class="small text-muted bg-light p-2 rounded">
|
<div class="small text-muted bg-light p-2 rounded">
|
||||||
@if (!string.IsNullOrWhiteSpace(r.Merchant))
|
@if (!string.IsNullOrWhiteSpace(r.Merchant))
|
||||||
@@ -277,6 +281,10 @@
|
|||||||
{
|
{
|
||||||
<div><strong>Date:</strong> @r.ReceiptDate.Value.ToString("yyyy-MM-dd")</div>
|
<div><strong>Date:</strong> @r.ReceiptDate.Value.ToString("yyyy-MM-dd")</div>
|
||||||
}
|
}
|
||||||
|
@if (r.DueDate.HasValue)
|
||||||
|
{
|
||||||
|
<div><strong>Due Date:</strong> @r.DueDate.Value.ToString("yyyy-MM-dd")</div>
|
||||||
|
}
|
||||||
@if (r.Total.HasValue)
|
@if (r.Total.HasValue)
|
||||||
{
|
{
|
||||||
<div><strong>Total:</strong> @r.Total.Value.ToString("C")</div>
|
<div><strong>Total:</strong> @r.Total.Value.ToString("C")</div>
|
||||||
@@ -294,7 +302,11 @@
|
|||||||
@if (matches.Any())
|
@if (matches.Any())
|
||||||
{
|
{
|
||||||
<div class="form-text mb-2">
|
<div class="form-text mb-2">
|
||||||
@if (r.ReceiptDate.HasValue)
|
@if (r.ReceiptDate.HasValue && r.DueDate.HasValue)
|
||||||
|
{
|
||||||
|
<span>Showing transactions between bill date (@r.ReceiptDate.Value.ToString("yyyy-MM-dd")) and due date (@r.DueDate.Value.ToString("yyyy-MM-dd")).</span>
|
||||||
|
}
|
||||||
|
else if (r.ReceiptDate.HasValue)
|
||||||
{
|
{
|
||||||
<span>Showing transactions within ±3 days of receipt date (@r.ReceiptDate.Value.ToString("yyyy-MM-dd")).</span>
|
<span>Showing transactions within ±3 days of receipt date (@r.ReceiptDate.Value.ToString("yyyy-MM-dd")).</span>
|
||||||
}
|
}
|
||||||
@@ -345,7 +357,11 @@
|
|||||||
{
|
{
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
No matching transactions found.
|
No matching transactions found.
|
||||||
@if (r.ReceiptDate.HasValue)
|
@if (r.ReceiptDate.HasValue && r.DueDate.HasValue)
|
||||||
|
{
|
||||||
|
<span>Try searching between @r.ReceiptDate.Value.ToString("yyyy-MM-dd") and @r.DueDate.Value.ToString("yyyy-MM-dd").</span>
|
||||||
|
}
|
||||||
|
else if (r.ReceiptDate.HasValue)
|
||||||
{
|
{
|
||||||
<span>Try searching within ±3 days of @r.ReceiptDate.Value.ToString("yyyy-MM-dd").</span>
|
<span>Try searching within ±3 days of @r.ReceiptDate.Value.ToString("yyyy-MM-dd").</span>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ namespace MoneyMap.Pages
|
|||||||
TransactionAmount = r.Transaction?.Amount,
|
TransactionAmount = r.Transaction?.Amount,
|
||||||
Merchant = r.Merchant,
|
Merchant = r.Merchant,
|
||||||
ReceiptDate = r.ReceiptDate,
|
ReceiptDate = r.ReceiptDate,
|
||||||
|
DueDate = r.DueDate,
|
||||||
Total = r.Total,
|
Total = r.Total,
|
||||||
StoragePath = r.StoragePath
|
StoragePath = r.StoragePath
|
||||||
}).ToList();
|
}).ToList();
|
||||||
@@ -232,9 +233,17 @@ namespace MoneyMap.Pages
|
|||||||
.Where(t => !transactionsWithReceipts.Contains(t.Id))
|
.Where(t => !transactionsWithReceipts.Contains(t.Id))
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
|
|
||||||
// If receipt has a date, filter by +/- 3 days (this is the primary filter)
|
// If receipt has a date, filter by date range
|
||||||
if (receipt.ReceiptDate.HasValue)
|
if (receipt.ReceiptDate.HasValue && receipt.DueDate.HasValue)
|
||||||
{
|
{
|
||||||
|
// For bills with due dates: use range from bill date to due date
|
||||||
|
var minDate = receipt.ReceiptDate.Value;
|
||||||
|
var maxDate = receipt.DueDate.Value;
|
||||||
|
query = query.Where(t => t.Date >= minDate && t.Date <= maxDate);
|
||||||
|
}
|
||||||
|
else if (receipt.ReceiptDate.HasValue)
|
||||||
|
{
|
||||||
|
// For regular receipts: use +/- 3 days (this is the primary filter)
|
||||||
var minDate = receipt.ReceiptDate.Value.AddDays(-3);
|
var minDate = receipt.ReceiptDate.Value.AddDays(-3);
|
||||||
var maxDate = receipt.ReceiptDate.Value.AddDays(3);
|
var maxDate = receipt.ReceiptDate.Value.AddDays(3);
|
||||||
query = query.Where(t => t.Date >= minDate && t.Date <= maxDate);
|
query = query.Where(t => t.Date >= minDate && t.Date <= maxDate);
|
||||||
@@ -350,6 +359,7 @@ namespace MoneyMap.Pages
|
|||||||
public decimal? TransactionAmount { get; set; }
|
public decimal? TransactionAmount { get; set; }
|
||||||
public string? Merchant { get; set; }
|
public string? Merchant { get; set; }
|
||||||
public DateTime? ReceiptDate { get; set; }
|
public DateTime? ReceiptDate { get; set; }
|
||||||
|
public DateTime? DueDate { get; set; }
|
||||||
public decimal? Total { get; set; }
|
public decimal? Total { get; set; }
|
||||||
public string StoragePath { get; set; } = "";
|
public string StoragePath { get; set; } = "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ namespace MoneyMap.Services
|
|||||||
receipt.Subtotal = parseData.Subtotal;
|
receipt.Subtotal = parseData.Subtotal;
|
||||||
receipt.Tax = parseData.Tax;
|
receipt.Tax = parseData.Tax;
|
||||||
receipt.ReceiptDate = parseData.ReceiptDate;
|
receipt.ReceiptDate = parseData.ReceiptDate;
|
||||||
|
receipt.DueDate = parseData.DueDate;
|
||||||
|
|
||||||
// Update transaction merchant if we extracted one and transaction doesn't have one yet
|
// Update transaction merchant if we extracted one and transaction doesn't have one yet
|
||||||
if (receipt.Transaction != null &&
|
if (receipt.Transaction != null &&
|
||||||
@@ -211,6 +212,7 @@ namespace MoneyMap.Services
|
|||||||
{
|
{
|
||||||
""merchant"": ""store name"",
|
""merchant"": ""store name"",
|
||||||
""receiptDate"": ""YYYY-MM-DD"" (or null if not found),
|
""receiptDate"": ""YYYY-MM-DD"" (or null if not found),
|
||||||
|
""dueDate"": ""YYYY-MM-DD"" (or null if not found - for bills only),
|
||||||
""subtotal"": 0.00 (or null if not found),
|
""subtotal"": 0.00 (or null if not found),
|
||||||
""tax"": 0.00 (or null if not found),
|
""tax"": 0.00 (or null if not found),
|
||||||
""total"": 0.00,
|
""total"": 0.00,
|
||||||
@@ -231,7 +233,9 @@ Extract all line items you can see on the receipt. For each item:
|
|||||||
- unitPrice: Price per unit if quantity applies, otherwise null
|
- unitPrice: Price per unit if quantity applies, otherwise null
|
||||||
- lineTotal: The total amount for this line (required)
|
- lineTotal: The total amount for this line (required)
|
||||||
|
|
||||||
For utility bills, service charges, fees, and taxes - these are NOT products with quantities, so set quantity and unitPrice to null.";
|
For utility bills, service charges, fees, and taxes - these are NOT products with quantities, so set quantity and unitPrice to null.
|
||||||
|
|
||||||
|
If this is a bill (utility, credit card, etc.), look for a due date, payment due date, or deadline and extract it as dueDate. For regular receipts, dueDate should be null.";
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(transactionName))
|
if (!string.IsNullOrWhiteSpace(transactionName))
|
||||||
{
|
{
|
||||||
@@ -313,6 +317,7 @@ For utility bills, service charges, fees, and taxes - these are NOT products wit
|
|||||||
{
|
{
|
||||||
public string? Merchant { get; set; }
|
public string? Merchant { get; set; }
|
||||||
public DateTime? ReceiptDate { get; set; }
|
public DateTime? ReceiptDate { get; set; }
|
||||||
|
public DateTime? DueDate { get; set; }
|
||||||
public decimal? Subtotal { get; set; }
|
public decimal? Subtotal { get; set; }
|
||||||
public decimal? Tax { get; set; }
|
public decimal? Tax { get; set; }
|
||||||
public decimal? Total { get; set; }
|
public decimal? Total { get; set; }
|
||||||
|
|||||||
@@ -96,10 +96,17 @@ namespace MoneyMap.Services
|
|||||||
.Include(t => t.Merchant)
|
.Include(t => t.Merchant)
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
|
|
||||||
// Start with date range filter (if we have a receipt date)
|
// Start with date range filter
|
||||||
if (receipt.ReceiptDate.HasValue)
|
if (receipt.ReceiptDate.HasValue && receipt.DueDate.HasValue)
|
||||||
{
|
{
|
||||||
// Allow +/- 3 days for transaction date to account for processing delays
|
// For bills with due dates: use range from bill date to due date
|
||||||
|
var minDate = receipt.ReceiptDate.Value;
|
||||||
|
var maxDate = receipt.DueDate.Value;
|
||||||
|
query = query.Where(t => t.Date >= minDate && t.Date <= maxDate);
|
||||||
|
}
|
||||||
|
else if (receipt.ReceiptDate.HasValue)
|
||||||
|
{
|
||||||
|
// For regular receipts: allow +/- 3 days for transaction date to account for processing delays
|
||||||
var minDate = receipt.ReceiptDate.Value.AddDays(-3);
|
var minDate = receipt.ReceiptDate.Value.AddDays(-3);
|
||||||
var maxDate = receipt.ReceiptDate.Value.AddDays(3);
|
var maxDate = receipt.ReceiptDate.Value.AddDays(3);
|
||||||
query = query.Where(t => t.Date >= minDate && t.Date <= maxDate);
|
query = query.Where(t => t.Date >= minDate && t.Date <= maxDate);
|
||||||
|
|||||||
Reference in New Issue
Block a user