PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.9
Yatra – Travel Booking & Tour Operator Software v3.0.2.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 / Controllers / DiscountController.php

DiscountController.php in Yatra – Travel Booking & Tour Operator Software 3.0.2.9, at app/Controllers/DiscountController.php

464 lines 16.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Yatra\Controllers;
6
7 use WP_REST_Request;
8 use WP_REST_Response;
9 use WP_Error;
10 use Yatra\Services\DiscountService;
11 use Yatra\Repositories\DiscountRepository;
12 use Yatra\Models\Discount;
13
14 /**
15 * Discount REST API Controller
16 *
17 * Endpoints:
18 * - GET /discounts - List discounts
19 * - POST /discounts - Create discount
20 * - GET /discounts/{id} - Get single discount
21 * - PUT /discounts/{id} - Update discount
22 * - DELETE /discounts/{id} - Delete discount
23 */
24 class DiscountController extends BaseController
25 {
26 protected string $rest_base = 'discounts';
27
28 private DiscountService $service;
29
30 public function __construct()
31 {
32 $this->service = new DiscountService();
33 }
34
35 public function register_routes(): void
36 {
37 $this->registerCrudRoutes(array_merge(
38 $this->getStatusArg(),
39 [
40 'type' => [
41 'default' => 'all',
42 'sanitize_callback' => 'sanitize_text_field',
43 ],
44 ]
45 ));
46
47 register_rest_route($this->namespace, '/' . $this->rest_base . '/stats', [
48 [
49 'methods' => \WP_REST_Server::READABLE,
50 'callback' => [$this, 'get_stats'],
51 'permission_callback' => [$this, 'check_permission'],
52 ],
53 ]);
54
55 // Group discount discoverability endpoint
56 register_rest_route($this->namespace, '/' . $this->rest_base . '/group-discounts', [
57 [
58 'methods' => \WP_REST_Server::READABLE,
59 'callback' => [$this, 'get_group_discounts'],
60 'permission_callback' => [$this, 'check_public_permission'],
61 'args' => [
62 'trip_ids' => [
63 'required' => false,
64 'type' => 'array',
65 'items' => ['type' => 'integer'],
66 'description' => 'Array of trip IDs to check for group discounts',
67 ],
68 ],
69 ],
70 ]);
71 }
72
73 public function check_public_permission(?WP_REST_Request $request = null): bool
74 {
75 // Allow public access to group discount discoverability
76 return true;
77 }
78
79 /**
80 * Get group discount availability for trips
81 * Public endpoint for frontend discoverability
82 */
83 public function get_group_discounts(WP_REST_Request $request)
84 {
85 try {
86 $tripIds = $request->get_param('trip_ids');
87
88 if (empty($tripIds) || !is_array($tripIds)) {
89 return $this->error_response(__('Trip IDs are required', 'yatra'), 400);
90 }
91
92 // Sanitize trip IDs
93 $tripIds = array_map('absint', array_filter($tripIds));
94
95 if (empty($tripIds)) {
96 return $this->error_response(__('Valid trip IDs are required', 'yatra'), 400);
97 }
98
99 $result = [];
100
101 foreach ($tripIds as $tripId) {
102 $groupDiscounts = $this->getTripGroupDiscounts($tripId);
103 $result[$tripId] = [
104 'has_group_discounts' => !empty($groupDiscounts),
105 'discounts' => $groupDiscounts,
106 'summary' => $this->generateGroupDiscountSummary($groupDiscounts),
107 ];
108 }
109
110 return $this->success_response($result);
111
112 } catch (\Exception $e) {
113 return $this->error_response($e->getMessage(), 500);
114 }
115 }
116
117 /**
118 * Get group discounts for a specific trip
119 */
120 private function getTripGroupDiscounts(int $tripId): array
121 {
122 // Check if Advanced Discount module is enabled - group discounts are a Pro feature
123 if (!apply_filters('yatra_advanced_discount_enabled', false)) {
124 return [];
125 }
126 // Discount is a simple DTO model (not Eloquent), so use repository + PHP filtering.
127 $repo = new DiscountRepository();
128 $rows = $repo->getActiveGroupDiscounts();
129
130 $today = date('Y-m-d');
131
132 $discounts = array_values(array_filter(array_map(function ($row) use ($today, $tripId) {
133 $arr = (array) $row;
134 $discount = Discount::fromArray($arr);
135
136 if (empty($discount->is_group_discount)) {
137 return null;
138 }
139
140 // Repository already filters status='publish', but keep this defensive.
141 if (($arr['status'] ?? null) !== 'publish' && ($discount->status ?? null) !== 'publish') {
142 return null;
143 }
144
145 $validFrom = is_string($discount->valid_from) ? trim($discount->valid_from) : '';
146 if ($validFrom !== '' && $validFrom !== '0000-00-00' && $validFrom > $today) {
147 return null;
148 }
149
150 $expiry = is_string($discount->expiry_date) ? trim($discount->expiry_date) : '';
151 if ($expiry !== '' && $expiry !== '0000-00-00' && $expiry < $today) {
152 return null;
153 }
154
155 $applicableTo = (string) ($discount->applicable_to ?? 'all');
156 if ($applicableTo === 'all') {
157 return $discount;
158 }
159
160 if ($applicableTo === 'specific_trips') {
161 $tripIds = $discount->trip_ids ?? [];
162 if (is_array($tripIds) && in_array($tripId, array_map('absint', $tripIds), true)) {
163 return $discount;
164 }
165 }
166
167 return null;
168 }, $rows)));
169
170 usort($discounts, function (Discount $a, Discount $b) {
171 return (int) ($a->min_group_size ?? 0) <=> (int) ($b->min_group_size ?? 0);
172 });
173
174 $result = [];
175 foreach ($discounts as $discount) {
176 $result[] = [
177 'id' => $discount->id,
178 'min_group_size' => $discount->min_group_size,
179 'max_group_size' => $discount->max_group_size,
180 'discount_type' => $discount->group_discount_type,
181 'discount_amount' => $discount->group_discount_amount,
182 'discount_mode' => $discount->group_discount_mode,
183 'category_discounts' => $discount->category_discounts,
184 'range_label' => $this->formatGroupSizeRange($discount),
185 'discount_label' => $this->formatDiscountLabel($discount),
186 ];
187 }
188
189 return $result;
190 }
191
192 /**
193 * Generate summary text for group discounts
194 */
195 private function generateGroupDiscountSummary(array $discounts): string
196 {
197 if (empty($discounts)) {
198 return '';
199 }
200
201 $ranges = array_map(function($discount) {
202 return $discount['range_label'];
203 }, $discounts);
204
205 $uniqueRanges = array_unique($ranges);
206
207 if (count($uniqueRanges) === 1) {
208 /* translators: 1: discount label, 2: range label */
209 return sprintf(__('Up to %1$s for %2$s', 'yatra'), $discounts[0]['discount_label'], $uniqueRanges[0]);
210 }
211
212 $firstDiscount = $discounts[0];
213 /* translators: 1: discount label, 2: minimum group size */
214 return sprintf(__('Up to %1$s for groups starting at %2$d people', 'yatra'), $firstDiscount['discount_label'], (int) $firstDiscount['min_group_size']);
215 }
216
217 /**
218 * Format group size range for display
219 */
220 private function formatGroupSizeRange($discount): string
221 {
222 if ($discount->max_group_size) {
223 /* translators: 1: min group size, 2: max group size */
224 return sprintf(__('%1$d-%2$d people', 'yatra'), (int) $discount->min_group_size, (int) $discount->max_group_size);
225 }
226
227 /* translators: %d: minimum group size */
228 return sprintf(__('%d+ people', 'yatra'), (int) $discount->min_group_size);
229 }
230
231 /**
232 * Format discount label for display
233 */
234 private function formatDiscountLabel($discount): string
235 {
236 if ($discount->group_discount_type === 'percentage') {
237 /* translators: %s: discount percentage */
238 return sprintf(__('%s%% off', 'yatra'), (string) $discount->group_discount_amount);
239 }
240
241 /* translators: %s: discount amount */
242 return sprintf(__('%s off', 'yatra'), '$' . number_format((float) $discount->group_discount_amount, 2));
243 }
244
245 public function check_permission(?WP_REST_Request $request = null): bool
246 {
247 if ($request === null) {
248 return true;
249 }
250
251 if (!is_user_logged_in()) {
252 return false;
253 }
254
255 if (current_user_can('manage_options')) {
256 return true;
257 }
258
259 switch ($request->get_method()) {
260 case 'GET':
261 return current_user_can('yatra_view_bookings');
262 case 'POST':
263 case 'PUT':
264 case 'PATCH':
265 case 'DELETE':
266 return current_user_can('yatra_edit_bookings');
267 default:
268 return current_user_can('manage_options');
269 }
270 }
271
272 /**
273 * GET /discounts/stats — counts per status for admin toolbar tabs
274 */
275 public function get_stats(WP_REST_Request $request)
276 {
277 try {
278 return $this->success_response($this->service->getAdminStatusCounts());
279 } catch (\Exception $e) {
280 return $this->error_response($e->getMessage(), 500);
281 }
282 }
283
284 public function get_items(WP_REST_Request $request)
285 {
286 try {
287 $params = $this->getPaginationParams($request);
288
289 // Override default orderby
290 $orderby = $request->get_param('orderby') ?: 'created_at';
291
292 $args = [
293 'limit' => $params['per_page'],
294 'offset' => ($params['page'] - 1) * $params['per_page'],
295 'order_by' => $orderby,
296 'order' => $params['order'],
297 ];
298
299 if (!empty($params['search'])) {
300 $args['search'] = $params['search'];
301 }
302
303 $status = $request->get_param('status');
304 if ($status && $status !== 'all') {
305 $args['status'] = sanitize_text_field($status);
306 }
307
308 $type = $request->get_param('type');
309 if ($type && $type !== 'all' && in_array($type, ['percentage', 'fixed'], true)) {
310 $args['type'] = $type;
311 }
312
313 $items = $this->service->getAll($args);
314 $total = $this->service->count($args);
315
316 $prepared = array_map([$this, 'prepareItem'], $items);
317
318 return $this->paginated_response($prepared, $total, $params['page'], $params['per_page']);
319 } catch (\Exception $e) {
320 return $this->error_response($e->getMessage(), 500);
321 }
322 }
323
324 public function get_item(WP_REST_Request $request)
325 {
326 try {
327 $item = $this->service->getById($this->getId($request));
328
329 if (!$item) {
330 return $this->not_found(__('Discount not found', 'yatra'));
331 }
332
333 return $this->success_response($this->prepareItem($item));
334 } catch (\Exception $e) {
335 return $this->error_response($e->getMessage(), 500);
336 }
337 }
338
339 public function create_item(WP_REST_Request $request)
340 {
341 try {
342 $data = $this->getBody($request);
343
344 // Check if Advanced Discount module is required for this discount type
345 $discount_mode = $data['discount_mode'] ?? 'promo';
346 $is_group_discount = !empty($data['is_group_discount']);
347
348 if (($discount_mode === 'group' || $discount_mode === 'both' || $is_group_discount)
349 && !apply_filters('yatra_advanced_discount_enabled', false)) {
350 return $this->validation_error(__('Advanced Discount module is required for Group Discounts. Please enable it in Modules.', 'yatra'));
351 }
352
353 $id = $this->service->create($data);
354
355 return $this->success_response([
356 'id' => $id,
357 'message' => __('Discount created successfully', 'yatra'),
358 ], 201);
359 } catch (\InvalidArgumentException $e) {
360 return $this->validation_error($e->getMessage());
361 } catch (\Exception $e) {
362 return $this->error_response($e->getMessage(), 500);
363 }
364 }
365
366 public function update_item(WP_REST_Request $request)
367 {
368 try {
369 $data = $this->getBody($request);
370
371 // Check if Advanced Discount module is required for this discount type
372 $discount_mode = $data['discount_mode'] ?? 'promo';
373 $is_group_discount = !empty($data['is_group_discount']);
374
375 if (($discount_mode === 'group' || $discount_mode === 'both' || $is_group_discount)
376 && !apply_filters('yatra_advanced_discount_enabled', false)) {
377 return $this->validation_error(__('Advanced Discount module is required for Group Discounts. Please enable it in Modules.', 'yatra'));
378 }
379
380 $result = $this->service->update($this->getId($request), $data);
381
382 if (!$result) {
383 return $this->error_response(__('Failed to update discount', 'yatra'), 500);
384 }
385
386 return $this->success_response([
387 'message' => __('Discount updated successfully', 'yatra'),
388 ]);
389 } catch (\InvalidArgumentException $e) {
390 return $this->validation_error($e->getMessage());
391 } catch (\Exception $e) {
392 return $this->error_response($e->getMessage(), 500);
393 }
394 }
395
396 public function delete_item(WP_REST_Request $request)
397 {
398 try {
399 $result = $this->service->delete($this->getId($request));
400
401 if (!$result) {
402 return $this->error_response(__('Failed to delete discount', 'yatra'), 500);
403 }
404
405 return $this->success_response([
406 'message' => __('Discount deleted successfully', 'yatra'),
407 ]);
408 } catch (\Exception $e) {
409 return $this->error_response($e->getMessage(), 500);
410 }
411 }
412
413 private function prepareItem($item): array
414 {
415 $prepared = (array) $item;
416
417 if (isset($prepared['trip_ids']) && is_string($prepared['trip_ids'])) {
418 $prepared['trip_ids'] = maybe_unserialize($prepared['trip_ids']);
419 }
420
421 // Ensure trip_ids is always an array
422 if (!is_array($prepared['trip_ids'])) {
423 $prepared['trip_ids'] = [];
424 }
425
426 // Deserialize category_discounts if it's a JSON string
427 if (isset($prepared['category_discounts']) && is_string($prepared['category_discounts'])) {
428 $prepared['category_discounts'] = json_decode($prepared['category_discounts'], true) ?: [];
429 }
430
431 // Deserialize group_discount_ranges if it's a JSON string
432 if (isset($prepared['group_discount_ranges']) && is_string($prepared['group_discount_ranges'])) {
433 $prepared['group_discount_ranges'] = json_decode($prepared['group_discount_ranges'], true) ?: [];
434 }
435
436 $prepared['first_time_customer_only'] = (bool) ($prepared['first_time_customer_only'] ?? false);
437 $prepared['is_group_discount'] = (bool) ($prepared['is_group_discount'] ?? false);
438
439 if (!$prepared['is_group_discount']) {
440 $prepared['min_group_size'] = null;
441 $prepared['group_discount_type'] = null;
442 $prepared['group_discount_amount'] = null;
443 }
444
445 if (!empty($prepared['created_by'])) {
446 $user = get_userdata((int) $prepared['created_by']);
447 $prepared['created_by_name'] = $user ? esc_html($user->display_name) : null;
448 }
449
450 if (!empty($prepared['updated_by'])) {
451 $user = get_userdata((int) $prepared['updated_by']);
452 $prepared['updated_by_name'] = $user ? esc_html($user->display_name) : null;
453 }
454
455 // Calculate actual usage count from bookings
456 if (!empty($prepared['code'])) {
457 $discountRepository = new \Yatra\Repositories\DiscountRepository();
458 $prepared['usage_count'] = $discountRepository->countUsage($prepared['code']);
459 }
460
461 return $prepared;
462 }
463 }
464