Refactor: Fully migrated to direct PostgreSQL, implemented Public API v1, fixed Vercel deployment conflicts, and updated documentation
This commit is contained in:
@@ -9,19 +9,17 @@ import {
|
||||
Smartphone,
|
||||
Calendar
|
||||
} from 'lucide-react';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
import { db } from '@/lib/db';
|
||||
import { format, subDays } from 'date-fns';
|
||||
import { tr } from 'date-fns/locale';
|
||||
import AnalyticsBarChart from '@/components/admin/AnalyticsBarChart';
|
||||
import QueryRangeSelector from '@/components/admin/QueryRangeSelector';
|
||||
|
||||
async function getAnalyticsData(rangeDays: number = 12) {
|
||||
const { data: transactions, error } = await supabaseAdmin
|
||||
.from('transactions')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: true });
|
||||
const result = await db.query('SELECT * FROM transactions ORDER BY created_at ASC');
|
||||
const transactions = result.rows;
|
||||
|
||||
if (error || !transactions) return null;
|
||||
if (!transactions) return null;
|
||||
|
||||
const successfulTransactions = transactions.filter(t => t.status === 'succeeded');
|
||||
const totalRevenue = successfulTransactions.reduce((acc, t) => acc + Number(t.amount), 0);
|
||||
|
||||
@@ -8,17 +8,15 @@ import {
|
||||
MoreHorizontal,
|
||||
ArrowUpRight
|
||||
} from 'lucide-react';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
import CustomerSearch from '@/components/admin/CustomerSearch';
|
||||
|
||||
async function getFilteredCustomers(queryText?: string) {
|
||||
const { data: transactions, error } = await supabaseAdmin
|
||||
.from('transactions')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
const result = await db.query('SELECT * FROM transactions ORDER BY created_at DESC');
|
||||
const transactions = result.rows;
|
||||
|
||||
if (error || !transactions) return null;
|
||||
if (!transactions) return null;
|
||||
|
||||
// Group transactions by name or phone
|
||||
const customerMap = new Map();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
import { db } from '@/lib/db';
|
||||
import {
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
@@ -15,12 +15,10 @@ import TransactionChart from '@/components/admin/TransactionChart';
|
||||
import QueryRangeSelector from '@/components/admin/QueryRangeSelector';
|
||||
|
||||
async function getStats(rangeDays: number = 30) {
|
||||
const { data: transactions, error } = await supabaseAdmin
|
||||
.from('transactions')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
const result = await db.query('SELECT * FROM transactions ORDER BY created_at DESC');
|
||||
const transactions = result.rows;
|
||||
|
||||
if (error || !transactions) return null;
|
||||
if (!transactions) return null;
|
||||
|
||||
const successfulTransactions = transactions.filter(t => t.status === 'succeeded');
|
||||
const totalRevenue = successfulTransactions.reduce((acc, t) => acc + Number(t.amount), 0);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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} ağı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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
53
app/api/transactions/[id]/details/route.ts
Normal file
53
app/api/transactions/[id]/details/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
129
app/api/v1/checkout/route.ts
Normal file
129
app/api/v1/checkout/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,54 +12,81 @@ import Link from 'next/link';
|
||||
|
||||
function CheckoutContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const amount = parseFloat(searchParams.get('amount') || '100');
|
||||
const currency = searchParams.get('currency') || 'TL';
|
||||
const refId = searchParams.get('ref_id') || 'SEC-99231-TX';
|
||||
const callbackUrl = searchParams.get('callback_url') || '/';
|
||||
const merchantId = searchParams.get('merchant_id') || null;
|
||||
const sessionId = searchParams.get('session_id');
|
||||
const amountParam = parseFloat(searchParams.get('amount') || '0');
|
||||
const currencyParam = searchParams.get('currency') || 'TL';
|
||||
const refIdParam = searchParams.get('ref_id') || 'TX-DEFAULT';
|
||||
const callbackUrlParam = searchParams.get('callback_url') || '/';
|
||||
const merchantIdParam = searchParams.get('merchant_id') || null;
|
||||
|
||||
const [clientSecret, setClientSecret] = useState<string | null>(null);
|
||||
const [paymentData, setPaymentData] = useState<any>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [paymentMethod, setPaymentMethod] = useState<'card' | 'crypto'>('card');
|
||||
const [paymentMethod, setPaymentMethod] = useState<'card' | 'crypto'>('crypto');
|
||||
const [merchantName, setMerchantName] = useState<string>('Yükleniyor...');
|
||||
const [displayAmount, setDisplayAmount] = useState<number>(0);
|
||||
const [displayCurrency, setDisplayCurrency] = useState<string>('TRY');
|
||||
|
||||
const isMock = process.env.NEXT_PUBLIC_USE_MOCK_PAYMENTS === 'true';
|
||||
|
||||
useEffect(() => {
|
||||
if (amount <= 0) {
|
||||
setError('Geçersiz işlem tutarı.');
|
||||
return;
|
||||
}
|
||||
async function initializeCheckout() {
|
||||
try {
|
||||
if (sessionId) {
|
||||
// Fetch data from existing transaction session
|
||||
const res = await fetch(`/api/transactions/${sessionId}/details`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error) {
|
||||
setError(data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/api/create-payment-intent', {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
amount,
|
||||
currency,
|
||||
ref_id: refId,
|
||||
callback_url: callbackUrl,
|
||||
merchant_id: merchantId
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
setError(data.error);
|
||||
} else {
|
||||
setClientSecret(data.clientSecret);
|
||||
setPaymentData(data);
|
||||
|
||||
// Auto-redirect if it's a redirect action
|
||||
setClientSecret(data.clientSecret);
|
||||
setMerchantName(data.merchant_name);
|
||||
setDisplayAmount(data.amount);
|
||||
setDisplayCurrency(data.currency);
|
||||
|
||||
if (data.nextAction === 'redirect' && data.redirectUrl) {
|
||||
setTimeout(() => {
|
||||
window.location.href = data.redirectUrl;
|
||||
}, 2000); // 2 second delay to show the message
|
||||
}, 2000);
|
||||
}
|
||||
} else if (amountParam > 0) {
|
||||
// Legacy flow: create on the fly
|
||||
const res = await fetch('/api/create-payment-intent', {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
amount: amountParam,
|
||||
currency: currencyParam,
|
||||
ref_id: refIdParam,
|
||||
callback_url: callbackUrlParam,
|
||||
merchant_id: merchantIdParam
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error) {
|
||||
setError(data.error);
|
||||
} else {
|
||||
setClientSecret(data.clientSecret);
|
||||
setPaymentData(data);
|
||||
setDisplayAmount(amountParam);
|
||||
setDisplayCurrency(currencyParam);
|
||||
setMerchantName('Yetkili Satıcı');
|
||||
}
|
||||
} else {
|
||||
setError('Geçersiz işlem parametreleri.');
|
||||
}
|
||||
})
|
||||
.catch(() => setError('Ödeme başlatılamadı. Lütfen tekrar deneyin.'));
|
||||
}, [amount, currency, refId, callbackUrl]);
|
||||
} catch (err) {
|
||||
setError('Ödeme başlatılamadı. Lütfen tekrar deneyin.');
|
||||
}
|
||||
}
|
||||
|
||||
initializeCheckout();
|
||||
}, [sessionId, amountParam, currencyParam, refIdParam, callbackUrlParam, merchantIdParam]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
@@ -121,7 +148,7 @@ function CheckoutContent() {
|
||||
<div className="pt-8 border-t border-white/10 flex flex-col sm:flex-row sm:items-center gap-8">
|
||||
<div>
|
||||
<p className="text-gray-400 text-[10px] font-bold uppercase tracking-wider mb-1">Satıcı</p>
|
||||
<p className="text-white font-medium text-sm">Ayris Digital Media INC.</p>
|
||||
<p className="text-white font-medium text-sm">{merchantName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-400 text-[10px] font-bold uppercase tracking-wider mb-1">Destek</p>
|
||||
@@ -141,8 +168,8 @@ function CheckoutContent() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full max-w-lg">
|
||||
{/* Payment Method Selector */}
|
||||
<div className="flex bg-gray-100 p-1.5 rounded-2xl mb-8">
|
||||
{/* Payment Method Selector (Hidden for now, only Crypto active) */}
|
||||
<div className="flex bg-gray-100 p-1.5 rounded-2xl mb-8 hidden">
|
||||
<button
|
||||
onClick={() => setPaymentMethod('card')}
|
||||
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest transition-all ${paymentMethod === 'card' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-400'}`}
|
||||
@@ -158,13 +185,15 @@ function CheckoutContent() {
|
||||
</div>
|
||||
|
||||
{paymentMethod === 'crypto' ? (
|
||||
<CryptoCheckout
|
||||
amount={amount}
|
||||
currency={currency}
|
||||
txId={paymentData?.clientSecret || 'TX-8231'}
|
||||
<CryptoCheckout
|
||||
amount={displayAmount}
|
||||
currency={displayCurrency}
|
||||
txId={paymentData?.id || 'TX-DYNAMIC'}
|
||||
wallets={paymentData?.wallets}
|
||||
onSuccess={(hash) => {
|
||||
setTimeout(() => {
|
||||
window.location.href = `${callbackUrl}?status=success&tx_hash=${hash}`;
|
||||
const url = paymentData?.callback_url || '/';
|
||||
window.location.href = `${url}${url.includes('?') ? '&' : '?'}status=success&tx_hash=${hash}`;
|
||||
}, 2000);
|
||||
}}
|
||||
/>
|
||||
@@ -188,13 +217,13 @@ function CheckoutContent() {
|
||||
</button>
|
||||
</div>
|
||||
) : isMock ? (
|
||||
<MockCheckoutForm amount={amount} currency={currency} callbackUrl={callbackUrl} clientSecret={clientSecret} refId={refId} />
|
||||
<MockCheckoutForm amount={displayAmount} currency={displayCurrency} callbackUrl={paymentData?.callback_url || '/'} clientSecret={clientSecret} refId={paymentData?.ref_id || 'REF'} />
|
||||
) : paymentData?.provider === 'stripe' ? (
|
||||
<Elements stripe={getStripe()} options={{ clientSecret, appearance: { theme: 'stripe' } }}>
|
||||
<CheckoutForm
|
||||
amount={amount}
|
||||
currency={currency}
|
||||
callbackUrl={callbackUrl}
|
||||
amount={displayAmount}
|
||||
currency={displayCurrency}
|
||||
callbackUrl={paymentData?.callback_url || '/'}
|
||||
piId={clientSecret.split('_secret')[0]}
|
||||
/>
|
||||
</Elements>
|
||||
@@ -209,10 +238,10 @@ function CheckoutContent() {
|
||||
)}
|
||||
|
||||
<div className="mt-8 flex justify-center lg:justify-start">
|
||||
<Link href={callbackUrl} className="flex items-center gap-2 text-sm font-semibold text-gray-500 hover:text-gray-900 transition translate-x-0 hover:-translate-x-1 duration-200">
|
||||
<ArrowLeft size={16} />
|
||||
Mağazaya Dön
|
||||
</Link>
|
||||
<Link href={paymentData?.callback_url || '/'} className="flex items-center gap-2 text-sm font-semibold text-gray-500 hover:text-gray-900 transition translate-x-0 hover:-translate-x-1 duration-200">
|
||||
<ArrowLeft size={16} />
|
||||
Mağazaya Dön
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,34 +1,28 @@
|
||||
import React from 'react';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
import { db } from '@/lib/db';
|
||||
import {
|
||||
Terminal,
|
||||
Copy,
|
||||
Check,
|
||||
Globe,
|
||||
Webhook,
|
||||
Zap,
|
||||
ShieldCheck,
|
||||
Code2
|
||||
Code2,
|
||||
Server,
|
||||
Link as LinkIcon
|
||||
} from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
async function getMerchant(identifier: string) {
|
||||
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 query = supabaseAdmin
|
||||
.from('merchants')
|
||||
.select('*');
|
||||
const queryText = isUUID
|
||||
? 'SELECT * FROM merchants WHERE id = $1 LIMIT 1'
|
||||
: 'SELECT * FROM merchants WHERE short_id = $1 LIMIT 1';
|
||||
|
||||
if (isUUID) {
|
||||
query.eq('id', identifier);
|
||||
} else {
|
||||
query.eq('short_id', identifier);
|
||||
}
|
||||
|
||||
const { data, error } = await query.single();
|
||||
return data;
|
||||
const result = await db.query(queryText, [identifier]);
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
export default async function MerchantIntegrationPage(props: {
|
||||
@@ -45,112 +39,147 @@ export default async function MerchantIntegrationPage(props: {
|
||||
redirect(`/merchant/${identifier}/login`);
|
||||
}
|
||||
|
||||
const checkoutUrl = `https://p2cgateway.com/checkout?merchant_id=${merchant.short_id || merchant.id}&amount=100¤cy=TRY&ref_id=SİPARİŞ_123&callback_url=https://siteniz.com/basarili`;
|
||||
const host = process.env.NEXT_PUBLIC_BASE_URL || 'https://p2cgateway.com';
|
||||
const checkoutUrl = `${host}/checkout?merchant_id=${merchant.short_id || merchant.id}&amount=100¤cy=TRY&ref_id=SİPARİŞ_123`;
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl space-y-12 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-20">
|
||||
<div className="max-w-6xl space-y-16 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-32">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-black text-gray-900 tracking-tight">Teknik Entegrasyon</h1>
|
||||
<p className="text-xs text-gray-400 font-bold uppercase tracking-widest mt-2 px-1">Ödeme sistemini sitenize nasıl bağlarsınız?</p>
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6">
|
||||
<div>
|
||||
<h1 className="text-4xl font-black text-gray-900 tracking-tight">Entegrasyon Rehberi</h1>
|
||||
<p className="text-xs text-gray-400 font-black uppercase tracking-[0.3em] mt-3 px-1 border-l-4 border-blue-600 pl-4">Ödeme sistemini sisteminize dahil edin</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Start Card */}
|
||||
<div className="bg-gray-900 rounded-[40px] p-12 text-white relative overflow-hidden shadow-2xl">
|
||||
<div className="relative z-10 grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-blue-600 rounded-2xl flex items-center justify-center">
|
||||
<Zap size={24} />
|
||||
</div>
|
||||
<h2 className="text-2xl font-black">Hızlı Ödeme Linki</h2>
|
||||
{/* Methods Selection */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10">
|
||||
{/* Option 1: Quick Link */}
|
||||
<div className="bg-white p-12 rounded-[48px] border border-gray-100 shadow-xl shadow-gray-100/50 space-y-10 relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-50 rounded-full -mr-16 -mt-16 group-hover:scale-150 transition-transform duration-700"></div>
|
||||
|
||||
<div className="space-y-6 relative">
|
||||
<div className="w-16 h-16 bg-blue-600 rounded-3xl flex items-center justify-center text-white shadow-lg shadow-blue-200">
|
||||
<LinkIcon size={32} />
|
||||
</div>
|
||||
<p className="text-gray-400 text-lg leading-relaxed font-medium">
|
||||
Entegrasyonun en basit yolu, müşterilerinizi aşağıdaki URL yapısını kullanarak ödeme sayfasına yönlendirmektir.
|
||||
<h2 className="text-2xl font-black text-gray-900">1. Hızlı Ödeme Linki</h2>
|
||||
<p className="text-gray-500 font-medium leading-relaxed">
|
||||
Kod yazmanıza gerek kalmadan, müşterilerinizi doğrudan ödeme sayfamıza yönlendirebilirsiniz. Parametreleri URL üzerinden iletebilirsiniz.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/5 p-6 rounded-3xl border border-white/10 space-y-4">
|
||||
<p className="text-[10px] font-black text-gray-500 uppercase tracking-widest">Sizin Hazır Linkiniz</p>
|
||||
<div className="bg-black p-4 rounded-xl border border-white/5 font-mono text-[10px] text-blue-400 break-all leading-relaxed">
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-1">Örnek Yapı</p>
|
||||
<div className="bg-gray-50 p-6 rounded-3xl border border-gray-100 font-mono text-[11px] text-gray-600 break-all leading-relaxed relative group/code">
|
||||
{checkoutUrl}
|
||||
<button className="absolute top-4 right-4 p-2 bg-white rounded-lg shadow-sm opacity-0 group-hover/code:opacity-100 transition-opacity">
|
||||
<Copy size={14} className="text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Option 2: Server-to-Server API */}
|
||||
<div className="bg-gray-900 p-12 rounded-[48px] shadow-2xl space-y-10 relative overflow-hidden group">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-white/5 rounded-full -mr-16 -mt-16"></div>
|
||||
|
||||
<div className="space-y-6 relative">
|
||||
<div className="w-16 h-16 bg-emerald-500 rounded-3xl flex items-center justify-center text-white shadow-lg shadow-emerald-900/20">
|
||||
<Server size={32} />
|
||||
</div>
|
||||
<h2 className="text-2xl font-black text-white">2. Profesyonel API (v1)</h2>
|
||||
<p className="text-gray-400 font-medium leading-relaxed">
|
||||
Sunucu taraflı entegrasyon ile daha güvenli işlemler başlatın. Fiyat manipülasyonunu engeller ve sessiz oturumlar oluşturur.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-[10px] font-black text-gray-500 uppercase tracking-widest pl-1">Endpoint</p>
|
||||
<div className="bg-white/5 p-6 rounded-3xl border border-white/10 font-mono text-[11px] text-emerald-400">
|
||||
POST {host}/api/v1/checkout
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Credentials */}
|
||||
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm space-y-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-blue-50 rounded-2xl flex items-center justify-center text-blue-600">
|
||||
<ShieldCheck size={24} />
|
||||
</div>
|
||||
<h3 className="text-xl font-black text-gray-900">Kimlik Bilgileri</h3>
|
||||
{/* API Details Section */}
|
||||
<div className="bg-white p-12 rounded-[48px] border border-gray-100 shadow-sm space-y-12">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="w-14 h-14 bg-gray-50 rounded-2xl flex items-center justify-center text-gray-900">
|
||||
<ShieldCheck size={28} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="p-6 bg-gray-50 rounded-3xl border border-gray-100 space-y-3">
|
||||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-1">Merchant ID (Firma Kimliği)</label>
|
||||
<div className="flex items-center justify-between gap-3 bg-white p-4 rounded-xl border border-gray-200">
|
||||
<code className="text-xs font-mono font-bold text-gray-600 truncate">{merchant.id}</code>
|
||||
<Copy size={14} className="text-gray-300 cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-gray-50 rounded-3xl border border-gray-100 space-y-3">
|
||||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-1">API Secret Key</label>
|
||||
<div className="flex items-center justify-between gap-3 bg-white p-4 rounded-xl border border-gray-200">
|
||||
<code className="text-xs font-mono font-bold text-gray-600">••••••••••••••••••••••••</code>
|
||||
<button className="text-[10px] font-black text-blue-600 uppercase">Göster</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-black text-gray-900">API Erişimi ve Güvenlik</h3>
|
||||
<p className="text-xs text-gray-400 font-bold uppercase tracking-widest mt-1">İsteklerinizi doğrulamak için bu anahtarları kullanın</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Webhooks */}
|
||||
<div className="bg-white p-10 rounded-[40px] border border-gray-100 shadow-sm space-y-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-purple-50 rounded-2xl flex items-center justify-center text-purple-600">
|
||||
<Webhook size={24} />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-10">
|
||||
<div className="space-y-4">
|
||||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest ml-2">Merchant ID</label>
|
||||
<div className="bg-gray-50 p-5 rounded-2xl border border-gray-100 flex items-center justify-between">
|
||||
<code className="text-xs font-mono font-bold text-gray-600">{merchant.id}</code>
|
||||
<Copy size={16} className="text-gray-300 cursor-pointer hover:text-blue-600 transition" />
|
||||
</div>
|
||||
<h3 className="text-xl font-black text-gray-900">Webhook Yapılandırması</h3>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-500 font-medium leading-relaxed">
|
||||
Ödeme başarılı olduğunda sistemimiz belirtilen adrese bir POST isteği gönderir.
|
||||
<div className="space-y-4">
|
||||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest ml-2">API Secret Key</label>
|
||||
<div className="bg-gray-50 p-5 rounded-2xl border border-gray-100 flex items-center justify-between">
|
||||
<code className="text-xs font-mono font-bold text-gray-600">
|
||||
{merchant.api_key.substring(0, 8)}••••••••••••••••••••••••
|
||||
</code>
|
||||
<button className="text-[10px] font-black text-blue-600 uppercase tracking-widest hover:underline">Anahtarı Göster</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Webhook Settings */}
|
||||
<div className="bg-white p-12 rounded-[48px] border border-gray-100 shadow-sm space-y-12">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="w-14 h-14 bg-purple-50 rounded-2xl flex items-center justify-center text-purple-600">
|
||||
<Webhook size={28} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-black text-gray-900">Olay Bildirimleri (Webhooks)</h3>
|
||||
<p className="text-xs text-gray-400 font-bold uppercase tracking-widest mt-1">Ödeme sonuçlarını anlık olarak sunucunuzda karşılayın</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
<p className="text-gray-500 font-medium leading-relaxed max-w-2xl">
|
||||
İşlem tamamlandığında sistemimiz belirttiğiniz URL'ye <code className="bg-gray-100 px-2 py-1 rounded text-blue-600 font-bold">POST</code> isteği gönderir. Bu isteğin içerisinde işlemin tüm detayları yer alır.
|
||||
</p>
|
||||
|
||||
<div className="p-6 bg-gray-50 rounded-3xl border border-gray-100 space-y-4">
|
||||
<div className="p-8 bg-gray-50 rounded-[32px] border border-gray-100 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Mevcut Webhook URL</span>
|
||||
<span className={`text-[10px] font-black px-2 py-0.5 rounded-md ${merchant.webhook_url ? 'bg-emerald-50 text-emerald-600' : 'bg-red-50 text-red-600'}`}>
|
||||
{merchant.webhook_url ? 'AKTİF' : 'AYARLANMAMIŞ'}
|
||||
<span className="text-[11px] font-black text-gray-400 uppercase tracking-widest ml-2">Webhook URL</span>
|
||||
<span className={`text-[10px] font-black px-3 py-1 rounded-full ${merchant.webhook_url ? 'bg-emerald-100 text-emerald-700' : 'bg-red-100 text-red-700'}`}>
|
||||
{merchant.webhook_url ? 'HİZMETE HAZIR' : 'HENÜZ TANIMLANMAMIŞ'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-xl border border-gray-200">
|
||||
<code className="text-xs font-bold text-gray-600 truncate block">
|
||||
{merchant.webhook_url || 'https://henuz-bir-adres-tanimlanmadi.com'}
|
||||
<div className="bg-white p-6 rounded-2xl border border-gray-200">
|
||||
<code className="text-sm font-bold text-gray-700 break-all">
|
||||
{merchant.webhook_url || 'https://siteniz.com/api/payment-callback'}
|
||||
</code>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400 font-bold uppercase text-center mt-2 leading-relaxed">
|
||||
Webook URL adresinizi değiştirmek için <br /> destek ekibi ile iletişime geçin.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Resources */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* Footer Resources */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{[
|
||||
{ title: 'API Referansı', icon: Code2, color: 'blue' },
|
||||
{ title: 'Hazır Kütüphaneler', icon: Terminal, color: 'emerald' },
|
||||
{ title: 'Teknik Destek', icon: Globe, color: 'purple' },
|
||||
{ title: 'Postman Koleksiyonu', icon: Terminal, color: 'blue' },
|
||||
{ title: 'Geliştirici Dokümanları', icon: Code2, color: 'emerald' },
|
||||
{ title: 'Teknik Yardım', icon: Globe, color: 'gray' },
|
||||
].map((r) => (
|
||||
<div key={r.title} className="bg-white p-8 rounded-[32px] border border-gray-100 shadow-sm flex items-center gap-6 hover:border-gray-300 transition-colors cursor-pointer group">
|
||||
<div className={`w-12 h-12 bg-${r.color}-50 rounded-2xl flex items-center justify-center text-${r.color}-600 group-hover:bg-${r.color}-600 group-hover:text-white transition-all`}>
|
||||
<r.icon size={22} />
|
||||
<div key={r.title} className="bg-white p-8 rounded-[32px] border border-gray-100 shadow-sm flex items-center gap-6 hover:shadow-lg transition-all cursor-pointer group hover:-translate-y-1">
|
||||
<div className={`w-14 h-14 bg-gray-50 rounded-2xl flex items-center justify-center text-gray-400 group-hover:bg-blue-600 group-hover:text-white transition-all`}>
|
||||
<r.icon size={24} />
|
||||
</div>
|
||||
<span className="text-sm font-black text-gray-900 leading-tight">{r.title}</span>
|
||||
<span className="text-sm font-black text-gray-900 uppercase tracking-tight">{r.title}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ShieldCheck
|
||||
} from 'lucide-react';
|
||||
import MerchantSidebar from '@/components/merchant/MerchantSidebar';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export default async function MerchantLayout({
|
||||
children,
|
||||
@@ -26,11 +26,8 @@ export default async function MerchantLayout({
|
||||
let resolvedId = identifier;
|
||||
|
||||
if (!isUUID) {
|
||||
const { data: merchant } = await supabaseAdmin
|
||||
.from('merchants')
|
||||
.select('id')
|
||||
.eq('short_id', identifier)
|
||||
.single();
|
||||
const result = await db.query('SELECT id FROM merchants WHERE short_id = $1 LIMIT 1', [identifier]);
|
||||
const merchant = result.rows[0];
|
||||
if (merchant) {
|
||||
resolvedId = merchant.id;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
import { db } from '@/lib/db';
|
||||
import {
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
@@ -21,30 +21,23 @@ async function getMerchantData(identifier: string) {
|
||||
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);
|
||||
|
||||
// Fetch merchant details
|
||||
const query = supabaseAdmin
|
||||
.from('merchants')
|
||||
.select('*');
|
||||
const mQueryText = isUUID
|
||||
? 'SELECT * FROM merchants WHERE id = $1 LIMIT 1'
|
||||
: 'SELECT * FROM merchants WHERE short_id = $1 LIMIT 1';
|
||||
|
||||
const mResult = await db.query(mQueryText, [identifier]);
|
||||
const merchant = mResult.rows[0];
|
||||
|
||||
if (isUUID) {
|
||||
query.eq('id', identifier);
|
||||
} else {
|
||||
query.eq('short_id', identifier);
|
||||
}
|
||||
|
||||
const { data: merchant, error: mError } = await query.single();
|
||||
|
||||
if (mError || !merchant) return null;
|
||||
if (!merchant) return null;
|
||||
|
||||
const id = merchant.id; // Always use UUID for internal lookups
|
||||
|
||||
// Fetch merchant transactions
|
||||
const { data: transactions, error: tError } = await supabaseAdmin
|
||||
.from('transactions')
|
||||
.select('*')
|
||||
.eq('merchant_id', id)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (tError) return null;
|
||||
const tResult = await db.query(
|
||||
'SELECT * FROM transactions WHERE merchant_id = $1 ORDER BY created_at DESC',
|
||||
[id]
|
||||
);
|
||||
const transactions = tResult.rows;
|
||||
|
||||
const successfulTransactions = transactions.filter(t => t.status === 'succeeded');
|
||||
const totalRevenue = successfulTransactions.reduce((acc, t) => acc + Number(t.amount), 0);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { supabaseAdmin } from '@/lib/supabase-admin';
|
||||
import { db } from '@/lib/db';
|
||||
import { format } from 'date-fns';
|
||||
import { tr } from 'date-fns/locale';
|
||||
import {
|
||||
@@ -16,27 +16,20 @@ import { redirect } from 'next/navigation';
|
||||
async function getMerchantTransactions(identifier: string) {
|
||||
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 query = supabaseAdmin
|
||||
.from('merchants')
|
||||
.select('id')
|
||||
const mQueryText = isUUID
|
||||
? 'SELECT id FROM merchants WHERE id = $1 LIMIT 1'
|
||||
: 'SELECT id FROM merchants WHERE short_id = $1 LIMIT 1';
|
||||
|
||||
if (isUUID) {
|
||||
query.eq('id', identifier);
|
||||
} else {
|
||||
query.eq('short_id', identifier);
|
||||
}
|
||||
|
||||
const { data: merchant } = await query.single();
|
||||
const mResult = await db.query(mQueryText, [identifier]);
|
||||
const merchant = mResult.rows[0];
|
||||
if (!merchant) return null;
|
||||
|
||||
const { data, error } = await supabaseAdmin
|
||||
.from('transactions')
|
||||
.select('*')
|
||||
.eq('merchant_id', merchant.id)
|
||||
.order('created_at', { ascending: false });
|
||||
const tResult = await db.query(
|
||||
'SELECT * FROM transactions WHERE merchant_id = $1 ORDER BY created_at DESC',
|
||||
[merchant.id]
|
||||
);
|
||||
|
||||
if (error) return null;
|
||||
return data;
|
||||
return tResult.rows;
|
||||
}
|
||||
|
||||
export default async function MerchantTransactionsPage(props: {
|
||||
@@ -53,7 +46,8 @@ export default async function MerchantTransactionsPage(props: {
|
||||
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);
|
||||
let resolvedId = identifier;
|
||||
if (!isUUID) {
|
||||
const { data: merchant } = await supabaseAdmin.from('merchants').select('id').eq('short_id', identifier).single();
|
||||
const result = await db.query('SELECT id FROM merchants WHERE short_id = $1 LIMIT 1', [identifier]);
|
||||
const merchant = result.rows[0];
|
||||
if (merchant) resolvedId = merchant.id;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user