ai-wpa/lib/billing-service.ts

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