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

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

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