PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 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 All 84 releases
← All changes | app/Services/CalculationService.php +344 -63 3.0.3 → 3.0.16 View file →
@@ -114,9 +114,16 @@
114 114 // Priority: discounted_price → sale_price → original_price
115 115 $original_price = (float) ($trip->original_price ?? 0);
116 116 $discounted_price = $this->resolveDiscountedPrice($trip, $availability);
117 117 $unit_price = $discounted_price > 0 ? $discounted_price : $original_price;
118 -
118 +
119 + // Snapshot the pre-DP per-unit price so the pricing-summary template
120 + // can render the dynamic-pricing impact as its own line item rather
121 + // than silently absorbing it into the Gross Total. Without this, an
122 + // admin/customer looking at the summary cannot tell what the trip
123 + // would have cost without DP rules.
124 + $unit_price_before_dp = $unit_price;
125 +
119 126 // Apply Dynamic Pricing filter (Pro DynamicPricingModule hooks here)
120 127 $unit_price = (float) apply_filters('yatra_booking_trip_price', $unit_price, $trip_id, [
121 128 'departure_date' => $travel_date,
122 129 'spots_remaining' => $spots_for_dp,
@@ -190,11 +197,71 @@
190 197
191 198 // ── Total discounts ─────────────────────────────────────────────
192 199 $total_discount_amount = ($group_discount_data['amount'] ?? 0) + ($coupon_discount_data['calculated_amount'] ?? 0);
193 200 $total_discount_amount = min($total_discount_amount, $subtotal); // discount cannot exceed subtotal
194 -
201 +
195 202 $discounted_subtotal = max(0, $subtotal - $total_discount_amount);
196 -
203 +
204 + // ── Discount stacking hook ───────────────────────────────────────
205 + // Premium-only enforcement point. The Pro Advanced Discount module
206 + // listens here when paired with Dynamic Pricing and rewrites the
207 + // discount/DP combination according to the operator's stacking
208 + // mode. Free sites (and sites missing either module) receive no
209 + // listener and the snapshot passes through unchanged — pricing
210 + // stays bit-for-bit identical to the pre-feature behavior.
211 + //
212 + // `pre_dp_base_amount` is the base before DP fires — works for
213 + // BOTH regular pricing (where DP modifies $unit_price) and
214 + // traveler-based pricing (where DP fires per-category inside
215 + // calculateBaseAmount and would otherwise be invisible to a
216 + // post-hoc listener doing `$unit_price < $unit_price_before_dp`).
217 + // Cheap to compute (no DB hits, just arithmetic) so we always
218 + // pay the cost — far cheaper than asking listeners to recompute.
219 + $pre_dp_base_amount = $this->computeBaseAmountWithoutDynamicPricing(
220 + $unit_price_before_dp,
221 + $travelers_count,
222 + $traveler_counts,
223 + $pricing_type,
224 + $price_types
225 + );
226 + $stackingSnapshot = (array) apply_filters('yatra_pricing_after_discount_stack', [
227 + 'unit_price' => $unit_price,
228 + 'unit_price_before_dp' => $unit_price_before_dp,
229 + 'base_amount' => $base_amount,
230 + 'pre_dp_base_amount' => $pre_dp_base_amount,
231 + 'subtotal' => $subtotal,
232 + 'group_discount_data' => $group_discount_data,
233 + 'coupon_discount_data' => $coupon_discount_data,
234 + 'total_discount_amount' => $total_discount_amount,
235 + 'discounted_subtotal' => $discounted_subtotal,
236 + ], [
237 + 'trip_id' => $trip_id,
238 + 'travelers_count' => $travelers_count,
239 + 'traveler_counts' => $traveler_counts,
240 + 'pricing_type' => $pricing_type,
241 + 'price_types' => $price_types,
242 + 'travel_date' => $travel_date,
243 + 'spots_for_dp' => $spots_for_dp,
244 + 'availability_id_for_dp' => $availability_id_for_dp,
245 + 'coupon_code' => $coupon_code,
246 + 'selected_services' => $selected_services,
247 + 'original_price' => $original_price,
248 + 'discounted_price' => $discounted_price,
249 + 'calculation_service' => $this,
250 + 'discount_service' => $discountService,
251 + ]);
252 + if (isset($stackingSnapshot['unit_price'])) { $unit_price = (float) $stackingSnapshot['unit_price']; }
253 + if (isset($stackingSnapshot['base_amount'])) { $base_amount = (float) $stackingSnapshot['base_amount']; }
254 + if (isset($stackingSnapshot['subtotal'])) { $subtotal = (float) $stackingSnapshot['subtotal']; }
255 + if (isset($stackingSnapshot['group_discount_data']) && is_array($stackingSnapshot['group_discount_data'])) {
256 + $group_discount_data = $stackingSnapshot['group_discount_data'];
257 + }
258 + if (isset($stackingSnapshot['coupon_discount_data']) && is_array($stackingSnapshot['coupon_discount_data'])) {
259 + $coupon_discount_data = $stackingSnapshot['coupon_discount_data'];
260 + }
261 + if (isset($stackingSnapshot['total_discount_amount'])) { $total_discount_amount = (float) $stackingSnapshot['total_discount_amount']; }
262 + if (isset($stackingSnapshot['discounted_subtotal'])) { $discounted_subtotal = (float) $stackingSnapshot['discounted_subtotal']; }
263 +
197 264 // ── Itinerary Costs ─────────────────────────────────────────
198 265 $itinerary_costs = apply_filters('yatra_booking_itinerary_costs', [], $trip_id, $travelers_count, $traveler_counts, $travel_date);
199 266 $itinerary_costs_total = 0;
200 267 if (!empty($itinerary_costs) && is_array($itinerary_costs)) {
@@ -203,13 +270,96 @@
203 270 }
204 271 }
205 272
206 273 // ── Additional Services (if any) ─────────────────────────────
207 - $additional_services_total = 0;
208 - // Note: Additional services can be added here via filters
274 + // Pull the available services for this trip via the Pro filter and
275 + // mark which ones are selected (or required/included). Pricing data
276 + // ships back to the template scope so the Pricing Summary partial's
277 + // selected-services loop has something to render — previously
278 + // `pricing_calculation['additional_services']` was never populated
279 + // here, so the per-row services list in the sidebar always rendered
280 + // empty even when the standalone Additional Services card showed
281 + // ticked checkboxes.
282 + $available_services = (array) apply_filters(
283 + 'yatra_booking_additional_services',
284 + [],
285 + $trip_id,
286 + $travelers_count,
287 + $traveler_counts,
288 + $travel_date
289 + );
290 + $selected_service_ids = array_map('intval', (array) $selected_services);
291 + $duration_days_for_services = (int) ($trip->duration_days ?? 1);
292 + $additional_services_total = 0.0;
293 + $additional_services_resolved = [];
294 + foreach ($available_services as $svc) {
295 + $svc = (array) $svc;
296 + $svc_id = (int) ($svc['id'] ?? 0);
297 + $is_required = !empty($svc['is_required']);
298 + $is_included = !empty($svc['is_included']);
299 + $is_selected = $is_required || $is_included || in_array($svc_id, $selected_service_ids, true);
300 +
301 + $base_price = (float) ($svc['price'] ?? 0);
302 + $price_per = $svc['price_per'] ?? 'person';
303 + // This MUST stay identical to the Pro charge path
304 + // (AdditionalServicesBookingHooks::calculateServicePrice) so the
305 + // displayed line-item equals the amount folded into the subtotal.
306 + if (($svc['price_type'] ?? 'fixed') === 'percentage') {
307 + // Percentage = % of the WHOLE trip base price. `$base_amount`
308 + // already accounts for travelers/categories, so Price Per is NOT
309 + // applied (it would double-count). Matches the "Percentage of
310 + // Trip Price" label and the free booking-services fallback.
311 + $unit_price = round(($base_price / 100) * $base_amount, 2);
312 + $calculated_price = $unit_price;
313 + } else {
314 + // Fixed: Price Per multiplies the entered flat amount.
315 + $unit_price = $base_price;
316 + switch ($price_per) {
317 + case 'person':
318 + $calculated_price = round($unit_price * max(1, $travelers_count), 2);
319 + break;
320 + case 'day':
321 + $calculated_price = round($unit_price * max(1, $duration_days_for_services), 2);
322 + break;
323 + case 'booking':
324 + default:
325 + $calculated_price = round($unit_price, 2);
326 + break;
327 + }
328 + }
329 +
330 + $svc['selected'] = $is_selected;
331 + $svc['unit_price'] = $unit_price;
332 + $svc['calculated_price'] = $calculated_price;
333 + $additional_services_resolved[] = $svc;
334 +
335 + // Only paid (non-included) selected services contribute to the
336 + // services subtotal. The taxable-amount line below will then
337 + // include this naturally.
338 + if ($is_selected && !$is_included) {
339 + $additional_services_total += $calculated_price;
340 + }
341 + }
342 + // Let Pro modules override the total (rounding, group rules, etc.).
343 + $additional_services_total = (float) apply_filters(
344 + 'yatra_booking_services_total',
345 + $additional_services_total,
346 + $additional_services_resolved,
347 + $trip_id,
348 + $travelers_count,
349 + $duration_days_for_services
350 + );
209 351
210 - // ── Taxable Amount (includes itinerary costs and services) ────
211 - $taxable_amount = $discounted_subtotal + $itinerary_costs_total + $additional_services_total;
352 + // ── Taxable Amount ────────────────────────────────────────────
353 + // We DON'T add `$additional_services_total` again here. The Pro
354 + // AdditionalServicesModule hooks into `yatra_calculate_subtotal`
355 + // (above), which already folded selected services into `$subtotal`
356 + // → `$discounted_subtotal`. Adding them a second time produced the
357 + // visible double-count bug ($159 + $112 services = $271 subtotal,
358 + // then $271 + $112 services = $383 net amount). `$additional_services_total`
359 + // stays available in the result payload so the sidebar can render
360 + // each service as a row for transparency.
361 + $taxable_amount = $discounted_subtotal + $itinerary_costs_total;
212 362
213 363 // ── Taxes ───────────────────────────────────────────────────────
214 364 $tax_calculation = $this->calculateTaxes($taxable_amount);
215 365
@@ -230,8 +380,11 @@
230 380 // ── Payment amounts (FlexiblePayments Pro module) ───────────────
231 381 $payment_amounts = $this->calculatePaymentAmounts($final_total, $payment_method, [
232 382 'trip_id' => $trip_id,
233 383 'travelers_count' => $travelers_count,
384 + // Tour start → Pro can force full payment when the tour is within
385 + // the balance-due window (tour-anchored scheduled payments).
386 + 'travel_date' => $travel_date,
234 387 ]);
235 388
236 389 // ── Currency ────────────────────────────────────────────────────
237 390 $currency = SettingsService::getCurrency();
@@ -238,8 +391,92 @@
238 391
239 392 // ── Gross total (subtotal before discounts/taxes, includes services) ───
240 393 $gross_total = $subtotal;
241 394
395 + // ── Dynamic-pricing breakdown (for the pricing-summary template) ─
396 + // The DP module already has an `addPricingBreakdown` callback wired to
397 + // the `yatra_price_breakdown` filter — but that filter was previously
398 + // never fired anywhere, so the template's `$dynamic_pricing` block was
399 + // dead code. We fire it here with the same context the per-unit DP
400 + // filter received, so Pro DP can populate the breakdown row that the
401 + // template renders.
402 + //
403 + // dp_total_adjustment is the signed dollar impact DP has on the trip
404 + // subtotal:
405 + // - For regular pricing: per-unit delta × travelers (the line 120
406 + // filter is the single DP entry point).
407 + // - For traveler_based pricing: DP is applied per-category inside
408 + // calculateBaseAmount, so we recompute a "pristine" base amount
409 + // using the same TripPricingService::resolveCategoryEffectivePrice
410 + // anchor that calculateBaseAmount starts from, and subtract from
411 + // the real (post-DP) base_amount.
412 + // If the discount-stacking filter reverted DP (the Pro
413 + // "discount_only" or "best_for_customer→discount" path
414 + // mutated $base_amount back to its pre-DP value), the
415 + // breakdown should reflect that — otherwise the per-category
416 + // line items would still display DP-adjusted prices that
417 + // don't match the final total customers actually pay.
418 + //
419 + // Free-only sites (no filter listener) hit the else branch
420 + // exactly as before: $base_amount tracks the post-DP value,
421 + // $dp_was_suppressed stays false, and the existing breakdown
422 + // math runs unchanged. So the pre-feature display contract
423 + // is bit-for-bit preserved.
424 + $dp_was_suppressed = $pre_dp_base_amount > 0
425 + && abs($base_amount - $pre_dp_base_amount) < 0.005;
426 + $dp_per_unit_delta = $dp_was_suppressed
427 + ? 0.0
428 + : $unit_price - $unit_price_before_dp;
429 + $dp_total_adjustment = 0.0;
430 + $category_prices_post_dp = [];
431 + if ($pricing_type === 'regular') {
432 + $dp_total_adjustment = $dp_per_unit_delta * max(1, $travelers_count);
433 + } elseif ($pricing_type === 'traveler_based' && !empty($price_types)) {
434 + $pre_dp_base = 0.0;
435 + foreach ($price_types as $pt) {
436 + $pt_arr = (array) $pt;
437 + $category_id = $pt_arr['category_id'] ?? 0;
438 + $pricing_mode = $pt_arr['pricing_mode'] ?? 'per_person';
439 + $pre_dp_price = (float) \Yatra\Services\TripPricingService::resolveCategoryEffectivePrice($pt_arr);
440 + $count = isset($traveler_counts[$category_id]) ? (int) $traveler_counts[$category_id] : 0;
441 +
442 + // Capture the DP-adjusted per-category price so the pricing-summary
443 + // category row can show the price the customer is actually paying.
444 + // When DP was suppressed by the stacking enforcer, skip the DP
445 + // filter entirely and surface the pristine pre-DP price so the
446 + // breakdown matches the final total.
447 + if ($dp_was_suppressed) {
448 + $post_dp_price = $pre_dp_price;
449 + } else {
450 + $post_dp_price = (float) apply_filters('yatra_booking_trip_price', $pre_dp_price, $trip_id, [
451 + 'departure_date' => $travel_date,
452 + 'spots_remaining' => $spots_for_dp,
453 + 'availability_id' => $availability_id_for_dp,
454 + 'category_id' => $category_id,
455 + 'original_price' => (float) ($pt_arr['original_price'] ?? 0),
456 + 'discounted_price' => (float) ($pt_arr['discounted_price'] ?? $pt_arr['sale_price'] ?? 0),
457 + ]);
458 + }
459 + if ($category_id) {
460 + $category_prices_post_dp[(string) $category_id] = $post_dp_price;
461 + }
462 +
463 + $pre_dp_base += TripPricingService::categoryLineSubtotal($pt_arr, $count, $pre_dp_price);
464 + }
465 + $dp_total_adjustment = $dp_was_suppressed ? 0.0 : ($base_amount - $pre_dp_base);
466 + }
467 + $price_breakdown = (array) apply_filters('yatra_price_breakdown', [], $trip_id, [
468 + 'price' => $unit_price,
469 + 'original_price' => $original_price,
470 + 'discounted_price' => $discounted_price,
471 + 'departure_date' => $travel_date,
472 + 'spots_remaining' => $spots_for_dp,
473 + 'availability_id' => $availability_id_for_dp,
474 + 'travelers_count' => $travelers_count,
475 + 'gross_total' => $gross_total,
476 + ]);
477 + $dynamic_pricing_breakdown = $price_breakdown['dynamic_pricing'] ?? null;
478 +
242 479 // ── Build result ────────────────────────────────────────────
243 480 $pricing_data = [
244 481 // Price info
245 482 'original_price' => $original_price,
@@ -244,10 +481,15 @@
244 481 // Price info
245 482 'original_price' => $original_price,
246 483 'discounted_price' => $discounted_price,
247 484 'unit_price' => $unit_price,
485 + 'unit_price_before_dp' => $unit_price_before_dp,
486 + 'dp_per_unit_delta' => $dp_per_unit_delta,
487 + 'dp_total_adjustment' => $dp_total_adjustment,
488 + 'category_prices_post_dp' => $category_prices_post_dp,
489 + 'dynamic_pricing' => $dynamic_pricing_breakdown,
248 490 'pricing_type' => $pricing_type,
249 -
491 +
250 492 // Base amounts
251 493 'base_amount' => $base_amount,
252 494 'subtotal' => $subtotal,
253 495 'discounted_subtotal' => $discounted_subtotal,
@@ -271,8 +513,14 @@
271 513 'payment_method' => $payment_method,
272 514 'currency' => $currency,
273 515
274 516 // Itinerary costs
517 + // Additional services with `selected` / `calculated_price` flags
518 + // — Checkout::getAdditionalServices() reads this; the sidebar's
519 + // selected-services loop renders one row per ticked service.
520 + 'additional_services' => $additional_services_resolved,
521 + 'services_total' => $additional_services_total,
522 +
275 523 'itinerary_costs' => $itinerary_costs,
276 524 'itinerary_costs_total' => $itinerary_costs_total,
277 525
278 526 // Metadata
@@ -367,37 +615,17 @@
367 615 if (empty($types)) {
368 616 return [];
369 617 }
370 618
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 -
619 + // Resolve pricing_mode / group-size limits authoritatively from the
620 + // TravelerCategory. This must OVERRIDE (not just fill-when-empty): the
621 + // price_types coming from a stored availability row or session can carry
622 + // a literal 'per_person' placeholder that an empty() check would skip,
623 + // which silently charged a per-group category by headcount. For
624 + // per-person categories this resolves back to 'per_person' (a no-op), so
625 + // the booking total for every existing trip is unchanged.
626 + $types = TripPricingService::applyCategoryPricingMeta($types);
627 +
400 628 return $types;
401 629 }
402 630
403 631 /**
@@ -430,13 +658,17 @@
430 658 }
431 659 }
432 660 }
433 661
434 - // Fallback: lookup by trip + date + time (time-aware for day tours)
662 + // Fallback: resolve through centralized resolver so rule-generated slots
663 + // (virtual, no numeric availability_id) use the exact same data shape as the UI.
435 664 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);
665 + $resolver = new \Yatra\Services\AvailabilityResolutionService();
666 + return $resolver->resolveAvailabilityForDate(
667 + $trip_id,
668 + $travel_date,
669 + $departure_time !== '' ? $departure_time : null
670 + );
439 671 }
440 672 } catch (\Exception $e) {
441 673 // Availability lookup failed, continue with trip-level pricing
442 674 }
@@ -449,13 +681,62 @@
449 681 *
450 682 * For traveler-based: uses per-category effective prices × counts
451 683 * For regular: uses unit_price × travelers_count
452 684 */
453 - private function calculateBaseAmount(
685 + /**
686 + * Compute the base amount for a trip WITHOUT firing the Dynamic
687 + * Pricing filter, regardless of pricing type.
688 + *
689 + * Used by the Advanced Discount stacking enforcer to detect whether
690 + * DP actually adjusted the booking (compare actual base_amount to
691 + * the no-DP base_amount) and to compute the "discount-only" alt
692 + * scenario where DP must be fully neutralized.
693 + *
694 + * For regular pricing this is just unit_price × travelers_count.
695 + * For traveler-based pricing it walks the per-category prices via
696 + * the canonical TripPricingService anchor (identical math to the
697 + * breakdown's `$pre_dp_base` at lines ~384-415 of calculatePricing).
698 + * Per-category prices come from saved trip data — DP filter is
699 + * deliberately NOT applied.
700 + */
701 + public function computeBaseAmountWithoutDynamicPricing(
454 702 float $unit_price,
455 703 int $travelers_count,
456 704 array $traveler_counts,
457 705 string $pricing_type,
706 + array $price_types
707 + ): float {
708 + if ($pricing_type === 'traveler_based' && !empty($price_types)) {
709 + $base_amount = 0.0;
710 + foreach ($price_types as $pt) {
711 + $pt = (array) $pt;
712 + $category_id = $pt['category_id'] ?? 0;
713 + $pricing_mode = $pt['pricing_mode'] ?? 'per_person';
714 + $category_price = (float) TripPricingService::resolveCategoryEffectivePrice($pt);
715 + $count = isset($traveler_counts[$category_id]) ? (int) $traveler_counts[$category_id] : 0;
716 +
717 + $base_amount += TripPricingService::categoryLineSubtotal($pt, $count, $category_price);
718 + }
719 + return round($base_amount, 2);
720 + }
721 +
722 + return round($unit_price * max(1, $travelers_count), 2);
723 + }
724 +
725 + /**
726 + * Compute the base amount for a trip from a given per-unit price.
727 + *
728 + * Public so Pro extensions (e.g. the Advanced Discount stacking-
729 + * enforcer) can re-derive the base when they need to recompute the
730 + * downstream pipeline with a different unit price — for example,
731 + * the "discount_only" stacking mode reverts the DP adjustment and
732 + * has to recompute base/subtotal/discounts against the pre-DP price.
733 + */
734 + public function calculateBaseAmount(
735 + float $unit_price,
736 + int $travelers_count,
737 + array $traveler_counts,
738 + string $pricing_type,
458 739 array $price_types,
459 740 int $trip_id,
460 741 string $travel_date = '',
461 742 ?int $spots_remaining = null,
@@ -482,22 +763,16 @@
482 763 ]);
483 764
484 765 $count = isset($traveler_counts[$category_id]) ? (int) $traveler_counts[$category_id] : 0;
485 766
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 - }
767 + // Single source of truth for the per-category line amount
768 + // (per-person × count, flat per-group, or per-block group pricing).
769 + $base_amount += TripPricingService::categoryLineSubtotal($pt, $count, $category_price);
495 770 }
496 -
771 +
497 772 return round($base_amount, 2);
498 773 }
499 -
774 +
500 775 // Regular pricing
501 776 return round($unit_price * $travelers_count, 2);
502 777 }
503 778
@@ -503,9 +778,9 @@
503 778
504 779 /**
505 780 * Calculate group discount
506 781 */
507 - private function calculateGroupDiscount(
782 + public function calculateGroupDiscount(
508 783 int $trip_id,
509 784 float $subtotal,
510 785 int $travelers_count,
511 786 array $traveler_counts,
@@ -627,26 +902,32 @@
627 902 }
628 903
629 904 /**
630 905 * Calculate payment amounts
631 - *
632 - * Free: full payment only
633 - * Pro FlexiblePayments: deposit/partial via yatra_calculate_amount_due filter
906 + *
907 + * Free: full payment only.
908 + * Pro FlexiblePayments: deposit/partial via `yatra_calculate_amount_due`.
909 + *
910 + * $context carries `trip_id` (and may carry more later). It is forwarded
911 + * to every payment-related filter so Pro can apply per-trip overrides
912 + * (e.g. trip.deposit_amount, trip.deposit_percentage) instead of only
913 + * the site-wide settings.
634 914 */
635 915 private function calculatePaymentAmounts(float $final_total, string $payment_method, array $context): array
636 916 {
637 917 $amount_due = $final_total;
638 918 $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 -
919 +
920 + // Apply Pro FlexiblePayments filter for deposit/partial. $context lets
921 + // Pro look up the trip and honour per-trip overrides.
922 + $amount_due = (float) apply_filters('yatra_calculate_amount_due', $amount_due, $final_total, $payment_method, $context);
923 +
643 924 // Fallback: if no Pro module handled it, use basic logic (never use === on floats)
644 925 $flexible_enabled = apply_filters('yatra_flexible_payments_enabled', false);
645 926 $unchanged = abs($amount_due - $final_total) < 0.000001;
646 927 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);
928 + $deposit_percentage = (int) apply_filters('yatra_deposit_percentage', 20, $context);
929 + $partial_percentage = (int) apply_filters('yatra_partial_payment_percentage', 30, $context);
649 930 if ($payment_method === 'deposit') {
650 931 $amount_due = round($final_total * ($deposit_percentage / 100), 2);
651 932 } elseif ($payment_method === 'partial') {
652 933 $amount_due = round($final_total * ($partial_percentage / 100), 2);