==================================================================================================== AI STUDIO INSTRUCTIONS ==================================================================================================== 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 ==================================================================================================== LIVE MANIFEST 1/5 — TABLES + COLUMNS ==================================================================================================== 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() ==================================================================================================== LIVE MANIFEST 2/5 — CONSTRAINTS + INDEXES + TRIGGERS ==================================================================================================== 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() ==================================================================================================== LIVE MANIFEST 3/5 — FUNCTIONS + RPC + TRIGGER FUNCTIONS ==================================================================================================== 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$ " ==================================================================================================== LIVE MANIFEST 4/5 — RLS + EXTENSIONS ==================================================================================================== 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 ==================================================================================================== === 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 ===