170 lines
6.7 KiB
TypeScript
170 lines
6.7 KiB
TypeScript
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
import { db } from '@/lib/db';
|
|
import { CryptoEngine } from '@/lib/crypto-engine';
|
|
|
|
export async function POST(req: NextRequest) {
|
|
try {
|
|
const { merchantId, amount, network, currency } = await req.json();
|
|
|
|
if (!merchantId || !amount || !network) {
|
|
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
|
}
|
|
|
|
// 1. Fetch Merchant details
|
|
const merchantRes = await db.query('SELECT * FROM merchants WHERE id = $1', [merchantId]);
|
|
const merchant = merchantRes.rows[0];
|
|
|
|
if (!merchant) {
|
|
return NextResponse.json({ error: 'Merchant not found' }, { status: 404 });
|
|
}
|
|
|
|
// Resolve per-network payout address
|
|
const netKey = ['POLYGON', 'BSC', 'ETH'].includes(network) ? 'EVM' : network;
|
|
const payoutAddresses = merchant.payout_addresses || {};
|
|
const destinationAddress = payoutAddresses[netKey] || merchant.payout_address;
|
|
|
|
if (!destinationAddress) {
|
|
return NextResponse.json({ error: `Merchant payout address for ${netKey} is not set. Please configure it in merchant settings.` }, { status: 400 });
|
|
}
|
|
|
|
const availableBalance = parseFloat(merchant.available_balance || '0');
|
|
const payoutAmount = parseFloat(amount);
|
|
|
|
if (availableBalance < payoutAmount) {
|
|
return NextResponse.json({ error: 'Insufficient balance' }, { status: 400 });
|
|
}
|
|
|
|
// 2. Get Treasury Private Key based on network
|
|
const treasuryKeys: Record<string, string | undefined> = {
|
|
'POLYGON': process.env.TREASURY_EVM_KEY,
|
|
'BSC': process.env.TREASURY_EVM_KEY,
|
|
'ETH': process.env.TREASURY_EVM_KEY,
|
|
'SOLANA': process.env.TREASURY_SOL_KEY,
|
|
'TRON': process.env.TREASURY_TRON_KEY,
|
|
'BITCOIN': process.env.TREASURY_BTC_KEY
|
|
};
|
|
|
|
const privateKey = treasuryKeys[network];
|
|
if (!privateKey) {
|
|
return NextResponse.json({ error: `Treasury private key for ${network} is not configured in .env` }, { status: 500 });
|
|
}
|
|
|
|
// 3. Check merchant's crypto balance for this network/token
|
|
const selectedCurrency = currency || 'SOL';
|
|
|
|
const balRes = await db.query(
|
|
'SELECT balance, withdrawn FROM merchant_balances WHERE merchant_id = $1 AND network = $2 AND token = $3',
|
|
[merchantId, network, selectedCurrency]
|
|
);
|
|
|
|
const cryptoBalance = balRes.rows[0] ? parseFloat(balRes.rows[0].balance) : 0;
|
|
const cryptoWithdrawn = balRes.rows[0] ? parseFloat(balRes.rows[0].withdrawn) : 0;
|
|
const cryptoAvailable = cryptoBalance - cryptoWithdrawn;
|
|
|
|
if (cryptoAvailable < payoutAmount) {
|
|
return NextResponse.json({
|
|
error: `Yetersiz ${selectedCurrency} bakiyesi. Çekilebilir: ${cryptoAvailable.toFixed(6)} ${selectedCurrency}, talep edilen: ${payoutAmount} ${selectedCurrency}`
|
|
}, { status: 400 });
|
|
}
|
|
|
|
// 4. Execute Transfer
|
|
const engine = new CryptoEngine(network);
|
|
const transfer = await engine.sendPayout(
|
|
privateKey,
|
|
destinationAddress,
|
|
amount.toString(),
|
|
selectedCurrency
|
|
);
|
|
|
|
if (!transfer.success) {
|
|
return NextResponse.json({ error: transfer.error || 'Blockchain transfer failed' }, { status: 500 });
|
|
}
|
|
|
|
// 5. Update Database
|
|
await db.query('BEGIN');
|
|
try {
|
|
// Deduct from merchant's crypto balance
|
|
await db.query(`
|
|
UPDATE merchant_balances
|
|
SET withdrawn = withdrawn + $1
|
|
WHERE merchant_id = $2 AND network = $3 AND token = $4
|
|
`, [payoutAmount, merchantId, network, selectedCurrency]);
|
|
|
|
// Also update legacy TRY balance (best effort price conversion)
|
|
try {
|
|
const coinIdMap: Record<string, string> = {
|
|
'SOL': 'solana', 'USDC': 'usd-coin', 'USDT': 'tether',
|
|
'TRX': 'tron', 'BTC': 'bitcoin', 'ETH': 'ethereum',
|
|
'MATIC': 'matic-network', 'BNB': 'binancecoin'
|
|
};
|
|
const coinId = coinIdMap[selectedCurrency] || 'solana';
|
|
const priceRes = await fetch(`https://api.coingecko.com/api/v3/simple/price?ids=${coinId}&vs_currencies=try`);
|
|
const priceData = await priceRes.json();
|
|
const tryEquivalent = payoutAmount * (priceData[coinId]?.try || 0);
|
|
|
|
if (tryEquivalent > 0) {
|
|
await db.query(`
|
|
UPDATE merchants
|
|
SET available_balance = GREATEST(0, available_balance - $1),
|
|
withdrawn_balance = withdrawn_balance + $1
|
|
WHERE id = $2
|
|
`, [tryEquivalent, merchantId]);
|
|
}
|
|
} catch (e) {
|
|
console.warn('[Payout] TRY balance update skipped (price fetch failed)');
|
|
}
|
|
|
|
// Log the payout
|
|
await db.query(`
|
|
INSERT INTO payouts (merchant_id, amount, currency, network, destination_address, tx_hash, status)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
`, [
|
|
merchantId,
|
|
payoutAmount,
|
|
selectedCurrency,
|
|
network,
|
|
destinationAddress,
|
|
transfer.txHash,
|
|
'succeeded'
|
|
]);
|
|
|
|
await db.query('COMMIT');
|
|
console.log(`[Payout] ✅ Sent ${payoutAmount} ${selectedCurrency} on ${network} to ${destinationAddress}`);
|
|
} catch (dbErr) {
|
|
await db.query('ROLLBACK');
|
|
throw dbErr;
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
txHash: transfer.txHash,
|
|
message: 'Payout processed successfully'
|
|
});
|
|
|
|
} catch (err: any) {
|
|
console.error('Payout Error:', err);
|
|
return NextResponse.json({ error: err.message }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function GET(req: NextRequest) {
|
|
try {
|
|
const { searchParams } = new URL(req.url);
|
|
const merchantId = searchParams.get('merchantId');
|
|
|
|
let query = 'SELECT * FROM payouts ORDER BY created_at DESC';
|
|
let params: any[] = [];
|
|
|
|
if (merchantId) {
|
|
query = 'SELECT * FROM payouts WHERE merchant_id = $1 ORDER BY created_at DESC';
|
|
params = [merchantId];
|
|
}
|
|
|
|
const result = await db.query(query, params);
|
|
return NextResponse.json(result.rows);
|
|
} catch (err: any) {
|
|
return NextResponse.json({ error: err.message }, { status: 500 });
|
|
}
|
|
}
|