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/TripPricingService.php +120 -0 3.0.14.2 → 3.0.16 View file →
@@ -65,8 +65,13 @@
65 65 'price_prefix' => '',
66 66 'pricing_type' => $pricing_type,
67 67 'price_types' => $price_types,
68 68 'has_traveler_pricing' => $has_traveler_pricing,
69 + // How the displayed amount is charged: 'per_person' or 'per_group'
70 + // (+ the pax range), taken from the SAME category the amount comes
71 + // from, so the label can never contradict the price.
72 + 'price_unit' => 'per_person',
73 + 'price_unit_label' => self::priceUnitLabel(null),
69 74 'currency' => SettingsService::getCurrency(),
70 75 ];
71 76
72 77 if ($has_traveler_pricing) {
@@ -74,10 +79,12 @@
74 79 // - If a default category is marked, use that for initial display (listings + single trip page-load).
75 80 // - Otherwise fall back to minimum effective price across categories (current behavior).
76 81 $default_price = 0.0;
77 82 $default_original = 0.0;
83 + $default_pt = null;
78 84 $min_price = PHP_FLOAT_MAX;
79 85 $min_original = 0.0;
86 + $min_pt = null;
80 87 $max_discount = 0;
81 88
82 89 foreach ($price_types as $pt) {
83 90 $pt = (array) $pt;
@@ -86,13 +93,15 @@
86 93
87 94 if (!empty($pt['is_default']) && $default_price <= 0 && $discounted > 0) {
88 95 $default_price = $discounted;
89 96 $default_original = $original;
97 + $default_pt = $pt;
90 98 }
91 99
92 100 if ($discounted > 0 && $discounted < $min_price) {
93 101 $min_price = $discounted;
94 102 $min_original = $original;
103 + $min_pt = $pt;
95 104 }
96 105
97 106 // Track max discount across categories
98 107 if ($original > 0 && $discounted > 0 && $discounted < $original) {
@@ -104,10 +113,14 @@
104 113 }
105 114
106 115 $chosen_price = $default_price > 0 ? $default_price : ($min_price < PHP_FLOAT_MAX ? $min_price : 0.0);
107 116 $chosen_original = $default_price > 0 ? $default_original : $min_original;
117 + $chosen_pt = $default_price > 0 ? $default_pt : $min_pt;
108 118
109 119 if ($chosen_price > 0) {
120 + $unit = self::priceUnit($chosen_pt);
121 + $result['price_unit'] = $unit['unit'];
122 + $result['price_unit_label'] = $unit['label'];
110 123 $result['effective_price_min'] = $chosen_price;
111 124 $result['min_category_original_price'] = $chosen_original;
112 125 $result['max_discount_percentage'] = $max_discount;
113 126 $result['current_price'] = $chosen_price;
@@ -139,8 +152,16 @@
139 152 $avail_min = self::findMinPriceFromAvailability($availabilityDates, $has_traveler_pricing);
140 153 if ($avail_min > 0 && ($result['effective_price_min'] <= 0 || $avail_min < $result['effective_price_min'])) {
141 154 $result['effective_price_min'] = $avail_min;
142 155 $result['current_price'] = $avail_min;
156 + if ($has_traveler_pricing) {
157 + $avail_pt = self::findAvailabilityPriceTypeAt($availabilityDates, $avail_min);
158 + if ($avail_pt !== null) {
159 + $unit = self::priceUnit($avail_pt);
160 + $result['price_unit'] = $unit['unit'];
161 + $result['price_unit_label'] = $unit['label'];
162 + }
163 + }
143 164 }
144 165 } elseif ($has_traveler_pricing) {
145 166 $result['price_prefix'] = __('From ', 'yatra');
146 167 }
@@ -177,8 +198,10 @@
177 198 'has_discount' => false,
178 199 'discount_percentage' => 0,
179 200 'pricing_type' => $pricing_type,
180 201 'price_types' => $pricing_type === 'traveler_based' ? $avail_price_types : [],
202 + 'price_unit' => 'per_person',
203 + 'price_unit_label' => self::priceUnitLabel(null),
181 204 ];
182 205
183 206 if ($pricing_type === 'traveler_based' && !empty($avail_price_types)) {
184 207 // Traveler-based: use first category's price as display
@@ -184,8 +207,11 @@
184 207 // Traveler-based: use first category's price as display
185 208 $first = (array) $avail_price_types[0];
186 209 $result['sale_price'] = self::resolveCategoryEffectivePrice($first);
187 210 $result['original_price'] = (float) ($first['original_price'] ?? $result['sale_price']);
211 + $unit = self::priceUnit($first);
212 + $result['price_unit'] = $unit['unit'];
213 + $result['price_unit_label'] = $unit['label'];
188 214 } elseif (isset($avail->effective_price) && (float) $avail->effective_price > 0) {
189 215 // Regular: use pre-calculated effective price
190 216 $result['sale_price'] = (float) $avail->effective_price;
191 217 $result['original_price'] = isset($avail->original_price) && (float) $avail->original_price > 0
@@ -525,8 +551,102 @@
525 551 );
526 552 }
527 553
528 554 return (array) apply_filters('yatra_resolve_discount_info', $result, $originalPrice, $currentPrice);
555 + }
556 +
557 + /**
558 + * How a category's price is charged, for display next to an amount.
559 + *
560 + * A traveller category is priced 'per_person' (× headcount) or 'per_group'
561 + * (flat for min_pax–max_pax people). The trip's stored price_types JSON does
562 + * not carry the mode, so it is read back from the category classification
563 + * (applyCategoryPricingMeta) exactly as checkout does. Pass the category
564 + * entry whose price is being displayed; null / a regular-pricing trip →
565 + * per person.
566 + *
567 + * @param array|object|null $pt
568 + * @return array{unit:string,label:string,min_pax:?int,max_pax:?int}
569 + */
570 + public static function priceUnit($pt): array
571 + {
572 + $unit = ['unit' => 'per_person', 'label' => '', 'min_pax' => null, 'max_pax' => null];
573 +
574 + if ($pt !== null) {
575 + $pt = (array) $pt;
576 + if (!isset($pt['pricing_mode']) && isset($pt['category_id'])) {
577 + $backfilled = self::applyCategoryPricingMeta([$pt]);
578 + $pt = (array) ($backfilled[0] ?? $pt);
579 + }
580 + if (($pt['pricing_mode'] ?? 'per_person') === 'per_group') {
581 + $unit['unit'] = 'per_group';
582 + $unit['min_pax'] = isset($pt['min_pax']) && $pt['min_pax'] !== '' && $pt['min_pax'] !== null ? (int) $pt['min_pax'] : null;
583 + $unit['max_pax'] = isset($pt['max_pax']) && $pt['max_pax'] !== '' && $pt['max_pax'] !== null ? (int) $pt['max_pax'] : null;
584 + }
585 + }
586 +
587 + if ($unit['unit'] === 'per_group') {
588 + if ($unit['min_pax'] > 0 && $unit['max_pax'] > 0) {
589 + /* translators: 1: minimum pax for the group price, 2: maximum pax. */
590 + $label = sprintf(__('per group (%1$d-%2$d pax)', 'yatra'), $unit['min_pax'], $unit['max_pax']);
591 + } elseif ($unit['max_pax'] > 0) {
592 + /* translators: %d: maximum pax for the group price. */
593 + $label = sprintf(__('per group (up to %d pax)', 'yatra'), $unit['max_pax']);
594 + } elseif ($unit['min_pax'] > 0) {
595 + /* translators: %d: minimum pax for the group price. */
596 + $label = sprintf(__('per group (%d+ pax)', 'yatra'), $unit['min_pax']);
597 + } else {
598 + $label = __('per group', 'yatra');
599 + }
600 + } else {
601 + $label = __('per person', 'yatra');
602 + }
603 +
604 + /**
605 + * Wording of the unit shown next to a price ("per person", "per group (1-2 pax)").
606 + *
607 + * @param string $label Translated label.
608 + * @param string $unit 'per_person' or 'per_group'.
609 + * @param array|null $pt The traveller-category price entry, if any.
610 + */
611 + $unit['label'] = (string) apply_filters('yatra_price_unit_label', $label, $unit['unit'], $pt);
612 +
613 + return $unit;
614 + }
615 +
616 + /**
617 + * Shorthand for priceUnit()['label'].
618 + *
619 + * @param array|object|null $pt
620 + */
621 + public static function priceUnitLabel($pt): string
622 + {
623 + return self::priceUnit($pt)['label'];
624 + }
625 +
626 + /**
627 + * The availability price-type entry whose effective price equals $price
628 + * (the value findMinPriceFromAvailability() picked), so the unit label can
629 + * follow that category. Null when the minimum came from the availability
630 + * row itself rather than a category.
631 + *
632 + * @param array<int, object> $availabilityDates
633 + */
634 + private static function findAvailabilityPriceTypeAt(array $availabilityDates, float $price): ?array
635 + {
636 + foreach ($availabilityDates as $avail) {
637 + if (empty($avail->price_types) || !is_array($avail->price_types)) {
638 + continue;
639 + }
640 + foreach ($avail->price_types as $pt) {
641 + $pt = (array) $pt;
642 + if (abs(self::resolveCategoryEffectivePrice($pt) - $price) < 0.005) {
643 + return $pt;
644 + }
645 + }
646 + }
647 +
648 + return null;
529 649 }
530 650
531 651 /**
532 652 * Find minimum price across availability dates.