rules->get()['max_daily_customer_bookings'];return ['count'=>$user?$this->bookings->dailyCount($user,Time::sql(Time::day($date)),Time::sql(Time::nextDay($date))):0,'limit'=>$limit]; } public function cutoff(int $start,?DateTimeInterface $clock=null): array { $now=$clock?DateTimeImmutable::createFromInterface($clock):new DateTimeImmutable('now',wp_timezone());$settings=$this->rules->get(); if($start<=$now->getTimestamp())return $this->restriction('abece_booking_booking_in_past',__('Bookings must start in the future.','abece-booking')); $closed=match($settings['online_booking_cutoff_mode']){'hours_before'=>$start-$now->getTimestamp()<($settings['online_booking_cutoff_hours']??0)*3600,'previous_day'=>Time::timestamp($start)->format('Y-m-d')<=$now->setTimezone(wp_timezone())->format('Y-m-d'),default=>false}; if(!$closed)return $this->restriction(null,null); /* translators: %s: venue contact telephone number. */ $phone=$settings['booking_contact_phone'];$message=$phone!==''?sprintf(__('This time can no longer be booked online. To book, call %s.','abece-booking'),$phone):__('This time can no longer be booked online. Please contact the operator.','abece-booking'); return $this->restriction('abece_booking_online_booking_closed',$message)+['booking_contact_phone'=>$phone,'booking_contact_tel'=>$phone!==''?'tel:'.preg_replace('/[^+0-9]/','',$phone):null]; } public function decision(int $start,?array $daily=null,?DateTimeInterface $clock=null): array { $result=$this->cutoff($start,$clock);if(!$result['can_book_online'])return $result; /* translators: %d: booking count or limit for the selected day. */ if($daily&&$daily['count']>=$daily['limit'])return $this->restriction('daily_booking_limit_reached',sprintf(__('You already have the maximum of %d bookings for this day.','abece-booking'),$daily['limit']));return $result; } public function assertCreate(int $start,?int $user,bool $admin,bool $confirmed=false): ?array { if($start<=time())throw new CatalogException('abece_booking_booking_in_past',__('Bookings must start in the future.','abece-booking'),400,'start_time'); $daily=$user?$this->daily($user,Time::timestamp($start)->format('Y-m-d')):null; /* translators: %d: booking count or limit for the selected day. */ if($admin){if($daily&&$daily['count']>=$daily['limit']){if(!$confirmed)throw new CatalogException('abece_booking_daily_limit_confirmation_required',sprintf(__('This customer already has %d active bookings for this day. Confirm to continue.','abece-booking'),$daily['count']),409,'confirm_daily_limit');return $daily;}return null;} $result=$this->decision($start,$daily);if(!$result['can_book_online'])throw new CatalogException($result['booking_restriction_code'],$result['booking_restriction_message'],409);return null; } public function debtWarning(array $customer): ?array { if(!$this->receivables)return null; $balance=$this->receivables->getOutstandingForCustomer($customer); return $balance['amounts_by_currency']?['amounts_by_currency'=>$balance['amounts_by_currency']]:null; } public function assertDebt(array $customer,bool $admin,bool $confirmed=false): ?array { if(!$this->receivables)return null; $balance=$this->receivables->getOutstandingForCustomer($customer); if($admin&&$balance['amounts_by_currency']){ if(!$confirmed)throw new CatalogException('abece_booking_outstanding_balance_confirmation_required',__('This customer has an outstanding amount from previous bookings. Confirm to continue.','abece-booking'),409,'confirm_outstanding_balance'); return ['customer_identity'=>array_intersect_key($customer,array_flip(['customer_user_id','customer_member_id','external_contact_id'])),'amounts_by_currency'=>$balance['amounts_by_currency']]; } if(!$admin&&$balance['blocked'])throw new CatalogException('outstanding_balance',__('You have an unpaid amount from a previous booking. You cannot create another booking until it is paid.','abece-booking'),409); return null; } public function lockMembers(array $customer,array $players):void{$this->usage?->lock($customer,$players);} public function assess(array $customer,array $players,array $interval,array $resource,bool $admin,array $input=[],bool $enforce=false,?int $exclude=null):array{ $blockers=[];$audits=[];$confirmedFlags=[];if($admin)foreach(['confirm_daily_limit','confirm_outstanding_balance','confirm_usage_limit','confirm_customer_restriction']as $flag)$confirmedFlags[$flag]=CatalogValidator::boolean($input[$flag]??false,$flag);$start=Time::fromSql($interval['starts_at']); $daily=$customer['customer_user_id']?$this->daily($customer['customer_user_id'],Time::timestamp($start)->format('Y-m-d')):null; if($exclude&&$daily){$old=$this->bookings->find($exclude);if($old&&$old['customer_user_id']===$customer['customer_user_id']&&$old['booking_source']==='web'&&in_array($old['status'],['pending','confirmed'],true)&&Time::timestamp(Time::fromSql($old['starts_at']))->format('Y-m-d')===Time::timestamp($start)->format('Y-m-d'))$daily['count']=max(0,$daily['count']-1);} $dailyWarning=$daily&&$daily['count']>=$daily['limit']?$daily:null; $balance=$this->receivables?->getOutstandingForCustomer($customer);$debt=$balance&&$balance['amounts_by_currency']?['amounts_by_currency'=>$balance['amounts_by_currency']]:null; $plans=$this->usage?->quoteBookingUsage($customer,$players,$interval,$resource,$exclude)??[]; $presented=$this->usage?->presentation($plans,$admin,$customer['customer_member_id']??null)??[];$usageWarnings=array_values(array_filter($presented,fn($p)=>$p['exceeded'])); $restrictionWarnings=$this->restrictions?->warnings($customer,$players,$interval,$admin)??[]; if(!$admin){$decision=$this->cutoff($start);if(!$decision['can_book_online'])$blockers[]=['code'=>$decision['booking_restriction_code'],'message'=>$decision['booking_restriction_message']];} $policies=[ /* translators: %d: booking count or limit for the selected day. */ ['present'=>$dailyWarning!==null,'flag'=>'confirm_daily_limit','code'=>$admin?'abece_booking_daily_limit_confirmation_required':'daily_booking_limit_reached','message'=>$admin?sprintf(__('This customer already has %d active bookings for this day. Confirm to continue.','abece-booking'),$daily['count']??0):sprintf(__('You already have the maximum of %d bookings for this day.','abece-booking'),$daily['limit']??0),'event'=>'daily_limit_overridden','metadata'=>$dailyWarning], ['present'=>$admin?$debt!==null:($balance['blocked']??false),'flag'=>'confirm_outstanding_balance','code'=>$admin?'abece_booking_outstanding_balance_confirmation_required':'outstanding_balance','message'=>$admin?__('This customer has an outstanding amount from previous bookings. Confirm to continue.','abece-booking'):__('You have an unpaid amount from a previous booking. You cannot create another booking until it is paid.','abece-booking'),'event'=>'booking_created_with_outstanding_balance_override','metadata'=>['customer_identity'=>array_intersect_key($customer,array_flip(['customer_user_id','customer_member_id','external_contact_id']))]+($debt??[])], ['present'=>count($usageWarnings)>0,'flag'=>'confirm_usage_limit','code'=>'usage_limit_exceeded','message'=>__('The booking exceeds a member playing time limit.','abece-booking'),'event'=>'usage_limit_override','metadata'=>['violations'=>array_map(fn($p)=>array_intersect_key($p,array_flip(['member_id','rule_id','period_start','period_end','used_minutes','requested_minutes','projected_minutes','limit_minutes','counting_mode','used_seconds','requested_seconds','projected_seconds','limit_seconds','effective_participant_count'])),array_values(array_filter($plans,fn($p)=>$p['exceeded']))) ]], ['present'=>count($restrictionWarnings)>0,'flag'=>'confirm_customer_restriction','code'=>'customer_restricted','message'=>__('A customer restriction blocks this booking.','abece-booking'),'event'=>'customer_restriction_override','metadata'=>['restriction_ids'=>array_column($restrictionWarnings,'id')]], ]; foreach($policies as $policy){if(!$policy['present'])continue;$confirmed=$admin&&$confirmedFlags[$policy['flag']];if($admin&&$confirmed)$audits[]=['event'=>$policy['event'],'metadata'=>$policy['metadata']];elseif(!$admin||$enforce)$blockers[]=['code'=>$policy['code'],'message'=>$policy['message'],'field'=>$admin?$policy['flag']:null];} $result=['participant_share_applies'=>count(array_filter($plans,fn($p)=>$p['counting_mode']==='participant_share'))>0,'usage'=>$presented,'usage_limit_warnings'=>$usageWarnings,'customer_restriction_warnings'=>$restrictionWarnings,'outstanding_balance_warning'=>$admin?$debt:null,'daily_limit_warning'=>$admin?$dailyWarning:null,'blockers'=>$blockers]; if($blockers&&($enforce||!$admin)){$first=$blockers[0];$messages=array_column($blockers,'message');if(count($blockers)===1&&$first['code']==='usage_limit_exceeded')$messages=array_column($usageWarnings,'message');if(!$admin&&count($blockers)===1&&$first['code']==='customer_restricted')$messages=array_column($restrictionWarnings,'message');throw new CatalogException($first['code'],implode(' ',$messages),409,$first['field']??null,$result);} return $result+['audits'=>$audits]; } public function restrictionWarnings(array $customer,array $players,array $interval):array{return $this->restrictions?->warnings($customer,$players,$interval,true)??[];} public function presentation(array $available,int $user,?DateTimeInterface $clock=null): array { $daily=$this->daily($user,$available['date']);$available['daily_bookings']=$daily;$available['restricted_intervals']=[];$allowed=[]; foreach($available['intervals'] as $slot){$decision=$this->decision(strtotime($slot['starts_at']),$daily,$clock);if($decision['can_book_online'])$allowed[]=$slot+$decision;else $available['restricted_intervals'][]=$slot+$decision;} $available['intervals']=$allowed;return $available; } private function restriction(?string $code,?string $message): array {return ['can_book_online'=>$code===null,'booking_restriction_code'=>$code,'booking_restriction_message'=>$message];} }