| @@ -299,22 +299,37 @@ | ||
| 299 | 299 | $is_selected = $is_required || $is_included || in_array($svc_id, $selected_service_ids, true); |
| 300 | 300 | |
| 301 | 301 | $base_price = (float) ($svc['price'] ?? 0); |
| 302 | 302 | $price_per = $svc['price_per'] ?? 'person'; |
| 303 | - switch ($price_per) { | |
| 304 | - case 'person': | |
| 305 | - $calculated_price = $base_price * max(1, $travelers_count); | |
| 306 | - break; | |
| 307 | - case 'day': | |
| 308 | - $calculated_price = $base_price * max(1, $duration_days_for_services); | |
| 309 | - break; | |
| 310 | - case 'booking': | |
| 311 | - default: | |
| 312 | - $calculated_price = $base_price; | |
| 313 | - break; | |
| 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 | + } | |
| 314 | 328 | } |
| 315 | 329 | |
| 316 | 330 | $svc['selected'] = $is_selected; |
| 331 | + $svc['unit_price'] = $unit_price; | |
| 317 | 332 | $svc['calculated_price'] = $calculated_price; |
| 318 | 333 | $additional_services_resolved[] = $svc; |
| 319 | 334 | |
| 320 | 335 | // Only paid (non-included) selected services contribute to the |
| @@ -365,8 +380,11 @@ | ||
| 365 | 380 | // ── Payment amounts (FlexiblePayments Pro module) ─────────────── |
| 366 | 381 | $payment_amounts = $this->calculatePaymentAmounts($final_total, $payment_method, [ |
| 367 | 382 | 'trip_id' => $trip_id, |
| 368 | 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, | |
| 369 | 387 | ]); |
| 370 | 388 | |
| 371 | 389 | // ── Currency ──────────────────────────────────────────────────── |
| 372 | 390 | $currency = SettingsService::getCurrency(); |
| @@ -441,15 +459,9 @@ | ||
| 441 | 459 | if ($category_id) { |
| 442 | 460 | $category_prices_post_dp[(string) $category_id] = $post_dp_price; |
| 443 | 461 | } |
| 444 | 462 | |
| 445 | - if ($pricing_mode === 'per_group') { | |
| 446 | - if ($count > 0) { | |
| 447 | - $pre_dp_base += $pre_dp_price; | |
| 448 | - } | |
| 449 | - } else { | |
| 450 | - $pre_dp_base += $pre_dp_price * $count; | |
| 451 | - } | |
| 463 | + $pre_dp_base += TripPricingService::categoryLineSubtotal($pt_arr, $count, $pre_dp_price); | |
| 452 | 464 | } |
| 453 | 465 | $dp_total_adjustment = $dp_was_suppressed ? 0.0 : ($base_amount - $pre_dp_base); |
| 454 | 466 | } |
| 455 | 467 | $price_breakdown = (array) apply_filters('yatra_price_breakdown', [], $trip_id, [ |
| @@ -603,37 +615,17 @@ | ||
| 603 | 615 | if (empty($types)) { |
| 604 | 616 | return []; |
| 605 | 617 | } |
| 606 | 618 | |
| 607 | - // Enrich with pricing_mode from category metadata if missing | |
| 608 | - $needs_enrichment = false; | |
| 609 | - foreach ($types as $pt) { | |
| 610 | - $pt = (array) $pt; | |
| 611 | - if (empty($pt['pricing_mode'])) { | |
| 612 | - $needs_enrichment = true; | |
| 613 | - break; | |
| 614 | - } | |
| 615 | - } | |
| 616 | - | |
| 617 | - if ($needs_enrichment) { | |
| 618 | - $category_ids = array_filter(array_map(function($pt) { | |
| 619 | - $pt = (array) $pt; | |
| 620 | - return isset($pt['category_id']) ? (int) $pt['category_id'] : null; | |
| 621 | - }, $types)); | |
| 622 | - | |
| 623 | - if (!empty($category_ids)) { | |
| 624 | - $category_meta = $this->getCategoryMetadata($category_ids); | |
| 625 | - foreach ($types as &$pt) { | |
| 626 | - if (is_object($pt)) $pt = (array) $pt; | |
| 627 | - $cat_id = isset($pt['category_id']) ? (int) $pt['category_id'] : null; | |
| 628 | - if ($cat_id && isset($category_meta[$cat_id]) && empty($pt['pricing_mode'])) { | |
| 629 | - $pt['pricing_mode'] = $category_meta[$cat_id]['pricing_mode'] ?? 'per_person'; | |
| 630 | - } | |
| 631 | - } | |
| 632 | - unset($pt); | |
| 633 | - } | |
| 634 | - } | |
| 635 | - | |
| 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 | + | |
| 636 | 628 | return $types; |
| 637 | 629 | } |
| 638 | 630 | |
| 639 | 631 | /** |
| @@ -721,15 +713,9 @@ | ||
| 721 | 713 | $pricing_mode = $pt['pricing_mode'] ?? 'per_person'; |
| 722 | 714 | $category_price = (float) TripPricingService::resolveCategoryEffectivePrice($pt); |
| 723 | 715 | $count = isset($traveler_counts[$category_id]) ? (int) $traveler_counts[$category_id] : 0; |
| 724 | 716 | |
| 725 | - if ($pricing_mode === 'per_group') { | |
| 726 | - if ($count > 0) { | |
| 727 | - $base_amount += $category_price; | |
| 728 | - } | |
| 729 | - } else { | |
| 730 | - $base_amount += $category_price * $count; | |
| 731 | - } | |
| 717 | + $base_amount += TripPricingService::categoryLineSubtotal($pt, $count, $category_price); | |
| 732 | 718 | } |
| 733 | 719 | return round($base_amount, 2); |
| 734 | 720 | } |
| 735 | 721 | |
| @@ -777,22 +763,16 @@ | ||
| 777 | 763 | ]); |
| 778 | 764 | |
| 779 | 765 | $count = isset($traveler_counts[$category_id]) ? (int) $traveler_counts[$category_id] : 0; |
| 780 | 766 | |
| 781 | - if ($pricing_mode === 'per_group') { | |
| 782 | - // Per group: charge flat price once if any travelers in this category | |
| 783 | - if ($count > 0) { | |
| 784 | - $base_amount += $category_price; | |
| 785 | - } | |
| 786 | - } else { | |
| 787 | - // Per person: charge per traveler | |
| 788 | - $base_amount += $category_price * $count; | |
| 789 | - } | |
| 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); | |
| 790 | 770 | } |
| 791 | - | |
| 771 | + | |
| 792 | 772 | return round($base_amount, 2); |
| 793 | 773 | } |
| 794 | - | |
| 774 | + | |
| 795 | 775 | // Regular pricing |
| 796 | 776 | return round($unit_price * $travelers_count, 2); |
| 797 | 777 | } |
| 798 | 778 | |