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 / Services / CalculationService.php

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

693 lines 29.7 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
514 }
515
516 if ($discountResult && !empty($discountResult['amount'])) {
517 return [
518 'amount' => (float) ($discountResult['amount'] ?? 0),
519 'label' => $discountResult['label'] ?? __('Group Discount', 'yatra'),
520 'code' => $discountResult['code'] ?? null,
521 ];
522 }
523 } catch (\Exception $e) {
524
525 }
526 }
527
528 // Fall back to filter (Pro plugins can hook here)
529 return (array) apply_filters('yatra_calculate_group_discount', $group_discount_data, [
530 'subtotal' => $subtotal,
531 'travelers_count' => $travelers_count,
532 'traveler_counts' => $traveler_counts,
533 'pricing_type' => $pricing_type,
534 'price_types' => $price_types,
535 'trip_id' => $trip_id,
536 ]);
537 }
538
539 /**
540 * Calculate taxes
541 */
542 private function calculateTaxes(float $subtotal): array
543 {
544 $enable_tax = SettingsService::get('enable_tax', false);
545 $tax_rate = (float) SettingsService::get('tax_rate', 0);
546 $tax_label = SettingsService::get('tax_label', 'Tax');
547 $tax_inclusive = (bool) SettingsService::get('tax_inclusive', false);
548 $multiple_taxes_enabled = (bool) SettingsService::get('multiple_taxes_enabled', false);
549 $multiple_taxes = SettingsService::get('multiple_taxes', []);
550
551 if (!is_array($multiple_taxes)) {
552 $multiple_taxes = [];
553 }
554
555 // If tax is enabled and taxes are configured, honor them even if the boolean
556 // "multiple_taxes_enabled" setting wasn't toggled in the UI (common case).
557 if ($enable_tax && !empty($multiple_taxes)) {
558 $multiple_taxes_enabled = true;
559 } elseif (count($multiple_taxes) > 1) {
560 $multiple_taxes_enabled = true;
561 }
562
563 $tax_breakdown = [];
564 $total_tax_amount = 0.0;
565
566 if ($multiple_taxes_enabled && !empty($multiple_taxes)) {
567 foreach ($multiple_taxes as $tax) {
568 $rate = isset($tax['rate']) ? (float) $tax['rate'] : 0;
569 $name = $tax['name'] ?? 'Tax';
570
571 if ($rate > 0) {
572 $amount = $tax_inclusive
573 ? $subtotal * ($rate / (100 + $rate))
574 : $subtotal * ($rate / 100);
575 $amount = round($amount, 2);
576
577 $tax_breakdown[] = ['name' => $name, 'rate' => $rate, 'amount' => $amount];
578 $total_tax_amount += $amount;
579 }
580 }
581 } elseif ($enable_tax && $tax_rate > 0) {
582 $amount = $tax_inclusive
583 ? $subtotal * ($tax_rate / (100 + $tax_rate))
584 : $subtotal * ($tax_rate / 100);
585 $amount = round($amount, 2);
586
587 $tax_breakdown[] = ['name' => $tax_label, 'rate' => $tax_rate, 'amount' => $amount];
588 $total_tax_amount = $amount;
589 }
590
591 return [
592 'enable_tax' => $enable_tax,
593 'tax_rate' => $tax_rate,
594 'tax_label' => $tax_label,
595 'tax_inclusive' => $tax_inclusive,
596 'multiple_taxes_enabled' => $multiple_taxes_enabled,
597 'multiple_taxes' => $multiple_taxes,
598 'tax_breakdown' => $tax_breakdown,
599 'total_tax_amount' => $total_tax_amount,
600 'subtotal' => $subtotal,
601 'total_with_tax' => $tax_inclusive ? $subtotal : ($subtotal + $total_tax_amount),
602 ];
603 }
604
605 /**
606 * Calculate payment amounts
607 *
608 * Free: full payment only
609 * Pro FlexiblePayments: deposit/partial via yatra_calculate_amount_due filter
610 */
611 private function calculatePaymentAmounts(float $final_total, string $payment_method, array $context): array
612 {
613 $amount_due = $final_total;
614 $amount_paid = 0.0;
615
616 // Apply Pro FlexiblePayments filter for deposit/partial
617 $amount_due = (float) apply_filters('yatra_calculate_amount_due', $amount_due, $final_total, $payment_method);
618
619 // Fallback: if no Pro module handled it, use basic logic (never use === on floats)
620 $flexible_enabled = apply_filters('yatra_flexible_payments_enabled', false);
621 $unchanged = abs($amount_due - $final_total) < 0.000001;
622 if ($unchanged && $payment_method !== 'full' && $flexible_enabled) {
623 $deposit_percentage = (int) apply_filters('yatra_deposit_percentage', 20);
624 $partial_percentage = (int) apply_filters('yatra_partial_payment_percentage', 30);
625 if ($payment_method === 'deposit') {
626 $amount_due = round($final_total * ($deposit_percentage / 100), 2);
627 } elseif ($payment_method === 'partial') {
628 $amount_due = round($final_total * ($partial_percentage / 100), 2);
629 }
630 }
631
632 $payment_data = (array) apply_filters('yatra_calculate_payment_amounts', [
633 'amount_due' => $amount_due,
634 'amount_paid' => $amount_paid,
635 'payment_method' => $payment_method,
636 'final_total' => $final_total,
637 ], $context);
638
639 return [
640 'amount_due' => (float) ($payment_data['amount_due'] ?? $amount_due),
641 'amount_paid' => (float) ($payment_data['amount_paid'] ?? $amount_paid),
642 ];
643 }
644
645 /**
646 * Calculate pricing from session data
647 *
648 * Used by booking-content.php template for checkout summary
649 */
650 public function calculateFromSession(array $session_data, string $coupon_code = '', string $payment_method = 'full'): array
651 {
652 $trip_id = (int) ($session_data['trip_id'] ?? 0);
653
654 if (empty($trip_id)) {
655 throw new \InvalidArgumentException('Trip ID is required in session data');
656 }
657
658 $payment_method = strtolower(trim($payment_method));
659 if ($payment_method === '') {
660 $payment_method = 'full';
661 }
662
663 $params = [
664 'trip_id' => $trip_id,
665 'travelers_count' => (int) ($session_data['travelers'] ?? 1),
666 'traveler_counts' => $session_data['traveler_counts'] ?? [],
667 'travel_date' => $session_data['travel_date'] ?? '',
668 'departure_time' => $session_data['departure_time'] ?? '',
669 'coupon_code' => $coupon_code,
670 'payment_method' => $payment_method,
671 'selected_services' => $session_data['additional_services'] ?? [],
672 'availability_id' => !empty($session_data['availability_id']) ? (int) $session_data['availability_id'] : null,
673 ];
674
675 $params = (array) apply_filters('yatra_session_calculation_params', $params, $session_data);
676
677 return $this->calculatePricing($params);
678 }
679
680 /**
681 * Quick pricing calculation for simple cases
682 */
683 public function quickCalculate(int $trip_id, int $travelers_count, string $travel_date = ''): array
684 {
685 return $this->calculatePricing([
686 'trip_id' => $trip_id,
687 'travelers_count' => $travelers_count,
688 'travel_date' => $travel_date,
689 'payment_method' => 'full',
690 ]);
691 }
692 }
693