376 lines
11 KiB
TypeScript
376 lines
11 KiB
TypeScript
import { Billing, IBilling, BillingSchema } from '@/models/billing'
|
|
import { Transaction } from '@/models/transaction'
|
|
import { User as UserModel } from '@/models/user'
|
|
import connectDB from '@/lib/mongodb'
|
|
|
|
// Utility function to generate unique billing ID
|
|
function toBase62(num: number): string {
|
|
const base62 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
|
let encoded = ''
|
|
let n = num
|
|
while (n > 0) {
|
|
encoded = base62[n % 62] + encoded
|
|
n = Math.floor(n / 62)
|
|
}
|
|
return encoded
|
|
}
|
|
|
|
export function generateBillingId(): string {
|
|
const epochStart = new Date('2020-01-01').getTime()
|
|
const now = Date.now()
|
|
const timestamp = (now - epochStart) % 1000000000000
|
|
const randomNum = Math.floor(Math.random() * 100)
|
|
const finalNum = timestamp * 100 + randomNum
|
|
return `bill_${toBase62(finalNum).padStart(8, '0')}`
|
|
}
|
|
|
|
export function generateTransactionId(serviceType: string): string {
|
|
const timestamp = Date.now()
|
|
const randomStr = Math.random().toString(36).substr(2, 9)
|
|
return `txn_${serviceType}_${timestamp}_${randomStr}`
|
|
}
|
|
|
|
// Interface for creating billing records
|
|
export interface CreateBillingParams {
|
|
user: {
|
|
id: string
|
|
email: string
|
|
siliconId?: string
|
|
}
|
|
service: {
|
|
name: string
|
|
type:
|
|
| 'vps'
|
|
| 'kubernetes'
|
|
| 'developer_hire'
|
|
| 'vpn'
|
|
| 'hosting'
|
|
| 'storage'
|
|
| 'database'
|
|
| 'ai_service'
|
|
| 'custom'
|
|
id?: string
|
|
clusterId?: string
|
|
instanceId?: string
|
|
config?: Record<string, any>
|
|
}
|
|
billing: {
|
|
amount: number
|
|
currency?: 'INR' | 'USD'
|
|
cycle: 'onetime' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly'
|
|
discountApplied?: number
|
|
taxAmount?: number
|
|
autoRenew?: boolean
|
|
billingPeriodStart?: Date
|
|
billingPeriodEnd?: Date
|
|
nextBillingDate?: Date
|
|
}
|
|
payment: {
|
|
transactionId?: string
|
|
status: 'pending' | 'paid' | 'failed' | 'refunded'
|
|
}
|
|
status: {
|
|
service: 'pending' | 'active' | 'completed' | 'cancelled' | 'failed' | 'refunded'
|
|
serviceStatus: 0 | 1 | 2 // 0: inactive, 1: active, 2: suspended
|
|
}
|
|
metadata?: {
|
|
remarks?: string
|
|
deploymentSuccess?: boolean
|
|
deploymentResponse?: any
|
|
userAgent?: string
|
|
ip?: string
|
|
[key: string]: any
|
|
}
|
|
}
|
|
|
|
export class BillingService {
|
|
/**
|
|
* Create a comprehensive billing record for any service
|
|
*/
|
|
static async createBillingRecord(params: CreateBillingParams): Promise<IBilling> {
|
|
await connectDB()
|
|
|
|
const billingId = generateBillingId()
|
|
const totalAmount =
|
|
params.billing.amount +
|
|
(params.billing.taxAmount || 0) -
|
|
(params.billing.discountApplied || 0)
|
|
|
|
// Validate the billing data
|
|
const billingData = {
|
|
billing_id: billingId,
|
|
service: params.service.name,
|
|
service_type: params.service.type,
|
|
amount: params.billing.amount,
|
|
currency: params.billing.currency || 'INR',
|
|
user_email: params.user.email,
|
|
silicon_id: params.user.siliconId || params.user.id,
|
|
user_id: params.user.id,
|
|
status: params.status.service,
|
|
payment_status: params.payment.status,
|
|
cycle: params.billing.cycle,
|
|
service_id: params.service.id,
|
|
cluster_id: params.service.clusterId,
|
|
instance_id: params.service.instanceId,
|
|
service_name: params.service.name,
|
|
service_config: params.service.config,
|
|
billing_period_start: params.billing.billingPeriodStart,
|
|
billing_period_end: params.billing.billingPeriodEnd,
|
|
next_billing_date: params.billing.nextBillingDate,
|
|
auto_renew: params.billing.autoRenew || false,
|
|
discount_applied: params.billing.discountApplied || 0,
|
|
tax_amount: params.billing.taxAmount || 0,
|
|
total_amount: totalAmount,
|
|
transaction_id: params.payment.transactionId,
|
|
remarks: params.metadata?.remarks,
|
|
metadata: params.metadata,
|
|
service_status: params.status.serviceStatus,
|
|
created_by: params.user.email,
|
|
}
|
|
|
|
// Create billing record directly without Zod validation to avoid field conflicts
|
|
// The Mongoose schema will handle validation
|
|
|
|
const newBilling = new Billing(billingData)
|
|
return await newBilling.save()
|
|
}
|
|
|
|
/**
|
|
* Update billing record status
|
|
*/
|
|
static async updateBillingStatus(
|
|
billingId: string,
|
|
updates: {
|
|
status?: 'pending' | 'active' | 'completed' | 'cancelled' | 'failed' | 'refunded'
|
|
paymentStatus?: 'pending' | 'paid' | 'failed' | 'refunded'
|
|
serviceStatus?: 0 | 1 | 2
|
|
remarks?: string
|
|
updatedBy?: string
|
|
}
|
|
): Promise<IBilling | null> {
|
|
await connectDB()
|
|
|
|
const updateData: any = {}
|
|
if (updates.status) updateData.status = updates.status
|
|
if (updates.paymentStatus) updateData.payment_status = updates.paymentStatus
|
|
if (updates.serviceStatus !== undefined) updateData.service_status = updates.serviceStatus
|
|
if (updates.remarks) updateData.remarks = updates.remarks
|
|
if (updates.updatedBy) updateData.updated_by = updates.updatedBy
|
|
|
|
return await Billing.findOneAndUpdate(
|
|
{ billing_id: billingId },
|
|
{ $set: updateData },
|
|
{ new: true }
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Get billing records for a user
|
|
*/
|
|
static async getUserBillings(
|
|
userEmail: string,
|
|
siliconId: string,
|
|
options?: {
|
|
serviceType?: string
|
|
status?: string
|
|
limit?: number
|
|
offset?: number
|
|
}
|
|
): Promise<IBilling[]> {
|
|
await connectDB()
|
|
|
|
const query: any = {
|
|
$or: [{ user_email: userEmail }, { silicon_id: siliconId }],
|
|
}
|
|
|
|
if (options?.serviceType) {
|
|
query.service_type = options.serviceType
|
|
}
|
|
|
|
if (options?.status) {
|
|
query.status = options.status
|
|
}
|
|
|
|
let billingQuery = Billing.find(query).sort({ createdAt: -1 })
|
|
|
|
if (options?.limit) {
|
|
billingQuery = billingQuery.limit(options.limit)
|
|
}
|
|
|
|
if (options?.offset) {
|
|
billingQuery = billingQuery.skip(options.offset)
|
|
}
|
|
|
|
return await billingQuery.exec()
|
|
}
|
|
|
|
/**
|
|
* Get active services for a user
|
|
*/
|
|
static async getActiveServices(userEmail: string, siliconId: string): Promise<IBilling[]> {
|
|
await connectDB()
|
|
return await (Billing as any).findActiveServices(userEmail, siliconId)
|
|
}
|
|
|
|
/**
|
|
* Process service deployment billing (deduct balance + create billing record)
|
|
*/
|
|
static async processServiceDeployment(params: {
|
|
user: { id: string; email: string; siliconId?: string }
|
|
service: {
|
|
name: string
|
|
type:
|
|
| 'vps'
|
|
| 'kubernetes'
|
|
| 'developer_hire'
|
|
| 'vpn'
|
|
| 'hosting'
|
|
| 'storage'
|
|
| 'database'
|
|
| 'ai_service'
|
|
| 'custom'
|
|
id?: string
|
|
clusterId?: string
|
|
instanceId?: string
|
|
config?: Record<string, any>
|
|
}
|
|
amount: number
|
|
currency?: 'INR' | 'USD'
|
|
cycle?: 'onetime' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly'
|
|
deploymentSuccess: boolean
|
|
deploymentResponse?: any
|
|
metadata?: Record<string, any>
|
|
}): Promise<{
|
|
billing: IBilling
|
|
transaction?: any
|
|
balanceUpdated: boolean
|
|
}> {
|
|
await connectDB()
|
|
|
|
// Get user data and check balance
|
|
const userData = await UserModel.findOne({ email: params.user.email })
|
|
if (!userData) {
|
|
throw new Error('User not found')
|
|
}
|
|
|
|
const currentBalance = userData.balance || 0
|
|
let balanceUpdated = false
|
|
let transactionRecord = null
|
|
|
|
// Deduct balance if deployment was successful and amount > 0
|
|
if (params.deploymentSuccess && params.amount > 0) {
|
|
if (currentBalance < params.amount) {
|
|
throw new Error(
|
|
`Insufficient balance. Required: ₹${params.amount}, Available: ₹${currentBalance}`
|
|
)
|
|
}
|
|
|
|
const newBalance = currentBalance - params.amount
|
|
await UserModel.updateOne({ email: params.user.email }, { $set: { balance: newBalance } })
|
|
balanceUpdated = true
|
|
|
|
// Create transaction record
|
|
const transactionId = generateTransactionId(params.service.type)
|
|
try {
|
|
const newTransaction = new Transaction({
|
|
transactionId,
|
|
userId: params.user.id,
|
|
email: params.user.email,
|
|
type: 'debit',
|
|
amount: params.amount,
|
|
service: params.service.name,
|
|
serviceId: params.service.id || params.service.clusterId || params.service.instanceId,
|
|
description: `Payment for ${params.service.name} - ${params.service.type}`,
|
|
status: 'completed',
|
|
previousBalance: currentBalance,
|
|
newBalance,
|
|
metadata: {
|
|
serviceType: params.service.type,
|
|
deploymentSuccess: params.deploymentSuccess,
|
|
...params.metadata,
|
|
},
|
|
})
|
|
|
|
transactionRecord = await newTransaction.save()
|
|
} catch (transactionError) {
|
|
console.error('Failed to create transaction record:', transactionError)
|
|
// Continue with billing creation even if transaction fails
|
|
}
|
|
}
|
|
|
|
// Create billing record
|
|
const billing = await this.createBillingRecord({
|
|
user: params.user,
|
|
service: params.service,
|
|
billing: {
|
|
amount: params.amount,
|
|
currency: params.currency || 'INR',
|
|
cycle: params.cycle || 'onetime',
|
|
},
|
|
payment: {
|
|
transactionId: transactionRecord?.transactionId,
|
|
status: params.deploymentSuccess && params.amount > 0 ? 'paid' : 'pending',
|
|
},
|
|
status: {
|
|
service: params.deploymentSuccess ? 'active' : 'failed',
|
|
serviceStatus: params.deploymentSuccess ? 1 : 0,
|
|
},
|
|
metadata: {
|
|
remarks: params.deploymentSuccess ? 'Deployment Success' : 'Deployment Failed',
|
|
deploymentSuccess: params.deploymentSuccess,
|
|
deploymentResponse: params.deploymentResponse,
|
|
...params.metadata,
|
|
},
|
|
})
|
|
|
|
return {
|
|
billing,
|
|
transaction: transactionRecord,
|
|
balanceUpdated,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get billing statistics for a user
|
|
*/
|
|
static async getBillingStats(
|
|
userEmail: string,
|
|
siliconId: string
|
|
): Promise<{
|
|
totalSpent: number
|
|
activeServices: number
|
|
totalServices: number
|
|
serviceBreakdown: Record<string, { count: number; amount: number }>
|
|
}> {
|
|
await connectDB()
|
|
|
|
const billings = await this.getUserBillings(userEmail, siliconId)
|
|
|
|
const stats = {
|
|
totalSpent: 0,
|
|
activeServices: 0,
|
|
totalServices: billings.length,
|
|
serviceBreakdown: {} as Record<string, { count: number; amount: number }>,
|
|
}
|
|
|
|
billings.forEach((billing) => {
|
|
stats.totalSpent += billing.total_amount || billing.amount
|
|
|
|
if (billing.status === 'active' && billing.service_status === 1) {
|
|
stats.activeServices++
|
|
}
|
|
|
|
const serviceType = billing.service_type
|
|
if (!stats.serviceBreakdown[serviceType]) {
|
|
stats.serviceBreakdown[serviceType] = { count: 0, amount: 0 }
|
|
}
|
|
stats.serviceBreakdown[serviceType].count++
|
|
stats.serviceBreakdown[serviceType].amount += billing.total_amount || billing.amount
|
|
})
|
|
|
|
return stats
|
|
}
|
|
}
|
|
|
|
export default BillingService
|