initial commit

This commit is contained in:
Kar k1
2025-08-30 18:18:57 +05:30
commit 7219108342
270 changed files with 70221 additions and 0 deletions

View File

@@ -0,0 +1,237 @@
import { NextRequest, NextResponse } from 'next/server'
import jwt from 'jsonwebtoken'
import crypto from 'crypto'
interface AddBalanceRequest {
amount: number
currency: 'INR' | 'USD'
}
interface UserTokenPayload {
siliconId: string
email: string
type: string
}
/**
* Add Balance API
* Initiates "Add Balance" transaction for user accounts
*/
export async function POST(request: NextRequest) {
try {
// Verify user authentication
const token = request.cookies.get('accessToken')?.value
if (!token) {
return NextResponse.json(
{ success: false, message: 'Authentication required' },
{ status: 401 }
)
}
const secret = process.env.JWT_SECRET || 'your-secret-key'
const user = jwt.verify(token, secret) as UserTokenPayload
// Parse request body
const body: AddBalanceRequest = await request.json()
const { amount, currency } = body
// Validate input
if (!amount || amount <= 0 || !['INR', 'USD'].includes(currency)) {
return NextResponse.json(
{ success: false, message: 'Invalid amount or currency' },
{ status: 400 }
)
}
// Validate amount limits
const minAmount = currency === 'INR' ? 100 : 2 // Minimum ₹100 or $2
const maxAmount = currency === 'INR' ? 100000 : 1500 // Maximum ₹1,00,000 or $1500
if (amount < minAmount || amount > maxAmount) {
return NextResponse.json(
{
success: false,
message: `Amount must be between ${currency === 'INR' ? '₹' : '$'}${minAmount} and ${currency === 'INR' ? '₹' : '$'}${maxAmount}`,
},
{ status: 400 }
)
}
try {
// Generate unique transaction ID
const transactionId = generateTransactionId()
// TODO: Save transaction to database
// In real implementation:
// await saveAddBalanceTransaction({
// siliconId: user.siliconId,
// amount,
// currency,
// transaction_id: transactionId,
// status: 'pending'
// })
// Mock database save for demo
await mockSaveAddBalanceTransaction(user.siliconId, amount, currency, transactionId)
// PayU configuration
const merchantKey = process.env.PAYU_MERCHANT_KEY || 'test-key'
const merchantSalt = process.env.PAYU_MERCHANT_SALT || 'test-salt'
const payuUrl = process.env.PAYU_URL || 'https://test.payu.in/_payment'
// Prepare payment data
const productinfo = 'add_balance'
const firstname = 'Customer'
const email = user.email
const phone = '9876543210' // Default phone or fetch from user profile
// Success and failure URLs for balance transactions
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:4023'
const surl = `${baseUrl}/api/balance/success`
const furl = `${baseUrl}/api/balance/failure`
// Generate PayU hash
const hashString = `${merchantKey}|${transactionId}|${amount}|${productinfo}|${firstname}|${email}|||||||||||${merchantSalt}`
const hash = crypto.createHash('sha512').update(hashString).digest('hex')
// Return payment form data for frontend submission
const paymentData = {
success: true,
transaction_id: transactionId,
currency,
payment_url: payuUrl,
form_data: {
key: merchantKey,
txnid: transactionId,
amount: amount.toFixed(2),
productinfo,
firstname,
email,
phone,
surl,
furl,
hash,
service_provider: 'payu_paisa',
},
}
return NextResponse.json(paymentData)
} catch (dbError) {
console.error('Database error during add balance:', dbError)
return NextResponse.json(
{ success: false, message: 'Failed to initiate balance transaction' },
{ status: 500 }
)
}
} catch (error) {
console.error('Add balance error:', error)
return NextResponse.json(
{ success: false, message: 'Add balance initiation failed' },
{ status: 500 }
)
}
}
/**
* Get Add Balance History
*/
export async function GET(request: NextRequest) {
try {
// Verify user authentication
const token = request.cookies.get('accessToken')?.value
if (!token) {
return NextResponse.json(
{ success: false, message: 'Authentication required' },
{ status: 401 }
)
}
const secret = process.env.JWT_SECRET || 'your-secret-key'
const user = jwt.verify(token, secret) as UserTokenPayload
// TODO: Fetch balance history from database
// In real implementation:
// const history = await getBalanceHistory(user.siliconId)
// Mock balance history for demo
const mockHistory = await getMockBalanceHistory(user.siliconId)
return NextResponse.json({
success: true,
balance_history: mockHistory,
})
} catch (error) {
console.error('Get balance history error:', error)
return NextResponse.json(
{ success: false, message: 'Failed to fetch balance history' },
{ status: 500 }
)
}
}
// Utility functions
function generateTransactionId(): string {
const timestamp = Date.now().toString(36)
const random = Math.random().toString(36).substr(2, 5)
return `bal_${timestamp}${random}`.toUpperCase()
}
// Mock functions for demonstration
async function mockSaveAddBalanceTransaction(
siliconId: string,
amount: number,
currency: string,
transactionId: string
) {
console.log(`Mock DB Save: Add Balance Transaction`)
console.log(`SiliconID: ${siliconId}, Amount: ${amount} ${currency}, TxnID: ${transactionId}`)
// Mock database record
const transaction = {
id: Date.now(),
silicon_id: siliconId,
transaction_id: transactionId,
amount,
currency,
status: 'pending',
created_at: new Date(),
updated_at: new Date(),
}
// Mock delay
await new Promise((resolve) => setTimeout(resolve, 100))
return transaction
}
async function getMockBalanceHistory(siliconId: string) {
// Mock balance transaction history
return [
{
id: 1,
transaction_id: 'BAL_123ABC',
amount: 1000,
currency: 'INR',
status: 'success',
created_at: '2025-01-15T10:30:00Z',
updated_at: '2025-01-15T10:31:00Z',
},
{
id: 2,
transaction_id: 'BAL_456DEF',
amount: 500,
currency: 'INR',
status: 'failed',
created_at: '2025-01-10T14:20:00Z',
updated_at: '2025-01-10T14:21:00Z',
},
{
id: 3,
transaction_id: 'BAL_789GHI',
amount: 2000,
currency: 'INR',
status: 'success',
created_at: '2025-01-05T09:15:00Z',
updated_at: '2025-01-05T09:16:00Z',
},
]
}

View File

@@ -0,0 +1,169 @@
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { authMiddleware } from '@/lib/auth-middleware'
import connectDB from '@/lib/mongodb'
import { User as UserModel } from '@/models/user'
import { Transaction } from '@/models/transaction'
// Schema for balance deduction request
const DeductBalanceSchema = z.object({
amount: z.number().positive('Amount must be positive'),
service: z.string().min(1, 'Service name is required'),
serviceId: z.string().optional(),
description: z.string().optional(),
transactionId: z.string().optional(),
})
// Schema for balance deduction response
const DeductBalanceResponseSchema = z.object({
success: z.boolean(),
data: z
.object({
transactionId: z.string(),
previousBalance: z.number(),
newBalance: z.number(),
amountDeducted: z.number(),
service: z.string(),
timestamp: z.string(),
})
.optional(),
error: z
.object({
message: z.string(),
code: z.string(),
})
.optional(),
})
export async function POST(request: NextRequest) {
try {
// Authenticate user
const user = await authMiddleware(request)
if (!user) {
return NextResponse.json(
{
success: false,
error: { message: 'Authentication required', code: 'UNAUTHORIZED' },
},
{ status: 401 }
)
}
await connectDB()
// Parse and validate request body
const body = await request.json()
const validatedData = DeductBalanceSchema.parse(body)
// Get user's current balance from database
const userData = await UserModel.findOne({ email: user.email })
if (!userData) {
return NextResponse.json(
{
success: false,
error: { message: 'User not found', code: 'USER_NOT_FOUND' },
},
{ status: 404 }
)
}
const currentBalance = userData.balance || 0
// Check if user has sufficient balance
if (currentBalance < validatedData.amount) {
return NextResponse.json(
{
success: false,
error: {
message: `Insufficient balance. Required: ₹${validatedData.amount}, Available: ₹${currentBalance}`,
code: 'INSUFFICIENT_BALANCE',
},
},
{ status: 400 }
)
}
// Calculate new balance
const newBalance = currentBalance - validatedData.amount
// Update user balance in database
await UserModel.updateOne(
{ email: user.email },
{
$set: { balance: newBalance },
$inc: { __v: 1 }, // Increment version for optimistic locking
}
)
// Generate transaction ID if not provided
const transactionId =
validatedData.transactionId || `txn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
// Save transaction record to database
try {
const newTransaction = new Transaction({
transactionId,
userId: user.id,
email: user.email,
type: 'debit',
amount: validatedData.amount,
service: validatedData.service,
serviceId: validatedData.serviceId,
description: validatedData.description || `Payment for ${validatedData.service}`,
status: 'completed',
previousBalance: currentBalance,
newBalance,
metadata: {
userAgent: request.headers.get('user-agent'),
ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'),
},
})
await newTransaction.save()
console.log('Transaction record saved:', transactionId)
} catch (transactionError) {
console.error('Failed to save transaction record:', transactionError)
// Continue even if transaction record fails - balance was already deducted
}
const responseData = {
success: true,
data: {
transactionId,
previousBalance: currentBalance,
newBalance,
amountDeducted: validatedData.amount,
service: validatedData.service,
timestamp: new Date().toISOString(),
},
}
// Validate response format
const validatedResponse = DeductBalanceResponseSchema.parse(responseData)
return NextResponse.json(validatedResponse, { status: 200 })
} catch (error) {
console.error('Balance deduction error:', error)
if (error instanceof z.ZodError) {
return NextResponse.json(
{
success: false,
error: {
message: 'Invalid request data',
code: 'VALIDATION_ERROR',
details: error.issues,
},
},
{ status: 400 }
)
}
return NextResponse.json(
{
success: false,
error: { message: 'Failed to deduct balance', code: 'INTERNAL_ERROR' },
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,147 @@
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'
interface PayUBalanceFailureResponse {
status: string
txnid: string
amount: string
productinfo: string
firstname: string
email: string
hash: string
key: string
error?: string
error_Message?: string
[key: string]: string | undefined
}
/**
* Balance Addition Failure Handler
* Processes failed "Add Balance" payments from PayU gateway
*/
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const payuResponse: Partial<PayUBalanceFailureResponse> = {}
// Extract all PayU response parameters
for (const [key, value] of formData.entries()) {
payuResponse[key] = value.toString()
}
const { status, txnid, amount, productinfo, firstname, email, hash, key: merchantKey, error, error_Message } = payuResponse
// Log failure details for debugging
console.log(`Balance addition failed - Transaction: ${txnid}, Status: ${status}, Error: ${error || error_Message || 'Unknown'}`)
// Validate required parameters
if (!txnid) {
console.error('Missing transaction ID in balance failure response')
return NextResponse.redirect(new URL('/payment/failed?error=invalid-response&type=balance', request.url))
}
// Verify PayU hash if provided (for security)
if (hash && merchantKey && status && amount) {
const merchantSalt = process.env.PAYU_MERCHANT_SALT || 'test-salt'
const expectedHashString = `${merchantSalt}|${status}|||||||||||${email}|${firstname}|${productinfo}|${amount}|${txnid}|${merchantKey}`
const expectedHash = crypto.createHash('sha512').update(expectedHashString).digest('hex').toLowerCase()
if (hash.toLowerCase() !== expectedHash) {
console.error(`Hash mismatch in balance failure response for transaction: ${txnid}`)
}
}
try {
// TODO: Update database with failed balance transaction
// In real implementation:
// await updateAddBalanceStatus(txnid, 'failed', error || error_Message)
// Mock database update for demo
await mockUpdateBalanceFailure(txnid as string, error || error_Message || 'Payment failed')
// Determine failure reason for user-friendly message
let failureReason = 'unknown'
let userMessage = 'Your balance addition failed. Please try again.'
if (error_Message?.toLowerCase().includes('insufficient')) {
failureReason = 'insufficient-funds'
userMessage = 'Insufficient funds in your payment method.'
} else if (error_Message?.toLowerCase().includes('declined')) {
failureReason = 'card-declined'
userMessage = 'Your card was declined. Please try a different payment method.'
} else if (error_Message?.toLowerCase().includes('timeout')) {
failureReason = 'timeout'
userMessage = 'Payment timed out. Please try again.'
} else if (error_Message?.toLowerCase().includes('cancelled')) {
failureReason = 'user-cancelled'
userMessage = 'Payment was cancelled.'
} else if (error_Message?.toLowerCase().includes('invalid')) {
failureReason = 'invalid-details'
userMessage = 'Invalid payment details. Please check and try again.'
}
// Redirect to profile page with failure message
const failureUrl = new URL('/profile', request.url)
failureUrl.searchParams.set('payment', 'failed')
failureUrl.searchParams.set('type', 'balance')
failureUrl.searchParams.set('reason', failureReason)
failureUrl.searchParams.set('txn', txnid as string)
failureUrl.searchParams.set('message', encodeURIComponent(userMessage))
if (amount) {
failureUrl.searchParams.set('amount', amount as string)
}
return NextResponse.redirect(failureUrl)
} catch (dbError) {
console.error('Database update error during balance failure handling:', dbError)
// Continue to failure page even if DB update fails
const failureUrl = new URL('/profile', request.url)
failureUrl.searchParams.set('payment', 'failed')
failureUrl.searchParams.set('type', 'balance')
failureUrl.searchParams.set('error', 'db-error')
failureUrl.searchParams.set('txn', txnid as string)
return NextResponse.redirect(failureUrl)
}
} catch (error) {
console.error('Balance failure handler error:', error)
return NextResponse.redirect(new URL('/profile?payment=failed&type=balance&error=processing-error', request.url))
}
}
// Mock function for demonstration
async function mockUpdateBalanceFailure(txnid: string, errorMessage: string) {
console.log(`Mock DB Update: Balance Transaction ${txnid} failed`)
console.log(`Failure reason: ${errorMessage}`)
// Mock: Update add_balance_history status
const historyUpdate = {
transaction_id: txnid,
status: 'failed',
error_message: errorMessage,
updated_at: new Date(),
retry_count: 1, // Could track retry attempts
payment_gateway_response: {
status: 'failed',
error: errorMessage,
failed_at: new Date()
}
}
// Mock: Could also track failed attempt statistics
const analytics = {
failed_balance_additions: 1,
common_failure_reasons: {
[errorMessage]: 1
}
}
console.log('Mock: Balance failure recorded for analysis')
// Mock delay
await new Promise(resolve => setTimeout(resolve, 100))
return { historyUpdate, analytics }
}

View File

@@ -0,0 +1,132 @@
import { NextRequest, NextResponse } from 'next/server'
import crypto from 'crypto'
interface PayUBalanceResponse {
status: string
txnid: string
amount: string
productinfo: string
firstname: string
email: string
hash: string
key: string
[key: string]: string
}
/**
* Balance Addition Success Handler
* Processes successful "Add Balance" payments from PayU gateway
*/
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const payuResponse: Partial<PayUBalanceResponse> = {}
// Extract all PayU response parameters
for (const [key, value] of formData.entries()) {
payuResponse[key] = value.toString()
}
const { status, txnid, amount, productinfo, firstname, email, hash, key: merchantKey } = payuResponse
// Validate required parameters
if (!status || !txnid || !amount || !hash) {
console.error('Missing required PayU parameters in balance success')
return NextResponse.redirect(new URL('/payment/failed?error=invalid-response', request.url))
}
// Verify payment status
if (status !== 'success') {
console.log(`Balance payment failed for transaction: ${txnid}, status: ${status}`)
return NextResponse.redirect(new URL('/payment/failed?txn=' + txnid + '&type=balance', request.url))
}
// Verify PayU hash for security
const merchantSalt = process.env.PAYU_MERCHANT_SALT || 'test-salt'
const expectedHashString = `${merchantSalt}|${status}|||||||||||${email}|${firstname}|${productinfo}|${amount}|${txnid}|${merchantKey}`
const expectedHash = crypto.createHash('sha512').update(expectedHashString).digest('hex').toLowerCase()
if (hash?.toLowerCase() !== expectedHash) {
console.error(`Hash mismatch for balance transaction: ${txnid}`)
return NextResponse.redirect(new URL('/payment/failed?error=hash-mismatch&type=balance', request.url))
}
try {
// TODO: Update database with successful balance addition
// In real implementation:
// const siliconId = await getSiliconIdFromTransaction(txnid)
// await updateAddBalanceStatus(txnid, 'success')
// await incrementUserBalance(siliconId, parseFloat(amount))
console.log(`Balance addition successful for transaction: ${txnid}, amount: ₹${amount}`)
// Mock database update for demo
const updateResult = await mockUpdateBalanceSuccess(txnid as string, parseFloat(amount as string))
// Redirect to profile page with success message
const successUrl = new URL('/profile', request.url)
successUrl.searchParams.set('payment', 'success')
successUrl.searchParams.set('type', 'balance')
successUrl.searchParams.set('amount', amount as string)
successUrl.searchParams.set('txn', txnid as string)
return NextResponse.redirect(successUrl)
} catch (dbError) {
console.error('Database update error during balance success:', dbError)
// Log for manual reconciliation - payment was successful at gateway
console.error(`CRITICAL: Balance payment ${txnid} succeeded at PayU but DB update failed`)
// Still redirect to success but with warning
const successUrl = new URL('/profile', request.url)
successUrl.searchParams.set('payment', 'success')
successUrl.searchParams.set('type', 'balance')
successUrl.searchParams.set('amount', amount as string)
successUrl.searchParams.set('txn', txnid as string)
successUrl.searchParams.set('warning', 'db-update-failed')
return NextResponse.redirect(successUrl)
}
} catch (error) {
console.error('Balance success handler error:', error)
return NextResponse.redirect(new URL('/payment/failed?error=processing-error&type=balance', request.url))
}
}
// Mock function for demonstration
async function mockUpdateBalanceSuccess(txnid: string, amount: number) {
console.log(`Mock DB Update: Balance Transaction ${txnid} successful`)
// Mock: Get user Silicon ID from transaction
const mockSiliconId = 'USR_12345' // In real implementation, fetch from add_balance_history table
// Mock: Update add_balance_history status
const historyUpdate = {
transaction_id: txnid,
status: 'success',
updated_at: new Date(),
payment_gateway_response: {
amount: amount,
currency: 'INR',
payment_date: new Date()
}
}
// Mock: Update user balance
const balanceUpdate = {
silicon_id: mockSiliconId,
balance_increment: amount,
previous_balance: 5000, // Mock previous balance
new_balance: 5000 + amount,
updated_at: new Date()
}
console.log(`Mock: User ${mockSiliconId} balance increased by ₹${amount}`)
console.log(`Mock: New balance: ₹${balanceUpdate.new_balance}`)
// Mock delay
await new Promise(resolve => setTimeout(resolve, 150))
return { historyUpdate, balanceUpdate }
}