PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.9
Yatra – Travel Booking & Tour Operator Software v3.0.9
3.0.15 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 All 83 releases
yatra / app / Services / DiscountService.php

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

1,249 lines 56.8 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 return "{$amount}% off";
873 } else {
874 return "$" . number_format((float) $amount, 2) . " off";
875 }
876 }
877
878 /**
879 * @param array $travelerCounts Array of category_id => count (e.g., ['3' => 4, '5' => 1])
880 * @param array $priceTypes Array of price type objects with category_id and effective_price
881 * @return array|null Discount info or null if no discount applies
882 */
883 public function calculateGroupDiscount(int $tripId, array $travelerCounts, array $priceTypes = []): ?array
884 {
885 // Cast tripId to int to ensure type safety
886 $tripId = (int) $tripId;
887
888 // Check if Advanced Discount module is enabled - group discounts are a Pro feature
889 if (!apply_filters('yatra_advanced_discount_enabled', false)) {
890 return null;
891 }
892
893 $groupDiscounts = $this->getGroupDiscountsForTrip($tripId);
894
895 if (empty($groupDiscounts)) {
896 return null;
897 }
898
899
900 $totalTravelers = array_sum(array_map('intval', $travelerCounts));
901
902 // Build price + price-type lookup by category_id
903 $priceByCategory = [];
904 $ptByCategory = [];
905 foreach ($priceTypes as $pt) {
906 $pt = (array) $pt;
907 $categoryId = $pt['category_id'] ?? null;
908 if ($categoryId !== null) {
909 $priceByCategory[$categoryId] = (float) ($pt['effective_price'] ?? $pt['sale_price'] ?? $pt['original_price'] ?? 0);
910 $ptByCategory[$categoryId] = $pt;
911 }
912 }
913
914 // Effective subtotal for a category — delegate to the single source of
915 // truth so the discount base ALWAYS matches CalculationService's charge
916 // (per-person × count, flat per-group, or per-block group pricing).
917 $catSubtotal = function ($categoryId, $count) use ($priceByCategory, $ptByCategory): float {
918 return \Yatra\Services\TripPricingService::categoryLineSubtotal(
919 $ptByCategory[$categoryId] ?? [],
920 (int) $count,
921 (float) ($priceByCategory[$categoryId] ?? 0)
922 );
923 };
924
925
926 foreach ($groupDiscounts as $discount) {
927 $discountMode = $discount->discount_mode ?? 'total';
928
929 // Category-based discounts: check each category's count and apply to that category's subtotal
930 if ($discountMode === 'category_based' && !empty($discount->category_discounts)) {
931 $totalDiscountAmount = 0;
932 $appliedCategories = [];
933
934 $categoryDiscounts = $this->decodeStoredList($discount->category_discounts ?? null);
935 foreach ($categoryDiscounts as $catDiscount) {
936 $catDiscount = (object) $catDiscount;
937 $categoryId = $catDiscount->traveler_category_id ?? null;
938 if ($categoryId === null) continue;
939
940 // Get the count for this specific category
941 $categoryCount = (int) ($travelerCounts[$categoryId] ?? 0);
942 if ($categoryCount <= 0) continue;
943
944 // Check if this category's count falls within any range
945 if (!empty($catDiscount->ranges)) {
946 $ranges = $this->decodeStoredList($catDiscount->ranges ?? null);
947 foreach ($ranges as $range) {
948 $range = (object) $range;
949 $minSize = (int) ($range->min_group_size ?? 0);
950 $maxSize = !empty($range->max_group_size) ? (int) $range->max_group_size : PHP_INT_MAX;
951
952 if ($categoryCount >= $minSize && $categoryCount <= $maxSize) {
953 $discountType = $range->discount_type ?? 'percentage';
954 $discountValue = (float) ($range->discount_amount ?? 0);
955
956 // Calculate discount for this category's subtotal only
957 // (flat for per-group, price × count for per-person).
958 $categorySubtotal = $catSubtotal($categoryId, $categoryCount);
959
960 if ($discountType === 'percentage') {
961 $categoryDiscount = $categorySubtotal * ($discountValue / 100);
962 } else {
963 $categoryDiscount = $discountValue;
964 }
965
966 $totalDiscountAmount += $categoryDiscount;
967 $appliedCategories[] = [
968 'category_id' => $categoryId,
969 'category_label' => $catDiscount->traveler_category_label ?? 'Traveler',
970 'count' => $categoryCount,
971 'discount_type' => $discountType,
972 'discount_value' => $discountValue,
973 'discount_amount' => $categoryDiscount,
974 ];
975 break; // Found matching range for this category
976 }
977 }
978 }
979 }
980
981 if ($totalDiscountAmount > 0) {
982 // Build label with discount info from applied categories
983 $discountInfo = '';
984 if (!empty($appliedCategories)) {
985 $firstCat = $appliedCategories[0];
986 if ($firstCat['discount_type'] === 'percentage') {
987 /* translators: %s: discount percentage value. */
988 $discountInfo = sprintf(__('Group Discount (%s%%)', 'yatra'), $firstCat['discount_value']);
989 } else {
990 /* translators: %s: formatted discount amount. */
991 $discountInfo = sprintf(__('Group Discount (%s)', 'yatra'), yatra_format_price($firstCat['discount_value']));
992 }
993 } else {
994 $discountInfo = __('Group Discount', 'yatra');
995 }
996
997 return [
998 'type' => 'category_based',
999 'amount' => round($totalDiscountAmount, 2),
1000 'code' => $discount->code ?? null,
1001 'label' => $discountInfo,
1002 'applied_categories' => $appliedCategories,
1003 ];
1004 }
1005 }
1006 // Total-based discounts: check total travelers and apply to total
1007 elseif (!empty($discount->group_discount_ranges)) {
1008 $groupDiscountRanges = $this->decodeStoredList($discount->group_discount_ranges ?? null);
1009 foreach ($groupDiscountRanges as $range) {
1010 $range = (object) $range;
1011 $minSize = (int) ($range->min_group_size ?? 0);
1012 $maxSize = !empty($range->max_group_size) ? (int) $range->max_group_size : PHP_INT_MAX;
1013
1014 if ($totalTravelers >= $minSize && $totalTravelers <= $maxSize) {
1015 $discountType = $range->discount_type ?? 'percentage';
1016 $discountValue = (float) ($range->discount_amount ?? 0);
1017
1018 // Calculate total subtotal from all categories
1019 $totalSubtotal = 0;
1020 foreach ($travelerCounts as $catId => $count) {
1021 $totalSubtotal += $catSubtotal($catId, $count);
1022 }
1023
1024 // Calculate the actual discount amount
1025 $calculatedAmount = $discountType === 'percentage'
1026 ? $totalSubtotal * ($discountValue / 100)
1027 : $discountValue;
1028
1029 return [
1030 'type' => $discountType,
1031 'value' => $discountValue,
1032 'amount' => round($calculatedAmount, 2),
1033 'code' => $discount->code ?? null,
1034 'label' => $discountType === 'percentage'
1035 /* translators: %s: discount percentage value. */
1036 ? sprintf(__('Group Discount (%s%%)', 'yatra'), $discountValue)
1037 /* translators: %s: formatted discount amount. */
1038 : sprintf(__('Group Discount (%s)', 'yatra'), yatra_format_price($discountValue)),
1039 ];
1040 }
1041 }
1042 } else {
1043 // Legacy format
1044 $minSize = (int) ($discount->min_group_size ?? 0);
1045 $maxSize = !empty($discount->max_group_size) ? (int) $discount->max_group_size : PHP_INT_MAX;
1046
1047 // If min_size is 0 but we have a discount value, this might be a simple discount
1048 // that applies to any group size >= 2
1049 if ($minSize === 0 && $discountValue > 0) {
1050 $minSize = 2; // Default to minimum 2 travelers for group discount
1051 }
1052
1053 // Handle different field names for discount type and value
1054 $discountType = $discount->type ?? $discount->discount_type ?? 'percentage';
1055 $discountValue = (float) ($discount->amount ?? $discount->discount_amount ?? 0);
1056
1057
1058 if ($totalTravelers >= $minSize && $totalTravelers <= $maxSize) {
1059
1060 // Calculate total subtotal from all categories
1061 $totalSubtotal = 0;
1062 foreach ($travelerCounts as $catId => $count) {
1063 $totalSubtotal += $catSubtotal($catId, $count);
1064 }
1065
1066 // Calculate the actual discount amount
1067 $calculatedAmount = $discountType === 'percentage'
1068 ? $totalSubtotal * ($discountValue / 100)
1069 : $discountValue;
1070
1071 return [
1072 'type' => $discountType,
1073 'value' => $discountValue,
1074 'amount' => round($calculatedAmount, 2),
1075 'code' => $discount->code ?? null,
1076 'label' => $discountType === 'percentage'
1077 /* translators: %s: discount percentage value. */
1078 ? sprintf(__('Group Discount (%s%%)', 'yatra'), $discountValue)
1079 /* translators: %s: formatted discount amount. */
1080 : sprintf(__('Group Discount (%s)', 'yatra'), yatra_format_price($discountValue)),
1081 ];
1082 }
1083 }
1084 }
1085
1086 return null;
1087 }
1088
1089 /**
1090 * Calculate coupon discount for booking
1091 *
1092 * @param string $coupon_code Coupon code to apply
1093 * @param float $subtotal Subtotal amount (after group discount)
1094 * @param int $trip_id Trip ID
1095 * @param int $travelers_count Total travelers
1096 * @param array $traveler_counts Traveler counts by category
1097 * @return array Coupon discount data with code, type, amount, calculated_amount, label
1098 */
1099 public function calculateCouponDiscount(
1100 string $coupon_code,
1101 float $subtotal,
1102 int $trip_id,
1103 int $travelers_count = 1,
1104 array $traveler_counts = []
1105 ): array {
1106 // Default empty discount
1107 $default = [
1108 'code' => $coupon_code,
1109 'type' => '',
1110 'amount' => 0,
1111 'calculated_amount' => 0,
1112 'label' => __('Coupon Discount', 'yatra'),
1113 ];
1114
1115 if (empty($coupon_code)) {
1116 return $default;
1117 }
1118
1119 // Find discount by code
1120 $discount = $this->repository->findByCode($coupon_code);
1121
1122 $status = (string) ($discount->status ?? '');
1123 // Migrated coupons once used "active"; 3.x uses "publish" for live discounts.
1124 $isLive = ($status === 'publish' || $status === 'active');
1125 if (!$discount || !$isLive) {
1126 return $default;
1127 }
1128
1129 // Validate coupon
1130 $validation = $this->validateCoupon($discount, $trip_id, $subtotal, $travelers_count);
1131 if (!$validation['valid']) {
1132 return $default;
1133 }
1134
1135 // Calculate discount amount
1136 $calculated_discount = 0;
1137
1138 if ($discount->type === 'percentage') {
1139 $calculated_discount = ($subtotal * (float) $discount->amount) / 100;
1140 } elseif ($discount->type === 'fixed') {
1141 $calculated_discount = min((float) $discount->amount, $subtotal);
1142 }
1143
1144 // Apply max discount cap if set
1145 if (!empty($discount->max_discount_amount) && $calculated_discount > (float) $discount->max_discount_amount) {
1146 $calculated_discount = (float) $discount->max_discount_amount;
1147 }
1148
1149 return [
1150 'code' => $coupon_code,
1151 'type' => $discount->type,
1152 'amount' => (float) $discount->amount,
1153 'calculated_amount' => round($calculated_discount, 2),
1154 'label' => $discount->type === 'percentage'
1155 /* translators: %s: discount percentage value. */
1156 ? sprintf(__('Coupon (%s%%)', 'yatra'), $discount->amount)
1157 : __('Coupon Discount', 'yatra'),
1158 ];
1159 }
1160
1161 /**
1162 * Decode list-shaped discount DB fields: JSON (current storage), array, or legacy PHP serialized.
1163 *
1164 * @param mixed $raw
1165 * @return array<int|string, mixed>
1166 */
1167 private function decodeStoredList($raw): array
1168 {
1169 if ($raw === null || $raw === '') {
1170 return [];
1171 }
1172 if (is_array($raw)) {
1173 return $raw;
1174 }
1175 if (is_object($raw)) {
1176 $asArray = json_decode(wp_json_encode($raw), true);
1177
1178 return is_array($asArray) ? $asArray : [];
1179 }
1180 if (!is_string($raw)) {
1181 return [];
1182 }
1183 $trimmed = trim($raw);
1184 if ($trimmed === '') {
1185 return [];
1186 }
1187 $first = $trimmed[0];
1188 if ($first === '[' || $first === '{') {
1189 $decoded = json_decode($trimmed, true);
1190
1191 return is_array($decoded) ? $decoded : [];
1192 }
1193
1194 $maybe = maybe_unserialize($trimmed);
1195
1196 return is_array($maybe) ? $maybe : [];
1197 }
1198
1199 /**
1200 * Validate coupon for booking
1201 *
1202 * @param \stdClass $discount Discount object
1203 * @param int $trip_id Trip ID
1204 * @param float $total Total amount
1205 * @param int $travelers_count Travelers count
1206 * @return array Validation result with 'valid' and 'message'
1207 */
1208 private function validateCoupon(\stdClass $discount, int $trip_id, float $total, int $travelers_count): array
1209 {
1210 // Check validity dates
1211 $now = current_time('Y-m-d');
1212
1213 if (!empty($discount->valid_from) && $now < $discount->valid_from) {
1214 return ['valid' => false, 'message' => __('This coupon is not yet valid.', 'yatra')];
1215 }
1216
1217 if (!empty($discount->expiry_date) && $now > $discount->expiry_date) {
1218 return ['valid' => false, 'message' => __('This coupon has expired.', 'yatra')];
1219 }
1220
1221 // Check usage limit
1222 if ($discount->usage_limit > 0 && $discount->usage_count >= $discount->usage_limit) {
1223 return ['valid' => false, 'message' => __('This coupon has reached its usage limit.', 'yatra')];
1224 }
1225
1226 // Check if applicable to this trip
1227 if ($discount->applicable_to === 'specific_trips') {
1228 $trip_ids = is_string($discount->trip_ids) ? maybe_unserialize($discount->trip_ids) : ($discount->trip_ids ?? []);
1229 if (!empty($trip_ids) && !in_array($trip_id, array_map('intval', $trip_ids), true)) {
1230 return ['valid' => false, 'message' => __('This coupon is not applicable to this trip.', 'yatra')];
1231 }
1232 }
1233
1234 // Check minimum amount
1235 if (!empty($discount->min_amount) && $total < (float) $discount->min_amount) {
1236 return [
1237 'valid' => false,
1238 'message' => sprintf(
1239 /* translators: %s: formatted minimum amount. */
1240 __('Minimum amount of %s required for this coupon.', 'yatra'),
1241 yatra_format_price((float) $discount->min_amount)
1242 )
1243 ];
1244 }
1245
1246 return ['valid' => true, 'message' => ''];
1247 }
1248 }
1249