etsy-finance-tracker/server/src/models/Expense.ts
dlawler489 99068d6710 Fix Australia Post CSV duplicate imports with comprehensive duplicate prevention
Multi-layer duplicate prevention system:
- Enhanced frontend duplicate detection with tracking number, amount, and date comparison
- Added MongoDB compound index to prevent database-level duplicates
- Improved backend error handling for duplicate key violations
- Added cleanup endpoint to remove existing duplicates
- Enhanced user feedback for import operations

Frontend changes:
- Stricter duplicate detection comparing tracking number, vendor, amount, and date
- Better error handling and user feedback for duplicate scenarios
- Added 'Clean Duplicates' button to remove existing duplicates

Backend changes:
- Database compound index on reference, vendor, userId, amount, date
- Enhanced error responses with duplicate detection flags
- New POST /expenses/cleanup-duplicates endpoint
- Improved duplicate key error handling

This should eliminate the double Australia Post expense entries.
2026-05-05 13:40:28 +10:00

53 lines
1.4 KiB
TypeScript

import mongoose, { Document, Schema } from 'mongoose';
export interface IExpense extends Document {
category: string;
description: string;
amount: number;
date: Date;
taxDeductible: boolean;
vendor?: string;
reference?: string;
receiptUrl?: string;
notes?: string;
userId: mongoose.Types.ObjectId;
dateCreated: Date;
dateUpdated: Date;
}
const ExpenseSchema: Schema = new Schema({
category: { type: String, required: true },
description: { type: String, required: true, trim: true },
amount: { type: Number, required: true, min: 0 },
date: { type: Date, required: true },
taxDeductible: { type: Boolean, default: false },
vendor: { type: String },
reference: { type: String },
receiptUrl: { type: String },
notes: { type: String },
userId: { type: Schema.Types.ObjectId, ref: 'User', required: true, index: true },
dateCreated: { type: Date, default: Date.now },
dateUpdated: { type: Date, default: Date.now },
});
ExpenseSchema.pre('save', function (next) {
this.dateUpdated = new Date();
next();
});
// Create compound index to prevent duplicate expenses with same reference, vendor, and user
ExpenseSchema.index({
reference: 1,
vendor: 1,
userId: 1,
amount: 1,
date: 1
}, {
unique: true,
partialFilterExpression: {
reference: { $exists: true, $ne: null },
vendor: { $exists: true, $ne: null }
}
});
export default mongoose.model<IExpense>('Expense', ExpenseSchema);