import mongoose from 'mongoose'; const invoiceSchema = new mongoose.Schema({ // Multi-tenant support tenantId: { type: mongoose.Schema.Types.ObjectId, ref: 'Tenant', required: true, index: true }, // Invoice details invoiceNumber: { type: String, required: true, unique: true }, subscriptionId: { type: mongoose.Schema.Types.ObjectId, ref: 'Subscription', required: true }, customerId: { type: String, required: true, index: true }, // Billing period periodStart: { type: Date, required: true }, periodEnd: { type: Date, required: true }, // Status status: { type: String, enum: ['draft', 'open', 'paid', 'void', 'uncollectible'], default: 'draft', index: true }, // Line items lineItems: [{ description: String, quantity: Number, unitPrice: Number, amount: Number, currency: String, period: { start: Date, end: Date }, proration: Boolean, type: { type: String, enum: ['subscription', 'usage', 'one_time', 'discount'] } }], // Amounts subtotal: { type: Number, required: true }, tax: { rate: Number, amount: Number }, discount: { coupon: String, amount: Number }, total: { type: Number, required: true }, currency: { type: String, default: 'USD' }, // Payment amountPaid: { type: Number, default: 0 }, amountDue: { type: Number, required: true }, attemptCount: { type: Number, default: 0 }, // Dates dueDate: Date, paidAt: Date, voidedAt: Date, // Payment details paymentMethod: { type: String, last4: String, brand: String }, paymentIntent: String, // Customer details (snapshot at invoice time) customer: { name: String, email: String, phone: String, address: { line1: String, line2: String, city: String, state: String, postalCode: String, country: String } }, // PDF pdfUrl: String, // Metadata metadata: mongoose.Schema.Types.Mixed, // Stripe specific stripeInvoiceId: String, stripePaymentIntentId: String }, { timestamps: true }); // Indexes invoiceSchema.index({ tenantId: 1, status: 1 }); invoiceSchema.index({ tenantId: 1, periodStart: -1 }); invoiceSchema.index({ tenantId: 1, customerId: 1 }); invoiceSchema.index({ tenantId: 1, invoiceNumber: 1 }, { unique: true }); // Virtual for is overdue invoiceSchema.virtual('isOverdue').get(function() { if (this.status !== 'open') return false; if (!this.dueDate) return false; return new Date() > this.dueDate; }); // Virtual for days overdue invoiceSchema.virtual('daysOverdue').get(function() { if (!this.isOverdue) return 0; const now = new Date(); const due = new Date(this.dueDate); return Math.floor((now - due) / (1000 * 60 * 60 * 24)); }); // Methods invoiceSchema.methods.addLineItem = function(item) { this.lineItems.push(item); this.calculateTotals(); }; invoiceSchema.methods.calculateTotals = function() { // Calculate subtotal this.subtotal = this.lineItems.reduce((sum, item) => { if (item.type === 'discount') { return sum - Math.abs(item.amount); } return sum + item.amount; }, 0); // Apply tax if (this.tax && this.tax.rate) { this.tax.amount = this.subtotal * (this.tax.rate / 100); } // Calculate total this.total = this.subtotal + (this.tax?.amount || 0); this.amountDue = this.total - this.amountPaid; }; invoiceSchema.methods.markPaid = async function(paymentDetails) { this.status = 'paid'; this.paidAt = new Date(); this.amountPaid = this.total; this.amountDue = 0; if (paymentDetails) { this.paymentMethod = paymentDetails.paymentMethod; this.paymentIntent = paymentDetails.paymentIntent; } await this.save(); }; invoiceSchema.methods.void = async function(reason) { this.status = 'void'; this.voidedAt = new Date(); if (reason) { this.metadata = this.metadata || {}; this.metadata.voidReason = reason; } await this.save(); }; invoiceSchema.methods.generatePDF = async function() { // This would integrate with a PDF generation service // For now, just return a placeholder this.pdfUrl = `/invoices/${this.invoiceNumber}.pdf`; await this.save(); return this.pdfUrl; }; // Statics invoiceSchema.statics.generateInvoiceNumber = async function(tenantId) { const date = new Date(); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); // Find the last invoice for this tenant in the current month const lastInvoice = await this.findOne({ tenantId, invoiceNumber: new RegExp(`^INV-${year}${month}`) }).sort({ invoiceNumber: -1 }); let sequence = 1; if (lastInvoice) { const lastSequence = parseInt(lastInvoice.invoiceNumber.split('-').pop()); sequence = lastSequence + 1; } return `INV-${year}${month}-${String(sequence).padStart(4, '0')}`; }; invoiceSchema.statics.findUnpaid = function(tenantId) { return this.find({ tenantId, status: 'open', amountDue: { $gt: 0 } }).sort({ dueDate: 1 }); }; invoiceSchema.statics.findOverdue = function(tenantId) { return this.find({ tenantId, status: 'open', dueDate: { $lt: new Date() } }).sort({ dueDate: 1 }); }; export const Invoice = mongoose.model('Invoice', invoiceSchema);