diff --git a/IPN_INTEGRATION_README.md b/IPN_INTEGRATION_README.md index 63da981..d663c7d 100644 --- a/IPN_INTEGRATION_README.md +++ b/IPN_INTEGRATION_README.md @@ -159,9 +159,9 @@ WHERE drop_id = ? If inventory is available: ```sql --- Create sale -INSERT INTO sales (drop_id, buyer_id, size, payment_id) -VALUES (?, ?, ?, ?) +-- Create sale (include buyer_data_id for delivery information) +INSERT INTO sales (drop_id, buyer_id, buyer_data_id, size, payment_id) +VALUES (?, ?, ?, ?, ?) -- Delete pending order DELETE FROM pending_orders WHERE id = ? @@ -234,11 +234,11 @@ async function handleIPNCallback(callbackData) { return { error: 'Inventory no longer available' }; } - // Step 5: Create sale + // Step 5: Create sale (include buyer_data_id for delivery information) await db.transaction(async (tx) => { await tx.query( - 'INSERT INTO sales (drop_id, buyer_id, size, payment_id) VALUES (?, ?, ?, ?)', - [pendingOrder.drop_id, pendingOrder.buyer_id, pendingOrder.size, pendingOrder.payment_id] + 'INSERT INTO sales (drop_id, buyer_id, buyer_data_id, size, payment_id) VALUES (?, ?, ?, ?, ?)', + [pendingOrder.drop_id, pendingOrder.buyer_id, pendingOrder.buyer_data_id, pendingOrder.size, pendingOrder.payment_id] ); await tx.query('DELETE FROM pending_orders WHERE id = ?', [pendingOrder.id]); }); @@ -367,8 +367,8 @@ WHERE drop_id = ? AND expires_at > NOW() ```sql START TRANSACTION; -INSERT INTO sales (drop_id, buyer_id, size, payment_id) -VALUES (?, ?, ?, ?); +INSERT INTO sales (drop_id, buyer_id, buyer_data_id, size, payment_id) +VALUES (?, ?, ?, ?, ?); DELETE FROM pending_orders WHERE id = ?; diff --git a/app/api/buyer-data/get-or-create/route.ts b/app/api/buyer-data/get-or-create/route.ts new file mode 100644 index 0000000..66307ff --- /dev/null +++ b/app/api/buyer-data/get-or-create/route.ts @@ -0,0 +1,74 @@ +import { NextRequest, NextResponse } from 'next/server' +import { cookies } from 'next/headers' +import pool from '@/lib/db' + +// POST /api/buyer-data/get-or-create - Get existing buyer_data or create new one +export async function POST(request: NextRequest) { + try { + const cookieStore = cookies() + const buyerIdCookie = cookieStore.get('buyer_id')?.value + + if (!buyerIdCookie) { + return NextResponse.json( + { error: 'Authentication required' }, + { status: 401 } + ) + } + + const buyer_id = parseInt(buyerIdCookie, 10) + const body = await request.json() + const { fullname, address, phone } = body + + // Validate required fields + if (!fullname || !address || !phone) { + return NextResponse.json( + { error: 'Full name, address, and phone are required' }, + { status: 400 } + ) + } + + // Validate phone format (basic validation - 10-15 digits) + const phoneRegex = /^[+]?[\d\s\-()]{10,15}$/ + if (!phoneRegex.test(phone)) { + return NextResponse.json( + { error: 'Invalid phone number format' }, + { status: 400 } + ) + } + + // Check if buyer_data with same values already exists for this buyer + const [existingRows] = await pool.execute( + 'SELECT id FROM buyer_data WHERE buyer_id = ? AND fullname = ? AND address = ? AND phone = ?', + [buyer_id, fullname.trim(), address.trim(), phone.trim()] + ) + + const existing = existingRows as any[] + if (existing.length > 0) { + // Return existing buyer_data_id + return NextResponse.json({ + buyer_data_id: existing[0].id, + created: false, + }) + } + + // Create new buyer_data record + const [result] = await pool.execute( + 'INSERT INTO buyer_data (buyer_id, fullname, address, phone) VALUES (?, ?, ?, ?)', + [buyer_id, fullname.trim(), address.trim(), phone.trim()] + ) + + const buyer_data_id = (result as any).insertId + + return NextResponse.json({ + buyer_data_id, + created: true, + }, { status: 201 }) + } catch (error) { + console.error('Error getting or creating buyer_data:', error) + return NextResponse.json( + { error: 'Failed to process buyer data' }, + { status: 500 } + ) + } +} + diff --git a/app/api/buyer-data/route.ts b/app/api/buyer-data/route.ts new file mode 100644 index 0000000..fead0c1 --- /dev/null +++ b/app/api/buyer-data/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from 'next/server' +import { cookies } from 'next/headers' +import pool from '@/lib/db' + +// GET /api/buyer-data - Get the most recent buyer_data for the current buyer +export async function GET(request: NextRequest) { + try { + const cookieStore = cookies() + const buyerIdCookie = cookieStore.get('buyer_id')?.value + + if (!buyerIdCookie) { + return NextResponse.json( + { error: 'Authentication required' }, + { status: 401 } + ) + } + + const buyer_id = parseInt(buyerIdCookie, 10) + + // Get the most recent buyer_data for this buyer + // Note: buyer_data table doesn't have created_at, so we'll get the one with highest ID (most recent) + const [rows] = await pool.execute( + 'SELECT id, fullname, address, phone FROM buyer_data WHERE buyer_id = ? ORDER BY id DESC LIMIT 1', + [buyer_id] + ) + + const buyerData = rows as any[] + + if (buyerData.length === 0) { + return NextResponse.json( + { buyer_data: null }, + { status: 200 } + ) + } + + return NextResponse.json({ + buyer_data: buyerData[0], + }) + } catch (error) { + console.error('Error fetching buyer_data:', error) + return NextResponse.json( + { error: 'Failed to fetch buyer data' }, + { status: 500 } + ) + } +} + diff --git a/app/api/payments/create-invoice/route.ts b/app/api/payments/create-invoice/route.ts index c0aca9e..0445626 100644 --- a/app/api/payments/create-invoice/route.ts +++ b/app/api/payments/create-invoice/route.ts @@ -22,12 +22,25 @@ export async function POST(request: NextRequest) { const buyer_id = parseInt(buyerIdCookie, 10) const body = await request.json() - const { drop_id, size, pay_currency } = body + const { drop_id, size, pay_currency, buyer_data_id } = body // Validate required fields - if (!drop_id || !size) { + if (!drop_id || !size || !buyer_data_id) { return NextResponse.json( - { error: 'Missing required fields: drop_id, size' }, + { error: 'Missing required fields: drop_id, size, and buyer_data_id' }, + { status: 400 } + ) + } + + // Verify buyer_data_id exists and belongs to the buyer + const [buyerDataRows] = await pool.execute( + 'SELECT id FROM buyer_data WHERE id = ? AND buyer_id = ?', + [buyer_data_id, buyer_id] + ) + const buyerData = buyerDataRows as any[] + if (buyerData.length === 0) { + return NextResponse.json( + { error: 'Invalid buyer_data_id or buyer_data does not belong to user' }, { status: 400 } ) } @@ -176,8 +189,8 @@ export async function POST(request: NextRequest) { // Store pending order with expiration time (atomically reserves inventory) // payment.payment_id is the NOWPayments payment ID await connection.execute( - 'INSERT INTO pending_orders (payment_id, order_id, drop_id, buyer_id, size, price_amount, price_currency, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', - [payment.payment_id, orderId, drop_id, buyer_id, size, priceAmount, nowPaymentsConfig.currency, expiresAt] + 'INSERT INTO pending_orders (payment_id, order_id, drop_id, buyer_id, buyer_data_id, size, price_amount, price_currency, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', + [payment.payment_id, orderId, drop_id, buyer_id, buyer_data_id, size, priceAmount, nowPaymentsConfig.currency, expiresAt] ) // Commit transaction - inventory is now reserved diff --git a/app/components/Drop.tsx b/app/components/Drop.tsx index d60a693..3cd4b8f 100644 --- a/app/components/Drop.tsx +++ b/app/components/Drop.tsx @@ -32,6 +32,9 @@ export default function Drop() { const [selectedCurrency, setSelectedCurrency] = useState('btc') const [availableCurrencies, setAvailableCurrencies] = useState([]) const [loadingCurrencies, setLoadingCurrencies] = useState(false) + const [buyerFullname, setBuyerFullname] = useState('') + const [buyerAddress, setBuyerAddress] = useState('') + const [buyerPhone, setBuyerPhone] = useState('') const [showConfirmModal, setShowConfirmModal] = useState(false) const [showAuthModal, setShowAuthModal] = useState(false) const [showPaymentModal, setShowPaymentModal] = useState(false) @@ -192,29 +195,82 @@ export default function Drop() { } } + const fetchBuyerData = async () => { + try { + const response = await fetch('/api/buyer-data', { + credentials: 'include', + }) + if (response.ok) { + const data = await response.json() + if (data.buyer_data) { + // Autofill form fields with existing buyer data + setBuyerFullname(data.buyer_data.fullname || '') + setBuyerAddress(data.buyer_data.address || '') + setBuyerPhone(data.buyer_data.phone || '') + } + } + } catch (error) { + console.error('Error fetching buyer data:', error) + } + } + const handleJoinDrop = () => { // Check if user is logged in if (!user) { setShowAuthModal(true) return } - // Fetch available currencies when opening confirm modal + // Fetch available currencies and buyer data when opening confirm modal fetchAvailableCurrencies() + fetchBuyerData() setShowConfirmModal(true) } const handleLogin = (loggedInUser: User) => { setUser(loggedInUser) setShowAuthModal(false) - // After login, show the confirmation modal + // After login, fetch buyer data and show the confirmation modal + fetchAvailableCurrencies() + fetchBuyerData() setShowConfirmModal(true) } const handleConfirmPurchase = async () => { if (!drop) return + // Validate buyer data fields + if (!buyerFullname.trim() || !buyerAddress.trim() || !buyerPhone.trim()) { + setErrorMessage('Please fill in all delivery information (full name, address, and phone)') + setShowErrorModal(true) + return + } + setProcessing(true) try { + // First, get or create buyer_data + const buyerDataResponse = await fetch('/api/buyer-data/get-or-create', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ + fullname: buyerFullname.trim(), + address: buyerAddress.trim(), + phone: buyerPhone.trim(), + }), + }) + + if (!buyerDataResponse.ok) { + const error = await buyerDataResponse.json() + setErrorMessage(error.error || 'Failed to save delivery information') + setShowErrorModal(true) + setProcessing(false) + return + } + + const buyerData = await buyerDataResponse.json() + // Create NOWPayments payment const response = await fetch('/api/payments/create-invoice', { method: 'POST', @@ -226,6 +282,7 @@ export default function Drop() { drop_id: drop.id, size: selectedSize, // Size in grams pay_currency: selectedCurrency, // Selected payment currency + buyer_data_id: buyerData.buyer_data_id, // Buyer delivery data ID }), }) @@ -481,9 +538,87 @@ export default function Drop() {

Price per {drop.unit}: {(drop.ppu / 1000).toFixed(2)} CHF

- + + {/* Delivery Information */} +
+

+ Delivery Information +

+ +
+ + setBuyerFullname(e.target.value)} + placeholder="Enter your full name" + required + style={{ + width: '100%', + padding: '12px', + background: 'var(--bg-soft)', + border: '1px solid var(--border)', + borderRadius: '8px', + fontSize: '14px', + color: 'var(--text)', + boxSizing: 'border-box', + }} + /> +
+ +
+ +