customerRepository = new CustomerRepository(); $this->bookingRepository = new BookingRepository(); $this->paymentRepository = new PaymentRepository(); } /** * Link any prior guest bookings made under a customer's email * to their newly-created WordPress user account. * * Without this, a customer who books as a guest first and only * registers later will never see those earlier bookings in My * Account — the rows persist with user_id=0 and the My Account * query filters by user_id. Wired to the `user_register` hook * (see Bootstrap::setupWordPressHooks). * * Returns the number of bookings that were linked. Returns 0 * silently on any failure — registration should never break on * a reconciliation glitch, and the operator can re-run the * reconciliation later via an admin tool if needed. */ public function linkGuestBookingsToUser(int $user_id): int { if ($user_id <= 0) { return 0; } $user = get_userdata($user_id); if (!$user || empty($user->user_email)) { return 0; } global $wpdb; $table = \Yatra\Database\Tables\BookingsTable::getTableName(); // Match by exact email + user_id IS NULL/0. Limited to bookings // not yet linked to any user so we never re-assign someone // else's account. $updated = $wpdb->query( $wpdb->prepare( "UPDATE `{$table}` SET user_id = %d, updated_at = %s WHERE contact_email = %s AND (user_id IS NULL OR user_id = 0)", $user_id, current_time('mysql'), $user->user_email ) ); if ($updated && $updated > 0) { // Side-effect hook so other modules (Pro: Channel Manager, // notifications, audit log) can react. Fires once per // registration with the count + the user object. do_action('yatra_guest_bookings_linked', (int) $user_id, (int) $updated, $user); } return (int) max(0, (int) $updated); } /** * Get customer statistics * * @return array */ public function getStats(): array { return $this->customerRepository->getStats(); } /** * Get paginated customers * * @param array $filters Filters * @return array */ public function getCustomers(array $filters = []): array { $result = $this->customerRepository->paginate($filters); $result['data'] = array_map([$this, 'formatCustomer'], $result['data']); return $result; } /** * Get single customer with details * * @param int $id Customer ID * @return array|null */ public function getCustomer(int $id): ?array { $customer = $this->customerRepository->find($id); if (!$customer) { return null; } return $this->formatCustomerWithDetails($customer); } /** * Get customer by email * * @param string $email Customer email * @return array|null */ public function getCustomerByEmail(string $email): ?array { $customer = $this->customerRepository->findByEmail($email); if (!$customer) { return null; } return $this->formatCustomer($customer); } /** * Get customer by WordPress user ID * * @param int $userId WordPress user ID * @return array|null */ public function getCustomerByUserId(int $userId): ?array { $customer = $this->customerRepository->findByUserId($userId); if (!$customer) { return null; } return $this->formatCustomer($customer); } /** * Account page /customers/me: Yatra customer when linked, otherwise WordPress user (display name, email). */ public function getAccountProfileForUser(int $userId): ?array { if ($userId <= 0) { return null; } $customer = $this->getCustomerByUserId($userId); if ($customer !== null) { return $customer; } $user = get_userdata($userId); if (!$user instanceof \WP_User) { return null; } return $this->buildProfileArrayFromWpUser($user); } /** * @return array */ private function buildProfileArrayFromWpUser(\WP_User $user): array { $first = trim((string) $user->first_name); $last = trim((string) $user->last_name); $fromParts = trim($first . ' ' . $last); $display = trim((string) $user->display_name); $name = $fromParts !== '' ? $fromParts : $display; if ($name === '') { $name = (string) $user->user_login; } return [ 'id' => 0, 'user_id' => (int) $user->ID, 'name' => $name, 'first_name' => $first, 'last_name' => $last, 'email' => (string) $user->user_email, 'phone' => '', 'country' => '', 'city' => '', 'status' => 'active', 'total_bookings' => 0, 'total_spent' => 0.0, 'loyalty_tier' => '', 'created_at' => $user->user_registered, 'last_booking_date' => null, 'registered_at' => $user->user_registered, ]; } /** * Create a new customer * * @param array $data Customer data * @return array {success: bool, customer_id?: int, message: string} */ public function createCustomer(array $data): array { // Validate required fields if (empty($data['email'])) { return ['success' => false, 'message' => __('Email is required.', 'yatra')]; } // Validate email format if (!is_email($data['email'])) { return ['success' => false, 'message' => __('Please provide a valid email address.', 'yatra')]; } // Check if customer already exists $existingCustomer = $this->customerRepository->findByEmail($data['email']); if ($existingCustomer) { return [ 'success' => false, 'message' => __('A customer with this email already exists.', 'yatra'), 'existing_id' => (int) $existingCustomer->id, ]; } // Create customer $customerId = $this->customerRepository->findOrCreate($data); if (!$customerId) { return ['success' => false, 'message' => __('Failed to create customer.', 'yatra')]; } return [ 'success' => true, 'customer_id' => $customerId, 'message' => __('Customer created successfully.', 'yatra'), ]; } /** * Update a customer * * @param int $id Customer ID * @param array $data Customer data * @return array {success: bool, message: string} */ public function updateCustomer(int $id, array $data): array { $customer = $this->customerRepository->find($id); if (!$customer) { return ['success' => false, 'message' => __('Customer not found.', 'yatra')]; } // Check email uniqueness if changing if (!empty($data['email']) && $data['email'] !== $customer->email) { $existingCustomer = $this->customerRepository->findByEmail($data['email']); if ($existingCustomer && (int) $existingCustomer->id !== $id) { return ['success' => false, 'message' => __('Email is already in use by another customer.', 'yatra')]; } } $updated = $this->customerRepository->updateCustomer($id, $data); if (!$updated) { return ['success' => false, 'message' => __('Failed to update customer.', 'yatra')]; } return [ 'success' => true, 'message' => __('Customer updated successfully.', 'yatra'), ]; } /** * Update customer status * * @param int $id Customer ID * @param string $status New status (active, inactive, blocked) * @return array {success: bool, message: string} */ public function updateStatus(int $id, string $status): array { $validStatuses = ['active', 'inactive', 'blocked']; if (!in_array($status, $validStatuses, true)) { return ['success' => false, 'message' => __('Invalid status.', 'yatra')]; } $customer = $this->customerRepository->find($id); if (!$customer) { return ['success' => false, 'message' => __('Customer not found.', 'yatra')]; } $updated = $this->customerRepository->updateCustomer($id, ['status' => $status]); if (!$updated) { return ['success' => false, 'message' => __('Failed to update status.', 'yatra')]; } return [ 'success' => true, 'message' => sprintf( /* translators: %s: new customer status. */ __('Customer status updated to %s.', 'yatra'), $status ), ]; } /** * Delete a customer * * @param int $id Customer ID * @return array {success: bool, message: string} */ public function deleteCustomer(int $id): array { $customer = $this->customerRepository->find($id); if (!$customer) { return ['success' => false, 'message' => __('Customer not found.', 'yatra')]; } // Check for existing bookings $bookings = $this->customerRepository->getCustomerBookings($id, 1); if (!empty($bookings)) { return [ 'success' => false, 'message' => __('Cannot delete customer with existing bookings. Consider deactivating instead.', 'yatra'), ]; } $deleted = $this->customerRepository->deleteCustomer($id); if (!$deleted) { return ['success' => false, 'message' => __('Failed to delete customer.', 'yatra')]; } return [ 'success' => true, 'message' => __('Customer deleted successfully.', 'yatra'), ]; } /** * Get customer's bookings * * @param int $customerId Customer ID * @param int $limit Limit results * @return array */ public function getCustomerBookings(int $customerId, int $limit = 10): array { return $this->customerRepository->getCustomerBookings($customerId, $limit); } /** * Get bookings by WordPress user ID (checks both customer_id and user_id) * * @param int $userId WordPress user ID * @param int $limit Limit results * @return array */ public function getBookingsByUserId(int $userId, int $limit = 10): array { // First, try to get customer and bookings by customer_id $customer = $this->getCustomerByUserId($userId); $bookings = []; if ($customer) { $bookings = $this->getCustomerBookings((int) $customer['id'], $limit); } // Also get bookings directly by user_id (in case bookings were made before customer record was created) $bookingsByUserId = $this->bookingRepository->findByUserId($userId, $limit); // And include bookings made via the same email address $emailBookings = []; $user = get_userdata($userId); if ($user && !empty($user->user_email)) { $emailBookings = $this->bookingRepository->findByContactEmail($user->user_email, $limit); } // Merge and deduplicate by booking ID $bookingIds = []; $allBookings = []; $sources = [$bookings, $bookingsByUserId, $emailBookings]; foreach ($sources as $collection) { foreach ($collection as $booking) { $bookingId = is_array($booking) ? ($booking['id'] ?? $booking['booking_id'] ?? null) : ($booking->id ?? null); if ($bookingId && !in_array($bookingId, $bookingIds, true)) { $bookingIds[] = $bookingId; $allBookings[] = $booking; } } } // Limit results if ($limit > 0 && count($allBookings) > $limit) { $allBookings = array_slice($allBookings, 0, $limit); } return $allBookings; } public function getBookingDetailsForUser(int $userId, int $bookingId): ?array { if ($userId <= 0 || $bookingId <= 0) { return null; } $booking = $this->bookingRepository->findWithTrip($bookingId); if (!$booking) { return null; } $user = get_userdata($userId); $userEmail = ($user && !empty($user->user_email)) ? (string) $user->user_email : ''; $customer = $this->getCustomerByUserId($userId); $customerId = $customer ? (int) ($customer['id'] ?? 0) : 0; $bookingUserId = isset($booking->user_id) ? (int) $booking->user_id : 0; $bookingCustomerId = isset($booking->customer_id) ? (int) $booking->customer_id : 0; $bookingEmail = isset($booking->contact_email) ? (string) $booking->contact_email : ''; $allowed = false; if ($bookingUserId > 0 && $bookingUserId === $userId) { $allowed = true; } if (!$allowed && $customerId > 0 && $bookingCustomerId > 0 && $bookingCustomerId === $customerId) { $allowed = true; } if (!$allowed && $userEmail !== '' && $bookingEmail !== '' && strtolower($userEmail) === strtolower($bookingEmail)) { $allowed = true; } if (!$allowed) { return null; } $emergencyContact = isset($booking->emergency_contact) ? maybe_unserialize($booking->emergency_contact) : null; if (is_string($emergencyContact)) { $decoded = json_decode($emergencyContact, true); if (is_array($decoded)) { $emergencyContact = $decoded; } } // Load travellers from the normalized meta tables — the SAME source the // admin booking screens use (TravellerRepository::getByBookingId, each // row carrying its dynamic `fields`). The previous code read // `$booking->travelers`, a column that does NOT exist (the schema only // has `travelers_count`), so `travelers_data` was ALWAYS empty and the // account-page "Travelers Information" card never rendered. Returns [] // for older bookings with no normalized rows (card simply hidden), so // this is safe for existing bookings. $travellerRepository = new \Yatra\Repositories\TravellerRepository(); $travelersList = $travellerRepository->getByBookingId($bookingId); if (!is_array($travelersList)) { $travelersList = []; } // contact_data is stored as JSON; decode to an array so the account // page can read custom contact fields (matches emergency_contact above // and BookingService::formatBookingWithDetails). maybe_unserialize is a // no-op on a JSON string, so a fallback json_decode is required. $contactData = isset($booking->contact_data) ? maybe_unserialize($booking->contact_data) : null; if (is_string($contactData)) { $decodedContact = json_decode($contactData, true); if (is_array($decodedContact)) { $contactData = $decodedContact; } } // Derive customer_* convenience fields. The admin React maps // contact_first_name + contact_last_name → customer_name at the // page level (see ViewBooking.tsx), so we mirror the same shape // server-side for the customer account view. Keeping ALL // original contact_* fields too so any caller depending on the // old shape (filters, integrations) stays unaffected. $contactFirst = (string) ($booking->contact_first_name ?? ''); $contactLast = (string) ($booking->contact_last_name ?? ''); $customerName = trim($contactFirst . ' ' . $contactLast); $details = [ 'id' => (int) ($booking->id ?? 0), 'reference' => $booking->reference ?? null, 'trip_id' => (int) ($booking->trip_id ?? 0), 'trip_title' => $booking->trip_title ?? null, 'trip_slug' => $booking->trip_slug ?? null, 'trip_url' => function_exists('yatra_get_trip_permalink') ? yatra_get_trip_permalink((int) ($booking->trip_id ?? 0)) : '', 'featured_image' => $booking->featured_image ?? null, 'created_at' => $booking->created_at ?? null, 'updated_at' => $booking->updated_at ?? null, 'travel_date' => $booking->travel_date ?? null, 'start_date' => $booking->start_date ?? $booking->travel_date ?? null, 'end_date' => $booking->end_date ?? null, 'travelers_count' => (int) ($booking->travelers_count ?? 0), 'total_amount' => (float) ($booking->total_amount ?? 0), 'amount_paid' => (float) ($booking->amount_paid ?? 0), 'amount_due' => (float) ($booking->amount_due ?? 0), 'currency' => $booking->currency ?? null, 'payment_status' => $booking->payment_status ?? null, 'status' => $booking->status ?? null, 'payment_gateway' => $booking->payment_gateway ?? null, 'contact_first_name' => $booking->contact_first_name ?? null, 'contact_last_name' => $booking->contact_last_name ?? null, 'contact_email' => $booking->contact_email ?? null, 'contact_phone' => $booking->contact_phone ?? null, 'contact_country' => $booking->contact_country ?? null, // Convenience aliases the React account page (BookingDetails.tsx) // reads as `customer_name`/`customer_email`/`customer_phone`. 'customer_name' => $customerName !== '' ? $customerName : null, 'customer_email' => $booking->contact_email ?? null, 'customer_phone' => $booking->contact_phone ?? null, 'special_requests' => $booking->special_requests ?? null, 'emergency_contact' => $emergencyContact, 'contact_data' => $contactData, 'travelers' => $travelersList, // React's BookingDetails reads `travelers_data` (same name // the admin ViewBooking screen uses); alias it here so the // "Travelers Information" card actually renders. 'travelers_data' => is_array($travelersList) ? $travelersList : [], 'payments' => [], ]; return apply_filters('yatra_customer_booking_details', $details, $booking, $userId); } /** * Get customer's payments * * @param int $customerId Customer ID * @param int $limit Limit results * @return array */ public function getCustomerPayments(int $customerId, int $limit = 50): array { $bookings = $this->customerRepository->getCustomerBookings($customerId, 1000); $bookingIds = array_map(static function($booking) { if (is_object($booking)) { return (int) ($booking->id ?? 0); } if (is_array($booking)) { return (int) ($booking['id'] ?? 0); } return 0; }, $bookings); // Include bookings linked via user ID or email (older bookings may not have customer_id) $customer = $this->customerRepository->find($customerId); if ($customer) { if (!empty($customer->user_id)) { $userBookings = $this->bookingRepository->findByUserId((int) $customer->user_id, 1000); $bookingIds = array_merge($bookingIds, array_map(static function($booking) { if (is_object($booking)) { return (int) ($booking->id ?? 0); } if (is_array($booking)) { return (int) ($booking['id'] ?? 0); } return 0; }, $userBookings)); } if (!empty($customer->email)) { $emailBookings = $this->bookingRepository->findByContactEmail($customer->email, 1000); $bookingIds = array_merge($bookingIds, array_map(static function($booking) { if (is_object($booking)) { return (int) ($booking->id ?? 0); } if (is_array($booking)) { return (int) ($booking['id'] ?? 0); } return 0; }, $emailBookings)); } } $bookingIds = array_values(array_unique(array_filter($bookingIds))); return $this->getPaymentsForBookingIds($bookingIds, $limit); } public function getPaymentsByUserId(int $userId, int $limit = 50): array { $bookings = $this->bookingRepository->findByUserId($userId, 1000); $user = get_userdata($userId); if ($user && !empty($user->user_email)) { $bookingsByEmail = $this->bookingRepository->findByContactEmail($user->user_email, 1000); $bookings = array_merge($bookings, $bookingsByEmail); } $bookingIds = array_map(static function($booking) { if (is_object($booking)) { return (int) ($booking->id ?? 0); } if (is_array($booking)) { return (int) ($booking['id'] ?? 0); } return 0; }, $bookings); return $this->getPaymentsForBookingIds($bookingIds, $limit); } private function getPaymentsForBookingIds(array $bookingIds, int $limit = 50): array { $bookingIds = array_values(array_filter(array_map('intval', $bookingIds))); // ensure ints if (empty($bookingIds)) { return []; } $customerRepository = new \Yatra\Repositories\CustomerRepository(); $payments = $customerRepository->getPaymentsForBookingIds($bookingIds, $limit); // Route the customer-facing payments through the shared formatter so // they emit the same field shape the rest of the app uses — most // importantly the React Account → Payments tab's aliases // (`date`, `method`, `reference`, `type`, `booking_number`, // `payment_date`, `payment_number`). The previous inline formatter // omitted those keys, which is why the Payments cards rendered // "N/A" for the date, blank for the method, and an empty space // above the "Booking:" label. $paymentService = new \Yatra\Services\PaymentService(); return array_map(static function ($payment) use ($paymentService) { $row = $paymentService->formatPayment($payment); // Preserve the booking-amount summary fields used by the React // payments tab to decide whether to render a "Pay Remaining" CTA. // formatPayment doesn't know about these (they come from the // CustomerRepository JOIN); attach them here so we keep the // canonical shape AND the extra context. $row['booking_amount_due'] = (float) ($payment->booking_amount_due ?? 0); $row['booking_amount_paid'] = (float) ($payment->booking_amount_paid ?? 0); $row['booking_total_amount'] = (float) ($payment->booking_total_amount ?? 0); return $row; }, $payments); } public function getDocumentsForBookings(array $bookings, int $customerId = 0): array { $documents = []; // Process each booking individually for vouchers and itineraries // but group by trip for downloads $tripsWithBookings = []; foreach ($bookings as $booking) { $bookingId = is_object($booking) ? (int) ($booking->id ?? 0) : (int) ($booking['id'] ?? $booking['booking_id'] ?? 0); $tripId = is_object($booking) ? (int) ($booking->trip_id ?? 0) : (int) ($booking['trip_id'] ?? 0); $tripTitle = is_object($booking) ? (string) ($booking->trip_title ?? '') : (string) ($booking['trip_title'] ?? ''); $reference = is_object($booking) ? ($booking->reference ?? null) : ($booking['reference'] ?? null); $status = is_object($booking) ? (string) ($booking->status ?? '') : (string) ($booking['status'] ?? ''); $createdAt = is_object($booking) ? (string) ($booking->created_at ?? '') : (string) ($booking['created_at'] ?? ''); if ($bookingId <= 0) { continue; } // Store trip info for downloads (grouped by trip) if ($tripId > 0 && !isset($tripsWithBookings[$tripId])) { $tripsWithBookings[$tripId] = [ 'booking_id' => $bookingId, 'trip_title' => $tripTitle, 'reference' => $reference, 'status' => $status, 'created_at' => $createdAt, ]; } // Get payments for this booking (invoices per payment) $payments = $this->paymentRepository->findByBookingId($bookingId); foreach ($payments as $payment) { $paymentId = (int) ($payment->id ?? 0); if ($paymentId <= 0) { continue; } $paymentStatus = (string) ($payment->status ?? ''); if (!in_array($paymentStatus, ['paid', 'completed', 'success'], true)) { continue; } $docRef = $reference ?: $bookingId; // Invoice per payment $invoiceUrl = rest_url('yatra/v1/payment/' . $paymentId . '/invoice'); $invoiceUrl = add_query_arg('_wpnonce', wp_create_nonce('wp_rest'), $invoiceUrl); $documents[] = [ 'id' => 'invoice-payment-' . $paymentId, 'name' => sprintf( /* translators: %s: booking reference or ID. */ __('Invoice #%s.pdf', 'yatra'), $docRef ), 'trip_title' => $tripTitle, 'category' => 'invoice', 'updated_at' => $payment->created_at ?? $createdAt ?: date('Y-m-d H:i:s'), 'url' => $invoiceUrl, 'booking_id' => $bookingId, 'payment_id' => $paymentId, ]; } // Voucher per booking if ($status === 'confirmed') { $docRef = $reference ?: $bookingId; $voucherUrl = rest_url('yatra/v1/bookings/' . $bookingId . '/voucher'); $voucherUrl = add_query_arg('_wpnonce', wp_create_nonce('wp_rest'), $voucherUrl); $documents[] = [ 'id' => 'voucher-' . $bookingId, // Booking-based ID 'name' => sprintf( /* translators: %s: booking reference or ID. */ __('Travel Voucher #%s.pdf', 'yatra'), $docRef ), 'trip_title' => $tripTitle, 'category' => 'voucher', 'updated_at' => $createdAt ?: date('Y-m-d H:i:s'), 'url' => $voucherUrl, 'booking_id' => $bookingId, ]; // Itinerary per booking $itineraryUrl = rest_url('yatra/v1/bookings/' . $bookingId . '/itinerary'); $itineraryUrl = add_query_arg('_wpnonce', wp_create_nonce('wp_rest'), $itineraryUrl); $documents[] = [ 'id' => 'itinerary-' . $bookingId, // Booking-based ID 'name' => sprintf( /* translators: %s: booking reference or ID. */ __('Travel Itinerary #%s.pdf', 'yatra'), $docRef ), 'trip_title' => $tripTitle, 'category' => 'itinerary', 'updated_at' => $createdAt ?: date('Y-m-d H:i:s'), 'url' => $itineraryUrl, 'booking_id' => $bookingId, ]; } } usort($documents, function ($a, $b) { return strtotime($b['updated_at']) - strtotime($a['updated_at']); }); // Apply downloads filter (which groups by trip) $documents = apply_filters('yatra_customer_documents', $documents, $bookings, $customerId); return is_array($documents) ? $documents : []; } /** * Get customer's documents (invoices, vouchers, itineraries) * * @param int $customerId Customer ID * @return array */ public function getCustomerDocuments(int $customerId): array { // Get customer's bookings $bookings = $this->customerRepository->getCustomerBookings($customerId, 1000); // Also include bookings linked via user_id/email (older bookings may not have customer_id) $customer = $this->customerRepository->find($customerId); if ($customer) { if (!empty($customer->user_id)) { $userBookings = $this->bookingRepository->findByUserId((int) $customer->user_id, 1000); $bookings = array_merge($bookings, $userBookings); } if (!empty($customer->email)) { $emailBookings = $this->bookingRepository->findByContactEmail((string) $customer->email, 1000); $bookings = array_merge($bookings, $emailBookings); } } // Deduplicate by booking id $seen = []; $unique = []; foreach ($bookings as $b) { $id = is_object($b) ? ($b->id ?? null) : ($b['id'] ?? $b['booking_id'] ?? null); if ($id && !isset($seen[$id])) { $seen[$id] = true; $unique[] = $b; } } return $this->getDocumentsForBookings($unique, $customerId); } /** * Get customer's support tickets * * @param int $customerId Customer ID * @return array */ public function getCustomerSupportTickets(int $customerId): array { // For now, return empty array as support tickets system may not be implemented yet // This can be extended when support ticket system is added return []; } /** * Merge two customer records * * @param int $sourceId Source customer ID (will be deleted) * @param int $targetId Target customer ID (will be kept) * @return array {success: bool, message: string} */ public function mergeCustomers(int $sourceId, int $targetId): array { if ($sourceId === $targetId) { return ['success' => false, 'message' => __('Cannot merge customer with itself.', 'yatra')]; } $source = $this->customerRepository->find($sourceId); $target = $this->customerRepository->find($targetId); if (!$source || !$target) { return ['success' => false, 'message' => __('One or both customers not found.', 'yatra')]; } // Update all bookings to point to target customer $this->bookingRepository->updateCustomerBookings($sourceId, $targetId); // Update target customer stats $this->customerRepository->updateCustomer($targetId, [ 'total_bookings' => (int) $target->total_bookings + (int) $source->total_bookings, 'total_spent' => (float) $target->total_spent + (float) $source->total_spent, ]); // Delete source customer $this->customerRepository->deleteCustomer($sourceId); return [ 'success' => true, 'message' => __('Customers merged successfully.', 'yatra'), ]; } /** * Format customer for API response * * @param object $customer Raw customer data * @return array */ private function formatCustomer(object $customer): array { $name = trim((string) ($customer->first_name ?? '') . ' ' . (string) ($customer->last_name ?? '')); if ($name === '') { $uid = (int) ($customer->user_id ?? 0); if ($uid > 0) { $u = get_userdata($uid); if ($u instanceof \WP_User) { $name = trim((string) $u->display_name); if ($name === '') { $name = trim($u->first_name . ' ' . $u->last_name); } if ($name === '') { $name = (string) $u->user_login; } } } } if ($name === '' && !empty($customer->email)) { $local = explode('@', (string) $customer->email)[0] ?? ''; $name = $local !== '' ? $local : $name; } $created = $customer->created_at ?? ''; return [ 'id' => (int) $customer->id, 'user_id' => $customer->user_id ? (int) $customer->user_id : null, 'name' => $name, 'first_name' => $customer->first_name ?? '', 'last_name' => $customer->last_name ?? '', 'email' => $customer->email, 'phone' => $customer->phone ?? '', 'country' => $customer->country ?? '', 'city' => $customer->city ?? '', // Address belongs to the account profile too. Without it here the // account page never received the saved value — which is exactly why // city/country updated but address didn't. 'address' => $customer->address ?? '', 'status' => $customer->status ?? 'active', 'total_bookings' => (int) ($customer->total_bookings ?? 0), 'total_spent' => (float) ($customer->total_spent ?? 0), 'loyalty_tier' => $customer->loyalty_tier ?? 'bronze', 'created_at' => $created, 'registered_at' => $created, 'last_booking_date' => $customer->last_booking_date ?? null, ]; } /** * Format customer with all details * * @param object $customer Raw customer data * @return array */ private function formatCustomerWithDetails(object $customer): array { $formatted = $this->formatCustomer($customer); // Add additional fields $formatted['secondary_phone'] = $customer->secondary_phone ?? null; $formatted['address'] = $customer->address ?? null; $formatted['state'] = $customer->state ?? null; $formatted['postal_code'] = $customer->postal_code ?? null; $formatted['date_of_birth'] = $customer->date_of_birth ?? null; $formatted['gender'] = $customer->gender ?? null; $formatted['nationality'] = $customer->nationality ?? null; // Emergency contact $formatted['emergency_contact'] = [ 'name' => $customer->emergency_name ?? null, 'phone' => $customer->emergency_phone ?? null, 'relationship' => $customer->emergency_relationship ?? null, ]; // Preferences $formatted['dietary_requirements'] = $customer->dietary_requirements ?? null; $formatted['medical_conditions'] = $customer->medical_conditions ?? null; $formatted['special_needs'] = $customer->special_needs ?? null; $formatted['preferred_language'] = $customer->preferred_language ?? 'en'; $formatted['preferred_currency'] = $customer->preferred_currency ?? 'USD'; // Marketing $formatted['newsletter_optin'] = (bool) ($customer->newsletter_optin ?? false); $formatted['marketing_optin'] = (bool) ($customer->marketing_optin ?? false); $formatted['source'] = $customer->source ?? null; // Stats $formatted['total_travelers'] = (int) ($customer->total_travelers ?? 0); $formatted['last_travel_date'] = $customer->last_travel_date ?? null; $formatted['loyalty_points'] = (int) ($customer->loyalty_points ?? 0); // Gateway IDs $formatted['stripe_customer_id'] = $customer->stripe_customer_id ?? null; $formatted['paypal_customer_id'] = $customer->paypal_customer_id ?? null; $formatted['razorpay_customer_id'] = $customer->razorpay_customer_id ?? null; // Notes $formatted['notes'] = $customer->notes ?? null; // Recent bookings $formatted['recent_bookings'] = $customer->recent_bookings ?? []; // Timestamps $formatted['updated_at'] = $customer->updated_at ?? null; $formatted['last_login_at'] = $customer->last_login_at ?? null; $formatted['verified_at'] = $customer->verified_at ?? null; return $formatted; } }