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 / ItineraryService.php

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

312 lines 12.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\ItineraryRepository;
8 use Yatra\Repositories\TripRepository;
9 use InvalidArgumentException;
10
11 /**
12 * Itinerary Service
13 * Business logic for itinerary entries
14 */
15 class ItineraryService
16 {
17 private ItineraryRepository $repository;
18 private TripRepository $tripRepository;
19
20 public function __construct(ItineraryRepository $repository, ?TripRepository $tripRepository = null)
21 {
22 $this->repository = $repository;
23 $this->tripRepository = $tripRepository ?? new TripRepository();
24 }
25
26 /**
27 * Validate itinerary entry data
28 */
29 public function validate(array $data, ?int $id = null): void
30 {
31 // Required fields
32 if (empty($data['trip_id'])) {
33 throw new InvalidArgumentException(__('Trip ID is required', 'yatra'));
34 }
35
36 if (empty($data['day'])) {
37 throw new InvalidArgumentException(__('Day number is required', 'yatra'));
38 }
39
40 // For day entries (item_type_id and item_id are null), title can be empty if day_title is provided
41 // For activity entries, title is required
42 $isDayEntry = empty($data['item_type_id']) && empty($data['item_id']);
43 if (!$isDayEntry && (empty($data['title']) || trim($data['title']) === '')) {
44 throw new InvalidArgumentException(__('Title is required', 'yatra'));
45 }
46
47 // For day entries, if title is empty, use day_title or generate one
48 if ($isDayEntry && (empty($data['title']) || trim($data['title']) === '')) {
49 $dayNumber = (int) $data['day'];
50 $data['title'] = !empty($data['day_title']) ? trim($data['day_title']) : sprintf(__('Day %d', 'yatra'), $dayNumber);
51 }
52
53 // Validate trip exists
54 $trip = $this->tripRepository->find((int) $data['trip_id']);
55 if (!$trip) {
56 throw new InvalidArgumentException(__('Trip not found', 'yatra'));
57 }
58
59 // Validate day number
60 $dayNumber = (int) $data['day'];
61 if ($dayNumber < 1) {
62 throw new InvalidArgumentException(__('Day number must be at least 1', 'yatra'));
63 }
64
65 // Check for duplicate day numbers
66 // Only check if item_type_id and item_id are null (meaning it's a day creation/update, not an activity)
67 if (empty($data['item_type_id']) && empty($data['item_id'])) {
68 global $wpdb;
69
70 if (!$wpdb) {
71 // If for some reason the global $wpdb is not available, skip duplicate-day validation
72 return;
73 }
74
75 $itineraryRepository = new \Yatra\Repositories\ItineraryRepository();
76
77 // If updating, first check if the current entry is actually a day entry (not an activity)
78 if ($id !== null) {
79 $currentEntry = $itineraryRepository->getEntryWithRelations($id);
80
81 // If the current entry has item_type_id or item_id, it's an activity, not a day entry
82 // In this case, we're converting an activity to a day entry, which is unusual but allowed
83 // We'll still validate the day number
84 if ($currentEntry && (!empty($currentEntry->item_type_id) || !empty($currentEntry->item_id))) {
85 // This is converting an activity to a day entry - allow it but validate day number
86 // (fall through to day validation below)
87 } else if ($currentEntry && $currentEntry->day_id) {
88 // Get the current day's day_number
89 $currentDay = $itineraryRepository->getDayById((int) $currentEntry->day_id);
90
91 if ($currentDay && (int) $currentDay->day_number === $dayNumber) {
92 // Same day number - this is allowed (no change)
93 return; // No validation error
94 }
95 }
96 }
97
98 // Check if day entry already exists (not just day record)
99 // A day record can exist without a day entry (when only activities exist)
100 $existingDayEntry = $this->repository->findDayEntryByTripAndDayNumber((int) $data['trip_id'], $dayNumber);
101
102 if ($existingDayEntry) {
103 // Day entry already exists - this should be an update, not a create
104 // If updating, we've already checked above - allow it (frontend handles confirmation)
105 if ($id !== null) {
106 return; // Allow it, frontend handles the confirmation
107 } else {
108 // Creating new day entry but one already exists - this shouldn't happen
109 // The backend's createEntry will handle updating it, so allow it here
110 return; // Allow it, backend will update existing entry
111 }
112 }
113
114 // Check if day record exists (for informational purposes, but don't block)
115 // Day records can exist without day entries, so we allow creating day entries for existing day records
116 $existingDay = $itineraryRepository->findDayByTripAndDayNumber((int) $data['trip_id'], $dayNumber);
117
118 // If day record exists but day entry doesn't, allow creation (this is the normal case)
119 // Only block if we're creating a completely new day (both record and entry don't exist)
120 // and there's a conflict. But since we're creating a day entry, not a day record,
121 // we should allow it if the day record exists.
122 // The backend's createEntry will handle creating/updating the day record if needed.
123 }
124 }
125
126 /**
127 * Create itinerary entry
128 */
129 public function create(array $data): int
130 {
131 $this->validate($data);
132
133 global $wpdb;
134 // Clear any previous errors
135 $wpdb->last_error = '';
136
137 $result = $this->repository->createEntry($data);
138
139 // Check for database errors
140 if ($wpdb->last_error) {
141 // Check for duplicate entry error
142 if (strpos($wpdb->last_error, 'Duplicate entry') !== false) {
143 throw new \Exception('A day with this number already exists for this trip. Please choose a different day number.');
144 }
145
146 throw new \Exception('Failed to create itinerary entry. Please try again.');
147 }
148
149 return $result;
150 }
151
152 /**
153 * Update itinerary entry
154 * @param int $id Entry ID
155 * @param array $data Update data
156 * @param string|null $mode 'day' or 'activity' to specify which table to update
157 */
158 public function update(int $id, array $data, ?string $mode = null): bool
159 {
160 $this->validate($data, $id);
161
162 global $wpdb;
163 // Clear any previous errors
164 $wpdb->last_error = '';
165
166 $result = $this->repository->updateEntry($id, $data, $mode);
167
168 // Check for database errors
169 if ($wpdb->last_error) {
170 // Check for duplicate entry error
171 if (strpos($wpdb->last_error, 'Duplicate entry') !== false) {
172 throw new \Exception('A day with this number already exists for this trip. Please choose a different day number.');
173 }
174
175 throw new \Exception('Failed to update itinerary entry. Please try again.');
176 }
177
178 return $result;
179 }
180
181 /**
182 * Bulk-save activities for a single day in one DB transaction-equivalent batch.
183 *
184 * Accepts an `activities` array; each row with `id` is updated, each row
185 * without `id` is created. Returns per-row result objects so the caller can
186 * surface partial failures. Cache invalidation fires once at the end (via
187 * the existing per-row hooks already invoked by the repository).
188 *
189 * @param int $dayId The yatra_trip_itinerary_days.id row.
190 * @param int $tripId The trip id (used for the create branch).
191 * @param array $activities Array of activity payloads. Each row may include
192 * an `id` (update) or omit it (create).
193 * @return array {created: int, updated: int, results: array}
194 */
195 public function bulkSaveDayActivities(int $dayId, int $tripId, array $activities): array
196 {
197 $results = [];
198 $created = 0;
199 $updated = 0;
200 $failed = 0;
201
202 foreach ($activities as $i => $row) {
203 if (!is_array($row)) {
204 $results[] = ['index' => $i, 'ok' => false, 'error' => 'Row is not an object'];
205 $failed++;
206 continue;
207 }
208 // The repo expects day_id + trip_id on creates; inject here so the
209 // client doesn't have to repeat them per row.
210 $row['day_id'] = $dayId;
211 if ($tripId > 0 && empty($row['trip_id'])) {
212 $row['trip_id'] = $tripId;
213 }
214
215 try {
216 if (!empty($row['id'])) {
217 $entryId = (int) $row['id'];
218 unset($row['id']);
219 $ok = $this->repository->updateEntry($entryId, $row, 'activity');
220 if ($ok) {
221 $updated++;
222 $results[] = ['index' => $i, 'ok' => true, 'id' => $entryId, 'op' => 'update'];
223 } else {
224 $failed++;
225 $results[] = ['index' => $i, 'ok' => false, 'id' => $entryId, 'error' => 'updateEntry returned false'];
226 }
227 } else {
228 $newId = $this->repository->createEntry($row);
229 if ($newId > 0) {
230 $created++;
231 $results[] = ['index' => $i, 'ok' => true, 'id' => $newId, 'op' => 'create'];
232 } else {
233 $failed++;
234 $results[] = ['index' => $i, 'ok' => false, 'error' => 'createEntry returned 0'];
235 }
236 }
237 } catch (\Throwable $e) {
238 $failed++;
239 $results[] = ['index' => $i, 'ok' => false, 'error' => $e->getMessage()];
240 }
241 }
242
243 return [
244 'day_id' => $dayId,
245 'trip_id' => $tripId,
246 'created' => $created,
247 'updated' => $updated,
248 'failed' => $failed,
249 'results' => $results,
250 ];
251 }
252
253 /**
254 * Get itinerary entry by ID
255 */
256 public function find(int $id): ?\stdClass
257 {
258 $result = $this->repository->getEntryWithRelations($id);
259 return $result;
260 }
261
262 /**
263 * Get activity entry by ID (specifically from entries table)
264 */
265 public function findActivity(int $id): ?\stdClass
266 {
267 return $this->repository->getActivityEntry($id);
268 }
269
270 /**
271 * Delete itinerary entry
272 * @param int $id Entry ID
273 * @param string|null $mode 'day' or 'activity' to specify which table to delete from
274 */
275 public function delete(int $id, ?string $mode = null): bool
276 {
277 return $this->repository->delete($id, $mode);
278 }
279
280 /**
281 * Bulk delete itinerary entries
282 * @param array $ids Array of entry IDs to delete
283 * @return array ['deleted' => count, 'failed' => count]
284 */
285 public function bulkDelete(array $ids, array $dayIds = []): array
286 {
287 // Activity entry ids and day ids are validated independently — see
288 // ItineraryRepository::bulkDelete() for why they must stay separate.
289 $ids = array_values(array_filter(array_map('intval', $ids), function ($id) {
290 return $id > 0;
291 }));
292 $dayIds = array_values(array_filter(array_map('intval', $dayIds), function ($id) {
293 return $id > 0;
294 }));
295
296 if (empty($ids) && empty($dayIds)) {
297 return ['deleted' => 0, 'failed' => 0];
298 }
299
300 return $this->repository->bulkDelete($ids, $dayIds);
301 }
302
303 /**
304 * Get all itinerary entries for a specific trip
305 */
306 public function getByTripId(int $tripId): array
307 {
308 return $this->repository->getByTripId($tripId);
309 }
310 }
311
312