Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file removed 1
Empty file.
95 changes: 95 additions & 0 deletions apps/backend/database/migrations/00006_add_atomic_functions.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
-- Migration: Add atomic functions for sync operations
-- This migration adds PostgreSQL functions for atomic event processing
-- to prevent inconsistent states when event logging or state updates fail.

-- Ensure sync_events table has necessary constraints
ALTER TABLE public.sync_events
ADD CONSTRAINT IF NOT EXISTS sync_events_event_id_unique UNIQUE (event_id);

-- Function for processing sync events atomically
-- This function handles event insertion, booking status updates, and event marking in a single transaction
CREATE OR REPLACE FUNCTION process_sync_event_atomic(
p_event_id TEXT,
p_event_type TEXT,
p_booking_id TEXT,
p_property_id TEXT,
p_user_id TEXT,
p_event_data JSONB,
p_new_status TEXT DEFAULT NULL
)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_sync_event_id UUID;
BEGIN
-- Step 1: Insert sync event (with duplicate check)
INSERT INTO public.sync_events (event_id, event_type, booking_id, property_id, user_id, event_data, processed, created_at)
VALUES (p_event_id, p_event_type, p_booking_id, p_property_id, p_user_id, p_event_data, false, now())
ON CONFLICT (event_id) DO NOTHING
RETURNING id INTO v_sync_event_id;

-- If event already exists (duplicate), return early
IF v_sync_event_id IS NULL THEN
RETURN jsonb_build_object('success', false, 'error', 'DUPLICATE_EVENT', 'event_id', p_event_id);
END IF;

-- Step 2: Update booking status if applicable
IF p_new_status IS NOT NULL AND p_booking_id IS NOT NULL THEN
UPDATE public.bookings
SET status = p_new_status, updated_at = now()
WHERE escrow_address = p_booking_id OR blockchain_booking_id = p_booking_id;
END IF;

-- Step 3: Mark event as processed
UPDATE public.sync_events
SET processed = true, processed_at = now()
WHERE id = v_sync_event_id;

-- Log success
INSERT INTO public.sync_logs (operation, status, message, data, created_at)
VALUES (
'process_sync_event_atomic',
'success',
'Event processed atomically',
jsonb_build_object(
'event_id', p_event_id,
'event_type', p_event_type,
'booking_id', p_booking_id,
'new_status', p_new_status
),
now()
);

RETURN jsonb_build_object(
'success', true,
'sync_event_id', v_sync_event_id,
'event_id', p_event_id
);

EXCEPTION WHEN OTHERS THEN
-- Log error
INSERT INTO public.sync_logs (operation, status, error_details, created_at)
VALUES (
'process_sync_event_atomic',
'error',
jsonb_build_object(
'error', SQLERRM,
'error_state', SQLSTATE,
'event_id', p_event_id,
'event_type', p_event_type
),
now()
);
-- Re-raise the exception to rollback the transaction
RAISE;
END;
$$;

-- Grant execute permission to authenticated users
GRANT EXECUTE ON FUNCTION process_sync_event_atomic(TEXT, TEXT, TEXT, TEXT, TEXT, JSONB, TEXT) TO authenticated;
GRANT EXECUTE ON FUNCTION process_sync_event_atomic(TEXT, TEXT, TEXT, TEXT, TEXT, JSONB, TEXT) TO service_role;

-- Add comment for documentation
COMMENT ON FUNCTION process_sync_event_atomic IS 'Atomically processes blockchain sync events: inserts event, updates booking status, and marks as processed in a single transaction.';
132 changes: 132 additions & 0 deletions apps/backend/database/migrations/00007_add_payment_constraints.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
-- Migration: Add payment validation constraints and atomic confirmation function
-- This migration prevents duplicate transaction hash usage and ensures atomic payment confirmation.

-- Unique constraint to prevent the same transaction hash being used for multiple bookings
CREATE UNIQUE INDEX IF NOT EXISTS bookings_payment_tx_hash_unique_idx
ON public.bookings (payment_transaction_hash)
WHERE payment_transaction_hash IS NOT NULL;

-- RPC function for atomic booking payment confirmation with validations
CREATE OR REPLACE FUNCTION confirm_booking_payment_atomic(
p_booking_id UUID,
p_transaction_hash TEXT
)
RETURNS JSONB
LANGUAGE plpgsql
AS $$
DECLARE
v_booking RECORD;
BEGIN
-- Validate transaction hash is provided
IF p_transaction_hash IS NULL OR p_transaction_hash = '' THEN
RETURN jsonb_build_object(
'success', false,
'error', 'INVALID_TRANSACTION_HASH',
'message', 'Transaction hash is required'
);
END IF;

-- Lock the booking row for update to prevent concurrent modifications
SELECT id, status, payment_transaction_hash
INTO v_booking
FROM public.bookings
WHERE id = p_booking_id
FOR UPDATE;

-- Check if booking exists
IF v_booking IS NULL THEN
RETURN jsonb_build_object(
'success', false,
'error', 'NOT_FOUND',
'message', 'Booking not found'
);
END IF;

-- Check if booking is already paid
IF v_booking.payment_transaction_hash IS NOT NULL THEN
RETURN jsonb_build_object(
'success', false,
'error', 'ALREADY_PAID',
'message', 'Booking has already been paid',
'existing_hash', v_booking.payment_transaction_hash
);
END IF;

-- Check if transaction hash is already used by another booking
IF EXISTS (
SELECT 1 FROM public.bookings
WHERE payment_transaction_hash = p_transaction_hash
AND id != p_booking_id
) THEN
RETURN jsonb_build_object(
'success', false,
'error', 'DUPLICATE_TRANSACTION',
'message', 'Transaction hash is already used by another booking'
);
END IF;

-- Update the booking with payment confirmation
UPDATE public.bookings
SET
status = 'confirmed',
payment_transaction_hash = p_transaction_hash,
paid_at = now(),
updated_at = now()
WHERE id = p_booking_id;

-- Log the successful payment confirmation
INSERT INTO public.sync_logs (operation, status, message, data, created_at)
VALUES (
'confirm_booking_payment_atomic',
'success',
'Payment confirmed atomically',
jsonb_build_object(
'booking_id', p_booking_id,
'transaction_hash', p_transaction_hash
),
now()
);

RETURN jsonb_build_object(
'success', true,
'booking_id', p_booking_id,
'transaction_hash', p_transaction_hash,
'status', 'confirmed'
);

EXCEPTION WHEN unique_violation THEN
-- Handle the case where another process inserted the same tx hash concurrently
RETURN jsonb_build_object(
'success', false,
'error', 'DUPLICATE_TRANSACTION',
'message', 'Transaction hash was just used by another booking (concurrent update)'
);
WHEN OTHERS THEN
-- Log error and return failure
INSERT INTO public.sync_logs (operation, status, error_details, created_at)
VALUES (
'confirm_booking_payment_atomic',
'error',
jsonb_build_object(
'error', SQLERRM,
'error_state', SQLSTATE,
'booking_id', p_booking_id::text,
'transaction_hash', p_transaction_hash
),
now()
);

RETURN jsonb_build_object(
'success', false,
'error', 'DB_ERROR',
'message', SQLERRM
);
END;
$$;

-- Grant execute permission
GRANT EXECUTE ON FUNCTION confirm_booking_payment_atomic(UUID, TEXT) TO authenticated;
GRANT EXECUTE ON FUNCTION confirm_booking_payment_atomic(UUID, TEXT) TO service_role;

-- Add comment for documentation
COMMENT ON FUNCTION confirm_booking_payment_atomic IS 'Atomically confirms a booking payment with validation: checks booking exists, is not already paid, and transaction hash is not reused.';
Loading
Loading