Files
telegram-management-system/marketing-agent/services/billing-service/src/models/Invoice.js
你的用户名 237c7802e5
Some checks failed
Deploy / deploy (push) Has been cancelled
Initial commit: Telegram Management System
Full-stack web application for Telegram management
- Frontend: Vue 3 + Vben Admin
- Backend: NestJS
- Features: User management, group broadcast, statistics

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 15:37:50 +08:00

257 lines
5.3 KiB
JavaScript

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