PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.13
Yatra – Travel Booking & Tour Operator Software v3.0.13
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 / DiscountService.php

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

1,253 lines 57.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\Repositories\DiscountRepository;
8
9 /**
10 * Discount Service
11 * Contains business logic for discounts
12 */
13 class DiscountService extends BaseService
14 {
15 private DiscountRepository $repository;
16
17 public function __construct()
18 {
19 $this->repository = new DiscountRepository();
20 }
21
22 protected function getRepository(): DiscountRepository
23 {
24 return $this->repository;
25 }
26
27 /**
28 * Get group discounts applicable to a specific trip
29 *
30 * @param int $tripId The trip ID
31 * @return array Array of group discount objects
32 */
33 public function getGroupDiscountsForTrip(int $tripId): array
34 {
35 global $wpdb;
36 $table = \Yatra\Database\Tables\DiscountsTable::getTableName();
37 $today = date('Y-m-d H:i:s');
38
39 // Query for active group discounts
40 // Check both is_group_discount=1 OR discount_mode IN ('group', 'both') for backward compatibility
41 $query = $wpdb->prepare(
42 "SELECT * FROM `{$table}`
43 WHERE (is_group_discount = 1 OR discount_mode IN ('group', 'both'))
44 AND status IN ('publish', 'active')
45 AND (valid_from IS NULL OR valid_from <= %s)
46 AND (expiry_date IS NULL OR expiry_date >= %s)
47 ORDER BY created_at DESC",
48 $today,
49 $today
50 );
51
52 $results = $wpdb->get_results($query);
53
54 // Filter by trip_ids in PHP since it's stored as serialized array
55 $filtered = [];
56 foreach ($results as $discount) {
57 $applicable = (string) ($discount->applicable_to ?? 'all');
58 if ($applicable === '' || $applicable === 'all') {
59 $filtered[] = $discount;
60 continue;
61 }
62 if ($applicable !== 'specific_trips') {
63 continue;
64 }
65
66 // specific_trips: include only when this trip is in the configured list
67 $rawIds = $discount->trip_ids;
68 $trip_ids = [];
69 if (is_string($rawIds) && $rawIds !== '') {
70 $t = trim($rawIds);
71 if ($t !== '' && ($t[0] === '[' || $t[0] === '{')) {
72 $decoded = json_decode($t, true);
73 $trip_ids = is_array($decoded) ? $decoded : [];
74 } else {
75 $unser = maybe_unserialize($rawIds);
76 $trip_ids = is_array($unser) ? $unser : array_map('trim', explode(',', $t));
77 }
78 } elseif (is_array($rawIds)) {
79 $trip_ids = $rawIds;
80 }
81 $trip_ids = array_values(array_unique(array_map('absint', $trip_ids)));
82 if (in_array($tripId, $trip_ids, true)) {
83 $filtered[] = $discount;
84 }
85 }
86
87 return $filtered;
88 }
89
90 protected function validate(array $data, ?int $id = null): void
91 {
92 if (empty($data['code'])) {
93 throw new \InvalidArgumentException('Discount code is required');
94 }
95
96 // Check code uniqueness (excluding current ID if updating)
97 $existing = $this->repository->findByCode($data['code']);
98 if ($existing && (int) $existing->id !== $id) {
99 throw new \InvalidArgumentException('Discount code already exists');
100 }
101
102 $allowed_types = ['percentage', 'fixed'];
103 if (isset($data['type']) && !in_array($data['type'], $allowed_types, true)) {
104 throw new \InvalidArgumentException('Invalid discount type. Must be one of: ' . implode(', ', $allowed_types));
105 }
106
107 $allowed_statuses = ['draft', 'publish', 'trash', 'expired'];
108 if (isset($data['status']) && !in_array($data['status'], $allowed_statuses, true)) {
109 throw new \InvalidArgumentException('Invalid status. Must be one of: ' . implode(', ', $allowed_statuses));
110 }
111
112 $allowed_applicable_to = ['all', 'specific_trips'];
113 if (isset($data['applicable_to']) && !in_array($data['applicable_to'], $allowed_applicable_to, true)) {
114 throw new \InvalidArgumentException('Invalid applicable_to. Must be one of: ' . implode(', ', $allowed_applicable_to));
115 }
116
117 if (isset($data['amount']) && (float) $data['amount'] < 0) {
118 throw new \InvalidArgumentException('Discount amount cannot be negative');
119 }
120 }
121
122 protected function processBeforeCreate(array $data): array
123 {
124 if (isset($data['code'])) {
125 $data['code'] = strtoupper(sanitize_text_field($data['code']));
126 }
127
128 if (isset($data['description'])) {
129 $data['description'] = sanitize_textarea_field($data['description']);
130 }
131
132 if (isset($data['type'])) {
133 $allowed_types = ['percentage', 'fixed'];
134 $data['type'] = in_array($data['type'], $allowed_types, true) ? $data['type'] : 'percentage';
135 } else {
136 $data['type'] = 'percentage';
137 }
138
139 if (isset($data['amount'])) {
140 $data['amount'] = (float) $data['amount'];
141 } else {
142 $data['amount'] = 0.0;
143 }
144
145 if (isset($data['max_discount_amount'])) {
146 $data['max_discount_amount'] = !empty($data['max_discount_amount']) ? (float) $data['max_discount_amount'] : null;
147 }
148
149 if (isset($data['usage_limit'])) {
150 $data['usage_limit'] = absint($data['usage_limit']);
151 } else {
152 $data['usage_limit'] = 0;
153 }
154
155 if (isset($data['usage_limit_per_customer'])) {
156 $data['usage_limit_per_customer'] = absint($data['usage_limit_per_customer']);
157 } else {
158 $data['usage_limit_per_customer'] = 0;
159 }
160
161 if (isset($data['valid_from']) && !empty($data['valid_from'])) {
162 $data['valid_from'] = sanitize_text_field($data['valid_from']);
163 } else {
164 $data['valid_from'] = null;
165 }
166
167 if (isset($data['expiry_date']) && !empty($data['expiry_date'])) {
168 $data['expiry_date'] = sanitize_text_field($data['expiry_date']);
169 } else {
170 $data['expiry_date'] = null;
171 }
172
173 if (isset($data['status'])) {
174 $allowed_statuses = ['draft', 'publish', 'trash', 'expired'];
175 $data['status'] = in_array($data['status'], $allowed_statuses, true) ? $data['status'] : 'draft';
176 } else {
177 $data['status'] = 'draft';
178 }
179
180 if (isset($data['applicable_to'])) {
181 $allowed_applicable_to = ['all', 'specific_trips'];
182 $data['applicable_to'] = in_array($data['applicable_to'], $allowed_applicable_to, true) ? $data['applicable_to'] : 'all';
183 } else {
184 $data['applicable_to'] = 'all';
185 }
186
187 if (isset($data['trip_ids']) && is_array($data['trip_ids'])) {
188 $trip_ids = array_map('absint', $data['trip_ids']);
189 $data['trip_ids'] = maybe_serialize($trip_ids);
190 } elseif (isset($data['trip_ids'])) {
191 $data['trip_ids'] = maybe_serialize([]);
192 } else {
193 $data['trip_ids'] = null;
194 }
195
196 if (isset($data['min_amount'])) {
197 $data['min_amount'] = !empty($data['min_amount']) ? (float) $data['min_amount'] : null;
198 }
199
200 if (isset($data['first_time_customer_only'])) {
201 $data['first_time_customer_only'] = (bool) $data['first_time_customer_only'];
202 } else {
203 $data['first_time_customer_only'] = false;
204 }
205
206 // Handle discount_mode (promo, group, or both)
207 if (isset($data['discount_mode'])) {
208 $allowed_modes = ['promo', 'group', 'both'];
209 $data['discount_mode'] = in_array($data['discount_mode'], $allowed_modes, true) ? $data['discount_mode'] : 'both';
210
211 // If discount_mode is 'group', ensure is_group_discount is true
212 if ($data['discount_mode'] === 'group') {
213 $data['is_group_discount'] = true;
214 }
215 } else {
216 $data['discount_mode'] = 'both';
217 }
218
219 if (isset($data['is_group_discount'])) {
220 $data['is_group_discount'] = (bool) $data['is_group_discount'];
221
222 // If group discount is disabled, clear all group discount fields
223 if (!$data['is_group_discount']) {
224 $data['min_group_size'] = null;
225 $data['max_group_size'] = null;
226 $data['group_discount_type'] = null;
227 $data['group_discount_amount'] = null;
228 $data['group_discount_mode'] = null;
229 $data['category_discounts'] = null;
230 } else {
231 // Only process group discount fields if enabled
232 if (isset($data['min_group_size'])) {
233 $data['min_group_size'] = !empty($data['min_group_size']) ? absint($data['min_group_size']) : null;
234 }
235
236 if (isset($data['max_group_size'])) {
237 $data['max_group_size'] = !empty($data['max_group_size']) ? absint($data['max_group_size']) : null;
238 }
239
240 // Validate min/max group size logic
241 if ($data['min_group_size'] && $data['max_group_size'] && $data['min_group_size'] >= $data['max_group_size']) {
242 throw new \InvalidArgumentException('Minimum group size must be less than maximum group size');
243 }
244
245 if (isset($data['group_discount_type'])) {
246 $allowed_types = ['percentage', 'fixed'];
247 $data['group_discount_type'] = in_array($data['group_discount_type'], $allowed_types, true) ? $data['group_discount_type'] : 'percentage';
248 } else {
249 $data['group_discount_type'] = 'percentage';
250 }
251
252 if (isset($data['group_discount_amount'])) {
253 $data['group_discount_amount'] = !empty($data['group_discount_amount']) ? (float) $data['group_discount_amount'] : null;
254 }
255
256 if (isset($data['group_discount_mode'])) {
257 $allowed_modes = ['total', 'category_based'];
258 $data['group_discount_mode'] = in_array($data['group_discount_mode'], $allowed_modes, true) ? $data['group_discount_mode'] : 'total';
259 } else {
260 $data['group_discount_mode'] = 'total';
261 }
262
263 // Process group discount ranges (for total mode)
264 if (isset($data['group_discount_ranges']) && is_array($data['group_discount_ranges'])) {
265 $processedRanges = [];
266 $minGroupSizeFromRanges = null;
267 foreach ($data['group_discount_ranges'] as $range) {
268 if (!is_array($range)) continue;
269
270 $allowed_types = ['percentage', 'fixed'];
271 $discountType = isset($range['discount_type']) && in_array($range['discount_type'], $allowed_types, true)
272 ? $range['discount_type']
273 : 'percentage';
274
275 $discountAmount = isset($range['discount_amount']) ? (float) $range['discount_amount'] : 0;
276 if ($discountAmount < 0) {
277 throw new \InvalidArgumentException('Group discount amount cannot be negative');
278 }
279
280 $rangeMinSize = isset($range['min_group_size']) && $range['min_group_size'] !== '' ? absint($range['min_group_size']) : null;
281
282 // Track the minimum group size from all ranges
283 if ($rangeMinSize !== null && ($minGroupSizeFromRanges === null || $rangeMinSize < $minGroupSizeFromRanges)) {
284 $minGroupSizeFromRanges = $rangeMinSize;
285 }
286
287 $processedRanges[] = [
288 'id' => sanitize_text_field($range['id'] ?? uniqid()),
289 'min_group_size' => $rangeMinSize,
290 'max_group_size' => isset($range['max_group_size']) && $range['max_group_size'] !== '' ? absint($range['max_group_size']) : null,
291 'discount_type' => $discountType,
292 'discount_amount' => $discountAmount,
293 'categories' => $range['categories'] ?? []
294 ];
295 }
296 $data['group_discount_ranges'] = !empty($processedRanges) ? json_encode($processedRanges) : null;
297
298 // Set min_group_size from ranges if not already set (for backward compatibility with frontend query)
299 if ($minGroupSizeFromRanges !== null && empty($data['min_group_size'])) {
300 $data['min_group_size'] = $minGroupSizeFromRanges;
301 }
302 } elseif (isset($data['group_discount_ranges'])) {
303 $data['group_discount_ranges'] = null;
304 }
305
306 // Process category discounts (new format with traveler categories and ranges)
307 if (isset($data['category_discounts']) && is_array($data['category_discounts'])) {
308 $processedCategories = [];
309 foreach ($data['category_discounts'] as $categoryData) {
310 // New format: {traveler_category_id, traveler_category_label, ranges: [...]}
311 if (is_array($categoryData) && isset($categoryData['traveler_category_id'])) {
312 $processedCategory = [
313 'traveler_category_id' => sanitize_text_field($categoryData['traveler_category_id']),
314 'traveler_category_label' => sanitize_text_field($categoryData['traveler_category_label'] ?? ''),
315 'ranges' => []
316 ];
317
318 // Process ranges for this category
319 if (isset($categoryData['ranges']) && is_array($categoryData['ranges'])) {
320 foreach ($categoryData['ranges'] as $range) {
321 if (!is_array($range)) continue;
322
323 $allowed_types = ['percentage', 'fixed'];
324 $discountType = isset($range['discount_type']) && in_array($range['discount_type'], $allowed_types, true)
325 ? $range['discount_type']
326 : 'percentage';
327
328 $discountAmount = isset($range['discount_amount']) ? (float) $range['discount_amount'] : 0;
329 if ($discountAmount < 0) {
330 throw new \InvalidArgumentException('Category discount amount cannot be negative');
331 }
332
333 $processedCategory['ranges'][] = [
334 'id' => sanitize_text_field($range['id'] ?? uniqid()),
335 'min_group_size' => isset($range['min_group_size']) && $range['min_group_size'] !== '' ? absint($range['min_group_size']) : null,
336 'max_group_size' => isset($range['max_group_size']) && $range['max_group_size'] !== '' ? absint($range['max_group_size']) : null,
337 'discount_type' => $discountType,
338 'discount_amount' => $discountAmount
339 ];
340 }
341 }
342
343 $processedCategories[] = $processedCategory;
344 }
345 // Legacy format support: {category => {type, amount}}
346 elseif (is_array($categoryData) && isset($categoryData['type']) && isset($categoryData['amount'])) {
347 $allowed_types = ['percentage', 'fixed'];
348 if (!in_array($categoryData['type'], $allowed_types, true)) {
349 throw new \InvalidArgumentException('Invalid category discount type');
350 }
351 if ((float) $categoryData['amount'] < 0) {
352 throw new \InvalidArgumentException('Category discount amount cannot be negative');
353 }
354 $processedCategories[] = $categoryData;
355 }
356 }
357 $data['category_discounts'] = !empty($processedCategories) ? json_encode($processedCategories) : null;
358 } elseif (isset($data['category_discounts'])) {
359 $data['category_discounts'] = null;
360 }
361 }
362 } else {
363 // If is_group_discount is not set, default to false and clear fields
364 $data['is_group_discount'] = false;
365 $data['min_group_size'] = null;
366 $data['max_group_size'] = null;
367 $data['group_discount_type'] = null;
368 $data['group_discount_amount'] = null;
369 $data['group_discount_mode'] = null;
370 $data['category_discounts'] = null;
371 }
372
373 $current_user_id = get_current_user_id();
374 $data['created_by'] = absint($current_user_id);
375 $data['updated_by'] = absint($current_user_id);
376 $data['usage_count'] = 0; // Always start at 0
377
378 return $data;
379 }
380
381 protected function processBeforeUpdate(int $id, array $data): array
382 {
383 if (isset($data['code'])) {
384 $data['code'] = strtoupper(sanitize_text_field($data['code']));
385 }
386
387 if (isset($data['description'])) {
388 $data['description'] = sanitize_textarea_field($data['description']);
389 }
390
391 if (isset($data['type'])) {
392 $allowed_types = ['percentage', 'fixed'];
393 $data['type'] = in_array($data['type'], $allowed_types, true) ? $data['type'] : 'percentage';
394 }
395
396 if (isset($data['amount'])) {
397 $data['amount'] = (float) $data['amount'];
398 }
399
400 if (isset($data['max_discount_amount'])) {
401 $data['max_discount_amount'] = !empty($data['max_discount_amount']) ? (float) $data['max_discount_amount'] : null;
402 }
403
404 if (isset($data['usage_limit'])) {
405 $data['usage_limit'] = absint($data['usage_limit']);
406 }
407
408 if (isset($data['usage_limit_per_customer'])) {
409 $data['usage_limit_per_customer'] = absint($data['usage_limit_per_customer']);
410 }
411
412 if (isset($data['valid_from']) && !empty($data['valid_from'])) {
413 $data['valid_from'] = sanitize_text_field($data['valid_from']);
414 } elseif (isset($data['valid_from']) && empty($data['valid_from'])) {
415 $data['valid_from'] = null;
416 }
417
418 if (isset($data['expiry_date']) && !empty($data['expiry_date'])) {
419 $data['expiry_date'] = sanitize_text_field($data['expiry_date']);
420 } elseif (isset($data['expiry_date']) && empty($data['expiry_date'])) {
421 $data['expiry_date'] = null;
422 }
423
424 if (isset($data['status'])) {
425 $allowed_statuses = ['draft', 'publish', 'trash', 'expired'];
426 $data['status'] = in_array($data['status'], $allowed_statuses, true) ? $data['status'] : 'draft';
427 }
428
429 if (isset($data['applicable_to'])) {
430 $allowed_applicable_to = ['all', 'specific_trips'];
431 $data['applicable_to'] = in_array($data['applicable_to'], $allowed_applicable_to, true) ? $data['applicable_to'] : 'all';
432 }
433
434 if (isset($data['trip_ids']) && is_array($data['trip_ids'])) {
435 $trip_ids = array_map('absint', $data['trip_ids']);
436 $data['trip_ids'] = maybe_serialize($trip_ids);
437 } elseif (isset($data['trip_ids']) && empty($data['trip_ids'])) {
438 $data['trip_ids'] = null;
439 }
440
441 if (isset($data['min_amount'])) {
442 $data['min_amount'] = !empty($data['min_amount']) ? (float) $data['min_amount'] : null;
443 }
444
445 if (isset($data['first_time_customer_only'])) {
446 $data['first_time_customer_only'] = (bool) $data['first_time_customer_only'];
447 }
448
449 // Handle discount_mode (promo, group, or both)
450 if (isset($data['discount_mode'])) {
451 $allowed_modes = ['promo', 'group', 'both'];
452 $data['discount_mode'] = in_array($data['discount_mode'], $allowed_modes, true) ? $data['discount_mode'] : 'both';
453
454 // If discount_mode is 'group', ensure is_group_discount is true
455 if ($data['discount_mode'] === 'group') {
456 $data['is_group_discount'] = true;
457 }
458 }
459
460 if (isset($data['is_group_discount'])) {
461 $data['is_group_discount'] = (bool) $data['is_group_discount'];
462
463 // If group discount is disabled, clear all group discount fields
464 if (!$data['is_group_discount']) {
465 $data['min_group_size'] = null;
466 $data['max_group_size'] = null;
467 $data['group_discount_type'] = null;
468 $data['group_discount_amount'] = null;
469 $data['group_discount_mode'] = null;
470 $data['category_discounts'] = null;
471 } else {
472 // Only process group discount fields if enabled
473 if (isset($data['min_group_size'])) {
474 $data['min_group_size'] = !empty($data['min_group_size']) ? absint($data['min_group_size']) : null;
475 }
476
477 if (isset($data['max_group_size'])) {
478 $data['max_group_size'] = !empty($data['max_group_size']) ? absint($data['max_group_size']) : null;
479 }
480
481 // Validate min/max group size logic
482 if ($data['min_group_size'] && $data['max_group_size'] && $data['min_group_size'] >= $data['max_group_size']) {
483 throw new \InvalidArgumentException('Minimum group size must be less than maximum group size');
484 }
485
486 if (isset($data['group_discount_type'])) {
487 $allowed_types = ['percentage', 'fixed'];
488 $data['group_discount_type'] = in_array($data['group_discount_type'], $allowed_types, true) ? $data['group_discount_type'] : 'percentage';
489 }
490
491 if (isset($data['group_discount_amount'])) {
492 $data['group_discount_amount'] = !empty($data['group_discount_amount']) ? (float) $data['group_discount_amount'] : null;
493 }
494
495 if (isset($data['group_discount_mode'])) {
496 $allowed_modes = ['total', 'category_based'];
497 $data['group_discount_mode'] = in_array($data['group_discount_mode'], $allowed_modes, true) ? $data['group_discount_mode'] : 'total';
498 }
499
500 // Process group discount ranges (for total mode)
501 if (isset($data['group_discount_ranges']) && is_array($data['group_discount_ranges'])) {
502 $processedRanges = [];
503 foreach ($data['group_discount_ranges'] as $range) {
504 if (!is_array($range)) continue;
505
506 $allowed_types = ['percentage', 'fixed'];
507 $discountType = isset($range['discount_type']) && in_array($range['discount_type'], $allowed_types, true)
508 ? $range['discount_type']
509 : 'percentage';
510
511 $discountAmount = isset($range['discount_amount']) ? (float) $range['discount_amount'] : 0;
512 if ($discountAmount < 0) {
513 throw new \InvalidArgumentException('Group discount amount cannot be negative');
514 }
515
516 $processedRanges[] = [
517 'id' => sanitize_text_field($range['id'] ?? uniqid()),
518 'min_group_size' => isset($range['min_group_size']) && $range['min_group_size'] !== '' ? absint($range['min_group_size']) : null,
519 'max_group_size' => isset($range['max_group_size']) && $range['max_group_size'] !== '' ? absint($range['max_group_size']) : null,
520 'discount_type' => $discountType,
521 'discount_amount' => $discountAmount,
522 'categories' => $range['categories'] ?? []
523 ];
524 }
525 $data['group_discount_ranges'] = !empty($processedRanges) ? json_encode($processedRanges) : null;
526 } elseif (isset($data['group_discount_ranges']) && empty($data['group_discount_ranges'])) {
527 $data['group_discount_ranges'] = null;
528 }
529
530 // Process category discounts (new format with traveler categories and ranges)
531 if (isset($data['category_discounts']) && is_array($data['category_discounts'])) {
532 $processedCategories = [];
533 foreach ($data['category_discounts'] as $categoryData) {
534 // New format: {traveler_category_id, traveler_category_label, ranges: [...]}
535 if (is_array($categoryData) && isset($categoryData['traveler_category_id'])) {
536 $processedCategory = [
537 'traveler_category_id' => sanitize_text_field($categoryData['traveler_category_id']),
538 'traveler_category_label' => sanitize_text_field($categoryData['traveler_category_label'] ?? ''),
539 'ranges' => []
540 ];
541
542 // Process ranges for this category
543 if (isset($categoryData['ranges']) && is_array($categoryData['ranges'])) {
544 foreach ($categoryData['ranges'] as $range) {
545 if (!is_array($range)) continue;
546
547 $allowed_types = ['percentage', 'fixed'];
548 $discountType = isset($range['discount_type']) && in_array($range['discount_type'], $allowed_types, true)
549 ? $range['discount_type']
550 : 'percentage';
551
552 $discountAmount = isset($range['discount_amount']) ? (float) $range['discount_amount'] : 0;
553 if ($discountAmount < 0) {
554 throw new \InvalidArgumentException('Category discount amount cannot be negative');
555 }
556
557 $processedCategory['ranges'][] = [
558 'id' => sanitize_text_field($range['id'] ?? uniqid()),
559 'min_group_size' => isset($range['min_group_size']) && $range['min_group_size'] !== '' ? absint($range['min_group_size']) : null,
560 'max_group_size' => isset($range['max_group_size']) && $range['max_group_size'] !== '' ? absint($range['max_group_size']) : null,
561 'discount_type' => $discountType,
562 'discount_amount' => $discountAmount
563 ];
564 }
565 }
566
567 $processedCategories[] = $processedCategory;
568 }
569 // Legacy format support: {category => {type, amount}}
570 elseif (is_array($categoryData) && isset($categoryData['type']) && isset($categoryData['amount'])) {
571 $allowed_types = ['percentage', 'fixed'];
572 if (!in_array($categoryData['type'], $allowed_types, true)) {
573 throw new \InvalidArgumentException('Invalid category discount type');
574 }
575 if ((float) $categoryData['amount'] < 0) {
576 throw new \InvalidArgumentException('Category discount amount cannot be negative');
577 }
578 $processedCategories[] = $categoryData;
579 }
580 }
581 $data['category_discounts'] = !empty($processedCategories) ? json_encode($processedCategories) : null;
582 } elseif (isset($data['category_discounts']) && empty($data['category_discounts'])) {
583 $data['category_discounts'] = null;
584 }
585 }
586 } else {
587 // If is_group_discount is not set, keep existing values (don't clear on partial updates)
588 // Only clear if explicitly set to false
589 }
590
591 $data['updated_by'] = absint(get_current_user_id());
592
593 return $data;
594 }
595
596 public function getAll(array $args = []): array
597 {
598 if (!empty($args['search'])) {
599 $search = sanitize_text_field($args['search']);
600 return $this->repository->search($search, $args);
601 }
602
603 if (!empty($args['status']) && $args['status'] !== 'all') {
604 $allowed_statuses = ['draft', 'publish', 'trash', 'expired'];
605 $status = in_array($args['status'], $allowed_statuses, true) ? $args['status'] : null;
606 if ($status) {
607 $args['where']['status'] = $status;
608 }
609 }
610
611 if (!empty($args['type']) && $args['type'] !== 'all') {
612 $allowed_types = ['percentage', 'fixed'];
613 $type = in_array($args['type'], $allowed_types, true) ? $args['type'] : null;
614 if ($type) {
615 $args['where']['type'] = $type;
616 }
617 }
618
619 return $this->repository->all($args);
620 }
621
622 public function count(array $args = []): int
623 {
624 if (!empty($args['search'])) {
625 $search = sanitize_text_field($args['search']);
626 $items = $this->repository->search($search, $args);
627 return count($items);
628 }
629
630 if (!empty($args['status']) && $args['status'] !== 'all') {
631 $allowed_statuses = ['draft', 'publish', 'trash', 'expired'];
632 $status = in_array($args['status'], $allowed_statuses, true) ? $args['status'] : null;
633 if ($status) {
634 $args['where']['status'] = $status;
635 }
636 }
637
638 if (!empty($args['type']) && $args['type'] !== 'all') {
639 $allowed_types = ['percentage', 'fixed'];
640 $type = in_array($args['type'], $allowed_types, true) ? $args['type'] : null;
641 if ($type) {
642 $args['where']['type'] = $type;
643 }
644 }
645
646 return $this->repository->count($args);
647 }
648
649 /**
650 * Admin toolbar: counts per discount status (stable, unfiltered).
651 *
652 * @return array{all: int, publish: int, draft: int, trash: int, expired: int}
653 */
654 public function getAdminStatusCounts(): array
655 {
656 return $this->repository->getAdminStatusCounts();
657 }
658
659 /**
660 * Calculate group discounts for booking
661 */
662 public function calculateGroupDiscounts(array $bookingData): array {
663 // Check if Advanced Discount module is enabled - group discounts are a Pro feature
664 if (!apply_filters('yatra_advanced_discount_enabled', false)) {
665 return [];
666 }
667
668 $totalTravelers = $this->countTotalTravelers($bookingData);
669
670 // Find applicable group discounts
671 $applicableDiscounts = $this->findApplicableGroupDiscounts($bookingData, $totalTravelers);
672
673 $discounts = [];
674 foreach ($applicableDiscounts as $discount) {
675 $discountAmount = $this->calculateGroupDiscountAmount($discount, $bookingData, $totalTravelers);
676 if ($discountAmount > 0) {
677 $discounts[] = [
678 'discount_id' => $discount->id,
679 'type' => 'group_discount',
680 'amount' => $discountAmount,
681 'description' => $this->generateGroupDiscountDescription($discount, $totalTravelers),
682 'mode' => $discount->group_discount_mode,
683 'category_breakdown' => $discount->group_discount_mode === 'category_based' ?
684 $this->calculateCategoryBreakdown($discount, $bookingData) : null
685 ];
686 }
687 }
688
689 return $discounts;
690 }
691
692 /**
693 * Count total billable travelers
694 */
695 private function countTotalTravelers(array $bookingData): int {
696 // Exclude infants from group size calculation (typically free or minimal cost)
697 return (
698 ($bookingData['travelers']['adults'] ?? 0) +
699 ($bookingData['travelers']['children'] ?? 0) +
700 ($bookingData['travelers']['seniors'] ?? 0)
701 );
702 }
703
704 /**
705 * Find applicable group discounts for the booking
706 */
707 private function findApplicableGroupDiscounts(array $bookingData, int $totalTravelers): array {
708 return \Yatra\Models\Discount::where('is_group_discount', true)
709 ->where('status', 'publish')
710 ->where(function($query) use ($bookingData) {
711 $query->whereNull('valid_from')
712 ->orWhere('valid_from', '<=', $bookingData['start_date']);
713 })
714 ->where(function($query) use ($bookingData) {
715 $query->whereNull('expiry_date')
716 ->orWhere('expiry_date', '>=', $bookingData['start_date']);
717 })
718 ->where(function($query) use ($bookingData) {
719 $query->where('applicable_to', 'all')
720 ->orWhere(function($subQuery) use ($bookingData) {
721 $subQuery->where('applicable_to', 'specific_trips')
722 ->whereJsonContains('trip_ids', $bookingData['trip_id']);
723 });
724 })
725 ->where(function($query) use ($totalTravelers) {
726 $query->where(function($subQuery) use ($totalTravelers) {
727 // Check if traveler count falls within any range
728 $subQuery->where(function($rangeQuery) use ($totalTravelers) {
729 // 1-10 range
730 $rangeQuery->where('min_group_size', '<=', $totalTravelers)
731 ->where(function($maxQuery) {
732 $maxQuery->whereNull('max_group_size')
733 ->orWhere('max_group_size', '>=', $totalTravelers);
734 });
735 });
736 });
737 })
738 ->orderBy('min_group_size', 'desc') // Prefer more restrictive discounts first
739 ->get();
740 }
741
742 /**
743 * Calculate group discount amount based on mode
744 */
745 private function calculateGroupDiscountAmount($discount, array $bookingData, int $totalTravelers): float {
746 if ($discount->group_discount_mode === 'category_based') {
747 return $this->calculateCategoryBasedDiscount($discount, $bookingData);
748 } else {
749 return $this->calculateTotalBasedDiscount($discount, $bookingData['subtotal'] ?? 0);
750 }
751 }
752
753 /**
754 * Calculate total-based group discount
755 */
756 private function calculateTotalBasedDiscount($discount, float $subtotal): float {
757 if ($discount->group_discount_type === 'percentage') {
758 $discountAmount = $subtotal * ($discount->group_discount_amount / 100);
759 } else {
760 $discountAmount = $discount->group_discount_amount;
761 }
762
763 return min($discountAmount, $subtotal); // Never exceed subtotal
764 }
765
766 /**
767 * Calculate category-based group discount
768 */
769 private function calculateCategoryBasedDiscount($discount, array $bookingData): float {
770 $totalDiscount = 0;
771 $categoryDiscounts = $discount->category_discounts ?? [];
772
773 foreach ($categoryDiscounts as $category => $discountConfig) {
774 $travelerCount = $bookingData['travelers'][$category] ?? 0;
775 if ($travelerCount > 0) {
776 // Calculate per-person price for this category
777 $categoryPrice = $this->calculateCategoryPrice($bookingData, $category);
778
779 if ($discountConfig['type'] === 'percentage') {
780 $categoryDiscount = $categoryPrice * $travelerCount * ($discountConfig['amount'] / 100);
781 } else {
782 $categoryDiscount = min($discountConfig['amount'] * $travelerCount, $categoryPrice * $travelerCount);
783 }
784
785 $totalDiscount += $categoryDiscount;
786 }
787 }
788
789 return $totalDiscount;
790 }
791
792 /**
793 * Calculate price per person for a specific traveler category
794 */
795 private function calculateCategoryPrice(array $bookingData, string $category): float {
796 $basePrice = $bookingData['trip_price'] ?? 0;
797
798 // Apply category multipliers (children might be 80%, seniors 90%, etc.)
799 $multipliers = [
800 'adults' => 1.0,
801 'children' => 0.8, // 80% of adult price
802 'seniors' => 0.9, // 90% of adult price
803 ];
804
805 return $basePrice * ($multipliers[$category] ?? 1.0);
806 }
807
808 /**
809 * Calculate category breakdown for display
810 */
811 private function calculateCategoryBreakdown($discount, array $bookingData): array {
812 $breakdown = [];
813 $categoryDiscounts = $discount->category_discounts ?? [];
814
815 foreach ($categoryDiscounts as $category => $discountConfig) {
816 $travelerCount = $bookingData['travelers'][$category] ?? 0;
817 if ($travelerCount > 0) {
818 $categoryPrice = $this->calculateCategoryPrice($bookingData, $category);
819 $totalCategoryPrice = $categoryPrice * $travelerCount;
820
821 if ($discountConfig['type'] === 'percentage') {
822 $discountAmount = $totalCategoryPrice * ($discountConfig['amount'] / 100);
823 } else {
824 $discountAmount = min($discountConfig['amount'] * $travelerCount, $totalCategoryPrice);
825 }
826
827 $breakdown[$category] = [
828 'traveler_count' => $travelerCount,
829 'original_price' => $totalCategoryPrice,
830 'discount_amount' => $discountAmount,
831 'final_price' => $totalCategoryPrice - $discountAmount,
832 'discount_type' => $discountConfig['type'],
833 'discount_rate' => $discountConfig['amount']
834 ];
835 }
836 }
837
838 return $breakdown;
839 }
840
841 /**
842 * Generate human-readable discount description
843 */
844 private function generateGroupDiscountDescription($discount, int $totalTravelers): string {
845 $rangeText = $this->formatGroupSizeRange($discount);
846 $discountText = $this->formatDiscountAmount($discount);
847
848 if ($discount->group_discount_mode === 'category_based') {
849 return "Group discount for {$totalTravelers} travelers ({$rangeText}) - Category-based rates";
850 } else {
851 return "Group discount for {$totalTravelers} travelers ({$rangeText}): {$discountText}";
852 }
853 }
854
855 /**
856 * Format group size range for display
857 */
858 private function formatGroupSizeRange($discount): string {
859 if ($discount->max_group_size) {
860 return "{$discount->min_group_size}-{$discount->max_group_size} people";
861 } else {
862 return "{$discount->min_group_size}+ people";
863 }
864 }
865
866 /**
867 * Format discount amount for display
868 */
869 private function formatDiscountAmount($discount): string {
870 $amount = $discount->group_discount_amount ?? 0;
871 if ($discount->group_discount_type === 'percentage') {
872 /* translators: %s: discount percentage. */
873 return sprintf(__('%s%% off', 'yatra'), $amount);
874 }
875
876 // Was a hardcoded "$" with default separators, so every non-dollar site
877 // showed the wrong currency (and the label could not be translated).
878 /* translators: %s: discount amount, already formatted with the site currency. */
879 return sprintf(__('%s off', 'yatra'), yatra_format_price((float) $amount, null, false));
880 }
881
882 /**
883 * @param array $travelerCounts Array of category_id => count (e.g., ['3' => 4, '5' => 1])
884 * @param array $priceTypes Array of price type objects with category_id and effective_price
885 * @return array|null Discount info or null if no discount applies
886 */
887 public function calculateGroupDiscount(int $tripId, array $travelerCounts, array $priceTypes = []): ?array
888 {
889 // Cast tripId to int to ensure type safety
890 $tripId = (int) $tripId;
891
892 // Check if Advanced Discount module is enabled - group discounts are a Pro feature
893 if (!apply_filters('yatra_advanced_discount_enabled', false)) {
894 return null;
895 }
896
897 $groupDiscounts = $this->getGroupDiscountsForTrip($tripId);
898
899 if (empty($groupDiscounts)) {
900 return null;
901 }
902
903
904 $totalTravelers = array_sum(array_map('intval', $travelerCounts));
905
906 // Build price + price-type lookup by category_id
907 $priceByCategory = [];
908 $ptByCategory = [];
909 foreach ($priceTypes as $pt) {
910 $pt = (array) $pt;
911 $categoryId = $pt['category_id'] ?? null;
912 if ($categoryId !== null) {
913 $priceByCategory[$categoryId] = (float) ($pt['effective_price'] ?? $pt['sale_price'] ?? $pt['original_price'] ?? 0);
914 $ptByCategory[$categoryId] = $pt;
915 }
916 }
917
918 // Effective subtotal for a category — delegate to the single source of
919 // truth so the discount base ALWAYS matches CalculationService's charge
920 // (per-person × count, flat per-group, or per-block group pricing).
921 $catSubtotal = function ($categoryId, $count) use ($priceByCategory, $ptByCategory): float {
922 return \Yatra\Services\TripPricingService::categoryLineSubtotal(
923 $ptByCategory[$categoryId] ?? [],
924 (int) $count,
925 (float) ($priceByCategory[$categoryId] ?? 0)
926 );
927 };
928
929
930 foreach ($groupDiscounts as $discount) {
931 $discountMode = $discount->discount_mode ?? 'total';
932
933 // Category-based discounts: check each category's count and apply to that category's subtotal
934 if ($discountMode === 'category_based' && !empty($discount->category_discounts)) {
935 $totalDiscountAmount = 0;
936 $appliedCategories = [];
937
938 $categoryDiscounts = $this->decodeStoredList($discount->category_discounts ?? null);
939 foreach ($categoryDiscounts as $catDiscount) {
940 $catDiscount = (object) $catDiscount;
941 $categoryId = $catDiscount->traveler_category_id ?? null;
942 if ($categoryId === null) continue;
943
944 // Get the count for this specific category
945 $categoryCount = (int) ($travelerCounts[$categoryId] ?? 0);
946 if ($categoryCount <= 0) continue;
947
948 // Check if this category's count falls within any range
949 if (!empty($catDiscount->ranges)) {
950 $ranges = $this->decodeStoredList($catDiscount->ranges ?? null);
951 foreach ($ranges as $range) {
952 $range = (object) $range;
953 $minSize = (int) ($range->min_group_size ?? 0);
954 $maxSize = !empty($range->max_group_size) ? (int) $range->max_group_size : PHP_INT_MAX;
955
956 if ($categoryCount >= $minSize && $categoryCount <= $maxSize) {
957 $discountType = $range->discount_type ?? 'percentage';
958 $discountValue = (float) ($range->discount_amount ?? 0);
959
960 // Calculate discount for this category's subtotal only
961 // (flat for per-group, price × count for per-person).
962 $categorySubtotal = $catSubtotal($categoryId, $categoryCount);
963
964 if ($discountType === 'percentage') {
965 $categoryDiscount = $categorySubtotal * ($discountValue / 100);
966 } else {
967 $categoryDiscount = $discountValue;
968 }
969
970 $totalDiscountAmount += $categoryDiscount;
971 $appliedCategories[] = [
972 'category_id' => $categoryId,
973 'category_label' => $catDiscount->traveler_category_label ?? 'Traveler',
974 'count' => $categoryCount,
975 'discount_type' => $discountType,
976 'discount_value' => $discountValue,
977 'discount_amount' => $categoryDiscount,
978 ];
979 break; // Found matching range for this category
980 }
981 }
982 }
983 }
984
985 if ($totalDiscountAmount > 0) {
986 // Build label with discount info from applied categories
987 $discountInfo = '';
988 if (!empty($appliedCategories)) {
989 $firstCat = $appliedCategories[0];
990 if ($firstCat['discount_type'] === 'percentage') {
991 /* translators: %s: discount percentage value. */
992 $discountInfo = sprintf(__('Group Discount (%s%%)', 'yatra'), $firstCat['discount_value']);
993 } else {
994 /* translators: %s: formatted discount amount. */
995 $discountInfo = sprintf(__('Group Discount (%s)', 'yatra'), yatra_format_price($firstCat['discount_value']));
996 }
997 } else {
998 $discountInfo = __('Group Discount', 'yatra');
999 }
1000
1001 return [
1002 'type' => 'category_based',
1003 'amount' => round($totalDiscountAmount, 2),
1004 'code' => $discount->code ?? null,
1005 'label' => $discountInfo,
1006 'applied_categories' => $appliedCategories,
1007 ];
1008 }
1009 }
1010 // Total-based discounts: check total travelers and apply to total
1011 elseif (!empty($discount->group_discount_ranges)) {
1012 $groupDiscountRanges = $this->decodeStoredList($discount->group_discount_ranges ?? null);
1013 foreach ($groupDiscountRanges as $range) {
1014 $range = (object) $range;
1015 $minSize = (int) ($range->min_group_size ?? 0);
1016 $maxSize = !empty($range->max_group_size) ? (int) $range->max_group_size : PHP_INT_MAX;
1017
1018 if ($totalTravelers >= $minSize && $totalTravelers <= $maxSize) {
1019 $discountType = $range->discount_type ?? 'percentage';
1020 $discountValue = (float) ($range->discount_amount ?? 0);
1021
1022 // Calculate total subtotal from all categories
1023 $totalSubtotal = 0;
1024 foreach ($travelerCounts as $catId => $count) {
1025 $totalSubtotal += $catSubtotal($catId, $count);
1026 }
1027
1028 // Calculate the actual discount amount
1029 $calculatedAmount = $discountType === 'percentage'
1030 ? $totalSubtotal * ($discountValue / 100)
1031 : $discountValue;
1032
1033 return [
1034 'type' => $discountType,
1035 'value' => $discountValue,
1036 'amount' => round($calculatedAmount, 2),
1037 'code' => $discount->code ?? null,
1038 'label' => $discountType === 'percentage'
1039 /* translators: %s: discount percentage value. */
1040 ? sprintf(__('Group Discount (%s%%)', 'yatra'), $discountValue)
1041 /* translators: %s: formatted discount amount. */
1042 : sprintf(__('Group Discount (%s)', 'yatra'), yatra_format_price($discountValue)),
1043 ];
1044 }
1045 }
1046 } else {
1047 // Legacy format
1048 $minSize = (int) ($discount->min_group_size ?? 0);
1049 $maxSize = !empty($discount->max_group_size) ? (int) $discount->max_group_size : PHP_INT_MAX;
1050
1051 // If min_size is 0 but we have a discount value, this might be a simple discount
1052 // that applies to any group size >= 2
1053 if ($minSize === 0 && $discountValue > 0) {
1054 $minSize = 2; // Default to minimum 2 travelers for group discount
1055 }
1056
1057 // Handle different field names for discount type and value
1058 $discountType = $discount->type ?? $discount->discount_type ?? 'percentage';
1059 $discountValue = (float) ($discount->amount ?? $discount->discount_amount ?? 0);
1060
1061
1062 if ($totalTravelers >= $minSize && $totalTravelers <= $maxSize) {
1063
1064 // Calculate total subtotal from all categories
1065 $totalSubtotal = 0;
1066 foreach ($travelerCounts as $catId => $count) {
1067 $totalSubtotal += $catSubtotal($catId, $count);
1068 }
1069
1070 // Calculate the actual discount amount
1071 $calculatedAmount = $discountType === 'percentage'
1072 ? $totalSubtotal * ($discountValue / 100)
1073 : $discountValue;
1074
1075 return [
1076 'type' => $discountType,
1077 'value' => $discountValue,
1078 'amount' => round($calculatedAmount, 2),
1079 'code' => $discount->code ?? null,
1080 'label' => $discountType === 'percentage'
1081 /* translators: %s: discount percentage value. */
1082 ? sprintf(__('Group Discount (%s%%)', 'yatra'), $discountValue)
1083 /* translators: %s: formatted discount amount. */
1084 : sprintf(__('Group Discount (%s)', 'yatra'), yatra_format_price($discountValue)),
1085 ];
1086 }
1087 }
1088 }
1089
1090 return null;
1091 }
1092
1093 /**
1094 * Calculate coupon discount for booking
1095 *
1096 * @param string $coupon_code Coupon code to apply
1097 * @param float $subtotal Subtotal amount (after group discount)
1098 * @param int $trip_id Trip ID
1099 * @param int $travelers_count Total travelers
1100 * @param array $traveler_counts Traveler counts by category
1101 * @return array Coupon discount data with code, type, amount, calculated_amount, label
1102 */
1103 public function calculateCouponDiscount(
1104 string $coupon_code,
1105 float $subtotal,
1106 int $trip_id,
1107 int $travelers_count = 1,
1108 array $traveler_counts = []
1109 ): array {
1110 // Default empty discount
1111 $default = [
1112 'code' => $coupon_code,
1113 'type' => '',
1114 'amount' => 0,
1115 'calculated_amount' => 0,
1116 'label' => __('Coupon Discount', 'yatra'),
1117 ];
1118
1119 if (empty($coupon_code)) {
1120 return $default;
1121 }
1122
1123 // Find discount by code
1124 $discount = $this->repository->findByCode($coupon_code);
1125
1126 $status = (string) ($discount->status ?? '');
1127 // Migrated coupons once used "active"; 3.x uses "publish" for live discounts.
1128 $isLive = ($status === 'publish' || $status === 'active');
1129 if (!$discount || !$isLive) {
1130 return $default;
1131 }
1132
1133 // Validate coupon
1134 $validation = $this->validateCoupon($discount, $trip_id, $subtotal, $travelers_count);
1135 if (!$validation['valid']) {
1136 return $default;
1137 }
1138
1139 // Calculate discount amount
1140 $calculated_discount = 0;
1141
1142 if ($discount->type === 'percentage') {
1143 $calculated_discount = ($subtotal * (float) $discount->amount) / 100;
1144 } elseif ($discount->type === 'fixed') {
1145 $calculated_discount = min((float) $discount->amount, $subtotal);
1146 }
1147
1148 // Apply max discount cap if set
1149 if (!empty($discount->max_discount_amount) && $calculated_discount > (float) $discount->max_discount_amount) {
1150 $calculated_discount = (float) $discount->max_discount_amount;
1151 }
1152
1153 return [
1154 'code' => $coupon_code,
1155 'type' => $discount->type,
1156 'amount' => (float) $discount->amount,
1157 'calculated_amount' => round($calculated_discount, 2),
1158 'label' => $discount->type === 'percentage'
1159 /* translators: %s: discount percentage value. */
1160 ? sprintf(__('Coupon (%s%%)', 'yatra'), $discount->amount)
1161 : __('Coupon Discount', 'yatra'),
1162 ];
1163 }
1164
1165 /**
1166 * Decode list-shaped discount DB fields: JSON (current storage), array, or legacy PHP serialized.
1167 *
1168 * @param mixed $raw
1169 * @return array<int|string, mixed>
1170 */
1171 private function decodeStoredList($raw): array
1172 {
1173 if ($raw === null || $raw === '') {
1174 return [];
1175 }
1176 if (is_array($raw)) {
1177 return $raw;
1178 }
1179 if (is_object($raw)) {
1180 $asArray = json_decode(wp_json_encode($raw), true);
1181
1182 return is_array($asArray) ? $asArray : [];
1183 }
1184 if (!is_string($raw)) {
1185 return [];
1186 }
1187 $trimmed = trim($raw);
1188 if ($trimmed === '') {
1189 return [];
1190 }
1191 $first = $trimmed[0];
1192 if ($first === '[' || $first === '{') {
1193 $decoded = json_decode($trimmed, true);
1194
1195 return is_array($decoded) ? $decoded : [];
1196 }
1197
1198 $maybe = maybe_unserialize($trimmed);
1199
1200 return is_array($maybe) ? $maybe : [];
1201 }
1202
1203 /**
1204 * Validate coupon for booking
1205 *
1206 * @param \stdClass $discount Discount object
1207 * @param int $trip_id Trip ID
1208 * @param float $total Total amount
1209 * @param int $travelers_count Travelers count
1210 * @return array Validation result with 'valid' and 'message'
1211 */
1212 private function validateCoupon(\stdClass $discount, int $trip_id, float $total, int $travelers_count): array
1213 {
1214 // Check validity dates
1215 $now = current_time('Y-m-d');
1216
1217 if (!empty($discount->valid_from) && $now < $discount->valid_from) {
1218 return ['valid' => false, 'message' => __('This coupon is not yet valid.', 'yatra')];
1219 }
1220
1221 if (!empty($discount->expiry_date) && $now > $discount->expiry_date) {
1222 return ['valid' => false, 'message' => __('This coupon has expired.', 'yatra')];
1223 }
1224
1225 // Check usage limit
1226 if ($discount->usage_limit > 0 && $discount->usage_count >= $discount->usage_limit) {
1227 return ['valid' => false, 'message' => __('This coupon has reached its usage limit.', 'yatra')];
1228 }
1229
1230 // Check if applicable to this trip
1231 if ($discount->applicable_to === 'specific_trips') {
1232 $trip_ids = is_string($discount->trip_ids) ? maybe_unserialize($discount->trip_ids) : ($discount->trip_ids ?? []);
1233 if (!empty($trip_ids) && !in_array($trip_id, array_map('intval', $trip_ids), true)) {
1234 return ['valid' => false, 'message' => __('This coupon is not applicable to this trip.', 'yatra')];
1235 }
1236 }
1237
1238 // Check minimum amount
1239 if (!empty($discount->min_amount) && $total < (float) $discount->min_amount) {
1240 return [
1241 'valid' => false,
1242 'message' => sprintf(
1243 /* translators: %s: formatted minimum amount. */
1244 __('Minimum amount of %s required for this coupon.', 'yatra'),
1245 yatra_format_price((float) $discount->min_amount)
1246 )
1247 ];
1248 }
1249
1250 return ['valid' => true, 'message' => ''];
1251 }
1252 }
1253