PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.4
Yatra – Travel Booking & Tour Operator Software v3.0.4
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 / AvailabilityRecurringRulesService.php

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

534 lines 17.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Yatra\Services;
4
5 use Yatra\Repositories\RecurringAvailabilityRepository;
6 use Yatra\Helpers\FormatHelper;
7 use Yatra\Helpers\ValidationHelper;
8
9 /**
10 * Availability Recurring Rules Service Class
11 *
12 * Handles business logic for recurring availability rules management.
13 * Provides high-level operations for managing recurring trip availability patterns.
14 *
15 * @package Yatra\Services
16 * @since 2.0.0
17 */
18 class AvailabilityRecurringRulesService
19 {
20 /**
21 * @var RecurringAvailabilityRepository Repository instance
22 */
23 private $repository;
24
25 /**
26 * Constructor
27 */
28 public function __construct()
29 {
30 $this->repository = new RecurringAvailabilityRepository();
31 }
32
33 /**
34 * Create a new recurring rule
35 *
36 * @param array $data Rule data
37 * @return int|false Created record ID or false on failure
38 */
39 public function create(array $data)
40 {
41 // Validate required fields
42 $required = ['trip_id', 'name', 'recurrence_type', 'start_date'];
43 if (!ValidationHelper::validateRequired($data, $required)) {
44 throw new \InvalidArgumentException('Missing required fields: ' . implode(', ', $required));
45 }
46
47 // Validate date formats
48 if (!ValidationHelper::validateDate($data['start_date'])) {
49 throw new \InvalidArgumentException('Invalid start_date format. Use Y-m-d format.');
50 }
51
52 if (isset($data['end_date']) && $data['end_date'] && !ValidationHelper::validateDate($data['end_date'])) {
53 throw new \InvalidArgumentException('Invalid end_date format. Use Y-m-d format.');
54 }
55
56 // Validate date range
57 if (isset($data['end_date']) && $data['end_date'] && $data['start_date'] > $data['end_date']) {
58 throw new \InvalidArgumentException('Start date cannot be after end date.');
59 }
60
61 // Validate recurrence type
62 $validTypes = ['daily', 'weekly', 'monthly', 'yearly', 'custom'];
63 if (!in_array($data['recurrence_type'], $validTypes)) {
64 throw new \InvalidArgumentException('Invalid recurrence_type. Must be one of: ' . implode(', ', $validTypes));
65 }
66
67 // Validate status
68 $validStatuses = ['active', 'inactive', 'paused'];
69 if (isset($data['status']) && !in_array($data['status'], $validStatuses)) {
70 throw new \InvalidArgumentException('Invalid status. Must be one of: ' . implode(', ', $validStatuses));
71 }
72
73 // Validate availability status
74 $validAvailabilityStatuses = ['available', 'unavailable', 'limited'];
75 if (isset($data['availability_status']) && !in_array($data['availability_status'], $validAvailabilityStatuses)) {
76 throw new \InvalidArgumentException('Invalid availability_status. Must be one of: ' . implode(', ', $validAvailabilityStatuses));
77 }
78
79 // Process data before create
80 $processedData = $this->processBeforeCreate($data);
81
82 // Create record
83 $id = $this->repository->create($processedData);
84
85 if ($id) {
86 // Post-process after successful create
87 $this->processAfterCreate($id, $processedData);
88 }
89
90 return $id;
91 }
92
93 /**
94 * Update an existing recurring rule
95 *
96 * @param int $id Rule ID
97 * @param array $data Update data
98 * @return bool Success status
99 */
100 public function update(int $id, array $data): bool
101 {
102 // Get existing record
103 $existing = $this->repository->getById($id);
104 if (!$existing) {
105 throw new \InvalidArgumentException('Recurring rule not found.');
106 }
107
108 // Validate date formats if provided
109 if (isset($data['start_date']) && !ValidationHelper::validateDate($data['start_date'])) {
110 throw new \InvalidArgumentException('Invalid start_date format. Use Y-m-d format.');
111 }
112
113 if (isset($data['end_date']) && $data['end_date'] && !ValidationHelper::validateDate($data['end_date'])) {
114 throw new \InvalidArgumentException('Invalid end_date format. Use Y-m-d format.');
115 }
116
117 // Validate date range if both dates are provided
118 $startDate = $data['start_date'] ?? $existing->start_date;
119 $endDate = $data['end_date'] ?? $existing->end_date;
120
121 if ($endDate && $startDate > $endDate) {
122 throw new \InvalidArgumentException('Start date cannot be after end date.');
123 }
124
125 // Validate recurrence type if provided
126 if (isset($data['recurrence_type'])) {
127 $validTypes = ['daily', 'weekly', 'monthly', 'yearly', 'custom'];
128 if (!in_array($data['recurrence_type'], $validTypes)) {
129 throw new \InvalidArgumentException('Invalid recurrence_type. Must be one of: ' . implode(', ', $validTypes));
130 }
131 }
132
133 // Validate status if provided
134 if (isset($data['status'])) {
135 $validStatuses = ['active', 'inactive', 'paused'];
136 if (!in_array($data['status'], $validStatuses)) {
137 throw new \InvalidArgumentException('Invalid status. Must be one of: ' . implode(', ', $validStatuses));
138 }
139 }
140
141 // Validate availability status if provided
142 if (isset($data['availability_status'])) {
143 $validAvailabilityStatuses = ['available', 'unavailable', 'limited'];
144 if (!in_array($data['availability_status'], $validAvailabilityStatuses)) {
145 throw new \InvalidArgumentException('Invalid availability_status. Must be one of: ' . implode(', ', $validAvailabilityStatuses));
146 }
147 }
148
149 // Process data before update
150 $processedData = $this->processBeforeUpdate($data, $existing);
151
152 // Update record
153 $success = $this->repository->update($id, $processedData);
154
155 if ($success) {
156 // Post-process after successful update
157 $this->processAfterUpdate($id, $processedData, $existing);
158 }
159
160 return $success;
161 }
162
163 /**
164 * Delete a recurring rule
165 *
166 * @param int $id Rule ID
167 * @return bool Success status
168 */
169 public function delete(int $id): bool
170 {
171 $existing = $this->repository->getById($id);
172 if (!$existing) {
173 throw new \InvalidArgumentException('Recurring rule not found.');
174 }
175
176 // Pre-delete processing
177 $this->processBeforeDelete($existing);
178
179 // Delete record
180 $success = $this->repository->delete($id);
181
182 if ($success) {
183 // Post-delete processing
184 $this->processAfterDelete($existing);
185 }
186
187 return $success;
188 }
189
190 /**
191 * Get active rules for a trip within a date range
192 *
193 * @param int $tripId Trip ID
194 * @param string $startDate Start date (Y-m-d)
195 * @param string $endDate End date (Y-m-d)
196 * @return array Array of active rules
197 */
198 public function getActiveRulesForTrip(int $tripId, string $startDate, string $endDate): array
199 {
200 return $this->repository->getActiveRulesForTrip($tripId, $startDate, $endDate);
201 }
202
203 /**
204 * Get all rules for a trip
205 *
206 * @param int $tripId Trip ID
207 * @return array Array of all rules for the trip
208 */
209 public function getRulesForTrip(int $tripId): array
210 {
211 return $this->repository->getRulesForTrip($tripId);
212 }
213
214 /**
215 * Get rules that apply to a specific date
216 *
217 * @param int $tripId Trip ID
218 * @param string $date Date (Y-m-d)
219 * @return array Array of rules that apply to the date
220 */
221 public function getRulesForDate(int $tripId, string $date): array
222 {
223 return $this->repository->getRulesForDate($tripId, $date);
224 }
225
226 /**
227 * Update rule status
228 *
229 * @param int $id Rule ID
230 * @param string $status New status
231 * @return bool Success status
232 */
233 public function updateStatus(int $id, string $status): bool
234 {
235 $validStatuses = ['active', 'inactive', 'paused'];
236 if (!in_array($status, $validStatuses)) {
237 throw new \InvalidArgumentException('Invalid status. Must be one of: ' . implode(', ', $validStatuses));
238 }
239
240 return $this->repository->updateStatus($id, $status);
241 }
242
243 /**
244 * Delete all rules for a trip
245 *
246 * @param int $tripId Trip ID
247 * @return int Number of deleted records
248 */
249 public function deleteRulesForTrip(int $tripId): int
250 {
251 return $this->repository->deleteRulesForTrip($tripId);
252 }
253
254 /**
255 * Add exception date to a rule
256 *
257 * @param int $id Rule ID
258 * @param string $date Date to add as exception (Y-m-d)
259 * @return bool Success status
260 */
261 public function addException(int $id, string $date): bool
262 {
263 if (!ValidationHelper::validateDate($date)) {
264 throw new \InvalidArgumentException('Invalid date format. Use Y-m-d format.');
265 }
266
267 return $this->repository->addException($id, $date);
268 }
269
270 /**
271 * Remove exception date from a rule
272 *
273 * @param int $id Rule ID
274 * @param string $date Date to remove from exceptions (Y-m-d)
275 * @return bool Success status
276 */
277 public function removeException(int $id, string $date): bool
278 {
279 if (!ValidationHelper::validateDate($date)) {
280 throw new \InvalidArgumentException('Invalid date format. Use Y-m-d format.');
281 }
282
283 return $this->repository->removeException($id, $date);
284 }
285
286 /**
287 * Get rules with exceptions for a trip
288 *
289 * @param int $tripId Trip ID
290 * @return array Array of rules that have exceptions
291 */
292 public function getRulesWithExceptions(int $tripId): array
293 {
294 return $this->repository->getRulesWithExceptions($tripId);
295 }
296
297 /**
298 * Generate recurring dates for a rule within a date range
299 *
300 * @param int $ruleId Rule ID
301 * @param string $startDate Start date (Y-m-d)
302 * @param string $endDate End date (Y-m-d)
303 * @return array Array of generated dates
304 */
305 public function generateRecurringDates(int $ruleId, string $startDate, string $endDate): array
306 {
307 $rule = $this->repository->getById($ruleId);
308 if (!$rule) {
309 throw new \InvalidArgumentException('Recurring rule not found.');
310 }
311
312 if ($rule->status !== 'active') {
313 return [];
314 }
315
316 $dates = [];
317 $current = new \DateTime($startDate);
318 $end = new \DateTime($endDate);
319
320 // Ensure we don't go beyond the rule's end date
321 if ($rule->end_date) {
322 $ruleEnd = new \DateTime($rule->end_date);
323 if ($ruleEnd < $end) {
324 $end = $ruleEnd;
325 }
326 }
327
328 while ($current <= $end) {
329 if ($this->repository->ruleAppliesToDate($rule, $current->format('Y-m-d'))) {
330 $dates[] = $current->format('Y-m-d');
331 }
332 $current->modify('+1 day');
333 }
334
335 return $dates;
336 }
337
338 /**
339 * Process data before create
340 *
341 * @param array $data Input data
342 * @return array Processed data
343 */
344 private function processBeforeCreate(array $data): array
345 {
346 $processed = $data;
347
348 // Set default status if not provided
349 if (!isset($processed['status'])) {
350 $processed['status'] = 'active';
351 }
352
353 // Set default availability status if not provided
354 if (!isset($processed['availability_status'])) {
355 $processed['availability_status'] = 'available';
356 }
357
358 // Set created by user if not provided
359 if (!isset($processed['created_by'])) {
360 $processed['created_by'] = get_current_user_id();
361 }
362
363 // Sanitize text fields
364 if (isset($processed['name'])) {
365 $processed['name'] = sanitize_text_field($processed['name']);
366 }
367
368 if (isset($processed['notes'])) {
369 $processed['notes'] = sanitize_textarea_field($processed['notes']);
370 }
371
372 // Process JSON fields
373 $jsonFields = ['recurrence_pattern', 'days_of_week', 'exceptions'];
374 foreach ($jsonFields as $field) {
375 if (isset($processed[$field])) {
376 if (is_array($processed[$field])) {
377 $processed[$field] = json_encode($processed[$field]);
378 } elseif (is_string($processed[$field])) {
379 // Validate JSON format
380 json_decode($processed[$field]);
381 if (json_last_error() !== JSON_ERROR_NONE) {
382 throw new \InvalidArgumentException("Invalid JSON format for {$field}.");
383 }
384 }
385 }
386 }
387
388 // Validate and sanitize numeric fields
389 if (isset($processed['max_bookings'])) {
390 $processed['max_bookings'] = max(0, (int) $processed['max_bookings']);
391 }
392
393 if (isset($processed['price_override'])) {
394 $processed['price_override'] = max(0, (float) $processed['price_override']);
395 }
396
397 // Validate price type
398 if (isset($processed['price_type'])) {
399 $validTypes = ['fixed', 'percentage'];
400 if (!in_array($processed['price_type'], $validTypes)) {
401 $processed['price_type'] = 'fixed';
402 }
403 }
404
405 // Validate day values
406 if (isset($processed['day_of_month'])) {
407 $processed['day_of_month'] = max(1, min(31, (int) $processed['day_of_month']));
408 }
409
410 if (isset($processed['month_of_year'])) {
411 $processed['month_of_year'] = max(1, min(12, (int) $processed['month_of_year']));
412 }
413
414 return $processed;
415 }
416
417 /**
418 * Process data before update
419 *
420 * @param array $data Input data
421 * @param object $existing Existing record
422 * @return array Processed data
423 */
424 private function processBeforeUpdate(array $data, object $existing): array
425 {
426 $processed = $data;
427
428 // Set updated by user if not provided
429 if (!isset($processed['updated_by'])) {
430 $processed['updated_by'] = get_current_user_id();
431 }
432
433 // Sanitize text fields
434 if (isset($processed['name'])) {
435 $processed['name'] = sanitize_text_field($processed['name']);
436 }
437
438 if (isset($processed['notes'])) {
439 $processed['notes'] = sanitize_textarea_field($processed['notes']);
440 }
441
442 // Process JSON fields
443 $jsonFields = ['recurrence_pattern', 'days_of_week', 'exceptions'];
444 foreach ($jsonFields as $field) {
445 if (isset($processed[$field])) {
446 if (is_array($processed[$field])) {
447 $processed[$field] = json_encode($processed[$field]);
448 } elseif (is_string($processed[$field])) {
449 // Validate JSON format
450 json_decode($processed[$field]);
451 if (json_last_error() !== JSON_ERROR_NONE) {
452 throw new \InvalidArgumentException("Invalid JSON format for {$field}.");
453 }
454 }
455 }
456 }
457
458 // Validate and sanitize numeric fields
459 if (isset($processed['max_bookings'])) {
460 $processed['max_bookings'] = max(0, (int) $processed['max_bookings']);
461 }
462
463 if (isset($processed['price_override'])) {
464 $processed['price_override'] = max(0, (float) $processed['price_override']);
465 }
466
467 // Validate price type
468 if (isset($processed['price_type'])) {
469 $validTypes = ['fixed', 'percentage'];
470 if (!in_array($processed['price_type'], $validTypes)) {
471 $processed['price_type'] = 'fixed';
472 }
473 }
474
475 // Validate day values
476 if (isset($processed['day_of_month'])) {
477 $processed['day_of_month'] = max(1, min(31, (int) $processed['day_of_month']));
478 }
479
480 if (isset($processed['month_of_year'])) {
481 $processed['month_of_year'] = max(1, min(12, (int) $processed['month_of_year']));
482 }
483
484 return $processed;
485 }
486
487 /**
488 * Process after create
489 *
490 * @param int $id Created rule ID
491 * @param array $data Processed data
492 */
493 private function processAfterCreate(int $id, array $data): void
494 {
495 // Log activity, trigger hooks, etc.
496 do_action('yatra_availability_recurring_rule_created', $id, $data);
497 }
498
499 /**
500 * Process after update
501 *
502 * @param int $id Updated rule ID
503 * @param array $data Processed data
504 * @param object $existing Original rule
505 */
506 private function processAfterUpdate(int $id, array $data, object $existing): void
507 {
508 // Log activity, trigger hooks, etc.
509 do_action('yatra_availability_recurring_rule_updated', $id, $data, $existing);
510 }
511
512 /**
513 * Process before delete
514 *
515 * @param object $existing Rule to be deleted
516 */
517 private function processBeforeDelete(object $existing): void
518 {
519 // Check dependencies, trigger hooks, etc.
520 do_action('yatra_availability_recurring_rule_before_delete', $existing);
521 }
522
523 /**
524 * Process after delete
525 *
526 * @param object $deleted Deleted rule
527 */
528 private function processAfterDelete(object $deleted): void
529 {
530 // Log activity, trigger hooks, etc.
531 do_action('yatra_availability_recurring_rule_deleted', $deleted);
532 }
533 }
534