membership->isUserMember( $user_id, $this->day( $date ) ); } public function memberId(int $user_id,string $date): ?int { $active=$this->membership->getActiveMembershipForUser($user_id,$this->day($date)); return isset($active['member_id'])?(int)$active['member_id']:null; } public function eligible( array $customer, string $date ): bool { return (bool) ( $customer['customer_member_id'] && $this->membership->getActiveMembershipForMember( $customer['customer_member_id'], $this->day( $date ) ) ); } /** Keep historical customer identity; eligibility is re-evaluated separately, never granted by identity alone. */ public function forEdit( array $input, array $booking, string $date, int $actor ): array { $this->requireAdmin( $actor ); if ( $input['customer_type'] === self::type( $booking ) && $input['customer_user_id'] === $booking['customer_user_id'] && $input['customer_member_id'] === $booking['customer_member_id'] ) { $source = CatalogValidator::choice( $input['booking_source'], 'booking_source', 'web' === $booking['booking_source'] ? [ 'web', 'admin', 'phone' ] : [ 'admin', 'phone' ] ); if ( 'phone' === $source && 'external' !== self::type( $booking ) ) { CatalogValidator::invalid( 'booking_source', __( 'Phone bookings must be external bookings.', 'abece-booking' ) ); } return [ 'customer_user_id' => $booking['customer_user_id'], 'customer_member_id' => $booking['customer_member_id'], 'booking_source' => $source ]; } if ( 'web' === $input['booking_source'] && 'web' === $booking['booking_source'] ) { $input['booking_source'] = 'admin'; } return $this->admin( $input, $date, $actor ); } public static function type( array $b ): string { return $b['customer_member_id'] ? 'member' : ( $b['customer_user_id'] ? 'registered_non_member' : 'external' ); } public function self( int $user_id, string $date ): array { BookingValidator::login( $user_id ); $this->profiles->requireComplete( $user_id ); $membership = $this->membership->getActiveMembershipForUser( $user_id, $this->day( $date ) ); return [ 'customer_user_id' => $user_id, 'customer_member_id' => $membership['member_id'] ?? null, 'booking_source' => 'web' ]; } public function admin( array $input, string $date, int $actor ): array { $this->requireAdmin( $actor ); $type = CatalogValidator::choice( $input['customer_type'] ?? null, 'customer_type', [ 'member', 'registered_non_member', 'external' ] ); $user_id = CatalogValidator::integer( $input['customer_user_id'] ?? null, 'customer_user_id', 1, PHP_INT_MAX, true ); $member_id = CatalogValidator::integer( $input['customer_member_id'] ?? null, 'customer_member_id', 1, PHP_INT_MAX, true ); $source = CatalogValidator::choice( $input['booking_source'] ?? ( 'external' === $type ? 'phone' : 'admin' ), 'booking_source', [ 'admin', 'phone' ] ); if ( 'member' === $type ) { $m = $member_id ? $this->members->find( $member_id ) : null; if ( ! $m || ! $this->membership->getActiveMembershipForMember( $member_id, $this->day( $date ) ) ) { CatalogValidator::invalid( 'customer_member_id', __( 'Select a member eligible on the booking date.', 'abece-booking' ) ); } $link = $m['wordpress_user'] ? $m['wp_user_id'] : null; if ( null !== $user_id && $link !== $user_id ) { CatalogValidator::invalid( 'customer_user_id', __( 'The customer account must match the selected member.', 'abece-booking' ) ); } $user_id = $link; } elseif ( 'registered_non_member' === $type ) { if ( ! $user_id || ! $this->users->find( $user_id ) || $this->isMember( $user_id, $date ) || null !== $member_id ) { CatalogValidator::invalid( 'customer_user_id', __( 'Select a registered user without membership on the booking date.', 'abece-booking' ) ); } } elseif ( null !== $user_id || null !== $member_id ) { CatalogValidator::invalid( 'customer_type', __( 'External bookings must not have a customer account or member.', 'abece-booking' ) ); } if ( 'phone' === $source && 'external' !== $type ) { CatalogValidator::invalid( 'booking_source', __( 'Phone bookings must be external bookings.', 'abece-booking' ) ); } return [ 'customer_user_id' => $user_id, 'customer_member_id' => $member_id, 'booking_source' => $source ]; } public function searchMembers( mixed $query, string $date, int $actor, bool $admin_projection = false ): array { MembershipModeService::requireEnabled(); BookingValidator::login( $actor ); $day = $this->day( $date ); if ( ! user_can( $actor, Capabilities::MANAGE_BOOKINGS ) && ! $this->isMember( $actor, $date ) ) { throw new CatalogException( 'abece_booking_member_search_forbidden', __( 'Only eligible members and booking managers can search for members.', 'abece-booking' ), 403 ); } if ( $admin_projection ) { $this->requireAdmin( $actor ); } $query = $this->query( $query ); $result = []; $page = 1; do { $items = $this->members->bookingSearch( $query, $page++, 100 ); foreach ( $items as $m ) { if ( $this->membership->getActiveMembershipForMember( (int) $m['id'], $day ) ) { $row = [ 'member_id' => (int) $m['id'], 'display_name' => trim( $m['first_name'] . ' ' . $m['last_name'] ) ]; if ( $m['member_number'] ) { $row['member_number'] = $m['member_number']; } if ( $admin_projection ) { $member = $this->members->find( (int) $m['id'] ); $row['customer_user_id'] = $member['wordpress_user'] ? $member['wp_user_id'] : null; } $result[] = $row; } if ( count( $result ) === 20 ) { break; } } } while ( count( $items ) === 100 && count( $result ) < 20 ); return $result; } public function searchCustomers( mixed $query, string $date, string $type, int $actor ): array { $this->requireAdmin( $actor ); CatalogValidator::choice( $type, 'type', [ 'member', 'registered_non_member' ] ); if ( 'member' === $type ) { return $this->searchMembers( $query, $date, $actor, true ); } $query = $this->query( $query ); $this->day( $date ); $page = 1; $result = []; do { $items = $this->users->bookingSearch( $query, $page++, 100 ); foreach ( $items['items'] as $user ) { if ( ! $this->isMember( $user['id'], $date ) ) { $result[] = [ 'customer_user_id' => $user['id'], 'display_name' => $user['display_name'], 'email' => $user['email'] ]; } if ( count( $result ) === 20 ) { break; } } } while ( ( $page - 1 ) * 100 < $items['total'] && count( $result ) < 20 ); return $result; } public function requireAdmin( int $actor ): void { BookingValidator::login( $actor ); if ( ! user_can( $actor, Capabilities::MANAGE_BOOKINGS ) ) { throw new CatalogException( 'abece_booking_booking_forbidden', __( 'You do not have permission to manage bookings.', 'abece-booking' ), 403 ); } } private function query( mixed $query ): string { $query = MembershipValidator::text( $query, 'q', 100 ); if ( mb_strlen( $query ) < 2 ) { CatalogValidator::invalid( 'q', __( 'Enter at least two characters to search for members.', 'abece-booking' ) ); } return $query; } }