Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
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>
This commit is contained in:
257
marketing-agent/services/billing-service/src/models/Invoice.js
Normal file
257
marketing-agent/services/billing-service/src/models/Invoice.js
Normal file
@@ -0,0 +1,257 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user