| @@ -91,24 +91,55 @@ | ||
| 91 | 91 | { |
| 92 | 92 | if (!$this->isTravelerBased() || empty($this->getPriceTypes())) { |
| 93 | 93 | return []; |
| 94 | 94 | } |
| 95 | - | |
| 95 | + | |
| 96 | 96 | $breakdown = []; |
| 97 | 97 | $priceTypes = $this->getPriceTypes(); |
| 98 | 98 | $travelerCounts = $this->getTravelerCounts(); |
| 99 | - | |
| 99 | + | |
| 100 | + // CalculationService now ships a per-category map of DP-adjusted unit | |
| 101 | + // prices. When present, we prefer it so the category row in the sidebar | |
| 102 | + // shows the price the customer is actually paying (e.g. Adult $157.52) | |
| 103 | + // instead of the pre-DP catalog price plus a separate "Dynamic | |
| 104 | + // Pricing −$21.48" subtraction line. The fallback keeps working for | |
| 105 | + // entry points (e.g. legacy sessions, partial recalcs) that don't | |
| 106 | + // carry this map. | |
| 107 | + $postDpPrices = isset($this->pricingCalculation['category_prices_post_dp']) | |
| 108 | + && is_array($this->pricingCalculation['category_prices_post_dp']) | |
| 109 | + ? $this->pricingCalculation['category_prices_post_dp'] | |
| 110 | + : []; | |
| 111 | + | |
| 112 | + // If the session carries explicit per-category counts, trust them | |
| 113 | + // verbatim. The previous "first category defaults to 1 when missing" | |
| 114 | + // fallback fired even alongside a real traveler_counts payload, which | |
| 115 | + // double-counted: e.g. with `{"53": 1}` (1 Senior) the Adult row at | |
| 116 | + // index 0 was getting a phantom +1, so the sidebar showed two | |
| 117 | + // travelers when the customer had picked one. | |
| 118 | + $hasExplicitCounts = is_array($travelerCounts) && !empty($travelerCounts); | |
| 119 | + | |
| 100 | 120 | foreach ($priceTypes as $index => $pt) { |
| 101 | 121 | $pt = (object) $pt; |
| 102 | 122 | $categoryId = $pt->category_id ?? $index; |
| 103 | 123 | $categoryLabel = $pt->category_label ?? __('Traveler', 'yatra'); |
| 104 | - $categoryPrice = isset($pt->effective_price) | |
| 105 | - ? (float) $pt->effective_price | |
| 124 | + $catalogPrice = isset($pt->effective_price) | |
| 125 | + ? (float) $pt->effective_price | |
| 106 | 126 | : \Yatra\Services\TripPricingService::resolveCategoryEffectivePrice((array) $pt); |
| 107 | - $count = isset($travelerCounts[$categoryId]) | |
| 108 | - ? (int) $travelerCounts[$categoryId] | |
| 109 | - : ($index === 0 ? 1 : 0); | |
| 110 | - | |
| 127 | + | |
| 128 | + // Prefer DP-adjusted price (post-rules) when present. | |
| 129 | + $categoryPrice = isset($postDpPrices[(string) $categoryId]) | |
| 130 | + ? (float) $postDpPrices[(string) $categoryId] | |
| 131 | + : $catalogPrice; | |
| 132 | + | |
| 133 | + if ($hasExplicitCounts) { | |
| 134 | + $count = isset($travelerCounts[$categoryId]) ? (int) $travelerCounts[$categoryId] : 0; | |
| 135 | + } else { | |
| 136 | + // No per-category data at all (legacy session shape) — keep the | |
| 137 | + // old "default first category to 1" so empty sessions still | |
| 138 | + // render a sensible single-traveler row. | |
| 139 | + $count = $index === 0 ? 1 : 0; | |
| 140 | + } | |
| 141 | + | |
| 111 | 142 | if ($count > 0) { |
| 112 | 143 | $breakdown[] = [ |
| 113 | 144 | 'category_id' => $categoryId, |
| 114 | 145 | 'label' => $categoryLabel, |
| @@ -113,13 +144,22 @@ | ||
| 113 | 144 | 'category_id' => $categoryId, |
| 114 | 145 | 'label' => $categoryLabel, |
| 115 | 146 | 'count' => $count, |
| 116 | 147 | 'price' => $categoryPrice, |
| 117 | - 'subtotal' => $categoryPrice * $count, | |
| 148 | + // The non-DP price stays available so the template can | |
| 149 | + // optionally render strike-through original pricing if it | |
| 150 | + // wants to highlight savings — but the default render is | |
| 151 | + // the post-DP price baked in here. | |
| 152 | + 'catalog_price' => $catalogPrice, | |
| 153 | + // Single source of truth for the line amount: per-person × | |
| 154 | + // count, flat per-group, or per-block group pricing. Keeps | |
| 155 | + // the breakdown row in lock-step with the charged total. | |
| 156 | + 'subtotal' => \Yatra\Services\TripPricingService::categoryLineSubtotal($pt, $count, $categoryPrice), | |
| 157 | + 'pricing_mode' => $pt->pricing_mode ?? 'per_person', | |
| 118 | 158 | ]; |
| 119 | 159 | } |
| 120 | 160 | } |
| 121 | - | |
| 161 | + | |
| 122 | 162 | return $breakdown; |
| 123 | 163 | } |
| 124 | 164 | |
| 125 | 165 | // ========== Pricing Amounts ========== |
| @@ -223,9 +263,56 @@ | ||
| 223 | 263 | public function getTotalDiscountAmount(): float |
| 224 | 264 | { |
| 225 | 265 | return $this->getGroupDiscountAmount() + $this->getCouponDiscountAmount(); |
| 226 | 266 | } |
| 227 | - | |
| 267 | + | |
| 268 | + // ========== Dynamic Pricing ========== | |
| 269 | + // | |
| 270 | + // These accessors expose the dynamic-pricing breakdown that CalculationService | |
| 271 | + // collects via the `yatra_price_breakdown` filter. The pricing-summary | |
| 272 | + // template uses them to render a "Dynamic Pricing" line item with the | |
| 273 | + // original price, applied rules, and savings, rather than silently folding | |
| 274 | + // the adjustment into the trip subtotal. | |
| 275 | + | |
| 276 | + public function getDynamicPricing(): ?array | |
| 277 | + { | |
| 278 | + $dp = $this->pricingCalculation['dynamic_pricing'] ?? null; | |
| 279 | + if (!is_array($dp) || empty($dp['rules'])) { | |
| 280 | + return null; | |
| 281 | + } | |
| 282 | + return $dp; | |
| 283 | + } | |
| 284 | + | |
| 285 | + public function hasDynamicPricing(): bool | |
| 286 | + { | |
| 287 | + return $this->getDynamicPricing() !== null; | |
| 288 | + } | |
| 289 | + | |
| 290 | + /** | |
| 291 | + * Total dollar impact dynamic pricing has on the booking. Negative means | |
| 292 | + * a discount, positive means a markup. For traveler-based trips the DP | |
| 293 | + * filter fires per-category so we don't have a single per-unit delta; in | |
| 294 | + * that case fall back to the breakdown's savings field. | |
| 295 | + */ | |
| 296 | + public function getDynamicPricingTotalAdjustment(): float | |
| 297 | + { | |
| 298 | + $delta = (float) ($this->pricingCalculation['dp_total_adjustment'] ?? 0); | |
| 299 | + if ($delta !== 0.0) { | |
| 300 | + return $delta; | |
| 301 | + } | |
| 302 | + $dp = $this->getDynamicPricing(); | |
| 303 | + if ($dp && isset($dp['savings'])) { | |
| 304 | + return -1.0 * (float) $dp['savings']; // savings positive = discount, so negate for signed delta | |
| 305 | + } | |
| 306 | + return 0.0; | |
| 307 | + } | |
| 308 | + | |
| 309 | + public function getDynamicPricingLabel(): string | |
| 310 | + { | |
| 311 | + $dp = $this->getDynamicPricing(); | |
| 312 | + return $dp ? (string) ($dp['label'] ?? __('Dynamic Pricing', 'yatra')) : ''; | |
| 313 | + } | |
| 314 | + | |
| 228 | 315 | // ========== Tax Information ========== |
| 229 | 316 | |
| 230 | 317 | public function isTaxEnabled(): bool |
| 231 | 318 | { |