Szedervölgyi Clean Master Schema Source

Authoritative, read-only source package for the Supabase clean-master reconstruction.

Important: Do not modify Bravos LIVE. Do not deploy anything from this page automatically. Read all five manifests and the AI Studio instructions before building the clean master.

Direct files

Recommended AI Studio entry point: manifest-full.txt. If a single fetch is truncated, use the five individual manifest links above in order.

Full source on this page

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 ===