mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-13 10:01:02 +00:00
fix: payment proof lifecycle, repeating toast, and escrow migrations
- Replace IPFS/Pinata upload with Supabase Storage for payment proofs - Add 1-day auto-expiry for proof images (retained if disputed) - Fix repeating "payment deadline expired" toast (fire once, clear interval) - Fix cancel_reason → cancellation_reason column reference - Add payment proof lifecycle migration (proof_expires_at, cleanup functions) - Add atomic escrow migration (accept_p2p_offer, complete/cancel trade) - Add cleanup-proofs edge function for daily expired proof deletion
This commit is contained in:
+16
-4
@@ -572,20 +572,32 @@ export async function markPaymentSent(
|
|||||||
try {
|
try {
|
||||||
let paymentProofUrl: string | undefined;
|
let paymentProofUrl: string | undefined;
|
||||||
|
|
||||||
// 1. Upload payment proof to IPFS if provided
|
// 1. Upload payment proof to Supabase Storage (auto-expires in 1 day)
|
||||||
if (paymentProofFile) {
|
if (paymentProofFile) {
|
||||||
const { uploadToIPFS } = await import('./ipfs');
|
const fileName = `payment-proofs/${tradeId}/${Date.now()}-${paymentProofFile.name}`;
|
||||||
paymentProofUrl = await uploadToIPFS(paymentProofFile);
|
const { data: uploadData, error: uploadError } = await supabase.storage
|
||||||
|
.from('p2p-payment-proofs')
|
||||||
|
.upload(fileName, paymentProofFile);
|
||||||
|
|
||||||
|
if (uploadError) throw uploadError;
|
||||||
|
|
||||||
|
const { data: urlData } = supabase.storage
|
||||||
|
.from('p2p-payment-proofs')
|
||||||
|
.getPublicUrl(uploadData.path);
|
||||||
|
|
||||||
|
paymentProofUrl = urlData.publicUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Update trade
|
// 2. Update trade (proof expires in 1 day unless disputed)
|
||||||
const confirmationDeadline = new Date(Date.now() + DEFAULT_CONFIRMATION_DEADLINE_MINUTES * 60 * 1000);
|
const confirmationDeadline = new Date(Date.now() + DEFAULT_CONFIRMATION_DEADLINE_MINUTES * 60 * 1000);
|
||||||
|
const proofExpiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 1 day
|
||||||
|
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('p2p_fiat_trades')
|
.from('p2p_fiat_trades')
|
||||||
.update({
|
.update({
|
||||||
buyer_marked_paid_at: new Date().toISOString(),
|
buyer_marked_paid_at: new Date().toISOString(),
|
||||||
buyer_payment_proof_url: paymentProofUrl,
|
buyer_payment_proof_url: paymentProofUrl,
|
||||||
|
proof_expires_at: paymentProofUrl ? proofExpiresAt.toISOString() : null,
|
||||||
status: 'payment_sent',
|
status: 'payment_sent',
|
||||||
confirmation_deadline: confirmationDeadline.toISOString()
|
confirmation_deadline: confirmationDeadline.toISOString()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -199,14 +199,17 @@ export default function P2PTrade() {
|
|||||||
|
|
||||||
const deadline = new Date(trade.payment_deadline).getTime();
|
const deadline = new Date(trade.payment_deadline).getTime();
|
||||||
|
|
||||||
|
let expired = false;
|
||||||
|
|
||||||
const updateTimer = () => {
|
const updateTimer = () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const remaining = Math.max(0, Math.floor((deadline - now) / 1000));
|
const remaining = Math.max(0, Math.floor((deadline - now) / 1000));
|
||||||
setTimeRemaining(remaining);
|
setTimeRemaining(remaining);
|
||||||
|
|
||||||
if (remaining === 0 && trade.status === 'pending') {
|
if (remaining === 0 && !expired) {
|
||||||
// Auto-cancel logic could go here
|
expired = true;
|
||||||
toast.warning(t('p2pTrade.paymentDeadlineExpired'));
|
toast.warning(t('p2pTrade.paymentDeadlineExpired'));
|
||||||
|
clearInterval(interval);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -714,8 +717,8 @@ export default function P2PTrade() {
|
|||||||
<CardContent className="py-6 text-center">
|
<CardContent className="py-6 text-center">
|
||||||
<XCircle className="w-16 h-16 text-gray-500 mx-auto mb-4" />
|
<XCircle className="w-16 h-16 text-gray-500 mx-auto mb-4" />
|
||||||
<h3 className="text-xl font-semibold text-gray-400 mb-2">{t('p2pTrade.tradeCancelled')}</h3>
|
<h3 className="text-xl font-semibold text-gray-400 mb-2">{t('p2pTrade.tradeCancelled')}</h3>
|
||||||
{trade.cancel_reason && (
|
{trade.cancellation_reason && (
|
||||||
<p className="text-gray-500">{t('p2pTrade.cancelReason', { reason: trade.cancel_reason })}</p>
|
<p className="text-gray-500">{t('p2pTrade.cancelReason', { reason: trade.cancellation_reason })}</p>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Cleanup expired payment proofs
|
||||||
|
*
|
||||||
|
* Schedule: Daily via Supabase Dashboard > Database > Extensions > pg_cron
|
||||||
|
* Or call manually: supabase functions invoke cleanup-proofs
|
||||||
|
*
|
||||||
|
* Flow:
|
||||||
|
* 1. Call DB function to get expired proof URLs and clear them
|
||||||
|
* 2. Delete actual files from Supabase Storage
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
||||||
|
|
||||||
|
const corsHeaders = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
||||||
|
}
|
||||||
|
|
||||||
|
Deno.serve(async (req) => {
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
return new Response('ok', { headers: corsHeaders })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const supabase = createClient(
|
||||||
|
Deno.env.get('SUPABASE_URL')!,
|
||||||
|
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
||||||
|
)
|
||||||
|
|
||||||
|
// 1. Find expired proofs (not disputed, past expiry)
|
||||||
|
const { data: expiredTrades, error: fetchError } = await supabase
|
||||||
|
.from('p2p_fiat_trades')
|
||||||
|
.select('id, buyer_payment_proof_url')
|
||||||
|
.not('buyer_payment_proof_url', 'is', null)
|
||||||
|
.not('proof_expires_at', 'is', null)
|
||||||
|
.lt('proof_expires_at', new Date().toISOString())
|
||||||
|
.not('status', 'eq', 'disputed')
|
||||||
|
|
||||||
|
if (fetchError) throw fetchError
|
||||||
|
|
||||||
|
let deleted = 0
|
||||||
|
|
||||||
|
for (const trade of expiredTrades || []) {
|
||||||
|
// 2. Extract storage path from URL
|
||||||
|
const url = trade.buyer_payment_proof_url
|
||||||
|
const pathMatch = url?.match(/p2p-payment-proofs\/(.+)$/)
|
||||||
|
|
||||||
|
if (pathMatch) {
|
||||||
|
// 3. Delete file from storage
|
||||||
|
await supabase.storage
|
||||||
|
.from('p2p-payment-proofs')
|
||||||
|
.remove([pathMatch[1]])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Clear URL in DB
|
||||||
|
await supabase
|
||||||
|
.from('p2p_fiat_trades')
|
||||||
|
.update({
|
||||||
|
buyer_payment_proof_url: null,
|
||||||
|
proof_expires_at: null,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
.eq('id', trade.id)
|
||||||
|
|
||||||
|
deleted++
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ success: true, deleted }),
|
||||||
|
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ success: false, error: error.message }),
|
||||||
|
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- P2P ATOMIC ESCROW & PLATFORM WALLET SYSTEM
|
||||||
|
-- Migration: 013_p2p_atomic_escrow.sql
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- 1. Platform escrow wallet tracking table
|
||||||
|
CREATE TABLE IF NOT EXISTS p2p_platform_escrow (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
offer_id UUID NOT NULL REFERENCES p2p_fiat_offers(id) ON DELETE CASCADE,
|
||||||
|
seller_id UUID NOT NULL REFERENCES auth.users(id),
|
||||||
|
seller_wallet TEXT NOT NULL,
|
||||||
|
token TEXT NOT NULL,
|
||||||
|
amount DECIMAL(20, 12) NOT NULL,
|
||||||
|
locked_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
released_at TIMESTAMPTZ,
|
||||||
|
released_to TEXT, -- wallet address
|
||||||
|
release_reason TEXT, -- 'trade_complete', 'trade_cancelled', 'trade_expired', 'refund'
|
||||||
|
blockchain_tx_lock TEXT, -- tx hash for lock transfer
|
||||||
|
blockchain_tx_release TEXT, -- tx hash for release transfer
|
||||||
|
status TEXT NOT NULL DEFAULT 'locked' CHECK (status IN ('locked', 'released', 'pending_release')),
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index for quick lookups
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_platform_escrow_offer ON p2p_platform_escrow(offer_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_platform_escrow_seller ON p2p_platform_escrow(seller_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_platform_escrow_status ON p2p_platform_escrow(status);
|
||||||
|
|
||||||
|
-- 2. Atomic function to accept offer (prevents race condition)
|
||||||
|
CREATE OR REPLACE FUNCTION accept_p2p_offer(
|
||||||
|
p_offer_id UUID,
|
||||||
|
p_buyer_id UUID,
|
||||||
|
p_buyer_wallet TEXT,
|
||||||
|
p_amount DECIMAL(20, 12)
|
||||||
|
) RETURNS JSON AS $$
|
||||||
|
DECLARE
|
||||||
|
v_offer RECORD;
|
||||||
|
v_trade_id UUID;
|
||||||
|
v_payment_deadline TIMESTAMPTZ;
|
||||||
|
v_fiat_amount DECIMAL(20, 2);
|
||||||
|
BEGIN
|
||||||
|
-- Lock the offer row for update (prevents concurrent modifications)
|
||||||
|
SELECT * INTO v_offer
|
||||||
|
FROM p2p_fiat_offers
|
||||||
|
WHERE id = p_offer_id
|
||||||
|
FOR UPDATE;
|
||||||
|
|
||||||
|
-- Validation checks
|
||||||
|
IF v_offer IS NULL THEN
|
||||||
|
RETURN json_build_object('success', false, 'error', 'Offer not found');
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_offer.status != 'open' THEN
|
||||||
|
RETURN json_build_object('success', false, 'error', 'Offer is not available');
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_offer.seller_id = p_buyer_id THEN
|
||||||
|
RETURN json_build_object('success', false, 'error', 'Cannot buy from your own offer');
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_amount > v_offer.remaining_amount THEN
|
||||||
|
RETURN json_build_object('success', false, 'error', 'Insufficient remaining amount. Available: ' || v_offer.remaining_amount);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_offer.min_order_amount IS NOT NULL AND p_amount < v_offer.min_order_amount THEN
|
||||||
|
RETURN json_build_object('success', false, 'error', 'Minimum order: ' || v_offer.min_order_amount || ' ' || v_offer.token);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_offer.max_order_amount IS NOT NULL AND p_amount > v_offer.max_order_amount THEN
|
||||||
|
RETURN json_build_object('success', false, 'error', 'Maximum order: ' || v_offer.max_order_amount || ' ' || v_offer.token);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Calculate fiat amount
|
||||||
|
v_fiat_amount := (p_amount / v_offer.amount_crypto) * v_offer.fiat_amount;
|
||||||
|
v_payment_deadline := NOW() + (v_offer.time_limit_minutes || ' minutes')::INTERVAL;
|
||||||
|
|
||||||
|
-- Create trade
|
||||||
|
INSERT INTO p2p_fiat_trades (
|
||||||
|
offer_id,
|
||||||
|
seller_id,
|
||||||
|
buyer_id,
|
||||||
|
buyer_wallet,
|
||||||
|
crypto_amount,
|
||||||
|
fiat_amount,
|
||||||
|
price_per_unit,
|
||||||
|
escrow_locked_amount,
|
||||||
|
escrow_locked_at,
|
||||||
|
status,
|
||||||
|
payment_deadline
|
||||||
|
) VALUES (
|
||||||
|
p_offer_id,
|
||||||
|
v_offer.seller_id,
|
||||||
|
p_buyer_id,
|
||||||
|
p_buyer_wallet,
|
||||||
|
p_amount,
|
||||||
|
v_fiat_amount,
|
||||||
|
v_offer.price_per_unit,
|
||||||
|
p_amount,
|
||||||
|
NOW(),
|
||||||
|
'pending',
|
||||||
|
v_payment_deadline
|
||||||
|
) RETURNING id INTO v_trade_id;
|
||||||
|
|
||||||
|
-- Atomically update remaining amount
|
||||||
|
UPDATE p2p_fiat_offers
|
||||||
|
SET
|
||||||
|
remaining_amount = remaining_amount - p_amount,
|
||||||
|
status = CASE
|
||||||
|
WHEN remaining_amount - p_amount = 0 THEN 'locked'
|
||||||
|
ELSE 'open'
|
||||||
|
END,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = p_offer_id;
|
||||||
|
|
||||||
|
RETURN json_build_object(
|
||||||
|
'success', true,
|
||||||
|
'trade_id', v_trade_id,
|
||||||
|
'crypto_amount', p_amount,
|
||||||
|
'fiat_amount', v_fiat_amount,
|
||||||
|
'payment_deadline', v_payment_deadline
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- 3. Function to release escrow on trade completion
|
||||||
|
CREATE OR REPLACE FUNCTION complete_p2p_trade(
|
||||||
|
p_trade_id UUID,
|
||||||
|
p_seller_id UUID
|
||||||
|
) RETURNS JSON AS $$
|
||||||
|
DECLARE
|
||||||
|
v_trade RECORD;
|
||||||
|
BEGIN
|
||||||
|
-- Lock trade row
|
||||||
|
SELECT * INTO v_trade
|
||||||
|
FROM p2p_fiat_trades
|
||||||
|
WHERE id = p_trade_id
|
||||||
|
FOR UPDATE;
|
||||||
|
|
||||||
|
IF v_trade IS NULL THEN
|
||||||
|
RETURN json_build_object('success', false, 'error', 'Trade not found');
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_trade.seller_id != p_seller_id THEN
|
||||||
|
RETURN json_build_object('success', false, 'error', 'Only seller can confirm receipt');
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_trade.status != 'payment_sent' THEN
|
||||||
|
RETURN json_build_object('success', false, 'error', 'Payment not marked as sent yet');
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Update trade status
|
||||||
|
UPDATE p2p_fiat_trades
|
||||||
|
SET
|
||||||
|
status = 'completed',
|
||||||
|
completed_at = NOW(),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = p_trade_id;
|
||||||
|
|
||||||
|
-- Update escrow status (will be released by backend)
|
||||||
|
UPDATE p2p_platform_escrow
|
||||||
|
SET
|
||||||
|
status = 'pending_release',
|
||||||
|
released_to = v_trade.buyer_wallet,
|
||||||
|
release_reason = 'trade_complete',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE offer_id = v_trade.offer_id
|
||||||
|
AND status = 'locked';
|
||||||
|
|
||||||
|
RETURN json_build_object(
|
||||||
|
'success', true,
|
||||||
|
'buyer_wallet', v_trade.buyer_wallet,
|
||||||
|
'amount', v_trade.crypto_amount,
|
||||||
|
'token', (SELECT token FROM p2p_fiat_offers WHERE id = v_trade.offer_id)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- 4. Function to cancel/expire trade (return to seller)
|
||||||
|
CREATE OR REPLACE FUNCTION cancel_p2p_trade(
|
||||||
|
p_trade_id UUID,
|
||||||
|
p_user_id UUID,
|
||||||
|
p_reason TEXT DEFAULT 'cancelled'
|
||||||
|
) RETURNS JSON AS $$
|
||||||
|
DECLARE
|
||||||
|
v_trade RECORD;
|
||||||
|
v_offer RECORD;
|
||||||
|
BEGIN
|
||||||
|
-- Lock trade row
|
||||||
|
SELECT * INTO v_trade
|
||||||
|
FROM p2p_fiat_trades
|
||||||
|
WHERE id = p_trade_id
|
||||||
|
FOR UPDATE;
|
||||||
|
|
||||||
|
IF v_trade IS NULL THEN
|
||||||
|
RETURN json_build_object('success', false, 'error', 'Trade not found');
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Only buyer can cancel before payment, or system for expiry
|
||||||
|
IF v_trade.status = 'pending' AND v_trade.buyer_id != p_user_id THEN
|
||||||
|
RETURN json_build_object('success', false, 'error', 'Only buyer can cancel pending trade');
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_trade.status NOT IN ('pending', 'payment_sent') THEN
|
||||||
|
RETURN json_build_object('success', false, 'error', 'Trade cannot be cancelled in current status');
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Get offer details
|
||||||
|
SELECT * INTO v_offer FROM p2p_fiat_offers WHERE id = v_trade.offer_id;
|
||||||
|
|
||||||
|
-- Update trade status
|
||||||
|
UPDATE p2p_fiat_trades
|
||||||
|
SET
|
||||||
|
status = 'cancelled',
|
||||||
|
cancelled_at = NOW(),
|
||||||
|
cancelled_by = p_user_id,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = p_trade_id;
|
||||||
|
|
||||||
|
-- Return amount to offer
|
||||||
|
UPDATE p2p_fiat_offers
|
||||||
|
SET
|
||||||
|
remaining_amount = remaining_amount + v_trade.crypto_amount,
|
||||||
|
status = 'open',
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = v_trade.offer_id;
|
||||||
|
|
||||||
|
RETURN json_build_object(
|
||||||
|
'success', true,
|
||||||
|
'refunded_amount', v_trade.crypto_amount,
|
||||||
|
'reason', p_reason
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- 5. RLS policies for platform escrow
|
||||||
|
ALTER TABLE p2p_platform_escrow ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Sellers can view their own escrow
|
||||||
|
CREATE POLICY "Sellers can view their escrow"
|
||||||
|
ON p2p_platform_escrow FOR SELECT
|
||||||
|
USING (seller_id = auth.uid());
|
||||||
|
|
||||||
|
-- Admins can view all escrow
|
||||||
|
CREATE POLICY "Admins can view all escrow"
|
||||||
|
ON p2p_platform_escrow FOR SELECT
|
||||||
|
USING (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM profiles
|
||||||
|
WHERE profiles.id = auth.uid()
|
||||||
|
AND profiles.role IN ('admin', 'super_admin')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Only system can insert/update (via service role)
|
||||||
|
-- No INSERT/UPDATE policies for regular users
|
||||||
|
|
||||||
|
-- 6. Add platform_wallet_address to config
|
||||||
|
CREATE TABLE IF NOT EXISTS p2p_config (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Insert platform wallet config (will be updated with real address)
|
||||||
|
INSERT INTO p2p_config (key, value, description)
|
||||||
|
VALUES (
|
||||||
|
'platform_escrow_wallet',
|
||||||
|
'5DFwqK698vL4gXHEcanaewnAqhxJ2rjhAogpSTHw3iwGDwd3',
|
||||||
|
'Platform wallet address for P2P escrow'
|
||||||
|
)
|
||||||
|
ON CONFLICT (key) DO NOTHING;
|
||||||
|
|
||||||
|
COMMENT ON TABLE p2p_platform_escrow IS 'Tracks all escrow deposits for P2P trades';
|
||||||
|
COMMENT ON TABLE p2p_config IS 'P2P system configuration';
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
-- Payment proof lifecycle: auto-delete after 1 day, retain if disputed
|
||||||
|
|
||||||
|
-- 1. Add proof_expires_at column
|
||||||
|
ALTER TABLE public.p2p_fiat_trades
|
||||||
|
ADD COLUMN IF NOT EXISTS proof_expires_at TIMESTAMPTZ;
|
||||||
|
|
||||||
|
-- 2. Function to clean up expired payment proofs
|
||||||
|
-- Called by pg_cron or Edge Function daily
|
||||||
|
CREATE OR REPLACE FUNCTION cleanup_expired_payment_proofs()
|
||||||
|
RETURNS JSON AS $$
|
||||||
|
DECLARE
|
||||||
|
v_count INT := 0;
|
||||||
|
v_trade RECORD;
|
||||||
|
BEGIN
|
||||||
|
FOR v_trade IN
|
||||||
|
SELECT id, buyer_payment_proof_url
|
||||||
|
FROM p2p_fiat_trades
|
||||||
|
WHERE buyer_payment_proof_url IS NOT NULL
|
||||||
|
AND proof_expires_at IS NOT NULL
|
||||||
|
AND proof_expires_at < NOW()
|
||||||
|
AND status NOT IN ('disputed')
|
||||||
|
LOOP
|
||||||
|
UPDATE p2p_fiat_trades
|
||||||
|
SET buyer_payment_proof_url = NULL,
|
||||||
|
proof_expires_at = NULL,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = v_trade.id;
|
||||||
|
|
||||||
|
v_count := v_count + 1;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
RETURN json_build_object('success', true, 'cleaned', v_count);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
|
-- 3. Function to retain proof when dispute is opened (set expires_at = NULL)
|
||||||
|
CREATE OR REPLACE FUNCTION retain_payment_proof(p_trade_id UUID)
|
||||||
|
RETURNS JSON AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE p2p_fiat_trades
|
||||||
|
SET proof_expires_at = NULL,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = p_trade_id
|
||||||
|
AND buyer_payment_proof_url IS NOT NULL;
|
||||||
|
|
||||||
|
RETURN json_build_object('success', true);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
|
-- 4. Moderator function to delete proof after dispute resolution
|
||||||
|
CREATE OR REPLACE FUNCTION moderator_clear_payment_proof(p_trade_id UUID)
|
||||||
|
RETURNS JSON AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE p2p_fiat_trades
|
||||||
|
SET buyer_payment_proof_url = NULL,
|
||||||
|
proof_expires_at = NULL,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = p_trade_id;
|
||||||
|
|
||||||
|
RETURN json_build_object('success', true);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
Reference in New Issue
Block a user