PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5.1
Yatra – Travel Booking & Tour Operator Software v3.0.5.1
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / app / Services / ItineraryPdfBuilder.php

ItineraryPdfBuilder.php in Yatra – Travel Booking & Tour Operator Software 3.0.5.1, at app/Services/ItineraryPdfBuilder.php

197 lines 10.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Services;
6
7 use Yatra\Helpers\FormatHelper;
8 use Yatra\Repositories\TripRepository;
9
10 /**
11 * Single source of truth for the customer-facing itinerary PDF.
12 *
13 * Two REST routes generate this exact same PDF — `/bookings/{id}/itinerary`
14 * (BookingsController, used when the booking has no payment row yet) and
15 * `/payments/{id}/itinerary` (PaymentGatewayController, used once payment
16 * exists). Before this class, each one composed its own template-data
17 * array, fetched its own trip, and re-implemented the same status mapping
18 * & date formatting — three subtle drift points where the two PDFs would
19 * disagree on the same booking (e.g. the payment-side one passed
20 * `default_font: DejaVu Sans` which overrode the locale-aware font,
21 * stripping every Nepali glyph in the booking-side PDF; only one path
22 * loaded the day-by-day itinerary; etc.).
23 *
24 * Callers now hand this class a normalized `$source` array — extracted
25 * from whichever data shape they have on hand (booking array vs joined
26 * payment object) — and we build + render the PDF identically for both.
27 */
28 class ItineraryPdfBuilder
29 {
30 private TripRepository $tripRepository;
31 private PdfService $pdfService;
32
33 public function __construct(?TripRepository $tripRepository = null, ?PdfService $pdfService = null)
34 {
35 $this->tripRepository = $tripRepository ?? new TripRepository();
36 $this->pdfService = $pdfService ?? new PdfService();
37 }
38
39 public function pdfService(): PdfService
40 {
41 return $this->pdfService;
42 }
43
44 /**
45 * Build the PDF binary for an itinerary.
46 *
47 * @param array<string,mixed> $source Booking-like array with at minimum
48 * `trip_id`, `booking_id`, `travel_date`, `created_at`,
49 * `contact_first_name`, `contact_last_name`, `contact_email`,
50 * `customer_name`, `booking_status`, `total_amount`,
51 * `amount_paid`, `amount_due`, `travelers_count`. Unknown keys
52 * are ignored.
53 * @return string Raw PDF bytes.
54 */
55 public function build(array $source): string
56 {
57 $tripId = (int) ($source['trip_id'] ?? 0);
58 $trip = null;
59 $itineraryDays = [];
60 if ($tripId > 0) {
61 // findWithRelations: needed so $trip carries the itinerary-day
62 // model for the "About this trip" sidebar. getItineraryDays
63 // returns the timeline used by the "Travel Timeline" section.
64 $trip = $this->tripRepository->findWithRelations($tripId)
65 ?: $this->tripRepository->find($tripId);
66 $itineraryDays = $this->tripRepository->getItineraryDays($tripId);
67 }
68
69 $bookingId = (int) ($source['booking_id'] ?? 0);
70 $bookingRef = $bookingId > 0
71 ? 'YTR-' . strtoupper(str_pad((string) $bookingId, 8, '0', STR_PAD_LEFT))
72 : '';
73
74 $createdAt = (string) ($source['created_at'] ?? $source['booking_date'] ?? '');
75 $bookingDate = $createdAt !== '' ? date_i18n(get_option('date_format'), strtotime($createdAt)) : '';
76 $travelRaw = (string) ($source['travel_date'] ?? '');
77 $travelDate = $travelRaw !== '' ? date_i18n(get_option('date_format'), strtotime($travelRaw)) : '';
78
79 $returnDate = '';
80 if ($travelRaw !== '' && $trip && !empty($trip->duration)) {
81 $returnDate = date_i18n(
82 get_option('date_format'),
83 strtotime($travelRaw . ' +' . (int) $trip->duration . ' days')
84 );
85 }
86
87 $statusRaw = strtolower((string) ($source['booking_status'] ?? $source['status'] ?? ''));
88 $statusClass = in_array($statusRaw, ['confirmed', 'completed', 'success'], true)
89 ? 'confirmed'
90 : (in_array($statusRaw, ['cancelled'], true) ? 'cancelled' : 'pending');
91
92 $customerName = trim(
93 (string) ($source['contact_first_name'] ?? '')
94 . ' '
95 . (string) ($source['contact_last_name'] ?? '')
96 );
97 if ($customerName === '') {
98 $customerName = (string) ($source['customer_name'] ?? __('Customer', 'yatra'));
99 }
100
101 $currency = SettingsService::getCurrency();
102
103 $tripFallbackTitle = (string) ($source['trip_title'] ?? __('Trip Booking', 'yatra'));
104
105 $templateData = [
106 'company_name' => SettingsService::get('company_name', get_bloginfo('name')),
107 'company_address' => SettingsService::get('company_address', ''),
108 'company_email' => SettingsService::get('company_email', get_option('admin_email')),
109 'company_phone' => SettingsService::get('company_phone', ''),
110 'customer_name' => $customerName,
111 'customer_email' => (string) ($source['contact_email'] ?? $source['customer_email'] ?? ''),
112 'booking_ref' => $bookingRef,
113 'booking_date' => $bookingDate,
114 'booking_status' => ucfirst($statusRaw !== '' ? $statusRaw : 'pending'),
115 'status_class' => $statusClass,
116 'trip_title' => $trip ? ((string) ($trip->title ?? $tripFallbackTitle)) : $tripFallbackTitle,
117 'trip_description'=> $trip ? (string) ($trip->description ?? $trip->content ?? '') : '',
118 /* translators: %d: trip duration in days. */
119 'trip_duration' => ($trip && !empty($trip->duration)) ? sprintf(__('%d days', 'yatra'), (int) $trip->duration) : '',
120 'trip_difficulty' => $trip ? (string) ($trip->difficulty_name ?? '') : '',
121 'trip_highlights' => $trip ? ($trip->highlights ?? $trip->trip_highlights ?? '') : '',
122 'trip_includes' => $trip ? ($trip->includes ?? $trip->trip_includes ?? '') : '',
123 'trip_excludes' => $trip ? ($trip->excludes ?? $trip->trip_excludes ?? '') : '',
124 'departure_location' => $trip ? (string) ($trip->departure_location ?? '') : '',
125 'destination' => $trip ? (string) ($trip->destination ?? '') : (string) ($source['destination'] ?? ''),
126 'travel_date' => $travelDate,
127 'return_date' => $returnDate,
128 'currency_symbol' => FormatHelper::getCurrencySymbol($currency),
129 'total_amount' => number_format((float) ($source['total_amount'] ?? 0), 2),
130 'amount_paid' => number_format((float) ($source['amount_paid'] ?? 0), 2),
131 'amount_due' => number_format((float) ($source['amount_due'] ?? 0), 2),
132 'traveler_count' => (int) ($source['travelers_count'] ?? $source['traveler_count'] ?? $source['travelers'] ?? 1),
133 'itinerary_days' => $itineraryDays,
134 // Real trip-specific Important Information fields, same
135 // ones the single-trip page surfaces in its "Important
136 // Information" section. The template renders only the
137 // rows that are non-empty — no more hardcoded "Comfortable
138 // clothing, walking shoes, sunscreen..." boilerplate.
139 'physical_requirements' => $trip ? (string) ($trip->physical_requirements ?? '') : '',
140 'visa_requirements' => $trip ? (string) ($trip->visa_requirements ?? '') : '',
141 'vaccination_requirements' => $trip ? (string) ($trip->vaccination_requirements ?? '') : '',
142 'cancellation_policy' => $trip ? (string) ($trip->cancellation_policy ?? '') : '',
143 'age_min' => $trip ? ($trip->age_min ?? null) : null,
144 'age_max' => $trip ? ($trip->age_max ?? null) : null,
145 'accommodation_type' => $trip ? (string) ($trip->accommodation_type ?? '') : '',
146 'accommodation_details' => $trip ? (string) ($trip->accommodation_details ?? '') : '',
147 'meal_plan' => $trip ? (string) ($trip->meal_plan ?? '') : '',
148 'transportation_included' => (bool) ($trip->transportation_included ?? false),
149 'pickup_location' => $trip ? (string) ($trip->pickup_location ?? '') : '',
150 'dropoff_location' => $trip ? (string) ($trip->dropoff_location ?? '') : '',
151 'transportation_details' => $trip ? (string) ($trip->transportation_details ?? '') : '',
152 ];
153
154 // Locale-aware font is resolved inside PdfService — do NOT
155 // pass a `default_font` override here, otherwise the Nepali /
156 // Hindi / Arabic fonts loaded for non-Latin locales get
157 // bypassed and we end up with the missing-glyph rectangles
158 // this builder was created to eliminate.
159 return $this->pdfService->renderTemplateToPdfSafely(
160 'pdf/itinerary.php',
161 $templateData,
162 ['paper' => 'A4', 'orientation' => 'portrait']
163 );
164 }
165
166 /**
167 * Convenience: normalize a `$payment` joined record (the shape
168 * PaymentRepository returns for itinerary endpoints — payment row
169 * with booking columns joined in) into the `$source` array `build()`
170 * accepts. Saves the caller from duplicating field mapping logic.
171 */
172 public function buildFromPaymentRecord(object $payment): string
173 {
174 return $this->build([
175 'trip_id' => $payment->trip_id ?? 0,
176 'booking_id' => $payment->booking_id ?? 0,
177 'created_at' => $payment->created_at ?? null,
178 'travel_date' => $payment->travel_date ?? null,
179 // Use the BOOKING status, not the payment status. Payment
180 // status can be "completed" while the booking itself is
181 // still "pending" admin confirmation — the itinerary
182 // header should reflect the booking, not the transaction.
183 'booking_status' => $payment->booking_status ?? $payment->status ?? null,
184 'contact_first_name' => $payment->contact_first_name ?? null,
185 'contact_last_name' => $payment->contact_last_name ?? null,
186 'contact_email' => $payment->contact_email ?? $payment->customer_email ?? null,
187 'customer_name' => $payment->customer_name ?? null,
188 'trip_title' => $payment->trip_title ?? null,
189 'destination' => $payment->destination ?? null,
190 'total_amount' => $payment->booking_total_amount ?? $payment->amount ?? 0,
191 'amount_paid' => $payment->booking_amount_paid ?? $payment->amount ?? 0,
192 'amount_due' => $payment->booking_amount_due ?? 0,
193 'travelers_count' => $payment->traveler_count ?? $payment->travelers_count ?? $payment->travelers ?? 1,
194 ]);
195 }
196 }
197