PluginProbe
Yatra – Travel Booking & Tour Operator Software / trunk
Yatra – Travel Booking & Tour Operator Software vtrunk
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 2.0.10 2.0.11 All 82 releases
yatra / app / Services / AdditionalServicesBookingService.php

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

325 lines 11.0 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\Repositories\BookingRepository;
8 use Yatra\Repositories\AdditionalServicesRepository;
9 use Yatra\Utils\Logger;
10
11 /**
12 * Additional Services persistence + retrieval (free fallback).
13 *
14 * Pro modules can override via existing hooks:
15 * - yatra_booking_save_services
16 * - yatra_booking_get_services
17 *
18 * This implementation stores selected additional service IDs (and optional snapshot)
19 * in the bookings.meta JSON so the admin "View Booking" screen can display them.
20 */
21 class AdditionalServicesBookingService
22 {
23 private static ?self $instance = null;
24
25 public static function init(): void
26 {
27 if (self::$instance !== null) {
28 return;
29 }
30 self::$instance = new self();
31
32 add_action('yatra_booking_save_services', [self::$instance, 'saveServicesToBookingMeta'], 10, 5);
33 add_filter('yatra_booking_get_services', [self::$instance, 'getServicesForBooking'], 5, 2);
34 }
35
36 /**
37 * Persist selected services into booking meta.
38 *
39 * @param int $booking_id
40 * @param int $trip_id
41 * @param array $data Request data (expects additional_services: int[])
42 * @param int $travelers_count
43 * @param int $duration_days
44 */
45 public function saveServicesToBookingMeta(int $booking_id, int $trip_id, array $data, int $travelers_count, int $duration_days): void
46 {
47 $selected = $data['additional_services'] ?? [];
48 if (!is_array($selected)) {
49 $selected = [];
50 }
51 $selected = array_values(array_unique(array_map('intval', array_filter($selected))));
52
53 // If nothing selected, still store empty array for clarity.
54 $repo = new BookingRepository();
55 $booking = $repo->findWithTrip($booking_id);
56 if (!$booking) {
57 return;
58 }
59
60 $meta = [];
61 if (!empty($booking->meta) && is_string($booking->meta)) {
62 $decoded = json_decode($booking->meta, true);
63 $meta = is_array($decoded) ? $decoded : [];
64 } elseif (!empty($booking->meta) && is_array($booking->meta)) {
65 $meta = $booking->meta;
66 }
67
68 $meta['additional_services'] = $selected;
69 $meta['additional_services_updated_at'] = current_time('mysql');
70
71 // Optional: snapshot basic service info at booking time (name/price)
72 $snapshot = $this->buildServicesSnapshot($selected);
73 if ($snapshot !== []) {
74 $meta['additional_services_snapshot'] = $snapshot;
75 }
76
77 $repo->update($booking_id, [
78 'meta' => wp_json_encode($meta),
79 ]);
80 }
81
82 /**
83 * Return services for admin UI / API booking response.
84 *
85 * @param array $services Existing services from other filters (higher priority)
86 * @param int $booking_id
87 * @return array<int, array<string, mixed>>
88 */
89 public function getServicesForBooking(array $services, int $booking_id): array
90 {
91 // If another module already provided services, keep them.
92 if (!empty($services)) {
93 return $services;
94 }
95
96 $repo = new BookingRepository();
97 $booking = $repo->findWithTrip($booking_id);
98 if (!$booking) {
99 return [];
100 }
101
102 $meta = [];
103 if (!empty($booking->meta) && is_string($booking->meta)) {
104 $decoded = json_decode($booking->meta, true);
105 $meta = is_array($decoded) ? $decoded : [];
106 } elseif (!empty($booking->meta) && is_array($booking->meta)) {
107 $meta = $booking->meta;
108 }
109
110 $selected = $meta['additional_services'] ?? [];
111 if (!is_array($selected) || $selected === []) {
112 return [];
113 }
114
115 $selected = array_values(array_unique(array_map('intval', array_filter($selected))));
116 if ($selected === []) {
117 return [];
118 }
119
120 if (defined('WP_DEBUG') && WP_DEBUG) {
121 Logger::debug('AdditionalServicesBookingService: resolving services', [
122 'context' => 'booking_services',
123 'booking_id' => $booking_id,
124 'selected_ids' => $selected,
125 'has_snapshot' => !empty($meta['additional_services_snapshot']),
126 'has_meta_column' => isset($booking->meta),
127 ]);
128 }
129
130 // Prefer snapshot if present (keeps booking-time prices), else query live table.
131 $snapshot = $meta['additional_services_snapshot'] ?? [];
132 if (is_array($snapshot) && $snapshot !== []) {
133 return array_values(array_filter(array_map(static function ($row) use ($selected) {
134 if (!is_array($row)) {
135 return null;
136 }
137 $id = isset($row['id']) ? (int) $row['id'] : 0;
138 if ($id <= 0 || !in_array($id, $selected, true)) {
139 return null;
140 }
141 return [
142 'id' => $id,
143 'name' => (string) ($row['name'] ?? ''),
144 'description' => (string) ($row['description'] ?? ''),
145 'price' => (float) ($row['price'] ?? 0),
146 'calculated_price' => (float) ($row['price'] ?? 0),
147 'selected' => true,
148 ];
149 }, $snapshot)));
150 }
151
152 return $this->queryServicesByIds($selected, $booking);
153 }
154
155 /**
156 * Query additional services table for display.
157 *
158 * @param int[] $ids
159 * @return array<int, array<string, mixed>>
160 */
161 private function queryServicesByIds(array $ids, ?object $booking = null): array
162 {
163 $ids = array_values(array_unique(array_map('intval', array_filter($ids))));
164 if ($ids === []) {
165 return [];
166 }
167 $repo = new AdditionalServicesRepository();
168 $rows = $repo->getByIds($ids);
169
170 if (defined('WP_DEBUG') && WP_DEBUG) {
171 Logger::debug('AdditionalServicesBookingService: services fetched', [
172 'context' => 'booking_services',
173 'ids' => $ids,
174 'count' => count($rows),
175 'sample' => $rows[0] ?? null,
176 ]);
177 }
178
179 $context = $this->bookingContextForServiceCalc($booking);
180
181 return array_map(static function (array $row) use ($context): array {
182 $price = (float) ($row['price'] ?? 0);
183 $priceType = (string) (($row['price_type'] ?? 'fixed') ?: 'fixed');
184 $pricePer = (string) (($row['price_per'] ?? 'booking') ?: 'booking');
185
186 $calculated = self::calculateServicePrice(
187 $price,
188 $priceType,
189 $pricePer,
190 (int) ($context['travelers_count'] ?? 1),
191 (int) ($context['duration_days'] ?? 1),
192 (float) ($context['base_amount'] ?? 0)
193 );
194 return [
195 'id' => (int) ($row['id'] ?? 0),
196 'name' => (string) ($row['name'] ?? ''),
197 'description' => (string) ($row['description'] ?? ''),
198 'price' => $price,
199 'calculated_price' => $calculated,
200 'price_type' => $priceType,
201 'price_per' => $pricePer,
202 'selected' => true,
203 ];
204 }, $rows);
205 }
206
207 /**
208 * Compute booking context used for service price calculation.
209 *
210 * @return array{travelers_count:int,duration_days:int,base_amount:float}
211 */
212 private function bookingContextForServiceCalc(?object $booking): array
213 {
214 $travelers = 1;
215 $duration = 1;
216 $baseAmount = 0.0;
217
218 if (is_object($booking)) {
219 $travelers = (int) ($booking->travelers_count ?? 1);
220 if ($travelers <= 0) {
221 $travelers = 1;
222 }
223
224 // Duration: use booking start/end if present; else 1.
225 $start = !empty($booking->start_date) ? (string) $booking->start_date : (!empty($booking->travel_date) ? (string) $booking->travel_date : '');
226 $end = !empty($booking->end_date) ? (string) $booking->end_date : '';
227 if ($start !== '' && $end !== '') {
228 try {
229 $startDt = new \DateTime($start);
230 $endDt = new \DateTime($end);
231 $diff = (int) $endDt->diff($startDt)->days;
232 $duration = max(1, $diff + 1);
233 } catch (\Throwable $e) {
234 $duration = 1;
235 }
236 }
237
238 // Base amount for percentage services: use booking subtotal if set, else total_amount (pre-tax when possible).
239 $total = (float) ($booking->total_amount ?? 0);
240 $tax = (float) ($booking->tax_amount ?? 0);
241 $taxInclusive = !empty($booking->tax_inclusive);
242 $subtotal = isset($booking->subtotal) ? (float) $booking->subtotal : 0.0;
243
244 if ($subtotal > 0) {
245 $baseAmount = $subtotal;
246 } elseif (!$taxInclusive && $tax > 0 && $total > 0) {
247 $baseAmount = max(0.0, $total - $tax);
248 } else {
249 $baseAmount = $total;
250 }
251 }
252
253 return [
254 'travelers_count' => $travelers,
255 'duration_days' => $duration,
256 'base_amount' => $baseAmount,
257 ];
258 }
259
260 /**
261 * Calculate effective service cost.
262 */
263 private static function calculateServicePrice(
264 float $price,
265 string $priceType,
266 string $pricePer,
267 int $travelersCount,
268 int $durationDays,
269 float $baseAmount
270 ): float {
271 $travelersCount = max(1, $travelersCount);
272 $durationDays = max(1, $durationDays);
273
274 $amount = 0.0;
275
276 if ($priceType === 'percentage') {
277 // Percentage services are assumed to apply to the booking base amount.
278 $amount = max(0.0, $baseAmount) * ($price / 100.0);
279 // Treat percentage as per-booking by default (avoids double-counting).
280 return round($amount, 2);
281 }
282
283 // Fixed price services.
284 $amount = $price;
285 switch ($pricePer) {
286 case 'person':
287 $amount = $price * $travelersCount;
288 break;
289 case 'day':
290 $amount = $price * $durationDays;
291 break;
292 case 'booking':
293 default:
294 $amount = $price;
295 break;
296 }
297
298 return round($amount, 2);
299 }
300
301 /**
302 * Build a stable snapshot for the selected services.
303 *
304 * @param int[] $ids
305 * @return array<int, array<string, mixed>>
306 */
307 private function buildServicesSnapshot(array $ids): array
308 {
309 $rows = $this->queryServicesByIds($ids);
310 if ($rows === []) {
311 return [];
312 }
313
314 return array_map(static function (array $row): array {
315 return [
316 'id' => (int) ($row['id'] ?? 0),
317 'name' => (string) ($row['name'] ?? ''),
318 'description' => (string) ($row['description'] ?? ''),
319 'price' => (float) ($row['price'] ?? 0),
320 ];
321 }, $rows);
322 }
323 }
324
325