PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.14.1
Yatra – Travel Booking & Tour Operator Software v3.0.14.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.14.1, at app/Services/ItineraryPdfBuilder.php

222 lines 12.1 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 // Departure / return date. The booking already stores the real
80 // trip end (`end_date`, written at booking time from the selected
81 // departure or start + duration), so prefer that authoritative
82 // value rather than re-deriving one — this is what keeps the
83 // travel timeline showing the ACTUAL booked end date instead of
84 // repeating the arrival date. Only when no stored end_date exists
85 // do we derive it, and then the SAME way
86 // BookingRepository::calculateEndDate does: start + (duration_days
87 // - 1). A 5-day trip therefore ends on day 5, not day 6 — the old
88 // "+ duration_days" was off by one and implied an extra night.
89 $returnDate = '';
90 $endRaw = (string) ($source['end_date'] ?? '');
91 if ($endRaw === '' && $travelRaw !== '' && $trip && (int) ($trip->duration_days ?? 0) > 1) {
92 $endRaw = date(
93 'Y-m-d',
94 strtotime($travelRaw . ' +' . ((int) $trip->duration_days - 1) . ' days')
95 );
96 }
97 // Expose a return date only when it genuinely differs from the
98 // arrival date. Single-day bookings store end_date == start_date,
99 // so the timeline renders one item instead of two identical dates.
100 if ($endRaw !== '' && $endRaw !== $travelRaw) {
101 $returnDate = date_i18n(get_option('date_format'), strtotime($endRaw));
102 }
103
104 $statusRaw = strtolower((string) ($source['booking_status'] ?? $source['status'] ?? ''));
105 $statusClass = in_array($statusRaw, ['confirmed', 'completed', 'success'], true)
106 ? 'confirmed'
107 : (in_array($statusRaw, ['cancelled'], true) ? 'cancelled' : 'pending');
108
109 $customerName = trim(
110 (string) ($source['contact_first_name'] ?? '')
111 . ' '
112 . (string) ($source['contact_last_name'] ?? '')
113 );
114 if ($customerName === '') {
115 $customerName = (string) ($source['customer_name'] ?? __('Customer', 'yatra'));
116 }
117
118 $currency = SettingsService::getCurrency();
119
120 $tripFallbackTitle = (string) ($source['trip_title'] ?? __('Trip Booking', 'yatra'));
121
122 $templateData = [
123 'company_name' => SettingsService::get('company_name', get_bloginfo('name')),
124 'company_address' => SettingsService::get('company_address', ''),
125 'company_address_lines' => \Yatra\Helpers\FormatHelper::companyAddressLines(),
126 'company_email' => SettingsService::get('company_email', get_option('admin_email')),
127 'company_phone' => SettingsService::get('company_phone', ''),
128 'customer_name' => $customerName,
129 'customer_email' => (string) ($source['contact_email'] ?? $source['customer_email'] ?? ''),
130 'booking_ref' => $bookingRef,
131 'booking_date' => $bookingDate,
132 'booking_status' => ucfirst($statusRaw !== '' ? $statusRaw : 'pending'),
133 'status_class' => $statusClass,
134 'trip_title' => $trip ? ((string) ($trip->title ?? $tripFallbackTitle)) : $tripFallbackTitle,
135 'trip_description'=> $trip ? (string) ($trip->description ?? $trip->content ?? '') : '',
136 // Duration is duration_days/duration_nights (no `duration` column).
137 'trip_duration' => $trip
138 ? yatra_format_duration((int) ($trip->duration_days ?? 0), isset($trip->duration_nights) ? (int) $trip->duration_nights : null)
139 : '',
140 'trip_difficulty' => $trip ? (string) ($trip->difficulty_name ?? '') : '',
141 'trip_highlights' => $trip ? ($trip->highlights ?? $trip->trip_highlights ?? '') : '',
142 'trip_includes' => $trip ? ($trip->includes ?? $trip->trip_includes ?? '') : '',
143 'trip_excludes' => $trip ? ($trip->excludes ?? $trip->trip_excludes ?? '') : '',
144 'departure_location' => $trip ? (string) ($trip->departure_location ?? '') : '',
145 'destination' => $trip ? (string) ($trip->destination ?? '') : (string) ($source['destination'] ?? ''),
146 'travel_date' => $travelDate,
147 'return_date' => $returnDate,
148 'currency_symbol' => FormatHelper::getCurrencySymbol($currency),
149 'total_amount' => yatra_format_price((float) ($source['total_amount'] ?? 0), $currency, false),
150 'amount_paid' => yatra_format_price((float) ($source['amount_paid'] ?? 0), $currency, false),
151 'amount_due' => yatra_format_price((float) ($source['amount_due'] ?? 0), $currency, false),
152 'traveler_count' => (int) ($source['travelers_count'] ?? $source['traveler_count'] ?? $source['travelers'] ?? 1),
153 'itinerary_days' => $itineraryDays,
154 // Real trip-specific Important Information fields, same
155 // ones the single-trip page surfaces in its "Important
156 // Information" section. The template renders only the
157 // rows that are non-empty — no more hardcoded "Comfortable
158 // clothing, walking shoes, sunscreen..." boilerplate.
159 'physical_requirements' => $trip ? (string) ($trip->physical_requirements ?? '') : '',
160 'visa_requirements' => $trip ? (string) ($trip->visa_requirements ?? '') : '',
161 'vaccination_requirements' => $trip ? (string) ($trip->vaccination_requirements ?? '') : '',
162 'cancellation_policy' => $trip ? (string) ($trip->cancellation_policy ?? '') : '',
163 'age_min' => $trip ? ($trip->age_min ?? null) : null,
164 'age_max' => $trip ? ($trip->age_max ?? null) : null,
165 'accommodation_type' => $trip ? (string) ($trip->accommodation_type ?? '') : '',
166 'accommodation_details' => $trip ? (string) ($trip->accommodation_details ?? '') : '',
167 'meal_plan' => $trip ? (string) ($trip->meal_plan ?? '') : '',
168 'transportation_included' => (bool) ($trip->transportation_included ?? false),
169 'pickup_location' => $trip ? (string) ($trip->pickup_location ?? '') : '',
170 'dropoff_location' => $trip ? (string) ($trip->dropoff_location ?? '') : '',
171 'transportation_details' => $trip ? (string) ($trip->transportation_details ?? '') : '',
172 ];
173
174 // Locale-aware font is resolved inside PdfService — do NOT
175 // pass a `default_font` override here, otherwise the Nepali /
176 // Hindi / Arabic fonts loaded for non-Latin locales get
177 // bypassed and we end up with the missing-glyph rectangles
178 // this builder was created to eliminate.
179 return $this->pdfService->renderTemplateToPdfSafely(
180 'pdf/itinerary.php',
181 $templateData,
182 ['paper' => 'A4', 'orientation' => 'portrait']
183 );
184 }
185
186 /**
187 * Convenience: normalize a `$payment` joined record (the shape
188 * PaymentRepository returns for itinerary endpoints — payment row
189 * with booking columns joined in) into the `$source` array `build()`
190 * accepts. Saves the caller from duplicating field mapping logic.
191 */
192 public function buildFromPaymentRecord(object $payment): string
193 {
194 return $this->build([
195 'trip_id' => $payment->trip_id ?? 0,
196 'booking_id' => $payment->booking_id ?? 0,
197 'created_at' => $payment->created_at ?? null,
198 'travel_date' => $payment->travel_date ?? null,
199 // Actual booked trip range — used for the arrival/departure
200 // timeline. Null-safe: build() falls back to a correct
201 // derivation when the joined record doesn't carry these.
202 'start_date' => $payment->start_date ?? null,
203 'end_date' => $payment->end_date ?? null,
204 // Use the BOOKING status, not the payment status. Payment
205 // status can be "completed" while the booking itself is
206 // still "pending" admin confirmation — the itinerary
207 // header should reflect the booking, not the transaction.
208 'booking_status' => $payment->booking_status ?? $payment->status ?? null,
209 'contact_first_name' => $payment->contact_first_name ?? null,
210 'contact_last_name' => $payment->contact_last_name ?? null,
211 'contact_email' => $payment->contact_email ?? $payment->customer_email ?? null,
212 'customer_name' => $payment->customer_name ?? null,
213 'trip_title' => $payment->trip_title ?? null,
214 'destination' => $payment->destination ?? null,
215 'total_amount' => $payment->booking_total_amount ?? $payment->amount ?? 0,
216 'amount_paid' => $payment->booking_amount_paid ?? $payment->amount ?? 0,
217 'amount_due' => $payment->booking_amount_due ?? 0,
218 'travelers_count' => $payment->traveler_count ?? $payment->travelers_count ?? $payment->travelers ?? 1,
219 ]);
220 }
221 }
222