add trade validation to prevent accepting invalid trades and update UI accordingly
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { Search, Globe, Users, Eye, ArrowLeftRight, Loader2, Clock, History, UserPlus, UserMinus, Check, X, Send, Settings, Save, ChevronLeft, RefreshCw, Plus, Minus } from 'lucide-react';
|
import { Search, Globe, Users, Eye, ArrowLeftRight, Loader2, Clock, History, UserPlus, UserMinus, Check, X, Send, Settings, Save, ChevronLeft, RefreshCw, Plus, Minus, AlertTriangle } from 'lucide-react';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useToast } from '../contexts/ToastContext';
|
import { useToast } from '../contexts/ToastContext';
|
||||||
import { supabase } from '../lib/supabase';
|
import { supabase } from '../lib/supabase';
|
||||||
@@ -1412,10 +1412,15 @@ export default function Community() {
|
|||||||
With: <strong>{otherUser?.username}</strong>
|
With: <strong>{otherUser?.username}</strong>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{trade.status === 'pending' && !trade.is_valid && (
|
||||||
|
<AlertTriangle size={14} className="text-red-400" title="Trade no longer valid" />
|
||||||
|
)}
|
||||||
<span className={`text-xs capitalize ${statusColors[trade.status]}`}>
|
<span className={`text-xs capitalize ${statusColors[trade.status]}`}>
|
||||||
{trade.status}
|
{trade.status}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Items */}
|
{/* Items */}
|
||||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { X, Check, ArrowLeftRight, DollarSign, Loader2, Edit, RefreshCcw, History } from 'lucide-react';
|
import { X, Check, ArrowLeftRight, DollarSign, Loader2, Edit, RefreshCcw, History, AlertTriangle } from 'lucide-react';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useToast } from '../contexts/ToastContext';
|
import { useToast } from '../contexts/ToastContext';
|
||||||
import { Trade, TradeHistoryEntry, getTradeVersionHistory } from '../services/tradesService';
|
import { Trade, TradeHistoryEntry, getTradeVersionHistory } from '../services/tradesService';
|
||||||
@@ -222,6 +222,18 @@ export default function TradeDetail({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{/* Invalid Trade Warning */}
|
||||||
|
{trade.status === 'pending' && !trade.is_valid && (
|
||||||
|
<div className="bg-red-900/30 border border-red-600 rounded-lg p-3 flex items-start gap-2">
|
||||||
|
<AlertTriangle size={20} className="text-red-400 flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-red-400 text-sm">Trade No Longer Valid</h4>
|
||||||
|
<p className="text-red-200 text-xs mt-1">
|
||||||
|
One or more cards in this trade are no longer available in the required quantities. This trade cannot be accepted until it is updated.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
{/* Your Side */}
|
{/* Your Side */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -367,8 +379,9 @@ export default function TradeDetail({
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleAccept}
|
onClick={handleAccept}
|
||||||
disabled={processing}
|
disabled={processing || !trade.is_valid}
|
||||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 rounded-lg font-medium transition"
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg font-medium transition"
|
||||||
|
title={!trade.is_valid ? 'This trade is no longer valid' : ''}
|
||||||
>
|
>
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<Loader2 className="animate-spin" size={18} />
|
<Loader2 className="animate-spin" size={18} />
|
||||||
@@ -431,8 +444,9 @@ export default function TradeDetail({
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleAccept}
|
onClick={handleAccept}
|
||||||
disabled={processing}
|
disabled={processing || !trade.is_valid}
|
||||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 rounded-lg font-medium transition"
|
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg font-medium transition"
|
||||||
|
title={!trade.is_valid ? 'This trade is no longer valid' : ''}
|
||||||
>
|
>
|
||||||
{processing ? (
|
{processing ? (
|
||||||
<Loader2 className="animate-spin" size={18} />
|
<Loader2 className="animate-spin" size={18} />
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface Trade {
|
|||||||
updated_at: string | null;
|
updated_at: string | null;
|
||||||
version: number;
|
version: number;
|
||||||
editor_id: string | null;
|
editor_id: string | null;
|
||||||
|
is_valid: boolean;
|
||||||
user1?: { username: string | null };
|
user1?: { username: string | null };
|
||||||
user2?: { username: string | null };
|
user2?: { username: string | null };
|
||||||
items?: TradeItem[];
|
items?: TradeItem[];
|
||||||
@@ -160,6 +161,25 @@ export async function createTrade(params: CreateTradeParams): Promise<Trade> {
|
|||||||
|
|
||||||
// Accept a trade (executes the card transfer)
|
// Accept a trade (executes the card transfer)
|
||||||
export async function acceptTrade(tradeId: string): Promise<boolean> {
|
export async function acceptTrade(tradeId: string): Promise<boolean> {
|
||||||
|
// First check if the trade is valid
|
||||||
|
const { data: trade, error: tradeError } = await supabase
|
||||||
|
.from('trades')
|
||||||
|
.select('is_valid, status')
|
||||||
|
.eq('id', tradeId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (tradeError) throw tradeError;
|
||||||
|
|
||||||
|
// Prevent accepting invalid trades
|
||||||
|
if (!trade.is_valid) {
|
||||||
|
throw new Error('This trade is no longer valid. One or more cards are no longer available in the required quantities.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent accepting non-pending trades
|
||||||
|
if (trade.status !== 'pending') {
|
||||||
|
throw new Error('This trade has already been processed.');
|
||||||
|
}
|
||||||
|
|
||||||
const { data, error } = await supabase.rpc('execute_trade', {
|
const { data, error } = await supabase.rpc('execute_trade', {
|
||||||
trade_id: tradeId,
|
trade_id: tradeId,
|
||||||
});
|
});
|
||||||
|
|||||||
101
supabase/migrations/20250127000001_add_trade_validation.sql
Normal file
101
supabase/migrations/20250127000001_add_trade_validation.sql
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
-- Add is_valid column to trades table
|
||||||
|
ALTER TABLE public.trades
|
||||||
|
ADD COLUMN IF NOT EXISTS is_valid BOOLEAN DEFAULT true;
|
||||||
|
|
||||||
|
-- Create index for filtering by validity
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_trades_is_valid ON public.trades(is_valid);
|
||||||
|
|
||||||
|
-- Function to validate if a trade can still be executed based on current collections
|
||||||
|
CREATE OR REPLACE FUNCTION public.validate_trade(p_trade_id uuid)
|
||||||
|
RETURNS boolean
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_item RECORD;
|
||||||
|
v_collection_quantity integer;
|
||||||
|
BEGIN
|
||||||
|
-- Check each item in the trade
|
||||||
|
FOR v_item IN
|
||||||
|
SELECT owner_id, card_id, quantity
|
||||||
|
FROM public.trade_items
|
||||||
|
WHERE trade_id = p_trade_id
|
||||||
|
LOOP
|
||||||
|
-- Get the quantity of this card in the owner's collection
|
||||||
|
SELECT COALESCE(quantity, 0) INTO v_collection_quantity
|
||||||
|
FROM public.collections
|
||||||
|
WHERE user_id = v_item.owner_id
|
||||||
|
AND card_id = v_item.card_id;
|
||||||
|
|
||||||
|
-- If owner doesn't have enough of this card, trade is invalid
|
||||||
|
IF v_collection_quantity < v_item.quantity THEN
|
||||||
|
RETURN false;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- All items are available, trade is valid
|
||||||
|
RETURN true;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Function to check and update validity of affected trades when collections change
|
||||||
|
CREATE OR REPLACE FUNCTION public.update_affected_trades_validity()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_user_id uuid;
|
||||||
|
v_card_id text;
|
||||||
|
v_trade RECORD;
|
||||||
|
v_is_valid boolean;
|
||||||
|
BEGIN
|
||||||
|
-- Get the user_id and card_id from the changed row
|
||||||
|
IF (TG_OP = 'DELETE') THEN
|
||||||
|
v_user_id := OLD.user_id;
|
||||||
|
v_card_id := OLD.card_id;
|
||||||
|
ELSE
|
||||||
|
v_user_id := NEW.user_id;
|
||||||
|
v_card_id := NEW.card_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Find all pending trades that involve this card from this user
|
||||||
|
FOR v_trade IN
|
||||||
|
SELECT DISTINCT t.id
|
||||||
|
FROM public.trades t
|
||||||
|
JOIN public.trade_items ti ON ti.trade_id = t.id
|
||||||
|
WHERE t.status = 'pending'
|
||||||
|
AND ti.owner_id = v_user_id
|
||||||
|
AND ti.card_id = v_card_id
|
||||||
|
LOOP
|
||||||
|
-- Validate the trade
|
||||||
|
v_is_valid := public.validate_trade(v_trade.id);
|
||||||
|
|
||||||
|
-- Update the trade's validity
|
||||||
|
UPDATE public.trades
|
||||||
|
SET is_valid = v_is_valid,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE id = v_trade.id;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
IF (TG_OP = 'DELETE') THEN
|
||||||
|
RETURN OLD;
|
||||||
|
ELSE
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Create trigger to auto-update trade validity when collections change
|
||||||
|
CREATE TRIGGER update_trades_on_collection_change
|
||||||
|
AFTER UPDATE OR DELETE ON public.collections
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION public.update_affected_trades_validity();
|
||||||
|
|
||||||
|
-- Add comment
|
||||||
|
COMMENT ON COLUMN public.trades.is_valid IS 'Indicates if the trade can still be executed based on current collections. Auto-updated when collections change.';
|
||||||
|
|
||||||
|
-- Initial validation: set is_valid for all existing pending trades
|
||||||
|
UPDATE public.trades
|
||||||
|
SET is_valid = public.validate_trade(id)
|
||||||
|
WHERE status = 'pending';
|
||||||
Reference in New Issue
Block a user