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

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

304 lines 10.2 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\RecurringRuleRepository;
8 use Yatra\Repositories\DepartureRepository;
9 use Yatra\Models\RecurringRule;
10
11 /**
12 * Recurring Rule Service
13 * Handles business logic for recurring rules and dynamic date generation
14 *
15 * Rules do NOT generate dates automatically in the database.
16 * They only act as rules for dynamic date generation on the frontend.
17 */
18 class RecurringRuleService
19 {
20 private RecurringRuleRepository $ruleRepository;
21 private DepartureRepository $departureRepository;
22
23 public function __construct(
24 RecurringRuleRepository $ruleRepository,
25 DepartureRepository $departureRepository
26 ) {
27 $this->ruleRepository = $ruleRepository;
28 $this->departureRepository = $departureRepository;
29 }
30
31 /**
32 * Generate dates based on recurring rules for a trip
33 *
34 * This method dynamically generates dates without storing them in the database.
35 * It checks for manually created departures and excludes those dates.
36 *
37 * @param int $tripId Trip ID
38 * @param string $fromDate Start date (YYYY-MM-DD)
39 * @param string $toDate End date (YYYY-MM-DD)
40 * @return array Array of generated date information
41 */
42 public function generateDatesForTrip(int $tripId, string $fromDate, string $toDate): array
43 {
44 // Get active recurring rules for this trip
45 $rules = $this->ruleRepository->findActiveForDateRange($tripId, $fromDate, $toDate);
46
47 if (empty($rules)) {
48 return [];
49 }
50
51 // Get all manually created departures for this date range
52 $manualDepartures = $this->departureRepository->findByTripId($tripId, [
53 'date_from' => $fromDate,
54 'date_to' => $toDate,
55 'source' => 'manual',
56 ]);
57
58 // Create a map of dates that have manual departures (these override rules)
59 $manualDates = [];
60 foreach ($manualDepartures as $departure) {
61 $manualDates[$departure->date] = true;
62 }
63
64 $generatedDates = [];
65
66 // Generate dates for each rule
67 foreach ($rules as $rule) {
68 $ruleDates = $this->generateDatesForRule($rule, $fromDate, $toDate);
69
70 // Filter out dates that have manual departures
71 foreach ($ruleDates as $date) {
72 if (!isset($manualDates[$date['date']])) {
73 $generatedDates[$date['date']] = $date;
74 }
75 }
76 }
77
78 // Sort by date
79 ksort($generatedDates);
80
81 return array_values($generatedDates);
82 }
83
84 /**
85 * Generate dates for a specific rule
86 *
87 * @param RecurringRule $rule The recurring rule
88 * @param string $fromDate Start date
89 * @param string $toDate End date
90 * @return array Array of date information
91 */
92 private function generateDatesForRule(RecurringRule $rule, string $fromDate, string $toDate): array
93 {
94 $dates = [];
95 $start = max($fromDate, $rule->start_date ?? $fromDate);
96 $end = min($toDate, $rule->end_date ?? $toDate);
97
98 if ($start > $end) {
99 return [];
100 }
101
102 $current = strtotime($start);
103 $endTimestamp = strtotime($end);
104
105 switch ($rule->recurrence_type) {
106 case 'daily':
107 // Generate every day
108 while ($current <= $endTimestamp) {
109 $dateStr = date('Y-m-d', $current);
110 if ($rule->isActiveForDate($dateStr)) {
111 $dates[] = $this->buildDateInfo($dateStr, $rule);
112 }
113 $current = strtotime('+1 day', $current);
114 }
115 break;
116
117 case 'weekly':
118 // Generate for specified weekdays
119 if (empty($rule->days_of_week)) {
120 break;
121 }
122
123 while ($current <= $endTimestamp) {
124 $dayOfWeek = (int) date('w', $current); // 0 = Sunday, 6 = Saturday
125 $dateStr = date('Y-m-d', $current);
126
127 if (in_array($dayOfWeek, $rule->days_of_week, true) && $rule->isActiveForDate($dateStr)) {
128 $dates[] = $this->buildDateInfo($dateStr, $rule);
129 }
130
131 $current = strtotime('+1 day', $current);
132 }
133 break;
134
135 case 'monthly':
136 // Generate for specific day of month (e.g., first Monday)
137 // This is simplified - you may want to enhance this
138 while ($current <= $endTimestamp) {
139 $dateStr = date('Y-m-d', $current);
140 if ($rule->isActiveForDate($dateStr)) {
141 $dates[] = $this->buildDateInfo($dateStr, $rule);
142 }
143 $current = strtotime('+1 month', $current);
144 }
145 break;
146
147 case 'custom_days':
148 // Generate for custom weekdays
149 if (empty($rule->days_of_week)) {
150 break;
151 }
152
153 while ($current <= $endTimestamp) {
154 $dayOfWeek = (int) date('w', $current);
155 $dateStr = date('Y-m-d', $current);
156
157 if (in_array($dayOfWeek, $rule->days_of_week, true) && $rule->isActiveForDate($dateStr)) {
158 $dates[] = $this->buildDateInfo($dateStr, $rule);
159 }
160
161 $current = strtotime('+1 day', $current);
162 }
163 break;
164 }
165
166 return $dates;
167 }
168
169 /**
170 * Build date information array from rule
171 */
172 private function buildDateInfo(string $date, RecurringRule $rule): array
173 {
174 // Mirror CapacityService's precedence: the rule editor writes the seat cap
175 // to `seats_total` and leaves `capacity_value` NULL, so reading
176 // capacity_value alone reported 0 seats for every rule created in the UI
177 // (a date that shows no capacity yet is not marked full).
178 $cap = (int) ($rule->seats_total ?? 0);
179 if ($cap <= 0) {
180 $cap = (int) ($rule->capacity_value ?? 0);
181 }
182
183 return [
184 'date' => $date,
185 'max_capacity' => $cap,
186 'base_price' => $rule->price_override,
187 'pricing_by_traveler_type' => null, // This field doesn't exist in the new schema
188 'source' => 'recurring_rule',
189 'rule_id' => $rule->id,
190 ];
191 }
192
193 /**
194 * Get preview of next N generated dates for a rule
195 *
196 * @param int $ruleId Rule ID
197 * @param int $count Number of dates to preview
198 * @return array Preview dates
199 */
200 public function getPreviewDates(int $ruleId, int $count = 10): array
201 {
202 $rule = $this->ruleRepository->findModel($ruleId);
203
204 if (!$rule) {
205 return [];
206 }
207
208 $fromDate = date('Y-m-d');
209 $toDate = date('Y-m-d', strtotime('+12 months'));
210
211 $dates = $this->generateDatesForRule($rule, $fromDate, $toDate);
212
213 return array_slice($dates, 0, $count);
214 }
215
216 /**
217 * Validate rule data
218 */
219 public function validate(array $data, ?int $id = null): void
220 {
221 if (empty($data['trip_id'])) {
222 throw new \InvalidArgumentException('Trip ID is required');
223 }
224
225 if (empty($data['recurrence_type'])) {
226 throw new \InvalidArgumentException('Recurrence type is required');
227 }
228
229 $validTypes = ['daily', 'weekly', 'monthly', 'custom_days'];
230 if (!in_array($data['recurrence_type'], $validTypes, true)) {
231 throw new \InvalidArgumentException('Invalid recurrence type. Must be: ' . implode(', ', $validTypes));
232 }
233
234 // Validate weekdays for weekly/custom_days
235 if (in_array($data['recurrence_type'], ['weekly', 'custom_days'], true)) {
236 if (empty($data['weekdays']) || !is_array($data['weekdays'])) {
237 throw new \InvalidArgumentException('Weekdays are required for weekly/custom_days rules');
238 }
239
240 foreach ($data['weekdays'] as $day) {
241 $dayInt = (int) $day;
242 if ($dayInt < 0 || $dayInt > 6) {
243 throw new \InvalidArgumentException('Weekdays must be 0-6 (Sunday-Saturday)');
244 }
245 }
246 }
247
248 // Validate dates
249 if (!empty($data['start_date']) && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $data['start_date'])) {
250 throw new \InvalidArgumentException('Invalid start date format. Use YYYY-MM-DD');
251 }
252
253 if (!empty($data['end_date'])) {
254 if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $data['end_date'])) {
255 throw new \InvalidArgumentException('Invalid end date format. Use YYYY-MM-DD');
256 }
257
258 if (!empty($data['start_date']) && strtotime($data['end_date']) < strtotime($data['start_date'])) {
259 throw new \InvalidArgumentException('End date must be after start date');
260 }
261 }
262
263 // Validate capacity
264 if (isset($data['max_capacity']) && (int) $data['max_capacity'] < 1) {
265 throw new \InvalidArgumentException('Max capacity must be at least 1');
266 }
267 }
268
269 /**
270 * Create a recurring rule
271 */
272 public function create(array $data): int
273 {
274 $this->validate($data);
275 return $this->ruleRepository->create($data);
276 }
277
278 /**
279 * Update a recurring rule
280 */
281 public function update(int $id, array $data): bool
282 {
283 $this->validate($data, $id);
284 return $this->ruleRepository->update($id, $data);
285 }
286
287 /**
288 * Delete a recurring rule
289 */
290 public function delete(int $id): bool
291 {
292 return $this->ruleRepository->delete($id);
293 }
294
295 /**
296 * Get rules by trip ID
297 */
298 public function getByTripId(int $tripId, bool $activeOnly = false): array
299 {
300 return $this->ruleRepository->findByTripId($tripId, $activeOnly);
301 }
302 }
303
304