PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
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 / CalculationService.php

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

717 lines 30.6 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\Services\SettingsService;
8 use Yatra\Services\DiscountService;
9 use Yatra\Repositories\TripRepository;
10
11 /**
12 * Core Pricing Calculation Service
13 *
14 * Single source of truth for ALL pricing calculations.
15 * Both booking session and checkout MUST go through this service.
16 *
17 * PRICING LOGIC:
18 * - Regular pricing: discounted_price → sale_price → original_price
19 * - Traveler-based pricing: per-category discounted → sale → original prices
20 *
21 * FREE PLUGIN FEATURES (handled here):
22 * - Base pricing (regular & traveler-based)
23 * - Tax calculations (single & multiple)
24 * - Coupon discount (via DiscountService)
25 * - Group discount (via DiscountService, requires Pro AdvancedDiscount module)
26 * - Payment amounts (full only, deposit/partial via Pro)
27 *
28 * PRO MODULE INTEGRATION (via filter hooks):
29 * - Dynamic Pricing → yatra_booking_trip_price (modifies per-unit price)
30 * - Additional Services → yatra_booking_services_total (adds services cost)
31 * - Advanced Discount → yatra_advanced_discount_enabled (enables group discounts)
32 * - Flexible Payments → yatra_calculate_amount_due (deposit/partial payments)
33 *
34 * FILTER HOOKS (in execution order):
35 * 1. yatra_before_calculation_params - Modify input params
36 * 2. yatra_booking_trip_price - Dynamic pricing per-unit price modification
37 * 3. yatra_calculate_base_amount - Modify calculated base amount
38 * 4. yatra_booking_services_total - Additional services cost (Pro)
39 * 5. yatra_calculate_subtotal - Modify subtotal (base + services)
40 * 6. yatra_calculate_group_discount - Group discount (Pro AdvancedDiscount)
41 * 7. yatra_calculate_coupon_discount - Coupon discount
42 * 8. yatra_calculate_final_total - Modify final total after tax
43 * 9. yatra_calculate_amount_due - Flexible payment amounts (Pro)
44 * 10. yatra_after_calculation_result - Modify complete result
45 *
46 * @package Yatra\Services
47 */
48 class CalculationService
49 {
50 /**
51 * Core pricing calculation (fetches trip data from repository)
52 *
53 * @param array $params Calculation parameters
54 * @return array Complete pricing breakdown
55 */
56 public function calculatePricing(array $params): array
57 {
58 $params = apply_filters('yatra_before_calculation_params', $params);
59
60 // Extract parameters
61 $trip_id = (int) ($params['trip_id'] ?? 0);
62 $travelers_count = (int) ($params['travelers_count'] ?? 1);
63 $traveler_counts = $params['traveler_counts'] ?? [];
64 $travel_date = $params['travel_date'] ?? '';
65 $departure_time = $params['departure_time'] ?? '';
66 $coupon_code = $params['coupon_code'] ?? '';
67 $payment_method = strtolower(trim((string) ($params['payment_method'] ?? 'full')));
68 if ($payment_method === '') {
69 $payment_method = 'full';
70 }
71 $selected_services = $params['selected_services'] ?? [];
72 $availability_id = $params['availability_id'] ?? null;
73
74 if (empty($trip_id)) {
75 throw new \InvalidArgumentException('Trip ID is required for calculation');
76 }
77
78 // Fetch trip data
79 $tripRepository = new TripRepository();
80 $trip = $tripRepository->find($trip_id);
81
82 if (!$trip) {
83 throw new \InvalidArgumentException("Trip with ID {$trip_id} not found");
84 }
85
86 // ── Resolve pricing type & price data ───────────────────────────
87 // Availability can override trip-level pricing
88 $availability = null;
89 if (!empty($availability_id) || !empty($travel_date)) {
90 $availability = $this->resolveAvailability($trip_id, $travel_date, $availability_id, $departure_time);
91 }
92
93 $pricing_type = $this->resolvePricingType($trip, $availability);
94 $price_types = $this->resolvePriceTypes($trip, $availability);
95
96 // Context for Pro dynamic pricing (inventory rules, etc.): prefer departure row seats, not trip max only.
97 $spots_for_dp = null;
98 if ($availability !== null) {
99 if (isset($availability->seats_available)) {
100 $spots_for_dp = (int) $availability->seats_available;
101 } elseif (isset($availability->spots_remaining)) {
102 $spots_for_dp = (int) $availability->spots_remaining;
103 }
104 }
105 if ($spots_for_dp === null && isset($trip->max_travelers)) {
106 $spots_for_dp = (int) $trip->max_travelers;
107 }
108 $availability_id_for_dp = $availability_id;
109 if ($availability_id_for_dp === null && $availability !== null && isset($availability->id)) {
110 $availability_id_for_dp = (int) $availability->id;
111 }
112
113 // ── Resolve per-unit price (Regular pricing) ────────────────────
114 // Priority: discounted_price → sale_price → original_price
115 $original_price = (float) ($trip->original_price ?? 0);
116 $discounted_price = $this->resolveDiscountedPrice($trip, $availability);
117 $unit_price = $discounted_price > 0 ? $discounted_price : $original_price;
118
119 // Apply Dynamic Pricing filter (Pro DynamicPricingModule hooks here)
120 $unit_price = (float) apply_filters('yatra_booking_trip_price', $unit_price, $trip_id, [
121 'departure_date' => $travel_date,
122 'spots_remaining' => $spots_for_dp,
123 'availability_id' => $availability_id_for_dp,
124 'original_price' => $original_price,
125 'discounted_price' => $discounted_price,
126 ]);
127
128 // ── Calculate base amount ───────────────────────────────────────
129 $base_amount = $this->calculateBaseAmount(
130 $unit_price,
131 $travelers_count,
132 $traveler_counts,
133 $pricing_type,
134 $price_types,
135 $trip_id,
136 $travel_date,
137 $spots_for_dp,
138 $availability_id_for_dp
139 );
140
141 $base_amount = (float) apply_filters('yatra_calculate_base_amount', $base_amount, [
142 'unit_price' => $unit_price,
143 'original_price' => $original_price,
144 'discounted_price' => $discounted_price,
145 'travelers_count' => $travelers_count,
146 'traveler_counts' => $traveler_counts,
147 'pricing_type' => $pricing_type,
148 'price_types' => $price_types,
149 'trip_id' => $trip_id,
150 ]);
151
152 // ── Subtotal (Pro modules can add services cost via filter) ─────
153 // Free plugin: subtotal = base_amount only
154 // Pro AdditionalServicesModule: hooks into yatra_calculate_subtotal to add services
155 $subtotal = $base_amount;
156 $subtotal = (float) apply_filters('yatra_calculate_subtotal', $subtotal, [
157 'base_amount' => $base_amount,
158 'trip_id' => $trip_id,
159 'travelers_count' => $travelers_count,
160 'traveler_counts' => $traveler_counts,
161 'travel_date' => $travel_date,
162 'selected_services' => $selected_services,
163 ]);
164
165 // ── Group Discount (AdvancedDiscount Pro module enables this) ───
166 $group_discount_data = $this->calculateGroupDiscount(
167 $trip_id, $subtotal, $travelers_count, $traveler_counts, $pricing_type, $price_types
168 );
169
170 // ── Coupon Discount ─────────────────────────────────────────────
171 // Use DiscountService for coupon calculation
172 $discountService = new \Yatra\Services\DiscountService();
173 $subtotal_after_group = $subtotal - ($group_discount_data['amount'] ?? 0);
174
175 $coupon_discount_data = $discountService->calculateCouponDiscount(
176 $coupon_code,
177 $subtotal_after_group,
178 $trip_id,
179 $travelers_count,
180 $traveler_counts
181 );
182
183 // Allow plugins to modify coupon discount
184 $coupon_discount_data = (array) apply_filters('yatra_calculate_coupon_discount', $coupon_discount_data, [
185 'subtotal' => $subtotal,
186 'group_discount_amount' => $group_discount_data['amount'] ?? 0,
187 'trip_id' => $trip_id,
188 'travelers_count' => $travelers_count,
189 ]);
190
191 // ── Total discounts ─────────────────────────────────────────────
192 $total_discount_amount = ($group_discount_data['amount'] ?? 0) + ($coupon_discount_data['calculated_amount'] ?? 0);
193 $total_discount_amount = min($total_discount_amount, $subtotal); // discount cannot exceed subtotal
194
195 $discounted_subtotal = max(0, $subtotal - $total_discount_amount);
196
197 // ── Itinerary Costs ─────────────────────────────────────────
198 $itinerary_costs = apply_filters('yatra_booking_itinerary_costs', [], $trip_id, $travelers_count, $traveler_counts, $travel_date);
199 $itinerary_costs_total = 0;
200 if (!empty($itinerary_costs) && is_array($itinerary_costs)) {
201 foreach ($itinerary_costs as $cost) {
202 $itinerary_costs_total += (float) ($cost['total_cost'] ?? 0);
203 }
204 }
205
206 // ── Additional Services (if any) ─────────────────────────────
207 $additional_services_total = 0;
208 // Note: Additional services can be added here via filters
209
210 // ── Taxable Amount (includes itinerary costs and services) ────
211 $taxable_amount = $discounted_subtotal + $itinerary_costs_total + $additional_services_total;
212
213 // ── Taxes ───────────────────────────────────────────────────────
214 $tax_calculation = $this->calculateTaxes($taxable_amount);
215
216 $final_total = $taxable_amount;
217 if (!$tax_calculation['tax_inclusive']) {
218 $final_total += $tax_calculation['total_tax_amount'];
219 }
220
221 $final_total = (float) apply_filters('yatra_calculate_final_total', $final_total, [
222 'discounted_subtotal' => $discounted_subtotal,
223 'itinerary_costs_total' => $itinerary_costs_total,
224 'taxable_amount' => $taxable_amount,
225 'tax_calculation' => $tax_calculation,
226 'trip_id' => $trip_id,
227 'payment_method' => $payment_method,
228 ]);
229
230 // ── Payment amounts (FlexiblePayments Pro module) ───────────────
231 $payment_amounts = $this->calculatePaymentAmounts($final_total, $payment_method, [
232 'trip_id' => $trip_id,
233 'travelers_count' => $travelers_count,
234 ]);
235
236 // ── Currency ────────────────────────────────────────────────────
237 $currency = SettingsService::getCurrency();
238
239 // ── Gross total (subtotal before discounts/taxes, includes services) ───
240 $gross_total = $subtotal;
241
242 // ── Build result ────────────────────────────────────────────
243 $pricing_data = [
244 // Price info
245 'original_price' => $original_price,
246 'discounted_price' => $discounted_price,
247 'unit_price' => $unit_price,
248 'pricing_type' => $pricing_type,
249
250 // Base amounts
251 'base_amount' => $base_amount,
252 'subtotal' => $subtotal,
253 'discounted_subtotal' => $discounted_subtotal,
254 'taxable_amount' => $taxable_amount,
255 'gross_total' => $gross_total,
256
257 // Discounts
258 'group_discount' => $group_discount_data,
259 'coupon_discount' => $coupon_discount_data,
260 'total_discount_amount' => $total_discount_amount,
261
262 // Taxes
263 'tax_calculation' => $tax_calculation,
264
265 // Final amounts
266 'final_total' => $final_total,
267 'amount_due' => $payment_amounts['amount_due'],
268 'amount_paid' => $payment_amounts['amount_paid'],
269
270 // Payment & currency
271 'payment_method' => $payment_method,
272 'currency' => $currency,
273
274 // Itinerary costs
275 'itinerary_costs' => $itinerary_costs,
276 'itinerary_costs_total' => $itinerary_costs_total,
277
278 // Metadata
279 'travelers_count' => $travelers_count,
280 'traveler_counts' => $traveler_counts,
281 'travel_date' => $travel_date,
282 'trip_id' => $trip_id,
283 ];
284
285 return (array) apply_filters('yatra_after_calculation_result', $pricing_data, $params);
286 }
287
288 /**
289 * Resolve the discounted price from availability or trip
290 * Priority: availability discounted → availability original → trip discounted → trip sale → 0
291 */
292 private function resolveDiscountedPrice(object $trip, ?object $availability): float
293 {
294 if ($availability) {
295 if (!empty($availability->discounted_price) && (float) $availability->discounted_price > 0) {
296 return (float) $availability->discounted_price;
297 }
298 if (!empty($availability->original_price) && (float) $availability->original_price > 0) {
299 return (float) $availability->original_price;
300 }
301 }
302
303 if (!empty($trip->discounted_price) && (float) $trip->discounted_price > 0) {
304 return (float) $trip->discounted_price;
305 }
306 if (!empty($trip->sale_price) && (float) $trip->sale_price > 0) {
307 return (float) $trip->sale_price;
308 }
309
310 return 0.0;
311 }
312
313 /**
314 * Resolve pricing type from availability or trip
315 */
316 private function resolvePricingType(object $trip, ?object $availability): string
317 {
318 // Authoritative model matches {@see TripPricingService::resolvePricingType}:
319 // explicit "regular" must not be overridden by inherited/stale availability price_types.
320 $trip_model = TripPricingService::resolvePricingType($trip);
321
322 if ($trip_model !== 'traveler_based') {
323 return 'regular';
324 }
325
326 // Traveler-based trip: use per-date categories when that row defines them; otherwise caller
327 // falls back to trip-level price_types via {@see self::resolvePriceTypes()}.
328 if ($availability && !empty($availability->price_types)) {
329 $types = is_string($availability->price_types)
330 ? json_decode($availability->price_types, true)
331 : $availability->price_types;
332 if (!empty($types) && is_array($types)) {
333 return 'traveler_based';
334 }
335 }
336
337 return 'traveler_based';
338 }
339
340 /**
341 * Resolve price_types array from availability or trip
342 */
343 private function resolvePriceTypes(object $trip, ?object $availability): array
344 {
345 if (TripPricingService::resolvePricingType($trip) === 'regular') {
346 return [];
347 }
348
349 $types = [];
350
351 // Priority 1: Availability price_types
352 if ($availability && !empty($availability->price_types)) {
353 $types = is_string($availability->price_types)
354 ? json_decode($availability->price_types, true)
355 : $availability->price_types;
356 }
357
358 // Priority 2: Trip price_types
359 if (empty($types) && !empty($trip->price_types)) {
360 if (is_string($trip->price_types)) {
361 $types = json_decode($trip->price_types, true) ?: [];
362 } else {
363 $types = is_array($trip->price_types) ? $trip->price_types : [];
364 }
365 }
366
367 if (empty($types)) {
368 return [];
369 }
370
371 // Enrich with pricing_mode from category metadata if missing
372 $needs_enrichment = false;
373 foreach ($types as $pt) {
374 $pt = (array) $pt;
375 if (empty($pt['pricing_mode'])) {
376 $needs_enrichment = true;
377 break;
378 }
379 }
380
381 if ($needs_enrichment) {
382 $category_ids = array_filter(array_map(function($pt) {
383 $pt = (array) $pt;
384 return isset($pt['category_id']) ? (int) $pt['category_id'] : null;
385 }, $types));
386
387 if (!empty($category_ids)) {
388 $category_meta = $this->getCategoryMetadata($category_ids);
389 foreach ($types as &$pt) {
390 if (is_object($pt)) $pt = (array) $pt;
391 $cat_id = isset($pt['category_id']) ? (int) $pt['category_id'] : null;
392 if ($cat_id && isset($category_meta[$cat_id]) && empty($pt['pricing_mode'])) {
393 $pt['pricing_mode'] = $category_meta[$cat_id]['pricing_mode'] ?? 'per_person';
394 }
395 }
396 unset($pt);
397 }
398 }
399
400 return $types;
401 }
402
403 /**
404 * Get category metadata (pricing_mode, etc.) by IDs
405 */
406 private function getCategoryMetadata(array $category_ids): array
407 {
408 // Use repository instead of direct database query
409 $repository = new \Yatra\Repositories\TravelerCategoryRepository();
410 return $repository->getMetadataByIds($category_ids);
411 }
412
413 /**
414 * Resolve availability data
415 */
416 private function resolveAvailability(int $trip_id, string $travel_date, ?int $availability_id, string $departure_time = ''): ?object
417 {
418 if (!class_exists('\Yatra\Services\AvailabilityService')) {
419 return null;
420 }
421
422 try {
423 // Try by availability_id first via repository
424 if (!empty($availability_id) && class_exists('\Yatra\Repositories\AvailabilityRepository')) {
425 $repo = new \Yatra\Repositories\AvailabilityRepository();
426 if (method_exists($repo, 'find')) {
427 $result = $repo->find($availability_id);
428 if ($result) {
429 return $result;
430 }
431 }
432 }
433
434 // Fallback: lookup by trip + date + time (time-aware for day tours)
435 if (!empty($travel_date)) {
436 $repo = new \Yatra\Repositories\AvailabilityRepository();
437 $availabilityService = new \Yatra\Services\AvailabilityService($repo);
438 return $availabilityService->getByTripAndDateTime($trip_id, $travel_date, $departure_time ?: null);
439 }
440 } catch (\Exception $e) {
441 // Availability lookup failed, continue with trip-level pricing
442 }
443
444 return null;
445 }
446
447 /**
448 * Calculate base amount (regular or traveler-based pricing)
449 *
450 * For traveler-based: uses per-category effective prices × counts
451 * For regular: uses unit_price × travelers_count
452 */
453 private function calculateBaseAmount(
454 float $unit_price,
455 int $travelers_count,
456 array $traveler_counts,
457 string $pricing_type,
458 array $price_types,
459 int $trip_id,
460 string $travel_date = '',
461 ?int $spots_remaining = null,
462 ?int $availability_id_for_dp = null
463 ): float {
464 if ($pricing_type === 'traveler_based' && !empty($price_types)) {
465 $base_amount = 0.0;
466
467 foreach ($price_types as $pt) {
468 $pt = (array) $pt;
469 $category_id = $pt['category_id'] ?? 0;
470 $pricing_mode = $pt['pricing_mode'] ?? 'per_person';
471
472 $category_price = TripPricingService::resolveCategoryEffectivePrice($pt);
473
474 // Apply Dynamic Pricing filter per category
475 $category_price = (float) apply_filters('yatra_booking_trip_price', $category_price, $trip_id, [
476 'departure_date' => $travel_date,
477 'category_id' => $category_id,
478 'spots_remaining' => $spots_remaining,
479 'availability_id' => $availability_id_for_dp,
480 'original_price' => (float) ($pt['original_price'] ?? 0),
481 'discounted_price' => (float) ($pt['discounted_price'] ?? $pt['sale_price'] ?? 0),
482 ]);
483
484 $count = isset($traveler_counts[$category_id]) ? (int) $traveler_counts[$category_id] : 0;
485
486 if ($pricing_mode === 'per_group') {
487 // Per group: charge flat price once if any travelers in this category
488 if ($count > 0) {
489 $base_amount += $category_price;
490 }
491 } else {
492 // Per person: charge per traveler
493 $base_amount += $category_price * $count;
494 }
495 }
496
497 return round($base_amount, 2);
498 }
499
500 // Regular pricing
501 return round($unit_price * $travelers_count, 2);
502 }
503
504 /**
505 * Calculate group discount
506 */
507 private function calculateGroupDiscount(
508 int $trip_id,
509 float $subtotal,
510 int $travelers_count,
511 array $traveler_counts,
512 string $pricing_type,
513 array $price_types
514 ): array {
515 $group_discount_data = [
516 'amount' => 0,
517 'label' => __('Group Discount', 'yatra'),
518 'code' => null,
519 ];
520
521 // Try DiscountService (requires Pro AdvancedDiscount module to enable)
522 if (class_exists('\Yatra\Services\DiscountService') && method_exists('\Yatra\Services\DiscountService', 'calculateGroupDiscount')) {
523 try {
524 $discountService = new DiscountService();
525 $discountResult = $discountService->calculateGroupDiscount($trip_id, $traveler_counts, $price_types);
526
527 // Debug: Log the calculation result
528 if (WP_DEBUG && WP_DEBUG_LOG) {
529 $priceTypesDebug = [];
530 foreach ($price_types as $pt) {
531 $pt = (object) $pt;
532 $priceTypesDebug[] = [
533 'category_id' => $pt->category_id ?? null,
534 'price' => $pt->effective_price ?? $pt->sale_price ?? $pt->original_price ?? 0
535 ];
536 }
537
538 }
539
540 if ($discountResult && !empty($discountResult['amount'])) {
541 return [
542 'amount' => (float) ($discountResult['amount'] ?? 0),
543 'label' => $discountResult['label'] ?? __('Group Discount', 'yatra'),
544 'code' => $discountResult['code'] ?? null,
545 ];
546 }
547 } catch (\Exception $e) {
548
549 }
550 }
551
552 // Fall back to filter (Pro plugins can hook here)
553 return (array) apply_filters('yatra_calculate_group_discount', $group_discount_data, [
554 'subtotal' => $subtotal,
555 'travelers_count' => $travelers_count,
556 'traveler_counts' => $traveler_counts,
557 'pricing_type' => $pricing_type,
558 'price_types' => $price_types,
559 'trip_id' => $trip_id,
560 ]);
561 }
562
563 /**
564 * Calculate taxes
565 */
566 private function calculateTaxes(float $subtotal): array
567 {
568 $enable_tax = SettingsService::get('enable_tax', false);
569 $tax_rate = (float) SettingsService::get('tax_rate', 0);
570 $tax_label = SettingsService::get('tax_label', 'Tax');
571 $tax_inclusive = (bool) SettingsService::get('tax_inclusive', false);
572 $multiple_taxes_enabled = (bool) SettingsService::get('multiple_taxes_enabled', false);
573 $multiple_taxes = SettingsService::get('multiple_taxes', []);
574
575 if (!is_array($multiple_taxes)) {
576 $multiple_taxes = [];
577 }
578
579 // If tax is enabled and taxes are configured, honor them even if the boolean
580 // "multiple_taxes_enabled" setting wasn't toggled in the UI (common case).
581 if ($enable_tax && !empty($multiple_taxes)) {
582 $multiple_taxes_enabled = true;
583 } elseif (count($multiple_taxes) > 1) {
584 $multiple_taxes_enabled = true;
585 }
586
587 $tax_breakdown = [];
588 $total_tax_amount = 0.0;
589
590 if ($multiple_taxes_enabled && !empty($multiple_taxes)) {
591 foreach ($multiple_taxes as $tax) {
592 $rate = isset($tax['rate']) ? (float) $tax['rate'] : 0;
593 $name = $tax['name'] ?? 'Tax';
594
595 if ($rate > 0) {
596 $amount = $tax_inclusive
597 ? $subtotal * ($rate / (100 + $rate))
598 : $subtotal * ($rate / 100);
599 $amount = round($amount, 2);
600
601 $tax_breakdown[] = ['name' => $name, 'rate' => $rate, 'amount' => $amount];
602 $total_tax_amount += $amount;
603 }
604 }
605 } elseif ($enable_tax && $tax_rate > 0) {
606 $amount = $tax_inclusive
607 ? $subtotal * ($tax_rate / (100 + $tax_rate))
608 : $subtotal * ($tax_rate / 100);
609 $amount = round($amount, 2);
610
611 $tax_breakdown[] = ['name' => $tax_label, 'rate' => $tax_rate, 'amount' => $amount];
612 $total_tax_amount = $amount;
613 }
614
615 return [
616 'enable_tax' => $enable_tax,
617 'tax_rate' => $tax_rate,
618 'tax_label' => $tax_label,
619 'tax_inclusive' => $tax_inclusive,
620 'multiple_taxes_enabled' => $multiple_taxes_enabled,
621 'multiple_taxes' => $multiple_taxes,
622 'tax_breakdown' => $tax_breakdown,
623 'total_tax_amount' => $total_tax_amount,
624 'subtotal' => $subtotal,
625 'total_with_tax' => $tax_inclusive ? $subtotal : ($subtotal + $total_tax_amount),
626 ];
627 }
628
629 /**
630 * Calculate payment amounts
631 *
632 * Free: full payment only
633 * Pro FlexiblePayments: deposit/partial via yatra_calculate_amount_due filter
634 */
635 private function calculatePaymentAmounts(float $final_total, string $payment_method, array $context): array
636 {
637 $amount_due = $final_total;
638 $amount_paid = 0.0;
639
640 // Apply Pro FlexiblePayments filter for deposit/partial
641 $amount_due = (float) apply_filters('yatra_calculate_amount_due', $amount_due, $final_total, $payment_method);
642
643 // Fallback: if no Pro module handled it, use basic logic (never use === on floats)
644 $flexible_enabled = apply_filters('yatra_flexible_payments_enabled', false);
645 $unchanged = abs($amount_due - $final_total) < 0.000001;
646 if ($unchanged && $payment_method !== 'full' && $flexible_enabled) {
647 $deposit_percentage = (int) apply_filters('yatra_deposit_percentage', 20);
648 $partial_percentage = (int) apply_filters('yatra_partial_payment_percentage', 30);
649 if ($payment_method === 'deposit') {
650 $amount_due = round($final_total * ($deposit_percentage / 100), 2);
651 } elseif ($payment_method === 'partial') {
652 $amount_due = round($final_total * ($partial_percentage / 100), 2);
653 }
654 }
655
656 $payment_data = (array) apply_filters('yatra_calculate_payment_amounts', [
657 'amount_due' => $amount_due,
658 'amount_paid' => $amount_paid,
659 'payment_method' => $payment_method,
660 'final_total' => $final_total,
661 ], $context);
662
663 return [
664 'amount_due' => (float) ($payment_data['amount_due'] ?? $amount_due),
665 'amount_paid' => (float) ($payment_data['amount_paid'] ?? $amount_paid),
666 ];
667 }
668
669 /**
670 * Calculate pricing from session data
671 *
672 * Used by booking-content.php template for checkout summary
673 */
674 public function calculateFromSession(array $session_data, string $coupon_code = '', string $payment_method = 'full'): array
675 {
676 $trip_id = (int) ($session_data['trip_id'] ?? 0);
677
678 if (empty($trip_id)) {
679 throw new \InvalidArgumentException('Trip ID is required in session data');
680 }
681
682 $payment_method = strtolower(trim($payment_method));
683 if ($payment_method === '') {
684 $payment_method = 'full';
685 }
686
687 $params = [
688 'trip_id' => $trip_id,
689 'travelers_count' => (int) ($session_data['travelers'] ?? 1),
690 'traveler_counts' => $session_data['traveler_counts'] ?? [],
691 'travel_date' => $session_data['travel_date'] ?? '',
692 'departure_time' => $session_data['departure_time'] ?? '',
693 'coupon_code' => $coupon_code,
694 'payment_method' => $payment_method,
695 'selected_services' => $session_data['additional_services'] ?? [],
696 'availability_id' => !empty($session_data['availability_id']) ? (int) $session_data['availability_id'] : null,
697 ];
698
699 $params = (array) apply_filters('yatra_session_calculation_params', $params, $session_data);
700
701 return $this->calculatePricing($params);
702 }
703
704 /**
705 * Quick pricing calculation for simple cases
706 */
707 public function quickCalculate(int $trip_id, int $travelers_count, string $travel_date = ''): array
708 {
709 return $this->calculatePricing([
710 'trip_id' => $trip_id,
711 'travelers_count' => $travelers_count,
712 'travel_date' => $travel_date,
713 'payment_method' => 'full',
714 ]);
715 }
716 }
717