buyer data

This commit is contained in:
root
2025-12-21 11:12:02 +01:00
parent 67646c75a4
commit 514e04f43d
6 changed files with 359 additions and 23 deletions

View File

@@ -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 }
)
}
}

View File

@@ -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 }
)
}
}

View File

@@ -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

View File

@@ -32,6 +32,9 @@ export default function Drop() {
const [selectedCurrency, setSelectedCurrency] = useState<string>('btc')
const [availableCurrencies, setAvailableCurrencies] = useState<string[]>([])
const [loadingCurrencies, setLoadingCurrencies] = useState(false)
const [buyerFullname, setBuyerFullname] = useState<string>('')
const [buyerAddress, setBuyerAddress] = useState<string>('')
const [buyerPhone, setBuyerPhone] = useState<string>('')
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() {
<p style={{ marginBottom: '12px', color: 'var(--muted)' }}>
<strong>Price per {drop.unit}:</strong> {(drop.ppu / 1000).toFixed(2)} CHF
</p>
{/* Delivery Information */}
<div style={{ marginTop: '24px', marginBottom: '16px' }}>
<h3 style={{ marginBottom: '16px', fontSize: '16px', color: 'var(--text)' }}>
Delivery Information
</h3>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', color: 'var(--muted)' }}>
<strong>Full Name *</strong>
</label>
<input
type="text"
value={buyerFullname}
onChange={(e) => 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',
}}
/>
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', color: 'var(--muted)' }}>
<strong>Address *</strong>
</label>
<textarea
value={buyerAddress}
onChange={(e) => setBuyerAddress(e.target.value)}
placeholder="Enter your delivery address"
required
rows={3}
style={{
width: '100%',
padding: '12px',
background: 'var(--bg-soft)',
border: '1px solid var(--border)',
borderRadius: '8px',
fontSize: '14px',
color: 'var(--text)',
fontFamily: 'inherit',
resize: 'vertical',
boxSizing: 'border-box',
}}
/>
</div>
<div style={{ marginBottom: '12px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', color: 'var(--muted)' }}>
<strong>Phone Number *</strong>
</label>
<input
type="tel"
value={buyerPhone}
onChange={(e) => setBuyerPhone(e.target.value)}
placeholder="Enter your phone number"
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',
}}
/>
</div>
</div>
{/* Currency Selection */}
<div style={{ marginBottom: '16px' }}>
<div style={{ marginTop: '24px', marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontSize: '14px', color: 'var(--muted)' }}>
<strong>Payment Currency:</strong>
</label>