Authoritative, read-only source package for the Supabase clean-master reconstruction.
Recommended AI Studio entry point: manifest-full.txt. If a single fetch is truncated,
use the five individual manifest links above in order.
SZEDERVÖLGYI CLEAN MASTER — AI STUDIO INSTRUCTIONS AUTHORITATIVE SOURCE Read ALL five manifest files linked from index.html before doing any build work. SOURCE PROJECT Bravos LIVE Supabase project ref: vcdzxndcmvtfttxbhdbc READ-ONLY SOURCE ONLY. NEVER MODIFY IT. TARGET PROJECT Supabase project ref: gnbnpxqlhfqcslqeqvsf Target URL: https://gnbnpxqlhfqcslqeqvsf.supabase.co DO NOT DEPLOY YET. TARGET CLUB Name: Szedervölgyi Teniszklub Csömör Slug: szedervolgyi-teniszklub-csomor SAFETY - Do not copy Bravos users, bookings, payments, credits, academy participants, audit data, venue payment data, or other business records. - Build structure and business logic only. - Preserve Tennis/Padel sport isolation. - Working Tennis behavior must not regress. - Padel changes must not alter Tennis behavior. - Do not expose or use secret/service_role keys. TARGETED BRAVOS ADAPTATIONS Do NOT do blind global replacement. Replace deployment club-targeting slug in: - RLS policy coach_availability_public_read_active - RLS policy user_profiles_insert_self - view booking_calendar_slots - view coach_booking_directory - view partner_finder_directory Old slug: bravos-sport-club New slug: szedervolgyi-teniszklub-csomor CRITICAL handle_new_auth_user ADAPTATION Do not carry any Bravos-specific hardcoded club UUID into the clean master. Resolve the club_id from public.clubs using slug: szedervolgyi-teniszklub-csomor Fail closed if it does not exist. LEGACY OBJECT pricing_rules_emergency_backup_20260716 is potentially a legacy backup table. Do not include it automatically. First check all function/trigger/view/policy/FK/code dependencies. DEPENDENCY ORDER 1. extensions 2. tables 3. PK / UNIQUE / CHECK constraints 4. foreign keys 5. standalone indexes only 6. helper functions 7. business RPCs 8. trigger functions 9. table triggers 10. RLS enable 11. RLS policies 12. views 13. auth.users trigger Do not recreate constraint-backed indexes separately. AUTH TRIGGER Create auth.on_auth_user_created_create_profile only after public.handle_new_auth_user() exists. DELIVERABLE Create dependency-safe clean master migration file(s) under supabase/migrations/. DO NOT DEPLOY. Final report must include: 1. migration files created 2. table count 3. function/RPC count 4. trigger count 5. RLS policy count 6. view count 7. extensions 8. Bravos hardcodes adapted 9. handle_new_auth_user club resolution 10. legacy backup table decision 11. unresolved dependencies 12. remaining Bravos UUID/slug/branding findings 13. Tennis/Padel isolation confirmation 14. LIVE Supabase modified? NEM 15. target Supabase deployed? NEM End with: READY FOR REVIEW — NOT DEPLOYED
schema_name,table_name,table_owner,rls_enabled,force_rls,table_acl,column_position,column_name,data_type,not_null,identity_type,generated_type,default_expression
public,_booking_move_audit_context,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",1,transaction_id,bigint,true,null,null,null
public,_booking_move_audit_context,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",2,auth_user_id,uuid,true,null,null,null
public,_booking_move_audit_context,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",3,booking_id,uuid,true,null,null,null
public,_booking_move_audit_context,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",4,created_at,timestamp with time zone,true,null,null,now()
public,academy_attendance,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,academy_attendance,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",2,club_id,uuid,true,null,null,null
public,academy_attendance,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",3,booking_id,uuid,true,null,null,null
public,academy_attendance,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",4,participant_id,uuid,true,null,null,null
public,academy_attendance,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",5,attendance_status,text,true,null,null,null
public,academy_attendance,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",6,recorded_by_user_profile_id,uuid,true,null,null,null
public,academy_attendance,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",7,recorded_at,timestamp with time zone,true,null,null,now()
public,academy_attendance,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",8,updated_at,timestamp with time zone,true,null,null,now()
public,academy_attendance_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,academy_attendance_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",2,club_id,uuid,true,null,null,null
public,academy_attendance_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",3,booking_id,uuid,false,null,null,null
public,academy_attendance_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",4,participant_id,uuid,false,null,null,null
public,academy_attendance_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",5,old_status,text,false,null,null,null
public,academy_attendance_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",6,new_status,text,true,null,null,null
public,academy_attendance_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",7,changed_by_user_profile_id,uuid,false,null,null,null
public,academy_attendance_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",8,changed_at,timestamp with time zone,true,null,null,now()
public,academy_booking_batches,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,academy_booking_batches,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",2,club_id,uuid,true,null,null,null
public,academy_booking_batches,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",3,academy_type,text,true,null,null,null
public,academy_booking_batches,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",4,booking_scope,text,true,null,null,null
public,academy_booking_batches,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",5,academy_program_id,uuid,false,null,null,null
public,academy_booking_batches,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",6,individual_player_name,text,false,null,null,null
public,academy_booking_batches,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",7,court_fee_huf_per_occurrence,numeric,true,null,null,null
public,academy_booking_batches,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",8,booking_note,text,false,null,null,null
public,academy_booking_batches,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",9,first_booking_date,date,true,null,null,null
public,academy_booking_batches,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",10,occurrence_count,integer,true,null,null,null
public,academy_booking_batches,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",11,status,text,true,null,null,'active'::text
public,academy_booking_batches,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",12,request_fingerprint,text,true,null,null,null
public,academy_booking_batches,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",13,created_by_user_profile_id,uuid,true,null,null,null
public,academy_booking_batches,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",14,created_at,timestamp with time zone,true,null,null,now()
public,academy_booking_batches,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",15,updated_at,timestamp with time zone,true,null,null,now()
public,academy_booking_coach_checkins,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,academy_booking_coach_checkins,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",2,club_id,uuid,true,null,null,null
public,academy_booking_coach_checkins,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",3,booking_id,uuid,true,null,null,null
public,academy_booking_coach_checkins,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",4,coach_profile_id,uuid,true,null,null,null
public,academy_booking_coach_checkins,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",5,checked_in_at,timestamp with time zone,true,null,null,now()
public,academy_booking_coach_checkins,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",6,checked_out_at,timestamp with time zone,false,null,null,null
public,academy_booking_coach_checkins,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",7,created_at,timestamp with time zone,true,null,null,now()
public,academy_booking_coach_checkins,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",8,updated_at,timestamp with time zone,true,null,null,now()
public,academy_booking_details,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",1,booking_id,uuid,true,null,null,null
public,academy_booking_details,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",2,academy_booking_batch_id,uuid,true,null,null,null
public,academy_booking_details,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",3,club_id,uuid,true,null,null,null
public,academy_booking_details,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",4,academy_type,text,true,null,null,null
public,academy_booking_details,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",5,academy_program_id,uuid,false,null,null,null
public,academy_booking_details,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",6,court_fee_huf,numeric,true,null,null,null
public,academy_booking_details,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",7,booking_note,text,false,null,null,null
public,academy_booking_details,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",8,occurrence_status,text,true,null,null,'scheduled'::text
public,academy_booking_details,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",9,cancelled_at,timestamp with time zone,false,null,null,null
public,academy_booking_details,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",10,cancelled_by_user_profile_id,uuid,false,null,null,null
public,academy_booking_details,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",11,cancellation_reason,text,false,null,null,null
public,academy_booking_details,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",12,created_at,timestamp with time zone,true,null,null,now()
public,academy_booking_details,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",13,updated_at,timestamp with time zone,true,null,null,now()
public,academy_camp_daily_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,academy_camp_daily_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",2,club_id,uuid,true,null,null,null
public,academy_camp_daily_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",3,participant_id,uuid,true,null,null,null
public,academy_camp_daily_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",4,attendance_date,date,true,null,null,null
public,academy_camp_daily_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",5,is_paid,boolean,true,null,null,false
public,academy_camp_daily_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",6,expected_fee_huf,numeric,false,null,null,null
public,academy_camp_daily_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",7,paid_amount_huf,numeric,false,null,null,null
public,academy_camp_daily_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",8,paid_on,date,false,null,null,null
public,academy_camp_daily_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",9,updated_by_user_profile_id,uuid,true,null,null,null
public,academy_camp_daily_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",10,created_at,timestamp with time zone,true,null,null,now()
public,academy_camp_daily_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",11,updated_at,timestamp with time zone,true,null,null,now()
public,academy_camp_weekly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,academy_camp_weekly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",2,club_id,uuid,true,null,null,null
public,academy_camp_weekly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",3,participant_id,uuid,true,null,null,null
public,academy_camp_weekly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",4,is_paid,boolean,true,null,null,false
public,academy_camp_weekly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",5,expected_fee_huf,numeric,false,null,null,null
public,academy_camp_weekly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",6,paid_amount_huf,numeric,false,null,null,null
public,academy_camp_weekly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",7,paid_on,date,false,null,null,null
public,academy_camp_weekly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",8,internal_note,text,false,null,null,null
public,academy_camp_weekly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",9,updated_by_user_profile_id,uuid,true,null,null,null
public,academy_camp_weekly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",10,created_at,timestamp with time zone,true,null,null,now()
public,academy_camp_weekly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",11,updated_at,timestamp with time zone,true,null,null,now()
public,academy_coach_checkin_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,academy_coach_checkin_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",2,club_id,uuid,true,null,null,null
public,academy_coach_checkin_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",3,booking_id,uuid,false,null,null,null
public,academy_coach_checkin_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",4,coach_profile_id,uuid,false,null,null,null
public,academy_coach_checkin_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",5,action,text,true,null,null,null
public,academy_coach_checkin_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",6,action_by_user_profile_id,uuid,false,null,null,null
public,academy_coach_checkin_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",7,action_at,timestamp with time zone,true,null,null,now()
public,academy_coach_compensation_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,academy_coach_compensation_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",2,club_id,uuid,true,null,null,null
public,academy_coach_compensation_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",3,academy_program_id,uuid,false,null,null,null
public,academy_coach_compensation_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",4,coach_profile_id,uuid,false,null,null,null
public,academy_coach_compensation_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",5,assignment_id,uuid,false,null,null,null
public,academy_coach_compensation_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",6,compensation_rate_id,uuid,false,null,null,null
public,academy_coach_compensation_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",7,action_type,text,true,null,null,null
public,academy_coach_compensation_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",8,old_values,jsonb,false,null,null,null
public,academy_coach_compensation_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",9,new_values,jsonb,false,null,null,null
public,academy_coach_compensation_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",10,changed_by_user_profile_id,uuid,false,null,null,null
public,academy_coach_compensation_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",11,changed_at,timestamp with time zone,true,null,null,now()
public,academy_coach_compensation_rates,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,academy_coach_compensation_rates,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",2,club_id,uuid,true,null,null,null
public,academy_coach_compensation_rates,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",3,assignment_id,uuid,true,null,null,null
public,academy_coach_compensation_rates,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",4,compensation_type,text,true,null,null,'per_session'::text
public,academy_coach_compensation_rates,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",5,amount_huf,"numeric(12,2)",true,null,null,null
public,academy_coach_compensation_rates,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",6,effective_from,date,true,null,null,null
public,academy_coach_compensation_rates,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",7,effective_to,date,false,null,null,null
public,academy_coach_compensation_rates,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",8,is_void,boolean,true,null,null,false
public,academy_coach_compensation_rates,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",9,created_by_user_profile_id,uuid,true,null,null,null
public,academy_coach_compensation_rates,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",10,updated_by_user_profile_id,uuid,true,null,null,null
public,academy_coach_compensation_rates,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",11,created_at,timestamp with time zone,true,null,null,now()
public,academy_coach_compensation_rates,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",12,updated_at,timestamp with time zone,true,null,null,now()
public,academy_group_court_cost_settings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",1,academy_program_id,uuid,true,null,null,null
public,academy_group_court_cost_settings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",2,club_id,uuid,true,null,null,null
public,academy_group_court_cost_settings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",3,default_court_fee_huf,"numeric(12,2)",true,null,null,0
public,academy_group_court_cost_settings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",4,created_by_user_profile_id,uuid,true,null,null,null
public,academy_group_court_cost_settings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",5,updated_by_user_profile_id,uuid,true,null,null,null
public,academy_group_court_cost_settings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",6,created_at,timestamp with time zone,true,null,null,now()
public,academy_group_court_cost_settings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",7,updated_at,timestamp with time zone,true,null,null,now()
public,academy_group_monthly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,academy_group_monthly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",2,club_id,uuid,true,null,null,null
public,academy_group_monthly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",3,participant_id,uuid,true,null,null,null
public,academy_group_monthly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",4,year,integer,true,null,null,null
public,academy_group_monthly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",5,month,integer,true,null,null,null
public,academy_group_monthly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",6,is_paid,boolean,true,null,null,false
public,academy_group_monthly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",7,expected_fee_huf,numeric,false,null,null,null
public,academy_group_monthly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",8,paid_amount_huf,numeric,false,null,null,null
public,academy_group_monthly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",9,paid_on,date,false,null,null,null
public,academy_group_monthly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",10,internal_note,text,false,null,null,null
public,academy_group_monthly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",11,updated_by_user_profile_id,uuid,true,null,null,null
public,academy_group_monthly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",12,created_at,timestamp with time zone,true,null,null,now()
public,academy_group_monthly_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",13,updated_at,timestamp with time zone,true,null,null,now()
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",2,club_id,uuid,true,null,null,null
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",3,participant_id,uuid,false,null,null,null
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",4,action_type,text,true,null,null,null
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",5,period_identifier,text,true,null,null,null
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",6,camp_fee_type,text,false,null,null,null
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",7,old_camp_fee_type,text,false,null,null,null
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",8,new_camp_fee_type,text,false,null,null,null
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",9,old_is_paid,boolean,false,null,null,null
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",10,new_is_paid,boolean,false,null,null,null
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",11,old_expected_fee,numeric,false,null,null,null
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",12,new_expected_fee,numeric,false,null,null,null
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",13,old_paid_amount,numeric,false,null,null,null
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",14,new_paid_amount,numeric,false,null,null,null
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",15,old_paid_on,date,false,null,null,null
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",16,new_paid_on,date,false,null,null,null
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",17,old_internal_note,text,false,null,null,null
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",18,new_internal_note,text,false,null,null,null
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",19,updated_by_user_profile_id,uuid,false,null,null,null
public,academy_payment_audit_logs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",20,created_at,timestamp with time zone,true,null,null,now()
public,academy_program_coach_assignments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,academy_program_coach_assignments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",2,club_id,uuid,true,null,null,null
public,academy_program_coach_assignments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",3,academy_program_id,uuid,true,null,null,null
public,academy_program_coach_assignments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",4,coach_profile_id,uuid,true,null,null,null
public,academy_program_coach_assignments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",5,assigned_from,date,true,null,null,null
public,academy_program_coach_assignments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",6,assigned_to,date,false,null,null,null
public,academy_program_coach_assignments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",7,is_void,boolean,true,null,null,false
public,academy_program_coach_assignments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",8,created_by_user_profile_id,uuid,true,null,null,null
public,academy_program_coach_assignments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",9,updated_by_user_profile_id,uuid,true,null,null,null
public,academy_program_coach_assignments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",10,created_at,timestamp with time zone,true,null,null,now()
public,academy_program_coach_assignments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",11,updated_at,timestamp with time zone,true,null,null,now()
public,academy_program_participants,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,academy_program_participants,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",2,club_id,uuid,true,null,null,null
public,academy_program_participants,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",3,academy_program_id,uuid,true,null,null,null
public,academy_program_participants,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",4,last_name,text,true,null,null,null
public,academy_program_participants,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",5,first_name,text,true,null,null,null
public,academy_program_participants,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",6,birth_year,integer,true,null,null,null
public,academy_program_participants,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",7,ctr,numeric,false,null,null,null
public,academy_program_participants,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",8,is_active,boolean,true,null,null,true
public,academy_program_participants,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",9,joined_on,date,true,null,null,CURRENT_DATE
public,academy_program_participants,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",10,left_on,date,false,null,null,null
public,academy_program_participants,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",11,monthly_fee_huf,numeric,false,null,null,null
public,academy_program_participants,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",12,camp_fee_type,text,false,null,null,null
public,academy_program_participants,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",13,camp_fee_huf,numeric,false,null,null,null
public,academy_program_participants,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",14,internal_note,text,false,null,null,null
public,academy_program_participants,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",15,created_by_user_profile_id,uuid,true,null,null,null
public,academy_program_participants,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",16,created_at,timestamp with time zone,true,null,null,now()
public,academy_program_participants,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",17,updated_at,timestamp with time zone,true,null,null,now()
public,academy_programs,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,academy_programs,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",2,club_id,uuid,true,null,null,null
public,academy_programs,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",3,program_type,text,true,null,null,null
public,academy_programs,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",4,name,text,true,null,null,null
public,academy_programs,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",5,description,text,false,null,null,null
public,academy_programs,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",6,is_active,boolean,true,null,null,true
public,academy_programs,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",7,created_by_user_profile_id,uuid,true,null,null,null
public,academy_programs,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",8,created_at,timestamp with time zone,true,null,null,now()
public,academy_programs,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",9,updated_at,timestamp with time zone,true,null,null,now()
public,academy_programs,postgres,true,false,"{postgres=arwdDxtm/postgres,authenticated=arwDxtm/postgres,service_role=arwdDxtm/postgres}",10,sport,text,true,null,null,'tennis'::text
public,audit_log,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,audit_log,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",2,club_id,uuid,true,null,null,null
public,audit_log,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",3,action_type,text,true,null,null,null
public,audit_log,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",4,entity_type,text,true,null,null,null
public,audit_log,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",5,entity_id,uuid,false,null,null,null
public,audit_log,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",6,changed_by_user_id,uuid,false,null,null,null
public,audit_log,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",7,changed_by_name,text,false,null,null,null
public,audit_log,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",8,changed_by_role,text,false,null,null,null
public,audit_log,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",9,old_value,jsonb,false,null,null,null
public,audit_log,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",10,new_value,jsonb,false,null,null,null
public,audit_log,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",11,created_at,timestamp with time zone,false,null,null,now()
public,booking_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,booking_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",2,club_id,uuid,true,null,null,null
public,booking_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",3,season_id,uuid,true,null,null,null
public,booking_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",4,booking_id,uuid,false,null,null,null
public,booking_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",5,user_profile_id,uuid,false,null,null,null
public,booking_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",6,payment_method,text,true,null,null,null
public,booking_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",7,payment_status,text,true,null,null,null
public,booking_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",8,amount,"numeric(14,2)",true,null,null,null
public,booking_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",9,currency,text,true,null,null,null
public,booking_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",10,credit_type,text,false,null,null,null
public,booking_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",11,wallet_id,uuid,false,null,null,null
public,booking_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",12,provider_reference,text,false,null,null,null
public,booking_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",13,created_at,timestamp with time zone,true,null,null,now()
public,booking_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",14,paid_at,timestamp with time zone,false,null,null,null
public,booking_payments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",15,refunded_at,timestamp with time zone,false,null,null,null
public,booking_requests,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",1,request_id,uuid,true,null,null,null
public,booking_requests,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",2,caller_user_profile_id,uuid,true,null,null,null
public,booking_requests,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",3,target_user_profile_id,uuid,false,null,null,null
public,booking_requests,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",4,club_id,uuid,true,null,null,null
public,booking_requests,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",5,season_id,uuid,true,null,null,null
public,booking_requests,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",6,payment_method,text,true,null,null,null
public,booking_requests,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",7,request_payload,jsonb,true,null,null,null
public,booking_requests,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",8,request_status,text,true,null,null,null
public,booking_requests,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",9,created_booking_ids,uuid[],false,null,null,null
public,booking_requests,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",10,created_at,timestamp with time zone,true,null,null,now()
public,booking_requests,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",11,completed_at,timestamp with time zone,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",2,club_id,uuid,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",3,season_id,uuid,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",4,court_id,uuid,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",5,booking_date,date,true,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",6,start_time,time without time zone,true,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",7,end_time,time without time zone,true,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",8,booking_type,text,true,null,null,'one_time'::text
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",9,recurring_series_id,uuid,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",10,booking_name,text,true,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",11,user_id,uuid,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",12,email,text,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",13,phone,text,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",14,booking_note,text,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",15,internal_admin_note,text,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",16,status,text,true,null,null,'confirmed'::text
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",17,is_all_day,boolean,false,null,null,false
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",18,is_all_courts,boolean,false,null,null,false
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",19,selected_court_ids,uuid[],false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",20,is_partner_search_enabled,boolean,false,null,null,false
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",21,booking_owner_utr_rating,numeric,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",22,partner_search_utr_min,numeric,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",23,partner_search_utr_max,numeric,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",24,partner_search_players_needed,integer,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",25,partner_search_status,text,false,null,null,'inactive'::text
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",26,price_is_configured,boolean,false,null,null,false
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",27,price_is_fully_configured,boolean,false,null,null,false
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",28,price_per_booking,numeric,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",29,estimated_total_price,numeric,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",30,currency,text,false,null,null,'USD'::text
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",31,price_breakdown,jsonb,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",32,created_by_user_id,uuid,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",33,created_by_role,text,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",34,created_at,timestamp with time zone,false,null,null,now()
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",35,updated_at,timestamp with time zone,false,null,null,now()
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",36,deleted_at,timestamp with time zone,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",37,pricing_mode,text,false,null,null,'standard'::text
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",38,payment_status,text,true,null,null,'pending'::text
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",39,payment_method,text,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",40,checked_in_coach_profile_id,uuid,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",41,coach_checked_in_at,timestamp with time zone,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",42,cancelled_by_profile_id,uuid,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",43,cancelled_by_name,text,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",44,cancelled_by_role,text,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",45,cancellation_reason_category,text,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",46,cancellation_reason,text,false,null,null,null
public,bookings,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",47,sport,text,true,null,null,'tennis'::text
public,club_ghl_config,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",1,club_id,uuid,true,null,null,null
public,club_ghl_config,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",2,location_id,text,false,null,null,null
public,club_ghl_config,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",3,sync_enabled,boolean,true,null,null,false
public,club_ghl_config,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",4,source_name,text,true,null,null,'Bravos Foglalórendszer'::text
public,club_ghl_config,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",5,contact_tags,text[],true,null,null,"ARRAY['Foglalórendszer'::text, 'Regisztrált felhasználó'::text]"
public,club_ghl_config,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",6,created_at,timestamp with time zone,true,null,null,now()
public,club_ghl_config,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",7,updated_at,timestamp with time zone,true,null,null,now()
public,clubs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=r/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,clubs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=r/postgres}",2,name,text,true,null,null,null
public,clubs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=r/postgres}",3,slug,text,true,null,null,null
public,clubs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=r/postgres}",4,timezone,text,false,null,null,'America/New_York'::text
public,clubs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=r/postgres}",5,currency,text,false,null,null,'USD'::text
public,clubs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=r/postgres}",6,gohighlevel_location_id,text,false,null,null,null
public,clubs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=r/postgres}",7,created_at,timestamp with time zone,false,null,null,now()
public,clubs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=r/postgres}",8,updated_at,timestamp with time zone,false,null,null,now()
public,clubs,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=r/postgres}",9,cancellation_cutoff_hours,integer,true,null,null,24
public,coach_availability,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,coach_availability,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",2,coach_id,uuid,true,null,null,null
public,coach_availability,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",3,day_of_week,integer,true,null,null,null
public,coach_availability,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",4,start_time,time without time zone,true,null,null,null
public,coach_availability,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",5,end_time,time without time zone,true,null,null,null
public,coach_availability,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",6,is_active,boolean,true,null,null,true
public,coach_availability,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",7,created_at,timestamp with time zone,true,null,null,now()
public,coach_availability,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",8,updated_at,timestamp with time zone,true,null,null,now()
public,coach_availability,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",9,sport,text,true,null,null,'tennis'::text
public,coach_permissions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,coach_permissions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",2,club_id,uuid,true,null,null,null
public,coach_permissions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",3,coach_user_profile_id,uuid,true,null,null,null
public,coach_permissions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",4,is_active,boolean,true,null,null,true
public,coach_permissions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",5,can_create_own_bookings,boolean,true,null,null,true
public,coach_permissions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",6,can_edit_own_bookings,boolean,true,null,null,true
public,coach_permissions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",7,can_delete_own_bookings,boolean,true,null,null,true
public,coach_permissions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",8,can_view_own_schedule,boolean,true,null,null,true
public,coach_permissions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",9,can_manage_other_bookings,boolean,true,null,null,false
public,coach_permissions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",10,can_access_admin,boolean,true,null,null,false
public,coach_permissions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",11,notes,text,false,null,null,null
public,coach_permissions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",12,created_by_user_profile_id,uuid,false,null,null,null
public,coach_permissions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",13,created_at,timestamp with time zone,true,null,null,now()
public,coach_permissions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",14,updated_at,timestamp with time zone,true,null,null,now()
public,coach_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,coach_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",2,user_id,uuid,true,null,null,null
public,coach_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",3,display_name,text,false,null,null,null
public,coach_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",4,color,text,true,null,null,'#2563eb'::text
public,coach_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",5,phone,text,false,null,null,null
public,coach_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",6,email,text,false,null,null,null
public,coach_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",7,min_advance_hours,integer,true,null,null,24
public,coach_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",8,is_active,boolean,true,null,null,true
public,coach_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",9,created_at,timestamp with time zone,true,null,null,now()
public,coach_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",10,updated_at,timestamp with time zone,true,null,null,now()
public,coach_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",11,user_profile_id,uuid,false,null,null,null
public,courts,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,courts,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",2,club_id,uuid,false,null,null,null
public,courts,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",3,season_id,uuid,false,null,null,null
public,courts,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",4,court_number,integer,true,null,null,null
public,courts,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",5,court_name,text,true,null,null,null
public,courts,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",6,is_active,boolean,false,null,null,true
public,courts,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",7,created_at,timestamp with time zone,false,null,null,now()
public,courts,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",8,updated_at,timestamp with time zone,false,null,null,now()
public,courts,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",9,surface_type,text,false,null,null,'clay'::text
public,courts,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",10,court_environment,text,false,null,null,'outdoor'::text
public,courts,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",11,sport,text,true,null,null,'tennis'::text
public,credit_transactions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,credit_transactions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",2,wallet_id,uuid,true,null,null,null
public,credit_transactions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",3,club_id,uuid,true,null,null,null
public,credit_transactions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",4,user_profile_id,uuid,true,null,null,null
public,credit_transactions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",5,season_id,uuid,true,null,null,null
public,credit_transactions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",6,booking_id,uuid,false,null,null,null
public,credit_transactions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",7,credit_type,text,true,null,null,null
public,credit_transactions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",8,currency,text,true,null,null,null
public,credit_transactions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",9,amount,"numeric(14,2)",true,null,null,null
public,credit_transactions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",10,transaction_category,text,true,null,null,null
public,credit_transactions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",11,reason,text,false,null,null,null
public,credit_transactions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",12,created_by,uuid,true,null,null,null
public,credit_transactions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",13,created_at,timestamp with time zone,true,null,null,now()
public,credit_transactions,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",14,idempotency_key,text,false,null,null,null
public,credit_wallets,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,credit_wallets,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",2,club_id,uuid,true,null,null,null
public,credit_wallets,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",3,user_profile_id,uuid,true,null,null,null
public,credit_wallets,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",4,season_id,uuid,true,null,null,null
public,credit_wallets,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",5,currency,text,true,null,null,null
public,credit_wallets,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",6,credit_type,text,true,null,null,null
public,credit_wallets,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",7,balance,"numeric(14,2)",true,null,null,0
public,credit_wallets,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",8,created_at,timestamp with time zone,true,null,null,now()
public,credit_wallets,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",9,updated_at,timestamp with time zone,true,null,null,now()
public,ghl_contact_sync_queue,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,ghl_contact_sync_queue,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",2,user_profile_id,uuid,true,null,null,null
public,ghl_contact_sync_queue,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",3,club_id,uuid,true,null,null,null
public,ghl_contact_sync_queue,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",4,sync_status,text,true,null,null,'pending'::text
public,ghl_contact_sync_queue,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",5,attempt_count,integer,true,null,null,0
public,ghl_contact_sync_queue,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",6,next_attempt_at,timestamp with time zone,true,null,null,now()
public,ghl_contact_sync_queue,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",7,last_attempt_at,timestamp with time zone,false,null,null,null
public,ghl_contact_sync_queue,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",8,synced_at,timestamp with time zone,false,null,null,null
public,ghl_contact_sync_queue,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",9,last_error,text,false,null,null,null
public,ghl_contact_sync_queue,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",10,gohighlevel_contact_id,text,false,null,null,null
public,ghl_contact_sync_queue,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",11,locked_at,timestamp with time zone,false,null,null,null
public,ghl_contact_sync_queue,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",12,locked_by,text,false,null,null,null
public,ghl_contact_sync_queue,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",13,created_at,timestamp with time zone,true,null,null,now()
public,ghl_contact_sync_queue,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",14,updated_at,timestamp with time zone,true,null,null,now()
public,opening_hours,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,opening_hours,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",2,club_id,uuid,false,null,null,null
public,opening_hours,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",3,season_id,uuid,false,null,null,null
public,opening_hours,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",4,weekday,integer,true,null,null,null
public,opening_hours,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",5,is_open,boolean,false,null,null,true
public,opening_hours,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",6,opening_time,time without time zone,true,null,null,null
public,opening_hours,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",7,closing_time,time without time zone,true,null,null,null
public,opening_hours,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",8,created_at,timestamp with time zone,false,null,null,now()
public,opening_hours,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",9,updated_at,timestamp with time zone,false,null,null,now()
public,partner_finder_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,partner_finder_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",2,club_id,uuid,true,null,null,null
public,partner_finder_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",3,user_profile_id,uuid,true,null,null,null
public,partner_finder_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",4,utr_rating,numeric,false,null,null,null
public,partner_finder_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",5,show_email_to_players,boolean,false,null,null,false
public,partner_finder_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",6,show_phone_to_players,boolean,false,null,null,false
public,partner_finder_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",7,partner_search_profile_enabled,boolean,false,null,null,false
public,partner_finder_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",8,last_partner_search_booking_date,date,false,null,null,null
public,partner_finder_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",9,created_at,timestamp with time zone,false,null,null,now()
public,partner_finder_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",10,updated_at,timestamp with time zone,false,null,null,now()
public,partner_finder_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=arwd/postgres}",11,sport,text,true,null,null,'tennis'::text
public,partner_search_availability,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,partner_search_availability,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",2,user_profile_id,uuid,true,null,null,null
public,partner_search_availability,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",3,club_id,uuid,true,null,null,null
public,partner_search_availability,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",4,weekday,integer,true,null,null,null
public,partner_search_availability,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",5,start_time,time without time zone,true,null,null,null
public,partner_search_availability,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",6,end_time,time without time zone,true,null,null,null
public,partner_search_availability,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",7,created_at,timestamp with time zone,true,null,null,now()
public,partner_search_availability,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",8,updated_at,timestamp with time zone,true,null,null,now()
public,partner_search_availability,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",9,sport,text,true,null,null,'tennis'::text
public,pricing_group_courts,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,pricing_group_courts,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",2,pricing_group_id,uuid,true,null,null,null
public,pricing_group_courts,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",3,court_id,uuid,true,null,null,null
public,pricing_group_courts,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",4,created_at,timestamp with time zone,true,null,null,now()
public,pricing_groups,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,pricing_groups,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",2,club_id,uuid,true,null,null,null
public,pricing_groups,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",3,season_id,uuid,true,null,null,null
public,pricing_groups,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",4,sport,text,true,null,null,'tennis'::text
public,pricing_groups,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",5,name,text,false,null,null,null
public,pricing_groups,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",6,display_order,integer,true,null,null,0
public,pricing_groups,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",7,created_at,timestamp with time zone,true,null,null,now()
public,pricing_groups,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",8,updated_at,timestamp with time zone,true,null,null,now()
public,pricing_rules,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,pricing_rules,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",2,club_id,uuid,false,null,null,null
public,pricing_rules,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",3,season_id,uuid,false,null,null,null
public,pricing_rules,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",4,weekday,integer,true,null,null,null
public,pricing_rules,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",5,start_time,time without time zone,true,null,null,null
public,pricing_rules,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",6,end_time,time without time zone,true,null,null,null
public,pricing_rules,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",7,price_per_hour,numeric,true,null,null,null
public,pricing_rules,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",8,currency,text,true,null,null,'USD'::text
public,pricing_rules,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",9,is_active,boolean,false,null,null,true
public,pricing_rules,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",10,created_at,timestamp with time zone,false,null,null,now()
public,pricing_rules,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",11,updated_at,timestamp with time zone,false,null,null,now()
public,pricing_rules,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",12,season_pass_price,"numeric(12,2)",false,null,null,null
public,pricing_rules,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",13,pricing_group_id,uuid,false,null,null,null
public,pricing_rules_emergency_backup_20260716,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",1,id,uuid,false,null,null,null
public,pricing_rules_emergency_backup_20260716,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",2,club_id,uuid,false,null,null,null
public,pricing_rules_emergency_backup_20260716,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",3,season_id,uuid,false,null,null,null
public,pricing_rules_emergency_backup_20260716,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",4,weekday,integer,false,null,null,null
public,pricing_rules_emergency_backup_20260716,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",5,start_time,time without time zone,false,null,null,null
public,pricing_rules_emergency_backup_20260716,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",6,end_time,time without time zone,false,null,null,null
public,pricing_rules_emergency_backup_20260716,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",7,price_per_hour,numeric,false,null,null,null
public,pricing_rules_emergency_backup_20260716,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",8,currency,text,false,null,null,null
public,pricing_rules_emergency_backup_20260716,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",9,is_active,boolean,false,null,null,null
public,pricing_rules_emergency_backup_20260716,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",10,created_at,timestamp with time zone,false,null,null,null
public,pricing_rules_emergency_backup_20260716,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",11,updated_at,timestamp with time zone,false,null,null,null
public,pricing_rules_emergency_backup_20260716,postgres,true,false,"{postgres=arwdDxtm/postgres,anon=arwdDxtm/postgres,authenticated=arwdDxtm/postgres,service_role=arwdDxtm/postgres}",12,season_pass_price,"numeric(12,2)",false,null,null,null
public,recurring_booking_series,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,recurring_booking_series,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",2,club_id,uuid,true,null,null,null
public,recurring_booking_series,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",3,season_id,uuid,true,null,null,null
public,recurring_booking_series,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",4,user_profile_id,uuid,false,null,null,null
public,recurring_booking_series,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",5,booking_name,text,true,null,null,null
public,recurring_booking_series,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",6,customer_email,text,false,null,null,null
public,recurring_booking_series,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",7,customer_phone,text,false,null,null,null
public,recurring_booking_series,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",8,currency,text,true,null,null,null
public,recurring_booking_series,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",9,pricing_mode,text,true,null,null,null
public,recurring_booking_series,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",10,status,text,true,null,null,'active'::text
public,recurring_booking_series,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",11,start_date,date,true,null,null,null
public,recurring_booking_series,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",12,end_date,date,true,null,null,null
public,recurring_booking_series,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",13,total_occurrences,integer,true,null,null,null
public,recurring_booking_series,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",14,total_amount,"numeric(14,2)",true,null,null,null
public,recurring_booking_series,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",15,created_by_user_profile_id,uuid,true,null,null,null
public,recurring_booking_series,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",16,created_at,timestamp with time zone,true,null,null,now()
public,recurring_booking_series,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",17,updated_at,timestamp with time zone,true,null,null,now()
public,recurring_booking_series,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",18,creation_request_id,uuid,false,null,null,null
public,seasons,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,seasons,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",2,club_id,uuid,false,null,null,null
public,seasons,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",3,name,text,true,null,null,null
public,seasons,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",4,start_date,date,true,null,null,null
public,seasons,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",5,end_date,date,true,null,null,null
public,seasons,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",6,court_quantity,integer,true,null,null,12
public,seasons,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",7,currency,text,true,null,null,'USD'::text
public,seasons,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",8,booking_status,text,true,null,null,'open'::text
public,seasons,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",9,is_current_season,boolean,false,null,null,false
public,seasons,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",10,is_next_season,boolean,false,null,null,false
public,seasons,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",11,created_at,timestamp with time zone,false,null,null,now()
public,seasons,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",12,updated_at,timestamp with time zone,false,null,null,now()
public,seasons,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,anon=r/postgres,authenticated=arwd/postgres}",13,sport,text,true,null,null,'tennis'::text
public,settings_history,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,settings_history,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",2,club_id,uuid,true,null,null,null
public,settings_history,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",3,version_number,integer,true,null,null,null
public,settings_history,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",4,changed_by_user_id,uuid,false,null,null,null
public,settings_history,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",5,changed_by_name,text,false,null,null,null
public,settings_history,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",6,changed_by_role,text,false,null,null,null
public,settings_history,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",7,change_description,text,true,null,null,null
public,settings_history,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",8,changed_setting_key,text,true,null,null,null
public,settings_history,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",9,previous_value,jsonb,false,null,null,null
public,settings_history,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",10,new_value,jsonb,false,null,null,null
public,settings_history,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",11,full_settings_snapshot,jsonb,false,null,null,null
public,settings_history,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",12,created_at,timestamp with time zone,false,null,null,now()
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",2,club_id,uuid,false,null,null,null
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",3,auth_user_id,uuid,false,null,null,null
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",4,full_name,text,true,null,null,null
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",5,first_name,text,false,null,null,null
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",6,last_name,text,false,null,null,null
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",7,email,text,false,null,null,null
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",8,phone,text,false,null,null,null
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",9,city,text,false,null,null,null
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",10,country,text,false,null,null,null
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",11,preferred_language,text,false,null,null,null
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",12,utr_rating,numeric,false,null,null,null
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",13,show_email_to_players,boolean,false,null,null,false
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",14,show_phone_to_players,boolean,false,null,null,false
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",15,role,text,false,null,null,'registered_user'::text
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",16,gohighlevel_contact_id,text,false,null,null,null
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",17,created_at,timestamp with time zone,false,null,null,now()
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",18,updated_at,timestamp with time zone,false,null,null,now()
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",19,partner_search_profile_enabled,boolean,false,null,null,false
public,user_profiles,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=ar/postgres}",20,is_supported_player,boolean,true,null,null,false
public,venue_payment_installment_events,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,venue_payment_installment_events,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",2,installment_id,uuid,true,null,null,null
public,venue_payment_installment_events,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",3,request_id,uuid,true,null,null,null
public,venue_payment_installment_events,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",4,event_type,text,true,null,null,null
public,venue_payment_installment_events,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",5,amount,"numeric(14,2)",true,null,null,null
public,venue_payment_installment_events,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",6,currency,text,true,null,null,null
public,venue_payment_installment_events,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",7,performed_by_admin_id,uuid,true,null,null,null
public,venue_payment_installment_events,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",8,occurred_at,timestamp with time zone,true,null,null,now()
public,venue_payment_installment_events,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",9,note,text,false,null,null,null
public,venue_payment_installment_events,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",10,reverses_event_id,uuid,false,null,null,null
public,venue_payment_installment_events,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",11,created_at,timestamp with time zone,true,null,null,now()
public,venue_payment_installments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,venue_payment_installments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",2,plan_id,uuid,true,null,null,null
public,venue_payment_installments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",3,installment_number,integer,true,null,null,null
public,venue_payment_installments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",4,amount,"numeric(14,2)",true,null,null,null
public,venue_payment_installments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",5,currency,text,true,null,null,null
public,venue_payment_installments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",6,due_date,date,false,null,null,null
public,venue_payment_installments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",7,status,text,true,null,null,'pending'::text
public,venue_payment_installments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",8,current_paid_event_id,uuid,false,null,null,null
public,venue_payment_installments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",9,created_at,timestamp with time zone,true,null,null,now()
public,venue_payment_installments,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",10,updated_at,timestamp with time zone,true,null,null,now()
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",1,id,uuid,true,null,null,gen_random_uuid()
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",2,creation_request_id,uuid,true,null,null,null
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",3,club_id,uuid,true,null,null,null
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",4,season_id,uuid,true,null,null,null
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",5,booking_id,uuid,false,null,null,null
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",6,recurring_series_id,uuid,false,null,null,null
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",7,user_profile_id,uuid,false,null,null,null
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",8,customer_name,text,true,null,null,null
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",9,customer_email,text,false,null,null,null
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",10,customer_phone,text,false,null,null,null
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",11,payment_method,text,true,null,null,'venue'::text
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",12,total_amount,"numeric(14,2)",true,null,null,null
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",13,currency,text,true,null,null,null
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",14,installment_count,integer,true,null,null,null
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",15,plan_status,text,true,null,null,'pending'::text
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",16,created_by_admin_id,uuid,false,null,null,null
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",17,linked_by_admin_id,uuid,false,null,null,null
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",18,linked_at,timestamp with time zone,false,null,null,null
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",19,link_request_id,uuid,false,null,null,null
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",20,created_at,timestamp with time zone,true,null,null,now()
public,venue_payment_plans,postgres,true,false,"{postgres=arwdDxtm/postgres,service_role=arwdDxtm/postgres,authenticated=r/postgres}",21,updated_at,timestamp with time zone,true,null,null,now()
object_type,schema_name,table_name,object_name,subtype,definition CONSTRAINT,public,_booking_move_audit_context,booking_move_audit_context_pkey,p,"PRIMARY KEY (transaction_id, auth_user_id, booking_id)" INDEX,public,_booking_move_audit_context,booking_move_audit_context_pkey,constraint_backed,"CREATE UNIQUE INDEX booking_move_audit_context_pkey ON public._booking_move_audit_context USING btree (transaction_id, auth_user_id, booking_id)" CONSTRAINT,public,academy_attendance,academy_attendance_attendance_status_check,c,"CHECK (attendance_status = ANY (ARRAY['present'::text, 'absent'::text]))" CONSTRAINT,public,academy_attendance,academy_attendance_booking_id_fkey,f,FOREIGN KEY (booking_id) REFERENCES bookings(id) ON DELETE CASCADE CONSTRAINT,public,academy_attendance,academy_attendance_booking_participant_key,u,"UNIQUE (booking_id, participant_id)" CONSTRAINT,public,academy_attendance,academy_attendance_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) CONSTRAINT,public,academy_attendance,academy_attendance_participant_id_fkey,f,FOREIGN KEY (participant_id) REFERENCES academy_program_participants(id) ON DELETE CASCADE CONSTRAINT,public,academy_attendance,academy_attendance_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,academy_attendance,academy_attendance_recorded_by_user_profile_id_fkey,f,FOREIGN KEY (recorded_by_user_profile_id) REFERENCES user_profiles(id) INDEX,public,academy_attendance,academy_attendance_booking_participant_key,constraint_backed,"CREATE UNIQUE INDEX academy_attendance_booking_participant_key ON public.academy_attendance USING btree (booking_id, participant_id)" INDEX,public,academy_attendance,academy_attendance_pkey,constraint_backed,CREATE UNIQUE INDEX academy_attendance_pkey ON public.academy_attendance USING btree (id) CONSTRAINT,public,academy_attendance_audit_logs,academy_attendance_audit_logs_booking_id_fkey,f,FOREIGN KEY (booking_id) REFERENCES bookings(id) ON DELETE SET NULL CONSTRAINT,public,academy_attendance_audit_logs,academy_attendance_audit_logs_changed_by_user_profile_id_fkey,f,FOREIGN KEY (changed_by_user_profile_id) REFERENCES user_profiles(id) ON DELETE SET NULL CONSTRAINT,public,academy_attendance_audit_logs,academy_attendance_audit_logs_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) CONSTRAINT,public,academy_attendance_audit_logs,academy_attendance_audit_logs_new_status_check,c,"CHECK (new_status = ANY (ARRAY['present'::text, 'absent'::text]))" CONSTRAINT,public,academy_attendance_audit_logs,academy_attendance_audit_logs_old_status_check,c,"CHECK (old_status IS NULL OR (old_status = ANY (ARRAY['present'::text, 'absent'::text])))" CONSTRAINT,public,academy_attendance_audit_logs,academy_attendance_audit_logs_participant_id_fkey,f,FOREIGN KEY (participant_id) REFERENCES academy_program_participants(id) ON DELETE SET NULL CONSTRAINT,public,academy_attendance_audit_logs,academy_attendance_audit_logs_pkey,p,PRIMARY KEY (id) INDEX,public,academy_attendance_audit_logs,academy_attendance_audit_logs_pkey,constraint_backed,CREATE UNIQUE INDEX academy_attendance_audit_logs_pkey ON public.academy_attendance_audit_logs USING btree (id) CONSTRAINT,public,academy_booking_batches,academy_booking_batches_academy_type_check,c,"CHECK (academy_type = ANY (ARRAY['individual'::text, 'group'::text, 'camp'::text]))" CONSTRAINT,public,academy_booking_batches,academy_booking_batches_booking_scope_check,c,"CHECK (booking_scope = ANY (ARRAY['one_time'::text, 'season'::text, 'camp_5_day'::text, 'camp_7_day'::text]))" CONSTRAINT,public,academy_booking_batches,academy_booking_batches_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE RESTRICT CONSTRAINT,public,academy_booking_batches,academy_booking_batches_court_fee_huf_per_occurrence_check,c,CHECK (court_fee_huf_per_occurrence >= 0::numeric) CONSTRAINT,public,academy_booking_batches,academy_booking_batches_created_by_user_profile_id_fkey,f,FOREIGN KEY (created_by_user_profile_id) REFERENCES user_profiles(id) ON DELETE RESTRICT CONSTRAINT,public,academy_booking_batches,academy_booking_batches_id_club_key,u,"UNIQUE (id, club_id)" CONSTRAINT,public,academy_booking_batches,academy_booking_batches_occurrence_count_check,c,CHECK (occurrence_count > 0) CONSTRAINT,public,academy_booking_batches,academy_booking_batches_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,academy_booking_batches,academy_booking_batches_program_club_fkey,f,"FOREIGN KEY (academy_program_id, club_id) REFERENCES academy_programs(id, club_id) ON DELETE RESTRICT" CONSTRAINT,public,academy_booking_batches,academy_booking_batches_status_check,c,"CHECK (status = ANY (ARRAY['active'::text, 'partially_cancelled'::text, 'cancelled'::text]))" CONSTRAINT,public,academy_booking_batches,academy_booking_batches_type_scope_check,c,"CHECK (academy_type = 'individual'::text AND academy_program_id IS NULL AND NULLIF(btrim(individual_player_name), ''::text) IS NOT NULL AND (booking_scope = ANY (ARRAY['one_time'::text, 'season'::text])) OR academy_type = 'group'::text AND academy_program_id IS NOT NULL AND individual_player_name IS NULL AND (booking_scope = ANY (ARRAY['one_time'::text, 'season'::text])) OR academy_type = 'camp'::text AND academy_program_id IS NOT NULL AND individual_player_name IS NULL AND (booking_scope = ANY (ARRAY['one_time'::text, 'season'::text, 'camp_5_day'::text, 'camp_7_day'::text])))" INDEX,public,academy_booking_batches,academy_booking_batches_id_club_key,constraint_backed,"CREATE UNIQUE INDEX academy_booking_batches_id_club_key ON public.academy_booking_batches USING btree (id, club_id)" INDEX,public,academy_booking_batches,academy_booking_batches_id_club_key,constraint_backed,"CREATE UNIQUE INDEX academy_booking_batches_id_club_key ON public.academy_booking_batches USING btree (id, club_id)" INDEX,public,academy_booking_batches,academy_booking_batches_pkey,constraint_backed,CREATE UNIQUE INDEX academy_booking_batches_pkey ON public.academy_booking_batches USING btree (id) INDEX,public,academy_booking_batches,idx_academy_booking_batches_club,standalone,CREATE INDEX idx_academy_booking_batches_club ON public.academy_booking_batches USING btree (club_id) INDEX,public,academy_booking_batches,idx_academy_booking_batches_first_date,standalone,"CREATE INDEX idx_academy_booking_batches_first_date ON public.academy_booking_batches USING btree (club_id, first_booking_date)" INDEX,public,academy_booking_batches,idx_academy_booking_batches_program,standalone,CREATE INDEX idx_academy_booking_batches_program ON public.academy_booking_batches USING btree (academy_program_id) WHERE (academy_program_id IS NOT NULL) INDEX,public,academy_booking_batches,uq_academy_booking_active_request,standalone,"CREATE UNIQUE INDEX uq_academy_booking_active_request ON public.academy_booking_batches USING btree (club_id, request_fingerprint) WHERE (status = ANY (ARRAY['active'::text, 'partially_cancelled'::text]))" TRIGGER,public,academy_booking_batches,trg_apply_group_court_cost_to_academy_batch,enabled,CREATE TRIGGER trg_apply_group_court_cost_to_academy_batch BEFORE INSERT ON academy_booking_batches FOR EACH ROW EXECUTE FUNCTION apply_group_court_cost_to_academy_batch() TRIGGER,public,academy_booking_batches,trg_touch_academy_booking_batches,enabled,CREATE TRIGGER trg_touch_academy_booking_batches BEFORE UPDATE ON academy_booking_batches FOR EACH ROW EXECUTE FUNCTION touch_academy_booking_updated_at() CONSTRAINT,public,academy_booking_coach_checkins,academy_booking_coach_checkins_booking_id_fkey,f,FOREIGN KEY (booking_id) REFERENCES bookings(id) ON DELETE CASCADE CONSTRAINT,public,academy_booking_coach_checkins,academy_booking_coach_checkins_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,academy_booking_coach_checkins,academy_booking_coach_checkins_coach_profile_id_fkey,f,FOREIGN KEY (coach_profile_id) REFERENCES coach_profiles(id) ON DELETE CASCADE CONSTRAINT,public,academy_booking_coach_checkins,academy_booking_coach_checkins_pkey,p,PRIMARY KEY (id) INDEX,public,academy_booking_coach_checkins,academy_booking_coach_checkins_pkey,constraint_backed,CREATE UNIQUE INDEX academy_booking_coach_checkins_pkey ON public.academy_booking_coach_checkins USING btree (id) INDEX,public,academy_booking_coach_checkins,idx_academy_booking_coach_checkins_active,standalone,"CREATE UNIQUE INDEX idx_academy_booking_coach_checkins_active ON public.academy_booking_coach_checkins USING btree (booking_id, coach_profile_id) WHERE (checked_out_at IS NULL)" CONSTRAINT,public,academy_booking_details,academy_booking_details_academy_type_check,c,"CHECK (academy_type = ANY (ARRAY['individual'::text, 'group'::text, 'camp'::text]))" CONSTRAINT,public,academy_booking_details,academy_booking_details_batch_club_fkey,f,"FOREIGN KEY (academy_booking_batch_id, club_id) REFERENCES academy_booking_batches(id, club_id) ON DELETE RESTRICT" CONSTRAINT,public,academy_booking_details,academy_booking_details_booking_id_fkey,f,FOREIGN KEY (booking_id) REFERENCES bookings(id) ON DELETE RESTRICT CONSTRAINT,public,academy_booking_details,academy_booking_details_cancelled_by_user_profile_id_fkey,f,FOREIGN KEY (cancelled_by_user_profile_id) REFERENCES user_profiles(id) ON DELETE RESTRICT CONSTRAINT,public,academy_booking_details,academy_booking_details_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE RESTRICT CONSTRAINT,public,academy_booking_details,academy_booking_details_court_fee_huf_check,c,CHECK (court_fee_huf >= 0::numeric) CONSTRAINT,public,academy_booking_details,academy_booking_details_occurrence_status_check,c,"CHECK (occurrence_status = ANY (ARRAY['scheduled'::text, 'held'::text, 'cancelled'::text]))" CONSTRAINT,public,academy_booking_details,academy_booking_details_pkey,p,PRIMARY KEY (booking_id) CONSTRAINT,public,academy_booking_details,academy_booking_details_program_club_fkey,f,"FOREIGN KEY (academy_program_id, club_id) REFERENCES academy_programs(id, club_id) ON DELETE RESTRICT" INDEX,public,academy_booking_details,academy_booking_details_pkey,constraint_backed,CREATE UNIQUE INDEX academy_booking_details_pkey ON public.academy_booking_details USING btree (booking_id) INDEX,public,academy_booking_details,idx_academy_booking_details_batch,standalone,CREATE INDEX idx_academy_booking_details_batch ON public.academy_booking_details USING btree (academy_booking_batch_id) INDEX,public,academy_booking_details,idx_academy_booking_details_club,standalone,CREATE INDEX idx_academy_booking_details_club ON public.academy_booking_details USING btree (club_id) INDEX,public,academy_booking_details,idx_academy_booking_details_program,standalone,CREATE INDEX idx_academy_booking_details_program ON public.academy_booking_details USING btree (academy_program_id) WHERE (academy_program_id IS NOT NULL) INDEX,public,academy_booking_details,idx_academy_booking_details_status,standalone,"CREATE INDEX idx_academy_booking_details_status ON public.academy_booking_details USING btree (club_id, occurrence_status)" TRIGGER,public,academy_booking_details,trg_apply_group_court_cost_to_academy_detail,enabled,CREATE TRIGGER trg_apply_group_court_cost_to_academy_detail BEFORE INSERT ON academy_booking_details FOR EACH ROW EXECUTE FUNCTION apply_group_court_cost_to_academy_detail() TRIGGER,public,academy_booking_details,trg_touch_academy_booking_details,enabled,CREATE TRIGGER trg_touch_academy_booking_details BEFORE UPDATE ON academy_booking_details FOR EACH ROW EXECUTE FUNCTION touch_academy_booking_updated_at() CONSTRAINT,public,academy_camp_daily_payments,academy_camp_daily_consistency_check,c,CHECK (is_paid = false OR is_paid = true AND paid_amount_huf IS NOT NULL AND paid_on IS NOT NULL) CONSTRAINT,public,academy_camp_daily_payments,academy_camp_daily_expected_fee_check,c,CHECK (expected_fee_huf IS NULL OR expected_fee_huf >= 0::numeric) CONSTRAINT,public,academy_camp_daily_payments,academy_camp_daily_paid_amount_check,c,CHECK (paid_amount_huf IS NULL OR paid_amount_huf >= 0::numeric) CONSTRAINT,public,academy_camp_daily_payments,academy_camp_daily_payments_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,academy_camp_daily_payments,academy_camp_daily_payments_participant_id_fkey,f,FOREIGN KEY (participant_id) REFERENCES academy_program_participants(id) ON DELETE CASCADE CONSTRAINT,public,academy_camp_daily_payments,academy_camp_daily_payments_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,academy_camp_daily_payments,academy_camp_daily_payments_unique,u,"UNIQUE (participant_id, attendance_date)" CONSTRAINT,public,academy_camp_daily_payments,academy_camp_daily_payments_updated_by_user_profile_id_fkey,f,FOREIGN KEY (updated_by_user_profile_id) REFERENCES user_profiles(id) INDEX,public,academy_camp_daily_payments,academy_camp_daily_payments_pkey,constraint_backed,CREATE UNIQUE INDEX academy_camp_daily_payments_pkey ON public.academy_camp_daily_payments USING btree (id) INDEX,public,academy_camp_daily_payments,academy_camp_daily_payments_unique,constraint_backed,"CREATE UNIQUE INDEX academy_camp_daily_payments_unique ON public.academy_camp_daily_payments USING btree (participant_id, attendance_date)" INDEX,public,academy_camp_daily_payments,idx_academy_camp_daily_payments_club_id,standalone,CREATE INDEX idx_academy_camp_daily_payments_club_id ON public.academy_camp_daily_payments USING btree (club_id) INDEX,public,academy_camp_daily_payments,idx_academy_camp_daily_payments_date,standalone,CREATE INDEX idx_academy_camp_daily_payments_date ON public.academy_camp_daily_payments USING btree (attendance_date) INDEX,public,academy_camp_daily_payments,idx_academy_camp_daily_payments_participant_id,standalone,CREATE INDEX idx_academy_camp_daily_payments_participant_id ON public.academy_camp_daily_payments USING btree (participant_id) CONSTRAINT,public,academy_camp_weekly_payments,academy_camp_weekly_consistency_check,c,CHECK (is_paid = false OR is_paid = true AND paid_amount_huf IS NOT NULL AND paid_on IS NOT NULL) CONSTRAINT,public,academy_camp_weekly_payments,academy_camp_weekly_expected_fee_check,c,CHECK (expected_fee_huf IS NULL OR expected_fee_huf >= 0::numeric) CONSTRAINT,public,academy_camp_weekly_payments,academy_camp_weekly_paid_amount_check,c,CHECK (paid_amount_huf IS NULL OR paid_amount_huf >= 0::numeric) CONSTRAINT,public,academy_camp_weekly_payments,academy_camp_weekly_payments_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,academy_camp_weekly_payments,academy_camp_weekly_payments_participant_id_fkey,f,FOREIGN KEY (participant_id) REFERENCES academy_program_participants(id) ON DELETE CASCADE CONSTRAINT,public,academy_camp_weekly_payments,academy_camp_weekly_payments_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,academy_camp_weekly_payments,academy_camp_weekly_payments_unique,u,UNIQUE (participant_id) CONSTRAINT,public,academy_camp_weekly_payments,academy_camp_weekly_payments_updated_by_user_profile_id_fkey,f,FOREIGN KEY (updated_by_user_profile_id) REFERENCES user_profiles(id) INDEX,public,academy_camp_weekly_payments,academy_camp_weekly_payments_pkey,constraint_backed,CREATE UNIQUE INDEX academy_camp_weekly_payments_pkey ON public.academy_camp_weekly_payments USING btree (id) INDEX,public,academy_camp_weekly_payments,academy_camp_weekly_payments_unique,constraint_backed,CREATE UNIQUE INDEX academy_camp_weekly_payments_unique ON public.academy_camp_weekly_payments USING btree (participant_id) INDEX,public,academy_camp_weekly_payments,idx_academy_camp_weekly_payments_club_id,standalone,CREATE INDEX idx_academy_camp_weekly_payments_club_id ON public.academy_camp_weekly_payments USING btree (club_id) INDEX,public,academy_camp_weekly_payments,idx_academy_camp_weekly_payments_participant_id,standalone,CREATE INDEX idx_academy_camp_weekly_payments_participant_id ON public.academy_camp_weekly_payments USING btree (participant_id) CONSTRAINT,public,academy_coach_checkin_audit_logs,academy_coach_checkin_audit_action_check,c,"CHECK (action = ANY (ARRAY['check_in'::text, 'check_out'::text]))" CONSTRAINT,public,academy_coach_checkin_audit_logs,academy_coach_checkin_audit_logs_action_by_user_profile_id_fkey,f,FOREIGN KEY (action_by_user_profile_id) REFERENCES user_profiles(id) ON DELETE SET NULL CONSTRAINT,public,academy_coach_checkin_audit_logs,academy_coach_checkin_audit_logs_booking_id_fkey,f,FOREIGN KEY (booking_id) REFERENCES bookings(id) ON DELETE SET NULL CONSTRAINT,public,academy_coach_checkin_audit_logs,academy_coach_checkin_audit_logs_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,academy_coach_checkin_audit_logs,academy_coach_checkin_audit_logs_coach_profile_id_fkey,f,FOREIGN KEY (coach_profile_id) REFERENCES coach_profiles(id) ON DELETE SET NULL CONSTRAINT,public,academy_coach_checkin_audit_logs,academy_coach_checkin_audit_logs_pkey,p,PRIMARY KEY (id) INDEX,public,academy_coach_checkin_audit_logs,academy_coach_checkin_audit_logs_pkey,constraint_backed,CREATE UNIQUE INDEX academy_coach_checkin_audit_logs_pkey ON public.academy_coach_checkin_audit_logs USING btree (id) CONSTRAINT,public,academy_coach_compensation_audit_logs,academy_coach_compensation_audi_changed_by_user_profile_id_fkey,f,FOREIGN KEY (changed_by_user_profile_id) REFERENCES user_profiles(id) ON DELETE SET NULL CONSTRAINT,public,academy_coach_compensation_audit_logs,academy_coach_compensation_audit_action_check,c,"CHECK (action_type = ANY (ARRAY['assignment_created'::text, 'assignment_deactivated'::text, 'assignment_voided'::text, 'rate_created'::text, 'rate_updated'::text, 'rate_closed'::text, 'rate_voided'::text]))" CONSTRAINT,public,academy_coach_compensation_audit_logs,academy_coach_compensation_audit_logs_academy_program_id_fkey,f,FOREIGN KEY (academy_program_id) REFERENCES academy_programs(id) ON DELETE SET NULL CONSTRAINT,public,academy_coach_compensation_audit_logs,academy_coach_compensation_audit_logs_assignment_id_fkey,f,FOREIGN KEY (assignment_id) REFERENCES academy_program_coach_assignments(id) ON DELETE SET NULL CONSTRAINT,public,academy_coach_compensation_audit_logs,academy_coach_compensation_audit_logs_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,academy_coach_compensation_audit_logs,academy_coach_compensation_audit_logs_coach_profile_id_fkey,f,FOREIGN KEY (coach_profile_id) REFERENCES coach_profiles(id) ON DELETE SET NULL CONSTRAINT,public,academy_coach_compensation_audit_logs,academy_coach_compensation_audit_logs_compensation_rate_id_fkey,f,FOREIGN KEY (compensation_rate_id) REFERENCES academy_coach_compensation_rates(id) ON DELETE SET NULL CONSTRAINT,public,academy_coach_compensation_audit_logs,academy_coach_compensation_audit_logs_pkey,p,PRIMARY KEY (id) INDEX,public,academy_coach_compensation_audit_logs,academy_coach_compensation_audit_logs_pkey,constraint_backed,CREATE UNIQUE INDEX academy_coach_compensation_audit_logs_pkey ON public.academy_coach_compensation_audit_logs USING btree (id) INDEX,public,academy_coach_compensation_audit_logs,idx_academy_coach_compensation_audit_club,standalone,"CREATE INDEX idx_academy_coach_compensation_audit_club ON public.academy_coach_compensation_audit_logs USING btree (club_id, changed_at DESC)" INDEX,public,academy_coach_compensation_audit_logs,idx_academy_coach_compensation_audit_coach,standalone,"CREATE INDEX idx_academy_coach_compensation_audit_coach ON public.academy_coach_compensation_audit_logs USING btree (coach_profile_id, changed_at DESC)" INDEX,public,academy_coach_compensation_audit_logs,idx_academy_coach_compensation_audit_program,standalone,"CREATE INDEX idx_academy_coach_compensation_audit_program ON public.academy_coach_compensation_audit_logs USING btree (academy_program_id, changed_at DESC)" CONSTRAINT,public,academy_coach_compensation_rates,academy_coach_compensation_rate_created_by_user_profile_id_fkey,f,FOREIGN KEY (created_by_user_profile_id) REFERENCES user_profiles(id) ON DELETE RESTRICT CONSTRAINT,public,academy_coach_compensation_rates,academy_coach_compensation_rate_updated_by_user_profile_id_fkey,f,FOREIGN KEY (updated_by_user_profile_id) REFERENCES user_profiles(id) ON DELETE RESTRICT CONSTRAINT,public,academy_coach_compensation_rates,academy_coach_compensation_rates_amount_check,c,CHECK (amount_huf >= 0::numeric) CONSTRAINT,public,academy_coach_compensation_rates,academy_coach_compensation_rates_assignment_id_fkey,f,FOREIGN KEY (assignment_id) REFERENCES academy_program_coach_assignments(id) ON DELETE CASCADE CONSTRAINT,public,academy_coach_compensation_rates,academy_coach_compensation_rates_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,academy_coach_compensation_rates,academy_coach_compensation_rates_dates_check,c,CHECK (effective_to IS NULL OR effective_to >= effective_from) CONSTRAINT,public,academy_coach_compensation_rates,academy_coach_compensation_rates_no_overlap,x,"EXCLUDE USING gist (assignment_id WITH =, daterange(effective_from, COALESCE(effective_to, 'infinity'::date), '[]'::text) WITH &&) WHERE (is_void = false)" CONSTRAINT,public,academy_coach_compensation_rates,academy_coach_compensation_rates_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,academy_coach_compensation_rates,academy_coach_compensation_rates_type_check,c,CHECK (compensation_type = 'per_session'::text) INDEX,public,academy_coach_compensation_rates,academy_coach_compensation_rates_no_overlap,constraint_backed,"CREATE INDEX academy_coach_compensation_rates_no_overlap ON public.academy_coach_compensation_rates USING gist (assignment_id, daterange(effective_from, COALESCE(effective_to, 'infinity'::date), '[]'::text)) WHERE (is_void = false)" INDEX,public,academy_coach_compensation_rates,academy_coach_compensation_rates_pkey,constraint_backed,CREATE UNIQUE INDEX academy_coach_compensation_rates_pkey ON public.academy_coach_compensation_rates USING btree (id) INDEX,public,academy_coach_compensation_rates,academy_coach_compensation_rates_pkey,constraint_backed,CREATE UNIQUE INDEX academy_coach_compensation_rates_pkey ON public.academy_coach_compensation_rates USING btree (id) INDEX,public,academy_coach_compensation_rates,idx_academy_coach_compensation_rates_assignment,standalone,CREATE INDEX idx_academy_coach_compensation_rates_assignment ON public.academy_coach_compensation_rates USING btree (assignment_id) INDEX,public,academy_coach_compensation_rates,idx_academy_coach_compensation_rates_club,standalone,CREATE INDEX idx_academy_coach_compensation_rates_club ON public.academy_coach_compensation_rates USING btree (club_id) INDEX,public,academy_coach_compensation_rates,idx_academy_coach_compensation_rates_lookup,standalone,"CREATE INDEX idx_academy_coach_compensation_rates_lookup ON public.academy_coach_compensation_rates USING btree (assignment_id, effective_from, effective_to) WHERE (is_void = false)" TRIGGER,public,academy_coach_compensation_rates,trg_academy_coach_compensation_rates_updated_at,enabled,CREATE TRIGGER trg_academy_coach_compensation_rates_updated_at BEFORE UPDATE ON academy_coach_compensation_rates FOR EACH ROW EXECUTE FUNCTION set_academy_coach_compensation_updated_at() TRIGGER,public,academy_coach_compensation_rates,trg_validate_academy_coach_compensation_rate,enabled,CREATE TRIGGER trg_validate_academy_coach_compensation_rate BEFORE INSERT OR UPDATE ON academy_coach_compensation_rates FOR EACH ROW EXECUTE FUNCTION validate_academy_coach_compensation_rate() CONSTRAINT,public,academy_group_court_cost_settings,academy_group_court_cost_settin_created_by_user_profile_id_fkey,f,FOREIGN KEY (created_by_user_profile_id) REFERENCES user_profiles(id) ON DELETE RESTRICT CONSTRAINT,public,academy_group_court_cost_settings,academy_group_court_cost_settin_updated_by_user_profile_id_fkey,f,FOREIGN KEY (updated_by_user_profile_id) REFERENCES user_profiles(id) ON DELETE RESTRICT CONSTRAINT,public,academy_group_court_cost_settings,academy_group_court_cost_settings_academy_program_id_fkey,f,FOREIGN KEY (academy_program_id) REFERENCES academy_programs(id) ON DELETE CASCADE CONSTRAINT,public,academy_group_court_cost_settings,academy_group_court_cost_settings_amount_check,c,CHECK (default_court_fee_huf >= 0::numeric) CONSTRAINT,public,academy_group_court_cost_settings,academy_group_court_cost_settings_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,academy_group_court_cost_settings,academy_group_court_cost_settings_pkey,p,PRIMARY KEY (academy_program_id) INDEX,public,academy_group_court_cost_settings,academy_group_court_cost_settings_pkey,constraint_backed,CREATE UNIQUE INDEX academy_group_court_cost_settings_pkey ON public.academy_group_court_cost_settings USING btree (academy_program_id) INDEX,public,academy_group_court_cost_settings,idx_academy_group_court_cost_settings_club,standalone,CREATE INDEX idx_academy_group_court_cost_settings_club ON public.academy_group_court_cost_settings USING btree (club_id) TRIGGER,public,academy_group_court_cost_settings,trg_academy_group_court_cost_updated_at,enabled,CREATE TRIGGER trg_academy_group_court_cost_updated_at BEFORE UPDATE ON academy_group_court_cost_settings FOR EACH ROW EXECUTE FUNCTION set_academy_group_court_cost_updated_at() TRIGGER,public,academy_group_court_cost_settings,trg_validate_academy_group_court_cost_setting,enabled,CREATE TRIGGER trg_validate_academy_group_court_cost_setting BEFORE INSERT OR UPDATE ON academy_group_court_cost_settings FOR EACH ROW EXECUTE FUNCTION validate_academy_group_court_cost_setting() CONSTRAINT,public,academy_group_monthly_payments,academy_group_monthly_consistency_check,c,CHECK (is_paid = false OR is_paid = true AND paid_amount_huf IS NOT NULL AND paid_on IS NOT NULL) CONSTRAINT,public,academy_group_monthly_payments,academy_group_monthly_expected_fee_check,c,CHECK (expected_fee_huf IS NULL OR expected_fee_huf >= 0::numeric) CONSTRAINT,public,academy_group_monthly_payments,academy_group_monthly_month_check,c,CHECK (month >= 1 AND month <= 12) CONSTRAINT,public,academy_group_monthly_payments,academy_group_monthly_paid_amount_check,c,CHECK (paid_amount_huf IS NULL OR paid_amount_huf >= 0::numeric) CONSTRAINT,public,academy_group_monthly_payments,academy_group_monthly_payments_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,academy_group_monthly_payments,academy_group_monthly_payments_participant_id_fkey,f,FOREIGN KEY (participant_id) REFERENCES academy_program_participants(id) ON DELETE CASCADE CONSTRAINT,public,academy_group_monthly_payments,academy_group_monthly_payments_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,academy_group_monthly_payments,academy_group_monthly_payments_unique,u,"UNIQUE (participant_id, year, month)" CONSTRAINT,public,academy_group_monthly_payments,academy_group_monthly_payments_updated_by_user_profile_id_fkey,f,FOREIGN KEY (updated_by_user_profile_id) REFERENCES user_profiles(id) CONSTRAINT,public,academy_group_monthly_payments,academy_group_monthly_year_check,c,CHECK (year >= 2000 AND year <= 2100) INDEX,public,academy_group_monthly_payments,academy_group_monthly_payments_pkey,constraint_backed,CREATE UNIQUE INDEX academy_group_monthly_payments_pkey ON public.academy_group_monthly_payments USING btree (id) INDEX,public,academy_group_monthly_payments,academy_group_monthly_payments_unique,constraint_backed,"CREATE UNIQUE INDEX academy_group_monthly_payments_unique ON public.academy_group_monthly_payments USING btree (participant_id, year, month)" INDEX,public,academy_group_monthly_payments,idx_academy_group_monthly_payments_club_id,standalone,CREATE INDEX idx_academy_group_monthly_payments_club_id ON public.academy_group_monthly_payments USING btree (club_id) INDEX,public,academy_group_monthly_payments,idx_academy_group_monthly_payments_participant_id,standalone,CREATE INDEX idx_academy_group_monthly_payments_participant_id ON public.academy_group_monthly_payments USING btree (participant_id) INDEX,public,academy_group_monthly_payments,idx_academy_group_monthly_payments_ym,standalone,"CREATE INDEX idx_academy_group_monthly_payments_ym ON public.academy_group_monthly_payments USING btree (year, month)" CONSTRAINT,public,academy_payment_audit_logs,academy_payment_audit_logs_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,academy_payment_audit_logs,academy_payment_audit_logs_participant_id_fkey,f,FOREIGN KEY (participant_id) REFERENCES academy_program_participants(id) ON DELETE SET NULL CONSTRAINT,public,academy_payment_audit_logs,academy_payment_audit_logs_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,academy_payment_audit_logs,academy_payment_audit_logs_updated_by_user_profile_id_fkey,f,FOREIGN KEY (updated_by_user_profile_id) REFERENCES user_profiles(id) ON DELETE SET NULL INDEX,public,academy_payment_audit_logs,academy_payment_audit_logs_pkey,constraint_backed,CREATE UNIQUE INDEX academy_payment_audit_logs_pkey ON public.academy_payment_audit_logs USING btree (id) INDEX,public,academy_payment_audit_logs,idx_academy_payment_audit_logs_club_id,standalone,CREATE INDEX idx_academy_payment_audit_logs_club_id ON public.academy_payment_audit_logs USING btree (club_id) INDEX,public,academy_payment_audit_logs,idx_academy_payment_audit_logs_participant_id,standalone,CREATE INDEX idx_academy_payment_audit_logs_participant_id ON public.academy_payment_audit_logs USING btree (participant_id) CONSTRAINT,public,academy_program_coach_assignments,academy_program_coach_assignmen_created_by_user_profile_id_fkey,f,FOREIGN KEY (created_by_user_profile_id) REFERENCES user_profiles(id) ON DELETE RESTRICT CONSTRAINT,public,academy_program_coach_assignments,academy_program_coach_assignmen_updated_by_user_profile_id_fkey,f,FOREIGN KEY (updated_by_user_profile_id) REFERENCES user_profiles(id) ON DELETE RESTRICT CONSTRAINT,public,academy_program_coach_assignments,academy_program_coach_assignments_academy_program_id_fkey,f,FOREIGN KEY (academy_program_id) REFERENCES academy_programs(id) ON DELETE CASCADE CONSTRAINT,public,academy_program_coach_assignments,academy_program_coach_assignments_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,academy_program_coach_assignments,academy_program_coach_assignments_coach_profile_id_fkey,f,FOREIGN KEY (coach_profile_id) REFERENCES coach_profiles(id) ON DELETE CASCADE CONSTRAINT,public,academy_program_coach_assignments,academy_program_coach_assignments_dates_check,c,CHECK (assigned_to IS NULL OR assigned_to >= assigned_from) CONSTRAINT,public,academy_program_coach_assignments,academy_program_coach_assignments_no_overlap,x,"EXCLUDE USING gist (academy_program_id WITH =, coach_profile_id WITH =, daterange(assigned_from, COALESCE(assigned_to, 'infinity'::date), '[]'::text) WITH &&) WHERE (is_void = false)" CONSTRAINT,public,academy_program_coach_assignments,academy_program_coach_assignments_pkey,p,PRIMARY KEY (id) INDEX,public,academy_program_coach_assignments,academy_program_coach_assignments_no_overlap,constraint_backed,"CREATE INDEX academy_program_coach_assignments_no_overlap ON public.academy_program_coach_assignments USING gist (academy_program_id, coach_profile_id, daterange(assigned_from, COALESCE(assigned_to, 'infinity'::date), '[]'::text)) WHERE (is_void = false)" INDEX,public,academy_program_coach_assignments,academy_program_coach_assignments_one_open_period,standalone,"CREATE UNIQUE INDEX academy_program_coach_assignments_one_open_period ON public.academy_program_coach_assignments USING btree (academy_program_id, coach_profile_id) WHERE ((assigned_to IS NULL) AND (is_void = false))" INDEX,public,academy_program_coach_assignments,academy_program_coach_assignments_pkey,constraint_backed,CREATE UNIQUE INDEX academy_program_coach_assignments_pkey ON public.academy_program_coach_assignments USING btree (id) INDEX,public,academy_program_coach_assignments,academy_program_coach_assignments_pkey,constraint_backed,CREATE UNIQUE INDEX academy_program_coach_assignments_pkey ON public.academy_program_coach_assignments USING btree (id) INDEX,public,academy_program_coach_assignments,academy_program_coach_assignments_pkey,constraint_backed,CREATE UNIQUE INDEX academy_program_coach_assignments_pkey ON public.academy_program_coach_assignments USING btree (id) INDEX,public,academy_program_coach_assignments,idx_academy_program_coach_assignments_club,standalone,CREATE INDEX idx_academy_program_coach_assignments_club ON public.academy_program_coach_assignments USING btree (club_id) INDEX,public,academy_program_coach_assignments,idx_academy_program_coach_assignments_coach,standalone,CREATE INDEX idx_academy_program_coach_assignments_coach ON public.academy_program_coach_assignments USING btree (coach_profile_id) INDEX,public,academy_program_coach_assignments,idx_academy_program_coach_assignments_lookup,standalone,"CREATE INDEX idx_academy_program_coach_assignments_lookup ON public.academy_program_coach_assignments USING btree (academy_program_id, coach_profile_id, assigned_from, assigned_to) WHERE (is_void = false)" INDEX,public,academy_program_coach_assignments,idx_academy_program_coach_assignments_program,standalone,CREATE INDEX idx_academy_program_coach_assignments_program ON public.academy_program_coach_assignments USING btree (academy_program_id) TRIGGER,public,academy_program_coach_assignments,trg_academy_program_coach_assignments_updated_at,enabled,CREATE TRIGGER trg_academy_program_coach_assignments_updated_at BEFORE UPDATE ON academy_program_coach_assignments FOR EACH ROW EXECUTE FUNCTION set_academy_coach_compensation_updated_at() TRIGGER,public,academy_program_coach_assignments,trg_validate_academy_program_coach_assignment,enabled,CREATE TRIGGER trg_validate_academy_program_coach_assignment BEFORE INSERT OR UPDATE ON academy_program_coach_assignments FOR EACH ROW EXECUTE FUNCTION validate_academy_program_coach_assignment() CONSTRAINT,public,academy_program_participants,academy_participant_birth_year_min,c,CHECK (birth_year >= 1900) CONSTRAINT,public,academy_program_participants,academy_participant_camp_fee_nonnegative,c,CHECK (camp_fee_huf IS NULL OR camp_fee_huf >= 0::numeric) CONSTRAINT,public,academy_program_participants,academy_participant_ctr_nonnegative,c,CHECK (ctr IS NULL OR ctr >= 0::numeric) CONSTRAINT,public,academy_program_participants,academy_participant_first_name_not_blank,c,CHECK (btrim(first_name) <> ''::text) CONSTRAINT,public,academy_program_participants,academy_participant_last_name_not_blank,c,CHECK (btrim(last_name) <> ''::text) CONSTRAINT,public,academy_program_participants,academy_participant_monthly_fee_nonnegative,c,CHECK (monthly_fee_huf IS NULL OR monthly_fee_huf >= 0::numeric) CONSTRAINT,public,academy_program_participants,academy_participant_program_club_fkey,f,"FOREIGN KEY (academy_program_id, club_id) REFERENCES academy_programs(id, club_id) ON DELETE RESTRICT" CONSTRAINT,public,academy_program_participants,academy_program_participants_camp_fee_type_check,c,"CHECK (camp_fee_type = ANY (ARRAY['daily'::text, 'weekly'::text]))" CONSTRAINT,public,academy_program_participants,academy_program_participants_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE RESTRICT CONSTRAINT,public,academy_program_participants,academy_program_participants_created_by_user_profile_id_fkey,f,FOREIGN KEY (created_by_user_profile_id) REFERENCES user_profiles(id) ON DELETE RESTRICT CONSTRAINT,public,academy_program_participants,academy_program_participants_pkey,p,PRIMARY KEY (id) INDEX,public,academy_program_participants,academy_program_participants_pkey,constraint_backed,CREATE UNIQUE INDEX academy_program_participants_pkey ON public.academy_program_participants USING btree (id) INDEX,public,academy_program_participants,academy_program_participants_pkey,constraint_backed,CREATE UNIQUE INDEX academy_program_participants_pkey ON public.academy_program_participants USING btree (id) INDEX,public,academy_program_participants,academy_program_participants_pkey,constraint_backed,CREATE UNIQUE INDEX academy_program_participants_pkey ON public.academy_program_participants USING btree (id) INDEX,public,academy_program_participants,academy_program_participants_pkey,constraint_backed,CREATE UNIQUE INDEX academy_program_participants_pkey ON public.academy_program_participants USING btree (id) INDEX,public,academy_program_participants,academy_program_participants_pkey,constraint_backed,CREATE UNIQUE INDEX academy_program_participants_pkey ON public.academy_program_participants USING btree (id) INDEX,public,academy_program_participants,academy_program_participants_pkey,constraint_backed,CREATE UNIQUE INDEX academy_program_participants_pkey ON public.academy_program_participants USING btree (id) INDEX,public,academy_program_participants,academy_program_participants_pkey,constraint_backed,CREATE UNIQUE INDEX academy_program_participants_pkey ON public.academy_program_participants USING btree (id) INDEX,public,academy_program_participants,idx_academy_participants_active,standalone,"CREATE INDEX idx_academy_participants_active ON public.academy_program_participants USING btree (academy_program_id, is_active)" INDEX,public,academy_program_participants,idx_academy_participants_club_id,standalone,CREATE INDEX idx_academy_participants_club_id ON public.academy_program_participants USING btree (club_id) INDEX,public,academy_program_participants,idx_academy_participants_program_id,standalone,CREATE INDEX idx_academy_participants_program_id ON public.academy_program_participants USING btree (academy_program_id) INDEX,public,academy_program_participants,uq_academy_active_participant,standalone,"CREATE UNIQUE INDEX uq_academy_active_participant ON public.academy_program_participants USING btree (academy_program_id, lower(btrim(last_name)), lower(btrim(first_name)), birth_year) WHERE (is_active = true)" TRIGGER,public,academy_program_participants,trg_validate_academy_participant,enabled,CREATE TRIGGER trg_validate_academy_participant BEFORE INSERT OR UPDATE ON academy_program_participants FOR EACH ROW EXECUTE FUNCTION validate_academy_participant_row() CONSTRAINT,public,academy_programs,academy_programs_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE RESTRICT CONSTRAINT,public,academy_programs,academy_programs_created_by_user_profile_id_fkey,f,FOREIGN KEY (created_by_user_profile_id) REFERENCES user_profiles(id) ON DELETE RESTRICT CONSTRAINT,public,academy_programs,academy_programs_id_club_key,u,"UNIQUE (id, club_id)" CONSTRAINT,public,academy_programs,academy_programs_name_not_blank,c,CHECK (btrim(name) <> ''::text) CONSTRAINT,public,academy_programs,academy_programs_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,academy_programs,academy_programs_program_type_check,c,"CHECK (program_type = ANY (ARRAY['group'::text, 'camp'::text]))" CONSTRAINT,public,academy_programs,academy_programs_sport_check,c,"CHECK (sport = ANY (ARRAY['tennis'::text, 'padel'::text]))" INDEX,public,academy_programs,academy_programs_id_club_key,constraint_backed,"CREATE UNIQUE INDEX academy_programs_id_club_key ON public.academy_programs USING btree (id, club_id)" INDEX,public,academy_programs,academy_programs_id_club_key,constraint_backed,"CREATE UNIQUE INDEX academy_programs_id_club_key ON public.academy_programs USING btree (id, club_id)" INDEX,public,academy_programs,academy_programs_id_club_key,constraint_backed,"CREATE UNIQUE INDEX academy_programs_id_club_key ON public.academy_programs USING btree (id, club_id)" INDEX,public,academy_programs,academy_programs_id_club_key,constraint_backed,"CREATE UNIQUE INDEX academy_programs_id_club_key ON public.academy_programs USING btree (id, club_id)" INDEX,public,academy_programs,academy_programs_pkey,constraint_backed,CREATE UNIQUE INDEX academy_programs_pkey ON public.academy_programs USING btree (id) INDEX,public,academy_programs,academy_programs_pkey,constraint_backed,CREATE UNIQUE INDEX academy_programs_pkey ON public.academy_programs USING btree (id) INDEX,public,academy_programs,academy_programs_pkey,constraint_backed,CREATE UNIQUE INDEX academy_programs_pkey ON public.academy_programs USING btree (id) INDEX,public,academy_programs,academy_programs_pkey,constraint_backed,CREATE UNIQUE INDEX academy_programs_pkey ON public.academy_programs USING btree (id) INDEX,public,academy_programs,idx_academy_programs_active,standalone,"CREATE INDEX idx_academy_programs_active ON public.academy_programs USING btree (club_id, is_active)" INDEX,public,academy_programs,idx_academy_programs_club_id,standalone,CREATE INDEX idx_academy_programs_club_id ON public.academy_programs USING btree (club_id) INDEX,public,academy_programs,idx_academy_programs_club_sport,standalone,"CREATE INDEX idx_academy_programs_club_sport ON public.academy_programs USING btree (club_id, sport)" INDEX,public,academy_programs,idx_academy_programs_type,standalone,"CREATE INDEX idx_academy_programs_type ON public.academy_programs USING btree (club_id, program_type)" INDEX,public,academy_programs,uq_academy_programs_active_name,standalone,"CREATE UNIQUE INDEX uq_academy_programs_active_name ON public.academy_programs USING btree (club_id, program_type, lower(btrim(name))) WHERE (is_active = true)" TRIGGER,public,academy_programs,trg_ensure_academy_group_court_cost_setting,enabled,CREATE TRIGGER trg_ensure_academy_group_court_cost_setting AFTER INSERT ON academy_programs FOR EACH ROW EXECUTE FUNCTION ensure_academy_group_court_cost_setting() TRIGGER,public,academy_programs,trg_prepare_academy_program,enabled,CREATE TRIGGER trg_prepare_academy_program BEFORE INSERT OR UPDATE ON academy_programs FOR EACH ROW EXECUTE FUNCTION prepare_academy_program_row() CONSTRAINT,public,audit_log,audit_log_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,audit_log,audit_log_pkey,p,PRIMARY KEY (id) INDEX,public,audit_log,audit_log_club_created_at_idx,standalone,"CREATE INDEX audit_log_club_created_at_idx ON public.audit_log USING btree (club_id, created_at DESC)" INDEX,public,audit_log,audit_log_entity_lookup_idx,standalone,"CREATE INDEX audit_log_entity_lookup_idx ON public.audit_log USING btree (club_id, entity_type, entity_id)" INDEX,public,audit_log,audit_log_pkey,constraint_backed,CREATE UNIQUE INDEX audit_log_pkey ON public.audit_log USING btree (id) TRIGGER,public,audit_log,enforce_audit_log_identity_trigger,enabled,CREATE TRIGGER enforce_audit_log_identity_trigger BEFORE INSERT ON audit_log FOR EACH ROW EXECUTE FUNCTION enforce_audit_log_identity() CONSTRAINT,public,booking_payments,booking_payments_amount_check,c,CHECK (amount >= 0::numeric AND amount <> 'NaN'::numeric AND amount <> 'Infinity'::numeric AND amount <> '-Infinity'::numeric) CONSTRAINT,public,booking_payments,booking_payments_booking_id_fkey,f,FOREIGN KEY (booking_id) REFERENCES bookings(id) ON DELETE SET NULL CONSTRAINT,public,booking_payments,booking_payments_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE RESTRICT CONSTRAINT,public,booking_payments,booking_payments_credit_type_check,c,"CHECK (credit_type = ANY (ARRAY['standard'::text, 'season_pass'::text]))" CONSTRAINT,public,booking_payments,booking_payments_currency_check,c,"CHECK (currency = ANY (ARRAY['HUF'::text, 'EUR'::text, 'USD'::text]))" CONSTRAINT,public,booking_payments,booking_payments_payment_method_check,c,"CHECK (payment_method = ANY (ARRAY['venue'::text, 'card'::text, 'credit_standard'::text, 'credit_season_pass'::text, 'supported'::text]))" CONSTRAINT,public,booking_payments,booking_payments_payment_status_check,c,"CHECK (payment_status = ANY (ARRAY['pending'::text, 'paid'::text, 'refunded'::text, 'failed'::text, 'cancelled'::text]))" CONSTRAINT,public,booking_payments,booking_payments_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,booking_payments,booking_payments_season_id_fkey,f,FOREIGN KEY (season_id) REFERENCES seasons(id) ON DELETE RESTRICT CONSTRAINT,public,booking_payments,booking_payments_user_profile_id_fkey,f,FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE RESTRICT CONSTRAINT,public,booking_payments,booking_payments_wallet_id_fkey,f,FOREIGN KEY (wallet_id) REFERENCES credit_wallets(id) ON DELETE RESTRICT INDEX,public,booking_payments,booking_payments_one_per_booking,standalone,CREATE UNIQUE INDEX booking_payments_one_per_booking ON public.booking_payments USING btree (booking_id) WHERE (booking_id IS NOT NULL) INDEX,public,booking_payments,booking_payments_pkey,constraint_backed,CREATE UNIQUE INDEX booking_payments_pkey ON public.booking_payments USING btree (id) INDEX,public,booking_payments,booking_payments_unique_provider_ref,standalone,CREATE UNIQUE INDEX booking_payments_unique_provider_ref ON public.booking_payments USING btree (provider_reference) WHERE (provider_reference IS NOT NULL) INDEX,public,booking_payments,idx_booking_payments_booking,standalone,CREATE INDEX idx_booking_payments_booking ON public.booking_payments USING btree (booking_id) CONSTRAINT,public,booking_requests,booking_requests_caller_user_profile_id_fkey,f,FOREIGN KEY (caller_user_profile_id) REFERENCES user_profiles(id) ON DELETE RESTRICT CONSTRAINT,public,booking_requests,booking_requests_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE RESTRICT CONSTRAINT,public,booking_requests,booking_requests_pkey,p,PRIMARY KEY (request_id) CONSTRAINT,public,booking_requests,booking_requests_request_status_check,c,"CHECK (request_status = ANY (ARRAY['processing'::text, 'completed'::text]))" CONSTRAINT,public,booking_requests,booking_requests_season_id_fkey,f,FOREIGN KEY (season_id) REFERENCES seasons(id) ON DELETE RESTRICT CONSTRAINT,public,booking_requests,booking_requests_target_user_profile_id_fkey,f,FOREIGN KEY (target_user_profile_id) REFERENCES user_profiles(id) ON DELETE RESTRICT INDEX,public,booking_requests,booking_requests_pkey,constraint_backed,CREATE UNIQUE INDEX booking_requests_pkey ON public.booking_requests USING btree (request_id) CONSTRAINT,public,bookings,bookings_cancellation_reason_category_check,c,"CHECK (cancellation_reason_category IS NULL OR (cancellation_reason_category = ANY (ARRAY['illness'::text, 'schedule_conflict'::text, 'coach_unavailable'::text, 'player_request'::text, 'weather'::text, 'court_unavailable'::text, 'club_decision'::text, 'other'::text])))" CONSTRAINT,public,bookings,bookings_cancelled_by_profile_id_fkey,f,FOREIGN KEY (cancelled_by_profile_id) REFERENCES user_profiles(id) ON DELETE SET NULL CONSTRAINT,public,bookings,bookings_checked_in_coach_profile_id_fkey,f,FOREIGN KEY (checked_in_coach_profile_id) REFERENCES coach_profiles(id) ON DELETE SET NULL NOT VALID CONSTRAINT,public,bookings,bookings_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,bookings,bookings_court_id_fkey,f,FOREIGN KEY (court_id) REFERENCES courts(id) ON DELETE SET NULL CONSTRAINT,public,bookings,bookings_created_by_user_id_fkey,f,FOREIGN KEY (created_by_user_id) REFERENCES user_profiles(id) ON DELETE SET NULL CONSTRAINT,public,bookings,bookings_overlap_exclude,x,"EXCLUDE USING gist (court_id WITH =, tsrange(booking_date + start_time, booking_date + end_time, '[)'::text) WITH &&) WHERE (deleted_at IS NULL AND (COALESCE(status, 'confirmed'::text) <> ALL (ARRAY['cancelled'::text, 'deleted by club'::text, 'failed'::text])))" CONSTRAINT,public,bookings,bookings_payment_method_check,c,"CHECK (payment_method = ANY (ARRAY['venue'::text, 'card'::text, 'credit_standard'::text, 'credit_season_pass'::text, 'supported'::text]))" CONSTRAINT,public,bookings,bookings_payment_status_check,c,"CHECK (payment_status = ANY (ARRAY['pending'::text, 'paid'::text, 'refunded'::text, 'failed'::text, 'cancelled'::text]))" CONSTRAINT,public,bookings,bookings_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,bookings,bookings_season_id_fkey,f,FOREIGN KEY (season_id) REFERENCES seasons(id) ON DELETE CASCADE CONSTRAINT,public,bookings,bookings_sport_check,c,"CHECK (sport = ANY (ARRAY['tennis'::text, 'padel'::text]))" CONSTRAINT,public,bookings,bookings_user_id_fkey,f,FOREIGN KEY (user_id) REFERENCES user_profiles(id) ON DELETE SET NULL CONSTRAINT,public,bookings,fk_bookings_recurring_series,f,FOREIGN KEY (recurring_series_id) REFERENCES recurring_booking_series(id) ON DELETE RESTRICT INDEX,public,bookings,bookings_calendar_lookup_idx,standalone,"CREATE INDEX bookings_calendar_lookup_idx ON public.bookings USING btree (club_id, booking_date, court_id, start_time) WHERE (deleted_at IS NULL)" INDEX,public,bookings,bookings_created_by_user_id_idx,standalone,CREATE INDEX bookings_created_by_user_id_idx ON public.bookings USING btree (created_by_user_id) WHERE (deleted_at IS NULL) INDEX,public,bookings,bookings_overlap_exclude,constraint_backed,"CREATE INDEX bookings_overlap_exclude ON public.bookings USING gist (court_id, tsrange((booking_date + start_time), (booking_date + end_time), '[)'::text)) WHERE ((deleted_at IS NULL) AND (COALESCE(status, 'confirmed'::text) <> ALL (ARRAY['cancelled'::text, 'deleted by club'::text, 'failed'::text])))" INDEX,public,bookings,bookings_pkey,constraint_backed,CREATE UNIQUE INDEX bookings_pkey ON public.bookings USING btree (id) INDEX,public,bookings,bookings_pkey,constraint_backed,CREATE UNIQUE INDEX bookings_pkey ON public.bookings USING btree (id) INDEX,public,bookings,bookings_pkey,constraint_backed,CREATE UNIQUE INDEX bookings_pkey ON public.bookings USING btree (id) INDEX,public,bookings,bookings_pkey,constraint_backed,CREATE UNIQUE INDEX bookings_pkey ON public.bookings USING btree (id) INDEX,public,bookings,bookings_pkey,constraint_backed,CREATE UNIQUE INDEX bookings_pkey ON public.bookings USING btree (id) INDEX,public,bookings,bookings_pkey,constraint_backed,CREATE UNIQUE INDEX bookings_pkey ON public.bookings USING btree (id) INDEX,public,bookings,bookings_pkey,constraint_backed,CREATE UNIQUE INDEX bookings_pkey ON public.bookings USING btree (id) INDEX,public,bookings,bookings_pkey,constraint_backed,CREATE UNIQUE INDEX bookings_pkey ON public.bookings USING btree (id) INDEX,public,bookings,bookings_pkey,constraint_backed,CREATE UNIQUE INDEX bookings_pkey ON public.bookings USING btree (id) INDEX,public,bookings,bookings_user_id_idx,standalone,CREATE INDEX bookings_user_id_idx ON public.bookings USING btree (user_id) WHERE (deleted_at IS NULL) INDEX,public,bookings,idx_bookings_cancelled_by_profile_id,standalone,CREATE INDEX idx_bookings_cancelled_by_profile_id ON public.bookings USING btree (cancelled_by_profile_id) WHERE (cancelled_by_profile_id IS NOT NULL) INDEX,public,bookings,idx_bookings_checked_in_coach_profile_id,standalone,CREATE INDEX idx_bookings_checked_in_coach_profile_id ON public.bookings USING btree (checked_in_coach_profile_id) WHERE (checked_in_coach_profile_id IS NOT NULL) INDEX,public,bookings,idx_bookings_club_sport_season_date,standalone,"CREATE INDEX idx_bookings_club_sport_season_date ON public.bookings USING btree (club_id, sport, season_id, booking_date)" INDEX,public,bookings,idx_bookings_recurring_series_id,standalone,CREATE INDEX idx_bookings_recurring_series_id ON public.bookings USING btree (recurring_series_id) WHERE (recurring_series_id IS NOT NULL) TRIGGER,public,bookings,enforce_booking_write_security_trigger,enabled,CREATE TRIGGER enforce_booking_write_security_trigger BEFORE INSERT OR DELETE OR UPDATE ON bookings FOR EACH ROW EXECUTE FUNCTION enforce_booking_write_security() TRIGGER,public,bookings,set_bookings_updated_at,enabled,CREATE TRIGGER set_bookings_updated_at BEFORE UPDATE ON bookings FOR EACH ROW EXECUTE FUNCTION set_updated_at() TRIGGER,public,bookings,sync_partner_finder_profile_from_booking_trigger,enabled,CREATE TRIGGER sync_partner_finder_profile_from_booking_trigger AFTER INSERT OR DELETE OR UPDATE ON bookings FOR EACH ROW EXECUTE FUNCTION sync_partner_finder_profile_from_booking() TRIGGER,public,bookings,trg_booking_court_sport_consistency,enabled,"CREATE TRIGGER trg_booking_court_sport_consistency BEFORE INSERT OR UPDATE OF sport, court_id ON bookings FOR EACH ROW EXECUTE FUNCTION check_booking_court_sport_consistency()" TRIGGER,public,bookings,trg_booking_season_sport_consistency,enabled,"CREATE TRIGGER trg_booking_season_sport_consistency BEFORE INSERT OR UPDATE OF sport, season_id ON bookings FOR EACH ROW EXECUTE FUNCTION check_booking_season_sport_consistency()" CONSTRAINT,public,club_ghl_config,club_ghl_config_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,club_ghl_config,club_ghl_config_enabled_requires_location,c,"CHECK (sync_enabled = false OR NULLIF(btrim(location_id), ''::text) IS NOT NULL)" CONSTRAINT,public,club_ghl_config,club_ghl_config_pkey,p,PRIMARY KEY (club_id) INDEX,public,club_ghl_config,club_ghl_config_pkey,constraint_backed,CREATE UNIQUE INDEX club_ghl_config_pkey ON public.club_ghl_config USING btree (club_id) TRIGGER,public,club_ghl_config,set_club_ghl_config_updated_at,enabled,CREATE TRIGGER set_club_ghl_config_updated_at BEFORE UPDATE ON club_ghl_config FOR EACH ROW EXECUTE FUNCTION set_ghl_contact_sync_updated_at() CONSTRAINT,public,clubs,clubs_cancellation_cutoff_hours_check,c,CHECK (cancellation_cutoff_hours >= 0 AND cancellation_cutoff_hours <= 720) CONSTRAINT,public,clubs,clubs_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,clubs,clubs_slug_key,u,UNIQUE (slug) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_pkey,constraint_backed,CREATE UNIQUE INDEX clubs_pkey ON public.clubs USING btree (id) INDEX,public,clubs,clubs_slug_key,constraint_backed,CREATE UNIQUE INDEX clubs_slug_key ON public.clubs USING btree (slug) TRIGGER,public,clubs,set_clubs_updated_at,enabled,CREATE TRIGGER set_clubs_updated_at BEFORE UPDATE ON clubs FOR EACH ROW EXECUTE FUNCTION set_updated_at() CONSTRAINT,public,coach_availability,coach_availability_coach_id_fkey,f,FOREIGN KEY (coach_id) REFERENCES coach_profiles(id) ON DELETE CASCADE CONSTRAINT,public,coach_availability,coach_availability_day_of_week_check,c,CHECK (day_of_week >= 0 AND day_of_week <= 6) CONSTRAINT,public,coach_availability,coach_availability_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,coach_availability,coach_availability_sport_check,c,"CHECK (sport = ANY (ARRAY['tennis'::text, 'padel'::text]))" CONSTRAINT,public,coach_availability,coach_availability_time_check,c,CHECK (end_time > start_time) INDEX,public,coach_availability,coach_availability_pkey,constraint_backed,CREATE UNIQUE INDEX coach_availability_pkey ON public.coach_availability USING btree (id) INDEX,public,coach_availability,coach_availability_unique_idx,standalone,"CREATE UNIQUE INDEX coach_availability_unique_idx ON public.coach_availability USING btree (coach_id, sport, day_of_week, start_time, end_time)" TRIGGER,public,coach_availability,set_coach_availability_updated_at,enabled,CREATE TRIGGER set_coach_availability_updated_at BEFORE UPDATE ON coach_availability FOR EACH ROW EXECUTE FUNCTION set_updated_at() CONSTRAINT,public,coach_permissions,coach_permissions_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,coach_permissions,coach_permissions_coach_user_profile_id_fkey,f,FOREIGN KEY (coach_user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE CONSTRAINT,public,coach_permissions,coach_permissions_created_by_user_profile_id_fkey,f,FOREIGN KEY (created_by_user_profile_id) REFERENCES user_profiles(id) ON DELETE SET NULL CONSTRAINT,public,coach_permissions,coach_permissions_pkey,p,PRIMARY KEY (id) INDEX,public,coach_permissions,coach_permissions_pkey,constraint_backed,CREATE UNIQUE INDEX coach_permissions_pkey ON public.coach_permissions USING btree (id) INDEX,public,coach_permissions,coach_permissions_unique_club_coach,standalone,"CREATE UNIQUE INDEX coach_permissions_unique_club_coach ON public.coach_permissions USING btree (club_id, coach_user_profile_id)" TRIGGER,public,coach_permissions,set_coach_permissions_updated_at,enabled,CREATE TRIGGER set_coach_permissions_updated_at BEFORE UPDATE ON coach_permissions FOR EACH ROW EXECUTE FUNCTION set_updated_at() CONSTRAINT,public,coach_profiles,coach_profiles_min_advance_hours_check,c,"CHECK (min_advance_hours = ANY (ARRAY[48, 24, 12, 6, 3]))" CONSTRAINT,public,coach_profiles,coach_profiles_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,coach_profiles,coach_profiles_user_id_fkey,f,FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE CONSTRAINT,public,coach_profiles,coach_profiles_user_id_key,u,UNIQUE (user_id) CONSTRAINT,public,coach_profiles,coach_profiles_user_profile_id_fkey,f,FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE INDEX,public,coach_profiles,coach_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX coach_profiles_pkey ON public.coach_profiles USING btree (id) INDEX,public,coach_profiles,coach_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX coach_profiles_pkey ON public.coach_profiles USING btree (id) INDEX,public,coach_profiles,coach_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX coach_profiles_pkey ON public.coach_profiles USING btree (id) INDEX,public,coach_profiles,coach_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX coach_profiles_pkey ON public.coach_profiles USING btree (id) INDEX,public,coach_profiles,coach_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX coach_profiles_pkey ON public.coach_profiles USING btree (id) INDEX,public,coach_profiles,coach_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX coach_profiles_pkey ON public.coach_profiles USING btree (id) INDEX,public,coach_profiles,coach_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX coach_profiles_pkey ON public.coach_profiles USING btree (id) INDEX,public,coach_profiles,coach_profiles_user_id_key,constraint_backed,CREATE UNIQUE INDEX coach_profiles_user_id_key ON public.coach_profiles USING btree (user_id) INDEX,public,coach_profiles,coach_profiles_user_id_unique,standalone,CREATE UNIQUE INDEX coach_profiles_user_id_unique ON public.coach_profiles USING btree (user_id) INDEX,public,coach_profiles,coach_profiles_user_profile_id_unique_full,standalone,CREATE UNIQUE INDEX coach_profiles_user_profile_id_unique_full ON public.coach_profiles USING btree (user_profile_id) TRIGGER,public,coach_profiles,coach_profiles_identity_compat_trigger,enabled,CREATE TRIGGER coach_profiles_identity_compat_trigger BEFORE INSERT OR UPDATE ON coach_profiles FOR EACH ROW EXECUTE FUNCTION coach_profiles_identity_compat() CONSTRAINT,public,courts,courts_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,courts,courts_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,courts,courts_season_id_fkey,f,FOREIGN KEY (season_id) REFERENCES seasons(id) ON DELETE CASCADE CONSTRAINT,public,courts,courts_sport_check,c,"CHECK (sport = ANY (ARRAY['tennis'::text, 'padel'::text]))" INDEX,public,courts,courts_pkey,constraint_backed,CREATE UNIQUE INDEX courts_pkey ON public.courts USING btree (id) INDEX,public,courts,courts_pkey,constraint_backed,CREATE UNIQUE INDEX courts_pkey ON public.courts USING btree (id) INDEX,public,courts,courts_pkey,constraint_backed,CREATE UNIQUE INDEX courts_pkey ON public.courts USING btree (id) INDEX,public,courts,idx_courts_club_sport_season,standalone,"CREATE INDEX idx_courts_club_sport_season ON public.courts USING btree (club_id, sport, season_id)" TRIGGER,public,courts,set_courts_updated_at,enabled,CREATE TRIGGER set_courts_updated_at BEFORE UPDATE ON courts FOR EACH ROW EXECUTE FUNCTION set_updated_at() TRIGGER,public,courts,trg_court_season_sport_consistency,enabled,"CREATE TRIGGER trg_court_season_sport_consistency BEFORE INSERT OR UPDATE OF sport, season_id ON courts FOR EACH ROW EXECUTE FUNCTION check_court_season_sport_consistency()" CONSTRAINT,public,credit_transactions,credit_transactions_amount_check,c,CHECK (amount <> 0::numeric AND amount <> 'NaN'::numeric AND amount <> 'Infinity'::numeric AND amount <> '-Infinity'::numeric) CONSTRAINT,public,credit_transactions,credit_transactions_booking_id_fkey,f,FOREIGN KEY (booking_id) REFERENCES bookings(id) ON DELETE SET NULL CONSTRAINT,public,credit_transactions,credit_transactions_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE RESTRICT CONSTRAINT,public,credit_transactions,credit_transactions_created_by_fkey,f,FOREIGN KEY (created_by) REFERENCES user_profiles(id) ON DELETE RESTRICT CONSTRAINT,public,credit_transactions,credit_transactions_credit_type_check,c,"CHECK (credit_type = ANY (ARRAY['standard'::text, 'season_pass'::text]))" CONSTRAINT,public,credit_transactions,credit_transactions_currency_check,c,"CHECK (currency = ANY (ARRAY['HUF'::text, 'EUR'::text, 'USD'::text]))" CONSTRAINT,public,credit_transactions,credit_transactions_idempotency_key_key,u,UNIQUE (idempotency_key) CONSTRAINT,public,credit_transactions,credit_transactions_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,credit_transactions,credit_transactions_season_id_fkey,f,FOREIGN KEY (season_id) REFERENCES seasons(id) ON DELETE RESTRICT CONSTRAINT,public,credit_transactions,credit_transactions_transaction_category_check,c,"CHECK (transaction_category = ANY (ARRAY['admin_grant'::text, 'admin_deduction'::text, 'booking_payment'::text, 'cancellation_refund'::text, 'admin_correction'::text, 'expiration'::text]))" CONSTRAINT,public,credit_transactions,credit_transactions_user_profile_id_fkey,f,FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE RESTRICT CONSTRAINT,public,credit_transactions,credit_transactions_wallet_id_fkey,f,FOREIGN KEY (wallet_id) REFERENCES credit_wallets(id) ON DELETE RESTRICT INDEX,public,credit_transactions,credit_transactions_idempotency_key_idx,standalone,CREATE UNIQUE INDEX credit_transactions_idempotency_key_idx ON public.credit_transactions USING btree (idempotency_key) WHERE (idempotency_key IS NOT NULL) INDEX,public,credit_transactions,credit_transactions_idempotency_key_key,constraint_backed,CREATE UNIQUE INDEX credit_transactions_idempotency_key_key ON public.credit_transactions USING btree (idempotency_key) INDEX,public,credit_transactions,credit_transactions_pkey,constraint_backed,CREATE UNIQUE INDEX credit_transactions_pkey ON public.credit_transactions USING btree (id) INDEX,public,credit_transactions,idx_credit_transactions_club,standalone,"CREATE INDEX idx_credit_transactions_club ON public.credit_transactions USING btree (club_id, created_at DESC)" INDEX,public,credit_transactions,idx_credit_transactions_user,standalone,"CREATE INDEX idx_credit_transactions_user ON public.credit_transactions USING btree (user_profile_id, created_at DESC)" CONSTRAINT,public,credit_wallets,credit_wallets_balance_check,c,CHECK (balance >= 0::numeric AND balance <> 'NaN'::numeric AND balance <> 'Infinity'::numeric AND balance <> '-Infinity'::numeric) CONSTRAINT,public,credit_wallets,credit_wallets_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE RESTRICT CONSTRAINT,public,credit_wallets,credit_wallets_club_id_user_profile_id_season_id_currency_c_key,u,"UNIQUE (club_id, user_profile_id, season_id, currency, credit_type)" CONSTRAINT,public,credit_wallets,credit_wallets_credit_type_check,c,"CHECK (credit_type = ANY (ARRAY['standard'::text, 'season_pass'::text]))" CONSTRAINT,public,credit_wallets,credit_wallets_currency_check,c,"CHECK (currency = ANY (ARRAY['HUF'::text, 'EUR'::text, 'USD'::text]))" CONSTRAINT,public,credit_wallets,credit_wallets_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,credit_wallets,credit_wallets_season_id_fkey,f,FOREIGN KEY (season_id) REFERENCES seasons(id) ON DELETE RESTRICT CONSTRAINT,public,credit_wallets,credit_wallets_user_profile_id_fkey,f,FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE RESTRICT INDEX,public,credit_wallets,credit_wallets_club_id_user_profile_id_season_id_currency_c_key,constraint_backed,"CREATE UNIQUE INDEX credit_wallets_club_id_user_profile_id_season_id_currency_c_key ON public.credit_wallets USING btree (club_id, user_profile_id, season_id, currency, credit_type)" INDEX,public,credit_wallets,credit_wallets_pkey,constraint_backed,CREATE UNIQUE INDEX credit_wallets_pkey ON public.credit_wallets USING btree (id) INDEX,public,credit_wallets,credit_wallets_pkey,constraint_backed,CREATE UNIQUE INDEX credit_wallets_pkey ON public.credit_wallets USING btree (id) INDEX,public,credit_wallets,credit_wallets_pkey,constraint_backed,CREATE UNIQUE INDEX credit_wallets_pkey ON public.credit_wallets USING btree (id) INDEX,public,credit_wallets,idx_credit_wallets_user,standalone,CREATE INDEX idx_credit_wallets_user ON public.credit_wallets USING btree (user_profile_id) CONSTRAINT,public,ghl_contact_sync_queue,ghl_contact_sync_queue_attempt_count_check,c,CHECK (attempt_count >= 0) CONSTRAINT,public,ghl_contact_sync_queue,ghl_contact_sync_queue_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,ghl_contact_sync_queue,ghl_contact_sync_queue_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,ghl_contact_sync_queue,ghl_contact_sync_queue_status_check,c,"CHECK (sync_status = ANY (ARRAY['pending'::text, 'processing'::text, 'synced'::text, 'failed'::text, 'disabled'::text]))" CONSTRAINT,public,ghl_contact_sync_queue,ghl_contact_sync_queue_user_profile_id_fkey,f,FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE INDEX,public,ghl_contact_sync_queue,ghl_contact_sync_queue_club_idx,standalone,"CREATE INDEX ghl_contact_sync_queue_club_idx ON public.ghl_contact_sync_queue USING btree (club_id, sync_status)" INDEX,public,ghl_contact_sync_queue,ghl_contact_sync_queue_pending_idx,standalone,"CREATE INDEX ghl_contact_sync_queue_pending_idx ON public.ghl_contact_sync_queue USING btree (sync_status, next_attempt_at, created_at)" INDEX,public,ghl_contact_sync_queue,ghl_contact_sync_queue_pkey,constraint_backed,CREATE UNIQUE INDEX ghl_contact_sync_queue_pkey ON public.ghl_contact_sync_queue USING btree (id) INDEX,public,ghl_contact_sync_queue,ghl_contact_sync_queue_user_profile_uidx,standalone,CREATE UNIQUE INDEX ghl_contact_sync_queue_user_profile_uidx ON public.ghl_contact_sync_queue USING btree (user_profile_id) TRIGGER,public,ghl_contact_sync_queue,set_ghl_contact_sync_queue_updated_at,enabled,CREATE TRIGGER set_ghl_contact_sync_queue_updated_at BEFORE UPDATE ON ghl_contact_sync_queue FOR EACH ROW EXECUTE FUNCTION set_ghl_contact_sync_updated_at() CONSTRAINT,public,opening_hours,opening_hours_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,opening_hours,opening_hours_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,opening_hours,opening_hours_season_id_fkey,f,FOREIGN KEY (season_id) REFERENCES seasons(id) ON DELETE CASCADE INDEX,public,opening_hours,opening_hours_pkey,constraint_backed,CREATE UNIQUE INDEX opening_hours_pkey ON public.opening_hours USING btree (id) TRIGGER,public,opening_hours,set_opening_hours_updated_at,enabled,CREATE TRIGGER set_opening_hours_updated_at BEFORE UPDATE ON opening_hours FOR EACH ROW EXECUTE FUNCTION set_updated_at() CONSTRAINT,public,partner_finder_profiles,partner_finder_profiles_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,partner_finder_profiles,partner_finder_profiles_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,partner_finder_profiles,partner_finder_profiles_sport_check,c,"CHECK (sport = ANY (ARRAY['tennis'::text, 'padel'::text]))" CONSTRAINT,public,partner_finder_profiles,partner_finder_profiles_user_profile_id_fkey,f,FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE INDEX,public,partner_finder_profiles,idx_partner_finder_profiles_club_sport,standalone,"CREATE INDEX idx_partner_finder_profiles_club_sport ON public.partner_finder_profiles USING btree (club_id, sport)" INDEX,public,partner_finder_profiles,partner_finder_profiles_club_enabled_idx,standalone,"CREATE INDEX partner_finder_profiles_club_enabled_idx ON public.partner_finder_profiles USING btree (club_id, partner_search_profile_enabled)" INDEX,public,partner_finder_profiles,partner_finder_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX partner_finder_profiles_pkey ON public.partner_finder_profiles USING btree (id) INDEX,public,partner_finder_profiles,partner_finder_profiles_user_profile_sport_unique,standalone,"CREATE UNIQUE INDEX partner_finder_profiles_user_profile_sport_unique ON public.partner_finder_profiles USING btree (user_profile_id, sport)" TRIGGER,public,partner_finder_profiles,enforce_partner_finder_profile_identity_trigger,enabled,CREATE TRIGGER enforce_partner_finder_profile_identity_trigger BEFORE INSERT OR UPDATE ON partner_finder_profiles FOR EACH ROW EXECUTE FUNCTION enforce_partner_finder_profile_identity() CONSTRAINT,public,partner_search_availability,partner_search_availability_check,c,CHECK (end_time > start_time) CONSTRAINT,public,partner_search_availability,partner_search_availability_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,partner_search_availability,partner_search_availability_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,partner_search_availability,partner_search_availability_sport_check,c,"CHECK (sport = ANY (ARRAY['tennis'::text, 'padel'::text]))" CONSTRAINT,public,partner_search_availability,partner_search_availability_user_profile_id_fkey,f,FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE CONSTRAINT,public,partner_search_availability,partner_search_availability_weekday_check,c,CHECK (weekday >= 1 AND weekday <= 7) INDEX,public,partner_search_availability,idx_partner_search_availability_club,standalone,CREATE INDEX idx_partner_search_availability_club ON public.partner_search_availability USING btree (club_id) INDEX,public,partner_search_availability,idx_partner_search_availability_profile,standalone,CREATE INDEX idx_partner_search_availability_profile ON public.partner_search_availability USING btree (user_profile_id) INDEX,public,partner_search_availability,partner_search_availability_pkey,constraint_backed,CREATE UNIQUE INDEX partner_search_availability_pkey ON public.partner_search_availability USING btree (id) INDEX,public,partner_search_availability,partner_search_availability_unique_idx,standalone,"CREATE UNIQUE INDEX partner_search_availability_unique_idx ON public.partner_search_availability USING btree (user_profile_id, club_id, sport, weekday, start_time, end_time)" CONSTRAINT,public,pricing_group_courts,pricing_group_courts_court_id_fkey,f,FOREIGN KEY (court_id) REFERENCES courts(id) ON DELETE CASCADE CONSTRAINT,public,pricing_group_courts,pricing_group_courts_court_id_unique,u,UNIQUE (court_id) CONSTRAINT,public,pricing_group_courts,pricing_group_courts_group_court_unique,u,"UNIQUE (pricing_group_id, court_id)" CONSTRAINT,public,pricing_group_courts,pricing_group_courts_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,pricing_group_courts,pricing_group_courts_pricing_group_id_fkey,f,FOREIGN KEY (pricing_group_id) REFERENCES pricing_groups(id) ON DELETE CASCADE INDEX,public,pricing_group_courts,idx_pricing_group_courts_court,standalone,CREATE INDEX idx_pricing_group_courts_court ON public.pricing_group_courts USING btree (court_id) INDEX,public,pricing_group_courts,pricing_group_courts_court_id_unique,constraint_backed,CREATE UNIQUE INDEX pricing_group_courts_court_id_unique ON public.pricing_group_courts USING btree (court_id) INDEX,public,pricing_group_courts,pricing_group_courts_group_court_unique,constraint_backed,"CREATE UNIQUE INDEX pricing_group_courts_group_court_unique ON public.pricing_group_courts USING btree (pricing_group_id, court_id)" INDEX,public,pricing_group_courts,pricing_group_courts_pkey,constraint_backed,CREATE UNIQUE INDEX pricing_group_courts_pkey ON public.pricing_group_courts USING btree (id) TRIGGER,public,pricing_group_courts,trg_pricing_group_court_consistency,enabled,CREATE TRIGGER trg_pricing_group_court_consistency BEFORE INSERT OR UPDATE ON pricing_group_courts FOR EACH ROW EXECUTE FUNCTION check_pricing_group_court_consistency() CONSTRAINT,public,pricing_groups,pricing_groups_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,pricing_groups,pricing_groups_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,pricing_groups,pricing_groups_season_id_fkey,f,FOREIGN KEY (season_id) REFERENCES seasons(id) ON DELETE CASCADE CONSTRAINT,public,pricing_groups,pricing_groups_sport_check,c,"CHECK (sport = ANY (ARRAY['tennis'::text, 'padel'::text]))" INDEX,public,pricing_groups,idx_pricing_groups_club_season_sport,standalone,"CREATE INDEX idx_pricing_groups_club_season_sport ON public.pricing_groups USING btree (club_id, season_id, sport)" INDEX,public,pricing_groups,pricing_groups_pkey,constraint_backed,CREATE UNIQUE INDEX pricing_groups_pkey ON public.pricing_groups USING btree (id) INDEX,public,pricing_groups,pricing_groups_pkey,constraint_backed,CREATE UNIQUE INDEX pricing_groups_pkey ON public.pricing_groups USING btree (id) INDEX,public,pricing_groups,pricing_groups_pkey,constraint_backed,CREATE UNIQUE INDEX pricing_groups_pkey ON public.pricing_groups USING btree (id) CONSTRAINT,public,pricing_rules,pricing_rules_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,pricing_rules,pricing_rules_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,pricing_rules,pricing_rules_pricing_group_id_fkey,f,FOREIGN KEY (pricing_group_id) REFERENCES pricing_groups(id) ON DELETE CASCADE CONSTRAINT,public,pricing_rules,pricing_rules_season_id_fkey,f,FOREIGN KEY (season_id) REFERENCES seasons(id) ON DELETE CASCADE INDEX,public,pricing_rules,idx_pricing_rules_group,standalone,CREATE INDEX idx_pricing_rules_group ON public.pricing_rules USING btree (pricing_group_id) INDEX,public,pricing_rules,pricing_rules_pkey,constraint_backed,CREATE UNIQUE INDEX pricing_rules_pkey ON public.pricing_rules USING btree (id) TRIGGER,public,pricing_rules,set_pricing_rules_updated_at,enabled,CREATE TRIGGER set_pricing_rules_updated_at BEFORE UPDATE ON pricing_rules FOR EACH ROW EXECUTE FUNCTION set_updated_at() CONSTRAINT,public,recurring_booking_series,recurring_booking_series_check,c,CHECK (end_date >= start_date) CONSTRAINT,public,recurring_booking_series,recurring_booking_series_check1,c,CHECK (total_amount >= 0::numeric AND 'NaN'::numeric <> total_amount AND total_amount <> 'Infinity'::numeric AND total_amount <> '-Infinity'::numeric AND (currency <> 'HUF'::text OR total_amount = round(total_amount))) CONSTRAINT,public,recurring_booking_series,recurring_booking_series_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE RESTRICT CONSTRAINT,public,recurring_booking_series,recurring_booking_series_created_by_user_profile_id_fkey,f,FOREIGN KEY (created_by_user_profile_id) REFERENCES user_profiles(id) ON DELETE RESTRICT CONSTRAINT,public,recurring_booking_series,recurring_booking_series_creation_request_id_key,u,UNIQUE (creation_request_id) CONSTRAINT,public,recurring_booking_series,recurring_booking_series_currency_check,c,"CHECK (currency = ANY (ARRAY['HUF'::text, 'EUR'::text, 'USD'::text]))" CONSTRAINT,public,recurring_booking_series,recurring_booking_series_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,recurring_booking_series,recurring_booking_series_season_id_fkey,f,FOREIGN KEY (season_id) REFERENCES seasons(id) ON DELETE RESTRICT CONSTRAINT,public,recurring_booking_series,recurring_booking_series_status_check,c,"CHECK (status = ANY (ARRAY['active'::text, 'cancelled'::text, 'completed'::text]))" CONSTRAINT,public,recurring_booking_series,recurring_booking_series_total_occurrences_check,c,CHECK (total_occurrences > 0) CONSTRAINT,public,recurring_booking_series,recurring_booking_series_user_profile_id_fkey,f,FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE SET NULL INDEX,public,recurring_booking_series,idx_recurring_booking_series_club_season,standalone,"CREATE INDEX idx_recurring_booking_series_club_season ON public.recurring_booking_series USING btree (club_id, season_id)" INDEX,public,recurring_booking_series,idx_recurring_booking_series_created_at,standalone,CREATE INDEX idx_recurring_booking_series_created_at ON public.recurring_booking_series USING btree (created_at DESC) INDEX,public,recurring_booking_series,idx_recurring_booking_series_status,standalone,CREATE INDEX idx_recurring_booking_series_status ON public.recurring_booking_series USING btree (status) INDEX,public,recurring_booking_series,idx_recurring_booking_series_user,standalone,CREATE INDEX idx_recurring_booking_series_user ON public.recurring_booking_series USING btree (user_profile_id) INDEX,public,recurring_booking_series,recurring_booking_series_creation_request_id_key,constraint_backed,CREATE UNIQUE INDEX recurring_booking_series_creation_request_id_key ON public.recurring_booking_series USING btree (creation_request_id) INDEX,public,recurring_booking_series,recurring_booking_series_pkey,constraint_backed,CREATE UNIQUE INDEX recurring_booking_series_pkey ON public.recurring_booking_series USING btree (id) INDEX,public,recurring_booking_series,recurring_booking_series_pkey,constraint_backed,CREATE UNIQUE INDEX recurring_booking_series_pkey ON public.recurring_booking_series USING btree (id) INDEX,public,recurring_booking_series,recurring_booking_series_pkey,constraint_backed,CREATE UNIQUE INDEX recurring_booking_series_pkey ON public.recurring_booking_series USING btree (id) TRIGGER,public,recurring_booking_series,set_recurring_booking_series_updated_at,enabled,CREATE TRIGGER set_recurring_booking_series_updated_at BEFORE UPDATE ON recurring_booking_series FOR EACH ROW EXECUTE FUNCTION set_updated_at() CONSTRAINT,public,seasons,seasons_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,seasons,seasons_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,seasons,seasons_sport_check,c,"CHECK (sport = ANY (ARRAY['tennis'::text, 'padel'::text]))" INDEX,public,seasons,idx_seasons_club_sport,standalone,"CREATE INDEX idx_seasons_club_sport ON public.seasons USING btree (club_id, sport)" INDEX,public,seasons,seasons_pkey,constraint_backed,CREATE UNIQUE INDEX seasons_pkey ON public.seasons USING btree (id) INDEX,public,seasons,seasons_pkey,constraint_backed,CREATE UNIQUE INDEX seasons_pkey ON public.seasons USING btree (id) INDEX,public,seasons,seasons_pkey,constraint_backed,CREATE UNIQUE INDEX seasons_pkey ON public.seasons USING btree (id) INDEX,public,seasons,seasons_pkey,constraint_backed,CREATE UNIQUE INDEX seasons_pkey ON public.seasons USING btree (id) INDEX,public,seasons,seasons_pkey,constraint_backed,CREATE UNIQUE INDEX seasons_pkey ON public.seasons USING btree (id) INDEX,public,seasons,seasons_pkey,constraint_backed,CREATE UNIQUE INDEX seasons_pkey ON public.seasons USING btree (id) INDEX,public,seasons,seasons_pkey,constraint_backed,CREATE UNIQUE INDEX seasons_pkey ON public.seasons USING btree (id) INDEX,public,seasons,seasons_pkey,constraint_backed,CREATE UNIQUE INDEX seasons_pkey ON public.seasons USING btree (id) INDEX,public,seasons,seasons_pkey,constraint_backed,CREATE UNIQUE INDEX seasons_pkey ON public.seasons USING btree (id) INDEX,public,seasons,seasons_pkey,constraint_backed,CREATE UNIQUE INDEX seasons_pkey ON public.seasons USING btree (id) INDEX,public,seasons,seasons_pkey,constraint_backed,CREATE UNIQUE INDEX seasons_pkey ON public.seasons USING btree (id) INDEX,public,seasons,seasons_pkey,constraint_backed,CREATE UNIQUE INDEX seasons_pkey ON public.seasons USING btree (id) TRIGGER,public,seasons,set_seasons_updated_at,enabled,CREATE TRIGGER set_seasons_updated_at BEFORE UPDATE ON seasons FOR EACH ROW EXECUTE FUNCTION set_updated_at() CONSTRAINT,public,settings_history,settings_history_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,settings_history,settings_history_pkey,p,PRIMARY KEY (id) INDEX,public,settings_history,settings_history_club_created_at_idx,standalone,"CREATE INDEX settings_history_club_created_at_idx ON public.settings_history USING btree (club_id, created_at DESC)" INDEX,public,settings_history,settings_history_club_version_unique,standalone,"CREATE UNIQUE INDEX settings_history_club_version_unique ON public.settings_history USING btree (club_id, version_number)" INDEX,public,settings_history,settings_history_pkey,constraint_backed,CREATE UNIQUE INDEX settings_history_pkey ON public.settings_history USING btree (id) TRIGGER,public,settings_history,enforce_settings_history_identity_trigger,enabled,CREATE TRIGGER enforce_settings_history_identity_trigger BEFORE INSERT ON settings_history FOR EACH ROW EXECUTE FUNCTION enforce_settings_history_identity() CONSTRAINT,public,user_profiles,user_profiles_auth_user_id_fkey,f,FOREIGN KEY (auth_user_id) REFERENCES auth.users(id) ON DELETE SET NULL CONSTRAINT,public,user_profiles,user_profiles_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE CASCADE CONSTRAINT,public,user_profiles,user_profiles_pkey,p,PRIMARY KEY (id) INDEX,public,user_profiles,user_profiles_auth_user_id_unique,standalone,CREATE UNIQUE INDEX user_profiles_auth_user_id_unique ON public.user_profiles USING btree (auth_user_id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) INDEX,public,user_profiles,user_profiles_pkey,constraint_backed,CREATE UNIQUE INDEX user_profiles_pkey ON public.user_profiles USING btree (id) TRIGGER,public,user_profiles,enqueue_new_user_for_ghl_contact_sync,enabled,CREATE TRIGGER enqueue_new_user_for_ghl_contact_sync AFTER INSERT ON user_profiles FOR EACH ROW EXECUTE FUNCTION enqueue_new_user_for_ghl_contact_sync() TRIGGER,public,user_profiles,set_user_profiles_updated_at,enabled,CREATE TRIGGER set_user_profiles_updated_at BEFORE UPDATE ON user_profiles FOR EACH ROW EXECUTE FUNCTION set_updated_at() CONSTRAINT,public,venue_payment_installment_events,venue_payment_installment_events_amount_check,c,CHECK (amount > 0::numeric AND 'NaN'::numeric <> amount AND amount <> 'Infinity'::numeric AND amount <> '-Infinity'::numeric) CONSTRAINT,public,venue_payment_installment_events,venue_payment_installment_events_check,c,CHECK (currency <> 'HUF'::text OR amount = round(amount)) CONSTRAINT,public,venue_payment_installment_events,venue_payment_installment_events_check1,c,CHECK (event_type = 'paid'::text AND reverses_event_id IS NULL OR event_type = 'reversed'::text AND reverses_event_id IS NOT NULL AND note IS NOT NULL AND TRIM(BOTH FROM note) <> ''::text) CONSTRAINT,public,venue_payment_installment_events,venue_payment_installment_events_currency_check,c,"CHECK (currency = ANY (ARRAY['HUF'::text, 'EUR'::text, 'USD'::text]))" CONSTRAINT,public,venue_payment_installment_events,venue_payment_installment_events_event_type_check,c,"CHECK (event_type = ANY (ARRAY['paid'::text, 'reversed'::text]))" CONSTRAINT,public,venue_payment_installment_events,venue_payment_installment_events_installment_id_fkey,f,FOREIGN KEY (installment_id) REFERENCES venue_payment_installments(id) ON DELETE RESTRICT CONSTRAINT,public,venue_payment_installment_events,venue_payment_installment_events_performed_by_admin_id_fkey,f,FOREIGN KEY (performed_by_admin_id) REFERENCES user_profiles(id) ON DELETE RESTRICT CONSTRAINT,public,venue_payment_installment_events,venue_payment_installment_events_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,venue_payment_installment_events,venue_payment_installment_events_request_id_key,u,UNIQUE (request_id) CONSTRAINT,public,venue_payment_installment_events,venue_payment_installment_events_reverses_event_id_fkey,f,FOREIGN KEY (reverses_event_id) REFERENCES venue_payment_installment_events(id) ON DELETE RESTRICT INDEX,public,venue_payment_installment_events,idx_venue_payment_events_installment,standalone,"CREATE INDEX idx_venue_payment_events_installment ON public.venue_payment_installment_events USING btree (installment_id, occurred_at DESC)" INDEX,public,venue_payment_installment_events,idx_venue_payment_events_reverses_id,standalone,CREATE UNIQUE INDEX idx_venue_payment_events_reverses_id ON public.venue_payment_installment_events USING btree (reverses_event_id) WHERE (reverses_event_id IS NOT NULL) INDEX,public,venue_payment_installment_events,idx_venue_payment_events_type,standalone,CREATE INDEX idx_venue_payment_events_type ON public.venue_payment_installment_events USING btree (event_type) INDEX,public,venue_payment_installment_events,venue_payment_installment_events_pkey,constraint_backed,CREATE UNIQUE INDEX venue_payment_installment_events_pkey ON public.venue_payment_installment_events USING btree (id) INDEX,public,venue_payment_installment_events,venue_payment_installment_events_pkey,constraint_backed,CREATE UNIQUE INDEX venue_payment_installment_events_pkey ON public.venue_payment_installment_events USING btree (id) INDEX,public,venue_payment_installment_events,venue_payment_installment_events_pkey,constraint_backed,CREATE UNIQUE INDEX venue_payment_installment_events_pkey ON public.venue_payment_installment_events USING btree (id) INDEX,public,venue_payment_installment_events,venue_payment_installment_events_request_id_key,constraint_backed,CREATE UNIQUE INDEX venue_payment_installment_events_request_id_key ON public.venue_payment_installment_events USING btree (request_id) TRIGGER,public,venue_payment_installment_events,trg_prevent_event_mutation,enabled,CREATE TRIGGER trg_prevent_event_mutation BEFORE DELETE OR UPDATE ON venue_payment_installment_events FOR EACH ROW EXECUTE FUNCTION prevent_event_mutation() CONSTRAINT,public,venue_payment_installments,fk_current_paid_event,f,FOREIGN KEY (current_paid_event_id) REFERENCES venue_payment_installment_events(id) ON DELETE RESTRICT CONSTRAINT,public,venue_payment_installments,venue_payment_installments_amount_check,c,CHECK (amount > 0::numeric AND 'NaN'::numeric <> amount AND amount <> 'Infinity'::numeric AND amount <> '-Infinity'::numeric) CONSTRAINT,public,venue_payment_installments,venue_payment_installments_check,c,CHECK (currency <> 'HUF'::text OR amount = round(amount)) CONSTRAINT,public,venue_payment_installments,venue_payment_installments_check1,c,CHECK (status = 'pending'::text AND current_paid_event_id IS NULL OR status = 'paid'::text AND current_paid_event_id IS NOT NULL) CONSTRAINT,public,venue_payment_installments,venue_payment_installments_currency_check,c,"CHECK (currency = ANY (ARRAY['HUF'::text, 'EUR'::text, 'USD'::text]))" CONSTRAINT,public,venue_payment_installments,venue_payment_installments_installment_number_check,c,"CHECK (installment_number = ANY (ARRAY[1, 2, 3]))" CONSTRAINT,public,venue_payment_installments,venue_payment_installments_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,venue_payment_installments,venue_payment_installments_plan_id_fkey,f,FOREIGN KEY (plan_id) REFERENCES venue_payment_plans(id) ON DELETE RESTRICT CONSTRAINT,public,venue_payment_installments,venue_payment_installments_status_check,c,"CHECK (status = ANY (ARRAY['pending'::text, 'paid'::text]))" INDEX,public,venue_payment_installments,idx_venue_payment_installments_plan,standalone,CREATE INDEX idx_venue_payment_installments_plan ON public.venue_payment_installments USING btree (plan_id) INDEX,public,venue_payment_installments,idx_venue_payment_installments_plan_number,standalone,"CREATE UNIQUE INDEX idx_venue_payment_installments_plan_number ON public.venue_payment_installments USING btree (plan_id, installment_number)" INDEX,public,venue_payment_installments,idx_venue_payment_installments_status,standalone,CREATE INDEX idx_venue_payment_installments_status ON public.venue_payment_installments USING btree (status) INDEX,public,venue_payment_installments,venue_payment_installments_pkey,constraint_backed,CREATE UNIQUE INDEX venue_payment_installments_pkey ON public.venue_payment_installments USING btree (id) INDEX,public,venue_payment_installments,venue_payment_installments_pkey,constraint_backed,CREATE UNIQUE INDEX venue_payment_installments_pkey ON public.venue_payment_installments USING btree (id) TRIGGER,public,venue_payment_installments,set_venue_payment_installments_updated_at,enabled,CREATE TRIGGER set_venue_payment_installments_updated_at BEFORE UPDATE ON venue_payment_installments FOR EACH ROW EXECUTE FUNCTION set_updated_at() TRIGGER,public,venue_payment_installments,trg_prevent_installment_core_mutation,enabled,CREATE TRIGGER trg_prevent_installment_core_mutation BEFORE UPDATE ON venue_payment_installments FOR EACH ROW EXECUTE FUNCTION prevent_installment_core_mutation() TRIGGER,public,venue_payment_installments,trg_prevent_installment_deletion,enabled,CREATE TRIGGER trg_prevent_installment_deletion BEFORE DELETE ON venue_payment_installments FOR EACH ROW EXECUTE FUNCTION prevent_installment_deletion() CONSTRAINT,public,venue_payment_plans,venue_payment_plans_booking_id_fkey,f,FOREIGN KEY (booking_id) REFERENCES bookings(id) ON DELETE RESTRICT CONSTRAINT,public,venue_payment_plans,venue_payment_plans_check,c,CHECK (total_amount > 0::numeric AND 'NaN'::numeric <> total_amount AND total_amount <> 'Infinity'::numeric AND total_amount <> '-Infinity'::numeric AND (currency <> 'HUF'::text OR total_amount = round(total_amount))) CONSTRAINT,public,venue_payment_plans,venue_payment_plans_check1,c,CHECK ((booking_id IS NULL) <> (recurring_series_id IS NULL)) CONSTRAINT,public,venue_payment_plans,venue_payment_plans_check2,c,CHECK (linked_at IS NULL AND linked_by_admin_id IS NULL AND link_request_id IS NULL OR linked_at IS NOT NULL AND linked_by_admin_id IS NOT NULL AND link_request_id IS NOT NULL) CONSTRAINT,public,venue_payment_plans,venue_payment_plans_club_id_fkey,f,FOREIGN KEY (club_id) REFERENCES clubs(id) ON DELETE RESTRICT CONSTRAINT,public,venue_payment_plans,venue_payment_plans_created_by_admin_id_fkey,f,FOREIGN KEY (created_by_admin_id) REFERENCES user_profiles(id) ON DELETE RESTRICT CONSTRAINT,public,venue_payment_plans,venue_payment_plans_creation_request_id_key,u,UNIQUE (creation_request_id) CONSTRAINT,public,venue_payment_plans,venue_payment_plans_currency_check,c,"CHECK (currency = ANY (ARRAY['HUF'::text, 'EUR'::text, 'USD'::text]))" CONSTRAINT,public,venue_payment_plans,venue_payment_plans_installment_count_check,c,"CHECK (installment_count = ANY (ARRAY[1, 2, 3]))" CONSTRAINT,public,venue_payment_plans,venue_payment_plans_link_request_id_key,u,UNIQUE (link_request_id) CONSTRAINT,public,venue_payment_plans,venue_payment_plans_linked_by_admin_id_fkey,f,FOREIGN KEY (linked_by_admin_id) REFERENCES user_profiles(id) ON DELETE RESTRICT CONSTRAINT,public,venue_payment_plans,venue_payment_plans_payment_method_check,c,CHECK (payment_method = 'venue'::text) CONSTRAINT,public,venue_payment_plans,venue_payment_plans_pkey,p,PRIMARY KEY (id) CONSTRAINT,public,venue_payment_plans,venue_payment_plans_plan_status_check,c,"CHECK (plan_status = ANY (ARRAY['pending'::text, 'partially_paid'::text, 'paid'::text, 'cancelled'::text]))" CONSTRAINT,public,venue_payment_plans,venue_payment_plans_recurring_series_id_fkey,f,FOREIGN KEY (recurring_series_id) REFERENCES recurring_booking_series(id) ON DELETE RESTRICT CONSTRAINT,public,venue_payment_plans,venue_payment_plans_season_id_fkey,f,FOREIGN KEY (season_id) REFERENCES seasons(id) ON DELETE RESTRICT CONSTRAINT,public,venue_payment_plans,venue_payment_plans_user_profile_id_fkey,f,FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE RESTRICT INDEX,public,venue_payment_plans,idx_venue_payment_plans_booking_id,standalone,CREATE UNIQUE INDEX idx_venue_payment_plans_booking_id ON public.venue_payment_plans USING btree (booking_id) WHERE (booking_id IS NOT NULL) INDEX,public,venue_payment_plans,idx_venue_payment_plans_club_season,standalone,"CREATE INDEX idx_venue_payment_plans_club_season ON public.venue_payment_plans USING btree (club_id, season_id)" INDEX,public,venue_payment_plans,idx_venue_payment_plans_created_at,standalone,CREATE INDEX idx_venue_payment_plans_created_at ON public.venue_payment_plans USING btree (created_at DESC) INDEX,public,venue_payment_plans,idx_venue_payment_plans_series_id,standalone,CREATE UNIQUE INDEX idx_venue_payment_plans_series_id ON public.venue_payment_plans USING btree (recurring_series_id) WHERE (recurring_series_id IS NOT NULL) INDEX,public,venue_payment_plans,idx_venue_payment_plans_status,standalone,CREATE INDEX idx_venue_payment_plans_status ON public.venue_payment_plans USING btree (plan_status) INDEX,public,venue_payment_plans,idx_venue_payment_plans_user,standalone,CREATE INDEX idx_venue_payment_plans_user ON public.venue_payment_plans USING btree (user_profile_id) INDEX,public,venue_payment_plans,venue_payment_plans_creation_request_id_key,constraint_backed,CREATE UNIQUE INDEX venue_payment_plans_creation_request_id_key ON public.venue_payment_plans USING btree (creation_request_id) INDEX,public,venue_payment_plans,venue_payment_plans_link_request_id_key,constraint_backed,CREATE UNIQUE INDEX venue_payment_plans_link_request_id_key ON public.venue_payment_plans USING btree (link_request_id) INDEX,public,venue_payment_plans,venue_payment_plans_pkey,constraint_backed,CREATE UNIQUE INDEX venue_payment_plans_pkey ON public.venue_payment_plans USING btree (id) INDEX,public,venue_payment_plans,venue_payment_plans_pkey,constraint_backed,CREATE UNIQUE INDEX venue_payment_plans_pkey ON public.venue_payment_plans USING btree (id) TRIGGER,public,venue_payment_plans,set_venue_payment_plans_updated_at,enabled,CREATE TRIGGER set_venue_payment_plans_updated_at BEFORE UPDATE ON venue_payment_plans FOR EACH ROW EXECUTE FUNCTION set_updated_at() TRIGGER,public,venue_payment_plans,trg_prevent_plan_core_mutation,enabled,CREATE TRIGGER trg_prevent_plan_core_mutation BEFORE UPDATE ON venue_payment_plans FOR EACH ROW EXECUTE FUNCTION prevent_plan_core_mutation() TRIGGER,public,venue_payment_plans,trg_prevent_plan_deletion,enabled,CREATE TRIGGER trg_prevent_plan_deletion BEFORE DELETE ON venue_payment_plans FOR EACH ROW EXECUTE FUNCTION prevent_plan_deletion()
schema_name,function_name,function_kind,identity_arguments,full_arguments,return_type,language,security_definer,volatility,owner,execute_acl,function_definition
public,_consume_booking_move_audit_context,f,p_booking_id uuid,p_booking_id uuid,boolean,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public._consume_booking_move_audit_context(p_booking_id uuid)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public'
AS $function$
DECLARE
v_auth_user_id uuid;
v_consumed boolean;
BEGIN
v_auth_user_id := auth.uid();
IF v_auth_user_id IS NULL
OR p_booking_id IS NULL
THEN
RETURN false;
END IF;
DELETE FROM public._booking_move_audit_context context_row
WHERE context_row.transaction_id = txid_current()
AND context_row.auth_user_id = v_auth_user_id
AND context_row.booking_id = p_booking_id
RETURNING true
INTO v_consumed;
RETURN COALESCE(v_consumed, false);
END;
$function$
"
public,_register_booking_move_audit_context,f,p_booking_id uuid,p_booking_id uuid,void,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public._register_booking_move_audit_context(p_booking_id uuid)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public'
AS $function$
DECLARE
v_auth_user_id uuid;
BEGIN
v_auth_user_id := auth.uid();
IF v_auth_user_id IS NULL THEN
RAISE EXCEPTION
'Az auditkontextus regisztrálásához bejelentkezés szükséges.';
END IF;
IF p_booking_id IS NULL THEN
RAISE EXCEPTION
'Az auditkontextushoz booking_id szükséges.';
END IF;
INSERT INTO public._booking_move_audit_context (
transaction_id,
auth_user_id,
booking_id,
created_at
)
VALUES (
txid_current(),
v_auth_user_id,
p_booking_id,
now()
)
ON CONFLICT (
transaction_id,
auth_user_id,
booking_id
)
DO UPDATE
SET created_at = EXCLUDED.created_at;
END;
$function$
"
public,admin_create_venue_payment_plan,f,"p_request_id uuid, p_booking_id uuid, p_recurring_series_id uuid, p_installment_count integer","p_request_id uuid, p_booking_id uuid, p_recurring_series_id uuid, p_installment_count integer",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.admin_create_venue_payment_plan(p_request_id uuid, p_booking_id uuid, p_recurring_series_id uuid, p_installment_count integer)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_profile public.user_profiles%ROWTYPE;
v_target_club_id uuid;
v_total_amount numeric(14,2) := 0;
v_currency text;
v_customer_name text;
v_customer_email text;
v_customer_phone text;
v_user_profile_id uuid;
v_season_id uuid;
v_booking public.bookings%ROWTYPE;
v_series public.recurring_booking_series%ROWTYPE;
v_child_booking public.bookings%ROWTYPE;
v_existing_plan public.venue_payment_plans%ROWTYPE;
v_base_amount numeric(14,2);
v_remainder numeric(14,2);
v_inserted_plan_id uuid;
v_i integer;
v_inst_amount numeric(14,2);
v_child_price numeric(14,2);
v_season public.seasons%ROWTYPE;
v_target_user public.user_profiles%ROWTYPE;
v_active_child_count integer := 0;
v_installments_json jsonb;
BEGIN
IF auth.uid() IS NULL THEN RAISE EXCEPTION 'Authentication required.'; END IF;
IF p_request_id IS NULL THEN RAISE EXCEPTION 'Request ID is required.'; END IF;
IF p_installment_count IS NULL OR p_installment_count NOT IN (1, 2, 3) THEN RAISE EXCEPTION 'Installment count must be 1, 2, or 3.'; END IF;
IF (p_booking_id IS NULL) = (p_recurring_series_id IS NULL) THEN RAISE EXCEPTION 'Exactly one of booking_id or recurring_series_id must be provided.'; END IF;
IF p_booking_id IS NOT NULL AND p_installment_count != 1 THEN RAISE EXCEPTION 'One-time bookings may only have 1 installment.'; END IF;
PERFORM pg_advisory_xact_lock(hashtextextended(p_request_id::text, 0));
SELECT * INTO v_existing_plan FROM public.venue_payment_plans WHERE creation_request_id = p_request_id;
IF v_existing_plan.id IS NOT NULL THEN
IF v_existing_plan.installment_count = p_installment_count AND
(v_existing_plan.booking_id = p_booking_id OR (v_existing_plan.booking_id IS NULL AND p_booking_id IS NULL)) AND
(v_existing_plan.recurring_series_id = p_recurring_series_id OR (v_existing_plan.recurring_series_id IS NULL AND p_recurring_series_id IS NULL)) THEN
SELECT jsonb_agg(jsonb_build_object(
'installment_id', id, 'installment_number', installment_number, 'amount', amount, 'currency', currency, 'due_date', due_date, 'status', status, 'current_paid_event_id', current_paid_event_id
) ORDER BY installment_number) INTO v_installments_json FROM public.venue_payment_installments WHERE plan_id = v_existing_plan.id;
RETURN jsonb_build_object('plan_id', v_existing_plan.id, 'booking_id', v_existing_plan.booking_id, 'recurring_series_id', v_existing_plan.recurring_series_id, 'user_profile_id', v_existing_plan.user_profile_id, 'total_amount', v_existing_plan.total_amount, 'currency', v_existing_plan.currency, 'installment_count', v_existing_plan.installment_count, 'plan_status', v_existing_plan.plan_status, 'paid_total', (SELECT COALESCE(SUM(amount), 0) FROM public.venue_payment_installments WHERE plan_id = v_existing_plan.id AND status = 'paid'), 'remaining_total', v_existing_plan.total_amount - (SELECT COALESCE(SUM(amount), 0) FROM public.venue_payment_installments WHERE plan_id = v_existing_plan.id AND status = 'paid'), 'installments', v_installments_json, 'already_processed', true);
ELSE
RAISE EXCEPTION 'Idempotency key collision.';
END IF;
END IF;
IF p_booking_id IS NOT NULL THEN
SELECT club_id INTO v_target_club_id FROM public.bookings WHERE id = p_booking_id;
ELSE
SELECT club_id INTO v_target_club_id FROM public.recurring_booking_series WHERE id = p_recurring_series_id;
END IF;
IF v_target_club_id IS NULL THEN RAISE EXCEPTION 'Target booking or series not found.'; END IF;
SELECT * INTO v_caller_profile FROM public.user_profiles WHERE auth_user_id = auth.uid() AND club_id = v_target_club_id;
IF v_caller_profile.id IS NULL THEN RAISE EXCEPTION 'Caller profile not found in target club.'; END IF;
IF v_caller_profile.role NOT IN ('owner', 'admin', 'secondary_admin') THEN RAISE EXCEPTION 'Unauthorized role.'; END IF;
IF p_booking_id IS NOT NULL THEN
SELECT * INTO v_booking FROM public.bookings WHERE id = p_booking_id FOR UPDATE;
IF v_booking.booking_type != 'one-time' THEN RAISE EXCEPTION 'Booking is not one-time.'; END IF;
IF v_booking.deleted_at IS NOT NULL THEN RAISE EXCEPTION 'Booking is deleted.'; END IF;
IF v_booking.status IN ('cancelled', 'deleted by club', 'failed') THEN RAISE EXCEPTION 'Booking is cancelled or failed.'; END IF;
IF v_booking.payment_method IS DISTINCT FROM 'venue' THEN RAISE EXCEPTION 'Payment method is not venue.'; END IF;
IF v_booking.payment_status IS DISTINCT FROM 'pending' THEN RAISE EXCEPTION 'Payment status is not pending.'; END IF;
IF v_booking.club_id IS NULL OR v_booking.season_id IS NULL THEN RAISE EXCEPTION 'Booking must have club and season.'; END IF;
SELECT * INTO v_season FROM public.seasons WHERE id = v_booking.season_id;
IF v_season.id IS NULL OR v_season.club_id != v_target_club_id THEN RAISE EXCEPTION 'Invalid season.'; END IF;
IF v_booking.currency IS DISTINCT FROM v_season.currency THEN RAISE EXCEPTION 'Booking currency does not match season currency.'; END IF;
-- COURT-AWARE one-time price (uses the booking's own court)
v_total_amount := public.calculate_booking_price(v_booking.club_id, v_booking.season_id, v_booking.booking_date, v_booking.start_time, v_booking.end_time, v_booking.pricing_mode, v_booking.currency, v_booking.court_id);
v_currency := v_booking.currency;
v_user_profile_id := v_booking.user_id;
v_customer_name := v_booking.booking_name;
v_customer_email := v_booking.email;
v_customer_phone := v_booking.phone;
v_season_id := v_booking.season_id;
ELSE
SELECT * INTO v_series FROM public.recurring_booking_series WHERE id = p_recurring_series_id FOR UPDATE;
IF v_series.status != 'active' THEN RAISE EXCEPTION 'Series is not active.'; END IF;
IF v_series.currency IS NULL OR v_series.pricing_mode IS NULL OR TRIM(v_series.pricing_mode) = '' THEN RAISE EXCEPTION 'Series pricing/currency incomplete.'; END IF;
SELECT * INTO v_season FROM public.seasons WHERE id = v_series.season_id;
IF v_season.id IS NULL OR v_season.club_id != v_target_club_id THEN RAISE EXCEPTION 'Invalid season.'; END IF;
IF v_series.currency IS DISTINCT FROM v_season.currency THEN RAISE EXCEPTION 'Series currency does not match season currency.'; END IF;
-- COURT-AWARE recurring child price: each child priced by its own court
FOR v_child_booking IN SELECT * FROM public.bookings WHERE recurring_series_id = p_recurring_series_id FOR UPDATE LOOP
IF v_child_booking.deleted_at IS NOT NULL OR v_child_booking.status IN ('cancelled', 'deleted by club', 'failed') THEN RAISE EXCEPTION 'Child booking is inactive at plan creation.'; END IF;
IF v_child_booking.booking_type != 'recurring' THEN RAISE EXCEPTION 'Child booking type invalid.'; END IF;
IF v_child_booking.club_id != v_series.club_id OR v_child_booking.season_id != v_series.season_id THEN RAISE EXCEPTION 'Child booking scope mismatch.'; END IF;
IF v_child_booking.user_id IS DISTINCT FROM v_series.user_profile_id THEN RAISE EXCEPTION 'Child booking user mismatch.'; END IF;
IF v_child_booking.currency IS DISTINCT FROM v_series.currency THEN RAISE EXCEPTION 'Child booking currency mismatch.'; END IF;
IF v_child_booking.pricing_mode IS DISTINCT FROM v_series.pricing_mode THEN RAISE EXCEPTION 'Child booking pricing mode mismatch.'; END IF;
IF v_child_booking.payment_method IS DISTINCT FROM 'venue' OR v_child_booking.payment_status IS DISTINCT FROM 'pending' THEN RAISE EXCEPTION 'Child booking payment state invalid.'; END IF;
v_active_child_count := v_active_child_count + 1;
v_child_price := public.calculate_booking_price(v_child_booking.club_id, v_child_booking.season_id, v_child_booking.booking_date, v_child_booking.start_time, v_child_booking.end_time, v_child_booking.pricing_mode, v_child_booking.currency, v_child_booking.court_id);
v_total_amount := v_total_amount + v_child_price;
END LOOP;
IF v_active_child_count = 0 THEN RAISE EXCEPTION 'No active child bookings found for series.'; END IF;
IF v_active_child_count != v_series.total_occurrences THEN RAISE EXCEPTION 'Active child count does not match series total occurrences.'; END IF;
IF v_total_amount != v_series.total_amount THEN RAISE EXCEPTION 'Calculated child bookings total does not match series total_amount.'; END IF;
v_currency := v_series.currency;
v_user_profile_id := v_series.user_profile_id;
v_customer_name := v_series.booking_name;
v_customer_email := v_series.customer_email;
v_customer_phone := v_series.customer_phone;
v_season_id := v_series.season_id;
END IF;
IF v_user_profile_id IS NOT NULL THEN
SELECT * INTO v_target_user FROM public.user_profiles WHERE id = v_user_profile_id;
IF v_target_user.id IS NULL OR v_target_user.club_id != v_target_club_id THEN RAISE EXCEPTION 'Target user profile not found or belongs to a different club.'; END IF;
END IF;
IF EXISTS (SELECT 1 FROM public.venue_payment_plans WHERE booking_id = p_booking_id) THEN RAISE EXCEPTION 'A payment plan already exists for this booking.'; END IF;
IF EXISTS (SELECT 1 FROM public.venue_payment_plans WHERE recurring_series_id = p_recurring_series_id) THEN RAISE EXCEPTION 'A payment plan already exists for this series.'; END IF;
IF v_currency NOT IN ('HUF', 'EUR', 'USD') THEN RAISE EXCEPTION 'Invalid currency.'; END IF;
IF v_total_amount <= 0 OR v_total_amount = 'NaN'::numeric OR v_total_amount = 'Infinity'::numeric OR v_total_amount = '-Infinity'::numeric THEN RAISE EXCEPTION 'Invalid total amount.'; END IF;
IF v_currency = 'HUF' AND v_total_amount != ROUND(v_total_amount) THEN RAISE EXCEPTION 'HUF amount must be a whole number.'; END IF;
IF v_currency = 'HUF' THEN
v_base_amount := FLOOR(v_total_amount / p_installment_count);
v_remainder := v_total_amount - (v_base_amount * p_installment_count);
ELSE
v_base_amount := TRUNC(v_total_amount / p_installment_count, 2);
v_remainder := v_total_amount - (v_base_amount * p_installment_count);
END IF;
INSERT INTO public.venue_payment_plans (
creation_request_id, club_id, season_id, booking_id, recurring_series_id, user_profile_id,
customer_name, customer_email, customer_phone, payment_method, total_amount, currency, installment_count, created_by_admin_id
) VALUES (
p_request_id, v_target_club_id, v_season_id, p_booking_id, p_recurring_series_id, v_user_profile_id,
v_customer_name, v_customer_email, v_customer_phone, 'venue', v_total_amount, v_currency, p_installment_count, v_caller_profile.id
) RETURNING id INTO v_inserted_plan_id;
FOR v_i IN 1..p_installment_count LOOP
IF v_i = 1 THEN
v_inst_amount := v_base_amount + v_remainder;
ELSE
v_inst_amount := v_base_amount;
END IF;
INSERT INTO public.venue_payment_installments (plan_id, installment_number, amount, currency) VALUES (v_inserted_plan_id, v_i, v_inst_amount, v_currency);
END LOOP;
SELECT jsonb_agg(jsonb_build_object(
'installment_id', id, 'installment_number', installment_number, 'amount', amount, 'currency', currency, 'due_date', due_date, 'status', status, 'current_paid_event_id', current_paid_event_id
) ORDER BY installment_number) INTO v_installments_json FROM public.venue_payment_installments WHERE plan_id = v_inserted_plan_id;
RETURN jsonb_build_object('plan_id', v_inserted_plan_id, 'booking_id', p_booking_id, 'recurring_series_id', p_recurring_series_id, 'user_profile_id', v_user_profile_id, 'total_amount', v_total_amount, 'currency', v_currency, 'installment_count', p_installment_count, 'plan_status', 'pending', 'paid_total', 0, 'remaining_total', v_total_amount, 'installments', v_installments_json, 'already_processed', false);
END;
$function$
"
public,admin_deduct_credit,f,"p_user_profile_id uuid, p_season_id uuid, p_credit_type text, p_amount numeric, p_reason text","p_user_profile_id uuid, p_season_id uuid, p_credit_type text, p_amount numeric, p_reason text DEFAULT NULL::text",void,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.admin_deduct_credit(p_user_profile_id uuid, p_season_id uuid, p_credit_type text, p_amount numeric, p_reason text DEFAULT NULL::text)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_club_id uuid;
v_currency text;
v_wallet_id uuid;
v_current_balance numeric;
v_caller_role text;
v_caller_club_id uuid;
v_caller_profile_id uuid;
v_season_end_date date;
v_club_timezone text;
v_season_end_timestamp timestamptz;
BEGIN
-- ----------------------------------------------------------
-- Authentication
-- ----------------------------------------------------------
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Not authenticated.';
END IF;
IF p_user_profile_id IS NULL THEN
RAISE EXCEPTION 'Target user profile ID is required.';
END IF;
IF p_season_id IS NULL THEN
RAISE EXCEPTION 'Season ID is required.';
END IF;
SELECT
id,
role,
club_id
INTO
v_caller_profile_id,
v_caller_role,
v_caller_club_id
FROM public.user_profiles
WHERE auth_user_id = auth.uid()
LIMIT 1;
IF v_caller_profile_id IS NULL THEN
RAISE EXCEPTION 'Caller profile not found.';
END IF;
-- ----------------------------------------------------------
-- Admin authorization
-- ----------------------------------------------------------
IF v_caller_role NOT IN (
'owner',
'admin',
'secondary_admin'
) THEN
RAISE EXCEPTION 'Unauthorized role.';
END IF;
-- ----------------------------------------------------------
-- Target user + club isolation
-- ----------------------------------------------------------
SELECT club_id
INTO v_club_id
FROM public.user_profiles
WHERE id = p_user_profile_id
LIMIT 1;
IF v_club_id IS NULL
OR v_club_id IS DISTINCT FROM v_caller_club_id
THEN
RAISE EXCEPTION
'Target user not found or belongs to a different club.';
END IF;
-- ----------------------------------------------------------
-- Season validation
-- ----------------------------------------------------------
SELECT
currency,
end_date
INTO
v_currency,
v_season_end_date
FROM public.seasons
WHERE id = p_season_id
AND club_id = v_club_id
LIMIT 1;
IF v_currency IS NULL THEN
RAISE EXCEPTION
'Season not found or belongs to a different club.';
END IF;
SELECT timezone
INTO v_club_timezone
FROM public.clubs
WHERE id = v_club_id;
v_season_end_timestamp :=
(
v_season_end_date::text || ' 23:59:59'
)::timestamp
AT TIME ZONE COALESCE(v_club_timezone, 'UTC');
IF now() > v_season_end_timestamp THEN
RAISE EXCEPTION
'Cannot deduct credit for an expired season.';
END IF;
-- ----------------------------------------------------------
-- Credit type validation
-- ----------------------------------------------------------
IF p_credit_type IS NULL
OR p_credit_type NOT IN (
'standard',
'season_pass'
)
THEN
RAISE EXCEPTION 'Invalid credit type.';
END IF;
-- ----------------------------------------------------------
-- Amount validation
-- ----------------------------------------------------------
IF p_amount IS NULL
OR p_amount <= 0
OR p_amount = 'NaN'::numeric
OR p_amount = 'Infinity'::numeric
OR p_amount = '-Infinity'::numeric
THEN
RAISE EXCEPTION
'Amount must be a finite positive number.';
END IF;
IF p_amount != ROUND(p_amount, 2) THEN
RAISE EXCEPTION
'Maximum two decimal places allowed.';
END IF;
IF v_currency = 'HUF'
AND p_amount != TRUNC(p_amount)
THEN
RAISE EXCEPTION
'HUF credit amounts must be whole numbers.';
END IF;
-- ----------------------------------------------------------
-- Lock exact wallet
-- ----------------------------------------------------------
SELECT
id,
balance
INTO
v_wallet_id,
v_current_balance
FROM public.credit_wallets
WHERE club_id = v_club_id
AND user_profile_id = p_user_profile_id
AND season_id = p_season_id
AND currency = v_currency
AND credit_type = p_credit_type
FOR UPDATE;
IF v_wallet_id IS NULL THEN
RAISE EXCEPTION
'No credit wallet found for this user, season, and credit type.';
END IF;
-- ----------------------------------------------------------
-- Balance validation
-- ----------------------------------------------------------
IF v_current_balance IS NULL
OR v_current_balance = 'NaN'::numeric
OR v_current_balance = 'Infinity'::numeric
OR v_current_balance = '-Infinity'::numeric
THEN
RAISE EXCEPTION
'Invalid current credit balance.';
END IF;
IF v_current_balance < p_amount THEN
RAISE EXCEPTION
'Insufficient credit balance. Current balance: %, requested deduction: %.',
v_current_balance,
p_amount;
END IF;
-- ----------------------------------------------------------
-- Deduct wallet balance
-- ----------------------------------------------------------
UPDATE public.credit_wallets
SET
balance = balance - p_amount,
updated_at = now()
WHERE id = v_wallet_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'Credit wallet update failed.';
END IF;
-- ----------------------------------------------------------
-- Audit transaction
-- ----------------------------------------------------------
INSERT INTO public.credit_transactions (
wallet_id,
club_id,
user_profile_id,
season_id,
credit_type,
currency,
amount,
transaction_category,
reason,
created_by
)
VALUES (
v_wallet_id,
v_club_id,
p_user_profile_id,
p_season_id,
p_credit_type,
v_currency,
-p_amount,
'admin_deduction',
COALESCE(
NULLIF(BTRIM(p_reason), ''),
'Credit deducted by administrator'
),
v_caller_profile_id
);
END;
$function$
"
public,admin_grant_credit,f,"p_user_profile_id uuid, p_season_id uuid, p_credit_type text, p_amount numeric, p_reason text","p_user_profile_id uuid, p_season_id uuid, p_credit_type text, p_amount numeric, p_reason text DEFAULT NULL::text",void,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.admin_grant_credit(p_user_profile_id uuid, p_season_id uuid, p_credit_type text, p_amount numeric, p_reason text DEFAULT NULL::text)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_target_club_id uuid;
v_currency text;
v_wallet_id uuid;
v_caller_role text;
v_caller_club_id uuid;
v_caller_profile_id uuid;
v_season_end_date date;
v_club_timezone text;
v_season_end_timestamp timestamptz;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Not authenticated.';
END IF;
SELECT
up.id,
up.role,
up.club_id
INTO
v_caller_profile_id,
v_caller_role,
v_caller_club_id
FROM public.user_profiles AS up
WHERE up.auth_user_id = auth.uid()
LIMIT 1;
IF v_caller_profile_id IS NULL THEN
RAISE EXCEPTION 'Caller profile not found.';
END IF;
IF v_caller_role NOT IN (
'owner',
'admin',
'secondary_admin'
) THEN
RAISE EXCEPTION 'Unauthorized role.';
END IF;
SELECT up.club_id
INTO v_target_club_id
FROM public.user_profiles AS up
WHERE up.id = p_user_profile_id
LIMIT 1;
IF v_target_club_id IS NULL
OR v_target_club_id != v_caller_club_id THEN
RAISE EXCEPTION
'Target user not found or belongs to a different club.';
END IF;
SELECT
s.currency,
s.end_date
INTO
v_currency,
v_season_end_date
FROM public.seasons AS s
WHERE s.id = p_season_id
AND s.club_id = v_target_club_id
LIMIT 1;
IF v_currency IS NULL THEN
RAISE EXCEPTION
'Season not found or belongs to a different club.';
END IF;
IF v_currency NOT IN ('HUF', 'EUR', 'USD') THEN
RAISE EXCEPTION 'Unsupported season currency.';
END IF;
SELECT c.timezone
INTO v_club_timezone
FROM public.clubs AS c
WHERE c.id = v_target_club_id;
v_season_end_timestamp :=
(
v_season_end_date::text || ' 23:59:59'
)::timestamp
AT TIME ZONE COALESCE(v_club_timezone, 'UTC');
IF now() > v_season_end_timestamp THEN
RAISE EXCEPTION
'Cannot grant credit for an expired season.';
END IF;
IF p_credit_type IS NULL
OR p_credit_type NOT IN ('standard', 'season_pass') THEN
RAISE EXCEPTION 'Invalid credit type.';
END IF;
IF p_amount IS NULL
OR p_amount <= 0
OR p_amount = 'NaN'::numeric
OR p_amount = 'Infinity'::numeric
OR p_amount = '-Infinity'::numeric THEN
RAISE EXCEPTION
'Amount must be a finite positive number.';
END IF;
IF p_amount != ROUND(p_amount, 2) THEN
RAISE EXCEPTION
'Maximum two decimal places allowed.';
END IF;
IF v_currency = 'HUF'
AND p_amount != TRUNC(p_amount) THEN
RAISE EXCEPTION
'HUF credit amounts must be whole numbers.';
END IF;
INSERT INTO public.credit_wallets (
club_id,
user_profile_id,
season_id,
currency,
credit_type,
balance
)
VALUES (
v_target_club_id,
p_user_profile_id,
p_season_id,
v_currency,
p_credit_type,
p_amount
)
ON CONFLICT (
club_id,
user_profile_id,
season_id,
currency,
credit_type
)
DO UPDATE
SET
balance = public.credit_wallets.balance + EXCLUDED.balance,
updated_at = now()
RETURNING id INTO v_wallet_id;
INSERT INTO public.credit_transactions (
wallet_id,
club_id,
user_profile_id,
season_id,
credit_type,
currency,
amount,
transaction_category,
reason,
created_by
)
VALUES (
v_wallet_id,
v_target_club_id,
p_user_profile_id,
p_season_id,
p_credit_type,
v_currency,
p_amount,
'admin_grant',
COALESCE(
NULLIF(BTRIM(p_reason), ''),
'Credit granted by administrator'
),
v_caller_profile_id
);
END;
$function$
"
public,admin_link_venue_payment_plan_user,f,"p_plan_id uuid, p_user_profile_id uuid, p_request_id uuid","p_plan_id uuid, p_user_profile_id uuid, p_request_id uuid",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.admin_link_venue_payment_plan_user(p_plan_id uuid, p_user_profile_id uuid, p_request_id uuid)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_profile public.user_profiles%ROWTYPE;
v_target_profile public.user_profiles%ROWTYPE;
v_plan public.venue_payment_plans%ROWTYPE;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Authentication required.';
END IF;
IF p_plan_id IS NULL OR p_user_profile_id IS NULL OR p_request_id IS NULL THEN
RAISE EXCEPTION 'Required parameters missing.';
END IF;
PERFORM pg_advisory_xact_lock(hashtextextended(p_request_id::text, 0));
SELECT *
INTO v_plan
FROM public.venue_payment_plans
WHERE id = p_plan_id
FOR UPDATE;
IF v_plan.id IS NULL THEN
RAISE EXCEPTION 'Plan not found.';
END IF;
IF v_plan.link_request_id = p_request_id THEN
IF v_plan.user_profile_id = p_user_profile_id THEN
RETURN jsonb_build_object(
'plan_id', p_plan_id,
'user_profile_id', p_user_profile_id,
'linked_by_admin_id', v_plan.linked_by_admin_id,
'linked_at', v_plan.linked_at,
'already_processed', true
);
ELSE
RAISE EXCEPTION 'Idempotency key collision.';
END IF;
END IF;
SELECT *
INTO v_caller_profile
FROM public.user_profiles
WHERE auth_user_id = auth.uid()
AND club_id = v_plan.club_id;
IF v_caller_profile.id IS NULL
OR v_caller_profile.role NOT IN ('owner', 'admin', 'secondary_admin') THEN
RAISE EXCEPTION 'Unauthorized.';
END IF;
IF v_plan.user_profile_id IS NOT NULL THEN
RAISE EXCEPTION 'Plan is already linked to a user.';
END IF;
SELECT *
INTO v_target_profile
FROM public.user_profiles
WHERE id = p_user_profile_id
AND club_id = v_plan.club_id;
IF v_target_profile.id IS NULL THEN
RAISE EXCEPTION 'Target user profile not found or belongs to a different club.';
END IF;
UPDATE public.venue_payment_plans
SET user_profile_id = p_user_profile_id,
linked_by_admin_id = v_caller_profile.id,
linked_at = now(),
link_request_id = p_request_id,
updated_at = now()
WHERE id = p_plan_id;
RETURN jsonb_build_object(
'plan_id', p_plan_id,
'user_profile_id', p_user_profile_id,
'linked_by_admin_id', v_caller_profile.id,
'linked_at', now(),
'already_processed', false
);
END;
$function$
"
public,admin_mark_venue_installment_paid,f,"p_installment_id uuid, p_request_id uuid, p_payment_note text","p_installment_id uuid, p_request_id uuid, p_payment_note text DEFAULT NULL::text",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.admin_mark_venue_installment_paid(p_installment_id uuid, p_request_id uuid, p_payment_note text DEFAULT NULL::text)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_profile public.user_profiles%ROWTYPE;
v_inst public.venue_payment_installments%ROWTYPE;
v_plan public.venue_payment_plans%ROWTYPE;
v_paid_total numeric(14,2);
v_remaining_total numeric(14,2);
v_existing_event public.venue_payment_installment_events%ROWTYPE;
v_inserted_event_id uuid;
v_active_target boolean := false;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Authentication required.';
END IF;
IF p_installment_id IS NULL OR p_request_id IS NULL THEN
RAISE EXCEPTION 'IDs required.';
END IF;
PERFORM pg_advisory_xact_lock(hashtextextended(p_request_id::text, 0));
SELECT *
INTO v_existing_event
FROM public.venue_payment_installment_events
WHERE request_id = p_request_id;
IF v_existing_event.id IS NOT NULL THEN
IF v_existing_event.installment_id = p_installment_id
AND v_existing_event.event_type = 'paid' THEN
SELECT *
INTO v_inst
FROM public.venue_payment_installments
WHERE id = p_installment_id;
IF v_inst.current_paid_event_id = v_existing_event.id
AND v_inst.status = 'paid' THEN
SELECT *
INTO v_plan
FROM public.venue_payment_plans
WHERE id = v_inst.plan_id;
SELECT COALESCE(SUM(amount), 0)
INTO v_paid_total
FROM public.venue_payment_installments
WHERE plan_id = v_plan.id
AND status = 'paid';
v_remaining_total := v_plan.total_amount - v_paid_total;
RETURN jsonb_build_object(
'plan_id', v_plan.id,
'installment_id', v_inst.id,
'installment_number', v_inst.installment_number,
'installment_amount', v_inst.amount,
'installment_status', v_inst.status,
'paid_event_id', v_existing_event.id,
'plan_status', v_plan.plan_status,
'paid_total', v_paid_total,
'remaining_total', v_remaining_total,
'currency', v_plan.currency,
'fully_paid', (v_plan.plan_status = 'paid'),
'already_processed', true
);
END IF;
END IF;
RAISE EXCEPTION 'Idempotency key collision.';
END IF;
SELECT *
INTO v_inst
FROM public.venue_payment_installments
WHERE id = p_installment_id
FOR UPDATE;
IF v_inst.id IS NULL THEN
RAISE EXCEPTION 'Installment not found.';
END IF;
SELECT *
INTO v_plan
FROM public.venue_payment_plans
WHERE id = v_inst.plan_id
FOR UPDATE;
IF v_plan.id IS NULL THEN
RAISE EXCEPTION 'Plan not found.';
END IF;
SELECT *
INTO v_caller_profile
FROM public.user_profiles
WHERE auth_user_id = auth.uid()
AND club_id = v_plan.club_id;
IF v_caller_profile.id IS NULL
OR v_caller_profile.role NOT IN ('owner', 'admin', 'secondary_admin') THEN
RAISE EXCEPTION 'Unauthorized.';
END IF;
IF v_plan.plan_status = 'cancelled' THEN
RAISE EXCEPTION 'Plan is cancelled.';
END IF;
IF v_inst.status = 'paid' OR v_inst.current_paid_event_id IS NOT NULL THEN
RAISE EXCEPTION 'Installment is already paid.';
END IF;
IF v_inst.status != 'pending' THEN
RAISE EXCEPTION 'Only pending installments can be marked paid.';
END IF;
IF v_plan.booking_id IS NOT NULL THEN
IF EXISTS (
SELECT 1
FROM public.bookings
WHERE id = v_plan.booking_id
AND deleted_at IS NULL
AND status NOT IN ('cancelled', 'deleted by club', 'failed')
AND payment_method = 'venue'
AND club_id = v_plan.club_id
AND season_id = v_plan.season_id
) THEN
v_active_target := true;
END IF;
ELSIF v_plan.recurring_series_id IS NOT NULL THEN
IF EXISTS (
SELECT 1
FROM public.recurring_booking_series
WHERE id = v_plan.recurring_series_id
AND status = 'active'
)
AND EXISTS (
SELECT 1
FROM public.bookings
WHERE recurring_series_id = v_plan.recurring_series_id
AND deleted_at IS NULL
AND status NOT IN ('cancelled', 'deleted by club', 'failed')
AND payment_method = 'venue'
AND club_id = v_plan.club_id
AND season_id = v_plan.season_id
) THEN
v_active_target := true;
END IF;
END IF;
IF NOT v_active_target THEN
RAISE EXCEPTION 'Target booking or series is no longer active.';
END IF;
INSERT INTO public.venue_payment_installment_events (
installment_id,
request_id,
event_type,
amount,
currency,
performed_by_admin_id,
note,
reverses_event_id
)
VALUES (
v_inst.id,
p_request_id,
'paid',
v_inst.amount,
v_inst.currency,
v_caller_profile.id,
NULLIF(BTRIM(p_payment_note), ''),
NULL
)
RETURNING id INTO v_inserted_event_id;
UPDATE public.venue_payment_installments
SET status = 'paid',
current_paid_event_id = v_inserted_event_id,
updated_at = now()
WHERE id = p_installment_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Failed to update installment.';
END IF;
PERFORM public.internal_update_venue_plan_status(v_plan.id);
SELECT *
INTO v_plan
FROM public.venue_payment_plans
WHERE id = v_plan.id;
SELECT COALESCE(SUM(amount), 0)
INTO v_paid_total
FROM public.venue_payment_installments
WHERE plan_id = v_plan.id
AND status = 'paid';
v_remaining_total := v_plan.total_amount - v_paid_total;
RETURN jsonb_build_object(
'plan_id', v_plan.id,
'installment_id', v_inst.id,
'installment_number', v_inst.installment_number,
'installment_amount', v_inst.amount,
'installment_status', 'paid',
'paid_event_id', v_inserted_event_id,
'plan_status', v_plan.plan_status,
'paid_total', v_paid_total,
'remaining_total', v_remaining_total,
'currency', v_plan.currency,
'fully_paid', (v_plan.plan_status = 'paid'),
'already_processed', false
);
END;
$function$
"
public,admin_reverse_venue_installment_payment,f,"p_installment_id uuid, p_request_id uuid, p_reversal_reason text","p_installment_id uuid, p_request_id uuid, p_reversal_reason text",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.admin_reverse_venue_installment_payment(p_installment_id uuid, p_request_id uuid, p_reversal_reason text)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_profile public.user_profiles%ROWTYPE;
v_inst public.venue_payment_installments%ROWTYPE;
v_plan public.venue_payment_plans%ROWTYPE;
v_paid_total numeric(14,2);
v_remaining_total numeric(14,2);
v_existing_event public.venue_payment_installment_events%ROWTYPE;
v_inserted_event_id uuid;
v_paid_event public.venue_payment_installment_events%ROWTYPE;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Authentication required.';
END IF;
IF p_installment_id IS NULL OR p_request_id IS NULL THEN
RAISE EXCEPTION 'IDs required.';
END IF;
IF NULLIF(BTRIM(p_reversal_reason), '') IS NULL THEN
RAISE EXCEPTION 'Reversal reason required.';
END IF;
PERFORM pg_advisory_xact_lock(hashtextextended(p_request_id::text, 0));
SELECT *
INTO v_existing_event
FROM public.venue_payment_installment_events
WHERE request_id = p_request_id;
IF v_existing_event.id IS NOT NULL THEN
IF v_existing_event.installment_id = p_installment_id
AND v_existing_event.event_type = 'reversed' THEN
SELECT *
INTO v_inst
FROM public.venue_payment_installments
WHERE id = p_installment_id;
IF v_inst.status = 'pending'
AND v_inst.current_paid_event_id IS NULL THEN
SELECT *
INTO v_plan
FROM public.venue_payment_plans
WHERE id = v_inst.plan_id;
SELECT COALESCE(SUM(amount), 0)
INTO v_paid_total
FROM public.venue_payment_installments
WHERE plan_id = v_plan.id
AND status = 'paid';
v_remaining_total := v_plan.total_amount - v_paid_total;
RETURN jsonb_build_object(
'plan_id', v_plan.id,
'installment_status', v_inst.status,
'reversal_event_id', v_existing_event.id,
'reversed_paid_event_id', v_existing_event.reverses_event_id,
'plan_status', v_plan.plan_status,
'paid_total', v_paid_total,
'remaining_total', v_remaining_total,
'already_processed', true
);
END IF;
END IF;
RAISE EXCEPTION 'Idempotency key collision.';
END IF;
SELECT *
INTO v_inst
FROM public.venue_payment_installments
WHERE id = p_installment_id
FOR UPDATE;
IF v_inst.id IS NULL THEN
RAISE EXCEPTION 'Installment not found.';
END IF;
SELECT *
INTO v_plan
FROM public.venue_payment_plans
WHERE id = v_inst.plan_id
FOR UPDATE;
IF v_plan.id IS NULL THEN
RAISE EXCEPTION 'Plan not found.';
END IF;
SELECT *
INTO v_caller_profile
FROM public.user_profiles
WHERE auth_user_id = auth.uid()
AND club_id = v_plan.club_id;
IF v_caller_profile.id IS NULL
OR v_caller_profile.role NOT IN ('owner', 'admin', 'secondary_admin') THEN
RAISE EXCEPTION 'Unauthorized.';
END IF;
IF v_plan.plan_status = 'cancelled' THEN
RAISE EXCEPTION 'Plan is cancelled.';
END IF;
IF v_inst.status != 'paid' OR v_inst.current_paid_event_id IS NULL THEN
RAISE EXCEPTION 'Only paid installments can be reversed.';
END IF;
SELECT *
INTO v_paid_event
FROM public.venue_payment_installment_events
WHERE id = v_inst.current_paid_event_id
FOR UPDATE;
IF v_paid_event.id IS NULL
OR v_paid_event.installment_id != v_inst.id
OR v_paid_event.amount != v_inst.amount
OR v_paid_event.currency IS DISTINCT FROM v_inst.currency
OR v_paid_event.event_type != 'paid'
OR v_paid_event.reverses_event_id IS NOT NULL THEN
RAISE EXCEPTION 'Invalid paid event state.';
END IF;
INSERT INTO public.venue_payment_installment_events (
installment_id,
request_id,
event_type,
amount,
currency,
performed_by_admin_id,
note,
reverses_event_id
)
VALUES (
v_inst.id,
p_request_id,
'reversed',
v_inst.amount,
v_inst.currency,
v_caller_profile.id,
NULLIF(BTRIM(p_reversal_reason), ''),
v_paid_event.id
)
RETURNING id INTO v_inserted_event_id;
UPDATE public.venue_payment_installments
SET status = 'pending',
current_paid_event_id = NULL,
updated_at = now()
WHERE id = p_installment_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Failed to update installment.';
END IF;
PERFORM public.internal_update_venue_plan_status(v_plan.id);
SELECT *
INTO v_plan
FROM public.venue_payment_plans
WHERE id = v_plan.id;
SELECT COALESCE(SUM(amount), 0)
INTO v_paid_total
FROM public.venue_payment_installments
WHERE plan_id = v_plan.id
AND status = 'paid';
v_remaining_total := v_plan.total_amount - v_paid_total;
RETURN jsonb_build_object(
'plan_id', v_plan.id,
'installment_status', 'pending',
'reversal_event_id', v_inserted_event_id,
'reversed_paid_event_id', v_paid_event.id,
'plan_status', v_plan.plan_status,
'paid_total', v_paid_total,
'remaining_total', v_remaining_total,
'already_processed', false
);
END;
$function$
"
public,admin_update_supported_player,f,"p_user_profile_id uuid, p_is_supported boolean","p_user_profile_id uuid, p_is_supported boolean",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.admin_update_supported_player(p_user_profile_id uuid, p_is_supported boolean)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public'
AS $function$
DECLARE
v_caller_role text;
v_caller_club_id uuid;
v_target_club_id uuid;
v_updated_id uuid;
v_updated_is_supported boolean;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Not authenticated.';
END IF;
SELECT
role::text,
club_id
INTO
v_caller_role,
v_caller_club_id
FROM public.user_profiles
WHERE auth_user_id = auth.uid()
LIMIT 1;
IF NOT FOUND THEN
RAISE EXCEPTION 'Caller profile not found.';
END IF;
IF v_caller_role NOT IN ('owner', 'admin') THEN
RAISE EXCEPTION 'Unauthorized to manage supported players.';
END IF;
SELECT club_id
INTO v_target_club_id
FROM public.user_profiles
WHERE id = p_user_profile_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Target user not found.';
END IF;
IF v_target_club_id IS DISTINCT FROM v_caller_club_id THEN
RAISE EXCEPTION 'Cannot modify users in another club.';
END IF;
UPDATE public.user_profiles
SET is_supported_player = p_is_supported
WHERE id = p_user_profile_id
RETURNING
id,
is_supported_player
INTO
v_updated_id,
v_updated_is_supported;
IF NOT FOUND THEN
RAISE EXCEPTION 'Supported player status could not be updated.';
END IF;
RETURN jsonb_build_object(
'id', v_updated_id,
'is_supported_player', v_updated_is_supported
);
END;
$function$
"
public,apply_group_court_cost_to_academy_batch,f,,,trigger,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.apply_group_court_cost_to_academy_batch()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_default_fee numeric(12, 2);
BEGIN
IF NEW.academy_type <> 'group' THEN
RETURN NEW;
END IF;
IF NEW.academy_program_id IS NULL THEN
RAISE EXCEPTION
'Csoportos foglaláshoz akadémiai csoport szükséges.';
END IF;
SELECT s.default_court_fee_huf
INTO v_default_fee
FROM public.academy_group_court_cost_settings s
JOIN public.academy_programs ap
ON ap.id = s.academy_program_id
AND ap.club_id = s.club_id
AND ap.program_type = 'group'
WHERE s.academy_program_id = NEW.academy_program_id
AND s.club_id = NEW.club_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'A csoport pályaköltsége nincs beállítva.';
END IF;
NEW.court_fee_huf_per_occurrence := v_default_fee;
RETURN NEW;
END;
$function$
"
public,apply_group_court_cost_to_academy_detail,f,,,trigger,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.apply_group_court_cost_to_academy_detail()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_batch_fee numeric(12, 2);
BEGIN
IF NEW.academy_type <> 'group' THEN
RETURN NEW;
END IF;
SELECT abb.court_fee_huf_per_occurrence
INTO v_batch_fee
FROM public.academy_booking_batches abb
WHERE abb.id = NEW.academy_booking_batch_id
AND abb.club_id = NEW.club_id
AND abb.academy_type = 'group'
AND abb.academy_program_id = NEW.academy_program_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'A csoportos foglalási csomag nem található vagy hibás.';
END IF;
NEW.court_fee_huf := v_batch_fee;
RETURN NEW;
END;
$function$
"
public,archive_academy_participant,f,"p_participant_id uuid, p_sport text","p_participant_id uuid, p_sport text DEFAULT 'tennis'::text",boolean,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.archive_academy_participant(p_participant_id uuid, p_sport text DEFAULT 'tennis'::text)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_club_id uuid;
v_sport text;
BEGIN
v_sport := lower(
COALESCE(NULLIF(btrim(p_sport), ''), 'tennis')
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION
'Érvénytelen sport. Engedélyezett értékek: tennis, padel.';
END IF;
SELECT app.club_id
INTO v_club_id
FROM public.academy_program_participants app
JOIN public.academy_programs ap
ON ap.id = app.academy_program_id
AND ap.club_id = app.club_id
WHERE app.id = p_participant_id
AND ap.sport = v_sport;
IF NOT FOUND THEN
RAISE EXCEPTION
'Az akadémiai résztvevő nem található ebben a sportban.';
END IF;
IF NOT public.can_manage_academy(v_club_id) THEN
RAISE EXCEPTION
'Nincs jogosultságod a résztvevő archiválásához.';
END IF;
UPDATE public.academy_program_participants
SET
is_active = false,
left_on = COALESCE(left_on, current_date),
updated_at = now()
WHERE id = p_participant_id;
RETURN true;
END;
$function$
"
public,archive_academy_program,f,p_program_id uuid,p_program_id uuid,boolean,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.archive_academy_program(p_program_id uuid)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_club_id uuid;
BEGIN
SELECT ap.club_id
INTO v_club_id
FROM public.academy_programs ap
WHERE ap.id = p_program_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Az akadémiai program nem található.';
END IF;
IF NOT public.can_manage_academy(v_club_id) THEN
RAISE EXCEPTION 'Nincs jogosultságod a program archiválásához.';
END IF;
UPDATE public.academy_programs
SET
is_active = false,
updated_at = now()
WHERE id = p_program_id;
RETURN true;
END;
$function$
"
public,calculate_booking_price,f,"p_club_id uuid, p_season_id uuid, p_date date, p_start_time time without time zone, p_end_time time without time zone, p_pricing_mode text, p_currency text, p_court_id uuid","p_club_id uuid, p_season_id uuid, p_date date, p_start_time time without time zone, p_end_time time without time zone, p_pricing_mode text, p_currency text, p_court_id uuid",numeric,plpgsql,true,STABLE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres,anon=X/postgres}","CREATE OR REPLACE FUNCTION public.calculate_booking_price(p_club_id uuid, p_season_id uuid, p_date date, p_start_time time without time zone, p_end_time time without time zone, p_pricing_mode text, p_currency text, p_court_id uuid)
RETURNS numeric
LANGUAGE plpgsql
STABLE SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_total_price numeric := 0;
v_current_time time := p_start_time;
v_segment_end time;
v_rule_price numeric;
v_day_of_week integer;
v_rule_count integer;
v_pricing_group_id uuid := NULL;
BEGIN
IF p_court_id IS NULL THEN
RAISE EXCEPTION 'p_court_id is mandatory for calculate_booking_price';
END IF;
IF p_date IS NULL OR p_start_time IS NULL OR p_end_time IS NULL OR p_currency IS NULL THEN
RAISE EXCEPTION 'Date, start time, end time, and currency must not be null';
END IF;
IF p_pricing_mode IS NULL OR p_pricing_mode NOT IN ('regular', 'season_pass') THEN
RAISE EXCEPTION 'Invalid pricing mode';
END IF;
IF EXTRACT(SECOND FROM p_start_time)::int <> 0 OR EXTRACT(SECOND FROM p_end_time)::int <> 0 THEN
RAISE EXCEPTION 'Booking times must have zero seconds';
END IF;
IF EXTRACT(MINUTE FROM p_start_time)::int % 30 <> 0 OR EXTRACT(MINUTE FROM p_end_time)::int % 30 <> 0 THEN
RAISE EXCEPTION 'Booking times must be on 30-minute boundaries';
END IF;
IF p_start_time >= p_end_time THEN
RAISE EXCEPTION 'Start time must be before end time';
END IF;
-- Resolve the single pricing group this court belongs to in this season.
SELECT pgc.pricing_group_id INTO v_pricing_group_id
FROM public.pricing_group_courts pgc
JOIN public.pricing_groups pg ON pg.id = pgc.pricing_group_id
WHERE pgc.court_id = p_court_id
AND pg.season_id = p_season_id
AND pg.club_id = p_club_id
LIMIT 1;
IF v_pricing_group_id IS NULL THEN
RAISE EXCEPTION 'Court % is not assigned to any pricing group in season %', p_court_id, p_season_id;
END IF;
v_day_of_week := EXTRACT(DOW FROM p_date)::integer;
WHILE v_current_time < p_end_time LOOP
v_segment_end := v_current_time + interval '30 minutes';
SELECT COUNT(*)
INTO v_rule_count
FROM public.pricing_rules pr
WHERE pr.club_id = p_club_id
AND pr.season_id = p_season_id
AND pr.pricing_group_id = v_pricing_group_id
AND pr.weekday = v_day_of_week
AND pr.is_active = true
AND pr.currency = p_currency
AND pr.start_time <= v_current_time
AND pr.end_time >= v_segment_end;
IF v_rule_count = 0 THEN
RAISE EXCEPTION 'No applicable pricing rule found for segment % - % (court_id: %, group_id: %)',
v_current_time, v_segment_end, p_court_id, v_pricing_group_id;
ELSIF v_rule_count > 1 THEN
RAISE EXCEPTION 'Multiple overlapping pricing rules found for segment % - % (court_id: %, group_id: %)',
v_current_time, v_segment_end, p_court_id, v_pricing_group_id;
END IF;
SELECT CASE WHEN p_pricing_mode = 'season_pass' THEN pr.season_pass_price ELSE pr.price_per_hour END
INTO v_rule_price
FROM public.pricing_rules pr
WHERE pr.club_id = p_club_id
AND pr.season_id = p_season_id
AND pr.pricing_group_id = v_pricing_group_id
AND pr.weekday = v_day_of_week
AND pr.is_active = true
AND pr.currency = p_currency
AND pr.start_time <= v_current_time
AND pr.end_time >= v_segment_end;
IF v_rule_price IS NULL
OR v_rule_price <= 0
OR v_rule_price = 'NaN'::numeric
OR v_rule_price = 'Infinity'::numeric
OR v_rule_price = '-Infinity'::numeric
THEN
RAISE EXCEPTION 'Invalid or missing price for segment % - % in pricing mode %',
v_current_time, v_segment_end, p_pricing_mode;
END IF;
v_total_price := v_total_price + (v_rule_price / 2.0);
v_current_time := v_segment_end;
END LOOP;
v_total_price := ROUND(v_total_price, 2);
IF v_total_price <= 0
OR v_total_price = 'NaN'::numeric
OR v_total_price = 'Infinity'::numeric
OR v_total_price = '-Infinity'::numeric
THEN
RAISE EXCEPTION 'Total price must be a finite positive amount';
END IF;
IF p_currency = 'HUF' AND v_total_price <> TRUNC(v_total_price) THEN
RAISE EXCEPTION 'HUF total must be a whole number';
END IF;
RETURN v_total_price;
END;
$function$
"
public,can_manage_academy,f,p_club_id uuid,p_club_id uuid,boolean,sql,true,STABLE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.can_manage_academy(p_club_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
SELECT EXISTS (
SELECT 1
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = p_club_id
AND (
up.role IN (
'owner',
'admin',
'secondary_admin'
)
OR EXISTS (
SELECT 1
FROM public.coach_profiles cp
WHERE cp.user_profile_id = up.id
AND cp.is_active = true
)
)
);
$function$
"
public,can_manage_academy_payments,f,p_club_id uuid,p_club_id uuid,boolean,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.can_manage_academy_payments(p_club_id uuid)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_role text;
BEGIN
SELECT role
INTO v_role
FROM public.user_profiles
WHERE auth_user_id = auth.uid()
AND club_id = p_club_id
LIMIT 1;
IF v_role IN ('owner', 'admin', 'secondary_admin') THEN
RETURN true;
END IF;
RETURN false;
END;
$function$
"
public,can_view_club_reports,f,p_club_id uuid,p_club_id uuid,boolean,plpgsql,true,STABLE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.can_view_club_reports(p_club_id uuid)
RETURNS boolean
LANGUAGE plpgsql
STABLE SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_role text;
BEGIN
IF auth.uid() IS NULL
OR p_club_id IS NULL THEN
RETURN false;
END IF;
SELECT up.role
INTO v_role
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = p_club_id
ORDER BY up.id
LIMIT 1;
RETURN v_role IN (
'owner',
'admin',
'secondary_admin'
);
END;
$function$
"
public,cancel_academy_booking_batch,f,"p_batch_id uuid, p_reason text, p_sport text","p_batch_id uuid, p_reason text DEFAULT NULL::text, p_sport text DEFAULT 'tennis'::text",integer,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.cancel_academy_booking_batch(p_batch_id uuid, p_reason text DEFAULT NULL::text, p_sport text DEFAULT 'tennis'::text)
RETURNS integer
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_club_id uuid;
v_caller_profile_id uuid;
v_updated_count integer;
v_sport text;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Nincs bejelentkezett felhasználó.';
END IF;
v_sport := lower(
COALESCE(NULLIF(btrim(p_sport), ''), 'tennis')
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION 'Érvénytelen sport.';
END IF;
SELECT b.club_id
INTO v_club_id
FROM public.academy_booking_batches b
WHERE b.id = p_batch_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Az akadémiai foglalási csomag nem található.';
END IF;
IF NOT public.can_manage_academy(v_club_id) THEN
RAISE EXCEPTION
'Nincs jogosultságod az akadémiai foglalási csomag lemondásához.';
END IF;
IF EXISTS (
SELECT 1
FROM public.academy_booking_details abd
LEFT JOIN public.bookings b ON b.id = abd.booking_id
WHERE abd.academy_booking_batch_id = v_batch_id
AND (
b.id IS NULL
OR b.club_id IS DISTINCT FROM v_club_id
OR b.sport IS DISTINCT FROM v_sport
)
) THEN
RAISE EXCEPTION 'Academy batch contains cross-club or cross-sport booking data.';
END IF;
SELECT up.id
INTO v_caller_profile_id
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_club_id
ORDER BY up.id
LIMIT 1;
UPDATE public.bookings b
SET status = 'cancelled'
FROM public.academy_booking_details d
WHERE d.academy_booking_batch_id = p_batch_id
AND d.booking_id = b.id
AND b.club_id = v_club_id
AND b.sport = v_sport
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, '')) <> 'cancelled';
GET DIAGNOSTICS v_updated_count = ROW_COUNT;
UPDATE public.academy_booking_details
SET
occurrence_status = 'cancelled',
cancelled_at = COALESCE(cancelled_at, now()),
cancelled_by_user_profile_id =
COALESCE(cancelled_by_user_profile_id, v_caller_profile_id),
cancellation_reason =
COALESCE(cancellation_reason, NULLIF(btrim(p_reason), ''))
WHERE academy_booking_batch_id = p_batch_id
AND occurrence_status <> 'cancelled';
UPDATE public.academy_booking_batches
SET status = 'cancelled'
WHERE id = p_batch_id
AND club_id = v_club_id;
RETURN v_updated_count;
END;
$function$
"
public,cancel_academy_booking_occurrence,f,"p_booking_id uuid, p_reason text, p_sport text","p_booking_id uuid, p_reason text DEFAULT NULL::text, p_sport text DEFAULT 'tennis'::text",boolean,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.cancel_academy_booking_occurrence(p_booking_id uuid, p_reason text DEFAULT NULL::text, p_sport text DEFAULT 'tennis'::text)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_club_id uuid;
v_batch_id uuid;
v_caller_profile_id uuid;
v_total_count integer;
v_cancelled_count integer;
v_booking_sport text;
v_sport text;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Nincs bejelentkezett felhasználó.';
END IF;
v_sport := lower(
COALESCE(NULLIF(btrim(p_sport), ''), 'tennis')
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION 'Érvénytelen sport.';
END IF;
SELECT d.club_id, d.academy_booking_batch_id, b.sport
INTO v_club_id, v_batch_id, v_booking_sport
FROM public.academy_booking_details d
JOIN public.bookings b
ON b.id = d.booking_id
AND b.club_id = d.club_id
WHERE d.booking_id = p_booking_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Az akadémiai foglalás nem található.';
END IF;
IF v_booking_sport IS DISTINCT FROM v_sport THEN
RAISE EXCEPTION 'Az akadémiai foglalás másik sporthoz tartozik.';
END IF;
IF NOT public.can_manage_academy(v_club_id) THEN
RAISE EXCEPTION
'Nincs jogosultságod az akadémiai foglalás lemondásához.';
END IF;
IF EXISTS (
SELECT 1
FROM public.academy_booking_details abd
LEFT JOIN public.bookings b ON b.id = abd.booking_id
WHERE abd.academy_booking_batch_id = v_batch_id
AND (
b.id IS NULL
OR b.club_id IS DISTINCT FROM v_club_id
OR b.sport IS DISTINCT FROM v_sport
)
) THEN
RAISE EXCEPTION 'Academy batch contains cross-club or cross-sport booking data.';
END IF;
SELECT up.id
INTO v_caller_profile_id
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_club_id
ORDER BY up.id
LIMIT 1;
UPDATE public.bookings
SET status = 'cancelled'
WHERE id = p_booking_id
AND club_id = v_club_id
AND sport = v_sport
AND deleted_at IS NULL;
UPDATE public.academy_booking_details
SET
occurrence_status = 'cancelled',
cancelled_at = now(),
cancelled_by_user_profile_id = v_caller_profile_id,
cancellation_reason = NULLIF(btrim(p_reason), '')
WHERE booking_id = p_booking_id;
SELECT
count(*),
count(*) FILTER (WHERE occurrence_status = 'cancelled')
INTO
v_total_count,
v_cancelled_count
FROM public.academy_booking_details
WHERE academy_booking_batch_id = v_batch_id;
UPDATE public.academy_booking_batches
SET status = CASE
WHEN v_cancelled_count = 0 THEN 'active'
WHEN v_cancelled_count = v_total_count THEN 'cancelled'
ELSE 'partially_cancelled'
END
WHERE id = v_batch_id;
RETURN true;
END;
$function$
"
public,cancel_academy_booking_remaining,f,"p_booking_id uuid, p_reason text, p_sport text","p_booking_id uuid, p_reason text DEFAULT NULL::text, p_sport text DEFAULT 'tennis'::text",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.cancel_academy_booking_remaining(p_booking_id uuid, p_reason text DEFAULT NULL::text, p_sport text DEFAULT 'tennis'::text)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_source_detail RECORD;
v_caller_profile RECORD;
v_club_id uuid;
v_batch_id uuid;
v_source_date date;
v_now timestamptz := now();
v_cancelled_count integer := 0;
v_total_in_batch integer := 0;
v_cancelled_in_batch integer := 0;
v_new_batch_status text;
v_occurrence RECORD;
v_booking_scope text;
v_effective_reason text;
v_sport text;
BEGIN
v_sport := lower(
COALESCE(NULLIF(btrim(p_sport), ''), 'tennis')
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION 'Invalid sport. Allowed values: tennis, padel.';
END IF;
-- ============================================================
-- 1. Load the Academy booking detail for the source booking FIRST
-- to determine the club_id before resolving the caller profile.
-- ============================================================
SELECT
abd.booking_id,
abd.academy_booking_batch_id,
abd.academy_type,
abd.academy_program_id,
abd.occurrence_status,
b.club_id,
b.booking_date,
b.status AS booking_status,
b.deleted_at,
b.sport
INTO v_source_detail
FROM public.academy_booking_details abd
JOIN public.bookings b ON b.id = abd.booking_id
WHERE abd.booking_id = p_booking_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'This booking is not an Academy booking.';
END IF;
IF v_source_detail.sport IS DISTINCT FROM v_sport THEN
RAISE EXCEPTION 'This Academy booking belongs to a different sport.';
END IF;
v_club_id := v_source_detail.club_id;
v_batch_id := v_source_detail.academy_booking_batch_id;
v_source_date := v_source_detail.booking_date;
IF v_batch_id IS NULL THEN
RAISE EXCEPTION 'This Academy booking does not belong to a batch.';
END IF;
-- ============================================================
-- 2. Resolve caller profile (club-specific, follows
-- cancel_academy_booking_occurrence semantics)
-- ============================================================
SELECT up.id, up.club_id, up.role
INTO v_caller_profile
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_club_id
ORDER BY up.id
LIMIT 1;
IF NOT FOUND THEN
RAISE EXCEPTION 'Caller profile not found in this club.';
END IF;
-- ============================================================
-- 3. Source booking status check
-- (occurrence_status, deleted_at, AND booking status)
-- ============================================================
IF v_source_detail.occurrence_status = 'cancelled'
OR v_source_detail.deleted_at IS NOT NULL
OR lower(COALESCE(v_source_detail.booking_status, '')) IN
('cancelled', 'deleted', 'deleted by club', 'failed')
THEN
RAISE EXCEPTION 'This booking has already been cancelled or deleted.';
END IF;
-- ============================================================
-- 4. Authorization: admin or coach with can_manage_academy
-- ============================================================
IF NOT public.can_manage_academy(v_club_id) THEN
RAISE EXCEPTION 'You do not have permission to manage Academy bookings.';
END IF;
-- ============================================================
-- 5. Lock the batch row to prevent concurrent bulk cancellations
-- and enforce booking_scope = 'season' at the backend level.
-- ============================================================
SELECT abb.booking_scope
INTO v_booking_scope
FROM public.academy_booking_batches abb
WHERE abb.id = v_batch_id
AND abb.club_id = v_club_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Academy batch not found in this club.';
END IF;
IF EXISTS (
SELECT 1
FROM public.academy_booking_details abd
LEFT JOIN public.bookings b ON b.id = abd.booking_id
WHERE abd.academy_booking_batch_id = v_batch_id
AND (
b.id IS NULL
OR b.club_id IS DISTINCT FROM v_club_id
OR b.sport IS DISTINCT FROM v_sport
)
) THEN
RAISE EXCEPTION 'Academy batch contains cross-club or cross-sport booking data.';
END IF;
IF v_booking_scope IS DISTINCT FROM 'season' THEN
RAISE EXCEPTION 'Remaining-season cancellation is only available for Academy season bookings.';
END IF;
-- ============================================================
-- 6. Re-check source detail status AFTER batch lock
-- A concurrent cancellation may have cancelled it while we
-- were waiting for the lock.
-- ============================================================
SELECT
abd.occurrence_status,
b.status,
b.deleted_at,
b.sport
INTO
v_source_detail.occurrence_status,
v_source_detail.booking_status,
v_source_detail.deleted_at,
v_source_detail.sport
FROM public.academy_booking_details abd
JOIN public.bookings b ON b.id = abd.booking_id
WHERE abd.booking_id = p_booking_id;
IF v_source_detail.sport IS DISTINCT FROM v_sport THEN
RAISE EXCEPTION 'This Academy booking belongs to a different sport.';
END IF;
IF v_source_detail.occurrence_status = 'cancelled'
OR v_source_detail.deleted_at IS NOT NULL
OR lower(COALESCE(v_source_detail.booking_status, '')) IN
('cancelled', 'deleted', 'deleted by club', 'failed')
THEN
RAISE EXCEPTION 'This booking has already been cancelled or deleted.';
END IF;
-- Normalize reason (consistent with cancel_academy_booking_occurrence)
v_effective_reason := NULLIF(btrim(p_reason), '');
-- ============================================================
-- 7. Cancel the source booking AND all future active occurrences
-- in the SAME academy_booking_batch only.
-- Past bookings are NEVER touched.
-- Uses IDENTICAL WHERE conditions as get_academy_booking_cancel_preview.
-- Atomic: if any update fails, entire transaction rolls back.
--
-- Booking UPDATE follows existing Academy cancellation semantics:
-- status = 'cancelled' (no deleted_at, no booking-level audit fields)
-- Academy detail is the audit source of truth:
-- occurrence_status, cancelled_at, cancelled_by_user_profile_id,
-- cancellation_reason
-- ============================================================
FOR v_occurrence IN
SELECT
abd.booking_id,
b.booking_date,
b.start_time
FROM public.academy_booking_details abd
JOIN public.bookings b ON b.id = abd.booking_id
WHERE abd.academy_booking_batch_id = v_batch_id
AND b.club_id = v_club_id
AND b.sport = v_sport
AND b.booking_date >= v_source_date
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, '')) NOT IN ('cancelled', 'deleted', 'deleted by club', 'failed')
AND lower(COALESCE(abd.occurrence_status, '')) <> 'cancelled'
ORDER BY b.booking_date, b.start_time
LOOP
-- Update the booking status (soft cancel, no hard delete)
-- Consistent with cancel_academy_booking_occurrence:
-- only status = 'cancelled', no deleted_at, no booking-level audit fields
UPDATE public.bookings
SET status = 'cancelled'
WHERE id = v_occurrence.booking_id
AND club_id = v_club_id
AND sport = v_sport;
-- Update the Academy detail (audit source of truth)
UPDATE public.academy_booking_details
SET
occurrence_status = 'cancelled',
cancelled_at = v_now,
cancelled_by_user_profile_id = v_caller_profile.id,
cancellation_reason = v_effective_reason
WHERE booking_id = v_occurrence.booking_id;
v_cancelled_count := v_cancelled_count + 1;
END LOOP;
-- ============================================================
-- 8. Zero cancelled count guard
-- ============================================================
IF v_cancelled_count = 0 THEN
RAISE EXCEPTION 'No active Academy occurrences were cancelled.';
END IF;
-- ============================================================
-- 9. Recalculate batch status based on ALL details in the batch
-- ============================================================
SELECT
COUNT(*),
COUNT(*) FILTER (
WHERE lower(COALESCE(abd.occurrence_status, '')) = 'cancelled'
)
INTO
v_total_in_batch,
v_cancelled_in_batch
FROM public.academy_booking_details abd
WHERE abd.academy_booking_batch_id = v_batch_id;
IF v_cancelled_in_batch = 0 THEN
v_new_batch_status := 'active';
ELSIF v_cancelled_in_batch = v_total_in_batch THEN
v_new_batch_status := 'cancelled';
ELSE
v_new_batch_status := 'partially_cancelled';
END IF;
UPDATE public.academy_booking_batches
SET status = v_new_batch_status
WHERE id = v_batch_id
AND club_id = v_club_id;
-- ============================================================
-- 10. Return summary
-- ============================================================
RETURN jsonb_build_object(
'success', true,
'cancelled_count', v_cancelled_count,
'batch_id', v_batch_id,
'new_batch_status', v_new_batch_status
);
END;
$function$
"
public,cancel_booking_with_credit_refund,f,"p_booking_id uuid, p_cancellation_reason_category text, p_cancellation_reason text","p_booking_id uuid, p_cancellation_reason_category text, p_cancellation_reason text DEFAULT NULL::text",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.cancel_booking_with_credit_refund(p_booking_id uuid, p_cancellation_reason_category text, p_cancellation_reason text DEFAULT NULL::text)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_booking record;
v_payment record;
v_wallet public.credit_wallets%ROWTYPE;
v_plan record;
v_caller_profile_id uuid;
v_caller_role text;
v_caller_name text;
v_reason_category text;
v_reason text;
v_club_timezone text;
v_cutoff_hours integer;
v_booking_start_tz timestamptz;
v_idempotency_key text;
v_existing_tx record;
v_mapped_credit_type text;
v_original_tx_count integer;
v_payment_count integer;
v_wallet_updated int;
v_payment_updated int;
v_booking_updated int;
v_new_wallet_balance numeric := 0;
v_refund_amount numeric := 0;
v_currency text;
v_target_user_profile_id uuid;
v_issue_refund boolean := false;
-- COACH DIFF (a): coach detection + own-booking flag
v_is_coach boolean := false;
v_is_own_booking boolean := false;
-- AUDIT-ONLY effective actor role (canonical). NOT used for
-- authorization — only for cancelled_by_role audit + output.
v_effective_actor_role text;
BEGIN
-- ============================================================
-- AUTH
-- ============================================================
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Not authenticated.';
END IF;
IF p_booking_id IS NULL THEN
RAISE EXCEPTION 'Booking ID is required.';
END IF;
-- ============================================================
-- REASON VALIDATION
-- ============================================================
v_reason_category :=
lower(
btrim(
COALESCE(
p_cancellation_reason_category,
''
)
)
);
v_reason :=
NULLIF(
btrim(
COALESCE(
p_cancellation_reason,
''
)
),
''
);
IF v_reason_category NOT IN (
'illness',
'schedule_conflict',
'coach_unavailable',
'player_request',
'weather',
'court_unavailable',
'club_decision',
'other'
) THEN
RAISE EXCEPTION
'A cancellation reason category is required.';
END IF;
IF v_reason_category = 'other'
AND v_reason IS NULL
THEN
RAISE EXCEPTION
'A cancellation reason is required when category is other.';
END IF;
-- ============================================================
-- BOOKING LOCK
-- ============================================================
SELECT *
INTO v_booking
FROM public.bookings
WHERE id = p_booking_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Booking not found.';
END IF;
-- ============================================================
-- CALLER PROFILE
-- ============================================================
SELECT
up.id,
up.role,
up.full_name
INTO
v_caller_profile_id,
v_caller_role,
v_caller_name
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_booking.club_id
LIMIT 1;
IF v_caller_profile_id IS NULL THEN
RAISE EXCEPTION
'Caller profile not found in this club.';
END IF;
v_caller_name :=
COALESCE(
NULLIF(
btrim(v_caller_name),
''
),
'Ismeretlen'
);
-- COACH DIFF (a): detect coach identity. Canonical definition:
-- role = 'coach' OR an active coach_profiles row linked by
-- user_profile_id. (LIVE data may have role='user' coaches.)
v_is_coach :=
(v_caller_role = 'coach')
OR EXISTS (
SELECT 1
FROM public.coach_profiles cp
WHERE cp.user_profile_id = v_caller_profile_id
AND cp.is_active = true
);
-- AUDIT-ONLY effective actor role. Admin roles keep their real
-- role; a canonical coach (incl. role='user' with an active
-- coach_profiles row) is recorded as 'coach'; everyone else
-- keeps their real role. NOT used for authorization.
v_effective_actor_role :=
CASE
WHEN v_caller_role IN ('owner', 'admin', 'secondary_admin')
THEN v_caller_role
WHEN v_is_coach
THEN 'coach'
ELSE v_caller_role
END;
-- ============================================================
-- EXISTING AUTHORIZATION + CUTOFF
-- ============================================================
IF v_caller_role NOT IN (
'owner',
'admin',
'secondary_admin'
) THEN
-- COACH DIFF (b): coach on OWN booking receives admin-equivalent
-- timing (skip past + cutoff). Coach on ANOTHER user's booking
-- is DENIED via the same ownership rule as everyone else.
IF v_is_coach
AND v_booking.user_id IS DISTINCT FROM v_caller_profile_id
AND v_booking.created_by_user_id IS DISTINCT FROM v_caller_profile_id
THEN
RAISE EXCEPTION
'Coaches can only cancel their own bookings.';
END IF;
IF v_is_coach
AND NOT (
v_booking.user_id IS DISTINCT FROM v_caller_profile_id
AND v_booking.created_by_user_id IS DISTINCT FROM v_caller_profile_id
)
THEN
-- Coach owns this booking. Admin-equivalent TIMING permission:
-- NO past-booking guard, NO cancellation cutoff. Financial
-- engine below is IDENTICAL to LIVE (no refund change).
v_is_own_booking := true;
ELSE
-- Every NON-admin, NON-(coach-own) caller: LIVE logic unchanged.
IF v_booking.user_id
IS DISTINCT FROM v_caller_profile_id
AND v_booking.created_by_user_id
IS DISTINCT FROM v_caller_profile_id
THEN
RAISE EXCEPTION
'Unauthorized to cancel this booking.';
END IF;
SELECT
timezone,
cancellation_cutoff_hours
INTO
v_club_timezone,
v_cutoff_hours
FROM public.clubs
WHERE id = v_booking.club_id;
v_booking_start_tz :=
(
v_booking.booking_date
+ v_booking.start_time
)
AT TIME ZONE
COALESCE(
v_club_timezone,
'UTC'
);
IF v_booking_start_tz < now() THEN
RAISE EXCEPTION
'Cannot cancel a past booking.';
END IF;
IF v_booking_start_tz <
now()
+ (
COALESCE(v_cutoff_hours, 0)
|| ' hours'
)::interval
THEN
RAISE EXCEPTION
'Cancellation cutoff time has passed.';
END IF;
END IF;
END IF;
-- COACH DIFF (c): a coach cancelling an OWN booking must supply a
-- non-empty textual reason (not just a category). Admins and
-- normal users are UNCHANGED. Empty / whitespace → DENIED.
IF v_is_coach AND v_is_own_booking AND v_reason IS NULL THEN
RAISE EXCEPTION
'A cancellation reason is required for coach own-booking cancellation.';
END IF;
-- ============================================================
-- IDEMPOTENCY
-- VÁLTOZATLAN
-- ============================================================
v_idempotency_key :=
'cancellation_refund:'
|| p_booking_id::text;
PERFORM pg_advisory_xact_lock(
hashtextextended(
v_idempotency_key,
0
)
);
SELECT *
INTO v_existing_tx
FROM public.credit_transactions
WHERE idempotency_key =
v_idempotency_key;
IF FOUND THEN
IF v_existing_tx.transaction_category =
'cancellation_refund'
AND v_existing_tx.booking_id =
p_booking_id
AND v_booking.status =
'cancelled'
THEN
RETURN jsonb_build_object(
'booking_id', p_booking_id,
'status', v_booking.status,
'payment_status', v_booking.payment_status,
'refund_amount', v_existing_tx.amount,
'currency', v_existing_tx.currency,
'credit_type', v_existing_tx.credit_type,
'wallet_id', v_existing_tx.wallet_id,
'wallet_balance', (
SELECT balance
FROM public.credit_wallets
WHERE id =
v_existing_tx.wallet_id
),
'already_processed', true,
'cancelled_by_profile_id', v_booking.cancelled_by_profile_id,
'cancelled_by_name', v_booking.cancelled_by_name,
'cancelled_by_role', v_booking.cancelled_by_role,
'cancellation_reason_category', v_booking.cancellation_reason_category,
'cancellation_reason', v_booking.cancellation_reason
);
END IF;
RAISE EXCEPTION
'Inconsistent state for existing refund transaction.';
END IF;
IF v_booking.status = 'cancelled'
AND v_booking.deleted_at IS NOT NULL
THEN
RETURN jsonb_build_object(
'booking_id', p_booking_id,
'status', v_booking.status,
'payment_status', v_booking.payment_status,
'refund_amount', 0,
'already_processed', true,
'cancelled_by_profile_id', v_booking.cancelled_by_profile_id,
'cancelled_by_name', v_booking.cancelled_by_name,
'cancelled_by_role', v_booking.cancelled_by_role,
'cancellation_reason_category', v_booking.cancellation_reason_category,
'cancellation_reason', v_booking.cancellation_reason
);
END IF;
IF v_booking.status IN (
'cancelled',
'deleted by club',
'failed'
) THEN
RAISE EXCEPTION
'Booking is already cancelled or deleted.';
END IF;
IF v_booking.deleted_at IS NOT NULL THEN
RAISE EXCEPTION
'Booking is already deleted.';
END IF;
-- ============================================================
-- PAYMENT METHOD BRANCHING
-- EZ A JELENLEGI ÉLES LOGIKA (VÁLTOZATLAN)
-- ============================================================
IF v_booking.payment_method IN (
'credit_standard',
'credit_season_pass'
) THEN
SELECT COUNT(*)
INTO v_payment_count
FROM public.booking_payments
WHERE booking_id =
p_booking_id;
IF v_payment_count <> 1 THEN
RAISE EXCEPTION
'Integrity error: exactly one payment record is required.';
END IF;
SELECT *
INTO v_payment
FROM public.booking_payments
WHERE booking_id =
p_booking_id
FOR UPDATE;
IF v_payment.user_profile_id
IS DISTINCT FROM
v_booking.user_id
THEN
RAISE EXCEPTION
'Payment user mismatch.';
END IF;
IF v_payment.payment_status
IS DISTINCT FROM 'paid'
THEN
RAISE EXCEPTION
'Payment status is not paid.';
END IF;
IF v_payment.paid_at IS NULL THEN
RAISE EXCEPTION
'Payment paid_at is null.';
END IF;
IF v_payment.wallet_id IS NULL THEN
RAISE EXCEPTION
'Payment wallet_id is null.';
END IF;
v_mapped_credit_type :=
CASE
WHEN v_booking.payment_method =
'credit_standard'
THEN 'standard'
ELSE 'season_pass'
END;
v_refund_amount :=
v_payment.amount;
v_currency :=
v_payment.currency;
v_target_user_profile_id :=
v_payment.user_profile_id;
v_issue_refund :=
true;
SELECT COUNT(*)
INTO v_original_tx_count
FROM public.credit_transactions
WHERE booking_id =
p_booking_id
AND transaction_category =
'booking_payment'
AND wallet_id =
v_payment.wallet_id
AND amount =
-v_payment.amount;
IF v_original_tx_count <> 1 THEN
RAISE EXCEPTION
'Integrity error: exactly one original credit charge is required.';
END IF;
ELSIF v_booking.payment_method =
'venue'
THEN
IF v_booking.booking_type =
'recurring'
THEN
v_issue_refund :=
true;
IF v_booking.user_id IS NOT NULL
AND EXISTS (
SELECT 1
FROM public.user_profiles
WHERE id =
v_booking.user_id
AND club_id =
v_booking.club_id
)
THEN
v_target_user_profile_id :=
v_booking.user_id;
ELSE
RAISE EXCEPTION
'Cannot issue replacement credit: no linked user profile. Please link the booking to a registered user before cancellation.';
END IF;
-- A jelenlegi működő szabály:
-- recurring venue -> season_pass credit
v_mapped_credit_type :=
'season_pass';
v_currency :=
v_booking.currency;
v_refund_amount :=
v_booking.price_per_booking;
IF v_refund_amount IS NULL
OR v_refund_amount <= 0
THEN
RAISE EXCEPTION
'No authoritative positive price_per_booking stored for this booking occurrence.';
END IF;
ELSE
IF v_booking.payment_status =
'paid'
THEN
SELECT *
INTO v_plan
FROM public.venue_payment_plans
WHERE booking_id =
p_booking_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION
'Venue payment plan not found.';
END IF;
IF v_plan.plan_status
IS DISTINCT FROM
'paid'
THEN
RAISE EXCEPTION
'Venue payment plan is not fully paid.';
END IF;
IF v_booking.user_id
IS NOT NULL
AND EXISTS (
SELECT 1
FROM public.user_profiles
WHERE id =
v_booking.user_id
AND club_id =
v_booking.club_id
)
THEN
v_target_user_profile_id :=
v_booking.user_id;
ELSIF v_plan.user_profile_id
IS NOT NULL
AND EXISTS (
SELECT 1
FROM public.user_profiles
WHERE id =
v_plan.user_profile_id
AND club_id =
v_booking.club_id
)
THEN
v_target_user_profile_id :=
v_plan.user_profile_id;
ELSE
RAISE EXCEPTION
'Cannot refund venue booking: no linked user profile. Please link the payment plan to a registered user before cancellation.';
END IF;
v_mapped_credit_type :=
'standard';
v_currency :=
v_booking.currency;
v_refund_amount :=
COALESCE(
v_booking.price_per_booking,
v_booking.estimated_total_price
);
IF v_refund_amount IS NULL
OR v_refund_amount <= 0
THEN
RAISE EXCEPTION
'No authoritative positive price stored for this one-time booking occurrence.';
END IF;
v_issue_refund :=
true;
ELSE
v_issue_refund :=
false;
END IF;
END IF;
ELSIF v_booking.payment_method =
'supported'
THEN
v_issue_refund :=
false;
v_refund_amount :=
0;
ELSE
RAISE EXCEPTION
'Unsupported payment method for refund.';
END IF;
-- ============================================================
-- REFUND
-- VÁLTOZATLAN
-- ============================================================
IF v_issue_refund THEN
INSERT INTO public.credit_wallets (
club_id,
user_profile_id,
season_id,
currency,
credit_type,
balance
)
VALUES (
v_booking.club_id,
v_target_user_profile_id,
v_booking.season_id,
v_currency,
v_mapped_credit_type,
0
)
ON CONFLICT (
club_id,
user_profile_id,
season_id,
currency,
credit_type
)
DO NOTHING;
SELECT *
INTO v_wallet
FROM public.credit_wallets
WHERE club_id =
v_booking.club_id
AND user_profile_id =
v_target_user_profile_id
AND season_id =
v_booking.season_id
AND currency =
v_currency
AND credit_type =
v_mapped_credit_type
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION
'Wallet not found.';
END IF;
INSERT INTO public.credit_transactions (
wallet_id,
club_id,
user_profile_id,
season_id,
booking_id,
credit_type,
currency,
amount,
transaction_category,
reason,
created_by,
idempotency_key
)
VALUES (
v_wallet.id,
v_booking.club_id,
v_target_user_profile_id,
v_booking.season_id,
p_booking_id,
v_mapped_credit_type,
v_currency,
v_refund_amount,
'cancellation_refund',
'Credit returned after booking cancellation',
v_caller_profile_id,
v_idempotency_key
);
UPDATE public.credit_wallets
SET
balance =
balance + v_refund_amount,
updated_at =
now()
WHERE id =
v_wallet.id
RETURNING balance
INTO v_new_wallet_balance;
GET DIAGNOSTICS
v_wallet_updated =
ROW_COUNT;
IF v_wallet_updated <> 1 THEN
RAISE EXCEPTION
'Wallet update failed.';
END IF;
IF v_booking.payment_method IN (
'credit_standard',
'credit_season_pass'
)
THEN
UPDATE public.booking_payments
SET
payment_status =
'refunded',
refunded_at =
now()
WHERE id =
v_payment.id;
GET DIAGNOSTICS
v_payment_updated =
ROW_COUNT;
IF v_payment_updated <> 1 THEN
RAISE EXCEPTION
'Payment update failed.';
END IF;
ELSIF v_booking.payment_method =
'venue'
AND v_booking.booking_type =
'one-time'
THEN
UPDATE public.venue_payment_plans
SET
plan_status =
'cancelled',
updated_at =
now()
WHERE booking_id =
p_booking_id;
END IF;
ELSE
IF v_booking.payment_method =
'venue'
AND v_booking.booking_type =
'one-time'
THEN
UPDATE public.venue_payment_plans
SET
plan_status =
'cancelled',
updated_at =
now()
WHERE booking_id =
p_booking_id;
END IF;
END IF;
-- ============================================================
-- BOOKING CANCELLATION + AUDIT
-- ============================================================
BEGIN
PERFORM set_config(
'app.bypass_booking_security',
'true',
true
);
UPDATE public.bookings
SET
status =
'cancelled',
payment_status =
CASE
WHEN v_issue_refund
AND NOT (
v_booking.payment_method =
'venue'
AND v_booking.booking_type =
'recurring'
)
THEN 'refunded'
ELSE payment_status
END,
deleted_at =
now(),
cancelled_by_profile_id =
v_caller_profile_id,
cancelled_by_name =
v_caller_name,
cancelled_by_role =
v_effective_actor_role,
cancellation_reason_category =
v_reason_category,
cancellation_reason =
v_reason
WHERE id =
p_booking_id
AND deleted_at IS NULL
AND status NOT IN (
'cancelled',
'deleted by club',
'failed'
);
GET DIAGNOSTICS
v_booking_updated =
ROW_COUNT;
IF v_booking_updated <> 1 THEN
RAISE EXCEPTION
'Booking update failed.';
END IF;
PERFORM set_config(
'app.bypass_booking_security',
'false',
true
);
EXCEPTION
WHEN OTHERS THEN
PERFORM set_config(
'app.bypass_booking_security',
'false',
true
);
RAISE;
END;
-- ============================================================
-- RESULT
-- ============================================================
RETURN jsonb_build_object(
'booking_id', p_booking_id,
'status', 'cancelled',
'payment_status', CASE
WHEN v_issue_refund
AND NOT (
v_booking.payment_method =
'venue'
AND v_booking.booking_type =
'recurring'
)
THEN 'refunded'
ELSE v_booking.payment_status
END,
'refund_amount', CASE
WHEN v_issue_refund
THEN v_refund_amount
ELSE 0
END,
'currency', v_booking.currency,
'credit_type', CASE
WHEN v_issue_refund
THEN v_mapped_credit_type
ELSE NULL
END,
'wallet_id', CASE
WHEN v_issue_refund
THEN v_wallet.id
ELSE NULL
END,
'wallet_balance', CASE
WHEN v_issue_refund
THEN v_new_wallet_balance
ELSE NULL
END,
'already_processed', false,
'cancelled_by_profile_id', v_caller_profile_id,
'cancelled_by_name', v_caller_name,
'cancelled_by_role', v_effective_actor_role,
'cancellation_reason_category', v_reason_category,
'cancellation_reason', v_reason
);
EXCEPTION
WHEN OTHERS THEN
PERFORM set_config(
'app.bypass_booking_security',
'false',
true
);
RAISE;
END;
$function$
"
public,cancel_recurring_season_occurrence,f,"p_booking_id uuid, p_cancellation_reason_category text, p_cancellation_reason text","p_booking_id uuid, p_cancellation_reason_category text DEFAULT NULL::text, p_cancellation_reason text DEFAULT NULL::text",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.cancel_recurring_season_occurrence(p_booking_id uuid, p_cancellation_reason_category text DEFAULT NULL::text, p_cancellation_reason text DEFAULT NULL::text)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_source_booking RECORD;
v_locked_booking RECORD;
v_caller_profile RECORD;
v_club_id uuid;
v_season_id uuid;
v_series_id uuid;
v_sport text;
v_is_admin boolean := false;
v_has_positive_booking_payment boolean := false;
v_can_no_refund boolean := false;
v_use_credit_refund boolean := false;
v_active_count integer := 0;
v_total_active_count integer := 0;
v_now timestamptz := now();
v_caller_name text;
v_has_venue_plan boolean := false;
v_plan_status text;
v_plan_id uuid;
v_refund_result jsonb;
BEGIN
-- ========================================================
-- 1. AUTH
-- ========================================================
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Authentication required.';
END IF;
-- ========================================================
-- 2. SOURCE BOOKING
-- ========================================================
SELECT
b.id,
b.club_id,
b.season_id,
b.recurring_series_id,
b.sport,
b.booking_type
INTO v_source_booking
FROM public.bookings b
WHERE b.id = p_booking_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Booking not found.';
END IF;
v_club_id := v_source_booking.club_id;
v_season_id := v_source_booking.season_id;
v_series_id := v_source_booking.recurring_series_id;
v_sport := v_source_booking.sport;
-- ========================================================
-- TENNIS / PADEL SAFETY
-- ========================================================
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION 'Invalid booking sport.';
END IF;
-- ========================================================
-- 3. RECURRING VALIDATION
-- ========================================================
IF v_series_id IS NULL THEN
RAISE EXCEPTION
'This booking is not part of a recurring series.';
END IF;
IF lower(COALESCE(v_source_booking.booking_type, ''))
<> 'recurring'
THEN
RAISE EXCEPTION
'This RPC only handles normal recurring bookings.';
END IF;
-- ========================================================
-- 4. CALLER
-- ========================================================
SELECT
up.id,
up.club_id,
up.role,
up.full_name,
up.first_name,
up.last_name
INTO v_caller_profile
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_club_id
LIMIT 1;
IF NOT FOUND THEN
RAISE EXCEPTION
'Caller profile not found in this club.';
END IF;
IF v_caller_profile.club_id IS DISTINCT FROM v_club_id THEN
RAISE EXCEPTION
'You do not have permission to cancel this booking.';
END IF;
v_is_admin :=
v_caller_profile.role IN (
'owner',
'admin',
'secondary_admin'
);
IF NOT v_is_admin THEN
RAISE EXCEPTION
'Recurring season cancellation is only available to administrators.';
END IF;
-- ========================================================
-- 5. REASON
-- ========================================================
IF p_cancellation_reason_category IS NULL
OR p_cancellation_reason_category NOT IN (
'illness',
'schedule_conflict',
'coach_unavailable',
'player_request',
'weather',
'court_unavailable',
'club_decision',
'other'
)
THEN
RAISE EXCEPTION
'A cancellation reason category is required.';
END IF;
IF p_cancellation_reason_category = 'other'
AND (
p_cancellation_reason IS NULL
OR trim(p_cancellation_reason) = ''
)
THEN
RAISE EXCEPTION
'A cancellation reason is required when the category is ""other"".';
END IF;
v_caller_name := COALESCE(
NULLIF(btrim(v_caller_profile.full_name), ''),
NULLIF(
btrim(
COALESCE(v_caller_profile.first_name, '')
|| ' '
|| COALESCE(v_caller_profile.last_name, '')
),
''
),
'Ismeretlen'
);
-- ========================================================
-- 6. LOCK PAYMENT PLAN FIRST
-- ========================================================
SELECT
id,
plan_status
INTO
v_plan_id,
v_plan_status
FROM public.venue_payment_plans
WHERE recurring_series_id = v_series_id
AND club_id = v_club_id
FOR UPDATE;
v_has_venue_plan := FOUND;
-- ========================================================
-- 7. LOCK BOOKING
-- ========================================================
SELECT b.*
INTO v_locked_booking
FROM public.bookings b
WHERE b.id = p_booking_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Booking not found.';
END IF;
-- ========================================================
-- 8. REVALIDATE
-- ========================================================
IF v_locked_booking.club_id IS DISTINCT FROM v_club_id THEN
RAISE EXCEPTION 'Club mismatch after lock.';
END IF;
IF v_locked_booking.recurring_series_id
IS DISTINCT FROM v_series_id
THEN
RAISE EXCEPTION 'Series mismatch after lock.';
END IF;
IF v_locked_booking.sport IS DISTINCT FROM v_sport THEN
RAISE EXCEPTION 'Sport mismatch after lock.';
END IF;
IF lower(COALESCE(v_locked_booking.booking_type, ''))
<> 'recurring'
THEN
RAISE EXCEPTION
'This RPC only handles normal recurring bookings.';
END IF;
IF v_locked_booking.status = 'cancelled'
OR v_locked_booking.deleted_at IS NOT NULL
THEN
RAISE EXCEPTION
'This booking has already been cancelled or deleted.';
END IF;
-- ========================================================
-- 9. ACADEMY ISOLATION
-- ========================================================
IF EXISTS (
SELECT 1
FROM public.academy_booking_details abd
WHERE abd.booking_id = p_booking_id
) THEN
RAISE EXCEPTION
'Academy bookings must be cancelled through the Academy cancellation system.';
END IF;
-- ========================================================
-- 10. EXACT PAYMENT DETECTION
-- ========================================================
v_has_positive_booking_payment := EXISTS (
SELECT 1
FROM public.booking_payments bp
WHERE bp.booking_id = p_booking_id
AND bp.payment_status = 'paid'
AND COALESCE(bp.amount, 0) > 0
);
-- ========================================================
-- 11. PAYMENT ROUTING
-- ========================================================
--
-- CREDIT BOOKING:
-- meglévő refund RPC kezeli.
--
-- PAID VENUE RECURRING:
-- meglévő refund RPC season_pass kreditet ír vissza.
--
-- FULLY PAID VENUE PLAN:
-- szintén refund.
--
-- PARTIALLY PAID PLAN:
-- fail closed, mert occurrence-level fizetettség nem
-- bizonyítható biztonságosan csak a plan státuszból.
--
-- VENUE + PENDING:
-- régi no-refund logika marad.
-- ========================================================
IF v_locked_booking.payment_method IN (
'credit_standard',
'credit_season_pass'
) THEN
v_use_credit_refund := true;
ELSIF v_locked_booking.payment_method = 'venue' THEN
IF v_locked_booking.payment_status = 'paid'
OR v_has_positive_booking_payment
OR (
v_has_venue_plan
AND v_plan_status = 'paid'
)
THEN
v_use_credit_refund := true;
ELSIF v_has_venue_plan
AND v_plan_status = 'partially_paid'
THEN
RAISE EXCEPTION
'Partially paid venue season cancellation requires financial review.';
ELSIF v_locked_booking.payment_status = 'pending' THEN
v_can_no_refund := true;
ELSE
RAISE EXCEPTION
'Unsupported venue payment state for cancellation.';
END IF;
ELSIF v_locked_booking.payment_method = 'supported'
AND COALESCE(
v_locked_booking.price_per_booking,
0
) = 0
THEN
v_can_no_refund := true;
ELSIF v_locked_booking.payment_method IS NULL
AND COALESCE(
v_locked_booking.price_per_booking,
0
) = 0
THEN
v_can_no_refund := true;
ELSE
RAISE EXCEPTION
'Unsupported payment state for recurring cancellation.';
END IF;
-- ========================================================
-- 12. PAID / CREDIT -> EXISTING REFUND ENGINE
-- ========================================================
IF v_use_credit_refund THEN
v_refund_result :=
public.cancel_booking_with_credit_refund(
p_booking_id,
p_cancellation_reason_category,
p_cancellation_reason
);
-- ========================================================
-- 13. UNPAID VENUE / SUPPORTED -> NO REFUND
-- ========================================================
ELSE
IF NOT v_can_no_refund THEN
RAISE EXCEPTION
'Cancellation payment classification failed.';
END IF;
-- ----------------------------------------------------
-- Existing unpaid venue partial cancellation protection
-- ----------------------------------------------------
IF v_locked_booking.payment_method = 'venue'
AND v_locked_booking.payment_status = 'pending'
AND v_has_venue_plan
THEN
SELECT COUNT(*)
INTO v_total_active_count
FROM public.bookings b
WHERE b.recurring_series_id = v_series_id
AND b.club_id = v_club_id
AND b.season_id = v_season_id
AND b.sport = v_sport
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, ''))
NOT IN (
'cancelled',
'deleted',
'deleted by club',
'failed'
);
IF v_total_active_count > 1 THEN
RAISE EXCEPTION
'Partial cancellation of an unpaid venue season requires payment plan adjustment.';
END IF;
END IF;
UPDATE public.bookings
SET
status = 'cancelled',
deleted_at = v_now,
cancelled_by_profile_id =
v_caller_profile.id,
cancelled_by_name =
v_caller_name,
cancelled_by_role =
v_caller_profile.role,
cancellation_reason_category =
p_cancellation_reason_category,
cancellation_reason =
NULLIF(
btrim(p_cancellation_reason),
''
),
updated_at = v_now
WHERE id = p_booking_id
AND club_id = v_club_id
AND sport = v_sport;
END IF;
-- ========================================================
-- 14. UPDATE SERIES STATUS
-- ========================================================
SELECT COUNT(*)
INTO v_active_count
FROM public.bookings b
WHERE b.recurring_series_id = v_series_id
AND b.club_id = v_club_id
AND b.season_id = v_season_id
AND b.sport = v_sport
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, ''))
NOT IN (
'cancelled',
'deleted',
'deleted by club',
'failed'
);
IF v_active_count = 0 THEN
UPDATE public.recurring_booking_series
SET
status = 'cancelled',
updated_at = v_now
WHERE id = v_series_id;
-- Pending plan only.
-- Paid plan remains historical financial record.
IF v_has_venue_plan
AND v_plan_status = 'pending'
THEN
UPDATE public.venue_payment_plans
SET
plan_status = 'cancelled',
updated_at = v_now
WHERE id = v_plan_id
AND plan_status = 'pending';
END IF;
ELSE
UPDATE public.recurring_booking_series
SET
status = 'active',
updated_at = v_now
WHERE id = v_series_id;
END IF;
-- ========================================================
-- 15. RESULT
-- ========================================================
RETURN jsonb_build_object(
'success',
true,
'booking_id',
p_booking_id,
'sport',
v_sport,
'refund_issued',
v_use_credit_refund,
'refund_amount',
CASE
WHEN v_use_credit_refund
THEN COALESCE(
(v_refund_result ->> 'refund_amount')::numeric,
0
)
ELSE 0
END,
'credit_type',
CASE
WHEN v_use_credit_refund
THEN v_refund_result ->> 'credit_type'
ELSE NULL
END,
'wallet_balance',
CASE
WHEN v_use_credit_refund
THEN v_refund_result -> 'wallet_balance'
ELSE NULL
END,
'cancelled_by_profile_id',
v_caller_profile.id,
'cancelled_by_name',
v_caller_name,
'cancelled_by_role',
v_caller_profile.role
);
END;
$function$
"
public,cancel_recurring_season_remaining,f,"p_booking_id uuid, p_cancellation_reason_category text, p_cancellation_reason text","p_booking_id uuid, p_cancellation_reason_category text DEFAULT NULL::text, p_cancellation_reason text DEFAULT NULL::text",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.cancel_recurring_season_remaining(p_booking_id uuid, p_cancellation_reason_category text DEFAULT NULL::text, p_cancellation_reason text DEFAULT NULL::text)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_source_booking RECORD;
v_caller_profile RECORD;
v_club_id uuid;
v_season_id uuid;
v_series_id uuid;
v_sport text;
v_is_admin boolean := false;
v_is_coach boolean := false; -- COACH DIFF (a)
v_is_own_booking boolean := false; -- COACH DIFF (a)
v_effective_actor_role text; -- AUDIT-ONLY canonical role
v_now timestamptz := now();
v_cancelled_count integer := 0;
v_occurrence RECORD;
v_caller_name text;
v_affected_count integer;
v_total_active_count integer;
v_active_after integer;
v_plan_record RECORD;
BEGIN
-- 1. auth required
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Authentication required.';
END IF;
-- 2. Load source booking FIRST
SELECT b.* INTO v_source_booking
FROM public.bookings b
WHERE b.id = p_booking_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Booking not found.';
END IF;
v_club_id := v_source_booking.club_id;
v_season_id := v_source_booking.season_id;
v_series_id := v_source_booking.recurring_series_id;
v_sport := v_source_booking.sport;
-- 3. Validate: must be a recurring series booking
IF v_series_id IS NULL THEN
RAISE EXCEPTION 'This booking is not part of a recurring series.';
END IF;
-- 4. Validate canonical booking_type for normal recurring
IF lower(COALESCE(v_source_booking.booking_type, '')) <> 'recurring' THEN
RAISE EXCEPTION 'This RPC only handles normal recurring bookings.';
END IF;
-- 5. Resolve caller profile WITHIN this club
SELECT up.id, up.club_id, up.role, up.full_name, up.first_name, up.last_name
INTO v_caller_profile
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_club_id
LIMIT 1;
IF NOT FOUND THEN
RAISE EXCEPTION 'Caller profile not found in this club.';
END IF;
-- 6. Club isolation
IF v_caller_profile.club_id != v_club_id THEN
RAISE EXCEPTION 'You do not have permission to cancel this booking.';
END IF;
-- 7. Already cancelled check
IF v_source_booking.status = 'cancelled' OR v_source_booking.deleted_at IS NOT NULL THEN
RAISE EXCEPTION 'This booking has already been cancelled or deleted.';
END IF;
-- 8. Academy isolation: reject if SOURCE is Academy
IF EXISTS (
SELECT 1 FROM public.academy_booking_details abd
WHERE abd.booking_id = p_booking_id
) THEN
RAISE EXCEPTION 'Academy bookings must be cancelled through the Academy cancellation system.';
END IF;
-- 9. Admin-only authorization (extended for coach-own)
v_is_admin := v_caller_profile.role IN ('owner', 'admin', 'secondary_admin');
-- COACH DIFF (a): canonical coach identity (role OR active
-- coach_profiles membership via user_profile_id).
v_is_coach :=
(v_caller_profile.role = 'coach')
OR EXISTS (
SELECT 1
FROM public.coach_profiles cp
WHERE cp.user_profile_id = v_caller_profile.id
AND cp.is_active = true
);
-- AUDIT-ONLY effective actor role (canonical). NOT used for
-- authorization — only for cancelled_by_role audit + output.
v_effective_actor_role :=
CASE
WHEN v_caller_profile.role IN ('owner', 'admin', 'secondary_admin')
THEN v_caller_profile.role
WHEN v_is_coach
THEN 'coach'
ELSE v_caller_profile.role
END;
IF NOT v_is_admin THEN
-- COACH DIFF (b): coach on OWN booking skips past + cutoff.
-- Coach on ANOTHER user's booking is DENIED via the same
-- ownership rule as everyone else (LIVE unchanged).
IF v_is_coach
AND v_source_booking.user_id IS DISTINCT FROM v_caller_profile.id
AND v_source_booking.created_by_user_id IS DISTINCT FROM v_caller_profile.id
THEN
RAISE EXCEPTION 'Coaches can only cancel their own bookings.';
END IF;
IF v_is_coach
AND NOT (
v_source_booking.user_id IS DISTINCT FROM v_caller_profile.id
AND v_source_booking.created_by_user_id IS DISTINCT FROM v_caller_profile.id
)
THEN
-- Coach owns this recurring series booking. Admin-equivalent
-- TIMING: skip past-booking guard + cutoff. Bulk financial
-- protections below are IDENTICAL to LIVE.
v_is_own_booking := true;
ELSE
-- Every NON-admin, NON-(coach-own) caller: LIVE logic unchanged.
IF v_source_booking.user_id IS DISTINCT FROM v_caller_profile.id
AND v_source_booking.created_by_user_id IS DISTINCT FROM v_caller_profile.id
THEN
RAISE EXCEPTION 'Unauthorized to cancel this booking.';
END IF;
-- Same past-booking / cancellation-cutoff rule
IF (
(v_source_booking.booking_date + v_source_booking.start_time)
AT TIME ZONE COALESCE(
(
SELECT c.timezone
FROM public.clubs c
WHERE c.id = v_source_booking.club_id
),
'UTC'
)
) < now()
THEN
RAISE EXCEPTION 'Cannot cancel a past booking.';
END IF;
IF (
(v_source_booking.booking_date + v_source_booking.start_time)
AT TIME ZONE COALESCE(
(
SELECT c.timezone
FROM public.clubs c
WHERE c.id = v_source_booking.club_id
),
'UTC'
)
) <
now()
+
(
COALESCE(
(
SELECT c.cancellation_cutoff_hours
FROM public.clubs c
WHERE c.id = v_source_booking.club_id
),
0
) || ' hours'
)::interval
THEN
RAISE EXCEPTION 'Cancellation cutoff time has passed.';
END IF;
END IF;
END IF;
-- 10. Validate reason category
IF p_cancellation_reason_category IS NULL OR p_cancellation_reason_category NOT IN (
'illness', 'schedule_conflict', 'coach_unavailable',
'player_request', 'weather', 'court_unavailable',
'club_decision', 'other'
) THEN
RAISE EXCEPTION 'A cancellation reason category is required.';
END IF;
IF p_cancellation_reason_category = 'other' AND (p_cancellation_reason IS NULL OR trim(p_cancellation_reason) = '') THEN
RAISE EXCEPTION 'A cancellation reason is required when the category is ""other"".';
END IF;
-- COACH DIFF (c): coach own-booking → non-empty textual reason required.
-- Admins and normal users are UNCHANGED. Empty / whitespace → DENIED.
IF v_is_coach
AND v_is_own_booking
AND (
p_cancellation_reason IS NULL
OR btrim(p_cancellation_reason) = ''
)
THEN
RAISE EXCEPTION 'A cancellation reason is required for coach own-booking cancellation.';
END IF;
v_caller_name := COALESCE(
v_caller_profile.full_name,
trim(COALESCE(v_caller_profile.first_name, '') || ' ' || COALESCE(v_caller_profile.last_name, ''))
);
-- 11. LOCK venue_payment_plans FOR UPDATE before classification (race-safe vs Mark-as-paid)
SELECT * INTO v_plan_record
FROM public.venue_payment_plans
WHERE recurring_series_id = v_series_id
AND club_id = v_club_id
FOR UPDATE;
-- 12. Count AFFECTED bookings (source date onwards)
SELECT COUNT(*) INTO v_affected_count
FROM public.bookings b
WHERE b.recurring_series_id = v_series_id
AND b.club_id = v_club_id
AND b.season_id = v_season_id
AND b.sport = v_sport
AND b.booking_date >= v_source_booking.booking_date
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, '')) NOT IN ('cancelled', 'deleted', 'deleted by club', 'failed');
IF v_affected_count = 0 THEN
RAISE EXCEPTION 'No active occurrences remain to cancel.';
END IF;
-- 12b. Count TOTAL active bookings in the entire series
SELECT COUNT(*) INTO v_total_active_count
FROM public.bookings b
WHERE b.recurring_series_id = v_series_id
AND b.club_id = v_club_id
AND b.season_id = v_season_id
AND b.sport = v_sport
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, '')) NOT IN ('cancelled', 'deleted', 'deleted by club', 'failed');
-- 12c. VENUE PARTIAL CANCEL PROTECTION
-- If there's a venue plan and affected < total active, this is a partial cancellation → BLOCK
IF v_plan_record.id IS NOT NULL AND v_affected_count < v_total_active_count THEN
RAISE EXCEPTION 'Partial cancellation of an unpaid venue season requires payment plan adjustment.';
END IF;
-- 13. PRE-VALIDATE: FAIL CLOSED IF ANY AFFECTED OCCURRENCE IS ACADEMY
IF EXISTS (
SELECT 1
FROM public.bookings b
JOIN public.academy_booking_details abd ON abd.booking_id = b.id
WHERE b.recurring_series_id = v_series_id
AND b.club_id = v_club_id
AND b.season_id = v_season_id
AND b.sport = v_sport
AND b.booking_date >= v_source_booking.booking_date
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, '')) NOT IN ('cancelled', 'deleted', 'deleted by club', 'failed')
) THEN
RAISE EXCEPTION 'One or more remaining occurrences is an Academy booking. Academy bookings must be cancelled through the Academy cancellation system.';
END IF;
-- 14. PRE-VALIDATE: GENERIC positive paid payment detection
-- ANY booking_payments with payment_status='paid' AND amount > 0 blocks
IF EXISTS (
SELECT 1
FROM public.bookings b
JOIN public.booking_payments bp ON bp.booking_id = b.id
WHERE b.recurring_series_id = v_series_id
AND b.club_id = v_club_id
AND b.season_id = v_season_id
AND b.sport = v_sport
AND b.booking_date >= v_source_booking.booking_date
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, '')) NOT IN ('cancelled', 'deleted', 'deleted by club', 'failed')
AND bp.payment_status = 'paid'
AND COALESCE(bp.amount, 0) > 0
) THEN
RAISE EXCEPTION 'One or more remaining bookings are already paid. Paid bulk cancellation requires financial review.';
END IF;
-- 15. PRE-VALIDATE: venue_payment_plans paid/partially_paid
IF v_plan_record.id IS NOT NULL AND v_plan_record.plan_status IN ('partially_paid', 'paid') THEN
RAISE EXCEPTION 'One or more remaining bookings are already paid. Paid bulk cancellation requires financial review.';
END IF;
-- 16. PRE-VALIDATE: NULL payment_method + positive price
IF EXISTS (
SELECT 1
FROM public.bookings b
WHERE b.recurring_series_id = v_series_id
AND b.club_id = v_club_id
AND b.season_id = v_season_id
AND b.sport = v_sport
AND b.booking_date >= v_source_booking.booking_date
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, '')) NOT IN ('cancelled', 'deleted', 'deleted by club', 'failed')
AND b.payment_method IS NULL
AND COALESCE(b.price_per_booking, 0) > 0
) THEN
RAISE EXCEPTION 'One or more occurrences has incomplete payment data (NULL payment method with positive price). Cancellation requires payment review.';
END IF;
-- 17. PRE-VALIDATE: unsupported payment state (only venue+pending, supported+0, NULL+0 allowed)
IF EXISTS (
SELECT 1
FROM public.bookings b
WHERE b.recurring_series_id = v_series_id
AND b.club_id = v_club_id
AND b.season_id = v_season_id
AND b.sport = v_sport
AND b.booking_date >= v_source_booking.booking_date
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, '')) NOT IN ('cancelled', 'deleted', 'deleted by club', 'failed')
AND NOT (
(b.payment_method = 'venue' AND b.payment_status = 'pending')
OR (b.payment_method = 'supported' AND COALESCE(b.price_per_booking, 0) = 0)
OR (b.payment_method IS NULL AND COALESCE(b.price_per_booking, 0) = 0)
)
) THEN
RAISE EXCEPTION 'One or more occurrences has an unsupported payment state for bulk no-refund cancellation.';
END IF;
-- 18. LOCK affected booking rows FOR UPDATE (deterministic order) before mutation
FOR v_occurrence IN
SELECT b.id, b.booking_date, b.start_time
FROM public.bookings b
WHERE b.recurring_series_id = v_series_id
AND b.club_id = v_club_id
AND b.season_id = v_season_id
AND b.sport = v_sport
AND b.booking_date >= v_source_booking.booking_date
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, '')) NOT IN ('cancelled', 'deleted', 'deleted by club', 'failed')
ORDER BY b.booking_date, b.start_time, b.id
FOR UPDATE
LOOP
-- 19. Inline no-refund cancellation (multi-club safe, does NOT call helper)
UPDATE public.bookings
SET
status = 'cancelled',
deleted_at = v_now,
cancelled_by_profile_id = v_caller_profile.id,
cancelled_by_name = v_caller_name,
cancelled_by_role = v_effective_actor_role,
cancellation_reason_category = p_cancellation_reason_category,
cancellation_reason = NULLIF(btrim(p_cancellation_reason), ''),
updated_at = v_now
WHERE id = v_occurrence.id;
v_cancelled_count := v_cancelled_count + 1;
END LOOP;
-- 20. Update recurring_booking_series status ('active' or 'cancelled' ONLY)
SELECT COUNT(*) INTO v_active_after
FROM public.bookings b
WHERE b.recurring_series_id = v_series_id
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, '')) NOT IN ('cancelled', 'deleted', 'deleted by club', 'failed');
IF v_active_after = 0 THEN
UPDATE public.recurring_booking_series
SET status = 'cancelled', updated_at = v_now
WHERE id = v_series_id;
-- If there's a PENDING venue payment plan, cancel it too
IF v_plan_record.id IS NOT NULL AND v_plan_record.plan_status = 'pending' THEN
UPDATE public.venue_payment_plans
SET plan_status = 'cancelled', updated_at = v_now
WHERE id = v_plan_record.id;
END IF;
ELSE
UPDATE public.recurring_booking_series
SET status = 'active', updated_at = v_now
WHERE id = v_series_id;
END IF;
-- 21. Return summary
RETURN jsonb_build_object(
'success', true,
'cancelled_count', v_cancelled_count,
'cancelled_by_profile_id', v_caller_profile.id,
'cancelled_by_name', v_caller_name,
'cancelled_by_role', v_effective_actor_role,
'cancellation_reason_category', p_cancellation_reason_category
);
END;
$function$
"
public,check_booking_court_sport_consistency,f,,,trigger,plpgsql,true,VOLATILE,postgres,"{=X/postgres,postgres=X/postgres,anon=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.check_booking_court_sport_consistency()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_court_sport text;
BEGIN
IF NEW.court_id IS NOT NULL THEN
SELECT sport INTO v_court_sport
FROM public.courts
WHERE id = NEW.court_id;
IF v_court_sport IS NOT NULL AND NEW.sport IS NOT NULL THEN
IF v_court_sport <> NEW.sport THEN
RAISE EXCEPTION
'Booking sport (%) does not match court sport (%). Booking ID: %',
NEW.sport, v_court_sport, NEW.id;
END IF;
END IF;
END IF;
RETURN NEW;
END;
$function$
"
public,check_booking_season_sport_consistency,f,,,trigger,plpgsql,true,VOLATILE,postgres,"{=X/postgres,postgres=X/postgres,anon=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.check_booking_season_sport_consistency()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_season_sport text;
BEGIN
IF NEW.season_id IS NOT NULL THEN
SELECT sport INTO v_season_sport
FROM public.seasons
WHERE id = NEW.season_id;
IF v_season_sport IS NOT NULL AND NEW.sport IS NOT NULL THEN
IF v_season_sport <> NEW.sport THEN
RAISE EXCEPTION
'Booking sport (%) does not match season sport (%). Booking ID: %',
NEW.sport, v_season_sport, NEW.id;
END IF;
END IF;
END IF;
RETURN NEW;
END;
$function$
"
public,check_court_season_sport_consistency,f,,,trigger,plpgsql,true,VOLATILE,postgres,"{=X/postgres,postgres=X/postgres,anon=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.check_court_season_sport_consistency()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_season_sport text;
BEGIN
-- Only check if season_id is set
IF NEW.season_id IS NOT NULL THEN
SELECT sport INTO v_season_sport
FROM public.seasons
WHERE id = NEW.season_id;
IF v_season_sport IS NOT NULL AND NEW.sport IS NOT NULL THEN
IF v_season_sport <> NEW.sport THEN
RAISE EXCEPTION
'Court sport (%) does not match season sport (%). Court ID: %',
NEW.sport, v_season_sport, NEW.id;
END IF;
END IF;
END IF;
RETURN NEW;
END;
$function$
"
public,check_pricing_group_court_consistency,f,,,trigger,plpgsql,true,VOLATILE,postgres,"{=X/postgres,postgres=X/postgres,anon=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.check_pricing_group_court_consistency()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_group_season_id uuid;
v_group_sport text;
v_group_club_id uuid;
v_court_season_id uuid;
v_court_sport text;
v_court_club_id uuid;
BEGIN
SELECT season_id, sport, club_id
INTO v_group_season_id, v_group_sport, v_group_club_id
FROM public.pricing_groups WHERE id = NEW.pricing_group_id;
IF v_group_season_id IS NULL THEN
RAISE EXCEPTION 'Target pricing group % does not exist.', NEW.pricing_group_id;
END IF;
SELECT season_id, sport, club_id
INTO v_court_season_id, v_court_sport, v_court_club_id
FROM public.courts WHERE id = NEW.court_id;
IF v_court_season_id IS NULL THEN
RAISE EXCEPTION 'Target court % does not exist.', NEW.court_id;
END IF;
IF v_group_club_id IS DISTINCT FROM v_court_club_id THEN
RAISE EXCEPTION 'Club mismatch: group club % vs court club %', v_group_club_id, v_court_club_id;
END IF;
IF v_group_season_id IS DISTINCT FROM v_court_season_id THEN
RAISE EXCEPTION 'Season mismatch: group season % vs court season %', v_group_season_id, v_court_season_id;
END IF;
IF v_group_sport IS DISTINCT FROM v_court_sport THEN
RAISE EXCEPTION 'Sport mismatch: group sport % vs court sport %', v_group_sport, v_court_sport;
END IF;
RETURN NEW;
END;
$function$
"
public,coach_check_in_booking,f,p_booking_id uuid,p_booking_id uuid,jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.coach_check_in_booking(p_booking_id uuid)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_booking record;
v_user_profile_id uuid;
v_coach_profile_id uuid;
v_academy_type text;
v_checkin_id uuid;
v_timezone text;
v_local_now timestamp;
v_booking_start timestamp;
v_booking_end timestamp;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Not authenticated';
END IF;
SELECT *
INTO v_booking
FROM public.bookings
WHERE id = p_booking_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Booking not found';
END IF;
IF v_booking.deleted_at IS NOT NULL
OR v_booking.status IN (
'cancelled',
'deleted by club',
'deleted'
) THEN
RAISE EXCEPTION
'Cannot check in to a deleted or cancelled booking';
END IF;
SELECT
up.id,
cp.id
INTO
v_user_profile_id,
v_coach_profile_id
FROM public.user_profiles up
JOIN public.coach_profiles cp
ON cp.user_profile_id = up.id
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_booking.club_id
AND cp.is_active = true
ORDER BY
CASE
WHEN cp.id =
v_booking.checked_in_coach_profile_id
THEN 0
ELSE 1
END,
cp.id
LIMIT 1;
IF v_user_profile_id IS NULL
OR v_coach_profile_id IS NULL THEN
RAISE EXCEPTION
'Active coach profile not found in the booking club';
END IF;
SELECT abd.academy_type
INTO v_academy_type
FROM public.academy_booking_details abd
WHERE abd.booking_id = p_booking_id
LIMIT 1;
SELECT COALESCE(
NULLIF(c.timezone, ''),
'UTC'
)
INTO v_timezone
FROM public.clubs c
WHERE c.id = v_booking.club_id;
v_timezone := COALESCE(v_timezone, 'UTC');
v_local_now :=
now() AT TIME ZONE v_timezone;
v_booking_start :=
(
v_booking.booking_date
+ v_booking.start_time
)::timestamp;
v_booking_end :=
(
v_booking.booking_date
+ v_booking.end_time
)::timestamp;
IF v_local_now <
v_booking_start - interval '7 days' THEN
RAISE EXCEPTION
'Cannot check in more than 7 days before the booking';
END IF;
IF v_local_now > v_booking_end THEN
RAISE EXCEPTION
'Cannot check in after the booking has ended';
END IF;
IF v_academy_type IN ('group', 'camp') THEN
INSERT INTO public.academy_booking_coach_checkins (
club_id,
booking_id,
coach_profile_id,
checked_in_at
)
VALUES (
v_booking.club_id,
p_booking_id,
v_coach_profile_id,
now()
)
ON CONFLICT (booking_id, coach_profile_id)
WHERE checked_out_at IS NULL
DO NOTHING
RETURNING id
INTO v_checkin_id;
IF v_checkin_id IS NOT NULL THEN
INSERT INTO public.academy_coach_checkin_audit_logs (
club_id,
booking_id,
coach_profile_id,
action,
action_by_user_profile_id,
action_at
)
VALUES (
v_booking.club_id,
p_booking_id,
v_coach_profile_id,
'check_in',
v_user_profile_id,
now()
);
END IF;
ELSE
IF v_booking.checked_in_coach_profile_id
IS DISTINCT FROM v_coach_profile_id THEN
UPDATE public.bookings
SET
checked_in_coach_profile_id =
v_coach_profile_id,
coach_checked_in_at = now()
WHERE id = p_booking_id;
END IF;
END IF;
RETURN jsonb_build_object(
'success', true,
'booking_id', p_booking_id,
'coach_profile_id', v_coach_profile_id,
'academy_type', v_academy_type,
'created_new_checkin',
v_checkin_id IS NOT NULL
);
END;
$function$
"
public,coach_check_in_booking_series,f,p_booking_id uuid,p_booking_id uuid,jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.coach_check_in_booking_series(p_booking_id uuid)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_booking record;
v_occurrence record;
v_user_profile_id uuid;
v_coach_profile_id uuid;
v_checkin_id uuid;
v_updated_count integer := 0;
v_timezone text;
v_local_now timestamp;
v_booking_start timestamp;
v_booking_end timestamp;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Not authenticated';
END IF;
SELECT *
INTO v_booking
FROM public.bookings
WHERE id = p_booking_id
AND deleted_at IS NULL
AND status NOT IN (
'cancelled',
'deleted by club',
'deleted'
);
IF NOT FOUND THEN
RAISE EXCEPTION
'Booking not found or cancelled';
END IF;
IF v_booking.recurring_series_id IS NULL THEN
RAISE EXCEPTION
'Booking is not part of a recurring series';
END IF;
SELECT
up.id,
cp.id
INTO
v_user_profile_id,
v_coach_profile_id
FROM public.user_profiles up
JOIN public.coach_profiles cp
ON cp.user_profile_id = up.id
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_booking.club_id
AND cp.is_active = true
ORDER BY
CASE
WHEN cp.id =
v_booking.checked_in_coach_profile_id
THEN 0
ELSE 1
END,
cp.id
LIMIT 1;
IF v_coach_profile_id IS NULL THEN
RAISE EXCEPTION
'Active coach profile not found';
END IF;
SELECT COALESCE(
NULLIF(c.timezone, ''),
'UTC'
)
INTO v_timezone
FROM public.clubs c
WHERE c.id = v_booking.club_id;
v_timezone := COALESCE(v_timezone, 'UTC');
FOR v_occurrence IN
SELECT
b.id,
b.booking_date,
b.start_time,
b.end_time,
b.checked_in_coach_profile_id,
abd.academy_type
FROM public.bookings b
LEFT JOIN public.academy_booking_details abd
ON abd.booking_id = b.id
WHERE b.recurring_series_id =
v_booking.recurring_series_id
AND b.club_id = v_booking.club_id
AND b.deleted_at IS NULL
AND b.status NOT IN (
'cancelled',
'deleted by club',
'deleted'
)
LOOP
v_checkin_id := NULL;
v_local_now :=
now() AT TIME ZONE v_timezone;
v_booking_start :=
(
v_occurrence.booking_date
+ v_occurrence.start_time
)::timestamp;
v_booking_end :=
(
v_occurrence.booking_date
+ v_occurrence.end_time
)::timestamp;
IF v_local_now >=
v_booking_start - interval '7 days'
AND v_local_now <= v_booking_end THEN
IF v_occurrence.academy_type
IN ('group', 'camp') THEN
INSERT INTO public.academy_booking_coach_checkins (
club_id,
booking_id,
coach_profile_id,
checked_in_at
)
VALUES (
v_booking.club_id,
v_occurrence.id,
v_coach_profile_id,
now()
)
ON CONFLICT (booking_id, coach_profile_id)
WHERE checked_out_at IS NULL
DO NOTHING
RETURNING id
INTO v_checkin_id;
IF v_checkin_id IS NOT NULL THEN
INSERT INTO public.academy_coach_checkin_audit_logs (
club_id,
booking_id,
coach_profile_id,
action,
action_by_user_profile_id,
action_at
)
VALUES (
v_booking.club_id,
v_occurrence.id,
v_coach_profile_id,
'check_in',
v_user_profile_id,
now()
);
v_updated_count :=
v_updated_count + 1;
END IF;
ELSE
UPDATE public.bookings
SET
checked_in_coach_profile_id =
v_coach_profile_id,
coach_checked_in_at = now()
WHERE id = v_occurrence.id
AND checked_in_coach_profile_id
IS DISTINCT FROM
v_coach_profile_id;
IF FOUND THEN
v_updated_count :=
v_updated_count + 1;
END IF;
END IF;
END IF;
END LOOP;
RETURN jsonb_build_object(
'success', true,
'recurring_series_id',
v_booking.recurring_series_id,
'updated_count',
v_updated_count,
'coach_profile_id',
v_coach_profile_id
);
END;
$function$
"
public,coach_check_out_booking,f,p_booking_id uuid,p_booking_id uuid,jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.coach_check_out_booking(p_booking_id uuid)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_booking record;
v_academy_type text;
v_closed record;
v_updated_count integer := 0;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Not authenticated';
END IF;
SELECT *
INTO v_booking
FROM public.bookings
WHERE id = p_booking_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Booking not found';
END IF;
IF NOT EXISTS (
SELECT 1
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_booking.club_id
) THEN
RAISE EXCEPTION
'User does not belong to the booking club';
END IF;
SELECT abd.academy_type
INTO v_academy_type
FROM public.academy_booking_details abd
WHERE abd.booking_id = p_booking_id
LIMIT 1;
IF v_academy_type IN ('group', 'camp') THEN
FOR v_closed IN
WITH closed AS (
UPDATE public.academy_booking_coach_checkins ci
SET
checked_out_at = now(),
updated_at = now()
FROM public.coach_profiles cp,
public.user_profiles up
WHERE ci.booking_id = p_booking_id
AND ci.club_id = v_booking.club_id
AND ci.checked_out_at IS NULL
AND cp.id = ci.coach_profile_id
AND cp.is_active = true
AND up.id = cp.user_profile_id
AND up.auth_user_id = auth.uid()
AND up.club_id = v_booking.club_id
RETURNING
ci.id,
ci.club_id,
ci.booking_id,
ci.coach_profile_id,
up.id AS user_profile_id
)
SELECT *
FROM closed
LOOP
INSERT INTO public.academy_coach_checkin_audit_logs (
club_id,
booking_id,
coach_profile_id,
action,
action_by_user_profile_id,
action_at
)
VALUES (
v_closed.club_id,
v_closed.booking_id,
v_closed.coach_profile_id,
'check_out',
v_closed.user_profile_id,
now()
);
v_updated_count :=
v_updated_count + 1;
END LOOP;
ELSE
UPDATE public.bookings b
SET
checked_in_coach_profile_id = NULL,
coach_checked_in_at = NULL
WHERE b.id = p_booking_id
AND b.checked_in_coach_profile_id IN (
SELECT cp.id
FROM public.user_profiles up
JOIN public.coach_profiles cp
ON cp.user_profile_id = up.id
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_booking.club_id
AND cp.is_active = true
);
GET DIAGNOSTICS
v_updated_count = ROW_COUNT;
END IF;
RETURN jsonb_build_object(
'success', true,
'booking_id', p_booking_id,
'academy_type', v_academy_type,
'updated_count', v_updated_count
);
END;
$function$
"
public,coach_check_out_booking_series,f,p_booking_id uuid,p_booking_id uuid,jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.coach_check_out_booking_series(p_booking_id uuid)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_booking record;
v_occurrence record;
v_closed record;
v_updated_count integer := 0;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Not authenticated';
END IF;
SELECT *
INTO v_booking
FROM public.bookings
WHERE id = p_booking_id
AND deleted_at IS NULL
AND status NOT IN (
'cancelled',
'deleted by club',
'deleted'
);
IF NOT FOUND THEN
RAISE EXCEPTION
'Booking not found or cancelled';
END IF;
IF v_booking.recurring_series_id IS NULL THEN
RAISE EXCEPTION
'Booking is not part of a recurring series';
END IF;
IF NOT EXISTS (
SELECT 1
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_booking.club_id
) THEN
RAISE EXCEPTION
'User does not belong to the booking club';
END IF;
FOR v_occurrence IN
SELECT
b.id,
b.checked_in_coach_profile_id,
abd.academy_type
FROM public.bookings b
LEFT JOIN public.academy_booking_details abd
ON abd.booking_id = b.id
WHERE b.recurring_series_id =
v_booking.recurring_series_id
AND b.club_id = v_booking.club_id
AND b.deleted_at IS NULL
AND b.status NOT IN (
'cancelled',
'deleted by club',
'deleted'
)
LOOP
IF v_occurrence.academy_type
IN ('group', 'camp') THEN
FOR v_closed IN
WITH closed AS (
UPDATE public.academy_booking_coach_checkins ci
SET
checked_out_at = now(),
updated_at = now()
FROM public.coach_profiles cp,
public.user_profiles up
WHERE ci.booking_id =
v_occurrence.id
AND ci.club_id =
v_booking.club_id
AND ci.checked_out_at IS NULL
AND cp.id =
ci.coach_profile_id
AND cp.is_active = true
AND up.id =
cp.user_profile_id
AND up.auth_user_id =
auth.uid()
AND up.club_id =
v_booking.club_id
RETURNING
ci.id,
ci.club_id,
ci.booking_id,
ci.coach_profile_id,
up.id AS user_profile_id
)
SELECT *
FROM closed
LOOP
INSERT INTO public.academy_coach_checkin_audit_logs (
club_id,
booking_id,
coach_profile_id,
action,
action_by_user_profile_id,
action_at
)
VALUES (
v_closed.club_id,
v_closed.booking_id,
v_closed.coach_profile_id,
'check_out',
v_closed.user_profile_id,
now()
);
v_updated_count :=
v_updated_count + 1;
END LOOP;
ELSE
UPDATE public.bookings b
SET
checked_in_coach_profile_id = NULL,
coach_checked_in_at = NULL
WHERE b.id = v_occurrence.id
AND b.checked_in_coach_profile_id IN (
SELECT cp.id
FROM public.user_profiles up
JOIN public.coach_profiles cp
ON cp.user_profile_id = up.id
WHERE up.auth_user_id =
auth.uid()
AND up.club_id =
v_booking.club_id
AND cp.is_active = true
);
IF FOUND THEN
v_updated_count :=
v_updated_count + 1;
END IF;
END IF;
END LOOP;
RETURN jsonb_build_object(
'success', true,
'recurring_series_id',
v_booking.recurring_series_id,
'updated_count',
v_updated_count
);
END;
$function$
"
public,coach_profile_club_id,f,target_coach_profile_id uuid,target_coach_profile_id uuid,uuid,sql,true,STABLE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.coach_profile_club_id(target_coach_profile_id uuid)
RETURNS uuid
LANGUAGE sql
STABLE SECURITY DEFINER
SET search_path TO ''
AS $function$
SELECT COALESCE(
(
SELECT up.club_id
FROM public.coach_profiles AS cp
JOIN public.user_profiles AS up
ON up.id = cp.user_profile_id
WHERE cp.id = target_coach_profile_id
LIMIT 1
),
(
SELECT up.club_id
FROM public.coach_profiles AS cp
JOIN public.user_profiles AS up
ON up.auth_user_id = cp.user_id
WHERE cp.id = target_coach_profile_id
LIMIT 1
)
);
$function$
"
public,coach_profiles_identity_compat,f,,,trigger,plpgsql,false,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.coach_profiles_identity_compat()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
-- Ha a bejelentkezett coach saját profilt készít,
-- automatikusan megkapja az Auth user ID-t.
IF NEW.user_id IS NULL THEN
NEW.user_id := auth.uid();
END IF;
-- A tényleges public.user_profiles rekord azonosítóját keressük meg.
IF NEW.user_profile_id IS NULL AND NEW.user_id IS NOT NULL THEN
SELECT up.id
INTO NEW.user_profile_id
FROM public.user_profiles AS up
WHERE up.auth_user_id = NEW.user_id
ORDER BY up.created_at ASC
LIMIT 1;
END IF;
NEW.updated_at := now();
RETURN NEW;
END;
$function$
"
public,create_academy_booking,f,"p_club_id uuid, p_season_id uuid, p_court_id uuid, p_first_booking_date date, p_start_time time without time zone, p_end_time time without time zone, p_academy_type text, p_booking_scope text, p_academy_program_id uuid, p_individual_player_name text, p_court_fee_huf_per_occurrence numeric, p_booking_note text, p_sport text","p_club_id uuid, p_season_id uuid, p_court_id uuid, p_first_booking_date date, p_start_time time without time zone, p_end_time time without time zone, p_academy_type text, p_booking_scope text, p_academy_program_id uuid, p_individual_player_name text, p_court_fee_huf_per_occurrence numeric, p_booking_note text, p_sport text DEFAULT 'tennis'::text",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.create_academy_booking(p_club_id uuid, p_season_id uuid, p_court_id uuid, p_first_booking_date date, p_start_time time without time zone, p_end_time time without time zone, p_academy_type text, p_booking_scope text, p_academy_program_id uuid, p_individual_player_name text, p_court_fee_huf_per_occurrence numeric, p_booking_note text, p_sport text DEFAULT 'tennis'::text)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_profile_id uuid;
v_caller_role text;
v_season_start_date date;
v_season_end_date date;
v_program_name text;
v_booking_name text;
v_dates date[] := ARRAY[]::date[];
v_date date;
v_occurrence_count integer;
v_batch_id uuid;
v_booking_id uuid;
v_booking_ids uuid[] := ARRAY[]::uuid[];
v_request_fingerprint text;
v_conflicts text[] := ARRAY[]::text[];
v_sport text;
BEGIN
-- --------------------------
-- Hitelesítés és jogosultság
-- --------------------------
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Nincs bejelentkezett felhasználó.';
END IF;
IF p_club_id IS NULL
OR p_season_id IS NULL
OR p_court_id IS NULL THEN
RAISE EXCEPTION
'A klub, szezon és pálya kiválasztása kötelező.';
END IF;
IF NOT public.can_manage_academy(p_club_id) THEN
RAISE EXCEPTION
'Nincs jogosultságod akadémiai foglalás létrehozásához.';
END IF;
-- --------------------------
-- TENNIS SAFETY GATE / sport
-- --------------------------
-- Régi hívás p_sport nélkül továbbra is Tennis.
-- Padel csak explicit p_sport='padel' értékkel működik.
v_sport := lower(
COALESCE(NULLIF(btrim(p_sport), ''), 'tennis')
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION
'Érvénytelen sport. Engedélyezett értékek: tennis, padel.';
END IF;
SELECT up.id, up.role
INTO v_caller_profile_id, v_caller_role
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = p_club_id
ORDER BY up.id
LIMIT 1;
IF v_caller_profile_id IS NULL THEN
RAISE EXCEPTION
'A klubhoz tartozó felhasználói profil nem található.';
END IF;
-- --------------------------
-- Alapmezők ellenőrzése
-- --------------------------
IF p_first_booking_date IS NULL THEN
RAISE EXCEPTION 'Az első foglalási nap kötelező.';
END IF;
IF p_start_time IS NULL
OR p_end_time IS NULL
OR p_end_time <= p_start_time THEN
RAISE EXCEPTION
'A befejezési időpontnak későbbinek kell lennie a kezdési időpontnál.';
END IF;
IF p_court_fee_huf_per_occurrence IS NULL
OR p_court_fee_huf_per_occurrence < 0 THEN
RAISE EXCEPTION
'Érvényes, nem negatív klub pályadíjat kell megadni.';
END IF;
IF p_academy_type NOT IN ('individual', 'group', 'camp') THEN
RAISE EXCEPTION 'Érvénytelen akadémiai foglalástípus.';
END IF;
IF p_booking_scope NOT IN (
'one_time',
'season',
'camp_5_day',
'camp_7_day'
) THEN
RAISE EXCEPTION 'Érvénytelen akadémiai foglalási időtartam.';
END IF;
IF p_academy_type IN ('individual', 'group')
AND p_booking_scope NOT IN ('one_time', 'season') THEN
RAISE EXCEPTION
'Az 5 és 7 napos foglalás kizárólag táborhoz használható.';
END IF;
-- --------------------------
-- Szezon és pálya ellenőrzése
-- --------------------------
SELECT s.start_date, s.end_date
INTO v_season_start_date, v_season_end_date
FROM public.seasons s
WHERE s.id = p_season_id
AND s.club_id = p_club_id
AND s.sport = v_sport;
IF NOT FOUND THEN
RAISE EXCEPTION
'A szezon nem található, vagy másik klubhoz tartozik.';
END IF;
IF v_season_start_date IS NULL
OR v_season_end_date IS NULL
OR v_season_end_date < v_season_start_date THEN
RAISE EXCEPTION 'A szezon dátumtartománya érvénytelen.';
END IF;
-- A pályasor zárolása: az akadémiai kérések ugyanazon pályán
-- ne ellenőrizhessenek párhuzamosan azonos pillanatban.
PERFORM 1
FROM public.courts c
WHERE c.id = p_court_id
AND c.club_id = p_club_id
AND c.sport = v_sport
AND c.season_id = p_season_id
AND c.is_active = true
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION
'A pálya nem található, nem aktív, vagy nem ehhez a klubhoz és szezonhoz tartozik.';
END IF;
-- --------------------------
-- Név és program ellenőrzése
-- --------------------------
IF p_academy_type = 'individual' THEN
IF p_academy_program_id IS NOT NULL THEN
RAISE EXCEPTION
'Egyéni akadémiai foglaláshoz nem választható csoport vagy tábor.';
END IF;
IF NULLIF(btrim(p_individual_player_name), '') IS NULL THEN
RAISE EXCEPTION
'Egyéni akadémiai foglalásnál a játékos neve kötelező.';
END IF;
v_booking_name := btrim(p_individual_player_name);
ELSE
IF p_academy_program_id IS NULL THEN
RAISE EXCEPTION
'Csoportos vagy tábori foglalásnál programot kell választani.';
END IF;
SELECT ap.name
INTO v_program_name
FROM public.academy_programs ap
WHERE ap.id = p_academy_program_id
AND ap.club_id = p_club_id
AND ap.sport = v_sport
AND ap.program_type = p_academy_type
AND ap.is_active = true;
IF NOT FOUND THEN
RAISE EXCEPTION
'Az akadémiai csoport vagy tábor nem található, inaktív, vagy nem megfelelő típusú.';
END IF;
v_booking_name := v_program_name;
END IF;
-- --------------------------
-- Dátumok létrehozása
-- --------------------------
IF p_booking_scope = 'one_time' THEN
v_dates := ARRAY[p_first_booking_date];
ELSIF p_booking_scope = 'camp_5_day' THEN
FOR i IN 0..4 LOOP
v_dates := array_append(
v_dates,
p_first_booking_date + i
);
END LOOP;
ELSIF p_booking_scope = 'camp_7_day' THEN
FOR i IN 0..6 LOOP
v_dates := array_append(
v_dates,
p_first_booking_date + i
);
END LOOP;
ELSIF p_booking_scope = 'season' THEN
v_date := p_first_booking_date;
WHILE v_date <= v_season_end_date LOOP
v_dates := array_append(v_dates, v_date);
v_date := v_date + 7;
END LOOP;
END IF;
v_occurrence_count := COALESCE(array_length(v_dates, 1), 0);
IF v_occurrence_count = 0 THEN
RAISE EXCEPTION 'Nem jött létre egyetlen foglalási dátum sem.';
END IF;
FOREACH v_date IN ARRAY v_dates LOOP
IF v_date < v_season_start_date
OR v_date > v_season_end_date THEN
RAISE EXCEPTION
'A(z) % dátum kívül esik az aktív szezonon.',
to_char(v_date, 'YYYY-MM-DD');
END IF;
END LOOP;
-- --------------------------
-- Ismételt azonos kérés védelme
-- --------------------------
v_request_fingerprint := md5(
concat_ws(
'|',
p_club_id::text,
v_sport,
p_season_id::text,
p_court_id::text,
p_first_booking_date::text,
p_start_time::text,
p_end_time::text,
p_academy_type,
p_booking_scope,
COALESCE(p_academy_program_id::text, ''),
lower(COALESCE(btrim(p_individual_player_name), '')),
p_court_fee_huf_per_occurrence::text
)
);
-- --------------------------
-- Minden ütközés összegyűjtése
-- --------------------------
FOREACH v_date IN ARRAY v_dates LOOP
IF EXISTS (
SELECT 1
FROM public.bookings b
WHERE b.club_id = p_club_id
AND b.sport = v_sport
AND b.court_id = p_court_id
AND b.booking_date = v_date
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, '')) NOT IN (
'cancelled',
'deleted',
'deleted by club',
'failed'
)
AND b.start_time < p_end_time
AND b.end_time > p_start_time
) THEN
v_conflicts := array_append(
v_conflicts,
format(
'%s %s–%s',
to_char(v_date, 'YYYY-MM-DD'),
to_char(p_start_time, 'HH24:MI'),
to_char(p_end_time, 'HH24:MI')
)
);
END IF;
END LOOP;
IF COALESCE(array_length(v_conflicts, 1), 0) > 0 THEN
RAISE EXCEPTION
'A foglalás ütközik a következő időpontokkal: %',
array_to_string(v_conflicts, ', ');
END IF;
-- --------------------------
-- Csomag létrehozása
-- --------------------------
INSERT INTO public.academy_booking_batches (
club_id,
academy_type,
booking_scope,
academy_program_id,
individual_player_name,
court_fee_huf_per_occurrence,
booking_note,
first_booking_date,
occurrence_count,
status,
request_fingerprint,
created_by_user_profile_id
)
VALUES (
p_club_id,
p_academy_type,
p_booking_scope,
p_academy_program_id,
CASE
WHEN p_academy_type = 'individual'
THEN btrim(p_individual_player_name)
ELSE NULL
END,
p_court_fee_huf_per_occurrence,
NULLIF(btrim(p_booking_note), ''),
p_first_booking_date,
v_occurrence_count,
'active',
v_request_fingerprint,
v_caller_profile_id
)
RETURNING id
INTO v_batch_id;
-- --------------------------
-- Valódi naptárfoglalások és privát részletek
-- --------------------------
FOREACH v_date IN ARRAY v_dates LOOP
INSERT INTO public.bookings (
club_id,
sport,
season_id,
court_id,
booking_date,
start_time,
end_time,
booking_type,
booking_name,
status,
price_is_configured,
price_is_fully_configured,
price_per_booking,
estimated_total_price,
currency,
payment_method,
payment_status,
created_by_user_id,
created_by_role
)
VALUES (
p_club_id,
v_sport,
p_season_id,
p_court_id,
v_date,
p_start_time,
p_end_time,
-- Szándékosan one-time technikai sorok.
-- Az akadémiai szezonkapcsolatot a batch tárolja.
'one-time',
v_booking_name,
'confirmed',
true,
true,
p_court_fee_huf_per_occurrence,
p_court_fee_huf_per_occurrence,
'HUF',
NULL,
'pending',
v_caller_profile_id,
v_caller_role
)
RETURNING id
INTO v_booking_id;
v_booking_ids := array_append(v_booking_ids, v_booking_id);
INSERT INTO public.academy_booking_details (
booking_id,
academy_booking_batch_id,
club_id,
academy_type,
academy_program_id,
court_fee_huf,
booking_note,
occurrence_status
)
VALUES (
v_booking_id,
v_batch_id,
p_club_id,
p_academy_type,
p_academy_program_id,
p_court_fee_huf_per_occurrence,
NULLIF(btrim(p_booking_note), ''),
'scheduled'
);
END LOOP;
RETURN jsonb_build_object(
'batch_id', v_batch_id,
'occurrence_count', v_occurrence_count,
'booking_ids', to_jsonb(v_booking_ids),
'recurring_series_id', NULL
);
EXCEPTION
WHEN unique_violation THEN
RAISE EXCEPTION
'Ez az akadémiai foglalási kérés már létrejött vagy jelenleg feldolgozás alatt áll.';
END;
$function$
"
public,create_one_time_credit_bookings,f,"p_request_id uuid, p_target_user_profile_id uuid, p_credit_type text, p_bookings jsonb","p_request_id uuid, p_target_user_profile_id uuid, p_credit_type text, p_bookings jsonb",uuid[],plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.create_one_time_credit_bookings(p_request_id uuid, p_target_user_profile_id uuid, p_credit_type text, p_bookings jsonb)
RETURNS uuid[]
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_profile_id uuid;
v_caller_role text;
v_target_club_id uuid;
v_club_timezone text;
v_season_end_date date;
v_season_start_date date;
v_season_currency text;
v_season_sport text;
v_wallet_id uuid;
v_wallet_balance numeric;
v_total_cost numeric := 0;
v_booking_record jsonb;
v_booking_ids uuid[] := '{}';
v_new_booking_id uuid;
v_calculated_price numeric;
v_pricing_mode text;
v_payment_method text;
v_existing_request record;
v_club_id uuid;
v_season_id uuid;
v_court_id uuid;
v_date date;
v_start_time time;
v_end_time time;
v_booking_type text;
v_normalized_bookings jsonb := '[]'::jsonb;
v_normalized_booking jsonb;
v_idx int;
v_jdx int;
v_booking_count int;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Not authenticated.';
END IF;
IF p_request_id IS NULL THEN
RAISE EXCEPTION 'Request ID is required.';
END IF;
IF p_target_user_profile_id IS NULL THEN
RAISE EXCEPTION 'Target user profile ID is required.';
END IF;
IF p_credit_type IS NULL
OR p_credit_type NOT IN ('standard', 'season_pass')
THEN
RAISE EXCEPTION 'Invalid credit type.';
END IF;
IF p_bookings IS NULL
OR jsonb_typeof(p_bookings) IS DISTINCT FROM 'array'
THEN
RAISE EXCEPTION 'Bookings must be a JSON array.';
END IF;
v_booking_count := jsonb_array_length(p_bookings);
IF v_booking_count < 1 OR v_booking_count > 12 THEN
RAISE EXCEPTION
'Bookings array must contain between 1 and 12 entries.';
END IF;
BEGIN
v_club_id := (p_bookings->0->>'club_id')::uuid;
v_season_id := (p_bookings->0->>'season_id')::uuid;
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION 'Malformed club_id or season_id.';
END;
IF v_club_id IS NULL OR v_season_id IS NULL THEN
RAISE EXCEPTION
'Club ID and Season ID are required in bookings.';
END IF;
SELECT club_id
INTO v_target_club_id
FROM public.user_profiles
WHERE id = p_target_user_profile_id
LIMIT 1;
IF v_target_club_id IS NULL THEN
RAISE EXCEPTION 'Target user not found.';
END IF;
IF v_club_id IS DISTINCT FROM v_target_club_id THEN
RAISE EXCEPTION
'Booking club does not match target user club.';
END IF;
SELECT id, role
INTO v_caller_profile_id, v_caller_role
FROM public.user_profiles
WHERE auth_user_id = auth.uid()
AND club_id = v_club_id
LIMIT 1;
IF v_caller_profile_id IS NULL THEN
RAISE EXCEPTION
'Caller profile not found in this club.';
END IF;
IF v_caller_profile_id
IS DISTINCT FROM p_target_user_profile_id
THEN
IF v_caller_role NOT IN (
'owner',
'admin',
'secondary_admin'
) THEN
RAISE EXCEPTION
'Unauthorized to book for another user.';
END IF;
END IF;
PERFORM pg_advisory_xact_lock(
hashtextextended(p_request_id::text, 0)
);
SELECT *
INTO v_existing_request
FROM public.booking_requests
WHERE request_id = p_request_id;
IF FOUND THEN
IF v_existing_request.caller_user_profile_id
IS DISTINCT FROM v_caller_profile_id
OR v_existing_request.target_user_profile_id
IS DISTINCT FROM p_target_user_profile_id
OR v_existing_request.club_id
IS DISTINCT FROM v_club_id
OR v_existing_request.season_id
IS DISTINCT FROM v_season_id
OR v_existing_request.payment_method
IS DISTINCT FROM ('credit_' || p_credit_type)
OR v_existing_request.request_payload
IS DISTINCT FROM p_bookings
THEN
RAISE EXCEPTION
'Idempotency key reuse with different parameters.';
END IF;
IF v_existing_request.request_status = 'completed' THEN
RETURN v_existing_request.created_booking_ids;
ELSE
RAISE EXCEPTION 'Request is already processing.';
END IF;
END IF;
SELECT timezone
INTO v_club_timezone
FROM public.clubs
WHERE id = v_club_id;
SELECT currency, start_date, end_date, sport
INTO
v_season_currency,
v_season_start_date,
v_season_end_date,
v_season_sport
FROM public.seasons
WHERE id = v_season_id
AND club_id = v_club_id;
IF v_season_currency IS NULL THEN
RAISE EXCEPTION 'Season not found.';
END IF;
v_pricing_mode :=
CASE
WHEN p_credit_type = 'season_pass'
THEN 'season_pass'
ELSE 'regular'
END;
v_payment_method := 'credit_' || p_credit_type;
FOR v_idx IN 0 .. v_booking_count - 1 LOOP
v_booking_record := p_bookings->v_idx;
IF (v_booking_record->>'club_id')::uuid
IS DISTINCT FROM v_club_id
OR (v_booking_record->>'season_id')::uuid
IS DISTINCT FROM v_season_id
THEN
RAISE EXCEPTION
'Mixed club or season in bookings array.';
END IF;
IF v_booking_record->>'court_id' IS NULL
OR v_booking_record->>'booking_date' IS NULL
OR v_booking_record->>'start_time' IS NULL
OR v_booking_record->>'end_time' IS NULL
THEN
RAISE EXCEPTION 'Missing required booking fields.';
END IF;
BEGIN
v_court_id :=
(v_booking_record->>'court_id')::uuid;
v_date :=
(v_booking_record->>'booking_date')::date;
v_start_time :=
(v_booking_record->>'start_time')::time;
v_end_time :=
(v_booking_record->>'end_time')::time;
v_booking_type :=
v_booking_record->>'booking_type';
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION
'Malformed booking fields at index %',
v_idx;
END;
IF COALESCE(v_booking_type, '') != 'one-time' THEN
RAISE EXCEPTION
'Only one-time bookings are supported for credit payment.';
END IF;
IF NOT EXISTS (
SELECT 1
FROM public.courts
WHERE id = v_court_id
AND club_id = v_club_id
AND season_id = v_season_id
AND is_active = true
AND sport = v_season_sport
) THEN
RAISE EXCEPTION 'Court is invalid or inactive.';
END IF;
IF v_date > v_season_end_date
OR v_date < v_season_start_date
THEN
RAISE EXCEPTION
'Booking date is outside the season.';
END IF;
IF (
(v_date + v_start_time)
AT TIME ZONE COALESCE(v_club_timezone, 'UTC')
) <= now()
THEN
RAISE EXCEPTION 'Booking must be in the future.';
END IF;
IF NOT EXISTS (
SELECT 1
FROM public.opening_hours
WHERE club_id = v_club_id
AND season_id = v_season_id
AND weekday = EXTRACT(DOW FROM v_date)
AND COALESCE(is_open, false) = true
AND opening_time <= v_start_time
AND closing_time >= v_end_time
) THEN
RAISE EXCEPTION
'Booking time is outside opening hours.';
END IF;
FOR v_jdx IN
0 .. jsonb_array_length(v_normalized_bookings) - 1
LOOP
IF (
v_normalized_bookings->v_jdx->>'court_id'
)::uuid = v_court_id
AND (
v_normalized_bookings->v_jdx->>'booking_date'
)::date = v_date
AND (
v_normalized_bookings->v_jdx->>'start_time'
)::time < v_end_time
AND (
v_normalized_bookings->v_jdx->>'end_time'
)::time > v_start_time
THEN
RAISE EXCEPTION
'Overlapping bookings in request.';
END IF;
END LOOP;
-- COURT-AWARE: each booking priced by its own court's pricing group
v_calculated_price :=
public.calculate_booking_price(
v_club_id,
v_season_id,
v_date,
v_start_time,
v_end_time,
v_pricing_mode,
v_season_currency,
v_court_id
);
v_total_cost := v_total_cost + v_calculated_price;
BEGIN
v_normalized_booking := jsonb_build_object(
'court_id', v_court_id,
'booking_date', v_date,
'start_time', v_start_time,
'end_time', v_end_time,
'calculated_price', v_calculated_price,
'booking_name',
v_booking_record->>'booking_name',
'email',
v_booking_record->>'email',
'phone',
v_booking_record->>'phone',
'booking_note',
v_booking_record->>'booking_note',
'internal_admin_note',
CASE
WHEN v_caller_role IN (
'owner',
'admin',
'secondary_admin'
)
THEN v_booking_record->>'internal_admin_note'
ELSE NULL
END,
'is_all_day',
COALESCE(
(v_booking_record->>'is_all_day')::boolean,
false
),
'is_all_courts',
COALESCE(
(v_booking_record->>'is_all_courts')::boolean,
false
),
'is_partner_search_enabled',
COALESCE(
(
v_booking_record
->>'is_partner_search_enabled'
)::boolean,
false
),
'booking_owner_utr_rating',
(
v_booking_record
->>'booking_owner_utr_rating'
)::numeric,
'partner_search_utr_min',
(
v_booking_record
->>'partner_search_utr_min'
)::numeric,
'partner_search_utr_max',
(
v_booking_record
->>'partner_search_utr_max'
)::numeric,
'partner_search_players_needed',
(
v_booking_record
->>'partner_search_players_needed'
)::integer,
'partner_search_status',
CASE
WHEN COALESCE(
(
v_booking_record
->>'is_partner_search_enabled'
)::boolean,
false
)
THEN 'active'
ELSE 'inactive'
END
);
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION
'Malformed optional booking fields at index %',
v_idx;
END;
v_normalized_bookings :=
v_normalized_bookings || v_normalized_booking;
END LOOP;
IF v_total_cost <= 0
OR v_total_cost = 'NaN'::numeric
OR v_total_cost = 'Infinity'::numeric
OR v_total_cost = '-Infinity'::numeric
THEN
RAISE EXCEPTION
'Total cost must be a finite positive amount';
END IF;
INSERT INTO public.booking_requests (
request_id,
caller_user_profile_id,
target_user_profile_id,
club_id,
season_id,
payment_method,
request_payload,
request_status
)
VALUES (
p_request_id,
v_caller_profile_id,
p_target_user_profile_id,
v_club_id,
v_season_id,
v_payment_method,
p_bookings,
'processing'
);
SELECT id, balance
INTO v_wallet_id, v_wallet_balance
FROM public.credit_wallets
WHERE user_profile_id = p_target_user_profile_id
AND club_id = v_club_id
AND season_id = v_season_id
AND currency = v_season_currency
AND credit_type = p_credit_type
FOR UPDATE;
IF v_wallet_id IS NULL THEN
RAISE EXCEPTION 'No matching credit wallet found.';
END IF;
IF v_wallet_balance < v_total_cost THEN
RAISE EXCEPTION 'Insufficient credit balance.';
END IF;
FOR v_idx IN
0 .. jsonb_array_length(v_normalized_bookings) - 1
LOOP
v_normalized_booking :=
v_normalized_bookings->v_idx;
BEGIN
PERFORM set_config(
'app.bypass_booking_security',
'true',
true
);
INSERT INTO public.bookings (
club_id,
season_id,
court_id,
booking_date,
start_time,
end_time,
booking_type,
booking_name,
user_id,
email,
phone,
booking_note,
internal_admin_note,
status,
is_all_day,
is_all_courts,
selected_court_ids,
is_partner_search_enabled,
booking_owner_utr_rating,
partner_search_utr_min,
partner_search_utr_max,
partner_search_players_needed,
partner_search_status,
price_is_configured,
price_is_fully_configured,
price_per_booking,
estimated_total_price,
currency,
pricing_mode,
payment_status,
payment_method,
created_by_user_id,
created_by_role,
sport
)
VALUES (
v_club_id,
v_season_id,
(
v_normalized_booking->>'court_id'
)::uuid,
(
v_normalized_booking->>'booking_date'
)::date,
(
v_normalized_booking->>'start_time'
)::time,
(
v_normalized_booking->>'end_time'
)::time,
'one-time',
v_normalized_booking->>'booking_name',
p_target_user_profile_id,
v_normalized_booking->>'email',
v_normalized_booking->>'phone',
v_normalized_booking->>'booking_note',
v_normalized_booking->>'internal_admin_note',
'confirmed',
(
v_normalized_booking->>'is_all_day'
)::boolean,
(
v_normalized_booking->>'is_all_courts'
)::boolean,
ARRAY[
(
v_normalized_booking->>'court_id'
)::uuid
]::uuid[],
(
v_normalized_booking
->>'is_partner_search_enabled'
)::boolean,
(
v_normalized_booking
->>'booking_owner_utr_rating'
)::numeric,
(
v_normalized_booking
->>'partner_search_utr_min'
)::numeric,
(
v_normalized_booking
->>'partner_search_utr_max'
)::numeric,
(
v_normalized_booking
->>'partner_search_players_needed'
)::integer,
v_normalized_booking->>'partner_search_status',
true,
true,
(
v_normalized_booking->>'calculated_price'
)::numeric,
(
v_normalized_booking->>'calculated_price'
)::numeric,
v_season_currency,
v_pricing_mode,
'paid',
v_payment_method,
v_caller_profile_id,
v_caller_role,
v_season_sport
)
RETURNING id
INTO v_new_booking_id;
PERFORM set_config(
'app.bypass_booking_security',
'false',
true
);
EXCEPTION
WHEN OTHERS THEN
PERFORM set_config(
'app.bypass_booking_security',
'false',
true
);
RAISE;
END;
v_booking_ids :=
array_append(v_booking_ids, v_new_booking_id);
INSERT INTO public.booking_payments (
club_id,
season_id,
booking_id,
user_profile_id,
payment_method,
payment_status,
amount,
currency,
credit_type,
wallet_id,
paid_at
)
VALUES (
v_club_id,
v_season_id,
v_new_booking_id,
p_target_user_profile_id,
v_payment_method,
'paid',
(
v_normalized_booking->>'calculated_price'
)::numeric,
v_season_currency,
p_credit_type,
v_wallet_id,
now()
);
INSERT INTO public.credit_transactions (
wallet_id,
club_id,
user_profile_id,
season_id,
booking_id,
credit_type,
currency,
amount,
transaction_category,
reason,
created_by,
idempotency_key
)
VALUES (
v_wallet_id,
v_club_id,
p_target_user_profile_id,
v_season_id,
v_new_booking_id,
p_credit_type,
v_season_currency,
-(
v_normalized_booking->>'calculated_price'
)::numeric,
'booking_payment',
'Booking payment',
v_caller_profile_id,
p_request_id::text
|| '_'
|| v_new_booking_id::text
);
END LOOP;
UPDATE public.credit_wallets
SET
balance = balance - v_total_cost,
updated_at = now()
WHERE id = v_wallet_id;
UPDATE public.booking_requests
SET
request_status = 'completed',
created_booking_ids = v_booking_ids,
completed_at = now()
WHERE request_id = p_request_id;
RETURN v_booking_ids;
EXCEPTION
WHEN OTHERS THEN
PERFORM set_config(
'app.bypass_booking_security',
'false',
true
);
RAISE;
END;
$function$
"
public,create_one_time_supported_bookings,f,"p_request_id uuid, p_target_user_profile_id uuid, p_bookings jsonb","p_request_id uuid, p_target_user_profile_id uuid, p_bookings jsonb",uuid[],plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.create_one_time_supported_bookings(p_request_id uuid, p_target_user_profile_id uuid, p_bookings jsonb)
RETURNS uuid[]
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_profile_id uuid;
v_caller_role text;
v_target_club_id uuid;
v_target_is_supported boolean;
v_club_timezone text;
v_season_start_date date;
v_season_end_date date;
v_season_currency text;
v_season_sport text;
v_existing_request record;
v_club_id uuid;
v_season_id uuid;
v_court_id uuid;
v_date date;
v_start_time time;
v_end_time time;
v_booking_type text;
v_booking_record jsonb;
v_normalized_booking jsonb;
v_normalized_bookings jsonb := '[]'::jsonb;
v_booking_ids uuid[] := '{}';
v_new_booking_id uuid;
v_booking_count integer;
v_idx integer;
v_jdx integer;
BEGIN
-- Bejelentkezés ellenőrzése
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Not authenticated.';
END IF;
IF p_request_id IS NULL THEN
RAISE EXCEPTION 'Request ID is required.';
END IF;
IF p_target_user_profile_id IS NULL THEN
RAISE EXCEPTION 'Target user profile ID is required.';
END IF;
IF p_bookings IS NULL
OR jsonb_typeof(p_bookings) IS DISTINCT FROM 'array'
THEN
RAISE EXCEPTION 'Bookings must be a JSON array.';
END IF;
v_booking_count := jsonb_array_length(p_bookings);
IF v_booking_count < 1 OR v_booking_count > 12 THEN
RAISE EXCEPTION
'Bookings array must contain between 1 and 12 entries.';
END IF;
-- Klub és szezon kiolvasása az első foglalásból
BEGIN
v_club_id := (p_bookings->0->>'club_id')::uuid;
v_season_id := (p_bookings->0->>'season_id')::uuid;
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION 'Malformed club_id or season_id.';
END;
IF v_club_id IS NULL OR v_season_id IS NULL THEN
RAISE EXCEPTION
'Club ID and Season ID are required in bookings.';
END IF;
-- Célfelhasználó és támogatotti státusz ellenőrzése
SELECT
club_id,
is_supported_player
INTO
v_target_club_id,
v_target_is_supported
FROM public.user_profiles
WHERE id = p_target_user_profile_id
LIMIT 1;
IF NOT FOUND THEN
RAISE EXCEPTION 'Target user not found.';
END IF;
IF v_target_club_id IS DISTINCT FROM v_club_id THEN
RAISE EXCEPTION
'Booking club does not match target user club.';
END IF;
IF v_target_is_supported IS NOT TRUE THEN
RAISE EXCEPTION
'Target user is not a supported player.';
END IF;
-- Bejelentkezett felhasználó profiljának ellenőrzése
SELECT
id,
role
INTO
v_caller_profile_id,
v_caller_role
FROM public.user_profiles
WHERE auth_user_id = auth.uid()
AND club_id = v_club_id
LIMIT 1;
IF v_caller_profile_id IS NULL THEN
RAISE EXCEPTION
'Caller profile not found in this club.';
END IF;
-- Normál felhasználó csak saját magának foglalhat
IF v_caller_profile_id
IS DISTINCT FROM p_target_user_profile_id
THEN
IF v_caller_role NOT IN (
'owner',
'admin',
'secondary_admin'
) THEN
RAISE EXCEPTION
'Unauthorized to book for another user.';
END IF;
END IF;
-- Ugyanaz a request_id ne hozhasson létre két foglalást
PERFORM pg_advisory_xact_lock(
hashtextextended(p_request_id::text, 0)
);
SELECT *
INTO v_existing_request
FROM public.booking_requests
WHERE request_id = p_request_id;
IF FOUND THEN
IF v_existing_request.caller_user_profile_id
IS DISTINCT FROM v_caller_profile_id
OR v_existing_request.target_user_profile_id
IS DISTINCT FROM p_target_user_profile_id
OR v_existing_request.club_id
IS DISTINCT FROM v_club_id
OR v_existing_request.season_id
IS DISTINCT FROM v_season_id
OR v_existing_request.payment_method
IS DISTINCT FROM 'supported'
OR v_existing_request.request_payload
IS DISTINCT FROM p_bookings
THEN
RAISE EXCEPTION
'Idempotency key reuse with different parameters.';
END IF;
IF v_existing_request.request_status = 'completed' THEN
RETURN v_existing_request.created_booking_ids;
END IF;
RAISE EXCEPTION 'Request is already processing.';
END IF;
-- Klub és szezon szerveroldali kiolvasása
SELECT timezone
INTO v_club_timezone
FROM public.clubs
WHERE id = v_club_id;
SELECT
currency,
start_date,
end_date,
sport
INTO
v_season_currency,
v_season_start_date,
v_season_end_date,
v_season_sport
FROM public.seasons
WHERE id = v_season_id
AND club_id = v_club_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Season not found.';
END IF;
IF v_season_currency IS NULL THEN
RAISE EXCEPTION 'Season currency is missing.';
END IF;
-- Minden foglalási elem ellenőrzése
FOR v_idx IN 0 .. v_booking_count - 1 LOOP
v_booking_record := p_bookings->v_idx;
IF (v_booking_record->>'club_id')::uuid
IS DISTINCT FROM v_club_id
OR (v_booking_record->>'season_id')::uuid
IS DISTINCT FROM v_season_id
THEN
RAISE EXCEPTION
'Mixed club or season in bookings array.';
END IF;
IF v_booking_record->>'court_id' IS NULL
OR v_booking_record->>'booking_date' IS NULL
OR v_booking_record->>'start_time' IS NULL
OR v_booking_record->>'end_time' IS NULL
THEN
RAISE EXCEPTION 'Missing required booking fields.';
END IF;
BEGIN
v_court_id :=
(v_booking_record->>'court_id')::uuid;
v_date :=
(v_booking_record->>'booking_date')::date;
v_start_time :=
(v_booking_record->>'start_time')::time;
v_end_time :=
(v_booking_record->>'end_time')::time;
v_booking_type :=
v_booking_record->>'booking_type';
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION
'Malformed booking fields at index %.',
v_idx;
END;
IF COALESCE(v_booking_type, '') <> 'one-time' THEN
RAISE EXCEPTION
'Only one-time bookings are supported by this function.';
END IF;
IF v_start_time >= v_end_time THEN
RAISE EXCEPTION
'Start time must be before end time.';
END IF;
-- Pálya ellenőrzése
IF NOT EXISTS (
SELECT 1
FROM public.courts
WHERE id = v_court_id
AND club_id = v_club_id
AND season_id = v_season_id
AND is_active = TRUE
AND sport = v_season_sport
) THEN
RAISE EXCEPTION 'Court is invalid or inactive.';
END IF;
-- Szezon dátumának ellenőrzése
IF v_date < v_season_start_date
OR v_date > v_season_end_date
THEN
RAISE EXCEPTION
'Booking date is outside the season.';
END IF;
-- Múltbeli foglalás tiltása
IF (
(v_date + v_start_time)
AT TIME ZONE COALESCE(v_club_timezone, 'UTC')
) <= now()
THEN
RAISE EXCEPTION 'Booking must be in the future.';
END IF;
-- Nyitvatartás ellenőrzése
IF NOT EXISTS (
SELECT 1
FROM public.opening_hours
WHERE club_id = v_club_id
AND season_id = v_season_id
AND weekday = EXTRACT(DOW FROM v_date)
AND COALESCE(is_open, FALSE) = TRUE
AND opening_time <= v_start_time
AND closing_time >= v_end_time
) THEN
RAISE EXCEPTION
'Booking time is outside opening hours.';
END IF;
-- Már létező foglalással való ütközés ellenőrzése
IF EXISTS (
SELECT 1
FROM public.bookings
WHERE court_id = v_court_id
AND booking_date = v_date
AND deleted_at IS NULL
AND COALESCE(status, 'confirmed')
NOT IN ('cancelled', 'deleted by club', 'failed')
AND start_time < v_end_time
AND end_time > v_start_time
) THEN
RAISE EXCEPTION
'The selected court is already booked for this time.';
END IF;
-- Ugyanazon kérésen belüli átfedés ellenőrzése
FOR v_jdx IN
0 .. jsonb_array_length(v_normalized_bookings) - 1
LOOP
IF (
v_normalized_bookings->v_jdx->>'court_id'
)::uuid = v_court_id
AND (
v_normalized_bookings->v_jdx->>'booking_date'
)::date = v_date
AND (
v_normalized_bookings->v_jdx->>'start_time'
)::time < v_end_time
AND (
v_normalized_bookings->v_jdx->>'end_time'
)::time > v_start_time
THEN
RAISE EXCEPTION
'Overlapping bookings in request.';
END IF;
END LOOP;
BEGIN
v_normalized_booking := jsonb_build_object(
'court_id',
v_court_id,
'booking_date',
v_date,
'start_time',
v_start_time,
'end_time',
v_end_time,
'booking_name',
v_booking_record->>'booking_name',
'email',
v_booking_record->>'email',
'phone',
v_booking_record->>'phone',
'booking_note',
v_booking_record->>'booking_note',
'internal_admin_note',
CASE
WHEN v_caller_role IN (
'owner',
'admin',
'secondary_admin'
)
THEN v_booking_record->>'internal_admin_note'
ELSE NULL
END,
'is_all_day',
COALESCE(
(v_booking_record->>'is_all_day')::boolean,
FALSE
),
'is_all_courts',
COALESCE(
(v_booking_record->>'is_all_courts')::boolean,
FALSE
),
'is_partner_search_enabled',
COALESCE(
(
v_booking_record
->>'is_partner_search_enabled'
)::boolean,
FALSE
),
'booking_owner_utr_rating',
(
v_booking_record
->>'booking_owner_utr_rating'
)::numeric,
'partner_search_utr_min',
(
v_booking_record
->>'partner_search_utr_min'
)::numeric,
'partner_search_utr_max',
(
v_booking_record
->>'partner_search_utr_max'
)::numeric,
'partner_search_players_needed',
(
v_booking_record
->>'partner_search_players_needed'
)::integer,
'partner_search_status',
CASE
WHEN COALESCE(
(
v_booking_record
->>'is_partner_search_enabled'
)::boolean,
FALSE
)
THEN 'active'
ELSE 'inactive'
END
);
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION
'Malformed optional booking fields at index %.',
v_idx;
END;
v_normalized_bookings :=
v_normalized_bookings
|| jsonb_build_array(v_normalized_booking);
END LOOP;
-- Idempotenciarekord
INSERT INTO public.booking_requests (
request_id,
caller_user_profile_id,
target_user_profile_id,
club_id,
season_id,
payment_method,
request_payload,
request_status
)
VALUES (
p_request_id,
v_caller_profile_id,
p_target_user_profile_id,
v_club_id,
v_season_id,
'supported',
p_bookings,
'processing'
);
-- Nullás, támogatott foglalások létrehozása
FOR v_idx IN
0 .. jsonb_array_length(v_normalized_bookings) - 1
LOOP
v_normalized_booking :=
v_normalized_bookings->v_idx;
BEGIN
PERFORM set_config(
'app.bypass_booking_security',
'true',
true
);
INSERT INTO public.bookings (
club_id,
season_id,
court_id,
booking_date,
start_time,
end_time,
booking_type,
booking_name,
user_id,
email,
phone,
booking_note,
internal_admin_note,
status,
is_all_day,
is_all_courts,
selected_court_ids,
is_partner_search_enabled,
booking_owner_utr_rating,
partner_search_utr_min,
partner_search_utr_max,
partner_search_players_needed,
partner_search_status,
price_is_configured,
price_is_fully_configured,
price_per_booking,
estimated_total_price,
currency,
pricing_mode,
payment_status,
payment_method,
created_by_user_id,
created_by_role,
sport
)
VALUES (
v_club_id,
v_season_id,
(
v_normalized_booking->>'court_id'
)::uuid,
(
v_normalized_booking->>'booking_date'
)::date,
(
v_normalized_booking->>'start_time'
)::time,
(
v_normalized_booking->>'end_time'
)::time,
'one-time',
v_normalized_booking->>'booking_name',
p_target_user_profile_id,
v_normalized_booking->>'email',
v_normalized_booking->>'phone',
v_normalized_booking->>'booking_note',
v_normalized_booking->>'internal_admin_note',
'confirmed',
(
v_normalized_booking->>'is_all_day'
)::boolean,
(
v_normalized_booking->>'is_all_courts'
)::boolean,
ARRAY[
(
v_normalized_booking->>'court_id'
)::uuid
]::uuid[],
(
v_normalized_booking
->>'is_partner_search_enabled'
)::boolean,
(
v_normalized_booking
->>'booking_owner_utr_rating'
)::numeric,
(
v_normalized_booking
->>'partner_search_utr_min'
)::numeric,
(
v_normalized_booking
->>'partner_search_utr_max'
)::numeric,
(
v_normalized_booking
->>'partner_search_players_needed'
)::integer,
v_normalized_booking->>'partner_search_status',
TRUE,
TRUE,
0,
0,
v_season_currency,
'regular',
'paid',
'supported',
v_caller_profile_id,
v_caller_role,
v_season_sport
)
RETURNING id
INTO v_new_booking_id;
PERFORM set_config(
'app.bypass_booking_security',
'false',
true
);
EXCEPTION
WHEN OTHERS THEN
PERFORM set_config(
'app.bypass_booking_security',
'false',
true
);
RAISE;
END;
v_booking_ids :=
array_append(v_booking_ids, v_new_booking_id);
INSERT INTO public.booking_payments (
club_id,
season_id,
booking_id,
user_profile_id,
payment_method,
payment_status,
amount,
currency,
paid_at
)
VALUES (
v_club_id,
v_season_id,
v_new_booking_id,
p_target_user_profile_id,
'supported',
'paid',
0,
v_season_currency,
now()
);
END LOOP;
UPDATE public.booking_requests
SET
request_status = 'completed',
created_booking_ids = v_booking_ids,
completed_at = now()
WHERE request_id = p_request_id;
RETURN v_booking_ids;
EXCEPTION
WHEN OTHERS THEN
PERFORM set_config(
'app.bypass_booking_security',
'false',
true
);
RAISE;
END;
$function$
"
public,create_one_time_venue_bookings,f,"p_request_id uuid, p_target_user_profile_id uuid, p_bookings jsonb","p_request_id uuid, p_target_user_profile_id uuid, p_bookings jsonb",uuid[],plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.create_one_time_venue_bookings(p_request_id uuid, p_target_user_profile_id uuid, p_bookings jsonb)
RETURNS uuid[]
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_profile_id uuid;
v_caller_role text;
v_club_id uuid;
v_season_id uuid;
v_booking_count integer;
v_idx integer;
v_jdx integer;
v_booking_record jsonb;
v_court_id uuid;
v_date date;
v_start_time time;
v_end_time time;
v_booking_type text;
v_club_timezone text;
v_season_start_date date;
v_season_end_date date;
v_season_currency text;
v_season_sport text;
v_normalized_bookings jsonb := '[]'::jsonb;
v_normalized_booking jsonb;
v_booking_ids uuid[] := '{}';
v_new_booking_id uuid;
v_existing_request record;
v_is_admin boolean := false;
v_is_guest boolean := false;
v_is_all_courts boolean := false;
v_calculated_price numeric;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Not authenticated.';
END IF;
IF p_request_id IS NULL THEN
RAISE EXCEPTION 'Request ID is required.';
END IF;
IF p_bookings IS NULL
OR jsonb_typeof(p_bookings) IS DISTINCT FROM 'array'
THEN
RAISE EXCEPTION 'Bookings must be a JSON array.';
END IF;
v_booking_count := jsonb_array_length(p_bookings);
IF v_booking_count < 1 OR v_booking_count > 12 THEN
RAISE EXCEPTION
'Bookings array must contain between 1 and 12 entries.';
END IF;
BEGIN
v_club_id := (p_bookings->0->>'club_id')::uuid;
v_season_id := (p_bookings->0->>'season_id')::uuid;
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION 'Malformed club_id or season_id.';
END;
IF v_club_id IS NULL OR v_season_id IS NULL THEN
RAISE EXCEPTION
'Club ID and Season ID are required in bookings.';
END IF;
SELECT id, role
INTO v_caller_profile_id, v_caller_role
FROM public.user_profiles
WHERE auth_user_id = auth.uid()
AND club_id = v_club_id
ORDER BY id
LIMIT 1;
IF v_caller_profile_id IS NULL THEN
RAISE EXCEPTION
'Caller profile not found in this club.';
END IF;
v_is_admin := v_caller_role IN ('owner', 'admin', 'secondary_admin');
v_is_guest := p_target_user_profile_id IS NULL;
v_is_all_courts := v_booking_count > 1;
IF NOT v_is_admin AND v_is_guest THEN
RAISE EXCEPTION
'Only administrators can create guest bookings.';
END IF;
IF NOT v_is_admin
AND p_target_user_profile_id IS DISTINCT FROM v_caller_profile_id
THEN
RAISE EXCEPTION
'Unauthorized to book for another user.';
END IF;
IF NOT v_is_guest THEN
IF NOT EXISTS (
SELECT 1
FROM public.user_profiles
WHERE id = p_target_user_profile_id
AND club_id = v_club_id
) THEN
RAISE EXCEPTION
'Target user not found in this club.';
END IF;
END IF;
PERFORM pg_advisory_xact_lock(
hashtextextended(p_request_id::text, 0)
);
SELECT *
INTO v_existing_request
FROM public.booking_requests
WHERE request_id = p_request_id;
IF FOUND THEN
IF v_existing_request.caller_user_profile_id
IS DISTINCT FROM v_caller_profile_id
OR v_existing_request.target_user_profile_id
IS DISTINCT FROM p_target_user_profile_id
OR v_existing_request.club_id
IS DISTINCT FROM v_club_id
OR v_existing_request.season_id
IS DISTINCT FROM v_season_id
OR v_existing_request.payment_method
IS DISTINCT FROM 'venue'
OR v_existing_request.request_payload
IS DISTINCT FROM p_bookings
THEN
RAISE EXCEPTION
'Idempotency key reuse with different parameters.';
END IF;
IF v_existing_request.request_status = 'completed' THEN
RETURN v_existing_request.created_booking_ids;
END IF;
RAISE EXCEPTION 'Request is already processing.';
END IF;
SELECT timezone
INTO v_club_timezone
FROM public.clubs
WHERE id = v_club_id;
SELECT
currency,
start_date,
end_date,
sport
INTO
v_season_currency,
v_season_start_date,
v_season_end_date,
v_season_sport
FROM public.seasons
WHERE id = v_season_id
AND club_id = v_club_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Season not found.';
END IF;
IF v_season_currency IS NULL THEN
RAISE EXCEPTION 'Season currency is missing.';
END IF;
IF v_season_sport IS NULL
OR v_season_sport NOT IN ('tennis', 'padel')
THEN
RAISE EXCEPTION 'Season sport is missing or invalid.';
END IF;
FOR v_idx IN 0 .. v_booking_count - 1 LOOP
v_booking_record := p_bookings->v_idx;
IF (v_booking_record->>'club_id')::uuid
IS DISTINCT FROM v_club_id
OR (v_booking_record->>'season_id')::uuid
IS DISTINCT FROM v_season_id
THEN
RAISE EXCEPTION
'Mixed club or season in bookings array.';
END IF;
IF v_booking_record->>'court_id' IS NULL
OR v_booking_record->>'booking_date' IS NULL
OR v_booking_record->>'start_time' IS NULL
OR v_booking_record->>'end_time' IS NULL
THEN
RAISE EXCEPTION 'Missing required booking fields.';
END IF;
BEGIN
v_court_id := (v_booking_record->>'court_id')::uuid;
v_date := (v_booking_record->>'booking_date')::date;
v_start_time := (v_booking_record->>'start_time')::time;
v_end_time := (v_booking_record->>'end_time')::time;
v_booking_type := v_booking_record->>'booking_type';
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION
'Malformed booking fields at index %.',
v_idx;
END;
IF COALESCE(v_booking_type, '') <> 'one-time' THEN
RAISE EXCEPTION
'Only one-time bookings are supported by this function.';
END IF;
IF v_start_time >= v_end_time THEN
RAISE EXCEPTION
'Start time must be before end time.';
END IF;
IF NOT EXISTS (
SELECT 1
FROM public.courts
WHERE id = v_court_id
AND club_id = v_club_id
AND season_id = v_season_id
AND sport = v_season_sport
AND is_active = TRUE
) THEN
RAISE EXCEPTION
'Court is invalid, inactive, or sport mismatch at index %.',
v_idx;
END IF;
IF v_date < v_season_start_date
OR v_date > v_season_end_date
THEN
RAISE EXCEPTION
'Booking date is outside the season.';
END IF;
IF (
(v_date + v_start_time)
AT TIME ZONE COALESCE(v_club_timezone, 'UTC')
) <= now()
THEN
RAISE EXCEPTION 'Booking must be in the future.';
END IF;
IF NOT EXISTS (
SELECT 1
FROM public.opening_hours
WHERE club_id = v_club_id
AND season_id = v_season_id
AND weekday = EXTRACT(DOW FROM v_date)
AND COALESCE(is_open, FALSE) = TRUE
AND opening_time <= v_start_time
AND closing_time >= v_end_time
) THEN
RAISE EXCEPTION
'Booking time is outside opening hours.';
END IF;
IF EXISTS (
SELECT 1
FROM public.bookings
WHERE court_id = v_court_id
AND booking_date = v_date
AND deleted_at IS NULL
AND COALESCE(status, 'confirmed')
NOT IN ('cancelled', 'deleted by club', 'failed')
AND start_time < v_end_time
AND end_time > v_start_time
) THEN
RAISE EXCEPTION
'The selected court is already booked for this time.';
END IF;
FOR v_jdx IN
0 .. jsonb_array_length(v_normalized_bookings) - 1
LOOP
IF (
v_normalized_bookings->v_jdx->>'court_id'
)::uuid = v_court_id
AND (
v_normalized_bookings->v_jdx->>'booking_date'
)::date = v_date
AND (
v_normalized_bookings->v_jdx->>'start_time'
)::time < v_end_time
AND (
v_normalized_bookings->v_jdx->>'end_time'
)::time > v_start_time
THEN
RAISE EXCEPTION
'Overlapping or duplicate bookings in request.';
END IF;
END LOOP;
-- COURT-AWARE: each court priced by its own pricing group
BEGIN
v_calculated_price := public.calculate_booking_price(
v_club_id,
v_season_id,
v_date,
v_start_time,
v_end_time,
'regular',
v_season_currency,
v_court_id
);
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION
'Authoritative price could not be calculated: %',
SQLERRM;
END;
IF v_calculated_price IS NULL
OR v_calculated_price = 'NaN'::numeric
OR v_calculated_price = 'Infinity'::numeric
OR v_calculated_price = '-Infinity'::numeric
OR v_calculated_price <= 0
THEN
RAISE EXCEPTION
'Calculated price is invalid for booking at index %.',
v_idx;
END IF;
IF v_season_currency = 'HUF'
AND v_calculated_price <> ROUND(v_calculated_price)
THEN
RAISE EXCEPTION
'HUF price must be an integer for booking at index %.',
v_idx;
END IF;
BEGIN
v_normalized_booking := jsonb_build_object(
'court_id', v_court_id,
'booking_date', v_date,
'start_time', v_start_time,
'end_time', v_end_time,
'booking_name', v_booking_record->>'booking_name',
'email', v_booking_record->>'email',
'phone', v_booking_record->>'phone',
'booking_note', v_booking_record->>'booking_note',
'internal_admin_note',
CASE
WHEN v_is_admin
THEN v_booking_record->>'internal_admin_note'
ELSE NULL
END,
'is_all_day',
COALESCE(
(v_booking_record->>'is_all_day')::boolean,
FALSE
),
'is_all_courts', v_is_all_courts,
'is_partner_search_enabled',
COALESCE(
(v_booking_record->>'is_partner_search_enabled')::boolean,
FALSE
),
'booking_owner_utr_rating',
(v_booking_record->>'booking_owner_utr_rating')::numeric,
'partner_search_utr_min',
(v_booking_record->>'partner_search_utr_min')::numeric,
'partner_search_utr_max',
(v_booking_record->>'partner_search_utr_max')::numeric,
'partner_search_players_needed',
(v_booking_record->>'partner_search_players_needed')::integer,
'partner_search_status',
CASE
WHEN COALESCE(
(v_booking_record->>'is_partner_search_enabled')::boolean,
FALSE
)
THEN 'active'
ELSE 'inactive'
END,
'calculated_price', v_calculated_price
);
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION
'Malformed optional booking fields at index %.',
v_idx;
END;
v_normalized_bookings :=
v_normalized_bookings || jsonb_build_array(v_normalized_booking);
END LOOP;
INSERT INTO public.booking_requests (
request_id,
caller_user_profile_id,
target_user_profile_id,
club_id,
season_id,
payment_method,
request_payload,
request_status
)
VALUES (
p_request_id,
v_caller_profile_id,
p_target_user_profile_id,
v_club_id,
v_season_id,
'venue',
p_bookings,
'processing'
);
FOR v_idx IN
0 .. jsonb_array_length(v_normalized_bookings) - 1
LOOP
v_normalized_booking := v_normalized_bookings->v_idx;
BEGIN
PERFORM set_config(
'app.bypass_booking_security',
'true',
true
);
INSERT INTO public.bookings (
club_id,
season_id,
court_id,
booking_date,
sport,
start_time,
end_time,
booking_type,
booking_name,
user_id,
email,
phone,
booking_note,
internal_admin_note,
status,
is_all_day,
is_all_courts,
selected_court_ids,
is_partner_search_enabled,
booking_owner_utr_rating,
partner_search_utr_min,
partner_search_utr_max,
partner_search_players_needed,
partner_search_status,
price_is_configured,
price_is_fully_configured,
price_per_booking,
estimated_total_price,
currency,
pricing_mode,
payment_status,
payment_method,
created_by_user_id,
created_by_role
)
VALUES (
v_club_id,
v_season_id,
(v_normalized_booking->>'court_id')::uuid,
(v_normalized_booking->>'booking_date')::date,
v_season_sport,
(v_normalized_booking->>'start_time')::time,
(v_normalized_booking->>'end_time')::time,
'one-time',
v_normalized_booking->>'booking_name',
p_target_user_profile_id,
v_normalized_booking->>'email',
v_normalized_booking->>'phone',
v_normalized_booking->>'booking_note',
v_normalized_booking->>'internal_admin_note',
'confirmed',
(v_normalized_booking->>'is_all_day')::boolean,
(v_normalized_booking->>'is_all_courts')::boolean,
ARRAY[
(v_normalized_booking->>'court_id')::uuid
]::uuid[],
(v_normalized_booking->>'is_partner_search_enabled')::boolean,
(v_normalized_booking->>'booking_owner_utr_rating')::numeric,
(v_normalized_booking->>'partner_search_utr_min')::numeric,
(v_normalized_booking->>'partner_search_utr_max')::numeric,
(v_normalized_booking->>'partner_search_players_needed')::integer,
v_normalized_booking->>'partner_search_status',
true,
true,
(v_normalized_booking->>'calculated_price')::numeric,
(v_normalized_booking->>'calculated_price')::numeric,
v_season_currency,
'regular',
'pending',
'venue',
v_caller_profile_id,
v_caller_role
)
RETURNING id
INTO v_new_booking_id;
PERFORM set_config(
'app.bypass_booking_security',
'false',
true
);
EXCEPTION
WHEN OTHERS THEN
PERFORM set_config(
'app.bypass_booking_security',
'false',
true
);
RAISE;
END;
v_booking_ids :=
array_append(v_booking_ids, v_new_booking_id);
END LOOP;
UPDATE public.booking_requests
SET
request_status = 'completed',
created_booking_ids = v_booking_ids,
completed_at = now()
WHERE request_id = p_request_id;
RETURN v_booking_ids;
EXCEPTION
WHEN OTHERS THEN
PERFORM set_config(
'app.bypass_booking_security',
'false',
true
);
RAISE;
END;
$function$
"
public,create_recurring_booking_series,f,"p_request_id uuid, p_target_user_profile_id uuid, p_payment_method text, p_series jsonb","p_request_id uuid, p_target_user_profile_id uuid, p_payment_method text, p_series jsonb",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.create_recurring_booking_series(p_request_id uuid, p_target_user_profile_id uuid, p_payment_method text, p_series jsonb)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_uid uuid;
v_caller_profile public.user_profiles%ROWTYPE;
v_target_profile public.user_profiles%ROWTYPE;
v_season public.seasons%ROWTYPE;
v_existing_series public.recurring_booking_series%ROWTYPE;
v_existing_plan public.venue_payment_plans%ROWTYPE;
v_wallet public.credit_wallets%ROWTYPE;
v_club_id uuid;
v_season_id uuid;
v_start_date date;
v_end_date date;
v_start_time time without time zone;
v_end_time time without time zone;
v_currency text;
v_pricing_mode text;
v_credit_type text;
v_requested_booking_name text;
v_requested_customer_email text;
v_requested_customer_phone text;
v_effective_booking_name text;
v_effective_customer_email text;
v_effective_customer_phone text;
v_booking_note text;
v_internal_admin_note text;
v_is_partner_search_enabled boolean := false;
v_booking_owner_utr_rating numeric;
v_partner_search_utr_min numeric;
v_partner_search_utr_max numeric;
v_partner_search_players_needed integer;
v_is_admin boolean := false;
v_is_guest boolean := false;
v_explicit_guest_signal boolean := false;
v_caller_profile_count integer;
v_court_value jsonb;
v_court_text text;
v_court_id uuid;
v_original_court_count integer;
v_distinct_court_count integer;
v_missing_courts text;
v_installment_numeric numeric;
v_installment_count integer;
v_base_amount numeric(14,2);
v_remainder numeric(14,2);
v_installment_amount numeric(14,2);
v_installment_index integer;
v_current_date date;
v_week_count integer := 0;
v_occurrence_count integer := 0;
v_child_price numeric(14,2);
v_total_amount numeric(14,2);
v_remaining_credit_balance numeric(14,2);
v_series_id uuid;
v_booking_id uuid;
v_plan_id uuid;
v_booking_ids uuid[] := ARRAY[]::uuid[];
v_occurrence record;
v_existing_series_count integer;
v_actual_child_count integer;
v_nonnull_payment_method_count integer;
v_distinct_payment_method_count integer;
v_original_payment_method text;
v_distinct_creator_role_count integer;
v_original_creator_role text;
v_existing_installment_count integer;
v_existing_installment_number_count integer;
v_existing_installment_total numeric(14,2);
BEGIN
v_caller_uid := auth.uid();
IF v_caller_uid IS NULL THEN
RAISE EXCEPTION 'Authentication required.';
END IF;
IF p_request_id IS NULL THEN
RAISE EXCEPTION 'A valid request UUID is required.';
END IF;
IF p_payment_method IS NULL
OR p_payment_method NOT IN (
'venue',
'credit_standard',
'credit_season_pass',
'supported'
)
THEN
RAISE EXCEPTION
'Payment method must be venue, credit_standard, credit_season_pass, or supported.';
END IF;
IF p_series IS NULL OR jsonb_typeof(p_series) <> 'object' THEN
RAISE EXCEPTION 'Series payload must be a JSON object.';
END IF;
BEGIN
v_club_id :=
NULLIF(BTRIM(p_series->>'club_id'), '')::uuid;
v_season_id :=
NULLIF(BTRIM(p_series->>'season_id'), '')::uuid;
v_start_date :=
NULLIF(BTRIM(p_series->>'start_date'), '')::date;
v_start_time :=
NULLIF(BTRIM(p_series->>'start_time'), '')::time;
v_end_time :=
NULLIF(BTRIM(p_series->>'end_time'), '')::time;
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION
'Club, season, date, or time has an invalid format.'
USING DETAIL = SQLERRM;
END;
IF v_club_id IS NULL
OR v_season_id IS NULL
OR v_start_date IS NULL
OR v_start_time IS NULL
OR v_end_time IS NULL
THEN
RAISE EXCEPTION
'Club, season, first date, start time, and end time are required.';
END IF;
IF EXTRACT(SECOND FROM v_start_time) <> 0
OR EXTRACT(SECOND FROM v_end_time) <> 0
OR MOD(EXTRACT(MINUTE FROM v_start_time)::integer, 30) <> 0
OR MOD(EXTRACT(MINUTE FROM v_end_time)::integer, 30) <> 0
THEN
RAISE EXCEPTION
'Booking times must be on exact 30-minute boundaries.';
END IF;
IF v_start_time >= v_end_time THEN
RAISE EXCEPTION
'Start time must be strictly earlier than end time; overnight bookings are not supported.';
END IF;
v_requested_booking_name :=
NULLIF(BTRIM(p_series->>'booking_name'), '');
v_requested_customer_email :=
NULLIF(BTRIM(p_series->>'customer_email'), '');
v_requested_customer_phone :=
NULLIF(BTRIM(p_series->>'customer_phone'), '');
v_booking_note :=
NULLIF(BTRIM(p_series->>'booking_note'), '');
v_internal_admin_note :=
NULLIF(BTRIM(p_series->>'internal_admin_note'), '');
BEGIN
IF p_series ? 'is_guest_booking' THEN
IF p_series->'is_guest_booking' = 'null'::jsonb
OR jsonb_typeof(p_series->'is_guest_booking') <> 'boolean'
THEN
RAISE EXCEPTION
'is_guest_booking must be a non-null boolean when supplied.';
END IF;
v_explicit_guest_signal :=
(p_series->>'is_guest_booking')::boolean;
END IF;
IF p_series ? 'is_partner_search_enabled'
AND p_series->'is_partner_search_enabled' <> 'null'::jsonb
THEN
v_is_partner_search_enabled :=
(p_series->>'is_partner_search_enabled')::boolean;
END IF;
IF p_series ? 'booking_owner_utr_rating'
AND p_series->'booking_owner_utr_rating' <> 'null'::jsonb
THEN
v_booking_owner_utr_rating :=
(p_series->>'booking_owner_utr_rating')::numeric;
END IF;
IF p_series ? 'partner_search_utr_min'
AND p_series->'partner_search_utr_min' <> 'null'::jsonb
THEN
v_partner_search_utr_min :=
(p_series->>'partner_search_utr_min')::numeric;
END IF;
IF p_series ? 'partner_search_utr_max'
AND p_series->'partner_search_utr_max' <> 'null'::jsonb
THEN
v_partner_search_utr_max :=
(p_series->>'partner_search_utr_max')::numeric;
END IF;
IF p_series ? 'partner_search_players_needed'
AND p_series->'partner_search_players_needed' <> 'null'::jsonb
THEN
v_partner_search_players_needed :=
(p_series->>'partner_search_players_needed')::integer;
END IF;
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION
'Partner-search metadata has an invalid type.'
USING DETAIL = SQLERRM;
END;
IF p_payment_method = 'venue' THEN
IF NOT (p_series ? 'installment_count')
OR p_series->'installment_count' = 'null'::jsonb
OR jsonb_typeof(p_series->'installment_count') <> 'number'
THEN
RAISE EXCEPTION
'Venue payment requires an explicit installment_count of 1, 2, or 3.';
END IF;
BEGIN
v_installment_numeric :=
(p_series->>'installment_count')::numeric;
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION
'installment_count must be the integer 1, 2, or 3.';
END;
IF v_installment_numeric <> TRUNC(v_installment_numeric)
OR v_installment_numeric NOT IN (1, 2, 3)
THEN
RAISE EXCEPTION
'installment_count must be the integer 1, 2, or 3.';
END IF;
v_installment_count := v_installment_numeric::integer;
ELSE
IF p_series ? 'installment_count'
AND p_series->'installment_count' <> 'null'::jsonb
THEN
RAISE EXCEPTION
'Non-venue recurring series must not include installment_count.';
END IF;
v_installment_count := NULL;
END IF;
IF NOT (p_series ? 'court_ids')
OR jsonb_typeof(p_series->'court_ids') <> 'array'
OR jsonb_array_length(p_series->'court_ids') = 0
THEN
RAISE EXCEPTION 'court_ids must be a non-empty JSON array.';
END IF;
CREATE TEMP TABLE pg_temp.phase2e_requested_courts (
court_id uuid PRIMARY KEY
) ON COMMIT DROP;
v_original_court_count :=
jsonb_array_length(p_series->'court_ids');
FOR v_court_value IN
SELECT value
FROM jsonb_array_elements(p_series->'court_ids')
LOOP
IF jsonb_typeof(v_court_value) <> 'string' THEN
RAISE EXCEPTION
'Every court_ids element must be a non-null UUID string.';
END IF;
v_court_text := NULLIF(BTRIM(v_court_value #>> '{}'), '');
IF v_court_text IS NULL THEN
RAISE EXCEPTION
'Every court_ids element must be a non-empty UUID string.';
END IF;
BEGIN
v_court_id := v_court_text::uuid;
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION 'Invalid court UUID: %.', v_court_text;
END;
INSERT INTO pg_temp.phase2e_requested_courts (court_id)
VALUES (v_court_id)
ON CONFLICT (court_id) DO NOTHING;
END LOOP;
SELECT COUNT(*)
INTO v_distinct_court_count
FROM pg_temp.phase2e_requested_courts;
IF v_distinct_court_count <> v_original_court_count THEN
RAISE EXCEPTION
'Duplicate court IDs are not permitted.';
END IF;
SELECT COUNT(*)
INTO v_caller_profile_count
FROM public.user_profiles
WHERE auth_user_id = v_caller_uid
AND club_id = v_club_id;
IF v_caller_profile_count <> 1 THEN
RAISE EXCEPTION
'Exactly one caller profile must exist in the requested club.';
END IF;
SELECT *
INTO v_caller_profile
FROM public.user_profiles
WHERE auth_user_id = v_caller_uid
AND club_id = v_club_id;
IF v_caller_profile.role IN (
'owner',
'admin',
'secondary_admin'
) THEN
v_is_admin := true;
ELSIF v_caller_profile.role IN (
'user',
'registered_user',
'coach'
) THEN
v_is_admin := false;
ELSE
RAISE EXCEPTION
'Caller role is null, unknown, or not permitted to create recurring bookings.';
END IF;
v_is_guest := p_target_user_profile_id IS NULL;
IF v_is_guest AND v_explicit_guest_signal IS NOT TRUE THEN
RAISE EXCEPTION
'A null target profile is permitted only with explicit is_guest_booking=true.';
END IF;
IF NOT v_is_guest AND v_explicit_guest_signal IS TRUE THEN
RAISE EXCEPTION
'is_guest_booking=true is incompatible with a registered target profile.';
END IF;
IF v_is_guest THEN
IF NOT v_is_admin THEN
RAISE EXCEPTION
'Only an owner, admin, or secondary_admin may create a guest recurring series.';
END IF;
IF p_payment_method <> 'venue' THEN
RAISE EXCEPTION
'Guest recurring series may use venue payment only.';
END IF;
IF v_requested_booking_name IS NULL THEN
RAISE EXCEPTION
'An explicit guest booking name is required.';
END IF;
v_effective_booking_name := v_requested_booking_name;
v_effective_customer_email := v_requested_customer_email;
v_effective_customer_phone := v_requested_customer_phone;
ELSE
SELECT *
INTO v_target_profile
FROM public.user_profiles
WHERE id = p_target_user_profile_id
AND club_id = v_club_id;
IF v_target_profile.id IS NULL THEN
RAISE EXCEPTION
'Target user profile was not found in the requested club.';
END IF;
IF NOT v_is_admin
AND p_target_user_profile_id IS DISTINCT FROM v_caller_profile.id
THEN
RAISE EXCEPTION
'A normal user may create a recurring series only for their own profile.';
END IF;
IF NOT v_is_admin THEN
v_effective_booking_name :=
NULLIF(BTRIM(v_target_profile.full_name), '');
v_effective_customer_email :=
NULLIF(BTRIM(v_target_profile.email), '');
v_effective_customer_phone :=
NULLIF(BTRIM(v_target_profile.phone), '');
v_internal_admin_note := NULL;
ELSE
v_effective_booking_name := COALESCE(
v_requested_booking_name,
NULLIF(BTRIM(v_target_profile.full_name), '')
);
v_effective_customer_email := COALESCE(
v_requested_customer_email,
NULLIF(BTRIM(v_target_profile.email), '')
);
v_effective_customer_phone := COALESCE(
v_requested_customer_phone,
NULLIF(BTRIM(v_target_profile.phone), '')
);
END IF;
IF v_effective_booking_name IS NULL THEN
RAISE EXCEPTION
'The target profile has no usable booking name.';
END IF;
IF p_payment_method = 'supported'
AND v_target_profile.is_supported_player IS NOT TRUE
THEN
RAISE EXCEPTION
'Target user is not a supported player.';
END IF;
END IF;
IF NOT v_is_admin THEN
v_internal_admin_note := NULL;
END IF;
SELECT *
INTO v_season
FROM public.seasons
WHERE id = v_season_id
AND club_id = v_club_id;
IF v_season.id IS NULL THEN
RAISE EXCEPTION
'Season was not found in the requested club.';
END IF;
IF v_season.start_date IS NULL
OR v_season.end_date IS NULL
OR v_season.currency IS NULL
OR v_season.currency NOT IN ('HUF', 'EUR', 'USD')
THEN
RAISE EXCEPTION
'Season date or currency configuration is invalid.';
END IF;
v_end_date := v_season.end_date;
v_currency := v_season.currency;
IF v_start_date < v_season.start_date
OR v_start_date > v_end_date
THEN
RAISE EXCEPTION
'The first selected date is outside the authoritative season.';
END IF;
CREATE TEMP TABLE pg_temp.phase2e_expected_occurrences (
booking_date date NOT NULL,
court_id uuid NOT NULL,
start_time time without time zone NOT NULL,
end_time time without time zone NOT NULL,
price numeric(14,2),
PRIMARY KEY (booking_date, court_id, start_time, end_time)
) ON COMMIT DROP;
v_current_date := v_start_date;
WHILE v_current_date <= v_end_date LOOP
INSERT INTO pg_temp.phase2e_expected_occurrences (
booking_date,
court_id,
start_time,
end_time
)
SELECT
v_current_date,
c.court_id,
v_start_time,
v_end_time
FROM pg_temp.phase2e_requested_courts AS c
ORDER BY c.court_id;
v_week_count := v_week_count + 1;
v_current_date := v_current_date + 7;
END LOOP;
SELECT COUNT(*)
INTO v_occurrence_count
FROM pg_temp.phase2e_expected_occurrences;
IF v_week_count = 0 OR v_occurrence_count = 0 THEN
RAISE EXCEPTION
'The request produces no weekly occurrences.';
END IF;
IF v_occurrence_count > 100 THEN
RAISE EXCEPTION
'Maximum allowed recurring child bookings (100) exceeded.';
END IF;
IF v_week_count = 1 THEN
v_pricing_mode := 'regular';
ELSE
v_pricing_mode := 'season_pass';
END IF;
PERFORM pg_advisory_xact_lock(
hashtextextended(p_request_id::text, 0)
);
SELECT COUNT(*)
INTO v_existing_series_count
FROM public.recurring_booking_series
WHERE creation_request_id = p_request_id;
IF v_existing_series_count > 1 THEN
RAISE EXCEPTION
'Integrity error: request UUID maps to multiple recurring series.';
END IF;
IF v_existing_series_count = 1 THEN
SELECT *
INTO v_existing_series
FROM public.recurring_booking_series
WHERE creation_request_id = p_request_id
FOR UPDATE;
IF v_existing_series.created_by_user_profile_id
IS DISTINCT FROM v_caller_profile.id
OR v_existing_series.user_profile_id
IS DISTINCT FROM p_target_user_profile_id
OR v_existing_series.club_id IS DISTINCT FROM v_club_id
OR v_existing_series.season_id IS DISTINCT FROM v_season_id
OR v_existing_series.start_date IS DISTINCT FROM v_start_date
OR v_existing_series.end_date IS DISTINCT FROM v_end_date
OR v_existing_series.currency IS DISTINCT FROM v_currency
OR v_existing_series.pricing_mode
IS DISTINCT FROM v_pricing_mode
OR v_existing_series.total_occurrences
IS DISTINCT FROM v_occurrence_count
THEN
RAISE EXCEPTION
'Idempotency collision: caller, target, club, season, date, schedule, currency, or pricing mode differs.';
END IF;
IF v_is_guest
AND (
v_existing_series.booking_name
IS DISTINCT FROM v_effective_booking_name
OR v_existing_series.customer_email
IS DISTINCT FROM v_effective_customer_email
OR v_existing_series.customer_phone
IS DISTINCT FROM v_effective_customer_phone
)
THEN
RAISE EXCEPTION
'Idempotency collision: guest identity fields differ.';
END IF;
SELECT COUNT(*)
INTO v_actual_child_count
FROM public.bookings
WHERE recurring_series_id = v_existing_series.id;
IF v_actual_child_count = 0 THEN
RAISE EXCEPTION
'Integrity error: existing recurring series has no child bookings.';
END IF;
IF EXISTS (
SELECT 1
FROM public.bookings
WHERE recurring_series_id = v_existing_series.id
GROUP BY booking_date, court_id, start_time, end_time
HAVING COUNT(*) > 1
) THEN
RAISE EXCEPTION
'Integrity error: existing recurring series contains duplicate schedule tuples.';
END IF;
IF EXISTS (
(
SELECT
e.booking_date,
e.court_id,
e.start_time,
e.end_time
FROM pg_temp.phase2e_expected_occurrences AS e
)
EXCEPT
(
SELECT
b.booking_date,
b.court_id,
b.start_time,
b.end_time
FROM public.bookings AS b
WHERE b.recurring_series_id = v_existing_series.id
)
)
OR EXISTS (
(
SELECT
b.booking_date,
b.court_id,
b.start_time,
b.end_time
FROM public.bookings AS b
WHERE b.recurring_series_id = v_existing_series.id
)
EXCEPT
(
SELECT
e.booking_date,
e.court_id,
e.start_time,
e.end_time
FROM pg_temp.phase2e_expected_occurrences AS e
)
)
THEN
RAISE EXCEPTION
'Idempotency collision or integrity error: complete child schedule differs.';
END IF;
IF v_actual_child_count <> v_occurrence_count
OR v_existing_series.total_occurrences <> v_actual_child_count
THEN
RAISE EXCEPTION
'Integrity error: child count does not match the request and series total.';
END IF;
IF EXISTS (
SELECT 1
FROM public.bookings AS b
WHERE b.recurring_series_id = v_existing_series.id
AND (
b.booking_type IS DISTINCT FROM 'recurring'
OR b.club_id IS DISTINCT FROM v_existing_series.club_id
OR b.season_id IS DISTINCT FROM v_existing_series.season_id
OR b.user_id IS DISTINCT FROM v_existing_series.user_profile_id
OR b.currency IS DISTINCT FROM v_existing_series.currency
OR b.pricing_mode
IS DISTINCT FROM v_existing_series.pricing_mode
OR b.created_by_user_id
IS DISTINCT FROM v_existing_series.created_by_user_profile_id
OR b.booking_name
IS DISTINCT FROM v_existing_series.booking_name
OR b.email IS DISTINCT FROM v_existing_series.customer_email
OR b.phone IS DISTINCT FROM v_existing_series.customer_phone
OR b.price_per_booking IS NULL
OR (
b.payment_method = 'supported'
AND b.price_per_booking <> 0
)
OR (
b.payment_method IS DISTINCT FROM 'supported'
AND b.price_per_booking <= 0
)
OR b.price_per_booking = 'NaN'::numeric
OR b.price_per_booking = 'Infinity'::numeric
OR b.price_per_booking = '-Infinity'::numeric
OR b.estimated_total_price
IS DISTINCT FROM b.price_per_booking
OR (
b.currency = 'HUF'
AND b.price_per_booking <> ROUND(b.price_per_booking)
)
)
) THEN
RAISE EXCEPTION
'Integrity error: one or more existing child booking invariants differ.';
END IF;
IF (
SELECT SUM(b.price_per_booking)
FROM public.bookings AS b
WHERE b.recurring_series_id = v_existing_series.id
) IS DISTINCT FROM v_existing_series.total_amount
THEN
RAISE EXCEPTION
'Integrity error: child prices do not sum to the stored series total.';
END IF;
SELECT
COUNT(b.payment_method),
COUNT(DISTINCT b.payment_method),
MIN(b.payment_method)
INTO
v_nonnull_payment_method_count,
v_distinct_payment_method_count,
v_original_payment_method
FROM public.bookings AS b
WHERE b.recurring_series_id = v_existing_series.id;
IF v_nonnull_payment_method_count <> v_actual_child_count
OR v_distinct_payment_method_count <> 1
OR v_original_payment_method NOT IN (
'venue',
'credit_standard',
'credit_season_pass',
'supported'
)
THEN
RAISE EXCEPTION
'Integrity error: all child bookings must have one supported non-null payment method.';
END IF;
IF v_original_payment_method IS DISTINCT FROM p_payment_method THEN
RAISE EXCEPTION
'Idempotency collision: original payment method differs.';
END IF;
SELECT
COUNT(DISTINCT b.created_by_role),
MIN(b.created_by_role)
INTO
v_distinct_creator_role_count,
v_original_creator_role
FROM public.bookings AS b
WHERE b.recurring_series_id = v_existing_series.id;
IF v_distinct_creator_role_count <> 1
OR v_original_creator_role IS NULL
OR EXISTS (
SELECT 1
FROM public.bookings AS b
WHERE b.recurring_series_id = v_existing_series.id
AND b.created_by_role IS NULL
)
OR v_original_creator_role NOT IN (
'owner',
'admin',
'secondary_admin',
'user',
'registered_user',
'coach'
)
THEN
RAISE EXCEPTION
'Integrity error: child creator-role metadata is missing or inconsistent.';
END IF;
IF v_original_payment_method = 'venue' THEN
IF v_installment_count IS NULL THEN
RAISE EXCEPTION
'Idempotency collision: venue installment count is missing.';
END IF;
IF (
SELECT COUNT(*)
FROM public.venue_payment_plans AS p
WHERE p.recurring_series_id = v_existing_series.id
) <> 1
THEN
RAISE EXCEPTION
'Integrity error: venue series must have exactly one linked payment plan.';
END IF;
SELECT *
INTO v_existing_plan
FROM public.venue_payment_plans
WHERE recurring_series_id = v_existing_series.id
FOR UPDATE;
IF v_existing_plan.creation_request_id
IS DISTINCT FROM p_request_id
OR v_existing_plan.booking_id IS NOT NULL
OR v_existing_plan.club_id
IS DISTINCT FROM v_existing_series.club_id
OR v_existing_plan.season_id
IS DISTINCT FROM v_existing_series.season_id
OR v_existing_plan.recurring_series_id
IS DISTINCT FROM v_existing_series.id
OR v_existing_plan.user_profile_id
IS DISTINCT FROM v_existing_series.user_profile_id
OR v_existing_plan.customer_name
IS DISTINCT FROM v_existing_series.booking_name
OR v_existing_plan.customer_email
IS DISTINCT FROM v_existing_series.customer_email
OR v_existing_plan.customer_phone
IS DISTINCT FROM v_existing_series.customer_phone
OR v_existing_plan.payment_method
IS DISTINCT FROM 'venue'
OR v_existing_plan.total_amount
IS DISTINCT FROM v_existing_series.total_amount
OR v_existing_plan.currency
IS DISTINCT FROM v_existing_series.currency
OR v_existing_plan.installment_count
IS DISTINCT FROM v_installment_count
OR (
v_original_creator_role IN (
'owner',
'admin',
'secondary_admin'
)
AND v_existing_plan.created_by_admin_id
IS DISTINCT FROM v_existing_series.created_by_user_profile_id
)
OR (
v_original_creator_role IN (
'user',
'registered_user',
'coach'
)
AND v_existing_plan.created_by_admin_id IS NOT NULL
)
THEN
RAISE EXCEPTION
'Idempotency collision or integrity error: venue plan differs from the original request/series.';
END IF;
SELECT
COUNT(*),
COUNT(DISTINCT i.installment_number),
SUM(i.amount)
INTO
v_existing_installment_count,
v_existing_installment_number_count,
v_existing_installment_total
FROM public.venue_payment_installments AS i
WHERE i.plan_id = v_existing_plan.id;
IF v_existing_installment_count
<> v_existing_plan.installment_count
OR v_existing_installment_number_count
<> v_existing_plan.installment_count
OR v_existing_installment_total
IS DISTINCT FROM v_existing_plan.total_amount
OR EXISTS (
SELECT 1
FROM public.venue_payment_installments AS i
WHERE i.plan_id = v_existing_plan.id
AND (
i.installment_number < 1
OR i.installment_number
> v_existing_plan.installment_count
OR i.currency
IS DISTINCT FROM v_existing_plan.currency
OR i.amount <= 0
OR i.amount = 'NaN'::numeric
OR i.amount = 'Infinity'::numeric
OR i.amount = '-Infinity'::numeric
OR (
i.currency = 'HUF'
AND i.amount <> ROUND(i.amount)
)
)
)
THEN
RAISE EXCEPTION
'Integrity error: venue installments do not exactly match the plan.';
END IF;
ELSIF v_original_payment_method IN (
'credit_standard',
'credit_season_pass'
) THEN
IF v_installment_count IS NOT NULL THEN
RAISE EXCEPTION
'Idempotency collision: credit series must not have installment_count.';
END IF;
IF EXISTS (
SELECT 1
FROM public.venue_payment_plans AS p
WHERE p.recurring_series_id = v_existing_series.id
OR p.creation_request_id = p_request_id
) THEN
RAISE EXCEPTION
'Integrity error: credit-paid series must not have a venue payment plan.';
END IF;
v_credit_type := CASE
WHEN v_original_payment_method = 'credit_standard'
THEN 'standard'
ELSE 'season_pass'
END;
IF EXISTS (
SELECT 1
FROM public.bookings AS b
WHERE b.recurring_series_id = v_existing_series.id
AND (
(
SELECT COUNT(*)
FROM public.booking_payments AS bp
WHERE bp.booking_id = b.id
) <> 1
OR NOT EXISTS (
SELECT 1
FROM public.booking_payments AS bp
WHERE bp.booking_id = b.id
AND bp.club_id = b.club_id
AND bp.season_id = b.season_id
AND bp.user_profile_id IS NOT DISTINCT FROM b.user_id
AND bp.payment_method = v_original_payment_method
AND bp.amount = b.price_per_booking
AND bp.currency = b.currency
AND bp.credit_type = v_credit_type
AND bp.wallet_id IS NOT NULL
)
OR (
SELECT COUNT(*)
FROM public.credit_transactions AS ct
WHERE ct.booking_id = b.id
AND ct.transaction_category = 'booking_payment'
) <> 1
OR NOT EXISTS (
SELECT 1
FROM public.credit_transactions AS ct
JOIN public.booking_payments AS bp
ON bp.booking_id = b.id
AND bp.wallet_id = ct.wallet_id
WHERE ct.booking_id = b.id
AND ct.club_id = b.club_id
AND ct.season_id = b.season_id
AND ct.user_profile_id = b.user_id
AND ct.credit_type = v_credit_type
AND ct.currency = b.currency
AND ct.amount = -b.price_per_booking
AND ct.transaction_category = 'booking_payment'
AND ct.created_by =
v_existing_series.created_by_user_profile_id
)
)
) THEN
RAISE EXCEPTION
'Integrity error: credit child payment or debit ledger records are incomplete/inconsistent.';
END IF;
SELECT w.balance
INTO v_remaining_credit_balance
FROM public.credit_wallets AS w
WHERE w.user_profile_id = v_existing_series.user_profile_id
AND w.club_id = v_existing_series.club_id
AND w.season_id = v_existing_series.season_id
AND w.currency = v_existing_series.currency
AND w.credit_type = v_credit_type;
ELSIF v_original_payment_method = 'supported' THEN
IF v_installment_count IS NOT NULL THEN
RAISE EXCEPTION
'Idempotency collision: supported series must not have installment_count.';
END IF;
IF v_existing_series.total_amount IS DISTINCT FROM 0::numeric THEN
RAISE EXCEPTION
'Integrity error: supported recurring series total must be zero.';
END IF;
IF EXISTS (
SELECT 1
FROM public.venue_payment_plans AS p
WHERE p.recurring_series_id = v_existing_series.id
OR p.creation_request_id = p_request_id
) THEN
RAISE EXCEPTION
'Integrity error: supported series must not have a venue payment plan.';
END IF;
IF EXISTS (
SELECT 1
FROM public.bookings AS b
WHERE b.recurring_series_id = v_existing_series.id
AND (
b.payment_status IS DISTINCT FROM 'paid'
OR b.price_per_booking IS DISTINCT FROM 0::numeric
OR b.estimated_total_price IS DISTINCT FROM 0::numeric
OR (
SELECT COUNT(*)
FROM public.booking_payments AS bp
WHERE bp.booking_id = b.id
) <> 1
OR NOT EXISTS (
SELECT 1
FROM public.booking_payments AS bp
WHERE bp.booking_id = b.id
AND bp.club_id = b.club_id
AND bp.season_id = b.season_id
AND bp.user_profile_id IS NOT DISTINCT FROM b.user_id
AND bp.payment_method = 'supported'
AND bp.payment_status = 'paid'
AND bp.amount = 0::numeric
AND bp.currency = b.currency
AND bp.credit_type IS NULL
AND bp.wallet_id IS NULL
AND bp.paid_at IS NOT NULL
)
OR EXISTS (
SELECT 1
FROM public.credit_transactions AS ct
WHERE ct.booking_id = b.id
)
)
) THEN
RAISE EXCEPTION
'Integrity error: supported child booking or zero-value payment records are incomplete/inconsistent.';
END IF;
ELSE
RAISE EXCEPTION
'Integrity error: unsupported recurring-series payment method.';
END IF;
SELECT ARRAY_AGG(
b.id
ORDER BY
b.booking_date,
b.court_id,
b.start_time,
b.end_time,
b.id
)
INTO v_booking_ids
FROM public.bookings AS b
WHERE b.recurring_series_id = v_existing_series.id;
RETURN jsonb_build_object(
'recurring_series_id', v_existing_series.id,
'booking_ids', v_booking_ids,
'total_occurrences', v_existing_series.total_occurrences,
'total_amount', v_existing_series.total_amount,
'currency', v_existing_series.currency,
'pricing_mode', v_existing_series.pricing_mode,
'payment_method', v_original_payment_method,
'target_user_profile_id', v_existing_series.user_profile_id,
'already_processed', true,
'remaining_credit_balance', v_remaining_credit_balance
);
END IF;
IF EXISTS (
SELECT 1
FROM public.venue_payment_plans
WHERE creation_request_id = p_request_id
) THEN
RAISE EXCEPTION
'Idempotency collision: request UUID already belongs to another venue payment plan.';
END IF;
SELECT STRING_AGG(r.court_id::text, ', ' ORDER BY r.court_id)
INTO v_missing_courts
FROM pg_temp.phase2e_requested_courts AS r
LEFT JOIN public.courts AS c
ON c.id = r.court_id
AND c.club_id = v_club_id
AND c.season_id = v_season_id
AND c.is_active IS TRUE
AND c.sport = v_season.sport
WHERE c.id IS NULL;
IF v_missing_courts IS NOT NULL THEN
RAISE EXCEPTION
'One or more courts are missing, inactive, or outside the requested club/season: %.',
v_missing_courts;
END IF;
IF EXISTS (
SELECT 1
FROM pg_temp.phase2e_expected_occurrences AS e
WHERE NOT EXISTS (
SELECT 1
FROM public.opening_hours AS h
WHERE h.club_id = v_club_id
AND h.season_id = v_season_id
AND h.weekday =
EXTRACT(DOW FROM e.booking_date)::integer
AND h.is_open IS TRUE
AND h.opening_time <= e.start_time
AND h.closing_time >= e.end_time
)
) THEN
RAISE EXCEPTION
'At least one weekly occurrence is outside configured opening hours.';
END IF;
IF EXISTS (
SELECT 1
FROM pg_temp.phase2e_expected_occurrences AS e
JOIN public.bookings AS b
ON b.court_id = e.court_id
AND b.booking_date = e.booking_date
AND b.deleted_at IS NULL
AND COALESCE(b.status, 'confirmed') NOT IN (
'cancelled',
'deleted by club',
'failed'
)
AND b.start_time < e.end_time
AND b.end_time > e.start_time
) THEN
RAISE EXCEPTION
'At least one requested recurring occurrence conflicts with an existing booking.';
END IF;
-- COURT-AWARE PRICING: price each (booking_date, court_id) occurrence
-- independently using that court's own pricing group. Supported = 0 Ft.
FOR v_occurrence IN
SELECT
e.booking_date,
e.court_id,
e.start_time,
e.end_time
FROM pg_temp.phase2e_expected_occurrences AS e
ORDER BY
e.booking_date,
e.court_id,
e.start_time,
e.end_time
LOOP
IF p_payment_method = 'supported' THEN
v_child_price := 0;
ELSE
v_child_price := public.calculate_booking_price(
v_club_id,
v_season_id,
v_occurrence.booking_date,
v_occurrence.start_time,
v_occurrence.end_time,
v_pricing_mode,
v_currency,
v_occurrence.court_id
);
END IF;
IF v_child_price IS NULL
OR v_child_price = 'NaN'::numeric
OR v_child_price = 'Infinity'::numeric
OR v_child_price = '-Infinity'::numeric
OR (
p_payment_method = 'supported'
AND v_child_price <> 0
)
OR (
p_payment_method <> 'supported'
AND v_child_price <= 0
)
OR (
v_currency = 'HUF'
AND v_child_price <> ROUND(v_child_price)
)
THEN
RAISE EXCEPTION
'Authoritative child price is invalid for % on court %.',
v_occurrence.booking_date, v_occurrence.court_id;
END IF;
UPDATE pg_temp.phase2e_expected_occurrences
SET price = v_child_price
WHERE booking_date = v_occurrence.booking_date
AND court_id = v_occurrence.court_id
AND start_time = v_occurrence.start_time
AND end_time = v_occurrence.end_time;
END LOOP;
IF EXISTS (
SELECT 1
FROM pg_temp.phase2e_expected_occurrences
WHERE price IS NULL
OR price = 'NaN'::numeric
OR price = 'Infinity'::numeric
OR price = '-Infinity'::numeric
OR (
p_payment_method = 'supported'
AND price <> 0
)
OR (
p_payment_method <> 'supported'
AND price <= 0
)
) THEN
RAISE EXCEPTION
'One or more child prices are invalid or unconfigured.';
END IF;
SELECT SUM(e.price)
INTO v_total_amount
FROM pg_temp.phase2e_expected_occurrences AS e;
IF v_total_amount IS NULL
OR v_total_amount = 'NaN'::numeric
OR v_total_amount = 'Infinity'::numeric
OR v_total_amount = '-Infinity'::numeric
OR (
p_payment_method = 'supported'
AND v_total_amount <> 0
)
OR (
p_payment_method <> 'supported'
AND v_total_amount <= 0
)
OR (
v_currency = 'HUF'
AND v_total_amount <> ROUND(v_total_amount)
)
THEN
RAISE EXCEPTION
'Authoritative recurring-series total is invalid.';
END IF;
IF p_payment_method IN (
'credit_standard',
'credit_season_pass'
) THEN
IF p_target_user_profile_id IS NULL THEN
RAISE EXCEPTION
'Credit payment requires a registered target profile.';
END IF;
v_credit_type := CASE
WHEN p_payment_method = 'credit_standard'
THEN 'standard'
ELSE 'season_pass'
END;
SELECT *
INTO v_wallet
FROM public.credit_wallets
WHERE user_profile_id = p_target_user_profile_id
AND club_id = v_club_id
AND season_id = v_season_id
AND currency = v_currency
AND credit_type = v_credit_type
FOR UPDATE;
IF v_wallet.id IS NULL THEN
RAISE EXCEPTION
'Matching credit wallet was not found.';
END IF;
IF v_wallet.balance IS NULL
OR v_wallet.balance < v_total_amount
OR v_wallet.balance = 'NaN'::numeric
OR v_wallet.balance = 'Infinity'::numeric
OR v_wallet.balance = '-Infinity'::numeric
THEN
RAISE EXCEPTION
'Insufficient or invalid credit balance. Required: %, available: %.',
v_total_amount,
v_wallet.balance;
END IF;
END IF;
INSERT INTO public.recurring_booking_series (
creation_request_id,
club_id,
season_id,
user_profile_id,
booking_name,
customer_email,
customer_phone,
currency,
pricing_mode,
status,
start_date,
end_date,
total_occurrences,
total_amount,
created_by_user_profile_id
)
VALUES (
p_request_id,
v_club_id,
v_season_id,
p_target_user_profile_id,
v_effective_booking_name,
v_effective_customer_email,
v_effective_customer_phone,
v_currency,
v_pricing_mode,
'active',
v_start_date,
v_end_date,
v_occurrence_count,
v_total_amount,
v_caller_profile.id
)
RETURNING id INTO v_series_id;
FOR v_occurrence IN
SELECT
e.booking_date,
e.court_id,
e.start_time,
e.end_time,
e.price
FROM pg_temp.phase2e_expected_occurrences AS e
ORDER BY
e.booking_date,
e.court_id,
e.start_time,
e.end_time
LOOP
PERFORM set_config(
'app.bypass_booking_security',
'true',
true
);
INSERT INTO public.bookings (
club_id,
season_id,
court_id,
booking_date,
start_time,
end_time,
booking_type,
recurring_series_id,
booking_name,
user_id,
email,
phone,
booking_note,
internal_admin_note,
status,
is_partner_search_enabled,
booking_owner_utr_rating,
partner_search_utr_min,
partner_search_utr_max,
partner_search_players_needed,
partner_search_status,
price_is_configured,
price_is_fully_configured,
price_per_booking,
estimated_total_price,
currency,
pricing_mode,
created_by_user_id,
created_by_role,
payment_status,
payment_method,
sport
)
VALUES (
v_club_id,
v_season_id,
v_occurrence.court_id,
v_occurrence.booking_date,
v_occurrence.start_time,
v_occurrence.end_time,
'recurring',
v_series_id,
v_effective_booking_name,
p_target_user_profile_id,
v_effective_customer_email,
v_effective_customer_phone,
v_booking_note,
v_internal_admin_note,
'confirmed',
v_is_partner_search_enabled,
v_booking_owner_utr_rating,
v_partner_search_utr_min,
v_partner_search_utr_max,
v_partner_search_players_needed,
CASE
WHEN v_is_partner_search_enabled
THEN 'active'
ELSE 'inactive'
END,
true,
true,
v_occurrence.price,
v_occurrence.price,
v_currency,
v_pricing_mode,
v_caller_profile.id,
v_caller_profile.role,
CASE
WHEN p_payment_method IN (
'credit_standard',
'credit_season_pass',
'supported'
)
THEN 'paid'
ELSE 'pending'
END,
p_payment_method,
v_season.sport
)
RETURNING id INTO v_booking_id;
PERFORM set_config(
'app.bypass_booking_security',
'false',
true
);
v_booking_ids :=
array_append(v_booking_ids, v_booking_id);
IF p_payment_method IN (
'credit_standard',
'credit_season_pass'
) THEN
INSERT INTO public.booking_payments (
club_id,
season_id,
booking_id,
user_profile_id,
payment_method,
payment_status,
amount,
currency,
credit_type,
wallet_id,
paid_at
)
VALUES (
v_club_id,
v_season_id,
v_booking_id,
p_target_user_profile_id,
p_payment_method,
'paid',
v_occurrence.price,
v_currency,
v_credit_type,
v_wallet.id,
now()
);
INSERT INTO public.credit_transactions (
wallet_id,
club_id,
user_profile_id,
season_id,
booking_id,
credit_type,
currency,
amount,
transaction_category,
reason,
created_by,
idempotency_key
)
VALUES (
v_wallet.id,
v_club_id,
p_target_user_profile_id,
v_season_id,
v_booking_id,
v_credit_type,
v_currency,
-v_occurrence.price,
'booking_payment',
'Recurring booking payment',
v_caller_profile.id,
'booking_payment:' || v_booking_id::text
);
ELSIF p_payment_method = 'supported' THEN
INSERT INTO public.booking_payments (
club_id,
season_id,
booking_id,
user_profile_id,
payment_method,
payment_status,
amount,
currency,
credit_type,
wallet_id,
paid_at
)
VALUES (
v_club_id,
v_season_id,
v_booking_id,
p_target_user_profile_id,
'supported',
'paid',
0,
v_currency,
NULL,
NULL,
now()
);
END IF;
END LOOP;
IF p_payment_method IN (
'credit_standard',
'credit_season_pass'
) THEN
UPDATE public.credit_wallets
SET balance = balance - v_total_amount,
updated_at = now()
WHERE id = v_wallet.id
AND balance >= v_total_amount
RETURNING balance INTO v_remaining_credit_balance;
IF NOT FOUND THEN
RAISE EXCEPTION
'Credit debit failed after wallet validation.';
END IF;
END IF;
IF p_payment_method = 'venue' THEN
IF v_currency = 'HUF' THEN
v_base_amount :=
FLOOR(v_total_amount / v_installment_count);
ELSE
v_base_amount :=
TRUNC(v_total_amount / v_installment_count, 2);
END IF;
v_remainder :=
v_total_amount -
(v_base_amount * v_installment_count);
IF v_base_amount <= 0 THEN
RAISE EXCEPTION
'Total amount is too small for the selected number of positive installments.';
END IF;
INSERT INTO public.venue_payment_plans (
creation_request_id,
club_id,
season_id,
booking_id,
recurring_series_id,
user_profile_id,
customer_name,
customer_email,
customer_phone,
payment_method,
total_amount,
currency,
installment_count,
plan_status,
created_by_admin_id
)
VALUES (
p_request_id,
v_club_id,
v_season_id,
NULL,
v_series_id,
p_target_user_profile_id,
v_effective_booking_name,
v_effective_customer_email,
v_effective_customer_phone,
'venue',
v_total_amount,
v_currency,
v_installment_count,
'pending',
CASE
WHEN v_is_admin THEN v_caller_profile.id
ELSE NULL
END
)
RETURNING id INTO v_plan_id;
FOR v_installment_index IN 1..v_installment_count LOOP
v_installment_amount :=
v_base_amount +
CASE
WHEN v_installment_index = 1
THEN v_remainder
ELSE 0
END;
IF v_installment_amount <= 0
OR v_installment_amount = 'NaN'::numeric
OR v_installment_amount = 'Infinity'::numeric
OR v_installment_amount = '-Infinity'::numeric
OR (
v_currency = 'HUF'
AND v_installment_amount
<> ROUND(v_installment_amount)
)
THEN
RAISE EXCEPTION
'Calculated venue installment amount is invalid.';
END IF;
INSERT INTO public.venue_payment_installments (
plan_id,
installment_number,
amount,
currency
)
VALUES (
v_plan_id,
v_installment_index,
v_installment_amount,
v_currency
);
END LOOP;
IF (
SELECT COUNT(*)
FROM public.venue_payment_installments
WHERE plan_id = v_plan_id
) <> v_installment_count
OR (
SELECT SUM(amount)
FROM public.venue_payment_installments
WHERE plan_id = v_plan_id
) IS DISTINCT FROM v_total_amount
THEN
RAISE EXCEPTION
'Created venue installments do not exactly match the plan total/count.';
END IF;
END IF;
RETURN jsonb_build_object(
'recurring_series_id', v_series_id,
'booking_ids', v_booking_ids,
'total_occurrences', v_occurrence_count,
'total_amount', v_total_amount,
'currency', v_currency,
'pricing_mode', v_pricing_mode,
'payment_method', p_payment_method,
'target_user_profile_id', p_target_user_profile_id,
'already_processed', false,
'remaining_credit_balance', v_remaining_credit_balance
);
EXCEPTION
WHEN OTHERS THEN
PERFORM set_config(
'app.bypass_booking_security',
'false',
true
);
RAISE;
END;
$function$
"
public,current_user_club_id,f,,,uuid,sql,true,STABLE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.current_user_club_id()
RETURNS uuid
LANGUAGE sql
STABLE SECURITY DEFINER
SET search_path TO ''
AS $function$
SELECT up.club_id
FROM public.user_profiles AS up
WHERE up.auth_user_id = (SELECT auth.uid())
LIMIT 1;
$function$
"
public,current_user_profile_id,f,,,uuid,sql,true,STABLE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.current_user_profile_id()
RETURNS uuid
LANGUAGE sql
STABLE SECURITY DEFINER
SET search_path TO ''
AS $function$
SELECT up.id
FROM public.user_profiles AS up
WHERE up.auth_user_id = (SELECT auth.uid())
LIMIT 1;
$function$
"
public,deactivate_academy_program_coach,f,"p_academy_program_id uuid, p_coach_profile_id uuid, p_assigned_to date","p_academy_program_id uuid, p_coach_profile_id uuid, p_assigned_to date DEFAULT NULL::date",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.deactivate_academy_program_coach(p_academy_program_id uuid, p_coach_profile_id uuid, p_assigned_to date DEFAULT NULL::date)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_club_id uuid;
v_program_type text;
v_timezone text;
v_today date;
v_assigned_to date;
v_actor_user_profile_id uuid;
v_assignment
public.academy_program_coach_assignments%ROWTYPE;
v_rate
public.academy_coach_compensation_rates%ROWTYPE;
v_old_values jsonb;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION
'A művelethez bejelentkezés szükséges.';
END IF;
SELECT
ap.club_id,
ap.program_type,
COALESCE(NULLIF(c.timezone, ''), 'UTC')
INTO
v_club_id,
v_program_type,
v_timezone
FROM public.academy_programs ap
JOIN public.clubs c
ON c.id = ap.club_id
WHERE ap.id = p_academy_program_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'Az akadémiai program nem található.';
END IF;
IF v_program_type <> 'group' THEN
RAISE EXCEPTION
'Edzői hozzárendelés csak csoportos programnál zárható le.';
END IF;
IF NOT public.can_manage_academy_payments(v_club_id) THEN
RAISE EXCEPTION
'Nincs jogosultságod az edzői hozzárendelés lezárásához.';
END IF;
SELECT up.id
INTO v_actor_user_profile_id
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_club_id
AND up.role IN (
'owner',
'admin',
'secondary_admin'
)
LIMIT 1;
IF v_actor_user_profile_id IS NULL THEN
RAISE EXCEPTION
'Nem található jogosult adminisztrátori profil.';
END IF;
v_today :=
(now() AT TIME ZONE v_timezone)::date;
v_assigned_to :=
COALESCE(p_assigned_to, v_today);
IF v_assigned_to < v_today THEN
RAISE EXCEPTION
'A hozzárendelés visszamenőleges lezárása nem engedélyezett.';
END IF;
SELECT a.*
INTO v_assignment
FROM public.academy_program_coach_assignments a
WHERE a.academy_program_id =
p_academy_program_id
AND a.coach_profile_id =
p_coach_profile_id
AND a.is_void = false
AND a.assigned_to IS NULL
ORDER BY a.assigned_from DESC
LIMIT 1
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION
'Az edzőnek nincs lezáratlan hozzárendelése ebben a csoportban.';
END IF;
IF v_assigned_to < v_assignment.assigned_from THEN
RAISE EXCEPTION
'A lezárás dátuma nem lehet korábbi a hozzárendelés kezdőnapjánál.';
END IF;
v_old_values :=
to_jsonb(v_assignment);
UPDATE public.academy_program_coach_assignments
SET
assigned_to =
v_assigned_to,
updated_by_user_profile_id =
v_actor_user_profile_id
WHERE id = v_assignment.id
RETURNING *
INTO v_assignment;
INSERT INTO
public.academy_coach_compensation_audit_logs (
club_id,
academy_program_id,
coach_profile_id,
assignment_id,
action_type,
old_values,
new_values,
changed_by_user_profile_id
)
VALUES (
v_club_id,
p_academy_program_id,
p_coach_profile_id,
v_assignment.id,
'assignment_deactivated',
v_old_values,
to_jsonb(v_assignment),
v_actor_user_profile_id
);
-- A lezárás utáni jövőbeli díjakat érvénytelenítjük.
FOR v_rate IN
SELECT r.*
FROM public.academy_coach_compensation_rates r
WHERE r.assignment_id = v_assignment.id
AND r.is_void = false
ORDER BY r.effective_from
FOR UPDATE
LOOP
IF v_rate.effective_from >
v_assigned_to THEN
v_old_values :=
to_jsonb(v_rate);
UPDATE public.academy_coach_compensation_rates
SET
is_void = true,
updated_by_user_profile_id =
v_actor_user_profile_id
WHERE id = v_rate.id
RETURNING *
INTO v_rate;
INSERT INTO
public.academy_coach_compensation_audit_logs (
club_id,
academy_program_id,
coach_profile_id,
assignment_id,
compensation_rate_id,
action_type,
old_values,
new_values,
changed_by_user_profile_id
)
VALUES (
v_club_id,
p_academy_program_id,
p_coach_profile_id,
v_assignment.id,
v_rate.id,
'rate_voided',
v_old_values,
to_jsonb(v_rate),
v_actor_user_profile_id
);
ELSIF v_rate.effective_to IS NULL
OR v_rate.effective_to >
v_assigned_to THEN
v_old_values :=
to_jsonb(v_rate);
UPDATE public.academy_coach_compensation_rates
SET
effective_to =
v_assigned_to,
updated_by_user_profile_id =
v_actor_user_profile_id
WHERE id = v_rate.id
RETURNING *
INTO v_rate;
INSERT INTO
public.academy_coach_compensation_audit_logs (
club_id,
academy_program_id,
coach_profile_id,
assignment_id,
compensation_rate_id,
action_type,
old_values,
new_values,
changed_by_user_profile_id
)
VALUES (
v_club_id,
p_academy_program_id,
p_coach_profile_id,
v_assignment.id,
v_rate.id,
'rate_closed',
v_old_values,
to_jsonb(v_rate),
v_actor_user_profile_id
);
END IF;
END LOOP;
RETURN jsonb_build_object(
'success', true,
'club_id', v_club_id,
'academy_program_id',
p_academy_program_id,
'coach_profile_id',
p_coach_profile_id,
'assignment_id',
v_assignment.id,
'assigned_from',
v_assignment.assigned_from,
'assigned_to',
v_assignment.assigned_to
);
END;
$function$
"
public,delete_academy_program,f,"p_program_id uuid, p_club_id uuid, p_sport text","p_program_id uuid, p_club_id uuid, p_sport text DEFAULT 'tennis'::text",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.delete_academy_program(p_program_id uuid, p_club_id uuid, p_sport text DEFAULT 'tennis'::text)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_profile_id uuid;
v_caller_role text;
v_program record;
v_has_booking_data boolean := false;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Not authenticated.';
END IF;
SELECT up.id, up.role
INTO v_caller_profile_id, v_caller_role
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = p_club_id
LIMIT 1;
IF v_caller_profile_id IS NULL THEN
RAISE EXCEPTION 'Caller profile not found in this club.';
END IF;
IF v_caller_role NOT IN ('owner', 'admin', 'secondary_admin') THEN
RAISE EXCEPTION 'Only administrators may delete academy programs.';
END IF;
IF p_sport IS NULL OR p_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION 'Invalid sport value.';
END IF;
SELECT *
INTO v_program
FROM public.academy_programs
WHERE id = p_program_id
AND club_id = p_club_id
AND sport = p_sport;
IF NOT FOUND THEN
RAISE EXCEPTION 'Program not found or does not belong to this club/sport.';
END IF;
SELECT
EXISTS (
SELECT 1
FROM public.academy_booking_details abd
WHERE abd.academy_program_id = p_program_id
)
OR EXISTS (
SELECT 1
FROM public.academy_booking_batches abb
WHERE abb.academy_program_id = p_program_id
)
OR EXISTS (
SELECT 1
FROM public.academy_group_monthly_payments gp
JOIN public.academy_program_participants p
ON p.id = gp.participant_id
WHERE p.academy_program_id = p_program_id
)
OR EXISTS (
SELECT 1
FROM public.academy_camp_weekly_payments wp
JOIN public.academy_program_participants p
ON p.id = wp.participant_id
WHERE p.academy_program_id = p_program_id
)
OR EXISTS (
SELECT 1
FROM public.academy_camp_daily_payments dp
JOIN public.academy_program_participants p
ON p.id = dp.participant_id
WHERE p.academy_program_id = p_program_id
)
OR EXISTS (
SELECT 1
FROM public.academy_payment_audit_logs pal
JOIN public.academy_program_participants p
ON p.id = pal.participant_id
WHERE p.academy_program_id = p_program_id
)
INTO v_has_booking_data;
IF v_has_booking_data THEN
RAISE EXCEPTION
'A program nem törölhető, mert már foglalási vagy pénzügyi adatok kapcsolódnak hozzá.';
END IF;
DELETE FROM public.academy_program_participants
WHERE academy_program_id = p_program_id;
DELETE FROM public.academy_programs
WHERE id = p_program_id
AND club_id = p_club_id
AND sport = p_sport;
IF NOT FOUND THEN
RAISE EXCEPTION 'Program deletion failed unexpectedly.';
END IF;
RETURN jsonb_build_object(
'success', true,
'deleted_program_id', p_program_id,
'message_hu', 'Program sikeresen törölve.',
'message_en', 'Program deleted successfully.'
);
END;
$function$
"
public,demote_coach_to_user,f,target_profile_id uuid,target_profile_id uuid,json,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.demote_coach_to_user(target_profile_id uuid)
RETURNS json
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_club_id uuid;
v_target_role text;
v_caller_profile_id uuid;
v_caller_role text;
v_role_updated integer := 0;
v_permissions_deactivated integer := 0;
v_profiles_deactivated integer := 0;
BEGIN
-- ==========================================================
-- 1. AUTH
-- ==========================================================
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Authentication required.';
END IF;
IF target_profile_id IS NULL THEN
RAISE EXCEPTION 'Target profile ID is required.';
END IF;
-- ==========================================================
-- 2. TARGET + CALLER RESOLUTION
--
-- Csak akkor találjuk meg a targetet, ha a bejelentkezett
-- adminnak ugyanabban a klubban van profilja.
--
-- A target sort zároljuk a művelet végéig.
-- ==========================================================
SELECT
target_up.club_id,
target_up.role,
caller_up.id,
caller_up.role
INTO
v_club_id,
v_target_role,
v_caller_profile_id,
v_caller_role
FROM public.user_profiles target_up
JOIN public.user_profiles caller_up
ON caller_up.club_id = target_up.club_id
AND caller_up.auth_user_id = auth.uid()
WHERE target_up.id = target_profile_id
ORDER BY caller_up.id
LIMIT 1
FOR UPDATE OF target_up;
IF NOT FOUND THEN
RAISE EXCEPTION
'Target profile not found in a club managed by the caller.';
END IF;
-- ==========================================================
-- 3. ADMIN JOGOSULTSÁG
-- ==========================================================
IF v_caller_role NOT IN (
'owner',
'admin',
'secondary_admin'
) THEN
RAISE EXCEPTION
'Unauthorized: Caller does not have permission to manage coaches.';
END IF;
-- ==========================================================
-- 4. CSAK VALÓDI EDZŐ MINŐSÍTHETŐ VISSZA
-- ==========================================================
IF v_target_role IS DISTINCT FROM 'coach' THEN
RAISE EXCEPTION
'Target user is not a coach.';
END IF;
-- ==========================================================
-- 5. ROLE: coach -> user
-- ==========================================================
UPDATE public.user_profiles
SET
role = 'user',
updated_at = now()
WHERE id = target_profile_id
AND club_id = v_club_id
AND role = 'coach';
GET DIAGNOSTICS v_role_updated = ROW_COUNT;
IF v_role_updated <> 1 THEN
RAISE EXCEPTION
'Coach role update failed.';
END IF;
-- ==========================================================
-- 6. COACH PERMISSIONS DEAKTIVÁLÁSA
--
-- Nem törlünk semmit.
-- ==========================================================
UPDATE public.coach_permissions
SET
is_active = false,
updated_at = now()
WHERE coach_user_profile_id = target_profile_id
AND club_id = v_club_id
AND is_active IS DISTINCT FROM false;
GET DIAGNOSTICS
v_permissions_deactivated = ROW_COUNT;
-- ==========================================================
-- 7. COACH PROFILE DEAKTIVÁLÁSA
--
-- Nem törlünk semmit, így a történeti booking/check-in/payroll
-- kapcsolatok megmaradnak.
-- ==========================================================
UPDATE public.coach_profiles
SET
is_active = false,
updated_at = now()
WHERE user_profile_id = target_profile_id
AND is_active IS DISTINCT FROM false;
GET DIAGNOSTICS
v_profiles_deactivated = ROW_COUNT;
-- ==========================================================
-- 8. RESULT
-- ==========================================================
RETURN json_build_object(
'success', true,
'target_profile_id', target_profile_id,
'new_role', 'user',
'coach_permissions_deactivated',
v_permissions_deactivated,
'coach_profiles_deactivated',
v_profiles_deactivated
);
END;
$function$
"
public,enforce_audit_log_identity,f,,,trigger,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.enforce_audit_log_identity()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO ''
AS $function$
DECLARE
v_profile_id uuid;
v_club_id uuid;
v_full_name text;
v_role text;
BEGIN
-- Kizárólag a biztonságos foglalásáthelyezési RPC által
-- ugyanebben a tranzakcióban létrehozott auditengedély.
IF TG_OP = 'INSERT' THEN
IF NEW.action_type = 'booking_occurrence_moved'
AND NEW.entity_type = 'booking'
AND public._consume_booking_move_audit_context(
NEW.entity_id
)
THEN
RETURN NEW;
END IF;
END IF;
IF COALESCE(auth.role(), '') = 'service_role' THEN
RETURN NEW;
END IF;
SELECT
up.id,
up.club_id,
up.full_name,
up.role
INTO
v_profile_id,
v_club_id,
v_full_name,
v_role
FROM public.user_profiles AS up
WHERE up.auth_user_id = auth.uid()
LIMIT 1;
IF v_profile_id IS NULL
OR v_role NOT IN (
'owner',
'admin',
'secondary_admin'
) THEN
RAISE EXCEPTION
USING ERRCODE = '42501',
MESSAGE = 'Only club administrators can create audit records';
END IF;
NEW.club_id := v_club_id;
NEW.changed_by_user_id := v_profile_id;
NEW.changed_by_name := v_full_name;
NEW.changed_by_role := v_role;
NEW.created_at := now();
RETURN NEW;
END;
$function$
"
public,enforce_booking_write_security,f,,,trigger,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.enforce_booking_write_security()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_role text;
v_caller_profile_id uuid;
v_is_bypassed text;
v_target_club_id uuid;
BEGIN
-- A service role továbbra is rendszerjogosultsággal rendelkezik.
IF auth.role() = 'service_role' THEN
IF TG_OP = 'DELETE' THEN
RETURN OLD;
ELSE
RETURN NEW;
END IF;
END IF;
IF auth.uid() IS NULL THEN
RAISE EXCEPTION
'Not authenticated.';
END IF;
v_is_bypassed :=
current_setting(
'app.bypass_booking_security',
true
);
-- Kizárólag kontrollált SECURITY DEFINER RPC használhatja.
IF v_is_bypassed = 'true' THEN
IF TG_OP = 'DELETE' THEN
RETURN OLD;
ELSE
RETURN NEW;
END IF;
END IF;
v_target_club_id :=
CASE
WHEN TG_OP = 'DELETE'
THEN OLD.club_id
WHEN TG_OP = 'UPDATE'
THEN OLD.club_id
ELSE NEW.club_id
END;
SELECT
up.id,
up.role
INTO
v_caller_profile_id,
v_caller_role
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_target_club_id
LIMIT 1;
IF v_caller_profile_id IS NULL THEN
RAISE EXCEPTION
'Caller profile not found in this club.';
END IF;
-- ---------------------------------------------------------------
-- Kreditfoglalás biztonságos törlésének megőrzése
-- ---------------------------------------------------------------
IF TG_OP IN ('UPDATE', 'DELETE') THEN
IF OLD.payment_method IN (
'credit_standard',
'credit_season_pass'
)
AND (
TG_OP = 'DELETE'
OR (
TG_OP = 'UPDATE'
AND (
(
OLD.deleted_at IS NULL
AND NEW.deleted_at IS NOT NULL
)
OR (
NEW.status IS DISTINCT FROM OLD.status
AND NEW.status IN (
'cancelled',
'deleted by club',
'failed'
)
)
)
)
)
THEN
RAISE EXCEPTION
'Secure credit refund cancellation is not activated yet.';
END IF;
END IF;
IF TG_OP = 'DELETE' THEN
RETURN OLD;
END IF;
-- ---------------------------------------------------------------
-- Klubváltás tiltása
-- ---------------------------------------------------------------
IF TG_OP = 'UPDATE'
AND NEW.club_id IS DISTINCT FROM OLD.club_id
THEN
RAISE EXCEPTION
'Cannot move booking to another club.';
END IF;
-- ---------------------------------------------------------------
-- ÚJ BIZTONSÁGI SZABÁLY
--
-- A foglalási pozíció közvetlen UPDATE-je minden szerepkörnek
-- tiltott, beleértve az adminokat is.
-- ---------------------------------------------------------------
IF TG_OP = 'UPDATE'
AND (
NEW.booking_date
IS DISTINCT FROM OLD.booking_date
OR NEW.start_time
IS DISTINCT FROM OLD.start_time
OR NEW.end_time
IS DISTINCT FROM OLD.end_time
OR NEW.court_id
IS DISTINCT FROM OLD.court_id
OR NEW.season_id
IS DISTINCT FROM OLD.season_id
)
THEN
RAISE EXCEPTION
'Direct booking rescheduling is not allowed. Use the secure booking move RPC.';
END IF;
-- ---------------------------------------------------------------
-- Nem admin felhasználók védett mezői
-- ---------------------------------------------------------------
IF v_caller_role NOT IN (
'owner',
'admin',
'secondary_admin'
) THEN
IF TG_OP = 'INSERT' THEN
NEW.user_id :=
v_caller_profile_id;
NEW.created_by_user_id :=
v_caller_profile_id;
NEW.created_by_role :=
'registered_user';
NEW.internal_admin_note :=
NULL;
NEW.deleted_at :=
NULL;
ELSIF TG_OP = 'UPDATE' THEN
NEW.user_id :=
OLD.user_id;
NEW.created_by_user_id :=
OLD.created_by_user_id;
NEW.created_by_role :=
OLD.created_by_role;
NEW.internal_admin_note :=
OLD.internal_admin_note;
NEW.deleted_at :=
OLD.deleted_at;
NEW.club_id :=
OLD.club_id;
END IF;
ELSE
IF TG_OP = 'INSERT'
AND NEW.created_by_user_id IS NULL
THEN
NEW.created_by_user_id :=
v_caller_profile_id;
NEW.created_by_role :=
v_caller_role;
END IF;
END IF;
-- ---------------------------------------------------------------
-- Közvetlen létrehozás pénzügyi korlátozása
-- ---------------------------------------------------------------
IF TG_OP = 'INSERT' THEN
IF NEW.payment_status
IS DISTINCT FROM 'pending'
THEN
RAISE EXCEPTION
'Direct inserts must have pending payment_status.';
END IF;
IF NEW.payment_method IS NOT NULL
AND NEW.payment_method <> 'venue'
THEN
RAISE EXCEPTION
'Direct inserts must not set credit/card payment methods.';
END IF;
END IF;
-- ---------------------------------------------------------------
-- Közvetlen fizetésmódosítás tiltása
-- ---------------------------------------------------------------
IF TG_OP = 'UPDATE' THEN
IF NEW.payment_status
IS DISTINCT FROM OLD.payment_status
OR NEW.payment_method
IS DISTINCT FROM OLD.payment_method
THEN
RAISE EXCEPTION
'Direct updates cannot modify payment status or method.';
END IF;
END IF;
RETURN NEW;
END;
$function$
"
public,enforce_partner_finder_profile_identity,f,,,trigger,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.enforce_partner_finder_profile_identity()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO ''
AS $function$
DECLARE
v_profile_club_id uuid;
BEGIN
SELECT up.club_id
INTO v_profile_club_id
FROM public.user_profiles AS up
WHERE up.id = NEW.user_profile_id
LIMIT 1;
IF v_profile_club_id IS NULL THEN
RAISE EXCEPTION
USING ERRCODE = '23503',
MESSAGE = 'The selected user profile does not belong to a club';
END IF;
-- A club_id nem fogadható el a böngészőtől:
-- mindig a user_profiles rekord alapján állítjuk be.
NEW.club_id := v_profile_club_id;
NEW.updated_at := now();
RETURN NEW;
END;
$function$
"
public,enforce_settings_history_identity,f,,,trigger,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.enforce_settings_history_identity()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO ''
AS $function$
DECLARE
v_profile_id uuid;
v_club_id uuid;
v_full_name text;
v_role text;
BEGIN
-- A megbízható szerveroldali service_role műveleteket
-- nem írjuk felül.
IF COALESCE(auth.role(), '') = 'service_role' THEN
RETURN NEW;
END IF;
SELECT
up.id,
up.club_id,
up.full_name,
up.role
INTO
v_profile_id,
v_club_id,
v_full_name,
v_role
FROM public.user_profiles AS up
WHERE up.auth_user_id = auth.uid()
LIMIT 1;
IF v_profile_id IS NULL
OR v_role NOT IN (
'owner',
'admin',
'secondary_admin'
) THEN
RAISE EXCEPTION
USING ERRCODE = '42501',
MESSAGE = 'Only club administrators can create settings history';
END IF;
NEW.club_id := v_club_id;
NEW.changed_by_user_id := v_profile_id;
NEW.changed_by_name := v_full_name;
NEW.changed_by_role := v_role;
NEW.created_at := now();
RETURN NEW;
END;
$function$
"
public,enqueue_new_user_for_ghl_contact_sync,f,,,trigger,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.enqueue_new_user_for_ghl_contact_sync()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
BEGIN
-- Csak valódi, regisztrált profil kerülhet a várólistára.
IF NEW.auth_user_id IS NULL
OR NEW.club_id IS NULL
OR NULLIF(BTRIM(NEW.email), '') IS NULL
THEN
RETURN NEW;
END IF;
INSERT INTO public.ghl_contact_sync_queue (
user_profile_id,
club_id,
sync_status,
attempt_count,
next_attempt_at,
created_at,
updated_at
)
VALUES (
NEW.id,
NEW.club_id,
'pending',
0,
now(),
now(),
now()
)
ON CONFLICT (
user_profile_id
)
DO NOTHING;
RETURN NEW;
END;
$function$
"
public,ensure_academy_group_court_cost_setting,f,,,trigger,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.ensure_academy_group_court_cost_setting()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
BEGIN
IF NEW.program_type = 'group' THEN
INSERT INTO public.academy_group_court_cost_settings (
academy_program_id,
club_id,
default_court_fee_huf,
created_by_user_profile_id,
updated_by_user_profile_id
)
VALUES (
NEW.id,
NEW.club_id,
0,
NEW.created_by_user_profile_id,
NEW.created_by_user_profile_id
)
ON CONFLICT (academy_program_id)
DO NOTHING;
END IF;
RETURN NEW;
END;
$function$
"
public,get_academy_attendance_status,f,"p_club_id uuid, p_booking_id uuid, p_sport text","p_club_id uuid, p_booking_id uuid, p_sport text DEFAULT 'tennis'::text",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.get_academy_attendance_status(p_club_id uuid, p_booking_id uuid, p_sport text DEFAULT 'tennis'::text)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_booking record;
v_has_active_coach boolean := false;
v_is_checked_in boolean := false;
v_can_edit boolean := false;
v_local_now timestamp;
v_booking_start timestamp;
v_booking_end timestamp;
v_timezone text;
v_window_status text;
v_participants jsonb;
v_sport text;
BEGIN
-- Bejelentkezés ellenőrzése
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Unauthorized: User is not authenticated.';
END IF;
v_sport := lower(
COALESCE(NULLIF(btrim(p_sport), ''), 'tennis')
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION
'Invalid sport. Allowed values: tennis, padel.';
END IF;
-- Foglalás és akadémiai adatok betöltése
SELECT
b.booking_date,
b.start_time,
b.end_time,
b.status,
b.deleted_at,
b.sport,
abd.academy_program_id,
abd.academy_type,
c.timezone
INTO v_booking
FROM public.bookings b
JOIN public.academy_booking_details abd
ON abd.booking_id = b.id
JOIN public.clubs c
ON c.id = b.club_id
WHERE b.id = p_booking_id
AND b.club_id = p_club_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'Booking not found or not an academy booking.';
END IF;
IF v_booking.sport IS DISTINCT FROM v_sport THEN
RAISE EXCEPTION
'Booking belongs to a different sport.';
END IF;
IF NOT EXISTS (
SELECT 1
FROM public.academy_programs ap
WHERE ap.id = v_booking.academy_program_id
AND ap.club_id = p_club_id
AND ap.sport = v_sport
) THEN
RAISE EXCEPTION
'Academy program does not belong to this club and sport.';
END IF;
IF v_booking.deleted_at IS NOT NULL
OR v_booking.status IN (
'cancelled',
'deleted by club',
'deleted'
) THEN
RAISE EXCEPTION
'Booking is cancelled or deleted.';
END IF;
IF v_booking.academy_type NOT IN (
'group',
'camp'
) THEN
RAISE EXCEPTION
'Attendance is only available for group and camp bookings.';
END IF;
IF v_booking.booking_date IS NULL
OR v_booking.start_time IS NULL
OR v_booking.end_time IS NULL THEN
RAISE EXCEPTION
'Booking is missing date or time information.';
END IF;
-- Ellenőrzi, hogy a hívónak van-e a klubhoz tartozó
-- aktív edzőprofilja.
-- Nem választ ki önkényesen egyetlen user_profile rekordot.
SELECT EXISTS (
SELECT 1
FROM public.user_profiles up
JOIN public.coach_profiles cp
ON cp.user_profile_id = up.id
WHERE up.auth_user_id = auth.uid()
AND up.club_id = p_club_id
AND cp.is_active = true
)
INTO v_has_active_coach;
IF v_has_active_coach IS NOT TRUE THEN
RAISE EXCEPTION
'Unauthorized: Caller is not an active coach in this club.';
END IF;
-- Ellenőrzi, hogy a hívó bármely aktív edzőprofilja
-- ténylegesen be van-e checkolva erre a foglalásra.
SELECT EXISTS (
SELECT 1
FROM public.academy_booking_coach_checkins ci
JOIN public.coach_profiles cp
ON cp.id = ci.coach_profile_id
JOIN public.user_profiles up
ON up.id = cp.user_profile_id
WHERE ci.booking_id = p_booking_id
AND ci.club_id = p_club_id
AND ci.checked_out_at IS NULL
AND cp.is_active = true
AND up.auth_user_id = auth.uid()
AND up.club_id = p_club_id
)
INTO v_is_checked_in;
-- Klub helyi időzónája
v_timezone :=
COALESCE(
NULLIF(v_booking.timezone, ''),
'UTC'
);
v_local_now :=
(now() AT TIME ZONE v_timezone);
v_booking_start :=
(
v_booking.booking_date
+ v_booking.start_time
)::timestamp;
v_booking_end :=
(
v_booking.booking_date
+ v_booking.end_time
)::timestamp;
-- Jelenléti időablak
IF v_local_now <
v_booking_start - interval '15 minutes' THEN
v_window_status := 'too_early';
ELSIF v_local_now > v_booking_end THEN
v_window_status := 'closed';
ELSE
v_window_status := 'open';
END IF;
v_can_edit :=
v_is_checked_in
AND v_window_status = 'open';
-- Aktív résztvevők és elmentett jelenlétek
SELECT COALESCE(
jsonb_agg(
jsonb_build_object(
'participant_id',
p.id,
'first_name',
p.first_name,
'last_name',
p.last_name,
'attendance_status',
a.attendance_status,
'recorded_at',
a.recorded_at
)
ORDER BY
p.last_name ASC,
p.first_name ASC
),
'[]'::jsonb
)
INTO v_participants
FROM public.academy_program_participants p
LEFT JOIN public.academy_attendance a
ON a.participant_id = p.id
AND a.booking_id = p_booking_id
WHERE p.academy_program_id =
v_booking.academy_program_id
AND p.club_id = p_club_id
AND p.is_active = true;
RETURN jsonb_build_object(
'participants',
v_participants,
'window_status',
v_window_status,
'is_checked_in',
v_is_checked_in,
'can_edit',
v_can_edit
);
END;
$function$
"
public,get_academy_booking_cancel_preview,f,"p_booking_id uuid, p_sport text","p_booking_id uuid, p_sport text DEFAULT 'tennis'::text",jsonb,plpgsql,true,STABLE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.get_academy_booking_cancel_preview(p_booking_id uuid, p_sport text DEFAULT 'tennis'::text)
RETURNS jsonb
LANGUAGE plpgsql
STABLE SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_source_detail RECORD;
v_caller_profile RECORD;
v_club_id uuid;
v_batch_id uuid;
v_source_date date;
v_academy_type text;
v_program_id uuid;
v_booking_scope text;
v_affected_count integer := 0;
v_first_date date;
v_last_date date;
v_sport text;
BEGIN
v_sport := lower(
COALESCE(NULLIF(btrim(p_sport), ''), 'tennis')
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION 'Invalid sport. Allowed values: tennis, padel.';
END IF;
-- ============================================================
-- 1. Load the Academy booking detail for the source booking FIRST
-- to determine the club_id before resolving the caller profile.
-- ============================================================
SELECT
abd.booking_id,
abd.academy_booking_batch_id,
abd.academy_type,
abd.academy_program_id,
abd.occurrence_status,
b.club_id,
b.booking_date,
b.status AS booking_status,
b.deleted_at,
b.sport
INTO v_source_detail
FROM public.academy_booking_details abd
JOIN public.bookings b ON b.id = abd.booking_id
WHERE abd.booking_id = p_booking_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'This booking is not an Academy booking.';
END IF;
IF v_source_detail.sport IS DISTINCT FROM v_sport THEN
RAISE EXCEPTION 'This Academy booking belongs to a different sport.';
END IF;
v_club_id := v_source_detail.club_id;
v_batch_id := v_source_detail.academy_booking_batch_id;
v_source_date := v_source_detail.booking_date;
v_academy_type := v_source_detail.academy_type;
v_program_id := v_source_detail.academy_program_id;
IF v_batch_id IS NULL THEN
RAISE EXCEPTION 'This Academy booking does not belong to a batch.';
END IF;
-- ============================================================
-- 2. Resolve caller profile (club-specific, follows
-- cancel_academy_booking_occurrence semantics)
-- ============================================================
SELECT up.id, up.club_id, up.role
INTO v_caller_profile
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_club_id
ORDER BY up.id
LIMIT 1;
IF NOT FOUND THEN
RAISE EXCEPTION 'Caller profile not found in this club.';
END IF;
-- ============================================================
-- 3. Source booking status check
-- (occurrence_status, deleted_at, AND booking status)
-- ============================================================
IF v_source_detail.occurrence_status = 'cancelled'
OR v_source_detail.deleted_at IS NOT NULL
OR lower(COALESCE(v_source_detail.booking_status, '')) IN
('cancelled', 'deleted', 'deleted by club', 'failed')
THEN
RAISE EXCEPTION 'This booking has already been cancelled or deleted.';
END IF;
-- ============================================================
-- 4. Authorization: admin or coach with can_manage_academy
-- ============================================================
IF NOT public.can_manage_academy(v_club_id) THEN
RAISE EXCEPTION 'You do not have permission to manage Academy bookings.';
END IF;
-- ============================================================
-- 5. Get batch booking_scope (club-specific guard)
-- ============================================================
SELECT abb.booking_scope
INTO v_booking_scope
FROM public.academy_booking_batches abb
WHERE abb.id = v_batch_id
AND abb.club_id = v_club_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Academy batch not found in this club.';
END IF;
IF EXISTS (
SELECT 1
FROM public.academy_booking_details abd
LEFT JOIN public.bookings b ON b.id = abd.booking_id
WHERE abd.academy_booking_batch_id = v_batch_id
AND (
b.id IS NULL
OR b.club_id IS DISTINCT FROM v_club_id
OR b.sport IS DISTINCT FROM v_sport
)
) THEN
RAISE EXCEPTION 'Academy batch contains cross-club or cross-sport booking data.';
END IF;
IF v_booking_scope IS DISTINCT FROM 'season' THEN
RAISE EXCEPTION 'Remaining-season cancellation is only available for Academy season bookings.';
END IF;
-- ============================================================
-- 6. Count affected bookings using IDENTICAL WHERE conditions as
-- cancel_academy_booking_remaining
-- ============================================================
SELECT
COUNT(*),
MIN(b.booking_date),
MAX(b.booking_date)
INTO
v_affected_count,
v_first_date,
v_last_date
FROM public.academy_booking_details abd
JOIN public.bookings b ON b.id = abd.booking_id
WHERE abd.academy_booking_batch_id = v_batch_id
AND b.club_id = v_club_id
AND b.sport = v_sport
AND b.booking_date >= v_source_date
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, '')) NOT IN ('cancelled', 'deleted', 'deleted by club', 'failed')
AND lower(COALESCE(abd.occurrence_status, '')) <> 'cancelled';
-- ============================================================
-- 7. Zero affected count guard
-- ============================================================
IF v_affected_count <= 0 THEN
RAISE EXCEPTION 'No active Academy occurrences remain to cancel.';
END IF;
RETURN jsonb_build_object(
'success', true,
'academy_booking_batch_id', v_batch_id,
'booking_scope', v_booking_scope,
'academy_type', v_academy_type,
'academy_program_id', v_program_id,
'affected_count', v_affected_count,
'first_booking_date', to_char(v_first_date, 'YYYY-MM-DD'),
'last_booking_date', to_char(v_last_date, 'YYYY-MM-DD'),
'source_booking_date', to_char(v_source_date, 'YYYY-MM-DD')
);
END;
$function$
"
public,get_academy_camp_daily_payments,f,"p_club_id uuid, p_program_id uuid, p_sport text","p_club_id uuid, p_program_id uuid, p_sport text DEFAULT 'tennis'::text",SETOF academy_camp_daily_payments,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.get_academy_camp_daily_payments(p_club_id uuid, p_program_id uuid, p_sport text DEFAULT 'tennis'::text)
RETURNS SETOF academy_camp_daily_payments
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_sport text;
BEGIN
v_sport := lower(
COALESCE(NULLIF(btrim(p_sport), ''), 'tennis')
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION 'Érvénytelen sport.';
END IF;
IF NOT public.can_manage_academy_payments(p_club_id) THEN
RAISE EXCEPTION 'Unauthorized';
END IF;
IF NOT EXISTS (
SELECT 1
FROM public.academy_programs ap
WHERE ap.id = p_program_id
AND ap.club_id = p_club_id
AND ap.sport = v_sport
) THEN
RAISE EXCEPTION
'Academy program not found in club/sport.';
END IF;
RETURN QUERY
SELECT p.*
FROM public.academy_camp_daily_payments p
JOIN public.academy_program_participants part
ON part.id = p.participant_id
WHERE p.club_id = p_club_id
AND part.academy_program_id = p_program_id;
END;
$function$
"
public,get_academy_camp_weekly_payments,f,"p_club_id uuid, p_program_id uuid, p_sport text","p_club_id uuid, p_program_id uuid, p_sport text DEFAULT 'tennis'::text",SETOF academy_camp_weekly_payments,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.get_academy_camp_weekly_payments(p_club_id uuid, p_program_id uuid, p_sport text DEFAULT 'tennis'::text)
RETURNS SETOF academy_camp_weekly_payments
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_sport text;
BEGIN
v_sport := lower(
COALESCE(NULLIF(btrim(p_sport), ''), 'tennis')
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION 'Érvénytelen sport.';
END IF;
IF NOT public.can_manage_academy_payments(p_club_id) THEN
RAISE EXCEPTION 'Unauthorized';
END IF;
IF NOT EXISTS (
SELECT 1
FROM public.academy_programs ap
WHERE ap.id = p_program_id
AND ap.club_id = p_club_id
AND ap.sport = v_sport
) THEN
RAISE EXCEPTION
'Academy program not found in club/sport.';
END IF;
RETURN QUERY
SELECT p.*
FROM public.academy_camp_weekly_payments p
JOIN public.academy_program_participants part
ON part.id = p.participant_id
WHERE p.club_id = p_club_id
AND part.academy_program_id = p_program_id;
END;
$function$
"
public,get_academy_coach_compensation_audit_logs,f,"p_academy_program_id uuid, p_limit integer","p_academy_program_id uuid, p_limit integer DEFAULT 100",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.get_academy_coach_compensation_audit_logs(p_academy_program_id uuid, p_limit integer DEFAULT 100)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_club_id uuid;
v_limit integer;
v_result jsonb;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION
'A művelethez bejelentkezés szükséges.';
END IF;
SELECT ap.club_id
INTO v_club_id
FROM public.academy_programs ap
WHERE ap.id = p_academy_program_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'Az akadémiai program nem található.';
END IF;
IF NOT public.can_manage_academy_payments(v_club_id) THEN
RAISE EXCEPTION
'Nincs jogosultságod az auditnapló megtekintéséhez.';
END IF;
v_limit :=
LEAST(
GREATEST(
COALESCE(p_limit, 100),
1
),
500
);
SELECT COALESCE(
jsonb_agg(
log_row.payload
ORDER BY log_row.changed_at DESC
),
'[]'::jsonb
)
INTO v_result
FROM (
SELECT
l.changed_at,
jsonb_build_object(
'id', l.id,
'club_id', l.club_id,
'academy_program_id',
l.academy_program_id,
'coach_profile_id',
l.coach_profile_id,
'assignment_id',
l.assignment_id,
'compensation_rate_id',
l.compensation_rate_id,
'action_type',
l.action_type,
'old_values',
l.old_values,
'new_values',
l.new_values,
'changed_by_user_profile_id',
l.changed_by_user_profile_id,
'changed_by_name',
actor.full_name,
'changed_at',
l.changed_at
) AS payload
FROM public.academy_coach_compensation_audit_logs l
LEFT JOIN public.user_profiles actor
ON actor.id =
l.changed_by_user_profile_id
WHERE l.academy_program_id =
p_academy_program_id
ORDER BY l.changed_at DESC
LIMIT v_limit
) log_row;
RETURN v_result;
END;
$function$
"
public,get_academy_coach_payroll_report,f,"p_club_id uuid, p_date_from date, p_date_to date, p_coach_profile_id uuid, p_academy_program_id uuid","p_club_id uuid, p_date_from date, p_date_to date, p_coach_profile_id uuid DEFAULT NULL::uuid, p_academy_program_id uuid DEFAULT NULL::uuid",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.get_academy_coach_payroll_report(p_club_id uuid, p_date_from date, p_date_to date, p_coach_profile_id uuid DEFAULT NULL::uuid, p_academy_program_id uuid DEFAULT NULL::uuid)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_timezone text;
v_result jsonb;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION
'A művelethez bejelentkezés szükséges.';
END IF;
IF p_club_id IS NULL THEN
RAISE EXCEPTION
'A klub azonosítója kötelező.';
END IF;
IF p_date_from IS NULL
OR p_date_to IS NULL THEN
RAISE EXCEPTION
'A kezdő- és záródátum kötelező.';
END IF;
IF p_date_to < p_date_from THEN
RAISE EXCEPTION
'A záródátum nem lehet korábbi a kezdődátumnál.';
END IF;
IF p_date_to - p_date_from > 730 THEN
RAISE EXCEPTION
'Egy lekérdezés legfeljebb 731 napos időszakot tartalmazhat.';
END IF;
IF NOT public.can_manage_academy_payments(p_club_id) THEN
RAISE EXCEPTION
'Nincs jogosultságod az edzői fizetési riport megtekintéséhez.';
END IF;
SELECT
COALESCE(
NULLIF(c.timezone, ''),
'UTC'
)
INTO v_timezone
FROM public.clubs c
WHERE c.id = p_club_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'A klub nem található.';
END IF;
WITH raw_group_checkins AS (
SELECT
ci.id AS checkin_id,
ci.booking_id,
ci.coach_profile_id,
ci.checked_in_at,
ci.checked_out_at,
b.booking_date,
b.start_time,
b.end_time,
b.status AS booking_status,
b.deleted_at,
b.recurring_series_id,
abd.academy_program_id,
ap.name AS program_name,
COALESCE(
NULLIF(cp.display_name, ''),
NULLIF(up.full_name, ''),
NULLIF(cp.email, ''),
NULLIF(up.email, ''),
'Névtelen edző'
) AS coach_name,
COALESCE(
NULLIF(cp.email, ''),
NULLIF(up.email, '')
) AS coach_email,
(
(b.booking_date + b.start_time)
AT TIME ZONE v_timezone
) AS booking_start_at,
(
(b.booking_date + b.end_time)
AT TIME ZONE v_timezone
) AS booking_end_at
FROM public.academy_booking_coach_checkins ci
JOIN public.bookings b
ON b.id = ci.booking_id
AND b.club_id = p_club_id
JOIN public.academy_booking_details abd
ON abd.booking_id = b.id
AND abd.academy_type = 'group'
JOIN public.academy_programs ap
ON ap.id = abd.academy_program_id
AND ap.club_id = p_club_id
AND ap.program_type = 'group'
JOIN public.coach_profiles cp
ON cp.id = ci.coach_profile_id
LEFT JOIN public.user_profiles up
ON up.id = cp.user_profile_id
AND up.club_id = p_club_id
WHERE ci.club_id = p_club_id
AND b.booking_date
BETWEEN p_date_from AND p_date_to
AND (
p_coach_profile_id IS NULL
OR ci.coach_profile_id =
p_coach_profile_id
)
AND (
p_academy_program_id IS NULL
OR abd.academy_program_id =
p_academy_program_id
)
),
invalid_timing_checkins AS (
SELECT
rgc.*,
CASE
WHEN rgc.checked_in_at <
rgc.booking_start_at - INTERVAL '7 days'
THEN 'checked_in_too_early'
WHEN rgc.checked_in_at >
rgc.booking_end_at
THEN 'checked_in_after_booking_end'
ELSE 'invalid_timing'
END AS invalid_reason
FROM raw_group_checkins rgc
WHERE rgc.checked_in_at <
rgc.booking_start_at - INTERVAL '7 days'
OR rgc.checked_in_at >
rgc.booking_end_at
),
valid_candidate_checkins AS (
SELECT
rgc.*
FROM raw_group_checkins rgc
WHERE rgc.deleted_at IS NULL
AND rgc.booking_status = 'confirmed'
-- Csak már lezárult edzés számolható el.
AND rgc.booking_end_at <= now()
-- A check-in legkorábban hét nappal az edzés előtt lehet.
AND rgc.checked_in_at >=
rgc.booking_start_at - INTERVAL '7 days'
-- Az edzés vége után létrejött check-in érvénytelen.
AND rgc.checked_in_at <=
rgc.booking_end_at
-- Az edzés kezdete előtti check-out kizárja a fizetést.
-- A kezdéskor vagy utána történő check-out nem veszi el a díjat.
AND (
rgc.checked_out_at IS NULL
OR rgc.checked_out_at >=
rgc.booking_start_at
)
),
-- Egy edző ugyanazon foglalásért csak egyszer számolható el.
eligible_checkins AS (
SELECT DISTINCT ON (
booking_id,
coach_profile_id
)
*
FROM valid_candidate_checkins
ORDER BY
booking_id,
coach_profile_id,
checked_in_at ASC
),
resolved_sessions AS (
SELECT
ec.*,
assignment_match.id
AS assignment_id,
assignment_match.assigned_from,
assignment_match.assigned_to,
rate_match.id
AS compensation_rate_id,
rate_match.amount_huf,
rate_match.effective_from
AS rate_effective_from,
rate_match.effective_to
AS rate_effective_to,
CASE
WHEN assignment_match.id IS NULL
THEN 'unassigned'
WHEN rate_match.id IS NULL
THEN 'missing_rate'
ELSE 'payable'
END AS payroll_status
FROM eligible_checkins ec
LEFT JOIN LATERAL (
SELECT a.*
FROM public.academy_program_coach_assignments a
WHERE a.club_id = p_club_id
AND a.academy_program_id =
ec.academy_program_id
AND a.coach_profile_id =
ec.coach_profile_id
AND a.is_void = false
AND a.assigned_from <=
ec.booking_date
AND (
a.assigned_to IS NULL
OR a.assigned_to >=
ec.booking_date
)
ORDER BY a.assigned_from DESC
LIMIT 1
) assignment_match
ON true
LEFT JOIN LATERAL (
SELECT r.*
FROM public.academy_coach_compensation_rates r
WHERE r.club_id = p_club_id
AND r.assignment_id =
assignment_match.id
AND r.compensation_type =
'per_session'
AND r.is_void = false
AND r.effective_from <=
ec.booking_date
AND (
r.effective_to IS NULL
OR r.effective_to >=
ec.booking_date
)
ORDER BY r.effective_from DESC
LIMIT 1
) rate_match
ON true
),
coach_totals AS (
SELECT
coach_profile_id,
coach_name,
coach_email,
COUNT(*) AS eligible_session_count,
COUNT(*) FILTER (
WHERE payroll_status = 'payable'
) AS payable_session_count,
COUNT(*) FILTER (
WHERE payroll_status = 'unassigned'
) AS unassigned_session_count,
COUNT(*) FILTER (
WHERE payroll_status = 'missing_rate'
) AS missing_rate_session_count,
COALESCE(
SUM(amount_huf) FILTER (
WHERE payroll_status = 'payable'
),
0
) AS total_amount_huf
FROM resolved_sessions
GROUP BY
coach_profile_id,
coach_name,
coach_email
),
program_totals AS (
SELECT
academy_program_id,
program_name,
COUNT(*) AS eligible_session_count,
COUNT(*) FILTER (
WHERE payroll_status = 'payable'
) AS payable_session_count,
COUNT(*) FILTER (
WHERE payroll_status = 'unassigned'
) AS unassigned_session_count,
COUNT(*) FILTER (
WHERE payroll_status = 'missing_rate'
) AS missing_rate_session_count,
COALESCE(
SUM(amount_huf) FILTER (
WHERE payroll_status = 'payable'
),
0
) AS total_amount_huf
FROM resolved_sessions
GROUP BY
academy_program_id,
program_name
),
daily_totals AS (
SELECT
booking_date AS period_start,
COUNT(*) AS eligible_session_count,
COUNT(*) FILTER (
WHERE payroll_status = 'payable'
) AS payable_session_count,
COALESCE(
SUM(amount_huf) FILTER (
WHERE payroll_status = 'payable'
),
0
) AS total_amount_huf
FROM resolved_sessions
GROUP BY booking_date
),
weekly_totals AS (
SELECT
date_trunc(
'week',
booking_date::timestamp
)::date AS period_start,
COUNT(*) AS eligible_session_count,
COUNT(*) FILTER (
WHERE payroll_status = 'payable'
) AS payable_session_count,
COALESCE(
SUM(amount_huf) FILTER (
WHERE payroll_status = 'payable'
),
0
) AS total_amount_huf
FROM resolved_sessions
GROUP BY
date_trunc(
'week',
booking_date::timestamp
)::date
),
monthly_totals AS (
SELECT
date_trunc(
'month',
booking_date::timestamp
)::date AS period_start,
COUNT(*) AS eligible_session_count,
COUNT(*) FILTER (
WHERE payroll_status = 'payable'
) AS payable_session_count,
COALESCE(
SUM(amount_huf) FILTER (
WHERE payroll_status = 'payable'
),
0
) AS total_amount_huf
FROM resolved_sessions
GROUP BY
date_trunc(
'month',
booking_date::timestamp
)::date
)
SELECT jsonb_build_object(
'club_id',
p_club_id,
'date_from',
p_date_from,
'date_to',
p_date_to,
'timezone',
v_timezone,
'currency',
'HUF',
'filters',
jsonb_build_object(
'coach_profile_id',
p_coach_profile_id,
'academy_program_id',
p_academy_program_id
),
'summary',
jsonb_build_object(
'eligible_session_count',
(
SELECT COUNT(*)
FROM resolved_sessions
),
'payable_session_count',
(
SELECT COUNT(*)
FROM resolved_sessions
WHERE payroll_status = 'payable'
),
'unassigned_session_count',
(
SELECT COUNT(*)
FROM resolved_sessions
WHERE payroll_status = 'unassigned'
),
'missing_rate_session_count',
(
SELECT COUNT(*)
FROM resolved_sessions
WHERE payroll_status = 'missing_rate'
),
'invalid_checkin_timing_count',
(
SELECT COUNT(*)
FROM invalid_timing_checkins
),
'total_amount_huf',
COALESCE(
(
SELECT SUM(amount_huf)
FROM resolved_sessions
WHERE payroll_status = 'payable'
),
0
)
),
'data_quality_issues',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'checkin_id',
itc.checkin_id,
'booking_id',
itc.booking_id,
'booking_date',
itc.booking_date,
'start_time',
itc.start_time,
'end_time',
itc.end_time,
'academy_program_id',
itc.academy_program_id,
'program_name',
itc.program_name,
'coach_profile_id',
itc.coach_profile_id,
'coach_name',
itc.coach_name,
'checked_in_at',
itc.checked_in_at,
'booking_start_at',
itc.booking_start_at,
'booking_end_at',
itc.booking_end_at,
'invalid_reason',
itc.invalid_reason
)
ORDER BY
itc.booking_date,
itc.start_time
)
FROM invalid_timing_checkins itc
),
'[]'::jsonb
),
'by_coach',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'coach_profile_id',
ct.coach_profile_id,
'coach_name',
ct.coach_name,
'coach_email',
ct.coach_email,
'eligible_session_count',
ct.eligible_session_count,
'payable_session_count',
ct.payable_session_count,
'unassigned_session_count',
ct.unassigned_session_count,
'missing_rate_session_count',
ct.missing_rate_session_count,
'total_amount_huf',
ct.total_amount_huf
)
ORDER BY ct.coach_name
)
FROM coach_totals ct
),
'[]'::jsonb
),
'by_program',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'academy_program_id',
pt.academy_program_id,
'program_name',
pt.program_name,
'eligible_session_count',
pt.eligible_session_count,
'payable_session_count',
pt.payable_session_count,
'unassigned_session_count',
pt.unassigned_session_count,
'missing_rate_session_count',
pt.missing_rate_session_count,
'total_amount_huf',
pt.total_amount_huf
)
ORDER BY pt.program_name
)
FROM program_totals pt
),
'[]'::jsonb
),
'daily_totals',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'date',
dt.period_start,
'eligible_session_count',
dt.eligible_session_count,
'payable_session_count',
dt.payable_session_count,
'total_amount_huf',
dt.total_amount_huf
)
ORDER BY dt.period_start
)
FROM daily_totals dt
),
'[]'::jsonb
),
'weekly_totals',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'week_start',
wt.period_start,
'eligible_session_count',
wt.eligible_session_count,
'payable_session_count',
wt.payable_session_count,
'total_amount_huf',
wt.total_amount_huf
)
ORDER BY wt.period_start
)
FROM weekly_totals wt
),
'[]'::jsonb
),
'monthly_totals',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'month_start',
mt.period_start,
'eligible_session_count',
mt.eligible_session_count,
'payable_session_count',
mt.payable_session_count,
'total_amount_huf',
mt.total_amount_huf
)
ORDER BY mt.period_start
)
FROM monthly_totals mt
),
'[]'::jsonb
),
'sessions',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'checkin_id',
rs.checkin_id,
'booking_id',
rs.booking_id,
'booking_date',
rs.booking_date,
'start_time',
rs.start_time,
'end_time',
rs.end_time,
'booking_start_at',
rs.booking_start_at,
'booking_end_at',
rs.booking_end_at,
'recurring_series_id',
rs.recurring_series_id,
'academy_program_id',
rs.academy_program_id,
'program_name',
rs.program_name,
'coach_profile_id',
rs.coach_profile_id,
'coach_name',
rs.coach_name,
'coach_email',
rs.coach_email,
'checked_in_at',
rs.checked_in_at,
'checked_out_at',
rs.checked_out_at,
'assignment_id',
rs.assignment_id,
'assigned_from',
rs.assigned_from,
'assigned_to',
rs.assigned_to,
'compensation_rate_id',
rs.compensation_rate_id,
'amount_huf',
rs.amount_huf,
'rate_effective_from',
rs.rate_effective_from,
'rate_effective_to',
rs.rate_effective_to,
'payroll_status',
rs.payroll_status
)
ORDER BY
rs.booking_date,
rs.start_time,
rs.coach_name
)
FROM resolved_sessions rs
),
'[]'::jsonb
)
)
INTO v_result;
RETURN v_result;
END;
$function$
"
public,get_academy_court_cost_report,f,"p_academy_program_id uuid, p_club_id uuid, p_date_from date, p_date_to date","p_academy_program_id uuid, p_club_id uuid, p_date_from date, p_date_to date",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.get_academy_court_cost_report(p_academy_program_id uuid, p_club_id uuid, p_date_from date, p_date_to date)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_program_name text;
v_program_type text;
v_timezone text;
v_default_fee numeric(12, 2);
v_result jsonb;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION
'A művelethez bejelentkezés szükséges.';
END IF;
IF p_club_id IS NULL
OR p_academy_program_id IS NULL THEN
RAISE EXCEPTION
'A klub és a csoport megadása kötelező.';
END IF;
IF p_date_from IS NULL
OR p_date_to IS NULL THEN
RAISE EXCEPTION
'A kezdő- és záródátum kötelező.';
END IF;
IF p_date_to < p_date_from THEN
RAISE EXCEPTION
'A záródátum nem lehet korábbi a kezdődátumnál.';
END IF;
IF p_date_to - p_date_from > 730 THEN
RAISE EXCEPTION
'Egy riport legfeljebb 731 napos időszakot tartalmazhat.';
END IF;
IF NOT public.can_manage_academy_payments(p_club_id) THEN
RAISE EXCEPTION
'Nincs jogosultságod a pályaköltség-riport megtekintéséhez.';
END IF;
SELECT
ap.name,
ap.program_type,
COALESCE(NULLIF(c.timezone, ''), 'UTC')
INTO
v_program_name,
v_program_type,
v_timezone
FROM public.academy_programs ap
JOIN public.clubs c
ON c.id = ap.club_id
WHERE ap.id = p_academy_program_id
AND ap.club_id = p_club_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'A csoport nem található vagy másik klubhoz tartozik.';
END IF;
IF v_program_type <> 'group' THEN
RAISE EXCEPTION
'Pályaköltség-riport csak csoportos programhoz kérhető.';
END IF;
SELECT s.default_court_fee_huf
INTO v_default_fee
FROM public.academy_group_court_cost_settings s
WHERE s.academy_program_id = p_academy_program_id
AND s.club_id = p_club_id;
v_default_fee := COALESCE(v_default_fee, 0);
WITH valid_sessions AS (
SELECT
b.id AS booking_id,
b.booking_date,
b.start_time,
b.end_time,
b.court_id,
b.status AS original_booking_status,
abd.occurrence_status,
abd.court_fee_huf,
(
(b.booking_date + b.start_time)
AT TIME ZONE v_timezone
) AS booking_start_at,
(
(b.booking_date + b.end_time)
AT TIME ZONE v_timezone
) AS booking_end_at,
CASE
WHEN (
(b.booking_date + b.end_time)
AT TIME ZONE v_timezone
) <= now()
THEN 'past'
ELSE 'future'
END AS booking_status
FROM public.academy_booking_details abd
JOIN public.bookings b
ON b.id = abd.booking_id
AND b.club_id = p_club_id
WHERE abd.club_id = p_club_id
AND abd.academy_program_id =
p_academy_program_id
AND abd.academy_type = 'group'
AND b.booking_date
BETWEEN p_date_from AND p_date_to
AND abd.cancelled_at IS NULL
AND lower(
COALESCE(
abd.occurrence_status,
'scheduled'
)
) NOT IN (
'cancelled',
'deleted',
'failed'
)
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, ''))
NOT IN (
'cancelled',
'deleted',
'deleted by club',
'failed'
)
),
daily_totals AS (
SELECT
booking_date AS period_start,
COUNT(*) AS session_count,
COUNT(*) FILTER (
WHERE booking_status = 'past'
) AS past_session_count,
COUNT(*) FILTER (
WHERE booking_status = 'future'
) AS future_session_count,
COALESCE(
SUM(court_fee_huf),
0
) AS total_amount_huf,
COALESCE(
SUM(court_fee_huf) FILTER (
WHERE booking_status = 'past'
),
0
) AS past_amount_huf,
COALESCE(
SUM(court_fee_huf) FILTER (
WHERE booking_status = 'future'
),
0
) AS future_amount_huf
FROM valid_sessions
GROUP BY booking_date
),
weekly_totals AS (
SELECT
date_trunc(
'week',
booking_date::timestamp
)::date AS period_start,
COUNT(*) AS session_count,
COUNT(*) FILTER (
WHERE booking_status = 'past'
) AS past_session_count,
COUNT(*) FILTER (
WHERE booking_status = 'future'
) AS future_session_count,
COALESCE(
SUM(court_fee_huf),
0
) AS total_amount_huf,
COALESCE(
SUM(court_fee_huf) FILTER (
WHERE booking_status = 'past'
),
0
) AS past_amount_huf,
COALESCE(
SUM(court_fee_huf) FILTER (
WHERE booking_status = 'future'
),
0
) AS future_amount_huf
FROM valid_sessions
GROUP BY
date_trunc(
'week',
booking_date::timestamp
)::date
),
monthly_totals AS (
SELECT
date_trunc(
'month',
booking_date::timestamp
)::date AS period_start,
COUNT(*) AS session_count,
COUNT(*) FILTER (
WHERE booking_status = 'past'
) AS past_session_count,
COUNT(*) FILTER (
WHERE booking_status = 'future'
) AS future_session_count,
COALESCE(
SUM(court_fee_huf),
0
) AS total_amount_huf,
COALESCE(
SUM(court_fee_huf) FILTER (
WHERE booking_status = 'past'
),
0
) AS past_amount_huf,
COALESCE(
SUM(court_fee_huf) FILTER (
WHERE booking_status = 'future'
),
0
) AS future_amount_huf
FROM valid_sessions
GROUP BY
date_trunc(
'month',
booking_date::timestamp
)::date
)
SELECT jsonb_build_object(
'club_id',
p_club_id,
'academy_program_id',
p_academy_program_id,
'program_name',
v_program_name,
'date_from',
p_date_from,
'date_to',
p_date_to,
'timezone',
v_timezone,
'currency',
'HUF',
'default_court_fee_huf',
v_default_fee,
'summary',
jsonb_build_object(
'total_session_count',
(
SELECT COUNT(*)
FROM valid_sessions
),
'past_session_count',
(
SELECT COUNT(*)
FROM valid_sessions
WHERE booking_status = 'past'
),
'future_session_count',
(
SELECT COUNT(*)
FROM valid_sessions
WHERE booking_status = 'future'
),
'total_court_cost_huf',
COALESCE(
(
SELECT SUM(court_fee_huf)
FROM valid_sessions
),
0
),
'past_court_cost_huf',
COALESCE(
(
SELECT SUM(court_fee_huf)
FROM valid_sessions
WHERE booking_status = 'past'
),
0
),
'future_court_cost_huf',
COALESCE(
(
SELECT SUM(court_fee_huf)
FROM valid_sessions
WHERE booking_status = 'future'
),
0
)
),
'sessions',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'booking_id',
vs.booking_id,
'booking_date',
vs.booking_date,
'start_time',
vs.start_time,
'end_time',
vs.end_time,
'booking_start_at',
vs.booking_start_at,
'booking_end_at',
vs.booking_end_at,
'court_id',
vs.court_id,
'program_name',
v_program_name,
'court_fee_huf',
vs.court_fee_huf,
'booking_status',
vs.booking_status,
'timing_status',
vs.booking_status,
'original_booking_status',
vs.original_booking_status,
'occurrence_status',
vs.occurrence_status
)
ORDER BY
vs.booking_date,
vs.start_time
)
FROM valid_sessions vs
),
'[]'::jsonb
),
'daily_totals',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'period_label',
to_char(
dt.period_start,
'YYYY-MM-DD'
),
'period_start',
dt.period_start,
'session_count',
dt.session_count,
'past_session_count',
dt.past_session_count,
'future_session_count',
dt.future_session_count,
'total_amount_huf',
dt.total_amount_huf,
'past_amount_huf',
dt.past_amount_huf,
'future_amount_huf',
dt.future_amount_huf
)
ORDER BY dt.period_start
)
FROM daily_totals dt
),
'[]'::jsonb
),
'weekly_totals',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'period_label',
to_char(
wt.period_start,
'YYYY-MM-DD'
),
'period_start',
wt.period_start,
'session_count',
wt.session_count,
'past_session_count',
wt.past_session_count,
'future_session_count',
wt.future_session_count,
'total_amount_huf',
wt.total_amount_huf,
'past_amount_huf',
wt.past_amount_huf,
'future_amount_huf',
wt.future_amount_huf
)
ORDER BY wt.period_start
)
FROM weekly_totals wt
),
'[]'::jsonb
),
'monthly_totals',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'period_label',
to_char(
mt.period_start,
'YYYY-MM'
),
'period_start',
mt.period_start,
'session_count',
mt.session_count,
'past_session_count',
mt.past_session_count,
'future_session_count',
mt.future_session_count,
'total_amount_huf',
mt.total_amount_huf,
'past_amount_huf',
mt.past_amount_huf,
'future_amount_huf',
mt.future_amount_huf
)
ORDER BY mt.period_start
)
FROM monthly_totals mt
),
'[]'::jsonb
)
)
INTO v_result;
RETURN v_result;
END;
$function$
"
public,get_academy_group_court_cost_setting,f,p_academy_program_id uuid,p_academy_program_id uuid,jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.get_academy_group_court_cost_setting(p_academy_program_id uuid)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_club_id uuid;
v_program_name text;
v_program_type text;
v_timezone text;
v_default_fee numeric(12, 2);
v_future_occurrence_count integer;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION
'A művelethez bejelentkezés szükséges.';
END IF;
SELECT
ap.club_id,
ap.name,
ap.program_type,
COALESCE(NULLIF(c.timezone, ''), 'UTC')
INTO
v_club_id,
v_program_name,
v_program_type,
v_timezone
FROM public.academy_programs ap
JOIN public.clubs c
ON c.id = ap.club_id
WHERE ap.id = p_academy_program_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'Az akadémiai csoport nem található.';
END IF;
IF v_program_type <> 'group' THEN
RAISE EXCEPTION
'Pályaköltség csak csoportos programhoz kérhető le.';
END IF;
IF NOT public.can_manage_academy_payments(v_club_id) THEN
RAISE EXCEPTION
'Nincs jogosultságod a pályaköltség megtekintéséhez.';
END IF;
SELECT s.default_court_fee_huf
INTO v_default_fee
FROM public.academy_group_court_cost_settings s
WHERE s.academy_program_id = p_academy_program_id
AND s.club_id = v_club_id;
v_default_fee := COALESCE(v_default_fee, 0);
SELECT COUNT(*)
INTO v_future_occurrence_count
FROM public.academy_booking_details abd
JOIN public.bookings b
ON b.id = abd.booking_id
WHERE abd.club_id = v_club_id
AND abd.academy_program_id = p_academy_program_id
AND abd.academy_type = 'group'
AND abd.cancelled_at IS NULL
AND lower(
COALESCE(
abd.occurrence_status,
'scheduled'
)
) NOT IN (
'cancelled',
'deleted',
'failed'
)
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, ''))
NOT IN (
'cancelled',
'deleted',
'deleted by club',
'failed'
)
AND (
(b.booking_date + b.start_time)
AT TIME ZONE v_timezone
) > now();
RETURN jsonb_build_object(
'club_id',
v_club_id,
'academy_program_id',
p_academy_program_id,
'program_name',
v_program_name,
'default_court_fee_huf',
v_default_fee,
'currency',
'HUF',
'future_occurrence_count',
v_future_occurrence_count
);
END;
$function$
"
public,get_academy_group_monthly_payments,f,"p_club_id uuid, p_program_id uuid, p_year integer, p_month integer, p_sport text","p_club_id uuid, p_program_id uuid, p_year integer, p_month integer, p_sport text DEFAULT 'tennis'::text",SETOF academy_group_monthly_payments,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.get_academy_group_monthly_payments(p_club_id uuid, p_program_id uuid, p_year integer, p_month integer, p_sport text DEFAULT 'tennis'::text)
RETURNS SETOF academy_group_monthly_payments
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_sport text;
BEGIN
v_sport := lower(
COALESCE(NULLIF(btrim(p_sport), ''), 'tennis')
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION 'Érvénytelen sport.';
END IF;
IF NOT public.can_manage_academy_payments(p_club_id) THEN
RAISE EXCEPTION 'Unauthorized';
END IF;
IF NOT EXISTS (
SELECT 1
FROM public.academy_programs ap
WHERE ap.id = p_program_id
AND ap.club_id = p_club_id
AND ap.sport = v_sport
) THEN
RAISE EXCEPTION
'Academy program not found in club/sport.';
END IF;
RETURN QUERY
SELECT p.*
FROM public.academy_group_monthly_payments p
JOIN public.academy_program_participants part
ON part.id = p.participant_id
WHERE p.club_id = p_club_id
AND part.academy_program_id = p_program_id
AND p.year = p_year
AND p.month = p_month;
END;
$function$
"
public,get_academy_program_coach_settings,f,p_academy_program_id uuid,p_academy_program_id uuid,jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.get_academy_program_coach_settings(p_academy_program_id uuid)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_club_id uuid;
v_program_name text;
v_program_type text;
v_timezone text;
v_today date;
v_result jsonb;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION
'A művelethez bejelentkezés szükséges.';
END IF;
SELECT
ap.club_id,
ap.name,
ap.program_type,
COALESCE(NULLIF(c.timezone, ''), 'UTC')
INTO
v_club_id,
v_program_name,
v_program_type,
v_timezone
FROM public.academy_programs ap
JOIN public.clubs c
ON c.id = ap.club_id
WHERE ap.id = p_academy_program_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'Az akadémiai program nem található.';
END IF;
IF v_program_type <> 'group' THEN
RAISE EXCEPTION
'Edzői díjazás jelenleg csak csoportos programhoz kérhető le.';
END IF;
IF NOT public.can_manage_academy_payments(v_club_id) THEN
RAISE EXCEPTION
'Nincs jogosultságod az edzői díjazás megtekintéséhez.';
END IF;
v_today := (now() AT TIME ZONE v_timezone)::date;
SELECT jsonb_build_object(
'club_id', v_club_id,
'academy_program_id', p_academy_program_id,
'program_name', v_program_name,
'timezone', v_timezone,
'today', v_today,
'coaches',
COALESCE(
jsonb_agg(
coach_data.payload
ORDER BY coach_data.sort_name
),
'[]'::jsonb
)
)
INTO v_result
FROM (
SELECT
COALESCE(
NULLIF(cp.display_name, ''),
NULLIF(up.full_name, ''),
NULLIF(cp.email, ''),
NULLIF(up.email, ''),
'Névtelen edző'
) AS sort_name,
jsonb_build_object(
'coach_profile_id', cp.id,
'user_profile_id', cp.user_profile_id,
'display_name',
COALESCE(
NULLIF(cp.display_name, ''),
NULLIF(up.full_name, ''),
NULLIF(cp.email, ''),
NULLIF(up.email, ''),
'Névtelen edző'
),
'email',
COALESCE(
NULLIF(cp.email, ''),
NULLIF(up.email, '')
),
'color', cp.color,
'coach_is_active', cp.is_active,
'is_assigned',
current_assignment.id IS NOT NULL,
'assignment_id',
current_assignment.id,
'assigned_from',
current_assignment.assigned_from,
'assigned_to',
current_assignment.assigned_to,
'current_rate_id',
current_rate.id,
'current_amount_huf',
current_rate.amount_huf,
'current_rate_effective_from',
current_rate.effective_from,
'current_rate_effective_to',
current_rate.effective_to,
'next_rate_id',
next_rate.id,
'next_amount_huf',
next_rate.amount_huf,
'next_rate_effective_from',
next_rate.effective_from,
'next_rate_effective_to',
next_rate.effective_to,
'assignment_history',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'assignment_id', ah.id,
'assigned_from', ah.assigned_from,
'assigned_to', ah.assigned_to,
'is_void', ah.is_void,
'created_at', ah.created_at,
'updated_at', ah.updated_at
)
ORDER BY ah.assigned_from DESC
)
FROM public.academy_program_coach_assignments ah
WHERE ah.academy_program_id =
p_academy_program_id
AND ah.coach_profile_id = cp.id
),
'[]'::jsonb
),
'rate_history',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'rate_id', rh.id,
'assignment_id', rah.id,
'amount_huf', rh.amount_huf,
'compensation_type',
rh.compensation_type,
'effective_from',
rh.effective_from,
'effective_to',
rh.effective_to,
'is_void',
rh.is_void,
'created_at',
rh.created_at,
'updated_at',
rh.updated_at
)
ORDER BY
rah.assigned_from DESC,
rh.effective_from DESC
)
FROM public.academy_program_coach_assignments rah
JOIN public.academy_coach_compensation_rates rh
ON rh.assignment_id = rah.id
WHERE rah.academy_program_id =
p_academy_program_id
AND rah.coach_profile_id = cp.id
),
'[]'::jsonb
)
) AS payload
FROM public.coach_profiles cp
JOIN public.user_profiles up
ON up.id = cp.user_profile_id
AND up.club_id = v_club_id
LEFT JOIN LATERAL (
SELECT a.*
FROM public.academy_program_coach_assignments a
WHERE a.academy_program_id =
p_academy_program_id
AND a.coach_profile_id = cp.id
AND a.is_void = false
AND a.assigned_from <= v_today
AND (
a.assigned_to IS NULL
OR a.assigned_to >= v_today
)
ORDER BY a.assigned_from DESC
LIMIT 1
) current_assignment
ON true
LEFT JOIN LATERAL (
SELECT r.*
FROM public.academy_coach_compensation_rates r
WHERE r.assignment_id =
current_assignment.id
AND r.is_void = false
AND r.effective_from <= v_today
AND (
r.effective_to IS NULL
OR r.effective_to >= v_today
)
ORDER BY r.effective_from DESC
LIMIT 1
) current_rate
ON true
LEFT JOIN LATERAL (
SELECT r.*
FROM public.academy_coach_compensation_rates r
WHERE r.assignment_id =
current_assignment.id
AND r.is_void = false
AND r.effective_from > v_today
ORDER BY r.effective_from ASC
LIMIT 1
) next_rate
ON true
WHERE cp.is_active = true
) coach_data;
RETURN v_result;
END;
$function$
"
public,get_admin_booking_report,f,"p_club_id uuid, p_date_from date, p_date_to date, p_user_profile_id uuid, p_court_id uuid, p_status_filter text, p_timing_filter text, p_limit integer, p_academy_program_id uuid, p_sport text","p_club_id uuid, p_date_from date, p_date_to date, p_user_profile_id uuid DEFAULT NULL::uuid, p_court_id uuid DEFAULT NULL::uuid, p_status_filter text DEFAULT 'all'::text, p_timing_filter text DEFAULT 'all'::text, p_limit integer DEFAULT 10000, p_academy_program_id uuid DEFAULT NULL::uuid, p_sport text DEFAULT 'tennis'::text",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.get_admin_booking_report(p_club_id uuid, p_date_from date, p_date_to date, p_user_profile_id uuid DEFAULT NULL::uuid, p_court_id uuid DEFAULT NULL::uuid, p_status_filter text DEFAULT 'all'::text, p_timing_filter text DEFAULT 'all'::text, p_limit integer DEFAULT 10000, p_academy_program_id uuid DEFAULT NULL::uuid, p_sport text DEFAULT 'tennis'::text)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_timezone_original text;
v_timezone text;
v_timezone_fallback boolean := false;
v_status_filter text;
v_timing_filter text;
v_effective_limit integer;
v_sport text;
v_result jsonb;
BEGIN
-- ========================================================
-- TENNIS SAFETY / AUTH
-- ========================================================
IF auth.uid() IS NULL THEN
RAISE EXCEPTION
'A riport lekéréséhez bejelentkezés szükséges.';
END IF;
IF p_club_id IS NULL THEN
RAISE EXCEPTION
'A klubazonosító kötelező.';
END IF;
IF NOT public.can_view_club_reports(p_club_id) THEN
RAISE EXCEPTION
'Nincs jogosultságod a klub riportjainak megtekintéséhez.';
END IF;
-- ========================================================
-- SPORT VALIDÁCIÓ
--
-- Régi hívás:
-- nincs p_sport -> tennis
--
-- Új Padel hívás:
-- p_sport = 'padel'
-- ========================================================
v_sport :=
lower(
COALESCE(
NULLIF(btrim(p_sport), ''),
'tennis'
)
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION
'Érvénytelen sport. Engedélyezett értékek: tennis, padel.';
END IF;
-- ========================================================
-- Klub és időzóna
-- ========================================================
SELECT c.timezone
INTO v_timezone_original
FROM public.clubs c
WHERE c.id = p_club_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'A klub nem található.';
END IF;
IF NULLIF(btrim(v_timezone_original), '') IS NULL THEN
v_timezone := 'Europe/Budapest';
v_timezone_fallback := true;
ELSIF NOT EXISTS (
SELECT 1
FROM pg_timezone_names t
WHERE t.name = v_timezone_original
) THEN
v_timezone := 'Europe/Budapest';
v_timezone_fallback := true;
ELSE
v_timezone := v_timezone_original;
END IF;
-- ========================================================
-- Dátumtartomány
-- ========================================================
IF p_date_from IS NULL
OR p_date_to IS NULL THEN
RAISE EXCEPTION
'A kezdő- és záródátum kötelező.';
END IF;
IF p_date_to < p_date_from THEN
RAISE EXCEPTION
'A záródátum nem lehet korábbi a kezdődátumnál.';
END IF;
IF p_date_to - p_date_from > 3660 THEN
RAISE EXCEPTION
'Egy riport legfeljebb tízéves időszakot tartalmazhat.';
END IF;
-- ========================================================
-- Szűrők
-- ========================================================
v_status_filter :=
lower(
COALESCE(
NULLIF(btrim(p_status_filter), ''),
'all'
)
);
v_timing_filter :=
lower(
COALESCE(
NULLIF(btrim(p_timing_filter), ''),
'all'
)
);
IF v_status_filter NOT IN (
'all',
'valid',
'cancelled',
'deleted',
'deleted_by_club',
'failed'
) THEN
RAISE EXCEPTION
'Érvénytelen foglalási státuszszűrő.';
END IF;
IF v_timing_filter NOT IN (
'all',
'past',
'future'
) THEN
RAISE EXCEPTION
'Érvénytelen időbeli szűrő.';
END IF;
v_effective_limit :=
LEAST(
GREATEST(
COALESCE(p_limit, 10000),
1
),
10000
);
-- ========================================================
-- User filter validation
--
-- A user account közös lehet Tennis/Padel között,
-- ezért itt nincs sport filter.
-- ========================================================
IF p_user_profile_id IS NOT NULL
AND NOT EXISTS (
SELECT 1
FROM public.user_profiles up
WHERE up.id = p_user_profile_id
AND up.club_id = p_club_id
) THEN
RAISE EXCEPTION
'A kiválasztott felhasználó nem található ebben a klubban.';
END IF;
-- ========================================================
-- COURT SPORT SAFETY
--
-- Tennis riporthoz Padel court ID nem adható.
-- Padel riporthoz Tennis court ID nem adható.
-- ========================================================
IF p_court_id IS NOT NULL
AND NOT EXISTS (
SELECT 1
FROM public.courts ct
WHERE ct.id = p_court_id
AND ct.club_id = p_club_id
AND ct.sport = v_sport
) THEN
RAISE EXCEPTION
'A kiválasztott pálya nem található ebben a klubban és sportban.';
END IF;
-- ========================================================
-- ACADEMY PROGRAM SPORT SAFETY
-- ========================================================
IF p_academy_program_id IS NOT NULL THEN
IF NOT EXISTS (
SELECT 1
FROM public.academy_programs ap
WHERE ap.id = p_academy_program_id
AND ap.club_id = p_club_id
AND ap.sport = v_sport
AND ap.program_type IN ('group', 'camp')
) THEN
RAISE EXCEPTION
'A kiválasztott akadémiai program nem tartozik ehhez a klubhoz és sporthoz.';
END IF;
END IF;
-- ========================================================
-- RIPORT
-- ========================================================
WITH base_rows AS (
SELECT
b.id AS booking_id,
b.club_id,
b.season_id,
b.court_id,
COALESCE(
NULLIF(to_jsonb(ct) ->> 'name', ''),
NULLIF(to_jsonb(ct) ->> 'court_name', ''),
NULLIF(to_jsonb(ct) ->> 'label', ''),
b.court_id::text,
'Nincs pálya'
) AS court_name,
b.booking_date,
b.start_time,
b.end_time,
round(
(
extract(
epoch
FROM (b.end_time - b.start_time)
) / 3600
)::numeric,
2
) AS duration_hours,
(
(b.booking_date + b.start_time)
AT TIME ZONE v_timezone
) AS booking_start_at,
(
(b.booking_date + b.end_time)
AT TIME ZONE v_timezone
) AS booking_end_at,
CASE
WHEN (
(b.booking_date + b.end_time)
AT TIME ZONE v_timezone
) <= now()
THEN 'past'
ELSE 'future'
END AS timing_status,
CASE
WHEN lower(COALESCE(b.status, '')) = 'cancelled'
THEN 'cancelled'
WHEN lower(COALESCE(b.status, '')) = 'deleted by club'
THEN 'deleted_by_club'
WHEN lower(COALESCE(b.status, '')) = 'deleted'
THEN 'deleted'
WHEN lower(COALESCE(b.status, '')) = 'failed'
THEN 'failed'
WHEN b.deleted_at IS NOT NULL
THEN 'deleted'
ELSE 'valid'
END AS report_status,
b.status AS raw_booking_status,
b.deleted_at,
b.cancelled_by_profile_id,
b.cancelled_by_name,
b.cancelled_by_role,
b.deleted_at AS cancelled_at,
b.cancellation_reason_category,
b.cancellation_reason,
b.booking_type,
b.recurring_series_id,
b.booking_name,
b.user_id AS booking_owner_user_profile_id,
COALESCE(
NULLIF(owner_profile.full_name, ''),
NULLIF(b.booking_name, ''),
'Ismeretlen'
) AS booking_owner_name,
COALESCE(
NULLIF(owner_profile.email, ''),
NULLIF(b.email, '')
) AS booking_owner_email,
COALESCE(
NULLIF(owner_profile.phone, ''),
NULLIF(b.phone, '')
) AS booking_owner_phone,
b.created_by_user_id,
creator_profile.full_name AS created_by_name,
b.created_by_role,
abd.academy_type,
abd.academy_program_id,
ap.name AS academy_program_name,
CASE
WHEN abd.academy_type = 'individual'
THEN 'academy_individual'
WHEN abd.academy_type = 'group'
THEN 'academy_group'
WHEN abd.academy_type = 'camp'
THEN 'academy_camp'
ELSE 'standard'
END AS booking_category,
COALESCE(
bp.payment_method,
b.payment_method
) AS payment_method,
CASE
WHEN abd.academy_type IS NOT NULL THEN
CASE
WHEN abd.court_fee_huf > 0
AND lower(COALESCE(b.status, '')) NOT IN (
'cancelled',
'deleted',
'deleted by club',
'failed'
)
AND b.deleted_at IS NULL
THEN 'paid'
ELSE 'pending'
END
ELSE COALESCE(bp.payment_status, b.payment_status)
END AS payment_status,
CASE
WHEN abd.academy_type IS NOT NULL
THEN abd.court_fee_huf
ELSE bp.amount
END AS payment_amount,
COALESCE(
bp.currency,
b.currency,
'HUF'
) AS currency,
CASE
WHEN abd.academy_type IS NOT NULL
THEN COALESCE(abd.court_fee_huf, b.price_per_booking)
ELSE b.price_per_booking
END AS price_per_booking,
b.booking_note,
b.created_at
FROM public.bookings b
LEFT JOIN public.courts ct
ON ct.id = b.court_id
AND ct.club_id = p_club_id
AND ct.sport = v_sport
LEFT JOIN public.user_profiles owner_profile
ON owner_profile.id = b.user_id
AND owner_profile.club_id = p_club_id
LEFT JOIN public.user_profiles creator_profile
ON creator_profile.id = b.created_by_user_id
AND creator_profile.club_id = p_club_id
LEFT JOIN public.academy_booking_details abd
ON abd.booking_id = b.id
AND abd.club_id = p_club_id
LEFT JOIN public.academy_programs ap
ON ap.id = abd.academy_program_id
AND ap.club_id = p_club_id
AND ap.sport = v_sport
LEFT JOIN public.booking_payments bp
ON bp.booking_id = b.id
AND bp.club_id = p_club_id
WHERE b.club_id = p_club_id
-- ==================================================
-- CRITICAL SPORT ISOLATION
-- ==================================================
AND b.sport = v_sport
AND b.booking_date
BETWEEN p_date_from AND p_date_to
AND (
p_user_profile_id IS NULL
OR b.user_id = p_user_profile_id
)
AND (
p_court_id IS NULL
OR b.court_id = p_court_id
)
AND (
p_academy_program_id IS NULL
OR abd.academy_program_id = p_academy_program_id
)
),
filtered_rows AS (
SELECT br.*
FROM base_rows br
WHERE (
v_status_filter = 'all'
OR br.report_status = v_status_filter
)
AND (
v_timing_filter = 'all'
OR br.timing_status = v_timing_filter
)
),
report_stats AS (
SELECT
COUNT(*) AS total_booking_count,
COUNT(*) FILTER (
WHERE report_status = 'valid'
) AS valid_booking_count,
COUNT(*) FILTER (
WHERE report_status = 'cancelled'
) AS cancelled_booking_count,
COUNT(*) FILTER (
WHERE report_status = 'deleted'
) AS deleted_booking_count,
COUNT(*) FILTER (
WHERE report_status = 'deleted_by_club'
) AS deleted_by_club_booking_count,
COUNT(*) FILTER (
WHERE report_status = 'failed'
) AS failed_booking_count,
COUNT(*) FILTER (
WHERE report_status = 'valid'
AND timing_status = 'past'
) AS past_valid_booking_count,
COUNT(*) FILTER (
WHERE report_status = 'valid'
AND timing_status = 'future'
) AS future_valid_booking_count,
COALESCE(
SUM(duration_hours) FILTER (
WHERE report_status = 'valid'
),
0
) AS valid_booked_hours,
COALESCE(
SUM(duration_hours) FILTER (
WHERE report_status = 'valid'
AND timing_status = 'past'
),
0
) AS past_valid_booked_hours,
COALESCE(
SUM(duration_hours) FILTER (
WHERE report_status = 'valid'
AND timing_status = 'future'
),
0
) AS future_valid_booked_hours,
COUNT(
DISTINCT booking_owner_user_profile_id
) AS distinct_booking_owner_count,
COUNT(*) FILTER (
WHERE booking_owner_user_profile_id IS NULL
) AS unassigned_or_guest_booking_count,
COUNT(*) FILTER (
WHERE academy_type IS NOT NULL
) AS academy_booking_count,
COUNT(*) FILTER (
WHERE academy_type IS NULL
) AS standard_booking_count,
COUNT(*) FILTER (
WHERE payment_method = 'supported'
) AS supported_booking_count,
COUNT(*) FILTER (
WHERE payment_status = 'paid'
) AS paid_booking_count,
COUNT(*) FILTER (
WHERE payment_status = 'refunded'
) AS refunded_booking_count,
COUNT(DISTINCT currency) FILTER (
WHERE payment_amount IS NOT NULL
) AS payment_currency_count,
COUNT(*) FILTER (
WHERE raw_booking_status IS NULL
OR lower(raw_booking_status) NOT IN (
'confirmed',
'cancelled',
'deleted',
'deleted by club',
'failed'
)
) AS unknown_raw_status_count
FROM filtered_rows
),
payment_totals AS (
SELECT
fr.currency,
COUNT(*) FILTER (
WHERE fr.payment_status = 'paid'
) AS paid_payment_count,
COALESCE(
SUM(fr.payment_amount) FILTER (
WHERE fr.payment_status = 'paid'
),
0
) AS paid_amount,
COUNT(*) FILTER (
WHERE fr.payment_status = 'refunded'
) AS refunded_payment_count,
COALESCE(
SUM(fr.payment_amount) FILTER (
WHERE fr.payment_status = 'refunded'
),
0
) AS refunded_amount,
COUNT(*) FILTER (
WHERE fr.payment_status = 'pending'
) AS pending_payment_count,
COALESCE(
SUM(fr.payment_amount) FILTER (
WHERE fr.payment_status = 'pending'
),
0
) AS pending_amount
FROM filtered_rows fr
WHERE fr.payment_amount IS NOT NULL
GROUP BY fr.currency
),
daily_totals AS (
SELECT
fr.booking_date AS period_start,
COUNT(*) AS total_booking_count,
COUNT(*) FILTER (
WHERE fr.report_status = 'valid'
) AS valid_booking_count,
COUNT(*) FILTER (
WHERE fr.report_status = 'cancelled'
) AS cancelled_booking_count,
COUNT(*) FILTER (
WHERE fr.report_status IN (
'deleted',
'deleted_by_club'
)
) AS deleted_booking_count,
COUNT(*) FILTER (
WHERE fr.report_status = 'failed'
) AS failed_booking_count,
COALESCE(
SUM(fr.duration_hours) FILTER (
WHERE fr.report_status = 'valid'
),
0
) AS valid_booked_hours
FROM filtered_rows fr
GROUP BY fr.booking_date
),
weekly_totals AS (
SELECT
date_trunc(
'week',
fr.booking_date::timestamp
)::date AS period_start,
COUNT(*) AS total_booking_count,
COUNT(*) FILTER (
WHERE fr.report_status = 'valid'
) AS valid_booking_count,
COUNT(*) FILTER (
WHERE fr.report_status = 'cancelled'
) AS cancelled_booking_count,
COUNT(*) FILTER (
WHERE fr.report_status IN (
'deleted',
'deleted_by_club'
)
) AS deleted_booking_count,
COUNT(*) FILTER (
WHERE fr.report_status = 'failed'
) AS failed_booking_count,
COALESCE(
SUM(fr.duration_hours) FILTER (
WHERE fr.report_status = 'valid'
),
0
) AS valid_booked_hours
FROM filtered_rows fr
GROUP BY
date_trunc(
'week',
fr.booking_date::timestamp
)::date
),
monthly_totals AS (
SELECT
date_trunc(
'month',
fr.booking_date::timestamp
)::date AS period_start,
COUNT(*) AS total_booking_count,
COUNT(*) FILTER (
WHERE fr.report_status = 'valid'
) AS valid_booking_count,
COUNT(*) FILTER (
WHERE fr.report_status = 'cancelled'
) AS cancelled_booking_count,
COUNT(*) FILTER (
WHERE fr.report_status IN (
'deleted',
'deleted_by_club'
)
) AS deleted_booking_count,
COUNT(*) FILTER (
WHERE fr.report_status = 'failed'
) AS failed_booking_count,
COALESCE(
SUM(fr.duration_hours) FILTER (
WHERE fr.report_status = 'valid'
),
0
) AS valid_booked_hours
FROM filtered_rows fr
GROUP BY
date_trunc(
'month',
fr.booking_date::timestamp
)::date
),
limited_rows AS (
SELECT fr.*
FROM filtered_rows fr
ORDER BY
fr.booking_date,
fr.start_time,
fr.booking_id
LIMIT v_effective_limit
)
SELECT jsonb_build_object(
'report_id',
'bookings',
'generated_at',
now(),
'club_id',
p_club_id,
'timezone',
v_timezone,
'date_from',
p_date_from,
'date_to',
p_date_to,
'filters',
jsonb_build_object(
'user_profile_id',
p_user_profile_id,
'court_id',
p_court_id,
'status',
v_status_filter,
'timing',
v_timing_filter,
'academy_program_id',
p_academy_program_id
),
'summary',
jsonb_build_object(
'total_booking_count',
rs.total_booking_count,
'valid_booking_count',
rs.valid_booking_count,
'cancelled_booking_count',
rs.cancelled_booking_count,
'deleted_booking_count',
rs.deleted_booking_count,
'deleted_by_club_booking_count',
rs.deleted_by_club_booking_count,
'failed_booking_count',
rs.failed_booking_count,
'past_valid_booking_count',
rs.past_valid_booking_count,
'future_valid_booking_count',
rs.future_valid_booking_count,
'valid_booked_hours',
rs.valid_booked_hours,
'past_valid_booked_hours',
rs.past_valid_booked_hours,
'future_valid_booked_hours',
rs.future_valid_booked_hours,
'distinct_booking_owner_count',
rs.distinct_booking_owner_count,
'unassigned_or_guest_booking_count',
rs.unassigned_or_guest_booking_count,
'academy_booking_count',
rs.academy_booking_count,
'standard_booking_count',
rs.standard_booking_count,
'supported_booking_count',
rs.supported_booking_count,
'paid_booking_count',
rs.paid_booking_count,
'refunded_booking_count',
rs.refunded_booking_count,
'payment_totals_by_currency',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'currency',
pt.currency,
'paid_payment_count',
pt.paid_payment_count,
'paid_amount',
pt.paid_amount,
'refunded_payment_count',
pt.refunded_payment_count,
'refunded_amount',
pt.refunded_amount,
'pending_payment_count',
pt.pending_payment_count,
'pending_amount',
pt.pending_amount
)
ORDER BY pt.currency
)
FROM payment_totals pt
),
'[]'::jsonb
),
'status_totals',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'status',
x.report_status,
'count',
x.row_count
)
ORDER BY x.report_status
)
FROM (
SELECT
fr.report_status,
COUNT(*) AS row_count
FROM filtered_rows fr
GROUP BY fr.report_status
) x
),
'[]'::jsonb
)
),
'breakdowns',
jsonb_build_object(
'daily',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'period_start',
dt.period_start,
'period_label',
to_char(
dt.period_start,
'YYYY-MM-DD'
),
'total_booking_count',
dt.total_booking_count,
'valid_booking_count',
dt.valid_booking_count,
'cancelled_booking_count',
dt.cancelled_booking_count,
'deleted_booking_count',
dt.deleted_booking_count,
'failed_booking_count',
dt.failed_booking_count,
'valid_booked_hours',
dt.valid_booked_hours
)
ORDER BY dt.period_start
)
FROM daily_totals dt
),
'[]'::jsonb
),
'weekly',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'period_start',
wt.period_start,
'period_label',
to_char(
wt.period_start,
'IYYY-""W""IW'
),
'total_booking_count',
wt.total_booking_count,
'valid_booking_count',
wt.valid_booking_count,
'cancelled_booking_count',
wt.cancelled_booking_count,
'deleted_booking_count',
wt.deleted_booking_count,
'failed_booking_count',
wt.failed_booking_count,
'valid_booked_hours',
wt.valid_booked_hours
)
ORDER BY wt.period_start
)
FROM weekly_totals wt
),
'[]'::jsonb
),
'monthly',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'period_start',
mt.period_start,
'period_label',
to_char(
mt.period_start,
'YYYY-MM'
),
'total_booking_count',
mt.total_booking_count,
'valid_booking_count',
mt.valid_booking_count,
'cancelled_booking_count',
mt.cancelled_booking_count,
'deleted_booking_count',
mt.deleted_booking_count,
'failed_booking_count',
mt.failed_booking_count,
'valid_booked_hours',
mt.valid_booked_hours
)
ORDER BY mt.period_start
)
FROM monthly_totals mt
),
'[]'::jsonb
)
),
'rows',
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'booking_id',
lr.booking_id,
'booking_date',
lr.booking_date,
'start_time',
lr.start_time,
'end_time',
lr.end_time,
'duration_hours',
lr.duration_hours,
'booking_start_at',
lr.booking_start_at,
'booking_end_at',
lr.booking_end_at,
'timing_status',
lr.timing_status,
'report_status',
lr.report_status,
'raw_booking_status',
lr.raw_booking_status,
'deleted_at',
lr.deleted_at,
'cancelled_by_profile_id',
lr.cancelled_by_profile_id,
'cancelled_by_name',
lr.cancelled_by_name,
'cancelled_by_role',
lr.cancelled_by_role,
'cancelled_at',
lr.cancelled_at,
'cancellation_reason_category',
lr.cancellation_reason_category,
'cancellation_reason',
lr.cancellation_reason,
'court_id',
lr.court_id,
'court_name',
lr.court_name,
'booking_type',
lr.booking_type,
'booking_category',
lr.booking_category,
'booking_name',
lr.booking_name,
'booking_owner_user_profile_id',
lr.booking_owner_user_profile_id,
'booking_owner_name',
lr.booking_owner_name,
'booking_owner_email',
lr.booking_owner_email,
'booking_owner_phone',
lr.booking_owner_phone,
'academy_type',
lr.academy_type,
'academy_program_id',
lr.academy_program_id,
'academy_program_name',
lr.academy_program_name,
'payment_method',
lr.payment_method,
'payment_status',
lr.payment_status,
'payment_amount',
lr.payment_amount,
'currency',
lr.currency,
'price_per_booking',
lr.price_per_booking,
'created_by_user_id',
lr.created_by_user_id,
'created_by_name',
lr.created_by_name,
'created_by_role',
lr.created_by_role,
'booking_note',
lr.booking_note,
'created_at',
lr.created_at
)
ORDER BY
lr.booking_date,
lr.start_time,
lr.booking_id
)
FROM limited_rows lr
),
'[]'::jsonb
),
'warnings',
COALESCE(
(
SELECT jsonb_agg(w.warning)
FROM (
SELECT jsonb_build_object(
'code',
'timezone_fallback',
'message_hu',
'A klub időzónája hiányzott vagy érvénytelen volt, ezért a rendszer Europe/Budapest időzónát használt.',
'message_en',
'The club timezone was missing or invalid, so Europe/Budapest was used.'
) AS warning
WHERE v_timezone_fallback = true
UNION ALL
SELECT jsonb_build_object(
'code',
'rows_truncated',
'message_hu',
format(
'A riport %s sort tartalmaz, de legfeljebb %s sor került visszaadásra.',
rs.total_booking_count,
v_effective_limit
),
'message_en',
format(
'The report contains %s rows, but only %s rows were returned.',
rs.total_booking_count,
v_effective_limit
)
)
WHERE rs.total_booking_count >
v_effective_limit
UNION ALL
SELECT jsonb_build_object(
'code',
'mixed_currencies',
'message_hu',
'A riport több pénznemet tartalmaz. A pénzügyi összegeket pénznemenként külön kell értelmezni.',
'message_en',
'The report contains multiple currencies. Financial totals must be interpreted separately by currency.'
)
WHERE rs.payment_currency_count > 1
UNION ALL
SELECT jsonb_build_object(
'code',
'unknown_booking_status',
'message_hu',
format(
'%s foglalás ismeretlen vagy nem szabványos nyers státuszt tartalmaz.',
rs.unknown_raw_status_count
),
'message_en',
format(
'%s bookings contain an unknown or non-standard raw status.',
rs.unknown_raw_status_count
)
)
WHERE rs.unknown_raw_status_count > 0
) w
),
'[]'::jsonb
),
'pagination',
jsonb_build_object(
'row_limit',
v_effective_limit,
'total_rows',
rs.total_booking_count,
'returned_rows',
LEAST(
rs.total_booking_count,
v_effective_limit
),
'truncated',
rs.total_booking_count >
v_effective_limit
),
'csv_columns',
jsonb_build_array(
jsonb_build_object(
'key', 'booking_date',
'label_hu', 'Dátum',
'label_en', 'Date',
'type', 'date'
),
jsonb_build_object(
'key', 'start_time',
'label_hu', 'Kezdés',
'label_en', 'Start time',
'type', 'time'
),
jsonb_build_object(
'key', 'end_time',
'label_hu', 'Befejezés',
'label_en', 'End time',
'type', 'time'
),
jsonb_build_object(
'key', 'duration_hours',
'label_hu', 'Órák száma',
'label_en', 'Hours',
'type', 'number'
),
jsonb_build_object(
'key', 'court_name',
'label_hu', 'Pálya',
'label_en', 'Court',
'type', 'text'
),
jsonb_build_object(
'key', 'booking_owner_name',
'label_hu', 'Foglaló neve',
'label_en', 'Booking owner',
'type', 'text'
),
jsonb_build_object(
'key', 'booking_owner_email',
'label_hu', 'E-mail',
'label_en', 'Email',
'type', 'text'
),
jsonb_build_object(
'key', 'booking_owner_phone',
'label_hu', 'Telefonszám',
'label_en', 'Phone',
'type', 'text'
),
jsonb_build_object(
'key', 'booking_name',
'label_hu', 'Foglalás neve',
'label_en', 'Booking name',
'type', 'text'
),
jsonb_build_object(
'key', 'booking_category',
'label_hu', 'Foglalás kategóriája',
'label_en', 'Booking category',
'type', 'text'
),
jsonb_build_object(
'key', 'academy_program_name',
'label_hu', 'Akadémiai program',
'label_en', 'Academy program',
'type', 'text'
),
jsonb_build_object(
'key', 'report_status',
'label_hu', 'Riportstátusz',
'label_en', 'Report status',
'type', 'text'
),
jsonb_build_object(
'key', 'timing_status',
'label_hu', 'Időbeli állapot',
'label_en', 'Timing status',
'type', 'text'
),
jsonb_build_object(
'key', 'payment_method',
'label_hu', 'Fizetési mód',
'label_en', 'Payment method',
'type', 'text'
),
jsonb_build_object(
'key', 'payment_status',
'label_hu', 'Fizetés állapota',
'label_en', 'Payment status',
'type', 'text'
),
jsonb_build_object(
'key', 'payment_amount',
'label_hu', 'Fizetett összeg',
'label_en', 'Payment amount',
'type', 'number'
),
jsonb_build_object(
'key', 'currency',
'label_hu', 'Pénznem',
'label_en', 'Currency',
'type', 'text'
),
jsonb_build_object(
'key', 'created_by_name',
'label_hu', 'Létrehozta',
'label_en', 'Created by',
'type', 'text'
),
jsonb_build_object(
'key', 'created_at',
'label_hu', 'Létrehozás ideje',
'label_en', 'Created at',
'type', 'datetime'
),
jsonb_build_object(
'key', 'cancelled_by_name',
'label_hu', 'Lemondta',
'label_en', 'Cancelled by',
'type', 'text'
),
jsonb_build_object(
'key', 'cancelled_by_role',
'label_hu', 'Szerepkör',
'label_en', 'Role',
'type', 'text'
),
jsonb_build_object(
'key', 'cancelled_at',
'label_hu', 'Lemondás ideje',
'label_en', 'Cancelled at',
'type', 'datetime'
),
jsonb_build_object(
'key', 'cancellation_reason_category',
'label_hu', 'Lemondás oka',
'label_en', 'Cancellation reason',
'type', 'text'
),
jsonb_build_object(
'key', 'cancellation_reason',
'label_hu', 'Lemondási megjegyzés',
'label_en', 'Cancellation note',
'type', 'text'
)
)
)
INTO v_result
FROM report_stats rs;
RETURN v_result;
END;
$function$
"
public,get_booking_coach_checkins,f,p_booking_id uuid,p_booking_id uuid,jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.get_booking_coach_checkins(p_booking_id uuid)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_booking record;
v_academy_type text;
v_active_checkins jsonb;
v_is_checked_in boolean := false;
v_can_check_in boolean := false;
v_has_active_coach boolean := false;
v_timezone text;
v_local_now timestamp;
v_booking_start timestamp;
v_booking_end timestamp;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Not authenticated';
END IF;
SELECT *
INTO v_booking
FROM public.bookings
WHERE id = p_booking_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Booking not found';
END IF;
IF NOT EXISTS (
SELECT 1
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_booking.club_id
) THEN
RAISE EXCEPTION
'User does not belong to the booking club';
END IF;
SELECT abd.academy_type
INTO v_academy_type
FROM public.academy_booking_details abd
WHERE abd.booking_id = p_booking_id
LIMIT 1;
SELECT EXISTS (
SELECT 1
FROM public.user_profiles up
JOIN public.coach_profiles cp
ON cp.user_profile_id = up.id
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_booking.club_id
AND cp.is_active = true
)
INTO v_has_active_coach;
SELECT COALESCE(
NULLIF(c.timezone, ''),
'UTC'
)
INTO v_timezone
FROM public.clubs c
WHERE c.id = v_booking.club_id;
v_timezone := COALESCE(v_timezone, 'UTC');
v_local_now :=
now() AT TIME ZONE v_timezone;
v_booking_start :=
(
v_booking.booking_date
+ v_booking.start_time
)::timestamp;
v_booking_end :=
(
v_booking.booking_date
+ v_booking.end_time
)::timestamp;
v_can_check_in :=
v_has_active_coach
AND v_booking.deleted_at IS NULL
AND v_booking.status NOT IN (
'cancelled',
'deleted by club',
'deleted'
)
AND v_local_now >=
v_booking_start - interval '7 days'
AND v_local_now <= v_booking_end;
IF v_academy_type IN ('group', 'camp') THEN
SELECT COALESCE(
jsonb_agg(
jsonb_build_object(
'coach_profile_id',
cp.id,
'display_name',
COALESCE(up.full_name, 'Edző'),
'color',
COALESCE(cp.color, '#d32f2f'),
'checked_in_at',
ci.checked_in_at
)
ORDER BY ci.checked_in_at ASC
),
'[]'::jsonb
)
INTO v_active_checkins
FROM public.academy_booking_coach_checkins ci
JOIN public.coach_profiles cp
ON cp.id = ci.coach_profile_id
JOIN public.user_profiles up
ON up.id = cp.user_profile_id
WHERE ci.booking_id = p_booking_id
AND ci.club_id = v_booking.club_id
AND ci.checked_out_at IS NULL;
SELECT EXISTS (
SELECT 1
FROM public.academy_booking_coach_checkins ci
JOIN public.coach_profiles cp
ON cp.id = ci.coach_profile_id
JOIN public.user_profiles up
ON up.id = cp.user_profile_id
WHERE ci.booking_id = p_booking_id
AND ci.club_id = v_booking.club_id
AND ci.checked_out_at IS NULL
AND cp.is_active = true
AND up.auth_user_id = auth.uid()
AND up.club_id = v_booking.club_id
)
INTO v_is_checked_in;
ELSE
SELECT COALESCE(
jsonb_agg(
jsonb_build_object(
'coach_profile_id',
cp.id,
'display_name',
COALESCE(up.full_name, 'Edző'),
'color',
COALESCE(cp.color, '#d32f2f'),
'checked_in_at',
v_booking.coach_checked_in_at
)
),
'[]'::jsonb
)
INTO v_active_checkins
FROM public.coach_profiles cp
JOIN public.user_profiles up
ON up.id = cp.user_profile_id
WHERE cp.id =
v_booking.checked_in_coach_profile_id;
SELECT EXISTS (
SELECT 1
FROM public.coach_profiles cp
JOIN public.user_profiles up
ON up.id = cp.user_profile_id
WHERE cp.id =
v_booking.checked_in_coach_profile_id
AND cp.is_active = true
AND up.auth_user_id = auth.uid()
AND up.club_id =
v_booking.club_id
)
INTO v_is_checked_in;
END IF;
RETURN jsonb_build_object(
'active_checkins',
COALESCE(
v_active_checkins,
'[]'::jsonb
),
'is_checked_in',
v_is_checked_in,
'can_check_in',
v_can_check_in
);
END;
$function$
"
public,get_partner_directory,f,"search_text text, min_utr numeric, max_utr numeric, p_sport text","search_text text DEFAULT NULL::text, min_utr numeric DEFAULT NULL::numeric, max_utr numeric DEFAULT NULL::numeric, p_sport text DEFAULT 'tennis'::text","TABLE(user_profile_id uuid, full_name text, utr_rating numeric, last_partner_search_booking_date date, email text, phone text, show_email_to_players boolean, show_phone_to_players boolean, availability jsonb)",plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.get_partner_directory(search_text text DEFAULT NULL::text, min_utr numeric DEFAULT NULL::numeric, max_utr numeric DEFAULT NULL::numeric, p_sport text DEFAULT 'tennis'::text)
RETURNS TABLE(user_profile_id uuid, full_name text, utr_rating numeric, last_partner_search_booking_date date, email text, phone text, show_email_to_players boolean, show_phone_to_players boolean, availability jsonb)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_club_id uuid;
BEGIN
IF p_sport IS NULL OR p_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION 'Invalid sport. Must be ''tennis'' or ''padel''.';
END IF;
IF auth.uid() IS NULL THEN
RETURN;
END IF;
SELECT up.club_id
INTO v_caller_club_id
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
ORDER BY up.id
LIMIT 1;
IF v_caller_club_id IS NULL THEN
RETURN;
END IF;
RETURN QUERY
SELECT
pfp.user_profile_id,
up.full_name,
pfp.utr_rating,
pfp.last_partner_search_booking_date,
CASE
WHEN COALESCE(pfp.show_email_to_players, false)
THEN up.email
ELSE NULL
END AS email,
CASE
WHEN COALESCE(pfp.show_phone_to_players, false)
THEN up.phone
ELSE NULL
END AS phone,
COALESCE(
pfp.show_email_to_players,
false
) AS show_email_to_players,
COALESCE(
pfp.show_phone_to_players,
false
) AS show_phone_to_players,
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'weekday', psa.weekday,
'start_time', to_char(psa.start_time, 'HH24:MI'),
'end_time', to_char(psa.end_time, 'HH24:MI')
)
ORDER BY
psa.weekday,
psa.start_time
)
FROM public.partner_search_availability psa
WHERE psa.user_profile_id = pfp.user_profile_id
AND psa.club_id = v_caller_club_id
AND psa.sport = p_sport
),
'[]'::jsonb
) AS availability
FROM public.partner_finder_profiles pfp
JOIN public.user_profiles up
ON up.id = pfp.user_profile_id
WHERE pfp.club_id = v_caller_club_id
AND up.club_id = v_caller_club_id
AND pfp.sport = p_sport
AND pfp.partner_search_profile_enabled = true
AND (
search_text IS NULL
OR btrim(search_text) = ''
OR up.full_name ILIKE '%' || btrim(search_text) || '%'
)
AND (
min_utr IS NULL
OR pfp.utr_rating >= min_utr
)
AND (
max_utr IS NULL
OR pfp.utr_rating <= max_utr
)
ORDER BY up.full_name;
END;
$function$
"
public,get_public_booking_blocks,f,"p_club_id uuid, p_season_id uuid","p_club_id uuid, p_season_id uuid","TABLE(booking_id uuid, club_id uuid, season_id uuid, court_id uuid, booking_date date, start_time time without time zone, end_time time without time zone, booking_status text, booking_name text, user_id uuid, created_by_user_id uuid, booking_color text, checked_in_coach_color text, partner_search_status text, is_partner_search_enabled boolean, booking_owner_utr_rating numeric, partner_search_utr_min numeric, partner_search_utr_max numeric, partner_search_players_needed integer)",sql,true,STABLE,postgres,"{postgres=X/postgres,service_role=X/postgres,anon=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.get_public_booking_blocks(p_club_id uuid, p_season_id uuid)
RETURNS TABLE(booking_id uuid, club_id uuid, season_id uuid, court_id uuid, booking_date date, start_time time without time zone, end_time time without time zone, booking_status text, booking_name text, user_id uuid, created_by_user_id uuid, booking_color text, checked_in_coach_color text, partner_search_status text, is_partner_search_enabled boolean, booking_owner_utr_rating numeric, partner_search_utr_min numeric, partner_search_utr_max numeric, partner_search_players_needed integer)
LANGUAGE sql
STABLE SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
SELECT
b.id AS booking_id,
b.club_id,
b.season_id,
b.court_id,
b.booking_date,
b.start_time,
b.end_time,
b.status AS booking_status,
b.booking_name,
b.user_id,
b.created_by_user_id,
cp.color AS booking_color,
cicp.color AS checked_in_coach_color,
b.partner_search_status,
b.is_partner_search_enabled,
b.booking_owner_utr_rating,
b.partner_search_utr_min,
b.partner_search_utr_max,
b.partner_search_players_needed
FROM public.bookings b
LEFT JOIN public.coach_profiles cp
ON cp.user_profile_id = b.user_id
AND cp.is_active = true
LEFT JOIN public.coach_profiles cicp
ON cicp.id = CASE
WHEN EXISTS (
SELECT 1
FROM public.academy_booking_details abd
WHERE abd.booking_id = b.id
AND abd.academy_type IN (
'group',
'camp'
)
)
THEN (
SELECT c.coach_profile_id
FROM public.academy_booking_coach_checkins c
WHERE c.booking_id = b.id
AND c.checked_out_at IS NULL
ORDER BY c.checked_in_at ASC
LIMIT 1
)
ELSE b.checked_in_coach_profile_id
END
WHERE b.club_id = p_club_id
AND b.season_id = p_season_id
AND b.deleted_at IS NULL
AND b.status NOT IN (
'cancelled',
'deleted by club',
'deleted'
);
$function$
"
public,get_recurring_season_cancel_preview,f,p_booking_id uuid,p_booking_id uuid,jsonb,plpgsql,true,STABLE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.get_recurring_season_cancel_preview(p_booking_id uuid)
RETURNS jsonb
LANGUAGE plpgsql
STABLE SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_source_booking RECORD;
v_caller_profile RECORD;
v_club_id uuid;
v_season_id uuid;
v_series_id uuid;
v_sport text;
v_is_admin boolean := false;
v_affected_count integer := 0;
v_total_active_count integer := 0;
v_first_date date;
v_last_date date;
v_has_paid_occurrences boolean := false;
v_can_cancel_without_refund boolean := true;
v_is_partial_venue_cancellation boolean := false;
v_payment_method text;
v_payment_status text;
v_remaining_total_amount numeric := 0;
v_has_venue_plan boolean := false;
v_plan_status text;
BEGIN
-- 1. auth required
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Authentication required.';
END IF;
-- 2. Load source booking FIRST
SELECT b.* INTO v_source_booking
FROM public.bookings b
WHERE b.id = p_booking_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Booking not found.';
END IF;
v_club_id := v_source_booking.club_id;
v_season_id := v_source_booking.season_id;
v_series_id := v_source_booking.recurring_series_id;
v_sport := v_source_booking.sport;
v_payment_method := v_source_booking.payment_method;
v_payment_status := v_source_booking.payment_status;
-- 3. Must be a recurring series booking
IF v_series_id IS NULL THEN
RAISE EXCEPTION 'This booking is not part of a recurring series.';
END IF;
-- 4. Validate canonical booking_type for normal recurring
IF lower(COALESCE(v_source_booking.booking_type, '')) <> 'recurring' THEN
RAISE EXCEPTION 'This RPC only handles normal recurring bookings.';
END IF;
-- 5. Resolve caller profile WITHIN this club
SELECT up.id, up.club_id, up.role
INTO v_caller_profile
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_club_id
LIMIT 1;
IF NOT FOUND THEN
RAISE EXCEPTION 'Caller profile not found in this club.';
END IF;
-- 6. Club isolation
IF v_caller_profile.club_id != v_club_id THEN
RAISE EXCEPTION 'You do not have permission to cancel this booking.';
END IF;
-- 7. Already cancelled check
IF v_source_booking.status = 'cancelled' OR v_source_booking.deleted_at IS NOT NULL THEN
RAISE EXCEPTION 'This booking has already been cancelled or deleted.';
END IF;
-- 8. Academy isolation: reject Academy bookings
IF EXISTS (
SELECT 1 FROM public.academy_booking_details abd
WHERE abd.booking_id = p_booking_id
) THEN
RAISE EXCEPTION 'Academy bookings must be cancelled through the Academy cancellation system.';
END IF;
-- 9. Admin-only authorization
v_is_admin := v_caller_profile.role IN ('owner', 'admin', 'secondary_admin');
IF NOT v_is_admin THEN
-- Same ownership rule as cancel_booking_with_credit_refund
IF v_source_booking.user_id IS DISTINCT FROM v_caller_profile.id
AND v_source_booking.created_by_user_id IS DISTINCT FROM v_caller_profile.id
THEN
RAISE EXCEPTION 'Unauthorized to cancel this booking.';
END IF;
-- Same past-booking / cancellation-cutoff rule
IF (
(v_source_booking.booking_date + v_source_booking.start_time)
AT TIME ZONE COALESCE(
(
SELECT c.timezone
FROM public.clubs c
WHERE c.id = v_source_booking.club_id
),
'UTC'
)
) < now()
THEN
RAISE EXCEPTION 'Cannot cancel a past booking.';
END IF;
IF (
(v_source_booking.booking_date + v_source_booking.start_time)
AT TIME ZONE COALESCE(
(
SELECT c.timezone
FROM public.clubs c
WHERE c.id = v_source_booking.club_id
),
'UTC'
)
) <
now()
+
(
COALESCE(
(
SELECT c.cancellation_cutoff_hours
FROM public.clubs c
WHERE c.id = v_source_booking.club_id
),
0
) || ' hours'
)::interval
THEN
RAISE EXCEPTION 'Cancellation cutoff time has passed.';
END IF;
END IF;
-- 10. Count AFFECTED bookings (source date onwards) using IDENTICAL WHERE conditions as bulk RPC
SELECT
COUNT(*),
MIN(b.booking_date),
MAX(b.booking_date),
COALESCE(SUM(COALESCE(b.price_per_booking, 0)), 0)
INTO
v_affected_count,
v_first_date,
v_last_date,
v_remaining_total_amount
FROM public.bookings b
WHERE b.recurring_series_id = v_series_id
AND b.club_id = v_club_id
AND b.season_id = v_season_id
AND b.sport = v_sport
AND b.booking_date >= v_source_booking.booking_date
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, '')) NOT IN ('cancelled', 'deleted', 'deleted by club', 'failed');
-- 10b. Count TOTAL active bookings in the entire series
SELECT COUNT(*) INTO v_total_active_count
FROM public.bookings b
WHERE b.recurring_series_id = v_series_id
AND b.club_id = v_club_id
AND b.season_id = v_season_id
AND b.sport = v_sport
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, '')) NOT IN ('cancelled', 'deleted', 'deleted by club', 'failed');
-- 10c. Check for venue payment plan
SELECT plan_status INTO v_plan_status
FROM public.venue_payment_plans
WHERE recurring_series_id = v_series_id
AND club_id = v_club_id
LIMIT 1;
v_has_venue_plan := FOUND;
-- 10d. Detect partial venue cancellation
IF v_has_venue_plan AND v_affected_count < v_total_active_count THEN
v_is_partial_venue_cancellation := true;
v_can_cancel_without_refund := false;
END IF;
-- 11. GENERIC positive paid payment detection (IDENTICAL to bulk RPC)
-- Check ANY affected booking with positive paid booking_payments
IF NOT v_is_partial_venue_cancellation THEN
v_has_paid_occurrences := EXISTS (
SELECT 1
FROM public.bookings b
JOIN public.booking_payments bp ON bp.booking_id = b.id
WHERE b.recurring_series_id = v_series_id
AND b.club_id = v_club_id
AND b.season_id = v_season_id
AND b.sport = v_sport
AND b.booking_date >= v_source_booking.booking_date
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, '')) NOT IN ('cancelled', 'deleted', 'deleted by club', 'failed')
AND bp.payment_status = 'paid'
AND COALESCE(bp.amount, 0) > 0
);
-- Also check venue_payment_plans for the series (paid/partially_paid)
IF NOT v_has_paid_occurrences AND v_has_venue_plan AND v_plan_status IN ('partially_paid', 'paid') THEN
v_has_paid_occurrences := true;
END IF;
-- 12. Check Academy in affected set (IDENTICAL to bulk RPC)
IF NOT v_has_paid_occurrences THEN
v_has_paid_occurrences := EXISTS (
SELECT 1
FROM public.bookings b
JOIN public.academy_booking_details abd ON abd.booking_id = b.id
WHERE b.recurring_series_id = v_series_id
AND b.club_id = v_club_id
AND b.season_id = v_season_id
AND b.sport = v_sport
AND b.booking_date >= v_source_booking.booking_date
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, '')) NOT IN ('cancelled', 'deleted', 'deleted by club', 'failed')
);
END IF;
-- 13. Check NULL payment_method + positive price (IDENTICAL to bulk RPC)
IF NOT v_has_paid_occurrences THEN
v_has_paid_occurrences := EXISTS (
SELECT 1
FROM public.bookings b
WHERE b.recurring_series_id = v_series_id
AND b.club_id = v_club_id
AND b.season_id = v_season_id
AND b.sport = v_sport
AND b.booking_date >= v_source_booking.booking_date
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, '')) NOT IN ('cancelled', 'deleted', 'deleted by club', 'failed')
AND b.payment_method IS NULL
AND COALESCE(b.price_per_booking, 0) > 0
);
END IF;
-- 14. Check unsupported payment state (IDENTICAL to bulk RPC)
-- Only venue+pending, supported+0, NULL+0 are allowed
IF NOT v_has_paid_occurrences THEN
v_has_paid_occurrences := EXISTS (
SELECT 1
FROM public.bookings b
WHERE b.recurring_series_id = v_series_id
AND b.club_id = v_club_id
AND b.season_id = v_season_id
AND b.sport = v_sport
AND b.booking_date >= v_source_booking.booking_date
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, '')) NOT IN ('cancelled', 'deleted', 'deleted by club', 'failed')
AND NOT (
(b.payment_method = 'venue' AND b.payment_status = 'pending')
OR (b.payment_method = 'supported' AND COALESCE(b.price_per_booking, 0) = 0)
OR (b.payment_method IS NULL AND COALESCE(b.price_per_booking, 0) = 0)
)
);
END IF;
v_can_cancel_without_refund := NOT v_has_paid_occurrences;
END IF;
RETURN jsonb_build_object(
'success', true,
'affected_count', v_affected_count,
'total_active_count', v_total_active_count,
'first_booking_date', to_char(v_first_date, 'YYYY-MM-DD'),
'last_booking_date', to_char(v_last_date, 'YYYY-MM-DD'),
'source_booking_date', to_char(v_source_booking.booking_date, 'YYYY-MM-DD'),
'recurring_series_id', v_series_id,
'payment_method', v_payment_method,
'payment_status', v_payment_status,
'has_paid_occurrences', v_has_paid_occurrences,
'can_cancel_without_refund', v_can_cancel_without_refund,
'is_partial_venue_cancellation', v_is_partial_venue_cancellation,
'has_venue_plan', v_has_venue_plan,
'plan_status', v_plan_status,
'remaining_total_amount', v_remaining_total_amount
);
END;
$function$
"
public,handle_new_auth_user,f,,,trigger,plpgsql,true,VOLATILE,postgres,"{=X/postgres,postgres=X/postgres,anon=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.handle_new_auth_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_full_name text;
v_first_name text;
v_last_name text;
v_phone text;
v_city text;
v_country text;
v_utr_raw text;
v_utr_rating numeric;
v_show_email boolean := false;
v_show_phone boolean := false;
v_language text;
v_raw_meta text;
BEGIN
v_first_name := NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'first_name'), '');
IF v_first_name IS NULL THEN
v_first_name := NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'firstName'), '');
END IF;
v_last_name := NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'last_name'), '');
IF v_last_name IS NULL THEN
v_last_name := NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'lastName'), '');
END IF;
v_full_name := NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'full_name'), '');
IF v_full_name IS NULL THEN
v_full_name := NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'fullName'), '');
END IF;
IF v_full_name IS NULL THEN
v_full_name := NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'name'), '');
END IF;
IF v_full_name IS NULL THEN
v_full_name := NULLIF(BTRIM(concat_ws(' ', v_first_name, v_last_name)), '');
END IF;
IF v_full_name IS NULL THEN
v_full_name := split_part(NEW.email, '@', 1);
END IF;
v_phone := NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'phone'), '');
v_city := NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'city'), '');
v_country := COALESCE(NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'country'), ''), 'Hungary');
v_utr_raw := COALESCE(
NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'utr_rating'), ''),
NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'utrRating'), ''),
NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'ctr_rating'), ''),
NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'ctrRating'), '')
);
IF v_utr_raw IS NOT NULL AND v_utr_raw ~ '^-?[0-9]+(\.[0-9]+)?$' THEN
v_utr_rating := v_utr_raw::numeric;
ELSE
v_utr_rating := NULL;
END IF;
v_raw_meta := NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'show_email_to_players'), '');
IF v_raw_meta IS NULL THEN
v_raw_meta := NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'showEmailToPlayers'), '');
END IF;
IF v_raw_meta IS NOT NULL THEN
BEGIN
v_show_email := v_raw_meta::boolean;
EXCEPTION WHEN OTHERS THEN
v_show_email := false;
END;
END IF;
v_raw_meta := NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'show_phone_to_players'), '');
IF v_raw_meta IS NULL THEN
v_raw_meta := NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'showPhoneToPlayers'), '');
END IF;
IF v_raw_meta IS NOT NULL THEN
BEGIN
v_show_phone := v_raw_meta::boolean;
EXCEPTION WHEN OTHERS THEN
v_show_phone := false;
END;
END IF;
v_language := COALESCE(
NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'preferred_language'), ''),
NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'preferredLanguage'), ''),
NULLIF(BTRIM(NEW.raw_user_meta_data ->> 'language'), ''),
'hu'
);
INSERT INTO public.user_profiles (
id, auth_user_id, email, full_name, first_name, last_name,
phone, city, country, utr_rating,
show_email_to_players, show_phone_to_players,
preferred_language, role, club_id, created_at, updated_at
)
VALUES (
gen_random_uuid(), NEW.id, NEW.email,
v_full_name, v_first_name, v_last_name,
v_phone, v_city, v_country, v_utr_rating,
v_show_email, v_show_phone,
v_language, 'user', 'e610b38b-024e-4e18-a8b9-4e78e4591d30',
NOW(), NOW()
)
ON CONFLICT (auth_user_id) DO NOTHING;
RETURN NEW;
END;
$function$
"
public,internal_update_venue_plan_status,f,p_plan_id uuid,p_plan_id uuid,void,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.internal_update_venue_plan_status(p_plan_id uuid)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_plan public.venue_payment_plans%ROWTYPE;
v_paid_total numeric;
v_new_status text;
v_inst public.venue_payment_installments%ROWTYPE;
v_sum_amounts numeric := 0;
v_inst_count integer := 0;
v_child_count integer;
v_updated_count integer;
BEGIN
SELECT *
INTO v_plan
FROM public.venue_payment_plans
WHERE id = p_plan_id
FOR UPDATE;
IF v_plan.id IS NULL THEN
RAISE EXCEPTION 'Plan not found.';
END IF;
IF v_plan.plan_status = 'cancelled' THEN
RETURN;
END IF;
v_paid_total := 0;
FOR v_inst IN
SELECT *
FROM public.venue_payment_installments
WHERE plan_id = p_plan_id
ORDER BY installment_number
FOR UPDATE
LOOP
v_inst_count := v_inst_count + 1;
v_sum_amounts := v_sum_amounts + v_inst.amount;
IF v_inst.currency IS DISTINCT FROM v_plan.currency THEN
RAISE EXCEPTION 'Installment currency mismatch.';
END IF;
IF v_inst.amount <= 0
OR v_inst.amount = 'NaN'::numeric
OR v_inst.amount = 'Infinity'::numeric
OR v_inst.amount = '-Infinity'::numeric THEN
RAISE EXCEPTION 'Invalid installment amount.';
END IF;
IF v_inst.currency = 'HUF' AND v_inst.amount != ROUND(v_inst.amount) THEN
RAISE EXCEPTION 'HUF installment amount must be a whole number.';
END IF;
IF v_inst.status = 'paid' THEN
v_paid_total := v_paid_total + v_inst.amount;
END IF;
END LOOP;
IF v_inst_count != v_plan.installment_count THEN
RAISE EXCEPTION 'Installment count mismatch.';
END IF;
IF v_sum_amounts != v_plan.total_amount THEN
RAISE EXCEPTION 'Installment sum does not match plan total.';
END IF;
IF v_paid_total > v_plan.total_amount THEN
RAISE EXCEPTION 'Paid total exceeds plan total.';
END IF;
IF v_paid_total = 0 THEN
v_new_status := 'pending';
ELSIF v_paid_total = v_plan.total_amount THEN
IF NOT EXISTS (
SELECT 1
FROM public.venue_payment_installments
WHERE plan_id = p_plan_id
AND status = 'pending'
) THEN
v_new_status := 'paid';
ELSE
v_new_status := 'partially_paid';
END IF;
ELSE
v_new_status := 'partially_paid';
END IF;
IF v_new_status IS DISTINCT FROM v_plan.plan_status THEN
UPDATE public.venue_payment_plans
SET plan_status = v_new_status,
updated_at = now()
WHERE id = p_plan_id;
IF v_new_status = 'paid' THEN
PERFORM set_config('app.bypass_booking_security', 'true', true);
IF v_plan.booking_id IS NOT NULL THEN
UPDATE public.bookings
SET payment_status = 'paid'
WHERE id = v_plan.booking_id
AND payment_status = 'pending'
AND deleted_at IS NULL
AND status NOT IN ('cancelled', 'deleted by club', 'failed')
AND payment_method = 'venue'
AND club_id = v_plan.club_id
AND season_id = v_plan.season_id
AND booking_type = 'one-time';
GET DIAGNOSTICS v_updated_count = ROW_COUNT;
IF v_updated_count != 1 THEN
RAISE EXCEPTION 'Failed to sync one-time booking to paid. Row count mismatch.';
END IF;
ELSIF v_plan.recurring_series_id IS NOT NULL THEN
SELECT COUNT(*)
INTO v_child_count
FROM public.bookings
WHERE recurring_series_id = v_plan.recurring_series_id
AND deleted_at IS NULL
AND status NOT IN ('cancelled', 'deleted by club', 'failed');
IF v_child_count > 0 THEN
UPDATE public.bookings
SET payment_status = 'paid'
WHERE recurring_series_id = v_plan.recurring_series_id
AND payment_status = 'pending'
AND deleted_at IS NULL
AND status NOT IN ('cancelled', 'deleted by club', 'failed')
AND payment_method = 'venue'
AND club_id = v_plan.club_id
AND season_id = v_plan.season_id
AND booking_type = 'recurring';
GET DIAGNOSTICS v_updated_count = ROW_COUNT;
IF v_updated_count != v_child_count THEN
RAISE EXCEPTION 'Failed to sync recurring bookings to paid. Row count mismatch.';
END IF;
END IF;
END IF;
PERFORM set_config('app.bypass_booking_security', 'false', true);
ELSIF v_new_status IN ('pending', 'partially_paid') THEN
PERFORM set_config('app.bypass_booking_security', 'true', true);
IF v_plan.booking_id IS NOT NULL THEN
UPDATE public.bookings
SET payment_status = 'pending'
WHERE id = v_plan.booking_id
AND payment_status = 'paid'
AND deleted_at IS NULL
AND status NOT IN ('cancelled', 'deleted by club', 'failed')
AND payment_method = 'venue'
AND club_id = v_plan.club_id
AND season_id = v_plan.season_id
AND booking_type = 'one-time';
GET DIAGNOSTICS v_updated_count = ROW_COUNT;
IF v_plan.plan_status = 'paid' AND v_updated_count != 1 THEN
RAISE EXCEPTION 'Failed to sync one-time booking to pending. Row count mismatch.';
END IF;
ELSIF v_plan.recurring_series_id IS NOT NULL THEN
SELECT COUNT(*)
INTO v_child_count
FROM public.bookings
WHERE recurring_series_id = v_plan.recurring_series_id
AND deleted_at IS NULL
AND status NOT IN ('cancelled', 'deleted by club', 'failed');
IF v_child_count > 0 THEN
UPDATE public.bookings
SET payment_status = 'pending'
WHERE recurring_series_id = v_plan.recurring_series_id
AND payment_status = 'paid'
AND deleted_at IS NULL
AND status NOT IN ('cancelled', 'deleted by club', 'failed')
AND payment_method = 'venue'
AND club_id = v_plan.club_id
AND season_id = v_plan.season_id
AND booking_type = 'recurring';
GET DIAGNOSTICS v_updated_count = ROW_COUNT;
IF v_plan.plan_status = 'paid' AND v_updated_count != v_child_count THEN
RAISE EXCEPTION 'Failed to sync recurring bookings to pending. Row count mismatch.';
END IF;
END IF;
END IF;
PERFORM set_config('app.bypass_booking_security', 'false', true);
END IF;
END IF;
EXCEPTION
WHEN OTHERS THEN
PERFORM set_config('app.bypass_booking_security', 'false', true);
RAISE;
END;
$function$
"
public,is_current_user_club_admin,f,target_club_id uuid,target_club_id uuid,boolean,sql,true,STABLE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.is_current_user_club_admin(target_club_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE SECURITY DEFINER
SET search_path TO ''
AS $function$
SELECT EXISTS (
SELECT 1
FROM public.user_profiles AS up
WHERE up.auth_user_id = (SELECT auth.uid())
AND up.club_id = target_club_id
AND up.role IN (
'owner',
'admin',
'secondary_admin'
)
);
$function$
"
public,is_current_user_coach_profile,f,target_coach_profile_id uuid,target_coach_profile_id uuid,boolean,sql,true,STABLE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.is_current_user_coach_profile(target_coach_profile_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE SECURITY DEFINER
SET search_path TO ''
AS $function$
SELECT EXISTS (
SELECT 1
FROM public.coach_profiles AS cp
WHERE cp.id = target_coach_profile_id
AND cp.user_id = (SELECT auth.uid())
);
$function$
"
public,list_academy_program_participants,f,"p_program_id uuid, p_include_archived boolean, p_sport text","p_program_id uuid, p_include_archived boolean DEFAULT false, p_sport text DEFAULT 'tennis'::text",SETOF academy_program_participants,plpgsql,true,STABLE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.list_academy_program_participants(p_program_id uuid, p_include_archived boolean DEFAULT false, p_sport text DEFAULT 'tennis'::text)
RETURNS SETOF academy_program_participants
LANGUAGE plpgsql
STABLE SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_club_id uuid;
v_sport text;
BEGIN
v_sport := lower(
COALESCE(NULLIF(btrim(p_sport), ''), 'tennis')
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION
'Érvénytelen sport. Engedélyezett értékek: tennis, padel.';
END IF;
SELECT ap.club_id
INTO v_club_id
FROM public.academy_programs ap
WHERE ap.id = p_program_id
AND ap.sport = v_sport;
IF NOT FOUND THEN
RAISE EXCEPTION
'Az akadémiai program nem található ebben a sportban.';
END IF;
IF NOT public.can_manage_academy(v_club_id) THEN
RAISE EXCEPTION
'Nincs jogosultságod a résztvevők megtekintéséhez.';
END IF;
RETURN QUERY
SELECT app.*
FROM public.academy_program_participants app
WHERE app.academy_program_id = p_program_id
AND (
p_include_archived = true
OR app.is_active = true
)
ORDER BY
app.is_active DESC,
lower(app.last_name),
lower(app.first_name);
END;
$function$
"
public,list_academy_programs,f,"p_club_id uuid, p_program_type text, p_include_archived boolean, p_sport text","p_club_id uuid, p_program_type text DEFAULT NULL::text, p_include_archived boolean DEFAULT false, p_sport text DEFAULT 'tennis'::text",SETOF academy_programs,plpgsql,true,STABLE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.list_academy_programs(p_club_id uuid, p_program_type text DEFAULT NULL::text, p_include_archived boolean DEFAULT false, p_sport text DEFAULT 'tennis'::text)
RETURNS SETOF academy_programs
LANGUAGE plpgsql
STABLE SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_sport text;
BEGIN
-- ========================================================
-- TENNIS SAFETY GATE
-- Existing calls without p_sport remain Tennis.
-- ========================================================
v_sport :=
lower(
COALESCE(
NULLIF(btrim(p_sport), ''),
'tennis'
)
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION
'Érvénytelen sport. Engedélyezett értékek: tennis, padel.';
END IF;
IF NOT public.can_manage_academy(p_club_id) THEN
RAISE EXCEPTION
'Nincs jogosultságod az akadémiai programok megtekintéséhez.';
END IF;
RETURN QUERY
SELECT ap.*
FROM public.academy_programs ap
WHERE ap.club_id = p_club_id
-- CRITICAL SPORT ISOLATION
AND ap.sport = v_sport
AND (
p_program_type IS NULL
OR ap.program_type = p_program_type
)
AND (
p_include_archived = true
OR ap.is_active = true
)
ORDER BY
ap.is_active DESC,
lower(ap.name);
END;
$function$
"
public,move_admin_booking_occurrence,f,"p_booking_id uuid, p_new_booking_date date, p_new_court_id uuid, p_new_start_time time without time zone, p_new_end_time time without time zone","p_booking_id uuid, p_new_booking_date date, p_new_court_id uuid, p_new_start_time time without time zone, p_new_end_time time without time zone",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres,authenticated=X/postgres}","CREATE OR REPLACE FUNCTION public.move_admin_booking_occurrence(p_booking_id uuid, p_new_booking_date date, p_new_court_id uuid, p_new_start_time time without time zone, p_new_end_time time without time zone)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_booking public.bookings%ROWTYPE;
v_academy_detail public.academy_booking_details%ROWTYPE;
v_caller_profile_id uuid;
v_caller_role text;
v_caller_name text;
v_caller_coach_profile_id uuid;
v_caller_is_active_checked_in boolean := false;
v_is_booking_owner boolean := false;
v_permission_basis text;
v_active_academy_coach_count integer := 0;
v_has_any_active_coach_checkin boolean := false;
v_club_timezone text;
v_club_currency text;
v_cancellation_cutoff_hours integer;
v_season_start_date date;
v_season_end_date date;
v_season_currency text;
v_source_start_at timestamptz;
v_destination_start_at timestamptz;
v_user_change_deadline timestamptz;
v_source_duration interval;
v_destination_duration interval;
v_destination_day_of_week integer;
v_destination_iso_day integer;
v_destination_court_name text;
v_destination_court_sport text;
v_source_court_name text;
v_opening_row_count integer;
v_within_opening_hours boolean;
v_conflict_booking_id uuid;
v_is_academy boolean := false;
v_is_recurring boolean := false;
v_has_attendance boolean := false;
v_has_attendance_audit boolean := false;
v_effective_pricing_mode text;
v_effective_currency text;
v_stored_original_price numeric;
v_original_comparison_price numeric;
v_destination_price numeric;
v_price_basis text;
v_price_relation text;
v_academy_program_id uuid;
v_academy_court_fee_huf numeric;
v_audit_log_id uuid;
v_old_position jsonb;
v_new_position jsonb;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION
'A foglalás áthelyezéséhez bejelentkezés szükséges.';
END IF;
IF p_booking_id IS NULL THEN
RAISE EXCEPTION
'A foglalásazonosító kötelező.';
END IF;
IF p_new_booking_date IS NULL
OR p_new_court_id IS NULL
OR p_new_start_time IS NULL
OR p_new_end_time IS NULL
THEN
RAISE EXCEPTION
'Az új dátum, pálya, kezdési és befejezési idő kötelező.';
END IF;
IF p_new_start_time >= p_new_end_time THEN
RAISE EXCEPTION
'A kezdési időnek korábbinak kell lennie a befejezési időnél.';
END IF;
IF EXTRACT(SECOND FROM p_new_start_time)::integer <> 0
OR EXTRACT(SECOND FROM p_new_end_time)::integer <> 0
THEN
RAISE EXCEPTION
'A foglalási időpontok másodpercértéke csak 00 lehet.';
END IF;
IF EXTRACT(MINUTE FROM p_new_start_time)::integer % 30 <> 0
OR EXTRACT(MINUTE FROM p_new_end_time)::integer % 30 <> 0
THEN
RAISE EXCEPTION
'A foglalás csak 30 perces időhatárokra helyezhető.';
END IF;
SELECT b.*
INTO v_booking
FROM public.bookings b
WHERE b.id = p_booking_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION
'A foglalás nem található.';
END IF;
IF v_booking.club_id IS NULL
OR v_booking.season_id IS NULL
OR v_booking.court_id IS NULL
THEN
RAISE EXCEPTION
'A foglalás klub-, szezon- vagy pályaadata hiányos.';
END IF;
SELECT
up.id,
up.role,
up.full_name
INTO
v_caller_profile_id,
v_caller_role,
v_caller_name
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_booking.club_id
LIMIT 1;
IF v_caller_profile_id IS NULL THEN
RAISE EXCEPTION
'A klubhoz tartozó felhasználói profil nem található.';
END IF;
v_is_booking_owner :=
v_booking.user_id = v_caller_profile_id
OR (
v_booking.user_id IS NULL
AND v_booking.created_by_user_id = v_caller_profile_id
);
SELECT cp.id
INTO v_caller_coach_profile_id
FROM public.coach_profiles cp
WHERE cp.user_profile_id = v_caller_profile_id
AND COALESCE(cp.is_active, false) = true
LIMIT 1;
IF v_caller_coach_profile_id IS NOT NULL THEN
v_caller_is_active_checked_in :=
EXISTS (
SELECT 1
FROM public.academy_booking_coach_checkins abcc
WHERE abcc.booking_id = v_booking.id
AND abcc.club_id = v_booking.club_id
AND abcc.coach_profile_id =
v_caller_coach_profile_id
AND abcc.checked_out_at IS NULL
)
OR (
v_booking.checked_in_coach_profile_id =
v_caller_coach_profile_id
AND v_booking.coach_checked_in_at IS NOT NULL
);
END IF;
IF v_caller_role IN (
'owner',
'admin',
'secondary_admin'
) THEN
v_permission_basis := 'club_admin';
ELSIF v_caller_is_active_checked_in THEN
v_permission_basis := 'active_checked_in_coach';
ELSIF v_caller_role IN (
'user',
'registered_user'
)
AND v_is_booking_owner
THEN
v_permission_basis := 'booking_owner';
ELSE
RAISE EXCEPTION
'Csak adminisztrátor, az adott alkalomra aktívan becheckolt edző vagy a foglalás tulajdonosa helyezheti át a foglalást.';
END IF;
IF v_permission_basis IN (
'active_checked_in_coach',
'booking_owner'
)
AND p_new_booking_date IS DISTINCT FROM v_booking.booking_date
THEN
IF v_permission_basis = 'booking_owner' THEN
RAISE EXCEPTION
'A saját foglalás csak ugyanazon a napon belül helyezhető át.';
ELSE
RAISE EXCEPTION
'A becheckolt edző csak ugyanazon a napon belül helyezheti át az alkalmat.';
END IF;
END IF;
IF v_booking.deleted_at IS NOT NULL THEN
RAISE EXCEPTION
'Törölt foglalás nem helyezhető át.';
END IF;
IF lower(COALESCE(v_booking.status, '')) IN (
'cancelled',
'deleted',
'deleted by club',
'failed'
) THEN
RAISE EXCEPTION
'Lemondott, törölt vagy sikertelen foglalás nem helyezhető át.';
END IF;
IF v_permission_basis = 'booking_owner'
AND v_booking.payment_status NOT IN (
'pending',
'paid'
)
THEN
RAISE EXCEPTION
'A foglalás fizetési állapota miatt nem helyezhető át.';
END IF;
IF COALESCE(v_booking.is_all_day, false) THEN
RAISE EXCEPTION
'Egész napos blokkolás nem helyezhető át ezzel a művelettel.';
END IF;
IF COALESCE(v_booking.is_all_courts, false)
OR COALESCE(
array_length(v_booking.selected_court_ids, 1),
0
) > 1
THEN
RAISE EXCEPTION
'Több pályát érintő blokkolás nem helyezhető át ezzel a művelettel.';
END IF;
SELECT
c.timezone,
c.currency,
c.cancellation_cutoff_hours
INTO
v_club_timezone,
v_club_currency,
v_cancellation_cutoff_hours
FROM public.clubs c
WHERE c.id = v_booking.club_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'A foglaláshoz tartozó klub nem található.';
END IF;
IF NULLIF(btrim(v_club_timezone), '') IS NULL
OR NOT EXISTS (
SELECT 1
FROM pg_timezone_names tz
WHERE tz.name = v_club_timezone
)
THEN
RAISE EXCEPTION
'A klub időzóna-beállítása hiányzik vagy érvénytelen.';
END IF;
v_cancellation_cutoff_hours :=
COALESCE(v_cancellation_cutoff_hours, 24);
v_source_start_at :=
(
v_booking.booking_date
+ v_booking.start_time
)
AT TIME ZONE v_club_timezone;
v_destination_start_at :=
(
p_new_booking_date
+ p_new_start_time
)
AT TIME ZONE v_club_timezone;
IF v_source_start_at <= now() THEN
RAISE EXCEPTION
'Már elkezdődött vagy múltbeli foglalás nem helyezhető át.';
END IF;
IF v_destination_start_at <= now() THEN
RAISE EXCEPTION
'A célidőpontnak a jövőben kell lennie.';
END IF;
IF v_permission_basis = 'booking_owner' THEN
v_user_change_deadline :=
now()
+ make_interval(
hours => v_cancellation_cutoff_hours
);
IF v_source_start_at < v_user_change_deadline THEN
RAISE EXCEPTION
'A foglalás már a klub módosítási határidején belül van, ezért nem helyezhető át.';
END IF;
IF v_destination_start_at < v_user_change_deadline THEN
RAISE EXCEPTION
'A célidőpontnak is a klub módosítási határidején kívülre kell esnie.';
END IF;
END IF;
v_source_duration :=
v_booking.end_time
- v_booking.start_time;
v_destination_duration :=
p_new_end_time
- p_new_start_time;
IF v_source_duration <= interval '0 seconds' THEN
RAISE EXCEPTION
'A forrásfoglalás időtartama érvénytelen.';
END IF;
IF v_destination_duration IS DISTINCT FROM v_source_duration THEN
RAISE EXCEPTION
'Áthelyezéskor a foglalás időtartama nem változtatható meg.';
END IF;
IF p_new_booking_date IS NOT DISTINCT FROM v_booking.booking_date
AND p_new_court_id IS NOT DISTINCT FROM v_booking.court_id
AND p_new_start_time IS NOT DISTINCT FROM v_booking.start_time
AND p_new_end_time IS NOT DISTINCT FROM v_booking.end_time
THEN
RETURN jsonb_build_object(
'success', true,
'moved', false,
'already_at_destination', true,
'booking_id', v_booking.id,
'permission_basis', v_permission_basis,
'audit_log_id', NULL,
'message_hu',
'A foglalás már ezen az időponton található.',
'message_en',
'The booking is already at this position.'
);
END IF;
SELECT
s.start_date,
s.end_date,
s.currency
INTO
v_season_start_date,
v_season_end_date,
v_season_currency
FROM public.seasons s
WHERE s.id = v_booking.season_id
AND s.club_id = v_booking.club_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'A foglalás szezonja nem található ebben a klubban.';
END IF;
IF p_new_booking_date < v_season_start_date
OR p_new_booking_date > v_season_end_date
THEN
RAISE EXCEPTION
'A célidőpont a foglalás szezonján kívülre esik.';
END IF;
SELECT
ct.court_name,
ct.sport
INTO
v_destination_court_name,
v_destination_court_sport
FROM public.courts ct
WHERE ct.id = p_new_court_id
AND ct.club_id = v_booking.club_id
AND ct.season_id = v_booking.season_id
AND COALESCE(ct.is_active, false) = true;
IF NOT FOUND THEN
RAISE EXCEPTION
'A kiválasztott célpálya nem aktív, vagy nem ehhez a klubhoz és szezonhoz tartozik.';
END IF;
IF v_destination_court_sport IS DISTINCT FROM v_booking.sport THEN
RAISE EXCEPTION
'A foglalás csak azonos sportágú pályára helyezhető át.';
END IF;
SELECT ct.court_name
INTO v_source_court_name
FROM public.courts ct
WHERE ct.id = v_booking.court_id;
v_destination_day_of_week :=
EXTRACT(DOW FROM p_new_booking_date)::integer;
v_destination_iso_day :=
EXTRACT(ISODOW FROM p_new_booking_date)::integer;
SELECT
COUNT(*),
COALESCE(
bool_or(
COALESCE(oh.is_open, false)
AND p_new_start_time >= oh.opening_time
AND p_new_end_time <= oh.closing_time
),
false
)
INTO
v_opening_row_count,
v_within_opening_hours
FROM public.opening_hours oh
WHERE oh.club_id = v_booking.club_id
AND oh.season_id = v_booking.season_id
AND oh.weekday IN (
v_destination_day_of_week,
v_destination_iso_day
);
IF v_opening_row_count = 0 THEN
RAISE EXCEPTION
'A célidőponthoz nincs beállítva nyitvatartás.';
END IF;
IF v_opening_row_count > 1 THEN
RAISE EXCEPTION
'A célidőponthoz több, egymással ütköző nyitvatartási beállítás található.';
END IF;
IF NOT v_within_opening_hours THEN
RAISE EXCEPTION
'A célidőpont a klub nyitvatartási idején kívülre esik.';
END IF;
SELECT abd.*
INTO v_academy_detail
FROM public.academy_booking_details abd
WHERE abd.booking_id = v_booking.id;
v_is_academy := FOUND;
IF v_is_academy THEN
v_academy_program_id :=
v_academy_detail.academy_program_id;
v_academy_court_fee_huf :=
v_academy_detail.court_fee_huf;
IF v_permission_basis = 'booking_owner' THEN
RAISE EXCEPTION
'Akadémiai foglalást normál felhasználó nem helyezhet át.';
END IF;
IF v_academy_detail.cancelled_at IS NOT NULL
OR lower(
COALESCE(
v_academy_detail.occurrence_status,
''
)
) <> 'scheduled'
THEN
RAISE EXCEPTION
'Csak tervezett, nem lemondott akadémiai alkalom helyezhető át.';
END IF;
END IF;
v_is_recurring :=
v_booking.recurring_series_id IS NOT NULL;
SELECT EXISTS (
SELECT 1
FROM public.academy_attendance aa
WHERE aa.booking_id = v_booking.id
)
INTO v_has_attendance;
SELECT EXISTS (
SELECT 1
FROM public.academy_attendance_audit_logs aal
WHERE aal.booking_id = v_booking.id
)
INTO v_has_attendance_audit;
IF v_has_attendance
OR v_has_attendance_audit
THEN
RAISE EXCEPTION
'A foglalás nem helyezhető át, mert már tartozik hozzá jelenléti adat.';
END IF;
SELECT COUNT(*)
INTO v_active_academy_coach_count
FROM public.academy_booking_coach_checkins abcc
WHERE abcc.booking_id = v_booking.id
AND abcc.checked_out_at IS NULL;
v_has_any_active_coach_checkin :=
v_active_academy_coach_count > 0
OR (
v_booking.checked_in_coach_profile_id IS NOT NULL
AND v_booking.coach_checked_in_at IS NOT NULL
);
SELECT other_booking.id
INTO v_conflict_booking_id
FROM public.bookings other_booking
WHERE other_booking.id <> v_booking.id
AND other_booking.club_id = v_booking.club_id
AND other_booking.court_id = p_new_court_id
AND other_booking.booking_date = p_new_booking_date
AND other_booking.deleted_at IS NULL
AND lower(
COALESCE(
other_booking.status,
'confirmed'
)
) NOT IN (
'cancelled',
'deleted',
'deleted by club',
'failed'
)
AND other_booking.start_time < p_new_end_time
AND other_booking.end_time > p_new_start_time
LIMIT 1;
IF v_conflict_booking_id IS NOT NULL THEN
RAISE EXCEPTION
'A kiválasztott pálya és időpont már foglalt.';
END IF;
v_effective_currency :=
COALESCE(
NULLIF(btrim(v_booking.currency), ''),
v_season_currency,
v_club_currency
);
IF v_effective_currency IS NULL
OR v_effective_currency NOT IN (
'HUF',
'EUR',
'USD'
)
THEN
RAISE EXCEPTION
'A foglalás pénzneme hiányzik vagy érvénytelen.';
END IF;
IF v_season_currency IS NOT NULL
AND v_effective_currency IS DISTINCT FROM v_season_currency
THEN
RAISE EXCEPTION
'A foglalás és a szezon pénzneme eltér egymástól.';
END IF;
IF v_is_academy THEN
v_effective_pricing_mode := 'regular';
ELSIF v_booking.payment_method = 'credit_season_pass'
OR lower(
COALESCE(
v_booking.pricing_mode,
''
)
) = 'season_pass'
THEN
v_effective_pricing_mode := 'season_pass';
ELSE
v_effective_pricing_mode := 'regular';
END IF;
v_stored_original_price :=
v_booking.price_per_booking;
IF v_booking.payment_method = 'supported'
OR v_is_academy
THEN
BEGIN
-- COURT-AWARE: source underlying tariff uses the SOURCE court
v_original_comparison_price :=
public.calculate_booking_price(
v_booking.club_id,
v_booking.season_id,
v_booking.booking_date,
v_booking.start_time,
v_booking.end_time,
v_effective_pricing_mode,
v_effective_currency,
v_booking.court_id
);
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION
'Az eredeti időpont mögöttes tarifája nem állapítható meg: %',
SQLERRM;
END;
v_price_basis :=
CASE
WHEN v_is_academy
THEN 'academy_underlying_regular_tariff'
ELSE 'supported_underlying_tariff'
END;
ELSE
IF v_stored_original_price IS NULL
OR v_stored_original_price <= 0
OR v_stored_original_price = 'NaN'::numeric
OR v_stored_original_price = 'Infinity'::numeric
OR v_stored_original_price = '-Infinity'::numeric
THEN
RAISE EXCEPTION
'A foglaláshoz nem tartozik érvényes eredeti alkalmi ár.';
END IF;
v_original_comparison_price :=
v_stored_original_price;
v_price_basis :=
'stored_occurrence_price';
END IF;
BEGIN
-- COURT-AWARE: destination tariff uses the DESTINATION court
v_destination_price :=
public.calculate_booking_price(
v_booking.club_id,
v_booking.season_id,
p_new_booking_date,
p_new_start_time,
p_new_end_time,
v_effective_pricing_mode,
v_effective_currency,
p_new_court_id
);
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION
'A célidőpont tarifája nem állapítható meg: %',
SQLERRM;
END;
IF v_destination_price > v_original_comparison_price THEN
RAISE EXCEPTION
'A foglalás csak azonos árú vagy olcsóbb időintervallumba helyezhető át.';
END IF;
v_price_relation :=
CASE
WHEN v_destination_price = v_original_comparison_price
THEN 'equal'
ELSE 'cheaper'
END;
v_old_position :=
jsonb_build_object(
'booking_date', v_booking.booking_date,
'court_id', v_booking.court_id,
'court_name', v_source_court_name,
'start_time', v_booking.start_time,
'end_time', v_booking.end_time,
'duration_seconds',
EXTRACT(EPOCH FROM v_source_duration),
'stored_price_per_booking',
v_booking.price_per_booking,
'comparison_price',
v_original_comparison_price,
'currency', v_effective_currency,
'pricing_mode', v_effective_pricing_mode,
'payment_method', v_booking.payment_method,
'payment_status', v_booking.payment_status,
'recurring_series_id',
v_booking.recurring_series_id,
'academy_program_id',
v_academy_program_id,
'academy_court_fee_huf',
v_academy_court_fee_huf,
'active_coach_checkin_preserved',
v_has_any_active_coach_checkin,
'active_academy_coach_count',
v_active_academy_coach_count
);
v_new_position :=
jsonb_build_object(
'booking_date', p_new_booking_date,
'court_id', p_new_court_id,
'court_name', v_destination_court_name,
'start_time', p_new_start_time,
'end_time', p_new_end_time,
'duration_seconds',
EXTRACT(EPOCH FROM v_destination_duration),
'destination_calculated_price',
v_destination_price,
'currency', v_effective_currency,
'pricing_mode', v_effective_pricing_mode,
'price_basis', v_price_basis,
'price_relation', v_price_relation,
'refund_issued', false,
'credit_returned', false,
'payment_modified', false,
'occurrence_only', true,
'permission_basis', v_permission_basis,
'moved_by_coach_profile_id',
v_caller_coach_profile_id,
'active_coach_checkin_preserved',
v_has_any_active_coach_checkin,
'user_cutoff_hours',
CASE
WHEN v_permission_basis = 'booking_owner'
THEN v_cancellation_cutoff_hours
ELSE NULL
END,
'source',
CASE
WHEN v_permission_basis = 'booking_owner'
THEN 'user_calendar_drag'
ELSE 'calendar_drag'
END
);
PERFORM set_config(
'app.bypass_booking_security',
'true',
true
);
BEGIN
UPDATE public.bookings
SET
booking_date = p_new_booking_date,
court_id = p_new_court_id,
start_time = p_new_start_time,
end_time = p_new_end_time
WHERE id = v_booking.id;
EXCEPTION
WHEN exclusion_violation THEN
RAISE EXCEPTION
'A kiválasztott pálya és időpont időközben foglalttá vált.';
END;
PERFORM set_config(
'app.bypass_booking_security',
'false',
true
);
PERFORM public._register_booking_move_audit_context(
v_booking.id
);
INSERT INTO public.audit_log (
club_id,
action_type,
entity_type,
entity_id,
changed_by_user_id,
changed_by_name,
changed_by_role,
old_value,
new_value,
created_at
)
VALUES (
v_booking.club_id,
'booking_occurrence_moved',
'booking',
v_booking.id,
v_caller_profile_id,
v_caller_name,
v_caller_role,
v_old_position,
v_new_position,
now()
)
RETURNING id
INTO v_audit_log_id;
RETURN jsonb_build_object(
'success', true,
'moved', true,
'booking_id', v_booking.id,
'club_id', v_booking.club_id,
'season_id', v_booking.season_id,
'recurring_series_id',
v_booking.recurring_series_id,
'is_recurring_occurrence',
v_is_recurring,
'is_academy_occurrence',
v_is_academy,
'academy_program_id',
v_academy_program_id,
'permission_basis',
v_permission_basis,
'moved_by_coach_profile_id',
v_caller_coach_profile_id,
'active_coach_checkin_preserved',
v_has_any_active_coach_checkin,
'old_position',
v_old_position,
'new_position',
v_new_position,
'price_check',
jsonb_build_object(
'basis', v_price_basis,
'original_comparison_price',
v_original_comparison_price,
'destination_price',
v_destination_price,
'relation',
v_price_relation,
'currency',
v_effective_currency,
'refund_issued',
false,
'credit_returned',
false
),
'financial_data_changed',
false,
'checkin_data_changed',
false,
'entire_series_changed',
false,
'entire_academy_batch_changed',
false,
'audit_log_id',
v_audit_log_id,
'message_hu',
CASE
WHEN v_permission_basis = 'booking_owner'
AND v_price_relation = 'cheaper'
THEN
'A foglalás sikeresen átkerült az olcsóbb időpontra. Ár- vagy kreditkülönbözet nem került visszatérítésre.'
WHEN v_permission_basis = 'booking_owner'
THEN
'A foglalás sikeresen átkerült az azonos árú időpontra.'
WHEN v_permission_basis =
'active_checked_in_coach'
AND v_price_relation = 'cheaper'
THEN
'Az alkalom sikeresen átkerült az olcsóbb időpontra. Az edzői check-in megmaradt, és ár- vagy kreditkülönbözet nem került visszatérítésre.'
WHEN v_permission_basis =
'active_checked_in_coach'
THEN
'Az alkalom sikeresen átkerült. Az edzői check-in változatlanul megmaradt.'
WHEN v_price_relation = 'cheaper'
THEN
'A foglalás sikeresen átkerült az olcsóbb időpontra. Ár- vagy kreditkülönbözet nem került visszatérítésre.'
ELSE
'A foglalás sikeresen átkerült az azonos árú időpontra.'
END,
'message_en',
CASE
WHEN v_permission_basis = 'booking_owner'
AND v_price_relation = 'cheaper'
THEN
'The booking was moved successfully to the cheaper interval. No price or credit difference was refunded.'
WHEN v_permission_basis = 'booking_owner'
THEN
'The booking was moved successfully to the equal-priced interval.'
WHEN v_permission_basis =
'active_checked_in_coach'
AND v_price_relation = 'cheaper'
THEN
'The occurrence was moved successfully to the cheaper interval. The coach check-in was preserved, and no price or credit difference was refunded.'
WHEN v_permission_basis =
'active_checked_in_coach'
THEN
'The occurrence was moved successfully. The coach check-in was preserved.'
WHEN v_price_relation = 'cheaper'
THEN
'The booking was moved successfully to the cheaper interval. No price or credit difference was refunded.'
ELSE
'The booking was moved successfully to the equal-priced interval.'
END
);
EXCEPTION
WHEN OTHERS THEN
PERFORM set_config(
'app.bypass_booking_security',
'false',
true
);
RAISE;
END;
$function$
"
public,prepare_academy_program_row,f,,,trigger,plpgsql,false,VOLATILE,postgres,"{=X/postgres,postgres=X/postgres,anon=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.prepare_academy_program_row()
RETURNS trigger
LANGUAGE plpgsql
SET search_path TO 'public', 'pg_temp'
AS $function$
BEGIN
NEW.name := btrim(NEW.name);
NEW.description := NULLIF(btrim(NEW.description), '');
NEW.updated_at := now();
RETURN NEW;
END;
$function$
"
public,prevent_event_mutation,f,,,trigger,plpgsql,false,VOLATILE,postgres,"{=X/postgres,postgres=X/postgres,anon=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.prevent_event_mutation()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
RAISE EXCEPTION 'venue_payment_installment_events is append-only. UPDATE and DELETE are forbidden.';
END;
$function$
"
public,prevent_installment_core_mutation,f,,,trigger,plpgsql,false,VOLATILE,postgres,"{=X/postgres,postgres=X/postgres,anon=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.prevent_installment_core_mutation()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
IF NEW.plan_id IS DISTINCT FROM OLD.plan_id
OR NEW.installment_number IS DISTINCT FROM OLD.installment_number
OR NEW.amount IS DISTINCT FROM OLD.amount
OR NEW.currency IS DISTINCT FROM OLD.currency THEN
RAISE EXCEPTION 'Core identity and financial fields of venue_payment_installments cannot be updated.';
END IF;
RETURN NEW;
END;
$function$
"
public,prevent_installment_deletion,f,,,trigger,plpgsql,false,VOLATILE,postgres,"{=X/postgres,postgres=X/postgres,anon=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.prevent_installment_deletion()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
RAISE EXCEPTION 'venue_payment_installments cannot be deleted.';
END;
$function$
"
public,prevent_plan_core_mutation,f,,,trigger,plpgsql,false,VOLATILE,postgres,"{=X/postgres,postgres=X/postgres,anon=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.prevent_plan_core_mutation()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
IF NEW.creation_request_id IS DISTINCT FROM OLD.creation_request_id
OR NEW.club_id IS DISTINCT FROM OLD.club_id
OR NEW.season_id IS DISTINCT FROM OLD.season_id
OR NEW.booking_id IS DISTINCT FROM OLD.booking_id
OR NEW.recurring_series_id IS DISTINCT FROM OLD.recurring_series_id
OR NEW.payment_method IS DISTINCT FROM OLD.payment_method
OR NEW.total_amount IS DISTINCT FROM OLD.total_amount
OR NEW.currency IS DISTINCT FROM OLD.currency
OR NEW.installment_count IS DISTINCT FROM OLD.installment_count
OR NEW.created_by_admin_id IS DISTINCT FROM OLD.created_by_admin_id
OR NEW.customer_name IS DISTINCT FROM OLD.customer_name
OR NEW.customer_email IS DISTINCT FROM OLD.customer_email
OR NEW.customer_phone IS DISTINCT FROM OLD.customer_phone THEN
RAISE EXCEPTION 'Core identity and financial fields of venue_payment_plans cannot be updated.';
END IF;
RETURN NEW;
END;
$function$
"
public,prevent_plan_deletion,f,,,trigger,plpgsql,false,VOLATILE,postgres,"{=X/postgres,postgres=X/postgres,anon=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.prevent_plan_deletion()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
RAISE EXCEPTION 'venue_payment_plans cannot be deleted.';
END;
$function$
"
public,promote_user_to_admin,f,target_profile_id uuid,target_profile_id uuid,json,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.promote_user_to_admin(target_profile_id uuid)
RETURNS json
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_profile record;
v_target_profile record;
v_previous_role text;
v_club_id uuid;
BEGIN
-- ========================================================
-- 1. Authentication
-- ========================================================
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Authentication required.';
END IF;
-- ========================================================
-- 2. Target profile lekérése + zárolása
-- ========================================================
SELECT *
INTO v_target_profile
FROM public.user_profiles
WHERE id = target_profile_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Target profile not found.';
END IF;
-- ========================================================
-- 3. Target profilnak klubhoz kell tartoznia
-- ========================================================
IF v_target_profile.club_id IS NULL THEN
RAISE EXCEPTION 'Target profile does not belong to a club.';
END IF;
v_club_id := v_target_profile.club_id;
-- ========================================================
-- 4. Owner védelem
-- ========================================================
IF v_target_profile.role = 'owner' THEN
RAISE EXCEPTION 'Owner role cannot be modified.';
END IF;
-- ========================================================
-- 5. Csak user vagy coach lehet admin
--
-- IS NULL külön kezelve, mert a role nullable.
-- ========================================================
IF v_target_profile.role IS NULL
OR v_target_profile.role NOT IN ('user', 'coach')
THEN
RAISE EXCEPTION
'Target user is not eligible to become an admin.';
END IF;
-- ========================================================
-- 6. Caller keresése UGYANABBAN a klubban
-- ========================================================
SELECT *
INTO v_caller_profile
FROM public.user_profiles
WHERE auth_user_id = auth.uid()
AND club_id = v_club_id
LIMIT 1;
IF NOT FOUND THEN
RAISE EXCEPTION
'Caller profile not found in this club.';
END IF;
-- ========================================================
-- 7. Csak OWNER vagy ADMIN adhat admin jogot
-- ========================================================
IF v_caller_profile.role IS NULL
OR v_caller_profile.role NOT IN ('owner', 'admin')
THEN
RAISE EXCEPTION
'Unauthorized: caller cannot manage administrator roles.';
END IF;
-- ========================================================
-- 8. Saját profil módosításának tiltása
-- ========================================================
IF v_caller_profile.id = target_profile_id THEN
RAISE EXCEPTION
'You cannot modify your own administrator role.';
END IF;
-- ========================================================
-- 9. Régi role mentése audit számára
-- ========================================================
v_previous_role := v_target_profile.role;
-- ========================================================
-- 10. Adminná tétel
-- ========================================================
UPDATE public.user_profiles
SET
role = 'admin',
updated_at = now()
WHERE id = target_profile_id;
-- ========================================================
-- 11. Audit log
-- ========================================================
INSERT INTO public.audit_log (
club_id,
action_type,
entity_type,
entity_id,
changed_by_user_id,
changed_by_name,
changed_by_role,
old_value,
new_value,
created_at
)
VALUES (
v_club_id,
'user_role_promoted_to_admin',
'user_profile',
target_profile_id,
v_caller_profile.id,
COALESCE(
NULLIF(trim(v_caller_profile.full_name), ''),
v_caller_profile.email,
'Unknown'
),
v_caller_profile.role,
jsonb_build_object(
'role', v_previous_role
),
jsonb_build_object(
'role', 'admin'
),
now()
);
-- ========================================================
-- 12. Success
-- ========================================================
RETURN json_build_object(
'success', true,
'profile_id', target_profile_id,
'previous_role', v_previous_role,
'new_role', 'admin'
);
END;
$function$
"
public,promote_user_to_coach,f,target_profile_id uuid,target_profile_id uuid,jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.promote_user_to_coach(target_profile_id uuid)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
caller_profile public.user_profiles%ROWTYPE;
target_profile public.user_profiles%ROWTYPE;
created_coach_id uuid;
target_is_privileged boolean;
BEGIN
-- 1. Bejelentkezés ellenőrzése
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'AUTHENTICATION_REQUIRED'
USING ERRCODE = '42501';
END IF;
-- 2. A műveletet végző felhasználó profilja
SELECT *
INTO caller_profile
FROM public.user_profiles
WHERE auth_user_id = auth.uid()
LIMIT 1;
IF NOT FOUND THEN
RAISE EXCEPTION 'CALLER_PROFILE_NOT_FOUND'
USING ERRCODE = '42501';
END IF;
-- 3. Csak megfelelő adminisztrátori szerepkör használhatja
IF caller_profile.role::text NOT IN (
'owner',
'admin',
'secondary_admin'
) THEN
RAISE EXCEPTION 'PERMISSION_DENIED'
USING ERRCODE = '42501';
END IF;
-- 4. Célfelhasználó betöltése és zárolása a párhuzamos módosítások ellen
SELECT *
INTO target_profile
FROM public.user_profiles
WHERE id = target_profile_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'TARGET_PROFILE_NOT_FOUND'
USING ERRCODE = 'P0002';
END IF;
-- 5. Csak azonos klubban lévő felhasználó léptethető elő
IF target_profile.club_id IS DISTINCT FROM caller_profile.club_id THEN
RAISE EXCEPTION 'TARGET_USER_NOT_IN_SAME_CLUB'
USING ERRCODE = '42501';
END IF;
-- 6. Az Auth-kapcsolat kötelező
IF target_profile.auth_user_id IS NULL THEN
RAISE EXCEPTION 'TARGET_AUTH_USER_MISSING'
USING ERRCODE = '23502';
END IF;
-- 7. Duplikáció megakadályozása
IF EXISTS (
SELECT 1
FROM public.coach_profiles cp
WHERE cp.user_id = target_profile.auth_user_id
OR cp.user_profile_id = target_profile.id
) THEN
RAISE EXCEPTION 'USER_ALREADY_COACH'
USING ERRCODE = '23505';
END IF;
target_is_privileged :=
target_profile.role::text IN (
'owner',
'admin',
'secondary_admin'
);
-- 8. Coach profil létrehozása
-- A szín, minimum előfoglalás, aktív státusz és időbélyegek
-- a már meglévő adatbázis-defaultokat használják.
INSERT INTO public.coach_profiles (
user_id,
user_profile_id,
display_name,
email,
phone
)
VALUES (
target_profile.auth_user_id,
target_profile.id,
COALESCE(
NULLIF(BTRIM(target_profile.full_name), ''),
NULLIF(BTRIM(target_profile.email), ''),
'Coach'
),
target_profile.email,
target_profile.phone
)
RETURNING id
INTO created_coach_id;
-- 9. Normál userből coach szerepkör lesz.
-- Owner/admin/secondary_admin szerepkört soha nem írunk felül.
UPDATE public.user_profiles
SET
role = 'coach',
updated_at = NOW()
WHERE id = target_profile.id
AND role::text = 'user';
-- 10. Alap coach jogosultságok létrehozása
INSERT INTO public.coach_permissions (
club_id,
coach_user_profile_id,
is_active,
can_create_own_bookings,
can_edit_own_bookings,
can_delete_own_bookings,
can_view_own_schedule,
can_manage_other_bookings,
can_access_admin,
notes,
created_by_user_profile_id,
created_at,
updated_at
)
SELECT
target_profile.club_id,
target_profile.id,
true,
true,
true,
true,
true,
target_is_privileged,
target_is_privileged,
CASE
WHEN target_is_privileged
THEN 'Admin acting as coach'
ELSE 'Promoted to coach'
END,
caller_profile.id,
NOW(),
NOW()
WHERE NOT EXISTS (
SELECT 1
FROM public.coach_permissions existing_permission
WHERE existing_permission.club_id = target_profile.club_id
AND existing_permission.coach_user_profile_id = target_profile.id
);
RETURN jsonb_build_object(
'success', true,
'coach_profile_id', created_coach_id,
'user_profile_id', target_profile.id,
'auth_user_id', target_profile.auth_user_id,
'club_id', target_profile.club_id,
'previous_role', target_profile.role,
'current_role',
CASE
WHEN target_profile.role::text = 'user'
THEN 'coach'
ELSE target_profile.role::text
END
);
END;
$function$
"
public,refresh_partner_finder_profile,f,p_user_profile_id uuid,p_user_profile_id uuid,void,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.refresh_partner_finder_profile(p_user_profile_id uuid)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_club_id uuid;
v_profile_utr numeric;
v_latest_booking_date date;
v_latest_booking_utr numeric;
BEGIN
IF p_user_profile_id IS NULL THEN
RETURN;
END IF;
SELECT
up.club_id,
up.utr_rating
INTO
v_club_id,
v_profile_utr
FROM public.user_profiles up
WHERE up.id = p_user_profile_id
LIMIT 1;
IF v_club_id IS NULL THEN
RETURN;
END IF;
SELECT
b.booking_date,
b.booking_owner_utr_rating
INTO
v_latest_booking_date,
v_latest_booking_utr
FROM public.bookings b
WHERE b.user_id = p_user_profile_id
AND b.club_id = v_club_id
AND b.is_partner_search_enabled IS TRUE
AND b.partner_search_status = 'active'
AND COALESCE(b.status, '') NOT IN ('cancelled', 'deleted')
AND b.deleted_at IS NULL
AND b.booking_date >= (timezone('Europe/Budapest', now()))::date
ORDER BY
b.booking_date DESC,
b.start_time DESC,
b.created_at DESC
LIMIT 1;
IF FOUND THEN
INSERT INTO public.partner_finder_profiles (
club_id,
user_profile_id,
utr_rating,
show_email_to_players,
show_phone_to_players,
partner_search_profile_enabled,
last_partner_search_booking_date,
created_at,
updated_at
)
SELECT
up.club_id,
up.id,
COALESCE(v_latest_booking_utr, up.utr_rating),
COALESCE(up.show_email_to_players, false),
COALESCE(up.show_phone_to_players, false),
true,
v_latest_booking_date,
now(),
now()
FROM public.user_profiles up
WHERE up.id = p_user_profile_id
ON CONFLICT (user_profile_id) DO UPDATE
SET
club_id = EXCLUDED.club_id,
utr_rating = COALESCE(
EXCLUDED.utr_rating,
public.partner_finder_profiles.utr_rating
),
partner_search_profile_enabled = true,
last_partner_search_booking_date =
EXCLUDED.last_partner_search_booking_date,
updated_at = now();
-- Contact visibility settings are intentionally not overwritten
-- during an update. Existing user choices remain preserved.
ELSE
UPDATE public.partner_finder_profiles
SET
partner_search_profile_enabled = false,
last_partner_search_booking_date = NULL,
updated_at = now()
WHERE user_profile_id = p_user_profile_id;
END IF;
END;
$function$
"
public,refresh_partner_finder_profile_from_bookings,f,"p_user_profile_id uuid, p_club_id uuid","p_user_profile_id uuid, p_club_id uuid",void,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.refresh_partner_finder_profile_from_bookings(p_user_profile_id uuid, p_club_id uuid)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_profile_utr numeric;
v_show_email boolean;
v_show_phone boolean;
v_latest_date date;
v_latest_booking_utr numeric;
v_has_active_search boolean := false;
BEGIN
IF p_user_profile_id IS NULL OR p_club_id IS NULL THEN
RETURN;
END IF;
SELECT
up.utr_rating,
COALESCE(up.show_email_to_players, false),
COALESCE(up.show_phone_to_players, false)
INTO
v_profile_utr,
v_show_email,
v_show_phone
FROM public.user_profiles up
WHERE up.id = p_user_profile_id
AND up.club_id = p_club_id
LIMIT 1;
IF NOT FOUND THEN
RETURN;
END IF;
SELECT
b.booking_date,
b.booking_owner_utr_rating
INTO
v_latest_date,
v_latest_booking_utr
FROM public.bookings b
WHERE b.user_id = p_user_profile_id
AND b.club_id = p_club_id
AND b.is_partner_search_enabled = true
AND b.partner_search_status = 'active'
AND b.status IS DISTINCT FROM 'cancelled'
AND b.status IS DISTINCT FROM 'deleted'
AND b.booking_date >= current_date
AND b.deleted_at IS NULL
ORDER BY
b.booking_date DESC,
b.start_time DESC
LIMIT 1;
v_has_active_search := FOUND;
IF v_has_active_search THEN
INSERT INTO public.partner_finder_profiles (
club_id,
user_profile_id,
utr_rating,
partner_search_profile_enabled,
last_partner_search_booking_date,
show_email_to_players,
show_phone_to_players,
updated_at
)
VALUES (
p_club_id,
p_user_profile_id,
COALESCE(v_latest_booking_utr, v_profile_utr),
true,
v_latest_date,
v_show_email,
v_show_phone,
now()
)
ON CONFLICT (user_profile_id)
DO UPDATE SET
club_id = EXCLUDED.club_id,
utr_rating = EXCLUDED.utr_rating,
partner_search_profile_enabled = true,
last_partner_search_booking_date =
EXCLUDED.last_partner_search_booking_date,
show_email_to_players = EXCLUDED.show_email_to_players,
show_phone_to_players = EXCLUDED.show_phone_to_players,
updated_at = now();
UPDATE public.user_profiles up
SET
partner_search_profile_enabled = true,
updated_at = now()
WHERE up.id = p_user_profile_id
AND up.partner_search_profile_enabled IS DISTINCT FROM true;
ELSE
UPDATE public.partner_finder_profiles pfp
SET
last_partner_search_booking_date = NULL,
updated_at = now()
WHERE pfp.user_profile_id = p_user_profile_id
AND pfp.club_id = p_club_id
AND pfp.last_partner_search_booking_date IS NOT NULL;
END IF;
END;
$function$
"
public,save_academy_attendance,f,"p_club_id uuid, p_booking_id uuid, p_attendance_data jsonb, p_sport text","p_club_id uuid, p_booking_id uuid, p_attendance_data jsonb, p_sport text DEFAULT 'tennis'::text",boolean,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.save_academy_attendance(p_club_id uuid, p_booking_id uuid, p_attendance_data jsonb, p_sport text DEFAULT 'tennis'::text)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_user_profile_id uuid;
v_booking record;
v_local_now timestamp;
v_booking_start timestamp;
v_booking_end timestamp;
v_timezone text;
v_item jsonb;
v_participant_id uuid;
v_status text;
v_old_status text;
v_dummy integer;
v_seen_uuids uuid[] := ARRAY[]::uuid[];
v_sport text;
BEGIN
-- Bejelentkezés ellenőrzése
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Unauthorized: User is not authenticated.';
END IF;
v_sport := lower(
COALESCE(NULLIF(btrim(p_sport), ''), 'tennis')
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION
'Invalid sport. Allowed values: tennis, padel.';
END IF;
-- Foglalás és akadémiai adatok betöltése
SELECT
b.booking_date,
b.start_time,
b.end_time,
b.status,
b.deleted_at,
b.sport,
abd.academy_program_id,
abd.academy_type,
c.timezone
INTO v_booking
FROM public.bookings b
JOIN public.academy_booking_details abd
ON abd.booking_id = b.id
JOIN public.clubs c
ON c.id = b.club_id
WHERE b.id = p_booking_id
AND b.club_id = p_club_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'Booking not found or not an academy booking.';
END IF;
IF v_booking.sport IS DISTINCT FROM v_sport THEN
RAISE EXCEPTION
'Booking belongs to a different sport.';
END IF;
IF NOT EXISTS (
SELECT 1
FROM public.academy_programs ap
WHERE ap.id = v_booking.academy_program_id
AND ap.club_id = p_club_id
AND ap.sport = v_sport
) THEN
RAISE EXCEPTION
'Academy program does not belong to this club and sport.';
END IF;
IF v_booking.deleted_at IS NOT NULL
OR v_booking.status IN (
'cancelled',
'deleted by club',
'deleted'
) THEN
RAISE EXCEPTION
'Booking is cancelled or deleted.';
END IF;
IF v_booking.academy_type NOT IN (
'group',
'camp'
) THEN
RAISE EXCEPTION
'Attendance is only available for group and camp bookings.';
END IF;
IF v_booking.booking_date IS NULL
OR v_booking.start_time IS NULL
OR v_booking.end_time IS NULL THEN
RAISE EXCEPTION
'Booking is missing date or time information.';
END IF;
-- A ténylegesen aktívan becheckolt edző user_profile rekordja.
-- Több duplikált profil esetén is kizárólag a check-inhez
-- kapcsolódó profilt választja ki.
SELECT up.id
INTO v_caller_user_profile_id
FROM public.academy_booking_coach_checkins ci
JOIN public.coach_profiles cp
ON cp.id = ci.coach_profile_id
JOIN public.user_profiles up
ON up.id = cp.user_profile_id
WHERE ci.booking_id = p_booking_id
AND ci.club_id = p_club_id
AND ci.checked_out_at IS NULL
AND cp.is_active = true
AND up.auth_user_id = auth.uid()
AND up.club_id = p_club_id
ORDER BY
ci.checked_in_at ASC,
ci.id ASC
LIMIT 1;
IF v_caller_user_profile_id IS NULL THEN
RAISE EXCEPTION
'Cannot edit attendance: caller is not actively checked in.';
END IF;
-- Időablak kiszámítása
v_timezone :=
COALESCE(
NULLIF(v_booking.timezone, ''),
'UTC'
);
v_local_now :=
(now() AT TIME ZONE v_timezone);
v_booking_start :=
(
v_booking.booking_date
+ v_booking.start_time
)::timestamp;
v_booking_end :=
(
v_booking.booking_date
+ v_booking.end_time
)::timestamp;
IF v_local_now <
v_booking_start - interval '15 minutes' THEN
RAISE EXCEPTION
'Cannot edit attendance: attendance window has not opened yet.';
END IF;
IF v_local_now > v_booking_end THEN
RAISE EXCEPTION
'Cannot edit attendance: attendance window is closed.';
END IF;
-- Payload típusa
IF p_attendance_data IS NULL
OR jsonb_typeof(p_attendance_data) <> 'array' THEN
RAISE EXCEPTION
'Payload must be a JSON array.';
END IF;
-- Egy foglalás jelenléti mentései egyszerre csak egy
-- tranzakcióban futhatnak.
-- Ez megakadályozza két edző egyidejű mentésének versenyhelyzetét.
PERFORM pg_advisory_xact_lock(
hashtextextended(
p_booking_id::text,
0
)
);
-- Jelenléti elemek feldolgozása
FOR v_item IN
SELECT value
FROM jsonb_array_elements(p_attendance_data) AS payload(value)
LOOP
-- Minden elem objektum legyen
IF jsonb_typeof(v_item) <> 'object' THEN
RAISE EXCEPTION
'Payload elements must be JSON objects.';
END IF;
-- Participant ID megléte
IF NOT (v_item ? 'participant_id')
OR v_item->>'participant_id' IS NULL
OR btrim(v_item->>'participant_id') = '' THEN
RAISE EXCEPTION
'Missing or empty participant_id in payload.';
END IF;
-- UUID validálása és normalizálása
BEGIN
v_participant_id :=
(v_item->>'participant_id')::uuid;
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION
'Invalid UUID format for participant_id: %',
v_item->>'participant_id';
END;
-- Status megléte
IF NOT (v_item ? 'status')
OR v_item->>'status' IS NULL
OR btrim(v_item->>'status') = '' THEN
RAISE EXCEPTION
'Missing or empty status for participant %.',
v_participant_id;
END IF;
v_status :=
btrim(v_item->>'status');
IF v_status NOT IN (
'present',
'absent'
) THEN
RAISE EXCEPTION
'Invalid status for participant %. Allowed values: present, absent.',
v_participant_id;
END IF;
-- Duplikált UUID tiltása a batchben
IF v_participant_id = ANY(v_seen_uuids) THEN
RAISE EXCEPTION
'Duplicate participant_id in payload: %.',
v_participant_id;
END IF;
v_seen_uuids :=
array_append(
v_seen_uuids,
v_participant_id
);
-- Résztvevő ellenőrzése és zárolása
SELECT 1
INTO v_dummy
FROM public.academy_program_participants p
WHERE p.id = v_participant_id
AND p.academy_program_id =
v_booking.academy_program_id
AND p.club_id = p_club_id
AND p.is_active = true
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION
'Invalid or inactive participant: %.',
v_participant_id;
END IF;
-- Aktuális státusz lekérése
v_old_status := NULL;
SELECT a.attendance_status
INTO v_old_status
FROM public.academy_attendance a
WHERE a.booking_id = p_booking_id
AND a.participant_id =
v_participant_id;
-- Csak tényleges változás esetén ment és auditál
IF v_old_status IS DISTINCT FROM v_status THEN
INSERT INTO public.academy_attendance (
club_id,
booking_id,
participant_id,
attendance_status,
recorded_by_user_profile_id,
recorded_at,
updated_at
)
VALUES (
p_club_id,
p_booking_id,
v_participant_id,
v_status,
v_caller_user_profile_id,
now(),
now()
)
ON CONFLICT (
booking_id,
participant_id
)
DO UPDATE SET
attendance_status =
EXCLUDED.attendance_status,
recorded_by_user_profile_id =
EXCLUDED.recorded_by_user_profile_id,
recorded_at =
EXCLUDED.recorded_at,
updated_at =
EXCLUDED.updated_at;
INSERT INTO public.academy_attendance_audit_logs (
club_id,
booking_id,
participant_id,
old_status,
new_status,
changed_by_user_profile_id,
changed_at
)
VALUES (
p_club_id,
p_booking_id,
v_participant_id,
v_old_status,
v_status,
v_caller_user_profile_id,
now()
);
END IF;
END LOOP;
RETURN true;
END;
$function$
"
public,save_academy_group_court_cost_setting,f,"p_academy_program_id uuid, p_default_court_fee_huf numeric, p_apply_from date","p_academy_program_id uuid, p_default_court_fee_huf numeric, p_apply_from date DEFAULT NULL::date",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.save_academy_group_court_cost_setting(p_academy_program_id uuid, p_default_court_fee_huf numeric, p_apply_from date DEFAULT NULL::date)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_club_id uuid;
v_program_name text;
v_program_type text;
v_timezone text;
v_today date;
v_apply_from date;
v_actor_user_profile_id uuid;
v_old_fee numeric(12, 2);
v_new_fee numeric(12, 2);
v_updated_occurrence_count integer;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION
'A művelethez bejelentkezés szükséges.';
END IF;
IF p_default_court_fee_huf IS NULL
OR p_default_court_fee_huf < 0 THEN
RAISE EXCEPTION
'A pályadíj nem lehet üres vagy negatív.';
END IF;
v_new_fee :=
round(p_default_court_fee_huf::numeric, 2);
SELECT
ap.club_id,
ap.name,
ap.program_type,
COALESCE(NULLIF(c.timezone, ''), 'UTC')
INTO
v_club_id,
v_program_name,
v_program_type,
v_timezone
FROM public.academy_programs ap
JOIN public.clubs c
ON c.id = ap.club_id
WHERE ap.id = p_academy_program_id
FOR UPDATE OF ap;
IF NOT FOUND THEN
RAISE EXCEPTION
'Az akadémiai csoport nem található.';
END IF;
IF v_program_type <> 'group' THEN
RAISE EXCEPTION
'Pályadíj csak csoportos programhoz állítható be.';
END IF;
IF NOT public.can_manage_academy_payments(v_club_id) THEN
RAISE EXCEPTION
'Nincs jogosultságod a pályaköltség módosításához.';
END IF;
SELECT up.id
INTO v_actor_user_profile_id
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_club_id
AND up.role IN (
'owner',
'admin',
'secondary_admin'
)
LIMIT 1;
IF v_actor_user_profile_id IS NULL THEN
RAISE EXCEPTION
'Nem található jogosult adminisztrátori profil.';
END IF;
v_today :=
(now() AT TIME ZONE v_timezone)::date;
v_apply_from :=
COALESCE(p_apply_from, v_today);
IF v_apply_from < v_today THEN
RAISE EXCEPTION
'A pályadíj nem alkalmazható visszamenőleges dátumtól.';
END IF;
SELECT s.default_court_fee_huf
INTO v_old_fee
FROM public.academy_group_court_cost_settings s
WHERE s.academy_program_id = p_academy_program_id
AND s.club_id = v_club_id
FOR UPDATE;
v_old_fee := COALESCE(v_old_fee, 0);
INSERT INTO public.academy_group_court_cost_settings (
academy_program_id,
club_id,
default_court_fee_huf,
created_by_user_profile_id,
updated_by_user_profile_id
)
VALUES (
p_academy_program_id,
v_club_id,
v_new_fee,
v_actor_user_profile_id,
v_actor_user_profile_id
)
ON CONFLICT (academy_program_id)
DO UPDATE
SET
default_court_fee_huf =
EXCLUDED.default_court_fee_huf,
updated_by_user_profile_id =
EXCLUDED.updated_by_user_profile_id,
updated_at = now();
UPDATE public.academy_booking_details abd
SET
court_fee_huf = v_new_fee,
updated_at = now()
FROM public.bookings b
WHERE b.id = abd.booking_id
AND abd.club_id = v_club_id
AND abd.academy_program_id =
p_academy_program_id
AND abd.academy_type = 'group'
AND abd.cancelled_at IS NULL
AND lower(
COALESCE(
abd.occurrence_status,
'scheduled'
)
) NOT IN (
'cancelled',
'deleted',
'failed'
)
AND b.deleted_at IS NULL
AND lower(COALESCE(b.status, ''))
NOT IN (
'cancelled',
'deleted',
'deleted by club',
'failed'
)
AND b.booking_date >= v_apply_from
AND (
(b.booking_date + b.start_time)
AT TIME ZONE v_timezone
) > now();
GET DIAGNOSTICS
v_updated_occurrence_count = ROW_COUNT;
RETURN jsonb_build_object(
'success',
true,
'club_id',
v_club_id,
'academy_program_id',
p_academy_program_id,
'program_name',
v_program_name,
'old_court_fee_huf',
v_old_fee,
'new_court_fee_huf',
v_new_fee,
'apply_from',
v_apply_from,
'updated_future_occurrence_count',
v_updated_occurrence_count,
'currency',
'HUF'
);
END;
$function$
"
public,save_academy_participant,f,"p_participant_id uuid, p_club_id uuid, p_program_id uuid, p_last_name text, p_first_name text, p_birth_year integer, p_ctr numeric, p_is_active boolean, p_monthly_fee_huf numeric, p_camp_fee_type text, p_camp_fee_huf numeric, p_internal_note text, p_sport text","p_participant_id uuid, p_club_id uuid, p_program_id uuid, p_last_name text, p_first_name text, p_birth_year integer, p_ctr numeric, p_is_active boolean, p_monthly_fee_huf numeric, p_camp_fee_type text, p_camp_fee_huf numeric, p_internal_note text, p_sport text DEFAULT 'tennis'::text",uuid,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.save_academy_participant(p_participant_id uuid, p_club_id uuid, p_program_id uuid, p_last_name text, p_first_name text, p_birth_year integer, p_ctr numeric, p_is_active boolean, p_monthly_fee_huf numeric, p_camp_fee_type text, p_camp_fee_huf numeric, p_internal_note text, p_sport text DEFAULT 'tennis'::text)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_profile_id uuid;
v_participant_id uuid;
v_program_type text;
v_sport text;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Nincs bejelentkezett felhasználó.';
END IF;
v_sport := lower(
COALESCE(NULLIF(btrim(p_sport), ''), 'tennis')
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION
'Érvénytelen sport. Engedélyezett értékek: tennis, padel.';
END IF;
IF NOT public.can_manage_academy(p_club_id) THEN
RAISE EXCEPTION
'Nincs jogosultságod az akadémiai résztvevők kezeléséhez.';
END IF;
SELECT up.id
INTO v_caller_profile_id
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = p_club_id
ORDER BY up.id
LIMIT 1;
IF v_caller_profile_id IS NULL THEN
RAISE EXCEPTION
'A klubhoz tartozó felhasználói profil nem található.';
END IF;
SELECT ap.program_type
INTO v_program_type
FROM public.academy_programs ap
WHERE ap.id = p_program_id
AND ap.club_id = p_club_id
AND ap.sport = v_sport;
IF NOT FOUND THEN
RAISE EXCEPTION
'A program nem található ebben a klubban és sportban.';
END IF;
IF p_birth_year < 1900
OR p_birth_year > extract(year from current_date)::integer THEN
RAISE EXCEPTION 'Érvénytelen születési év.';
END IF;
IF p_ctr IS NOT NULL AND p_ctr < 0 THEN
RAISE EXCEPTION 'A rating nem lehet negatív.';
END IF;
IF v_program_type = 'group' THEN
IF p_monthly_fee_huf IS NULL OR p_monthly_fee_huf < 0 THEN
RAISE EXCEPTION
'Csoportos játékosnál érvényes havi díj szükséges.';
END IF;
IF p_camp_fee_type IS NOT NULL
OR p_camp_fee_huf IS NOT NULL THEN
RAISE EXCEPTION
'Csoportos játékoshoz nem tartozhat tábori díj.';
END IF;
ELSIF v_program_type = 'camp' THEN
IF p_monthly_fee_huf IS NOT NULL THEN
RAISE EXCEPTION
'Tábori játékoshoz nem tartozhat havi csoportdíj.';
END IF;
IF p_camp_fee_type NOT IN ('daily', 'weekly')
OR p_camp_fee_huf IS NULL
OR p_camp_fee_huf < 0 THEN
RAISE EXCEPTION
'Tábori játékosnál napi vagy heti díj szükséges.';
END IF;
END IF;
IF p_participant_id IS NULL THEN
INSERT INTO public.academy_program_participants (
club_id,
academy_program_id,
last_name,
first_name,
birth_year,
ctr,
is_active,
monthly_fee_huf,
camp_fee_type,
camp_fee_huf,
internal_note,
created_by_user_profile_id
)
VALUES (
p_club_id,
p_program_id,
btrim(p_last_name),
btrim(p_first_name),
p_birth_year,
p_ctr,
COALESCE(p_is_active, true),
p_monthly_fee_huf,
p_camp_fee_type,
p_camp_fee_huf,
NULLIF(btrim(p_internal_note), ''),
v_caller_profile_id
)
RETURNING id
INTO v_participant_id;
ELSE
UPDATE public.academy_program_participants app
SET
last_name = btrim(p_last_name),
first_name = btrim(p_first_name),
birth_year = p_birth_year,
ctr = p_ctr,
is_active = COALESCE(p_is_active, app.is_active),
left_on = CASE
WHEN COALESCE(p_is_active, app.is_active) = true
THEN NULL
WHEN app.left_on IS NULL
THEN current_date
ELSE app.left_on
END,
monthly_fee_huf = p_monthly_fee_huf,
camp_fee_type = p_camp_fee_type,
camp_fee_huf = p_camp_fee_huf,
internal_note = NULLIF(btrim(p_internal_note), ''),
updated_at = now()
WHERE app.id = p_participant_id
AND app.club_id = p_club_id
AND app.academy_program_id = p_program_id
RETURNING app.id
INTO v_participant_id;
IF v_participant_id IS NULL THEN
RAISE EXCEPTION
'A résztvevő nem található, vagy nem ehhez a programhoz tartozik.';
END IF;
END IF;
RETURN v_participant_id;
END;
$function$
"
public,save_academy_program,f,"p_program_id uuid, p_club_id uuid, p_program_type text, p_name text, p_description text, p_is_active boolean, p_sport text","p_program_id uuid, p_club_id uuid, p_program_type text, p_name text, p_description text, p_is_active boolean, p_sport text DEFAULT 'tennis'::text",uuid,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.save_academy_program(p_program_id uuid, p_club_id uuid, p_program_type text, p_name text, p_description text, p_is_active boolean, p_sport text DEFAULT 'tennis'::text)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_profile_id uuid;
v_program_id uuid;
v_existing_program_type text;
v_existing_sport text;
v_sport text;
BEGIN
-- ========================================================
-- TENNIS SAFETY GATE
--
-- Existing calls without p_sport remain Tennis.
-- Padel must explicitly pass p_sport='padel'.
-- Existing program sport can NEVER be changed here.
-- ========================================================
IF auth.uid() IS NULL THEN
RAISE EXCEPTION
'Nincs bejelentkezett felhasználó.';
END IF;
IF p_club_id IS NULL THEN
RAISE EXCEPTION
'A klubazonosító kötelező.';
END IF;
v_sport :=
lower(
COALESCE(
NULLIF(btrim(p_sport), ''),
'tennis'
)
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION
'Érvénytelen sport. Engedélyezett értékek: tennis, padel.';
END IF;
IF p_program_type NOT IN ('group', 'camp') THEN
RAISE EXCEPTION
'Érvénytelen akadémiai programtípus.';
END IF;
IF NULLIF(btrim(p_name), '') IS NULL THEN
RAISE EXCEPTION
'A program neve kötelező.';
END IF;
IF NOT public.can_manage_academy(p_club_id) THEN
RAISE EXCEPTION
'Nincs jogosultságod az akadémiai adatok kezeléséhez.';
END IF;
SELECT up.id
INTO v_caller_profile_id
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = p_club_id
ORDER BY up.id
LIMIT 1;
IF v_caller_profile_id IS NULL THEN
RAISE EXCEPTION
'A klubhoz tartozó felhasználói profil nem található.';
END IF;
-- ========================================================
-- INSERT
-- ========================================================
IF p_program_id IS NULL THEN
INSERT INTO public.academy_programs (
club_id,
program_type,
name,
description,
is_active,
created_by_user_profile_id,
sport
)
VALUES (
p_club_id,
p_program_type,
btrim(p_name),
NULLIF(btrim(p_description), ''),
COALESCE(p_is_active, true),
v_caller_profile_id,
v_sport
)
RETURNING id
INTO v_program_id;
-- ========================================================
-- UPDATE
-- ========================================================
ELSE
SELECT
ap.program_type,
ap.sport
INTO
v_existing_program_type,
v_existing_sport
FROM public.academy_programs ap
WHERE ap.id = p_program_id
AND ap.club_id = p_club_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'A program nem található, vagy másik klubhoz tartozik.';
END IF;
-- ====================================================
-- CRITICAL CROSS-SPORT PROTECTION
--
-- Egy Tennis program Padel hívással nem módosítható.
-- Egy Padel program Tennis hívással nem módosítható.
-- A sport létrehozás után ezen RPC-n keresztül nem
-- változtatható meg.
-- ====================================================
IF v_existing_sport <> v_sport THEN
RAISE EXCEPTION
'A program másik sporthoz tartozik. Cross-sport módosítás tiltva.';
END IF;
-- Csoportot nem alakítunk át táborrá,
-- vagy fordítva.
IF v_existing_program_type <> p_program_type THEN
RAISE EXCEPTION
'A program típusa létrehozás után nem módosítható.';
END IF;
UPDATE public.academy_programs
SET
name = btrim(p_name),
description = NULLIF(btrim(p_description), ''),
is_active = COALESCE(p_is_active, is_active),
updated_at = now()
WHERE id = p_program_id
AND club_id = p_club_id
AND sport = v_sport
RETURNING id
INTO v_program_id;
IF v_program_id IS NULL THEN
RAISE EXCEPTION
'A program módosítása sikertelen vagy sportütközés történt.';
END IF;
END IF;
RETURN v_program_id;
END;
$function$
"
public,save_academy_program_coach,f,"p_academy_program_id uuid, p_coach_profile_id uuid, p_amount_huf numeric, p_effective_from date","p_academy_program_id uuid, p_coach_profile_id uuid, p_amount_huf numeric, p_effective_from date DEFAULT NULL::date",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.save_academy_program_coach(p_academy_program_id uuid, p_coach_profile_id uuid, p_amount_huf numeric, p_effective_from date DEFAULT NULL::date)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_club_id uuid;
v_program_type text;
v_program_is_active boolean;
v_timezone text;
v_today date;
v_effective_from date;
v_actor_user_profile_id uuid;
v_coach_club_id uuid;
v_coach_is_active boolean;
v_assignment
public.academy_program_coach_assignments%ROWTYPE;
v_latest_rate
public.academy_coach_compensation_rates%ROWTYPE;
v_overlapping_rate
public.academy_coach_compensation_rates%ROWTYPE;
v_new_rate
public.academy_coach_compensation_rates%ROWTYPE;
v_old_values jsonb;
v_assignment_created boolean := false;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION
'A művelethez bejelentkezés szükséges.';
END IF;
IF p_amount_huf IS NULL OR p_amount_huf < 0 THEN
RAISE EXCEPTION
'Az edzői díj nem lehet üres vagy negatív.';
END IF;
SELECT
ap.club_id,
ap.program_type,
ap.is_active,
COALESCE(NULLIF(c.timezone, ''), 'UTC')
INTO
v_club_id,
v_program_type,
v_program_is_active,
v_timezone
FROM public.academy_programs ap
JOIN public.clubs c
ON c.id = ap.club_id
WHERE ap.id = p_academy_program_id
FOR UPDATE OF ap;
IF NOT FOUND THEN
RAISE EXCEPTION
'Az akadémiai program nem található.';
END IF;
IF v_program_type <> 'group' THEN
RAISE EXCEPTION
'Edző jelenleg csak csoportos akadémiai programhoz rendelhető.';
END IF;
IF NOT v_program_is_active THEN
RAISE EXCEPTION
'Archivált csoporthoz nem rendelhető új edző vagy díj.';
END IF;
IF NOT public.can_manage_academy_payments(v_club_id) THEN
RAISE EXCEPTION
'Nincs jogosultságod az edzők és díjazásuk kezeléséhez.';
END IF;
SELECT up.id
INTO v_actor_user_profile_id
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = v_club_id
AND up.role IN (
'owner',
'admin',
'secondary_admin'
)
LIMIT 1;
IF v_actor_user_profile_id IS NULL THEN
RAISE EXCEPTION
'Nem található jogosult adminisztrátori profil.';
END IF;
SELECT
up.club_id,
cp.is_active
INTO
v_coach_club_id,
v_coach_is_active
FROM public.coach_profiles cp
JOIN public.user_profiles up
ON up.id = cp.user_profile_id
WHERE cp.id = p_coach_profile_id
FOR UPDATE OF cp;
IF NOT FOUND THEN
RAISE EXCEPTION
'Az edzői profil nem található vagy nincs felhasználói profilhoz kapcsolva.';
END IF;
IF v_coach_club_id <> v_club_id THEN
RAISE EXCEPTION
'Az edző nem ugyanahhoz a klubhoz tartozik, mint a csoport.';
END IF;
IF NOT v_coach_is_active THEN
RAISE EXCEPTION
'Inaktív edző nem rendelhető csoporthoz.';
END IF;
v_today :=
(now() AT TIME ZONE v_timezone)::date;
v_effective_from :=
COALESCE(p_effective_from, v_today);
-- Megkeressük azt a hozzárendelési időszakot,
-- amely tartalmazza az új díj kezdőnapját.
SELECT a.*
INTO v_assignment
FROM public.academy_program_coach_assignments a
WHERE a.academy_program_id =
p_academy_program_id
AND a.coach_profile_id =
p_coach_profile_id
AND a.is_void = false
AND a.assigned_from <=
v_effective_from
AND (
a.assigned_to IS NULL
OR a.assigned_to >=
v_effective_from
)
ORDER BY a.assigned_from DESC
LIMIT 1
FOR UPDATE;
-- Ha nincs ilyen időszak, új hozzárendelést hozunk létre.
IF NOT FOUND THEN
INSERT INTO
public.academy_program_coach_assignments (
club_id,
academy_program_id,
coach_profile_id,
assigned_from,
assigned_to,
is_void,
created_by_user_profile_id,
updated_by_user_profile_id
)
VALUES (
v_club_id,
p_academy_program_id,
p_coach_profile_id,
v_effective_from,
NULL,
false,
v_actor_user_profile_id,
v_actor_user_profile_id
)
RETURNING *
INTO v_assignment;
v_assignment_created := true;
INSERT INTO
public.academy_coach_compensation_audit_logs (
club_id,
academy_program_id,
coach_profile_id,
assignment_id,
action_type,
old_values,
new_values,
changed_by_user_profile_id
)
VALUES (
v_club_id,
p_academy_program_id,
p_coach_profile_id,
v_assignment.id,
'assignment_created',
NULL,
to_jsonb(v_assignment),
v_actor_user_profile_id
);
END IF;
-- Az adott hozzárendelés legkésőbbi aktív díja.
SELECT r.*
INTO v_latest_rate
FROM public.academy_coach_compensation_rates r
WHERE r.assignment_id = v_assignment.id
AND r.is_void = false
ORDER BY r.effective_from DESC
LIMIT 1
FOR UPDATE;
-- Első díj a hozzárendeléshez.
IF NOT FOUND THEN
INSERT INTO
public.academy_coach_compensation_rates (
club_id,
assignment_id,
compensation_type,
amount_huf,
effective_from,
effective_to,
is_void,
created_by_user_profile_id,
updated_by_user_profile_id
)
VALUES (
v_club_id,
v_assignment.id,
'per_session',
round(p_amount_huf::numeric, 2),
v_effective_from,
v_assignment.assigned_to,
false,
v_actor_user_profile_id,
v_actor_user_profile_id
)
RETURNING *
INTO v_new_rate;
INSERT INTO
public.academy_coach_compensation_audit_logs (
club_id,
academy_program_id,
coach_profile_id,
assignment_id,
compensation_rate_id,
action_type,
old_values,
new_values,
changed_by_user_profile_id
)
VALUES (
v_club_id,
p_academy_program_id,
p_coach_profile_id,
v_assignment.id,
v_new_rate.id,
'rate_created',
NULL,
to_jsonb(v_new_rate),
v_actor_user_profile_id
);
ELSIF v_effective_from <
v_latest_rate.effective_from THEN
RAISE EXCEPTION
'A megadott díj kezdőnapja korábbi egy már rögzített díj kezdőnapjánál. Korábbi díj utólagos beszúrása nem engedélyezett.';
ELSIF v_effective_from =
v_latest_rate.effective_from THEN
-- Ugyanaz az összeg: idempotens siker.
IF v_latest_rate.amount_huf =
round(p_amount_huf::numeric, 2) THEN
v_new_rate := v_latest_rate;
ELSE
-- Múltban már megkezdődött díjat nem írunk felül.
IF v_effective_from < v_today THEN
RAISE EXCEPTION
'Már megkezdődött historikus díj összege nem írható felül. Új, mai vagy jövőbeli kezdőnappal hozz létre díjat.';
END IF;
v_old_values := to_jsonb(v_latest_rate);
UPDATE public.academy_coach_compensation_rates
SET
amount_huf =
round(p_amount_huf::numeric, 2),
updated_by_user_profile_id =
v_actor_user_profile_id
WHERE id = v_latest_rate.id
RETURNING *
INTO v_new_rate;
INSERT INTO
public.academy_coach_compensation_audit_logs (
club_id,
academy_program_id,
coach_profile_id,
assignment_id,
compensation_rate_id,
action_type,
old_values,
new_values,
changed_by_user_profile_id
)
VALUES (
v_club_id,
p_academy_program_id,
p_coach_profile_id,
v_assignment.id,
v_new_rate.id,
'rate_updated',
v_old_values,
to_jsonb(v_new_rate),
v_actor_user_profile_id
);
END IF;
ELSE
-- Már van díjtörténet. Új historikus díj visszamenőleg
-- nem hozható létre.
IF v_effective_from < v_today THEN
RAISE EXCEPTION
'Új díj csak a mai naptól vagy jövőbeli dátumtól hozható létre.';
END IF;
-- Lezárjuk azt a díjat, amely átfedne az újjal.
SELECT r.*
INTO v_overlapping_rate
FROM public.academy_coach_compensation_rates r
WHERE r.assignment_id = v_assignment.id
AND r.is_void = false
AND r.effective_from <
v_effective_from
AND (
r.effective_to IS NULL
OR r.effective_to >=
v_effective_from
)
ORDER BY r.effective_from DESC
LIMIT 1
FOR UPDATE;
IF FOUND THEN
v_old_values :=
to_jsonb(v_overlapping_rate);
UPDATE public.academy_coach_compensation_rates
SET
effective_to =
v_effective_from - 1,
updated_by_user_profile_id =
v_actor_user_profile_id
WHERE id = v_overlapping_rate.id
RETURNING *
INTO v_overlapping_rate;
INSERT INTO
public.academy_coach_compensation_audit_logs (
club_id,
academy_program_id,
coach_profile_id,
assignment_id,
compensation_rate_id,
action_type,
old_values,
new_values,
changed_by_user_profile_id
)
VALUES (
v_club_id,
p_academy_program_id,
p_coach_profile_id,
v_assignment.id,
v_overlapping_rate.id,
'rate_closed',
v_old_values,
to_jsonb(v_overlapping_rate),
v_actor_user_profile_id
);
END IF;
INSERT INTO
public.academy_coach_compensation_rates (
club_id,
assignment_id,
compensation_type,
amount_huf,
effective_from,
effective_to,
is_void,
created_by_user_profile_id,
updated_by_user_profile_id
)
VALUES (
v_club_id,
v_assignment.id,
'per_session',
round(p_amount_huf::numeric, 2),
v_effective_from,
v_assignment.assigned_to,
false,
v_actor_user_profile_id,
v_actor_user_profile_id
)
RETURNING *
INTO v_new_rate;
INSERT INTO
public.academy_coach_compensation_audit_logs (
club_id,
academy_program_id,
coach_profile_id,
assignment_id,
compensation_rate_id,
action_type,
old_values,
new_values,
changed_by_user_profile_id
)
VALUES (
v_club_id,
p_academy_program_id,
p_coach_profile_id,
v_assignment.id,
v_new_rate.id,
'rate_created',
NULL,
to_jsonb(v_new_rate),
v_actor_user_profile_id
);
END IF;
RETURN jsonb_build_object(
'success', true,
'assignment_created',
v_assignment_created,
'club_id',
v_club_id,
'academy_program_id',
p_academy_program_id,
'coach_profile_id',
p_coach_profile_id,
'assignment_id',
v_assignment.id,
'assigned_from',
v_assignment.assigned_from,
'assigned_to',
v_assignment.assigned_to,
'rate_id',
v_new_rate.id,
'amount_huf',
v_new_rate.amount_huf,
'effective_from',
v_new_rate.effective_from,
'effective_to',
v_new_rate.effective_to
);
END;
$function$
"
public,save_coach_settings,f,"p_coach_profile_id uuid, p_club_id uuid, p_user_profile_id uuid, p_display_name text, p_color text, p_phone text, p_email text, p_min_advance_hours integer, p_is_active boolean, p_availability jsonb, p_sport text","p_coach_profile_id uuid, p_club_id uuid, p_user_profile_id uuid, p_display_name text, p_color text, p_phone text, p_email text, p_min_advance_hours integer, p_is_active boolean, p_availability jsonb, p_sport text",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.save_coach_settings(p_coach_profile_id uuid, p_club_id uuid, p_user_profile_id uuid, p_display_name text, p_color text, p_phone text, p_email text, p_min_advance_hours integer, p_is_active boolean, p_availability jsonb, p_sport text)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_profile_id uuid;
v_caller_role text;
v_target_auth_user_id uuid;
v_coach_id uuid;
v_matching_coach_count integer;
v_availability_record jsonb;
v_start_time time;
v_end_time time;
v_day_of_week integer;
v_is_active_avail boolean;
v_sport text;
BEGIN
-- ========================================================
-- 1. AUTH
-- ========================================================
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Not authenticated';
END IF;
-- ========================================================
-- 2. SPORT VALIDATION
-- ========================================================
IF p_sport IS NULL
OR lower(btrim(p_sport)) NOT IN ('tennis', 'padel')
THEN
RAISE EXCEPTION
'p_sport is required and must be tennis or padel';
END IF;
v_sport := lower(btrim(p_sport));
-- ========================================================
-- 3. CALLER PROFILE IN REQUESTED CLUB
-- ========================================================
SELECT
up.id,
up.role
INTO
v_caller_profile_id,
v_caller_role
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
AND up.club_id = p_club_id
LIMIT 1;
IF v_caller_profile_id IS NULL THEN
RAISE EXCEPTION
'Caller profile not found in this club';
END IF;
-- ========================================================
-- 4. TARGET USER PROFILE
--
-- Resolve the auth.users ID from user_profiles.
-- coach_profiles.user_id is NOT NULL and references auth.users.
-- Club membership is validated HERE, not on coach_profiles.
-- ========================================================
SELECT
up.auth_user_id
INTO
v_target_auth_user_id
FROM public.user_profiles up
WHERE up.id = p_user_profile_id
AND up.club_id = p_club_id
LIMIT 1;
IF v_target_auth_user_id IS NULL THEN
RAISE EXCEPTION
'Target user profile not found in this club or has no auth user';
END IF;
-- ========================================================
-- 5. AUTHORIZATION
-- ========================================================
IF v_caller_role NOT IN (
'owner',
'admin',
'secondary_admin'
)
AND v_caller_profile_id IS DISTINCT FROM p_user_profile_id
THEN
RAISE EXCEPTION
'Unauthorized to modify this coach profile';
END IF;
-- ========================================================
-- 6. SAFETY CHECK FOR LEGACY / INCONSISTENT COACH ROWS
-- There must not be two separate coach_profiles rows where
-- one matches user_id and another matches user_profile_id.
-- ========================================================
SELECT COUNT(*)
INTO v_matching_coach_count
FROM public.coach_profiles cp
WHERE cp.user_profile_id = p_user_profile_id
OR cp.user_id = v_target_auth_user_id;
IF v_matching_coach_count > 1 THEN
RAISE EXCEPTION
'Multiple coach profiles found for this user. Resolve duplicate coach profile data before saving.';
END IF;
-- ========================================================
-- 7. RESOLVE EXISTING COACH PROFILE
-- ========================================================
IF p_coach_profile_id IS NOT NULL
AND p_coach_profile_id <>
'00000000-0000-0000-0000-000000000000'::uuid
THEN
SELECT cp.id
INTO v_coach_id
FROM public.coach_profiles cp
WHERE cp.id = p_coach_profile_id
AND (
cp.user_profile_id = p_user_profile_id
OR cp.user_id = v_target_auth_user_id
)
LIMIT 1;
IF v_coach_id IS NULL THEN
RAISE EXCEPTION
'Coach profile not found or does not belong to target user';
END IF;
ELSE
SELECT cp.id
INTO v_coach_id
FROM public.coach_profiles cp
WHERE cp.user_profile_id = p_user_profile_id
OR cp.user_id = v_target_auth_user_id
LIMIT 1;
END IF;
-- ========================================================
-- 8. CREATE / UPDATE SHARED COACH PROFILE
--
-- coach_profiles is account-level.
-- Availability alone is sport-specific.
-- ========================================================
IF v_coach_id IS NULL THEN
INSERT INTO public.coach_profiles (
user_id,
user_profile_id,
display_name,
color,
phone,
email,
min_advance_hours,
is_active
)
VALUES (
v_target_auth_user_id,
p_user_profile_id,
p_display_name,
COALESCE(NULLIF(btrim(p_color), ''), '#2563eb'),
p_phone,
p_email,
p_min_advance_hours,
p_is_active
)
RETURNING id
INTO v_coach_id;
ELSE
UPDATE public.coach_profiles
SET
user_id = v_target_auth_user_id,
user_profile_id = p_user_profile_id,
display_name = p_display_name,
color = COALESCE(
NULLIF(btrim(p_color), ''),
color
),
phone = p_phone,
email = p_email,
min_advance_hours = p_min_advance_hours,
is_active = p_is_active,
updated_at = now()
WHERE id = v_coach_id;
END IF;
-- ========================================================
-- 9. DELETE AVAILABILITY ONLY FOR SELECTED SPORT
-- ========================================================
DELETE FROM public.coach_availability
WHERE coach_id = v_coach_id
AND sport = v_sport;
-- ========================================================
-- 10. INSERT SELECTED SPORT AVAILABILITY
-- ========================================================
IF p_availability IS NOT NULL THEN
IF jsonb_typeof(p_availability) <> 'array' THEN
RAISE EXCEPTION
'Availability must be a JSON array';
END IF;
FOR v_availability_record IN
SELECT value
FROM jsonb_array_elements(p_availability)
LOOP
IF NOT (
v_availability_record ? 'day_of_week'
AND v_availability_record ? 'start_time'
AND v_availability_record ? 'end_time'
)
THEN
RAISE EXCEPTION
'Every availability must contain day_of_week, start_time and end_time';
END IF;
BEGIN
v_day_of_week :=
(v_availability_record->>'day_of_week')::integer;
v_start_time :=
(v_availability_record->>'start_time')::time;
v_end_time :=
(v_availability_record->>'end_time')::time;
v_is_active_avail :=
COALESCE(
(v_availability_record->>'is_active')::boolean,
true
);
EXCEPTION
WHEN invalid_text_representation
OR datetime_field_overflow
THEN
RAISE EXCEPTION
'Invalid coach availability value';
END;
IF v_day_of_week NOT BETWEEN 0 AND 6 THEN
RAISE EXCEPTION
'day_of_week must be between 0 and 6';
END IF;
IF v_start_time >= v_end_time THEN
RAISE EXCEPTION
'Start time must be before end time';
END IF;
INSERT INTO public.coach_availability (
coach_id,
sport,
day_of_week,
start_time,
end_time,
is_active
)
VALUES (
v_coach_id,
v_sport,
v_day_of_week,
v_start_time,
v_end_time,
v_is_active_avail
)
ON CONFLICT (
coach_id,
sport,
day_of_week,
start_time,
end_time
)
DO UPDATE SET
is_active = EXCLUDED.is_active,
updated_at = now();
END LOOP;
END IF;
RETURN jsonb_build_object(
'success', true,
'coach_id', v_coach_id,
'sport', v_sport
);
END;
$function$
"
public,set_academy_coach_compensation_updated_at,f,,,trigger,plpgsql,false,VOLATILE,postgres,"{=X/postgres,postgres=X/postgres,anon=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.set_academy_coach_compensation_updated_at()
RETURNS trigger
LANGUAGE plpgsql
SET search_path TO 'public', 'pg_temp'
AS $function$
BEGIN
NEW.updated_at := now();
RETURN NEW;
END;
$function$
"
public,set_academy_group_court_cost_updated_at,f,,,trigger,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.set_academy_group_court_cost_updated_at()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
BEGIN
NEW.updated_at := now();
RETURN NEW;
END;
$function$
"
public,set_ghl_contact_sync_updated_at,f,,,trigger,plpgsql,false,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.set_ghl_contact_sync_updated_at()
RETURNS trigger
LANGUAGE plpgsql
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
BEGIN
NEW.updated_at := now();
RETURN NEW;
END;
$function$
"
public,set_updated_at,f,,,trigger,plpgsql,false,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.set_updated_at()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
NEW.updated_at := now();
RETURN NEW;
END;
$function$
"
public,sync_partner_finder_profile_from_booking,f,,,trigger,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.sync_partner_finder_profile_from_booking()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
BEGIN
IF TG_OP = 'DELETE' THEN
PERFORM public.refresh_partner_finder_profile_from_bookings(
OLD.user_id,
OLD.club_id
);
RETURN OLD;
END IF;
IF TG_OP = 'UPDATE' THEN
IF OLD.user_id IS DISTINCT FROM NEW.user_id
OR OLD.club_id IS DISTINCT FROM NEW.club_id
THEN
PERFORM public.refresh_partner_finder_profile_from_bookings(
OLD.user_id,
OLD.club_id
);
END IF;
END IF;
PERFORM public.refresh_partner_finder_profile_from_bookings(
NEW.user_id,
NEW.club_id
);
RETURN NEW;
END;
$function$
"
public,touch_academy_booking_updated_at,f,,,trigger,plpgsql,false,VOLATILE,postgres,"{=X/postgres,postgres=X/postgres,anon=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.touch_academy_booking_updated_at()
RETURNS trigger
LANGUAGE plpgsql
SET search_path TO 'public', 'pg_temp'
AS $function$
BEGIN
NEW.updated_at := now();
RETURN NEW;
END;
$function$
"
public,update_academy_camp_daily_payment,f,"p_club_id uuid, p_participant_id uuid, p_attendance_date date, p_is_paid boolean, p_expected_fee_huf numeric, p_paid_amount_huf numeric, p_paid_on date, p_sport text","p_club_id uuid, p_participant_id uuid, p_attendance_date date, p_is_paid boolean, p_expected_fee_huf numeric, p_paid_amount_huf numeric, p_paid_on date, p_sport text DEFAULT 'tennis'::text",uuid,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.update_academy_camp_daily_payment(p_club_id uuid, p_participant_id uuid, p_attendance_date date, p_is_paid boolean, p_expected_fee_huf numeric, p_paid_amount_huf numeric, p_paid_on date, p_sport text DEFAULT 'tennis'::text)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_id uuid;
v_payment_id uuid;
v_camp_fee_type text;
v_old_is_paid boolean;
v_old_expected_fee numeric;
v_old_paid_amount numeric;
v_old_paid_on date;
v_sport text;
BEGIN
v_sport := lower(
COALESCE(NULLIF(btrim(p_sport), ''), 'tennis')
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION 'Érvénytelen sport.';
END IF;
IF NOT public.can_manage_academy_payments(p_club_id) THEN
RAISE EXCEPTION 'Unauthorized';
END IF;
SELECT id
INTO v_caller_id
FROM public.user_profiles
WHERE auth_user_id = auth.uid()
AND club_id = p_club_id
LIMIT 1;
IF v_caller_id IS NULL THEN
RAISE EXCEPTION 'Caller profile not found in club';
END IF;
PERFORM 1
FROM public.academy_program_participants app
JOIN public.academy_programs ap
ON ap.id = app.academy_program_id
AND ap.club_id = app.club_id
WHERE app.id = p_participant_id
AND app.club_id = p_club_id
AND ap.sport = v_sport
FOR UPDATE OF app;
IF NOT FOUND THEN
RAISE EXCEPTION
'Participant not found in club/sport';
END IF;
SELECT camp_fee_type
INTO v_camp_fee_type
FROM public.academy_program_participants
WHERE id = p_participant_id
AND club_id = p_club_id;
IF v_camp_fee_type <> 'daily' THEN
RAISE EXCEPTION
'Participant fee type is not daily. Cannot save daily payment.';
END IF;
IF EXISTS (
SELECT 1
FROM public.academy_camp_weekly_payments
WHERE participant_id = p_participant_id
) THEN
RAISE EXCEPTION
'Participant already has a weekly payment. Cannot save daily payment.';
END IF;
SELECT
is_paid,
expected_fee_huf,
paid_amount_huf,
paid_on
INTO
v_old_is_paid,
v_old_expected_fee,
v_old_paid_amount,
v_old_paid_on
FROM public.academy_camp_daily_payments
WHERE participant_id = p_participant_id
AND attendance_date = p_attendance_date;
INSERT INTO public.academy_camp_daily_payments (
club_id,
participant_id,
attendance_date,
is_paid,
expected_fee_huf,
paid_amount_huf,
paid_on,
updated_by_user_profile_id
)
VALUES (
p_club_id,
p_participant_id,
p_attendance_date,
p_is_paid,
p_expected_fee_huf,
p_paid_amount_huf,
p_paid_on,
v_caller_id
)
ON CONFLICT (participant_id, attendance_date)
DO UPDATE SET
is_paid = EXCLUDED.is_paid,
expected_fee_huf = EXCLUDED.expected_fee_huf,
paid_amount_huf = EXCLUDED.paid_amount_huf,
paid_on = EXCLUDED.paid_on,
updated_by_user_profile_id =
EXCLUDED.updated_by_user_profile_id,
updated_at = now()
RETURNING id
INTO v_payment_id;
IF COALESCE(v_old_is_paid, false) <> p_is_paid
OR COALESCE(v_old_expected_fee, -1)
<> COALESCE(p_expected_fee_huf, -1)
OR COALESCE(v_old_paid_amount, -1)
<> COALESCE(p_paid_amount_huf, -1)
OR COALESCE(v_old_paid_on, '1900-01-01'::date)
<> COALESCE(p_paid_on, '1900-01-01'::date)
THEN
INSERT INTO public.academy_payment_audit_logs (
club_id,
participant_id,
action_type,
period_identifier,
camp_fee_type,
old_is_paid,
new_is_paid,
old_expected_fee,
new_expected_fee,
old_paid_amount,
new_paid_amount,
old_paid_on,
new_paid_on,
updated_by_user_profile_id
)
VALUES (
p_club_id,
p_participant_id,
'camp_daily_payment_update',
p_attendance_date::text,
'daily',
v_old_is_paid,
p_is_paid,
v_old_expected_fee,
p_expected_fee_huf,
v_old_paid_amount,
p_paid_amount_huf,
v_old_paid_on,
p_paid_on,
v_caller_id
);
END IF;
RETURN v_payment_id;
END;
$function$
"
public,update_academy_camp_weekly_payment,f,"p_club_id uuid, p_participant_id uuid, p_is_paid boolean, p_expected_fee_huf numeric, p_paid_amount_huf numeric, p_paid_on date, p_internal_note text, p_sport text","p_club_id uuid, p_participant_id uuid, p_is_paid boolean, p_expected_fee_huf numeric, p_paid_amount_huf numeric, p_paid_on date, p_internal_note text, p_sport text DEFAULT 'tennis'::text",uuid,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.update_academy_camp_weekly_payment(p_club_id uuid, p_participant_id uuid, p_is_paid boolean, p_expected_fee_huf numeric, p_paid_amount_huf numeric, p_paid_on date, p_internal_note text, p_sport text DEFAULT 'tennis'::text)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_id uuid;
v_payment_id uuid;
v_camp_fee_type text;
v_old_is_paid boolean;
v_old_expected_fee numeric;
v_old_paid_amount numeric;
v_old_paid_on date;
v_old_internal_note text;
v_sport text;
BEGIN
v_sport := lower(
COALESCE(NULLIF(btrim(p_sport), ''), 'tennis')
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION 'Érvénytelen sport.';
END IF;
IF NOT public.can_manage_academy_payments(p_club_id) THEN
RAISE EXCEPTION 'Unauthorized';
END IF;
SELECT id
INTO v_caller_id
FROM public.user_profiles
WHERE auth_user_id = auth.uid()
AND club_id = p_club_id
LIMIT 1;
IF v_caller_id IS NULL THEN
RAISE EXCEPTION 'Caller profile not found in club';
END IF;
PERFORM 1
FROM public.academy_program_participants app
JOIN public.academy_programs ap
ON ap.id = app.academy_program_id
AND ap.club_id = app.club_id
WHERE app.id = p_participant_id
AND app.club_id = p_club_id
AND ap.sport = v_sport
FOR UPDATE OF app;
IF NOT FOUND THEN
RAISE EXCEPTION
'Participant not found in club/sport';
END IF;
SELECT camp_fee_type
INTO v_camp_fee_type
FROM public.academy_program_participants
WHERE id = p_participant_id
AND club_id = p_club_id;
IF v_camp_fee_type <> 'weekly' THEN
RAISE EXCEPTION
'Participant fee type is not weekly. Cannot save weekly payment.';
END IF;
IF EXISTS (
SELECT 1
FROM public.academy_camp_daily_payments
WHERE participant_id = p_participant_id
) THEN
RAISE EXCEPTION
'Participant already has daily payments. Cannot save weekly payment.';
END IF;
SELECT
is_paid,
expected_fee_huf,
paid_amount_huf,
paid_on,
internal_note
INTO
v_old_is_paid,
v_old_expected_fee,
v_old_paid_amount,
v_old_paid_on,
v_old_internal_note
FROM public.academy_camp_weekly_payments
WHERE participant_id = p_participant_id;
INSERT INTO public.academy_camp_weekly_payments (
club_id,
participant_id,
is_paid,
expected_fee_huf,
paid_amount_huf,
paid_on,
internal_note,
updated_by_user_profile_id
)
VALUES (
p_club_id,
p_participant_id,
p_is_paid,
p_expected_fee_huf,
p_paid_amount_huf,
p_paid_on,
p_internal_note,
v_caller_id
)
ON CONFLICT (participant_id)
DO UPDATE SET
is_paid = EXCLUDED.is_paid,
expected_fee_huf = EXCLUDED.expected_fee_huf,
paid_amount_huf = EXCLUDED.paid_amount_huf,
paid_on = EXCLUDED.paid_on,
internal_note = EXCLUDED.internal_note,
updated_by_user_profile_id =
EXCLUDED.updated_by_user_profile_id,
updated_at = now()
RETURNING id
INTO v_payment_id;
IF COALESCE(v_old_is_paid, false) <> p_is_paid
OR COALESCE(v_old_expected_fee, -1)
<> COALESCE(p_expected_fee_huf, -1)
OR COALESCE(v_old_paid_amount, -1)
<> COALESCE(p_paid_amount_huf, -1)
OR COALESCE(v_old_paid_on, '1900-01-01'::date)
<> COALESCE(p_paid_on, '1900-01-01'::date)
OR COALESCE(v_old_internal_note, '')
<> COALESCE(p_internal_note, '')
THEN
INSERT INTO public.academy_payment_audit_logs (
club_id,
participant_id,
action_type,
period_identifier,
camp_fee_type,
old_is_paid,
new_is_paid,
old_expected_fee,
new_expected_fee,
old_paid_amount,
new_paid_amount,
old_paid_on,
new_paid_on,
old_internal_note,
new_internal_note,
updated_by_user_profile_id
)
VALUES (
p_club_id,
p_participant_id,
'camp_weekly_payment_update',
'weekly',
'weekly',
v_old_is_paid,
p_is_paid,
v_old_expected_fee,
p_expected_fee_huf,
v_old_paid_amount,
p_paid_amount_huf,
v_old_paid_on,
p_paid_on,
v_old_internal_note,
p_internal_note,
v_caller_id
);
END IF;
RETURN v_payment_id;
END;
$function$
"
public,update_academy_group_monthly_payment,f,"p_club_id uuid, p_participant_id uuid, p_year integer, p_month integer, p_is_paid boolean, p_expected_fee_huf numeric, p_paid_amount_huf numeric, p_paid_on date, p_internal_note text, p_sport text","p_club_id uuid, p_participant_id uuid, p_year integer, p_month integer, p_is_paid boolean, p_expected_fee_huf numeric, p_paid_amount_huf numeric, p_paid_on date, p_internal_note text, p_sport text DEFAULT 'tennis'::text",uuid,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.update_academy_group_monthly_payment(p_club_id uuid, p_participant_id uuid, p_year integer, p_month integer, p_is_paid boolean, p_expected_fee_huf numeric, p_paid_amount_huf numeric, p_paid_on date, p_internal_note text, p_sport text DEFAULT 'tennis'::text)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_id uuid;
v_payment_id uuid;
v_old_is_paid boolean;
v_old_expected_fee numeric;
v_old_paid_amount numeric;
v_old_paid_on date;
v_old_internal_note text;
v_sport text;
BEGIN
v_sport := lower(
COALESCE(NULLIF(btrim(p_sport), ''), 'tennis')
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION 'Érvénytelen sport.';
END IF;
IF NOT public.can_manage_academy_payments(p_club_id) THEN
RAISE EXCEPTION 'Unauthorized';
END IF;
SELECT id
INTO v_caller_id
FROM public.user_profiles
WHERE auth_user_id = auth.uid()
AND club_id = p_club_id
LIMIT 1;
IF v_caller_id IS NULL THEN
RAISE EXCEPTION 'Caller profile not found in club';
END IF;
IF NOT EXISTS (
SELECT 1
FROM public.academy_program_participants app
JOIN public.academy_programs ap
ON ap.id = app.academy_program_id
AND ap.club_id = app.club_id
WHERE app.id = p_participant_id
AND app.club_id = p_club_id
AND ap.sport = v_sport
) THEN
RAISE EXCEPTION
'Participant not found in club/sport';
END IF;
SELECT
is_paid,
expected_fee_huf,
paid_amount_huf,
paid_on,
internal_note
INTO
v_old_is_paid,
v_old_expected_fee,
v_old_paid_amount,
v_old_paid_on,
v_old_internal_note
FROM public.academy_group_monthly_payments
WHERE participant_id = p_participant_id
AND year = p_year
AND month = p_month;
INSERT INTO public.academy_group_monthly_payments (
club_id,
participant_id,
year,
month,
is_paid,
expected_fee_huf,
paid_amount_huf,
paid_on,
internal_note,
updated_by_user_profile_id
)
VALUES (
p_club_id,
p_participant_id,
p_year,
p_month,
p_is_paid,
p_expected_fee_huf,
p_paid_amount_huf,
p_paid_on,
p_internal_note,
v_caller_id
)
ON CONFLICT (participant_id, year, month)
DO UPDATE SET
is_paid = EXCLUDED.is_paid,
expected_fee_huf = EXCLUDED.expected_fee_huf,
paid_amount_huf = EXCLUDED.paid_amount_huf,
paid_on = EXCLUDED.paid_on,
internal_note = EXCLUDED.internal_note,
updated_by_user_profile_id =
EXCLUDED.updated_by_user_profile_id,
updated_at = now()
RETURNING id
INTO v_payment_id;
IF COALESCE(v_old_is_paid, false) <> p_is_paid
OR COALESCE(v_old_expected_fee, -1)
<> COALESCE(p_expected_fee_huf, -1)
OR COALESCE(v_old_paid_amount, -1)
<> COALESCE(p_paid_amount_huf, -1)
OR COALESCE(v_old_paid_on, '1900-01-01'::date)
<> COALESCE(p_paid_on, '1900-01-01'::date)
OR COALESCE(v_old_internal_note, '')
<> COALESCE(p_internal_note, '')
THEN
INSERT INTO public.academy_payment_audit_logs (
club_id,
participant_id,
action_type,
period_identifier,
old_is_paid,
new_is_paid,
old_expected_fee,
new_expected_fee,
old_paid_amount,
new_paid_amount,
old_paid_on,
new_paid_on,
old_internal_note,
new_internal_note,
updated_by_user_profile_id
)
VALUES (
p_club_id,
p_participant_id,
'monthly_payment_update',
p_year::text || '-' || p_month::text,
v_old_is_paid,
p_is_paid,
v_old_expected_fee,
p_expected_fee_huf,
v_old_paid_amount,
p_paid_amount_huf,
v_old_paid_on,
p_paid_on,
v_old_internal_note,
p_internal_note,
v_caller_id
);
END IF;
RETURN v_payment_id;
END;
$function$
"
public,update_academy_participant_fee_type,f,"p_club_id uuid, p_participant_id uuid, p_new_fee_type text, p_internal_note text, p_sport text","p_club_id uuid, p_participant_id uuid, p_new_fee_type text, p_internal_note text, p_sport text DEFAULT 'tennis'::text",boolean,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.update_academy_participant_fee_type(p_club_id uuid, p_participant_id uuid, p_new_fee_type text, p_internal_note text, p_sport text DEFAULT 'tennis'::text)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_caller_id uuid;
v_old_fee_type text;
v_sport text;
BEGIN
v_sport := lower(
COALESCE(NULLIF(btrim(p_sport), ''), 'tennis')
);
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION 'Érvénytelen sport.';
END IF;
IF NOT public.can_manage_academy_payments(p_club_id) THEN
RAISE EXCEPTION 'Unauthorized';
END IF;
SELECT id
INTO v_caller_id
FROM public.user_profiles
WHERE auth_user_id = auth.uid()
AND club_id = p_club_id
LIMIT 1;
IF v_caller_id IS NULL THEN
RAISE EXCEPTION 'Caller profile not found in club';
END IF;
IF p_new_fee_type IS NULL
OR p_new_fee_type NOT IN ('daily', 'weekly') THEN
RAISE EXCEPTION
'Invalid fee type. Allowed values: daily, weekly.';
END IF;
PERFORM 1
FROM public.academy_program_participants app
JOIN public.academy_programs ap
ON ap.id = app.academy_program_id
AND ap.club_id = app.club_id
WHERE app.id = p_participant_id
AND app.club_id = p_club_id
AND ap.sport = v_sport
FOR UPDATE OF app;
IF NOT FOUND THEN
RAISE EXCEPTION
'Participant not found in club/sport';
END IF;
SELECT camp_fee_type
INTO v_old_fee_type
FROM public.academy_program_participants
WHERE id = p_participant_id
AND club_id = p_club_id;
IF v_old_fee_type = p_new_fee_type THEN
RETURN true;
END IF;
IF v_old_fee_type = 'weekly'
AND EXISTS (
SELECT 1
FROM public.academy_camp_weekly_payments
WHERE participant_id = p_participant_id
)
THEN
RAISE EXCEPTION
'Cannot change fee type: weekly payments already exist for this participant.';
END IF;
IF v_old_fee_type = 'daily'
AND EXISTS (
SELECT 1
FROM public.academy_camp_daily_payments
WHERE participant_id = p_participant_id
)
THEN
RAISE EXCEPTION
'Cannot change fee type: daily payments already exist for this participant.';
END IF;
UPDATE public.academy_program_participants
SET
camp_fee_type = p_new_fee_type,
updated_at = now()
WHERE id = p_participant_id
AND club_id = p_club_id;
INSERT INTO public.academy_payment_audit_logs (
club_id,
participant_id,
action_type,
period_identifier,
old_camp_fee_type,
new_camp_fee_type,
new_internal_note,
updated_by_user_profile_id
)
VALUES (
p_club_id,
p_participant_id,
'camp_fee_type_change',
'fee_type',
v_old_fee_type,
p_new_fee_type,
p_internal_note,
v_caller_id
);
RETURN true;
END;
$function$
"
public,update_my_partner_availability,f,"p_sport text, p_availabilities jsonb","p_sport text, p_availabilities jsonb",void,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,anon=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.update_my_partner_availability(p_sport text, p_availabilities jsonb)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_profile_id uuid;
v_club_id uuid;
v_sport text;
v_item jsonb;
v_weekday integer;
v_start_time time without time zone;
v_end_time time without time zone;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Not authenticated';
END IF;
v_sport := lower(btrim(COALESCE(p_sport, '')));
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION 'Invalid sport. Expected tennis or padel.';
END IF;
IF p_availabilities IS NOT NULL
AND jsonb_typeof(p_availabilities) <> 'array'
THEN
RAISE EXCEPTION 'Availabilities must be a JSON array';
END IF;
SELECT
up.id,
up.club_id
INTO
v_profile_id,
v_club_id
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
LIMIT 1;
IF v_profile_id IS NULL OR v_club_id IS NULL THEN
RAISE EXCEPTION 'Profile or club not found';
END IF;
-- CRITICAL:
-- Delete ONLY selected sport availability.
DELETE FROM public.partner_search_availability
WHERE user_profile_id = v_profile_id
AND club_id = v_club_id
AND sport = v_sport;
IF p_availabilities IS NULL
OR jsonb_array_length(p_availabilities) = 0
THEN
RETURN;
END IF;
FOR v_item IN
SELECT value
FROM jsonb_array_elements(p_availabilities)
LOOP
IF NOT (
v_item ? 'weekday'
AND v_item ? 'start_time'
AND v_item ? 'end_time'
)
THEN
RAISE EXCEPTION
'Every availability must contain weekday, start_time and end_time';
END IF;
BEGIN
v_weekday :=
(v_item->>'weekday')::integer;
v_start_time :=
(v_item->>'start_time')::time;
v_end_time :=
(v_item->>'end_time')::time;
EXCEPTION
WHEN invalid_text_representation
OR datetime_field_overflow
THEN
RAISE EXCEPTION
'Invalid availability day or time value';
END;
IF v_weekday NOT BETWEEN 1 AND 7 THEN
RAISE EXCEPTION
'Weekday must be between 1 and 7';
END IF;
IF v_end_time <= v_start_time THEN
RAISE EXCEPTION
'End time must be later than start time';
END IF;
IF EXISTS (
SELECT 1
FROM public.partner_search_availability psa
WHERE psa.user_profile_id = v_profile_id
AND psa.club_id = v_club_id
AND psa.sport = v_sport
AND psa.weekday = v_weekday
AND psa.start_time = v_start_time
AND psa.end_time = v_end_time
)
THEN
RAISE EXCEPTION
'Duplicate availability row';
END IF;
INSERT INTO public.partner_search_availability (
user_profile_id,
club_id,
sport,
weekday,
start_time,
end_time
)
VALUES (
v_profile_id,
v_club_id,
v_sport,
v_weekday,
v_start_time,
v_end_time
);
END LOOP;
END;
$function$
"
public,update_my_partner_settings,f,"p_utr_rating numeric, p_show_email boolean, p_show_phone boolean, p_email text, p_phone text, p_profile_enabled boolean, p_sport text","p_utr_rating numeric, p_show_email boolean, p_show_phone boolean, p_email text, p_phone text, p_profile_enabled boolean, p_sport text",void,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,anon=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.update_my_partner_settings(p_utr_rating numeric, p_show_email boolean, p_show_phone boolean, p_email text, p_phone text, p_profile_enabled boolean, p_sport text)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_profile_id uuid;
v_club_id uuid;
v_sport text;
v_existing public.partner_finder_profiles%ROWTYPE;
v_effective_show_email boolean;
v_effective_show_phone boolean;
v_effective_enabled boolean;
BEGIN
IF auth.uid() IS NULL THEN
RAISE EXCEPTION 'Not authenticated';
END IF;
v_sport := lower(btrim(COALESCE(p_sport, '')));
IF v_sport NOT IN ('tennis', 'padel') THEN
RAISE EXCEPTION 'Invalid sport. Expected tennis or padel.';
END IF;
-- Current user's club profile
SELECT
up.id,
up.club_id
INTO
v_profile_id,
v_club_id
FROM public.user_profiles up
WHERE up.auth_user_id = auth.uid()
LIMIT 1;
IF v_profile_id IS NULL THEN
RAISE EXCEPTION 'Profile not found';
END IF;
IF v_club_id IS NULL THEN
RAISE EXCEPTION 'User profile is not assigned to a club';
END IF;
-- Existing profile ONLY for the selected sport.
SELECT *
INTO v_existing
FROM public.partner_finder_profiles pfp
WHERE pfp.user_profile_id = v_profile_id
AND pfp.sport = v_sport
LIMIT 1;
v_effective_show_email :=
COALESCE(
p_show_email,
v_existing.show_email_to_players,
false
);
v_effective_show_phone :=
COALESCE(
p_show_phone,
v_existing.show_phone_to_players,
false
);
v_effective_enabled :=
COALESCE(
p_profile_enabled,
v_existing.partner_search_profile_enabled,
false
);
-- --------------------------------------------------------
-- Sport-specific Partner Finder profile
--
-- NOTE:
-- p_utr_rating is used as the generic numeric rating field:
-- Tennis row = CTR
-- Padel row = CPR
-- --------------------------------------------------------
INSERT INTO public.partner_finder_profiles (
club_id,
user_profile_id,
sport,
utr_rating,
show_email_to_players,
show_phone_to_players,
partner_search_profile_enabled,
updated_at
)
VALUES (
v_club_id,
v_profile_id,
v_sport,
p_utr_rating,
v_effective_show_email,
v_effective_show_phone,
v_effective_enabled,
now()
)
ON CONFLICT (user_profile_id, sport)
DO UPDATE SET
club_id = EXCLUDED.club_id,
utr_rating = EXCLUDED.utr_rating,
show_email_to_players = EXCLUDED.show_email_to_players,
show_phone_to_players = EXCLUDED.show_phone_to_players,
partner_search_profile_enabled =
EXCLUDED.partner_search_profile_enabled,
updated_at = now();
-- --------------------------------------------------------
-- Shared account data
-- Email + phone remain common between Tennis / Padel.
--
-- Legacy Tennis fields are mirrored ONLY from Tennis,
-- so saving a Padel CPR cannot overwrite the Tennis rating.
-- --------------------------------------------------------
UPDATE public.user_profiles AS up
SET
email =
CASE
WHEN p_email IS NULL OR btrim(p_email) = ''
THEN up.email
ELSE btrim(p_email)
END,
phone =
CASE
WHEN p_phone IS NULL
THEN up.phone
ELSE NULLIF(btrim(p_phone), '')
END,
utr_rating =
CASE
WHEN v_sport = 'tennis'
THEN p_utr_rating
ELSE up.utr_rating
END,
show_email_to_players =
CASE
WHEN v_sport = 'tennis'
THEN v_effective_show_email
ELSE up.show_email_to_players
END,
show_phone_to_players =
CASE
WHEN v_sport = 'tennis'
THEN v_effective_show_phone
ELSE up.show_phone_to_players
END,
-- Legacy account-level flag:
-- TRUE if at least one sport-specific profile is enabled.
partner_search_profile_enabled =
EXISTS (
SELECT 1
FROM public.partner_finder_profiles pfp
WHERE pfp.user_profile_id = v_profile_id
AND pfp.partner_search_profile_enabled = true
),
updated_at = now()
WHERE up.id = v_profile_id;
END;
$function$
"
public,update_my_personal_profile,f,"p_first_name text, p_last_name text, p_phone text","p_first_name text, p_last_name text, p_phone text",jsonb,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.update_my_personal_profile(p_first_name text, p_last_name text, p_phone text)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'pg_catalog', 'public', 'pg_temp'
AS $function$
DECLARE
v_user_profile_id uuid;
v_auth_uid uuid;
v_trimmed_first text;
v_trimmed_last text;
v_trimmed_phone text;
v_full_name text;
BEGIN
v_auth_uid := auth.uid();
IF v_auth_uid IS NULL THEN
RAISE EXCEPTION 'Not authenticated';
END IF;
SELECT id INTO v_user_profile_id
FROM public.user_profiles
WHERE auth_user_id = v_auth_uid
LIMIT 1;
IF v_user_profile_id IS NULL THEN
RAISE EXCEPTION 'User profile not found';
END IF;
v_trimmed_first := NULLIF(BTRIM(p_first_name), '');
v_trimmed_last := NULLIF(BTRIM(p_last_name), '');
v_trimmed_phone := NULLIF(BTRIM(p_phone), '');
IF v_trimmed_first IS NULL THEN
RAISE EXCEPTION 'First name cannot be empty';
END IF;
IF v_trimmed_last IS NULL THEN
RAISE EXCEPTION 'Last name cannot be empty';
END IF;
v_full_name := v_trimmed_first || ' ' || v_trimmed_last;
UPDATE public.user_profiles
SET
full_name = v_full_name,
phone = v_trimmed_phone,
updated_at = now()
WHERE id = v_user_profile_id;
RETURN jsonb_build_object('success', true);
END;
$function$
"
public,validate_academy_coach_compensation_rate,f,,,trigger,plpgsql,false,VOLATILE,postgres,"{=X/postgres,postgres=X/postgres,anon=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.validate_academy_coach_compensation_rate()
RETURNS trigger
LANGUAGE plpgsql
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_assignment_club_id uuid;
v_assigned_from date;
v_assigned_to date;
v_assignment_is_void boolean;
BEGIN
SELECT
a.club_id,
a.assigned_from,
a.assigned_to,
a.is_void
INTO
v_assignment_club_id,
v_assigned_from,
v_assigned_to,
v_assignment_is_void
FROM public.academy_program_coach_assignments a
WHERE a.id = NEW.assignment_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'Az edzői hozzárendelés nem található.';
END IF;
IF NEW.club_id <> v_assignment_club_id THEN
RAISE EXCEPTION
'A díj club_id értéke nem egyezik a hozzárendelés klubjával.';
END IF;
-- Érvénytelenített díjat nem kell az időintervallummal ellenőrizni.
IF NEW.is_void THEN
RETURN NEW;
END IF;
IF v_assignment_is_void THEN
RAISE EXCEPTION
'Érvénytelenített hozzárendeléshez nem hozható létre aktív díj.';
END IF;
IF NEW.effective_from < v_assigned_from THEN
RAISE EXCEPTION
'A díj kezdőnapja nem lehet korábbi a hozzárendelés kezdőnapjánál.';
END IF;
IF v_assigned_to IS NOT NULL
AND NEW.effective_from > v_assigned_to THEN
RAISE EXCEPTION
'A díj kezdőnapja a hozzárendelés lezárása utánra esik.';
END IF;
IF v_assigned_to IS NOT NULL
AND (
NEW.effective_to IS NULL
OR NEW.effective_to > v_assigned_to
) THEN
RAISE EXCEPTION
'A díj érvényessége nem nyúlhat túl a hozzárendelési időszakon.';
END IF;
RETURN NEW;
END;
$function$
"
public,validate_academy_group_court_cost_setting,f,,,trigger,plpgsql,true,VOLATILE,postgres,"{postgres=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.validate_academy_group_court_cost_setting()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_program_club_id uuid;
v_program_type text;
v_created_by_club_id uuid;
v_updated_by_club_id uuid;
BEGIN
SELECT
ap.club_id,
ap.program_type
INTO
v_program_club_id,
v_program_type
FROM public.academy_programs ap
WHERE ap.id = NEW.academy_program_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'Az akadémiai csoport nem található.';
END IF;
IF v_program_type <> 'group' THEN
RAISE EXCEPTION
'Pályaköltség-beállítás csak csoportos programhoz használható.';
END IF;
IF NEW.club_id <> v_program_club_id THEN
RAISE EXCEPTION
'A pályaköltség klubazonosítója nem egyezik a csoport klubjával.';
END IF;
SELECT up.club_id
INTO v_created_by_club_id
FROM public.user_profiles up
WHERE up.id = NEW.created_by_user_profile_id;
IF NOT FOUND
OR v_created_by_club_id <> NEW.club_id THEN
RAISE EXCEPTION
'A létrehozó felhasználó nem ugyanahhoz a klubhoz tartozik.';
END IF;
SELECT up.club_id
INTO v_updated_by_club_id
FROM public.user_profiles up
WHERE up.id = NEW.updated_by_user_profile_id;
IF NOT FOUND
OR v_updated_by_club_id <> NEW.club_id THEN
RAISE EXCEPTION
'A módosító felhasználó nem ugyanahhoz a klubhoz tartozik.';
END IF;
RETURN NEW;
END;
$function$
"
public,validate_academy_participant_row,f,,,trigger,plpgsql,false,VOLATILE,postgres,"{=X/postgres,postgres=X/postgres,anon=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.validate_academy_participant_row()
RETURNS trigger
LANGUAGE plpgsql
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_program_type text;
v_program_club_id uuid;
BEGIN
NEW.last_name := btrim(NEW.last_name);
NEW.first_name := btrim(NEW.first_name);
NEW.internal_note := NULLIF(btrim(NEW.internal_note), '');
NEW.updated_at := now();
IF NEW.last_name = '' OR NEW.first_name = '' THEN
RAISE EXCEPTION 'A vezetéknév és a keresztnév kötelező.';
END IF;
IF NEW.birth_year > extract(year from current_date)::integer THEN
RAISE EXCEPTION 'A születési év nem lehet jövőbeli.';
END IF;
SELECT
ap.program_type,
ap.club_id
INTO
v_program_type,
v_program_club_id
FROM public.academy_programs ap
WHERE ap.id = NEW.academy_program_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Az akadémiai program nem található.';
END IF;
IF v_program_club_id <> NEW.club_id THEN
RAISE EXCEPTION 'A résztvevő és a program klubja nem egyezik.';
END IF;
IF v_program_type = 'group' THEN
IF NEW.monthly_fee_huf IS NULL THEN
RAISE EXCEPTION 'Csoportos játékosnál a havi díj kötelező.';
END IF;
IF NEW.camp_fee_type IS NOT NULL
OR NEW.camp_fee_huf IS NOT NULL THEN
RAISE EXCEPTION
'Csoportos játékoshoz nem tartozhat tábori díj.';
END IF;
ELSIF v_program_type = 'camp' THEN
IF NEW.monthly_fee_huf IS NOT NULL THEN
RAISE EXCEPTION
'Tábori játékoshoz nem tartozhat havi csoportdíj.';
END IF;
IF NEW.camp_fee_type NOT IN ('daily', 'weekly')
OR NEW.camp_fee_huf IS NULL THEN
RAISE EXCEPTION
'Tábori játékosnál napi vagy heti díjat kell megadni.';
END IF;
END IF;
IF NEW.is_active = true THEN
NEW.left_on := NULL;
ELSIF NEW.left_on IS NULL THEN
NEW.left_on := current_date;
END IF;
RETURN NEW;
END;
$function$
"
public,validate_academy_program_coach_assignment,f,,,trigger,plpgsql,false,VOLATILE,postgres,"{=X/postgres,postgres=X/postgres,anon=X/postgres,authenticated=X/postgres,service_role=X/postgres}","CREATE OR REPLACE FUNCTION public.validate_academy_program_coach_assignment()
RETURNS trigger
LANGUAGE plpgsql
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
v_program_club_id uuid;
v_program_type text;
v_coach_club_id uuid;
BEGIN
SELECT
ap.club_id,
ap.program_type
INTO
v_program_club_id,
v_program_type
FROM public.academy_programs ap
WHERE ap.id = NEW.academy_program_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'Az akadémiai program nem található.';
END IF;
IF v_program_type <> 'group' THEN
RAISE EXCEPTION
'Edzői díjazás jelenleg csak csoportos akadémiai programhoz használható.';
END IF;
SELECT up.club_id
INTO v_coach_club_id
FROM public.coach_profiles cp
JOIN public.user_profiles up
ON up.id = cp.user_profile_id
WHERE cp.id = NEW.coach_profile_id;
IF NOT FOUND THEN
RAISE EXCEPTION
'Az edzőhöz nem tartozik érvényes felhasználói profil.';
END IF;
IF NEW.club_id <> v_program_club_id THEN
RAISE EXCEPTION
'A hozzárendelés club_id értéke nem egyezik a program klubjával.';
END IF;
IF NEW.club_id <> v_coach_club_id THEN
RAISE EXCEPTION
'Az edző nem ugyanahhoz a klubhoz tartozik, mint a csoport.';
END IF;
RETURN NEW;
END;
$function$
"
object_type,schema_name,parent_object,object_name,definition
EXTENSION,extensions,null,btree_gist,version=1.7
EXTENSION,extensions,null,pg_stat_statements,version=1.11
EXTENSION,extensions,null,pgcrypto,version=1.3
EXTENSION,extensions,null,uuid-ossp,version=1.1
EXTENSION,pg_catalog,null,plpgsql,version=1.0
EXTENSION,vault,null,supabase_vault,version=0.3.1
RLS_POLICY,public,academy_booking_batches,Academy booking batches select,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=can_manage_academy(club_id) | with_check=NULL
RLS_POLICY,public,academy_booking_coach_checkins,Academy multi coach checkin read,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(club_id IN ( SELECT up.club_id
FROM user_profiles up
WHERE (up.auth_user_id = auth.uid()))) | with_check=NULL"
RLS_POLICY,public,academy_booking_details,Academy booking details select,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=can_manage_academy(club_id) | with_check=NULL
RLS_POLICY,public,academy_camp_daily_payments,Academy camp daily payments access,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=can_manage_academy_payments(club_id) | with_check=NULL
RLS_POLICY,public,academy_camp_weekly_payments,Academy camp weekly payments access,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=can_manage_academy_payments(club_id) | with_check=NULL
RLS_POLICY,public,academy_coach_checkin_audit_logs,Academy multi coach checkin audit read,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(club_id IN ( SELECT up.club_id
FROM user_profiles up
WHERE (up.auth_user_id = auth.uid()))) | with_check=NULL"
RLS_POLICY,public,academy_group_monthly_payments,Academy group monthly payments access,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=can_manage_academy_payments(club_id) | with_check=NULL
RLS_POLICY,public,academy_payment_audit_logs,Academy payment audit logs access,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=can_manage_academy_payments(club_id) | with_check=NULL
RLS_POLICY,public,academy_program_participants,Academy participants insert,permissive=PERMISSIVE | roles={authenticated} | command=INSERT | using=NULL | with_check=can_manage_academy(club_id)
RLS_POLICY,public,academy_program_participants,Academy participants select,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=can_manage_academy(club_id) | with_check=NULL
RLS_POLICY,public,academy_program_participants,Academy participants update,permissive=PERMISSIVE | roles={authenticated} | command=UPDATE | using=can_manage_academy(club_id) | with_check=can_manage_academy(club_id)
RLS_POLICY,public,academy_programs,Academy programs insert,permissive=PERMISSIVE | roles={authenticated} | command=INSERT | using=NULL | with_check=can_manage_academy(club_id)
RLS_POLICY,public,academy_programs,Academy programs select,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=can_manage_academy(club_id) | with_check=NULL
RLS_POLICY,public,academy_programs,Academy programs update,permissive=PERMISSIVE | roles={authenticated} | command=UPDATE | using=can_manage_academy(club_id) | with_check=can_manage_academy(club_id)
RLS_POLICY,public,audit_log,audit_log_admin_insert,permissive=PERMISSIVE | roles={authenticated} | command=INSERT | using=NULL | with_check=is_current_user_club_admin(club_id)
RLS_POLICY,public,audit_log,audit_log_admin_select,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=is_current_user_club_admin(club_id) | with_check=NULL
RLS_POLICY,public,booking_payments,Admins can read club payments,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(club_id IN ( SELECT up.club_id
FROM user_profiles up
WHERE ((up.auth_user_id = auth.uid()) AND (up.role = ANY (ARRAY['owner'::text, 'admin'::text, 'secondary_admin'::text]))))) | with_check=NULL"
RLS_POLICY,public,booking_payments,Users can read own payments,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(user_profile_id IN ( SELECT up.id
FROM user_profiles up
WHERE (up.auth_user_id = auth.uid()))) | with_check=NULL"
RLS_POLICY,public,booking_requests,Admins can read club requests,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(club_id IN ( SELECT user_profiles.club_id
FROM user_profiles
WHERE ((user_profiles.auth_user_id = auth.uid()) AND (user_profiles.role = ANY (ARRAY['owner'::text, 'admin'::text, 'secondary_admin'::text]))))) | with_check=NULL"
RLS_POLICY,public,booking_requests,Users can read own requests,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(caller_user_profile_id IN ( SELECT user_profiles.id
FROM user_profiles
WHERE (user_profiles.auth_user_id = auth.uid()))) | with_check=NULL"
RLS_POLICY,public,bookings,bookings_delete_self_or_admin,permissive=PERMISSIVE | roles={authenticated} | command=DELETE | using=(is_current_user_club_admin(club_id) OR (((user_id = current_user_profile_id()) OR (created_by_user_id = current_user_profile_id())) AND ((booking_date + start_time) >= ((now() AT TIME ZONE 'Europe/Budapest'::text) + '24:00:00'::interval)))) | with_check=NULL
RLS_POLICY,public,bookings,bookings_insert_self_or_admin,permissive=PERMISSIVE | roles={authenticated} | command=INSERT | using=NULL | with_check=(is_current_user_club_admin(club_id) OR ((club_id = current_user_club_id()) AND (user_id = current_user_profile_id()) AND (created_by_user_id = current_user_profile_id()) AND ((booking_date + start_time) > (now() AT TIME ZONE 'Europe/Budapest'::text))))
RLS_POLICY,public,bookings,bookings_select_own_or_admin,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(is_current_user_club_admin(club_id) OR (user_id = current_user_profile_id()) OR (created_by_user_id = current_user_profile_id())) | with_check=NULL
RLS_POLICY,public,bookings,bookings_update_self_or_admin,permissive=PERMISSIVE | roles={authenticated} | command=UPDATE | using=(is_current_user_club_admin(club_id) OR (((user_id = current_user_profile_id()) OR (created_by_user_id = current_user_profile_id())) AND ((booking_date + start_time) >= ((now() AT TIME ZONE 'Europe/Budapest'::text) + '24:00:00'::interval)))) | with_check=(is_current_user_club_admin(club_id) OR ((club_id = current_user_club_id()) AND (user_id = current_user_profile_id()) AND (created_by_user_id = current_user_profile_id())))
RLS_POLICY,public,clubs,clubs_admin_update,permissive=PERMISSIVE | roles={authenticated} | command=UPDATE | using=is_current_user_club_admin(id) | with_check=is_current_user_club_admin(id)
RLS_POLICY,public,clubs,clubs_public_read,"permissive=PERMISSIVE | roles={anon,authenticated} | command=SELECT | using=true | with_check=NULL"
RLS_POLICY,public,coach_availability,coach_availability_delete_own_or_admin,permissive=PERMISSIVE | roles={authenticated} | command=DELETE | using=(is_current_user_coach_profile(coach_id) OR is_current_user_club_admin(coach_profile_club_id(coach_id))) | with_check=NULL
RLS_POLICY,public,coach_availability,coach_availability_insert_own_or_admin,permissive=PERMISSIVE | roles={authenticated} | command=INSERT | using=NULL | with_check=(is_current_user_coach_profile(coach_id) OR is_current_user_club_admin(coach_profile_club_id(coach_id)))
RLS_POLICY,public,coach_availability,coach_availability_public_read_active,"permissive=PERMISSIVE | roles={anon,authenticated} | command=SELECT | using=((is_active IS TRUE) AND (EXISTS ( SELECT 1
FROM ((coach_profiles cp
JOIN user_profiles up ON ((up.id = cp.user_profile_id)))
JOIN clubs c ON ((c.id = up.club_id)))
WHERE ((cp.id = coach_availability.coach_id) AND (cp.is_active IS TRUE) AND (c.slug = 'bravos-sport-club'::text))))) | with_check=NULL"
RLS_POLICY,public,coach_availability,coach_availability_select_own_or_admin,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(is_current_user_coach_profile(coach_id) OR is_current_user_club_admin(coach_profile_club_id(coach_id))) | with_check=NULL
RLS_POLICY,public,coach_availability,coach_availability_update_own_or_admin,permissive=PERMISSIVE | roles={authenticated} | command=UPDATE | using=(is_current_user_coach_profile(coach_id) OR is_current_user_club_admin(coach_profile_club_id(coach_id))) | with_check=(is_current_user_coach_profile(coach_id) OR is_current_user_club_admin(coach_profile_club_id(coach_id)))
RLS_POLICY,public,coach_permissions,coach_permissions_delete_admin,permissive=PERMISSIVE | roles={authenticated} | command=DELETE | using=is_current_user_club_admin(club_id) | with_check=NULL
RLS_POLICY,public,coach_permissions,coach_permissions_insert_admin,"permissive=PERMISSIVE | roles={authenticated} | command=INSERT | using=NULL | with_check=(is_current_user_club_admin(club_id) AND (EXISTS ( SELECT 1
FROM user_profiles up
WHERE ((up.id = coach_permissions.coach_user_profile_id) AND (up.club_id = coach_permissions.club_id)))))"
RLS_POLICY,public,coach_permissions,coach_permissions_select_own_or_admin,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=((coach_user_profile_id = current_user_profile_id()) OR is_current_user_club_admin(club_id)) | with_check=NULL
RLS_POLICY,public,coach_permissions,coach_permissions_update_admin,"permissive=PERMISSIVE | roles={authenticated} | command=UPDATE | using=is_current_user_club_admin(club_id) | with_check=(is_current_user_club_admin(club_id) AND (EXISTS ( SELECT 1
FROM user_profiles up
WHERE ((up.id = coach_permissions.coach_user_profile_id) AND (up.club_id = coach_permissions.club_id)))))"
RLS_POLICY,public,coach_profiles,coach_profiles_delete_admin,permissive=PERMISSIVE | roles={authenticated} | command=DELETE | using=is_current_user_club_admin(coach_profile_club_id(id)) | with_check=NULL
RLS_POLICY,public,coach_profiles,coach_profiles_insert_own_or_admin,"permissive=PERMISSIVE | roles={authenticated} | command=INSERT | using=NULL | with_check=(((user_id = ( SELECT auth.uid() AS uid)) AND (user_profile_id = current_user_profile_id())) OR (EXISTS ( SELECT 1
FROM user_profiles up
WHERE ((up.id = coach_profiles.user_profile_id) AND (up.auth_user_id = coach_profiles.user_id) AND is_current_user_club_admin(up.club_id)))))"
RLS_POLICY,public,coach_profiles,coach_profiles_select_same_club,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=((user_id = ( SELECT auth.uid() AS uid)) OR ((is_active IS TRUE) AND (coach_profile_club_id(id) = current_user_club_id())) OR is_current_user_club_admin(coach_profile_club_id(id))) | with_check=NULL
RLS_POLICY,public,coach_profiles,coach_profiles_update_own_or_admin,"permissive=PERMISSIVE | roles={authenticated} | command=UPDATE | using=((user_id = ( SELECT auth.uid() AS uid)) OR is_current_user_club_admin(coach_profile_club_id(id))) | with_check=(((user_id = ( SELECT auth.uid() AS uid)) AND (user_profile_id = current_user_profile_id())) OR (EXISTS ( SELECT 1
FROM user_profiles up
WHERE ((up.id = coach_profiles.user_profile_id) AND (up.auth_user_id = coach_profiles.user_id) AND is_current_user_club_admin(up.club_id)))))"
RLS_POLICY,public,courts,courts_admin_delete,permissive=PERMISSIVE | roles={authenticated} | command=DELETE | using=is_current_user_club_admin(club_id) | with_check=NULL
RLS_POLICY,public,courts,courts_admin_insert,permissive=PERMISSIVE | roles={authenticated} | command=INSERT | using=NULL | with_check=((club_id IS NOT NULL) AND is_current_user_club_admin(club_id))
RLS_POLICY,public,courts,courts_admin_read_all,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=is_current_user_club_admin(club_id) | with_check=NULL
RLS_POLICY,public,courts,courts_admin_update,permissive=PERMISSIVE | roles={authenticated} | command=UPDATE | using=is_current_user_club_admin(club_id) | with_check=((club_id IS NOT NULL) AND is_current_user_club_admin(club_id))
RLS_POLICY,public,courts,courts_public_read,"permissive=PERMISSIVE | roles={anon,authenticated} | command=SELECT | using=(is_active IS TRUE) | with_check=NULL"
RLS_POLICY,public,credit_transactions,Admins can read club transactions,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(club_id IN ( SELECT up.club_id
FROM user_profiles up
WHERE ((up.auth_user_id = auth.uid()) AND (up.role = ANY (ARRAY['owner'::text, 'admin'::text, 'secondary_admin'::text]))))) | with_check=NULL"
RLS_POLICY,public,credit_transactions,Users can read own transactions,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(user_profile_id IN ( SELECT up.id
FROM user_profiles up
WHERE (up.auth_user_id = auth.uid()))) | with_check=NULL"
RLS_POLICY,public,credit_wallets,Admins can read club wallets,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(club_id IN ( SELECT up.club_id
FROM user_profiles up
WHERE ((up.auth_user_id = auth.uid()) AND (up.role = ANY (ARRAY['owner'::text, 'admin'::text, 'secondary_admin'::text]))))) | with_check=NULL"
RLS_POLICY,public,credit_wallets,Users can read own wallets,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(user_profile_id IN ( SELECT up.id
FROM user_profiles up
WHERE (up.auth_user_id = auth.uid()))) | with_check=NULL"
RLS_POLICY,public,opening_hours,opening_hours_admin_delete,permissive=PERMISSIVE | roles={authenticated} | command=DELETE | using=is_current_user_club_admin(club_id) | with_check=NULL
RLS_POLICY,public,opening_hours,opening_hours_admin_insert,permissive=PERMISSIVE | roles={authenticated} | command=INSERT | using=NULL | with_check=((club_id IS NOT NULL) AND is_current_user_club_admin(club_id))
RLS_POLICY,public,opening_hours,opening_hours_admin_update,permissive=PERMISSIVE | roles={authenticated} | command=UPDATE | using=is_current_user_club_admin(club_id) | with_check=((club_id IS NOT NULL) AND is_current_user_club_admin(club_id))
RLS_POLICY,public,opening_hours,opening_hours_public_read,"permissive=PERMISSIVE | roles={anon,authenticated} | command=SELECT | using=true | with_check=NULL"
RLS_POLICY,public,partner_finder_profiles,partner_finder_delete_own_or_admin,permissive=PERMISSIVE | roles={authenticated} | command=DELETE | using=((user_profile_id = current_user_profile_id()) OR is_current_user_club_admin(club_id)) | with_check=NULL
RLS_POLICY,public,partner_finder_profiles,partner_finder_insert_own_or_admin,"permissive=PERMISSIVE | roles={authenticated} | command=INSERT | using=NULL | with_check=(((user_profile_id = current_user_profile_id()) AND (club_id = current_user_club_id())) OR (is_current_user_club_admin(club_id) AND (EXISTS ( SELECT 1
FROM user_profiles up
WHERE ((up.id = partner_finder_profiles.user_profile_id) AND (up.club_id = partner_finder_profiles.club_id))))))"
RLS_POLICY,public,partner_finder_profiles,partner_finder_select_own_or_admin,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=((user_profile_id = current_user_profile_id()) OR is_current_user_club_admin(club_id)) | with_check=NULL
RLS_POLICY,public,partner_finder_profiles,partner_finder_update_own_or_admin,"permissive=PERMISSIVE | roles={authenticated} | command=UPDATE | using=((user_profile_id = current_user_profile_id()) OR is_current_user_club_admin(club_id)) | with_check=(((user_profile_id = current_user_profile_id()) AND (club_id = current_user_club_id())) OR (is_current_user_club_admin(club_id) AND (EXISTS ( SELECT 1
FROM user_profiles up
WHERE ((up.id = partner_finder_profiles.user_profile_id) AND (up.club_id = partner_finder_profiles.club_id))))))"
RLS_POLICY,public,partner_search_availability,Users can read their own partner search availability,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(EXISTS ( SELECT 1
FROM user_profiles up
WHERE ((up.id = partner_search_availability.user_profile_id) AND (up.auth_user_id = auth.uid()) AND (up.club_id = partner_search_availability.club_id)))) | with_check=NULL"
RLS_POLICY,public,pricing_group_courts,Allow admin write pricing_group_courts,"permissive=PERMISSIVE | roles={authenticated} | command=ALL | using=(EXISTS ( SELECT 1
FROM (pricing_groups pg
JOIN user_profiles up ON ((up.club_id = pg.club_id)))
WHERE ((pg.id = pricing_group_courts.pricing_group_id) AND (up.auth_user_id = auth.uid()) AND (up.role = ANY (ARRAY['owner'::text, 'admin'::text, 'secondary_admin'::text]))))) | with_check=(EXISTS ( SELECT 1
FROM (pricing_groups pg
JOIN user_profiles up ON ((up.club_id = pg.club_id)))
WHERE ((pg.id = pricing_group_courts.pricing_group_id) AND (up.auth_user_id = auth.uid()) AND (up.role = ANY (ARRAY['owner'::text, 'admin'::text, 'secondary_admin'::text])))))"
RLS_POLICY,public,pricing_group_courts,Allow public read pricing_group_courts,permissive=PERMISSIVE | roles={public} | command=SELECT | using=true | with_check=NULL
RLS_POLICY,public,pricing_groups,Allow admin write pricing_groups,"permissive=PERMISSIVE | roles={authenticated} | command=ALL | using=(EXISTS ( SELECT 1
FROM user_profiles
WHERE ((user_profiles.auth_user_id = auth.uid()) AND (user_profiles.club_id = pricing_groups.club_id) AND (user_profiles.role = ANY (ARRAY['owner'::text, 'admin'::text, 'secondary_admin'::text]))))) | with_check=(EXISTS ( SELECT 1
FROM user_profiles
WHERE ((user_profiles.auth_user_id = auth.uid()) AND (user_profiles.club_id = pricing_groups.club_id) AND (user_profiles.role = ANY (ARRAY['owner'::text, 'admin'::text, 'secondary_admin'::text])))))"
RLS_POLICY,public,pricing_groups,Allow public read pricing_groups,permissive=PERMISSIVE | roles={public} | command=SELECT | using=true | with_check=NULL
RLS_POLICY,public,pricing_rules,pricing_rules_admin_delete,permissive=PERMISSIVE | roles={authenticated} | command=DELETE | using=is_current_user_club_admin(club_id) | with_check=NULL
RLS_POLICY,public,pricing_rules,pricing_rules_admin_insert,permissive=PERMISSIVE | roles={authenticated} | command=INSERT | using=NULL | with_check=((club_id IS NOT NULL) AND is_current_user_club_admin(club_id))
RLS_POLICY,public,pricing_rules,pricing_rules_admin_read_all,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=is_current_user_club_admin(club_id) | with_check=NULL
RLS_POLICY,public,pricing_rules,pricing_rules_admin_update,permissive=PERMISSIVE | roles={authenticated} | command=UPDATE | using=is_current_user_club_admin(club_id) | with_check=((club_id IS NOT NULL) AND is_current_user_club_admin(club_id))
RLS_POLICY,public,pricing_rules,pricing_rules_public_read,"permissive=PERMISSIVE | roles={anon,authenticated} | command=SELECT | using=(is_active IS TRUE) | with_check=NULL"
RLS_POLICY,public,recurring_booking_series,recurring_booking_series_select_admin,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(EXISTS ( SELECT 1
FROM user_profiles
WHERE ((user_profiles.auth_user_id = auth.uid()) AND (user_profiles.club_id = recurring_booking_series.club_id) AND (user_profiles.role = ANY (ARRAY['owner'::text, 'admin'::text, 'secondary_admin'::text]))))) | with_check=NULL"
RLS_POLICY,public,recurring_booking_series,recurring_booking_series_select_own,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(EXISTS ( SELECT 1
FROM user_profiles
WHERE ((user_profiles.auth_user_id = auth.uid()) AND (user_profiles.id = recurring_booking_series.user_profile_id)))) | with_check=NULL"
RLS_POLICY,public,seasons,seasons_admin_delete,permissive=PERMISSIVE | roles={authenticated} | command=DELETE | using=is_current_user_club_admin(club_id) | with_check=NULL
RLS_POLICY,public,seasons,seasons_admin_insert,permissive=PERMISSIVE | roles={authenticated} | command=INSERT | using=NULL | with_check=((club_id IS NOT NULL) AND is_current_user_club_admin(club_id))
RLS_POLICY,public,seasons,seasons_admin_read_all,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=is_current_user_club_admin(club_id) | with_check=NULL
RLS_POLICY,public,seasons,seasons_admin_update,permissive=PERMISSIVE | roles={authenticated} | command=UPDATE | using=is_current_user_club_admin(club_id) | with_check=((club_id IS NOT NULL) AND is_current_user_club_admin(club_id))
RLS_POLICY,public,seasons,seasons_public_read,"permissive=PERMISSIVE | roles={anon,authenticated} | command=SELECT | using=((booking_status = 'open'::text) OR (is_current_season IS TRUE) OR (is_next_season IS TRUE)) | with_check=NULL"
RLS_POLICY,public,settings_history,settings_history_admin_insert,permissive=PERMISSIVE | roles={authenticated} | command=INSERT | using=NULL | with_check=is_current_user_club_admin(club_id)
RLS_POLICY,public,settings_history,settings_history_admin_select,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=is_current_user_club_admin(club_id) | with_check=NULL
RLS_POLICY,public,user_profiles,user_profiles_insert_self,"permissive=PERMISSIVE | roles={authenticated} | command=INSERT | using=NULL | with_check=((auth_user_id = ( SELECT auth.uid() AS uid)) AND (COALESCE(role, 'registered_user'::text) = 'registered_user'::text) AND (club_id = ( SELECT c.id
FROM clubs c
WHERE (c.slug = 'bravos-sport-club'::text)
LIMIT 1)))"
RLS_POLICY,public,user_profiles,user_profiles_select_own_or_admin,permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=((auth_user_id = ( SELECT auth.uid() AS uid)) OR is_current_user_club_admin(club_id)) | with_check=NULL
RLS_POLICY,public,user_profiles,user_profiles_update_self,permissive=PERMISSIVE | roles={authenticated} | command=UPDATE | using=(auth_user_id = ( SELECT auth.uid() AS uid)) | with_check=(auth_user_id = ( SELECT auth.uid() AS uid))
RLS_POLICY,public,venue_payment_installment_events,venue_payment_events_select_admin,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(EXISTS ( SELECT 1
FROM ((venue_payment_installments i
JOIN venue_payment_plans p ON ((p.id = i.plan_id)))
JOIN user_profiles u ON ((u.club_id = p.club_id)))
WHERE ((i.id = venue_payment_installment_events.installment_id) AND (u.auth_user_id = auth.uid()) AND (u.role = ANY (ARRAY['owner'::text, 'admin'::text, 'secondary_admin'::text]))))) | with_check=NULL"
RLS_POLICY,public,venue_payment_installment_events,venue_payment_events_select_own,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(EXISTS ( SELECT 1
FROM ((venue_payment_installments i
JOIN venue_payment_plans p ON ((p.id = i.plan_id)))
JOIN user_profiles u ON ((u.id = p.user_profile_id)))
WHERE ((i.id = venue_payment_installment_events.installment_id) AND (u.auth_user_id = auth.uid())))) | with_check=NULL"
RLS_POLICY,public,venue_payment_installments,venue_payment_installments_select_admin,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(EXISTS ( SELECT 1
FROM (venue_payment_plans p
JOIN user_profiles u ON ((u.club_id = p.club_id)))
WHERE ((p.id = venue_payment_installments.plan_id) AND (u.auth_user_id = auth.uid()) AND (u.role = ANY (ARRAY['owner'::text, 'admin'::text, 'secondary_admin'::text]))))) | with_check=NULL"
RLS_POLICY,public,venue_payment_installments,venue_payment_installments_select_own,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(EXISTS ( SELECT 1
FROM (venue_payment_plans p
JOIN user_profiles u ON ((u.id = p.user_profile_id)))
WHERE ((p.id = venue_payment_installments.plan_id) AND (u.auth_user_id = auth.uid())))) | with_check=NULL"
RLS_POLICY,public,venue_payment_plans,venue_payment_plans_select_admin,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(EXISTS ( SELECT 1
FROM user_profiles
WHERE ((user_profiles.auth_user_id = auth.uid()) AND (user_profiles.club_id = venue_payment_plans.club_id) AND (user_profiles.role = ANY (ARRAY['owner'::text, 'admin'::text, 'secondary_admin'::text]))))) | with_check=NULL"
RLS_POLICY,public,venue_payment_plans,venue_payment_plans_select_own,"permissive=PERMISSIVE | roles={authenticated} | command=SELECT | using=(EXISTS ( SELECT 1
FROM user_profiles
WHERE ((user_profiles.auth_user_id = auth.uid()) AND (user_profiles.id = venue_payment_plans.user_profile_id)))) | with_check=NULL"
=== LIVE MANIFEST 5/5 — VIEWS + AUTH TRIGGER — RAW LIVE SOURCE ===
VIEW: public.booking_calendar_slots
CREATE OR REPLACE VIEW public.booking_calendar_slots AS
SELECT
id,
club_id,
season_id,
court_id,
booking_date,
start_time,
end_time,
booking_type,
recurring_series_id,
status,
is_all_day,
is_all_courts,
selected_court_ids,
is_partner_search_enabled,
partner_search_utr_min,
partner_search_utr_max,
partner_search_players_needed,
partner_search_status
FROM public.bookings b
WHERE deleted_at IS NULL
AND status <> 'cancelled'::text
AND club_id = (
SELECT c.id
FROM public.clubs c
WHERE c.slug = 'bravos-sport-club'::text
LIMIT 1
);
VIEW: public.coach_booking_directory
CREATE OR REPLACE VIEW public.coach_booking_directory AS
SELECT
cp.id AS coach_id,
up.club_id,
cp.display_name,
cp.color,
cp.min_advance_hours,
cp.is_active
FROM public.coach_profiles cp
JOIN public.user_profiles up
ON up.id = cp.user_profile_id
JOIN public.clubs c
ON c.id = up.club_id
WHERE cp.is_active IS TRUE
AND c.slug = 'bravos-sport-club'::text;
VIEW: public.partner_finder_directory
CREATE OR REPLACE VIEW public.partner_finder_directory AS
SELECT
pfp.id,
pfp.club_id,
pfp.user_profile_id,
up.full_name,
COALESCE(pfp.utr_rating, up.utr_rating) AS utr_rating,
CASE
WHEN pfp.show_email_to_players IS TRUE THEN up.email
ELSE NULL::text
END AS email,
CASE
WHEN pfp.show_phone_to_players IS TRUE THEN up.phone
ELSE NULL::text
END AS phone,
pfp.show_email_to_players,
pfp.show_phone_to_players,
pfp.last_partner_search_booking_date
FROM public.partner_finder_profiles pfp
JOIN public.user_profiles up
ON up.id = pfp.user_profile_id
JOIN public.clubs c
ON c.id = pfp.club_id
WHERE pfp.partner_search_profile_enabled IS TRUE
AND c.slug = 'bravos-sport-club'::text;
AUTH TRIGGER: auth.on_auth_user_created_create_profile
CREATE TRIGGER on_auth_user_created_create_profile
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION handle_new_auth_user();
=== LIVE MANIFEST 5/5 — VÉGE ===