This commit is contained in:
root
2025-12-31 08:19:59 +00:00
parent 0d8c2ea3a3
commit d138dae2ca
8 changed files with 785 additions and 8 deletions

View File

@@ -0,0 +1,216 @@
import { NextRequest, NextResponse } from 'next/server'
import { cookies } from 'next/headers'
import pool from '@/lib/db'
import { ALLOWED_PAYMENT_CURRENCIES, isAllowedCurrency } from '@/lib/payment-currencies'
// POST /api/referral-points/redeem - Redeem referral points to crypto
export async function POST(request: NextRequest) {
const connection = await pool.getConnection()
try {
const cookieStore = await 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 { points, crypto_currency, wallet_address } = body
// Validate required fields
if (!points || !crypto_currency || !wallet_address) {
return NextResponse.json(
{ error: 'Missing required fields: points, crypto_currency, wallet_address' },
{ status: 400 }
)
}
const pointsToRedeem = parseFloat(points)
const normalizedCryptoCurrency = crypto_currency.toLowerCase().trim()
const normalizedWalletAddress = wallet_address.trim()
// Validate points amount
if (isNaN(pointsToRedeem) || pointsToRedeem <= 0) {
return NextResponse.json(
{ error: 'Points must be a positive number' },
{ status: 400 }
)
}
// Validate crypto currency
if (!isAllowedCurrency(normalizedCryptoCurrency)) {
return NextResponse.json(
{ error: `Unsupported cryptocurrency. Allowed: ${ALLOWED_PAYMENT_CURRENCIES.join(', ')}` },
{ status: 400 }
)
}
// Basic wallet address validation (non-empty, reasonable length)
if (normalizedWalletAddress.length < 10 || normalizedWalletAddress.length > 255) {
return NextResponse.json(
{ error: 'Invalid wallet address format' },
{ status: 400 }
)
}
await connection.beginTransaction()
try {
// Get buyer's current points balance
const [buyerRows] = await connection.execute(
'SELECT referral_points FROM buyers WHERE id = ? FOR UPDATE',
[buyer_id]
)
const buyers = buyerRows as any[]
if (buyers.length === 0) {
await connection.rollback()
return NextResponse.json(
{ error: 'Buyer not found' },
{ status: 404 }
)
}
const currentPoints = parseFloat(buyers[0].referral_points) || 0
// Get redemption settings
const [settingsRows] = await connection.execute(
'SELECT setting_key, setting_value FROM referral_settings'
)
const settings = settingsRows as any[]
const pointsToCryptoChf = parseFloat(
settings.find(s => s.setting_key === 'points_to_crypto_chf')?.setting_value || '100'
)
const minRedemptionPoints = parseFloat(
settings.find(s => s.setting_key === 'min_redemption_points')?.setting_value || '1000'
)
// Validate minimum redemption amount
if (pointsToRedeem < minRedemptionPoints) {
await connection.rollback()
return NextResponse.json(
{ error: `Minimum redemption is ${minRedemptionPoints} points` },
{ status: 400 }
)
}
// Validate user has enough points
if (currentPoints < pointsToRedeem) {
await connection.rollback()
return NextResponse.json(
{ error: 'Insufficient points' },
{ status: 400 }
)
}
// Calculate CHF value of points
const chfValue = pointsToRedeem / pointsToCryptoChf
// TODO: Get current crypto exchange rate
// For now, we'll use a placeholder that would need to be replaced with actual exchange rate API
// This should fetch the current rate from an exchange API (e.g., CoinGecko, Binance, etc.)
const cryptoExchangeRate = await getCryptoExchangeRate(normalizedCryptoCurrency, 'chf')
if (!cryptoExchangeRate) {
await connection.rollback()
return NextResponse.json(
{ error: 'Failed to fetch exchange rate. Please try again later.' },
{ status: 500 }
)
}
// Calculate crypto amount
const cryptoAmount = chfValue / cryptoExchangeRate
// Deduct points from buyer's balance
const newBalance = currentPoints - pointsToRedeem
await connection.execute(
'UPDATE buyers SET referral_points = ? WHERE id = ?',
[newBalance, buyer_id]
)
// Create redemption record
const [redemptionResult] = await connection.execute(
`INSERT INTO point_redemptions
(buyer_id, points, crypto_currency, wallet_address, crypto_amount, status)
VALUES (?, ?, ?, ?, ?, 'pending')`,
[buyer_id, pointsToRedeem, normalizedCryptoCurrency, normalizedWalletAddress, cryptoAmount]
)
const redemptionId = (redemptionResult as any).insertId
// Record transaction
await connection.execute(
`INSERT INTO referral_point_transactions
(buyer_id, points, type, description)
VALUES (?, ?, 'redeemed', ?)`,
[
buyer_id,
pointsToRedeem,
`Points redeemed to ${normalizedCryptoCurrency.toUpperCase()} (Redemption #${redemptionId})`
]
)
await connection.commit()
return NextResponse.json({
success: true,
redemption_id: redemptionId,
points_redeemed: pointsToRedeem,
crypto_currency: normalizedCryptoCurrency,
crypto_amount: cryptoAmount,
chf_value: chfValue,
new_balance: newBalance,
message: 'Redemption request created successfully. Your crypto will be sent within 24-48 hours.'
})
} catch (error) {
await connection.rollback()
throw error
}
} catch (error: any) {
console.error('Error redeeming points:', error)
return NextResponse.json(
{ error: error.message || 'Failed to redeem points' },
{ status: 500 }
)
} finally {
connection.release()
}
}
// Helper function to get crypto exchange rate
// TODO: Replace with actual exchange rate API integration
async function getCryptoExchangeRate(crypto: string, fiat: string): Promise<number | null> {
try {
// Placeholder: In production, this should call a real exchange rate API
// Examples: CoinGecko, Binance, Coinbase, etc.
// For now, return a mock rate (this should be replaced)
// In production, you would do something like:
// const response = await fetch(`https://api.coingecko.com/api/v3/simple/price?ids=${crypto}&vs_currencies=${fiat}`)
// const data = await response.json()
// return data[crypto][fiat]
// Mock rates (CHF per 1 unit of crypto) - REPLACE WITH REAL API
const mockRates: Record<string, number> = {
'btc': 85000,
'eth': 2500,
'sol': 100,
'xrp': 0.6,
'bnbbsc': 300,
'usdterc20': 0.9, // Approximate CHF per USDT
}
return mockRates[crypto.toLowerCase()] || null
} catch (error) {
console.error('Error fetching exchange rate:', error)
return null
}
}

View File

@@ -44,6 +44,12 @@ export async function GET(request: NextRequest) {
const pointsPerChf = parseFloat( const pointsPerChf = parseFloat(
settings.find(s => s.setting_key === 'points_per_chf')?.setting_value || '10' settings.find(s => s.setting_key === 'points_per_chf')?.setting_value || '10'
) )
const pointsToCryptoChf = parseFloat(
settings.find(s => s.setting_key === 'points_to_crypto_chf')?.setting_value || '100'
)
const minRedemptionPoints = parseFloat(
settings.find(s => s.setting_key === 'min_redemption_points')?.setting_value || '1000'
)
// Calculate maximum discount available // Calculate maximum discount available
const maxDiscountChf = referralPoints / pointsToChf const maxDiscountChf = referralPoints / pointsToChf
@@ -52,6 +58,8 @@ export async function GET(request: NextRequest) {
referral_points: referralPoints, referral_points: referralPoints,
points_to_chf: pointsToChf, points_to_chf: pointsToChf,
points_per_chf: pointsPerChf, points_per_chf: pointsPerChf,
points_to_crypto_chf: pointsToCryptoChf,
min_redemption_points: minRedemptionPoints,
max_discount_chf: maxDiscountChf, max_discount_chf: maxDiscountChf,
}) })
} catch (error) { } catch (error) {

View File

@@ -960,6 +960,8 @@ export default function Drop() {
padding: '32px', padding: '32px',
maxWidth: '500px', maxWidth: '500px',
width: '100%', width: '100%',
maxHeight: '90vh',
overflowY: 'auto',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)', boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)',
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
@@ -1241,6 +1243,7 @@ export default function Drop() {
display: 'flex', display: 'flex',
gap: '12px', gap: '12px',
justifyContent: 'flex-end', justifyContent: 'flex-end',
marginTop: '24px',
}} }}
> >
<button <button

View File

@@ -2,6 +2,7 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import AuthModal from './AuthModal' import AuthModal from './AuthModal'
import RedeemPointsModal from './RedeemPointsModal'
import LanguageSwitcher from './LanguageSwitcher' import LanguageSwitcher from './LanguageSwitcher'
import { useI18n } from '@/lib/i18n' import { useI18n } from '@/lib/i18n'
@@ -16,6 +17,7 @@ export default function Nav() {
const { t } = useI18n() const { t } = useI18n()
const [user, setUser] = useState<User | null>(null) const [user, setUser] = useState<User | null>(null)
const [showAuthModal, setShowAuthModal] = useState(false) const [showAuthModal, setShowAuthModal] = useState(false)
const [showRedeemModal, setShowRedeemModal] = useState(false)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
@@ -73,6 +75,23 @@ export default function Nav() {
} }
} }
const handleRedeemSuccess = async () => {
// Refresh points after successful redemption
if (user) {
try {
const pointsResponse = await fetch('/api/referral-points', {
credentials: 'include',
})
if (pointsResponse.ok) {
const pointsData = await pointsResponse.json()
setUser({ ...user, referral_points: pointsData.referral_points })
}
} catch (error) {
console.error('Error fetching referral points:', error)
}
}
}
return ( return (
<> <>
<nav> <nav>
@@ -104,14 +123,38 @@ export default function Nav() {
{user.username} {user.username}
</span> </span>
{user.referral_points !== undefined && user.referral_points > 0 && ( {user.referral_points !== undefined && user.referral_points > 0 && (
<span style={{ <>
color: '#0a7931', <span style={{
fontSize: '14px', color: '#0a7931',
marginLeft: '12px', fontSize: '14px',
fontWeight: 500, marginLeft: '12px',
}}> fontWeight: 500,
{user.referral_points.toFixed(0)} pts }}>
</span> {user.referral_points.toFixed(0)} pts
</span>
<button
onClick={() => {
setShowRedeemModal(true)
setMobileMenuOpen(false)
}}
style={{
background: 'transparent',
border: '1px solid #0a7931',
color: '#0a7931',
padding: '8px 16px',
borderRadius: '8px',
fontSize: '14px',
marginLeft: '12px',
lineHeight: '1',
boxSizing: 'border-box',
display: 'inline-block',
cursor: 'pointer',
fontWeight: 500,
}}
>
Redeem
</button>
</>
)} )}
<a <a
href="/orders" href="/orders"
@@ -189,6 +232,15 @@ export default function Nav() {
onClose={() => setShowAuthModal(false)} onClose={() => setShowAuthModal(false)}
onLogin={handleLogin} onLogin={handleLogin}
/> />
{user && user.referral_points !== undefined && (
<RedeemPointsModal
isOpen={showRedeemModal}
onClose={() => setShowRedeemModal(false)}
currentPoints={user.referral_points}
onRedeemSuccess={handleRedeemSuccess}
/>
)}
</> </>
) )
} }

View File

@@ -0,0 +1,411 @@
'use client'
import { useState, useEffect } from 'react'
import { useI18n } from '@/lib/i18n'
import { ALLOWED_PAYMENT_CURRENCIES } from '@/lib/payment-currencies'
interface RedeemPointsModalProps {
isOpen: boolean
onClose: () => void
currentPoints: number
onRedeemSuccess: () => void
}
export default function RedeemPointsModal({
isOpen,
onClose,
currentPoints,
onRedeemSuccess,
}: RedeemPointsModalProps) {
const { t } = useI18n()
const [pointsToRedeem, setPointsToRedeem] = useState<string>('')
const [cryptoCurrency, setCryptoCurrency] = useState<string>('btc')
const [walletAddress, setWalletAddress] = useState<string>('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string>('')
const [success, setSuccess] = useState(false)
const [redemptionDetails, setRedemptionDetails] = useState<any>(null)
const [pointsToCryptoChf, setPointsToCryptoChf] = useState<number>(100)
const [minRedemptionPoints, setMinRedemptionPoints] = useState<number>(1000)
const [estimatedCrypto, setEstimatedCrypto] = useState<number>(0)
useEffect(() => {
if (isOpen) {
fetchRedemptionSettings()
// Reset form
setPointsToRedeem('')
setWalletAddress('')
setCryptoCurrency('btc')
setError('')
setSuccess(false)
setRedemptionDetails(null)
}
}, [isOpen])
const fetchRedemptionSettings = async () => {
try {
const response = await fetch('/api/referral-points', {
credentials: 'include',
})
if (response.ok) {
const data = await response.json()
setPointsToCryptoChf(data.points_to_crypto_chf || data.points_to_chf || 100)
setMinRedemptionPoints(data.min_redemption_points || 1000)
}
} catch (error) {
console.error('Error fetching redemption settings:', error)
}
}
useEffect(() => {
// Calculate estimated crypto amount when points or currency changes
if (pointsToRedeem && !isNaN(parseFloat(pointsToRedeem))) {
const points = parseFloat(pointsToRedeem)
const chfValue = points / pointsToCryptoChf
// Mock exchange rates (should match API)
const mockRates: Record<string, number> = {
'btc': 85000,
'eth': 2500,
'sol': 100,
'xrp': 0.6,
'bnbbsc': 300,
'usdterc20': 0.9,
}
const rate = mockRates[cryptoCurrency.toLowerCase()] || 1
setEstimatedCrypto(chfValue / rate)
} else {
setEstimatedCrypto(0)
}
}, [pointsToRedeem, cryptoCurrency, pointsToCryptoChf])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
setLoading(true)
try {
const points = parseFloat(pointsToRedeem)
if (isNaN(points) || points <= 0) {
setError(t('redeemPoints.invalidPoints'))
setLoading(false)
return
}
if (points < minRedemptionPoints) {
setError(t('redeemPoints.minPoints', { min: minRedemptionPoints }))
setLoading(false)
return
}
if (points > currentPoints) {
setError(t('redeemPoints.insufficientPoints'))
setLoading(false)
return
}
if (!walletAddress.trim()) {
setError(t('redeemPoints.invalidWallet'))
setLoading(false)
return
}
const response = await fetch('/api/referral-points/redeem', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
points: points,
crypto_currency: cryptoCurrency,
wallet_address: walletAddress.trim(),
}),
})
const data = await response.json()
if (!response.ok) {
setError(data.error || t('redeemPoints.error'))
setLoading(false)
return
}
setSuccess(true)
setRedemptionDetails(data)
onRedeemSuccess()
} catch (error: any) {
console.error('Error redeeming points:', error)
setError(error.message || t('redeemPoints.error'))
} finally {
setLoading(false)
}
}
if (!isOpen) return null
const pointsNum = parseFloat(pointsToRedeem) || 0
const chfValue = pointsNum / pointsToCryptoChf
return (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
padding: '20px',
}}
onClick={onClose}
>
<div
style={{
background: 'var(--bg)',
borderRadius: '12px',
padding: '32px',
maxWidth: '500px',
width: '100%',
maxHeight: '90vh',
overflowY: 'auto',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
}}
onClick={(e) => e.stopPropagation()}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<h2 style={{ margin: 0, fontSize: '24px', fontWeight: 600 }}>
{t('redeemPoints.title')}
</h2>
<button
onClick={onClose}
style={{
background: 'transparent',
border: 'none',
fontSize: '24px',
cursor: 'pointer',
color: 'var(--muted)',
padding: 0,
width: '32px',
height: '32px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
×
</button>
</div>
{success ? (
<div>
<div style={{
background: '#0a7931',
color: 'white',
padding: '16px',
borderRadius: '8px',
marginBottom: '24px'
}}>
<strong> {t('redeemPoints.success')}</strong>
</div>
{redemptionDetails && (
<div style={{ marginBottom: '24px' }}>
<p><strong>{t('redeemPoints.redemptionId')}:</strong> #{redemptionDetails.redemption_id}</p>
<p><strong>{t('redeemPoints.pointsRedeemed')}:</strong> {redemptionDetails.points_redeemed.toFixed(2)}</p>
<p><strong>{t('redeemPoints.cryptoAmount')}:</strong> {redemptionDetails.crypto_amount.toFixed(8)} {redemptionDetails.crypto_currency.toUpperCase()}</p>
<p><strong>{t('redeemPoints.newBalance')}:</strong> {redemptionDetails.new_balance.toFixed(2)} {t('redeemPoints.points')}</p>
<p style={{ marginTop: '16px', fontSize: '14px', color: 'var(--muted)' }}>
{redemptionDetails.message}
</p>
</div>
)}
<button
onClick={onClose}
style={{
width: '100%',
padding: '12px',
background: 'var(--primary)',
color: 'white',
border: 'none',
borderRadius: '8px',
fontSize: '16px',
cursor: 'pointer',
fontWeight: 500,
}}
>
{t('common.close')}
</button>
</div>
) : (
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: '20px' }}>
<div style={{
background: 'var(--bg-soft)',
padding: '16px',
borderRadius: '8px',
marginBottom: '16px'
}}>
<div style={{ fontSize: '14px', color: 'var(--muted)', marginBottom: '4px' }}>
{t('redeemPoints.currentBalance')}
</div>
<div style={{ fontSize: '24px', fontWeight: 600, color: '#0a7931' }}>
{currentPoints.toFixed(2)} {t('redeemPoints.points')}
</div>
</div>
<label style={{ display: 'block', marginBottom: '8px', fontSize: '14px', fontWeight: 500 }}>
{t('redeemPoints.selectCrypto')} *
</label>
<select
value={cryptoCurrency}
onChange={(e) => setCryptoCurrency(e.target.value)}
style={{
width: '100%',
padding: '12px',
background: 'var(--bg-soft)',
border: '1px solid var(--border)',
borderRadius: '8px',
fontSize: '14px',
color: 'var(--text)',
marginBottom: '16px',
}}
required
>
{ALLOWED_PAYMENT_CURRENCIES.map((currency) => (
<option key={currency} value={currency}>
{currency.toUpperCase()}
</option>
))}
</select>
<label style={{ display: 'block', marginBottom: '8px', fontSize: '14px', fontWeight: 500 }}>
{t('redeemPoints.walletAddress')} *
</label>
<input
type="text"
value={walletAddress}
onChange={(e) => setWalletAddress(e.target.value)}
placeholder={t('redeemPoints.walletAddressPlaceholder')}
style={{
width: '100%',
padding: '12px',
background: 'var(--bg-soft)',
border: '1px solid var(--border)',
borderRadius: '8px',
fontSize: '14px',
color: 'var(--text)',
marginBottom: '16px',
fontFamily: 'monospace',
}}
required
/>
<label style={{ display: 'block', marginBottom: '8px', fontSize: '14px', fontWeight: 500 }}>
{t('redeemPoints.pointsToRedeem')} *
<span style={{ marginLeft: '8px', fontSize: '12px', color: 'var(--muted)', fontWeight: 'normal' }}>
({t('redeemPoints.min')}: {minRedemptionPoints})
</span>
</label>
<input
type="number"
value={pointsToRedeem}
onChange={(e) => setPointsToRedeem(e.target.value)}
min={minRedemptionPoints}
max={currentPoints}
step="1"
placeholder={minRedemptionPoints.toString()}
style={{
width: '100%',
padding: '12px',
background: 'var(--bg-soft)',
border: '1px solid var(--border)',
borderRadius: '8px',
fontSize: '14px',
color: 'var(--text)',
marginBottom: '16px',
}}
required
/>
{pointsNum > 0 && (
<div style={{
background: 'var(--bg-soft)',
padding: '12px',
borderRadius: '8px',
marginBottom: '16px',
fontSize: '14px',
}}>
<div style={{ marginBottom: '4px' }}>
<strong>{t('redeemPoints.estimatedValue')}:</strong>
</div>
<div style={{ color: 'var(--muted)' }}>
{chfValue.toFixed(2)} CHF {estimatedCrypto.toFixed(8)} {cryptoCurrency.toUpperCase()}
</div>
</div>
)}
{error && (
<div style={{
background: '#ff4444',
color: 'white',
padding: '12px',
borderRadius: '8px',
marginBottom: '16px',
fontSize: '14px',
}}>
{error}
</div>
)}
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<button
type="button"
onClick={onClose}
style={{
flex: 1,
padding: '12px',
background: 'transparent',
color: 'var(--text)',
border: '1px solid var(--border)',
borderRadius: '8px',
fontSize: '16px',
cursor: 'pointer',
fontWeight: 500,
}}
>
{t('common.cancel')}
</button>
<button
type="submit"
disabled={loading || pointsNum < minRedemptionPoints || pointsNum > currentPoints}
style={{
flex: 1,
padding: '12px',
background: loading || pointsNum < minRedemptionPoints || pointsNum > currentPoints
? 'var(--muted)'
: 'var(--primary)',
color: 'white',
border: 'none',
borderRadius: '8px',
fontSize: '16px',
cursor: loading || pointsNum < minRedemptionPoints || pointsNum > currentPoints
? 'not-allowed'
: 'pointer',
fontWeight: 500,
}}
>
{loading ? t('common.processing') : t('redeemPoints.redeem')}
</button>
</div>
</form>
)}
</div>
</div>
)
}

View File

@@ -210,6 +210,28 @@
}, },
"payment": { "payment": {
"cancelled": "Zahlung wurde abgebrochen." "cancelled": "Zahlung wurde abgebrochen."
},
"redeemPoints": {
"title": "Punkte gegen Krypto einlösen",
"currentBalance": "Aktueller Kontostand",
"points": "Punkte",
"selectCrypto": "Kryptowährung auswählen",
"walletAddress": "Wallet-Adresse",
"walletAddressPlaceholder": "Geben Sie Ihre Krypto-Wallet-Adresse ein",
"pointsToRedeem": "Einzulösende Punkte",
"min": "Minimum",
"estimatedValue": "Geschätzter Wert",
"redeem": "Einlösen",
"success": "Einlösungsanfrage erfolgreich!",
"redemptionId": "Einlösungs-ID",
"pointsRedeemed": "Eingelöste Punkte",
"cryptoAmount": "Kryptobetrag",
"newBalance": "Neuer Kontostand",
"invalidPoints": "Bitte geben Sie eine gültige Anzahl von Punkten ein",
"minPoints": "Die Mindesteinlösung beträgt {min} Punkte",
"insufficientPoints": "Sie haben nicht genug Punkte",
"invalidWallet": "Bitte geben Sie eine gültige Wallet-Adresse ein",
"error": "Beim Verarbeiten Ihrer Einlösung ist ein Fehler aufgetreten"
} }
} }

View File

@@ -207,6 +207,28 @@
}, },
"payment": { "payment": {
"cancelled": "Payment was cancelled." "cancelled": "Payment was cancelled."
},
"redeemPoints": {
"title": "Redeem Points to Crypto",
"currentBalance": "Current Balance",
"points": "points",
"selectCrypto": "Select Cryptocurrency",
"walletAddress": "Wallet Address",
"walletAddressPlaceholder": "Enter your crypto wallet address",
"pointsToRedeem": "Points to Redeem",
"min": "Minimum",
"estimatedValue": "Estimated Value",
"redeem": "Redeem",
"success": "Redemption Request Successful!",
"redemptionId": "Redemption ID",
"pointsRedeemed": "Points Redeemed",
"cryptoAmount": "Crypto Amount",
"newBalance": "New Balance",
"invalidPoints": "Please enter a valid number of points",
"minPoints": "Minimum redemption is {min} points",
"insufficientPoints": "You don't have enough points",
"invalidWallet": "Please enter a valid wallet address",
"error": "An error occurred while processing your redemption"
} }
} }

View File

@@ -0,0 +1,43 @@
-- Migration to add point redemption to crypto feature
-- Date: 2025-01-XX
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
-- Update referral_point_transactions type enum to include 'redeemed'
ALTER TABLE `referral_point_transactions`
MODIFY COLUMN `type` enum('earned','spent','redeemed') NOT NULL;
-- Create point_redemptions table to track crypto redemptions
CREATE TABLE `point_redemptions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`buyer_id` int(11) NOT NULL,
`points` decimal(10,2) NOT NULL,
`crypto_currency` varchar(20) NOT NULL,
`wallet_address` varchar(255) NOT NULL,
`crypto_amount` decimal(20,8) DEFAULT NULL,
`status` enum('pending','processing','completed','failed','cancelled') NOT NULL DEFAULT 'pending',
`transaction_hash` varchar(255) DEFAULT NULL,
`error_message` text DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
KEY `buyer_id` (`buyer_id`),
KEY `status` (`status`),
CONSTRAINT `point_redemptions_ibfk_1` FOREIGN KEY (`buyer_id`) REFERENCES `buyers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- Add redemption rate setting (points per 1 CHF worth of crypto)
-- This determines how many points are needed to redeem 1 CHF worth of crypto
INSERT INTO `referral_settings` (`setting_key`, `setting_value`, `description`)
VALUES ('points_to_crypto_chf', '100', 'Number of referral points required to redeem 1 CHF worth of crypto')
ON DUPLICATE KEY UPDATE `description` = 'Number of referral points required to redeem 1 CHF worth of crypto';
-- Add minimum redemption amount setting
INSERT INTO `referral_settings` (`setting_key`, `setting_value`, `description`)
VALUES ('min_redemption_points', '1000', 'Minimum number of points required for crypto redemption')
ON DUPLICATE KEY UPDATE `description` = 'Minimum number of points required for crypto redemption';
COMMIT;