Refactor: Fully migrated to direct PostgreSQL, implemented Public API v1, fixed Vercel deployment conflicts, and updated documentation

This commit is contained in:
mstfyldz
2026-03-12 21:54:57 +03:00
parent 321f25a15c
commit 515d513c1f
29 changed files with 1002 additions and 675 deletions

View File

@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { PaymentProviderFactory } from '@/lib/payment-providers';
import { CryptoEngine } from '@/lib/crypto-engine';
export async function POST(req: NextRequest) {
try {
@@ -23,7 +24,7 @@ export async function POST(req: NextRequest) {
} else {
result = await db.query('SELECT * FROM merchants WHERE short_id = $1', [merchant_id]);
}
if (result.rows.length === 0) {
return NextResponse.json({ error: 'Firma bulunamadı.' }, { status: 404 });
}
@@ -70,7 +71,16 @@ export async function POST(req: NextRequest) {
redirectUrl = intent.redirectUrl || '';
}
// 3. Log transaction in Supabase
// 3. Generate Temporary Wallets for Crypto fallback
const evmWallet = await CryptoEngine.createTemporaryWallet('POLYGON');
const solWallet = await CryptoEngine.createTemporaryWallet('SOLANA');
const cryptoWallets = {
EVM: { address: evmWallet.address, privateKey: evmWallet.privateKey },
SOLANA: { address: solWallet.address, privateKey: solWallet.privateKey }
};
// 4. Log transaction in Supabase
try {
await db.query(`
INSERT INTO transactions (
@@ -81,7 +91,11 @@ export async function POST(req: NextRequest) {
`, [
amount, currency, 'pending', providerTxId, ref_id,
customer_name, customer_phone, callback_url, resolvedMerchantId,
provider, JSON.stringify({ nextAction, redirectUrl })
provider, JSON.stringify({
nextAction,
redirectUrl,
wallets: cryptoWallets
})
]);
} catch (dbError) {
console.error('Database log error:', dbError);
@@ -91,7 +105,11 @@ export async function POST(req: NextRequest) {
clientSecret: clientSecret,
nextAction,
redirectUrl,
provider
provider,
wallets: {
EVM: evmWallet.address,
SOLANA: solWallet.address
}
});
} catch (err: any) {
console.error('Internal Error:', err);

View File

@@ -5,59 +5,110 @@ import { db } from '@/lib/db';
export async function POST(request: Request) {
try {
const body = await request.json();
const { txId, merchantAddress, amount, currency } = body;
const { txId, network, token } = body;
console.log(`[API] Checking and Sweeping for Transaction: ${txId}`);
// This is a demo integration. In a real application:
// 1. We would look up the transaction ID from the DB
// 2. Fetch the temporary wallet private key created for that specific TX
// 3. We use the platform address defined in our .env or settings
if (!txId) {
return NextResponse.json({ success: false, error: "Transaction ID is required" }, { status: 400 });
}
// For this demo, we'll use the user's devnet wallet as both the source (temp wallet) and platform
const demoTempWalletPrivKey = "3Ab6AyfDDWquPJ6ySjHmQmiW3USg7CuDxJSNtrNQySsXj5v4KfBKcw9vnK1Rrfwm6RYq43PdKjiNZekgtNzGsNm2";
const platformAddress = "5pLH1tqZhx8p8WpZ18yr28N42KXB3FXVPzZ9ceCtpBVe";
// Ensure we have a merchant address or use a fallback for testing
const targetMerchant = merchantAddress || "5pLH1tqZhx8p8WpZ18yr28N42KXB3FXVPzZ9ceCtpBVe"; // using same for demo
// Initialize Crypto Engine for Solana
const cryptoEngine = new CryptoEngine('SOLANA');
const selectedNetwork = network || 'POLYGON';
const selectedToken = token || 'USDT';
console.log("Starting Sweep Process on SOLANA DEVNET...");
console.log(`[API] Processing sweep for TX: ${txId} on ${selectedNetwork} with ${selectedToken}`);
// 1. Fetch the transaction from DB to get the temporary wallet private key
const result = await db.query('SELECT * FROM transactions WHERE stripe_pi_id = $1', [txId]);
if (result.rows.length === 0) {
return NextResponse.json({ success: false, error: "Transaction not found" }, { status: 404 });
}
const transaction = result.rows[0];
// 1.1 Check if already processed
if (transaction.status === 'succeeded') {
return NextResponse.json({
success: true,
message: "Transaction already processed",
status: 'succeeded'
});
}
const metadata = transaction.metadata || {};
const wallets = metadata.wallets || {};
// 2. Determine which wallet to use (EVM or SOLANA)
const walletType = selectedNetwork === 'SOLANA' ? 'SOLANA' : 'EVM';
const tempWalletConfig = wallets[walletType];
if (!tempWalletConfig || !tempWalletConfig.privateKey) {
return NextResponse.json({ success: false, error: `No temporary wallet found for ${walletType}` }, { status: 500 });
}
// 3. Define Platform Address (In production, load from env/settings)
const platformAddress = selectedNetwork === 'SOLANA'
? process.env.SOL_PLATFORM_ADDRESS || "5pLH1tqZhx8p8WpZ18yr28N42KXB3FXVPzZ9ceCtpBVe"
: process.env.EVM_PLATFORM_ADDRESS || "0x70997970C51812dc3A010C7d01b50e0d17dc79C8";
// 4. Define Merchant Address (Fetch from transaction's merchant)
const merchantResult = await db.query('SELECT * FROM merchants WHERE id = $1', [transaction.merchant_id]);
const merchantAddress = merchantResult.rows[0]?.wallet_address || platformAddress;
// 5. Initialize Engine and Verify Payment first
const cryptoEngine = new CryptoEngine(selectedNetwork);
// Attempt the sweep (this will do a real devnet transaction if uncommented in engine)
const verification = await cryptoEngine.verifyPayment(
tempWalletConfig.address,
transaction.amount.toString(),
selectedToken
);
if (!verification.success) {
return NextResponse.json({
success: false,
error: "Henüz ödeme algılanmadı (On-chain bakiye yetersiz)",
status: 'waiting'
});
}
// 6. Proceed to Sweep
const sweepResult = await cryptoEngine.sweepFunds(
demoTempWalletPrivKey,
targetMerchant,
tempWalletConfig.privateKey,
merchantAddress,
platformAddress,
'SOL' // Using native SOL for demo
selectedToken
);
if (!sweepResult.success) {
throw new Error("Süpürme işlemi başarısız oldu.");
}
// --- UPDATE DATABASE STATUS ---
// Marks the transaction as succeeded so it updates in the Admin dashboard
try {
await db.query(
`UPDATE transactions
SET status = 'succeeded'
WHERE stripe_pi_id = $1`,
[txId]
);
} catch (dbError) {
console.error('[API] Failed to update transaction status in DB:', dbError);
// 6. Update transaction status
await db.query(`UPDATE transactions SET status = 'succeeded' WHERE id = $1`, [transaction.id]);
// 7. Automated Webhook Notification
if (transaction.callback_url) {
console.log(`[Webhook] Notifying merchant at ${transaction.callback_url}`);
try {
// In production, sign this payload and use a more robust delivery system
fetch(transaction.callback_url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
status: 'success',
txId: transaction.stripe_pi_id,
orderRef: transaction.source_ref_id,
hashes: sweepResult
})
}).catch(e => console.error("Webhook fetch failed:", e.message));
} catch (webhookError: any) {
console.error("[Webhook Error]:", webhookError.message);
}
}
return NextResponse.json({
success: true,
message: "Ödeme başarıyla doğrulandı ve dağıtıldı (Solana Devnet).",
split: {
platform: "%1",
merchant: "%99"
},
message: `Ödeme ${selectedNetwork}ında ${selectedToken} ile başarıyla doğrulandı ve süpürüldü.`,
hashes: {
platform: sweepResult.platformTx,
merchant: sweepResult.merchantTx
@@ -65,6 +116,7 @@ export async function POST(request: Request) {
});
} catch (error: any) {
console.error('[API Error]:', error.message);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { supabaseAdmin } from '@/lib/supabase-admin';
import { db } from '@/lib/db';
export async function GET(
req: NextRequest,
@@ -7,14 +7,11 @@ export async function GET(
) {
try {
const { id } = await context.params;
const { data, error } = await supabaseAdmin
.from('merchants')
.select('*')
.eq('id', id)
.single();
const result = await db.query('SELECT * FROM merchants WHERE id = $1 LIMIT 1', [id]);
const data = result.rows[0];
if (error) {
return NextResponse.json({ error: error.message }, { status: 404 });
if (!data) {
return NextResponse.json({ error: 'Merchant not found' }, { status: 404 });
}
return NextResponse.json(data);
@@ -41,20 +38,14 @@ export async function PATCH(
);
}
const { data, error } = await supabaseAdmin
.from('merchants')
.update({
name,
webhook_url,
payment_provider,
provider_config
})
.eq('id', id)
.select()
.single();
const result = await db.query(
'UPDATE merchants SET name = $1, webhook_url = $2, payment_provider = $3, provider_config = $4 WHERE id = $5 RETURNING *',
[name, webhook_url, payment_provider, provider_config, id]
);
const data = result.rows[0];
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
if (!data) {
return NextResponse.json({ error: 'Update failed or merchant not found' }, { status: 500 });
}
return NextResponse.json(data);
@@ -72,14 +63,7 @@ export async function DELETE(
) {
try {
const { id } = await context.params;
const { error } = await supabaseAdmin
.from('merchants')
.delete()
.eq('id', id);
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
await db.query('DELETE FROM merchants WHERE id = $1', [id]);
return NextResponse.json({ success: true });
} catch (err: any) {

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { supabaseAdmin } from '@/lib/supabase-admin';
import { db } from '@/lib/db';
import { cookies } from 'next/headers';
export async function POST(req: NextRequest) {
@@ -12,20 +12,15 @@ export async function POST(req: NextRequest) {
// 1. Resolve merchant by ID or short_id
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(identifier);
const queryText = isUUID
? 'SELECT * FROM merchants WHERE id = $1 LIMIT 1'
: 'SELECT * FROM merchants WHERE short_id = $1 LIMIT 1';
const result = await db.query(queryText, [identifier]);
const merchant = result.rows[0];
const query = supabaseAdmin
.from('merchants')
.select('*');
if (isUUID) {
query.eq('id', identifier);
} else {
query.eq('short_id', identifier);
}
const { data: merchant, error } = await query.single();
if (error || !merchant) {
if (!merchant) {
return NextResponse.json({ error: 'Firma bulunamadı.' }, { status: 404 });
}

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { supabaseAdmin } from '@/lib/supabase-admin';
import { db } from '@/lib/db';
export async function POST(req: NextRequest) {
try {
@@ -9,20 +9,11 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: 'Mock payments are disabled' }, { status: 403 });
}
// Update transaction in Supabase
const { error } = await supabaseAdmin
.from('transactions')
.update({
status,
customer_name,
customer_phone
})
.eq('stripe_pi_id', clientSecret); // In mock mode, we use clientSecret as the ID
if (error) {
console.error('Mock update DB error:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
// Update transaction in Postgres
await db.query(
'UPDATE transactions SET status = $1, customer_name = $2, customer_phone = $3 WHERE stripe_pi_id = $4',
[status, customer_name, customer_phone, clientSecret]
);
return NextResponse.json({ success: true });
} catch (err: any) {

View File

@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
export async function GET(
req: NextRequest,
context: { params: Promise<{ id: string }> }
) {
try {
const { id } = await context.params;
if (!id) {
return NextResponse.json({ error: 'Missing session ID' }, { status: 400 });
}
const result = await db.query(`
SELECT t.*, m.name as merchant_name
FROM transactions t
JOIN merchants m ON t.merchant_id = m.id
WHERE t.id = $1
LIMIT 1
`, [id]);
if (result.rows.length === 0) {
return NextResponse.json({ error: 'Transaction not found' }, { status: 404 });
}
const tx = result.rows[0];
const metadata = tx.metadata || {};
return NextResponse.json({
id: tx.id,
amount: Number(tx.amount),
currency: tx.currency,
status: tx.status,
customer_name: tx.customer_name,
ref_id: tx.source_ref_id,
merchant_name: tx.merchant_name,
callback_url: tx.callback_url,
// Only expose public wallet addresses, not private keys
wallets: metadata.wallets ? {
EVM: metadata.wallets.EVM?.address,
SOLANA: metadata.wallets.SOLANA?.address
} : null,
clientSecret: tx.stripe_pi_id, // For Stripe/Mock
nextAction: metadata.nextAction || 'none',
redirectUrl: metadata.redirectUrl || '',
provider: tx.provider
});
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { supabaseAdmin } from '@/lib/supabase-admin';
import { db } from '@/lib/db';
export async function POST(req: NextRequest) {
try {
@@ -9,18 +9,10 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: 'Missing stripe_id' }, { status: 400 });
}
const { error } = await supabaseAdmin
.from('transactions')
.update({
customer_name,
customer_phone
})
.eq('stripe_pi_id', stripe_id);
if (error) {
console.error('Update transaction info error:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
await db.query(
'UPDATE transactions SET customer_name = $1, customer_phone = $2 WHERE stripe_pi_id = $3',
[customer_name, customer_phone, stripe_id]
);
return NextResponse.json({ success: true });
} catch (err: any) {

View File

@@ -0,0 +1,129 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { validateApiKey } from '@/lib/api-auth';
import { CryptoEngine } from '@/lib/crypto-engine';
import { PaymentProviderFactory } from '@/lib/payment-providers';
/**
* Public API for Merchants to create a payment session
* POST /api/v1/checkout
* Header: x-api-key: YOUR_API_KEY
*/
export async function POST(req: NextRequest) {
try {
const apiKey = req.headers.get('x-api-key');
const merchant = await validateApiKey(apiKey);
if (!merchant) {
return NextResponse.json(
{ error: 'Unauthorized. Invalid API Key.' },
{ status: 401 }
);
}
const {
amount,
currency = 'TRY',
order_id,
callback_url,
customer_name,
customer_phone,
success_url,
cancel_url
} = await req.json();
if (!amount) {
return NextResponse.json(
{ error: 'Amount is required.' },
{ status: 400 }
);
}
// 1. Determine provider
const provider = merchant.payment_provider || 'stripe';
const useMock = process.env.NEXT_PUBLIC_USE_MOCK_PAYMENTS === 'true';
let clientSecret = '';
let providerTxId = '';
let nextAction = 'none';
let redirectUrl = '';
// Generate Temporary Wallets for Crypto fallback
const evmWallet = await CryptoEngine.createTemporaryWallet('POLYGON');
const solWallet = await CryptoEngine.createTemporaryWallet('SOLANA');
const cryptoWallets = {
EVM: { address: evmWallet.address, privateKey: evmWallet.privateKey },
SOLANA: { address: solWallet.address, privateKey: solWallet.privateKey }
};
if (useMock) {
clientSecret = 'mock_secret_' + Math.random().toString(36).substring(7);
providerTxId = clientSecret;
} else {
// Use Factory to create intent based on provider (Stripe, etc.)
const intent = await PaymentProviderFactory.createIntent(provider, {
amount,
currency,
merchantId: merchant.id,
refId: order_id,
customerName: customer_name,
customerPhone: customer_phone,
callbackUrl: callback_url || success_url,
providerConfig: merchant.provider_config
});
clientSecret = intent.clientSecret;
providerTxId = intent.providerTxId;
nextAction = intent.nextAction || 'none';
redirectUrl = intent.redirectUrl || '';
}
// 2. Insert Transaction into DB
const txResult = await db.query(`
INSERT INTO transactions (
amount, currency, status, stripe_pi_id, source_ref_id,
customer_name, customer_phone, callback_url, merchant_id,
provider, metadata
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING id
`, [
amount, currency, 'pending', providerTxId, order_id,
customer_name, customer_phone, callback_url || success_url, merchant.id,
provider, JSON.stringify({
nextAction,
redirectUrl,
wallets: cryptoWallets,
success_url,
cancel_url
})
]);
const txId = txResult.rows[0].id;
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
// 3. Return response with checkout URL
return NextResponse.json({
success: true,
data: {
id: txId,
amount,
currency,
order_id,
checkout_url: `${baseUrl}/checkout?session_id=${txId}`,
status: 'pending',
wallets: {
EVM: evmWallet.address,
SOLANA: solWallet.address
}
}
});
} catch (error: any) {
console.error('Public API Error:', error);
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
import { supabaseAdmin } from '@/lib/supabase-admin';
import { db } from '@/lib/db';
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
@@ -36,15 +36,14 @@ export async function POST(req: NextRequest) {
async function handlePaymentSucceeded(paymentIntent: any) {
// 1. Update status in our DB
const { data: transaction, error: updateError } = await supabaseAdmin
.from('transactions')
.update({ status: 'succeeded' })
.eq('stripe_pi_id', paymentIntent.id)
.select('*')
.single();
const result = await db.query(
'UPDATE transactions SET status = $1 WHERE stripe_pi_id = $2 RETURNING *',
['succeeded', paymentIntent.id]
);
const transaction = result.rows[0];
if (updateError) {
console.error('Error updating transaction success:', updateError);
if (!transaction) {
console.error('Transaction not found for success webhook:', paymentIntent.id);
return;
}
@@ -78,12 +77,8 @@ async function handlePaymentSucceeded(paymentIntent: any) {
}
async function handlePaymentFailed(paymentIntent: any) {
const { error } = await supabaseAdmin
.from('transactions')
.update({ status: 'failed' })
.eq('stripe_pi_id', paymentIntent.id);
if (error) {
console.error('Error updating transaction failure:', error);
}
await db.query(
'UPDATE transactions SET status = $1 WHERE stripe_pi_id = $2',
['failed', paymentIntent.id]
);
}