initial commit
This commit is contained in:
375
lib/billing-service.ts
Normal file
375
lib/billing-service.ts
Normal file
@@ -0,0 +1,375 @@
|
||||
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
|
||||
Reference in New Issue
Block a user