312 lines
7.7 KiB
TypeScript
312 lines
7.7 KiB
TypeScript
import mongoose, { Schema, Document } from 'mongoose'
|
|
import { z } from 'zod'
|
|
|
|
// Zod schema for validation
|
|
export const BillingSchema = z.object({
|
|
billing_id: z.string().min(1, 'Billing ID is required'),
|
|
service: z.string().min(1, 'Service name is required'),
|
|
service_type: z.enum([
|
|
'vps',
|
|
'kubernetes',
|
|
'developer_hire',
|
|
'vpn',
|
|
'hosting',
|
|
'storage',
|
|
'database',
|
|
'ai_service',
|
|
'custom',
|
|
]),
|
|
amount: z.number().min(0, 'Amount must be non-negative'),
|
|
currency: z.enum(['INR', 'USD']).default('INR'),
|
|
user_email: z.string().email('Invalid email format'),
|
|
silicon_id: z.string().min(1, 'Silicon ID is required'),
|
|
user_id: z.string().min(1, 'User ID is required'),
|
|
status: z.enum(['pending', 'active', 'completed', 'cancelled', 'failed', 'refunded']),
|
|
payment_status: z.enum(['pending', 'paid', 'failed', 'refunded']).default('pending'),
|
|
cycle: z.enum(['onetime', 'hourly', 'daily', 'weekly', 'monthly', 'quarterly', 'yearly']),
|
|
service_id: z.string().optional(),
|
|
cluster_id: z.string().optional(),
|
|
instance_id: z.string().optional(),
|
|
service_name: z.string().optional(),
|
|
service_config: z.record(z.string(), z.any()).optional(),
|
|
billing_period_start: z.date().optional(),
|
|
billing_period_end: z.date().optional(),
|
|
next_billing_date: z.date().optional(),
|
|
auto_renew: z.boolean().default(false),
|
|
discount_applied: z.number().min(0).default(0),
|
|
tax_amount: z.number().min(0).default(0),
|
|
total_amount: z.number().min(0),
|
|
transaction_id: z.string().optional(),
|
|
invoice_id: z.string().optional(),
|
|
remarks: z.string().optional(),
|
|
metadata: z.record(z.string(), z.any()).optional(),
|
|
service_status: z.number().min(0).max(2).default(0), // 0: inactive, 1: active, 2: suspended
|
|
created_by: z.string().optional(),
|
|
updated_by: z.string().optional(),
|
|
})
|
|
|
|
export type BillingType = z.infer<typeof BillingSchema>
|
|
|
|
// Mongoose interface
|
|
export interface IBilling extends Document {
|
|
billing_id: string
|
|
service: string
|
|
service_type:
|
|
| 'vps'
|
|
| 'kubernetes'
|
|
| 'developer_hire'
|
|
| 'vpn'
|
|
| 'hosting'
|
|
| 'storage'
|
|
| 'database'
|
|
| 'ai_service'
|
|
| 'custom'
|
|
amount: number
|
|
currency: 'INR' | 'USD'
|
|
user_email: string
|
|
silicon_id: string
|
|
user_id: string
|
|
status: 'pending' | 'active' | 'completed' | 'cancelled' | 'failed' | 'refunded'
|
|
payment_status: 'pending' | 'paid' | 'failed' | 'refunded'
|
|
cycle: 'onetime' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly'
|
|
service_id?: string
|
|
cluster_id?: string
|
|
instance_id?: string
|
|
service_name?: string
|
|
service_config?: Record<string, any>
|
|
billing_period_start?: Date
|
|
billing_period_end?: Date
|
|
next_billing_date?: Date
|
|
auto_renew: boolean
|
|
discount_applied: number
|
|
tax_amount: number
|
|
total_amount: number
|
|
transaction_id?: string
|
|
invoice_id?: string
|
|
remarks?: string
|
|
metadata?: Record<string, any>
|
|
service_status: number
|
|
created_by?: string
|
|
updated_by?: string
|
|
createdAt: Date
|
|
updatedAt: Date
|
|
}
|
|
|
|
const billingSchema: Schema = new Schema<IBilling>(
|
|
{
|
|
billing_id: {
|
|
type: String,
|
|
required: true,
|
|
unique: true,
|
|
index: true,
|
|
},
|
|
service: {
|
|
type: String,
|
|
required: true,
|
|
trim: true,
|
|
},
|
|
service_type: {
|
|
type: String,
|
|
enum: [
|
|
'vps',
|
|
'kubernetes',
|
|
'developer_hire',
|
|
'vpn',
|
|
'hosting',
|
|
'storage',
|
|
'database',
|
|
'ai_service',
|
|
'custom',
|
|
],
|
|
required: true,
|
|
},
|
|
amount: {
|
|
type: Number,
|
|
required: true,
|
|
min: 0,
|
|
},
|
|
currency: {
|
|
type: String,
|
|
enum: ['INR', 'USD'],
|
|
default: 'INR',
|
|
},
|
|
user_email: {
|
|
type: String,
|
|
required: true,
|
|
lowercase: true,
|
|
trim: true,
|
|
index: true,
|
|
},
|
|
silicon_id: {
|
|
type: String,
|
|
required: true,
|
|
index: true,
|
|
},
|
|
user_id: {
|
|
type: String,
|
|
required: true,
|
|
index: true,
|
|
},
|
|
status: {
|
|
type: String,
|
|
enum: ['pending', 'active', 'completed', 'cancelled', 'failed', 'refunded'],
|
|
default: 'pending',
|
|
},
|
|
payment_status: {
|
|
type: String,
|
|
enum: ['pending', 'paid', 'failed', 'refunded'],
|
|
default: 'pending',
|
|
},
|
|
cycle: {
|
|
type: String,
|
|
enum: ['onetime', 'hourly', 'daily', 'weekly', 'monthly', 'quarterly', 'yearly'],
|
|
required: true,
|
|
},
|
|
service_id: {
|
|
type: String,
|
|
trim: true,
|
|
},
|
|
cluster_id: {
|
|
type: String,
|
|
trim: true,
|
|
},
|
|
instance_id: {
|
|
type: String,
|
|
trim: true,
|
|
},
|
|
service_name: {
|
|
type: String,
|
|
trim: true,
|
|
},
|
|
service_config: {
|
|
type: mongoose.Schema.Types.Mixed,
|
|
},
|
|
billing_period_start: {
|
|
type: Date,
|
|
},
|
|
billing_period_end: {
|
|
type: Date,
|
|
},
|
|
next_billing_date: {
|
|
type: Date,
|
|
},
|
|
auto_renew: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
discount_applied: {
|
|
type: Number,
|
|
min: 0,
|
|
default: 0,
|
|
},
|
|
tax_amount: {
|
|
type: Number,
|
|
min: 0,
|
|
default: 0,
|
|
},
|
|
total_amount: {
|
|
type: Number,
|
|
required: true,
|
|
min: 0,
|
|
},
|
|
transaction_id: {
|
|
type: String,
|
|
trim: true,
|
|
},
|
|
invoice_id: {
|
|
type: String,
|
|
trim: true,
|
|
},
|
|
remarks: {
|
|
type: String,
|
|
trim: true,
|
|
},
|
|
metadata: {
|
|
type: mongoose.Schema.Types.Mixed,
|
|
},
|
|
service_status: {
|
|
type: Number,
|
|
min: 0,
|
|
max: 2,
|
|
default: 0,
|
|
},
|
|
created_by: {
|
|
type: String,
|
|
trim: true,
|
|
},
|
|
updated_by: {
|
|
type: String,
|
|
trim: true,
|
|
},
|
|
},
|
|
{
|
|
timestamps: true,
|
|
}
|
|
)
|
|
|
|
// Indexes for performance
|
|
billingSchema.index({ user_email: 1, createdAt: -1 })
|
|
billingSchema.index({ silicon_id: 1, createdAt: -1 })
|
|
billingSchema.index({ user_id: 1, createdAt: -1 })
|
|
billingSchema.index({ service_type: 1, status: 1 })
|
|
billingSchema.index({ payment_status: 1 })
|
|
billingSchema.index({ next_billing_date: 1, auto_renew: 1 })
|
|
billingSchema.index({ service_id: 1 })
|
|
billingSchema.index({ cluster_id: 1 })
|
|
billingSchema.index({ instance_id: 1 })
|
|
|
|
// Pre-save middleware to calculate total_amount
|
|
billingSchema.pre('save', function (next) {
|
|
if (
|
|
this.isModified('amount') ||
|
|
this.isModified('tax_amount') ||
|
|
this.isModified('discount_applied')
|
|
) {
|
|
const amount = Number(this.amount) || 0
|
|
const taxAmount = Number(this.tax_amount) || 0
|
|
const discountApplied = Number(this.discount_applied) || 0
|
|
this.total_amount = amount + taxAmount - discountApplied
|
|
}
|
|
next()
|
|
})
|
|
|
|
// Transform method to format response
|
|
billingSchema.methods.toJSON = function () {
|
|
const billing = this.toObject()
|
|
return billing
|
|
}
|
|
|
|
// Static methods for common queries
|
|
billingSchema.statics.findByUser = function (userEmail: string, siliconId: string) {
|
|
return this.find({
|
|
$or: [{ user_email: userEmail }, { silicon_id: siliconId }],
|
|
}).sort({ createdAt: -1 })
|
|
}
|
|
|
|
billingSchema.statics.findActiveServices = function (userEmail: string, siliconId: string) {
|
|
return this.find({
|
|
$or: [{ user_email: userEmail }, { silicon_id: siliconId }],
|
|
status: 'active',
|
|
service_status: 1,
|
|
}).sort({ createdAt: -1 })
|
|
}
|
|
|
|
billingSchema.statics.findByServiceType = function (
|
|
serviceType: string,
|
|
userEmail?: string,
|
|
siliconId?: string
|
|
) {
|
|
const query: any = { service_type: serviceType }
|
|
if (userEmail || siliconId) {
|
|
query.$or = []
|
|
if (userEmail) query.$or.push({ user_email: userEmail })
|
|
if (siliconId) query.$or.push({ silicon_id: siliconId })
|
|
}
|
|
return this.find(query).sort({ createdAt: -1 })
|
|
}
|
|
|
|
// Check if model already exists before compiling it
|
|
export const Billing =
|
|
(mongoose.models?.Billing as mongoose.Model<IBilling>) ||
|
|
mongoose.model<IBilling>('Billing', billingSchema)
|