PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.13
Yatra – Travel Booking & Tour Operator Software v3.0.13
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 2.0.11 All 82 releases
yatra / app / Models / Checkout.php

Checkout.php in Yatra – Travel Booking & Tour Operator Software 3.0.13, at app/Models/Checkout.php

474 lines 14.4 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\Models;
6
7 /**
8 * Checkout Model
9 *
10 * Encapsulates all checkout/booking data and provides clean getter methods
11 * for templates to access pricing, trip, traveler, and discount information.
12 *
13 * @package Yatra\Models
14 * @since 3.0.0
15 */
16 class Checkout
17 {
18 private \stdClass $trip;
19 private array $session;
20 private array $pricingCalculation;
21 private array $taxCalculation;
22
23 public function __construct(\stdClass $trip, array $session, array $pricingCalculation)
24 {
25 $this->trip = $trip;
26 $this->session = $session;
27 $this->pricingCalculation = $pricingCalculation;
28 $this->taxCalculation = $pricingCalculation['tax_calculation'] ?? [
29 'enable_tax' => false,
30 'tax_breakdown' => [],
31 'total_tax_amount' => 0,
32 'tax_inclusive' => false,
33 ];
34 }
35
36 // ========== Trip Information ==========
37
38 public function getTrip(): \stdClass
39 {
40 return $this->trip;
41 }
42
43 public function getTripId(): int
44 {
45 return (int) $this->trip->id;
46 }
47
48 public function getTripName(): string
49 {
50 return $this->trip->name ?? '';
51 }
52
53 public function getTravelDate(): string
54 {
55 return $this->session['travel_date'] ?? '';
56 }
57
58 public function getDepartureTime(): string
59 {
60 return $this->session['departure_time'] ?? '';
61 }
62
63 // ========== Pricing Type & Travelers ==========
64
65 public function getPricingType(): string
66 {
67 return $this->session['pricing_type'] ?? 'regular';
68 }
69
70 public function isTravelerBased(): bool
71 {
72 return $this->getPricingType() === 'traveler_based';
73 }
74
75 public function getTotalTravelers(): int
76 {
77 return (int) ($this->session['travelers'] ?? 1);
78 }
79
80 public function getTravelerCounts(): array
81 {
82 return $this->session['traveler_counts'] ?? [];
83 }
84
85 public function getPriceTypes(): array
86 {
87 return $this->session['price_types'] ?? [];
88 }
89
90 public function getCategoryBreakdown(): array
91 {
92 if (!$this->isTravelerBased() || empty($this->getPriceTypes())) {
93 return [];
94 }
95
96 $breakdown = [];
97 $priceTypes = $this->getPriceTypes();
98 $travelerCounts = $this->getTravelerCounts();
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
120 foreach ($priceTypes as $index => $pt) {
121 $pt = (object) $pt;
122 $categoryId = $pt->category_id ?? $index;
123 $categoryLabel = $pt->category_label ?? __('Traveler', 'yatra');
124 $catalogPrice = isset($pt->effective_price)
125 ? (float) $pt->effective_price
126 : \Yatra\Services\TripPricingService::resolveCategoryEffectivePrice((array) $pt);
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
142 if ($count > 0) {
143 $breakdown[] = [
144 'category_id' => $categoryId,
145 'label' => $categoryLabel,
146 'count' => $count,
147 'price' => $categoryPrice,
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',
158 ];
159 }
160 }
161
162 return $breakdown;
163 }
164
165 // ========== Pricing Amounts ==========
166
167 public function getPricePerPerson(): float
168 {
169 // Use unit_price from pricing calculation, or fallback to trip's discounted/original price
170 if (isset($this->pricingCalculation['unit_price']) && $this->pricingCalculation['unit_price'] > 0) {
171 return (float) $this->pricingCalculation['unit_price'];
172 }
173
174 // Fallback to trip pricing via centralized TripPricingService
175 return \Yatra\Services\TripPricingService::resolveRegularCurrentPrice($this->trip);
176 }
177
178 public function getBaseAmount(): float
179 {
180 return (float) ($this->pricingCalculation['base_amount'] ?? 0);
181 }
182
183 public function getGrossTotal(): float
184 {
185 return (float) ($this->pricingCalculation['gross_total'] ?? $this->getBaseAmount());
186 }
187
188 public function getSubtotal(): float
189 {
190 return $this->getGrossTotal();
191 }
192
193 public function getNetTotal(): float
194 {
195 return (float) ($this->pricingCalculation['final_total'] ?? 0);
196 }
197
198 public function getFinalTotal(): float
199 {
200 return $this->getNetTotal();
201 }
202
203 public function getAmountDue(): float
204 {
205 return (float) ($this->pricingCalculation['amount_due'] ?? $this->getNetTotal());
206 }
207
208 // ========== Discounts ==========
209
210 public function getGroupDiscount(): ?array
211 {
212 $discount = $this->pricingCalculation['group_discount'] ?? null;
213 if (empty($discount) || empty($discount['amount'])) {
214 return null;
215 }
216 return $discount;
217 }
218
219 public function getGroupDiscountAmount(): float
220 {
221 $discount = $this->getGroupDiscount();
222 return $discount ? (float) ($discount['amount'] ?? 0) : 0.0;
223 }
224
225 public function getGroupDiscountLabel(): string
226 {
227 $discount = $this->getGroupDiscount();
228 return $discount ? ($discount['label'] ?? __('Group Discount', 'yatra')) : '';
229 }
230
231 public function getCouponDiscount(): ?array
232 {
233 $discount = $this->pricingCalculation['coupon_discount'] ?? null;
234 if (empty($discount) || empty($discount['calculated_amount'])) {
235 return null;
236 }
237 return $discount;
238 }
239
240 public function getCouponDiscountAmount(): float
241 {
242 $discount = $this->getCouponDiscount();
243 return $discount ? (float) ($discount['calculated_amount'] ?? 0) : 0.0;
244 }
245
246 public function getCouponDiscountLabel(): string
247 {
248 $discount = $this->getCouponDiscount();
249 return $discount ? ($discount['label'] ?? __('Coupon Discount', 'yatra')) : '';
250 }
251
252 public function getCouponCode(): string
253 {
254 $discount = $this->getCouponDiscount();
255 return $discount ? ($discount['code'] ?? '') : '';
256 }
257
258 public function hasCoupon(): bool
259 {
260 return !empty($this->getCouponCode());
261 }
262
263 public function getTotalDiscountAmount(): float
264 {
265 return $this->getGroupDiscountAmount() + $this->getCouponDiscountAmount();
266 }
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
315 // ========== Tax Information ==========
316
317 public function isTaxEnabled(): bool
318 {
319 return !empty($this->taxCalculation['enable_tax']);
320 }
321
322 public function getTaxBreakdown(): array
323 {
324 return $this->taxCalculation['tax_breakdown'] ?? [];
325 }
326
327 public function getTotalTaxAmount(): float
328 {
329 return (float) ($this->taxCalculation['total_tax_amount'] ?? 0);
330 }
331
332 public function isTaxInclusive(): bool
333 {
334 return !empty($this->taxCalculation['tax_inclusive']);
335 }
336
337 public function hasTaxes(): bool
338 {
339 return $this->isTaxEnabled() && !empty($this->getTaxBreakdown());
340 }
341
342 public function getTaxableAmount(): float
343 {
344 return (float) ($this->pricingCalculation['taxable_amount'] ?? 0);
345 }
346
347 // ========== Payment Methods ==========
348
349 public function getPaymentMethod(): string
350 {
351 return $this->session['payment_method'] ?? 'full';
352 }
353
354 public function isDepositPayment(): bool
355 {
356 return $this->getPaymentMethod() === 'deposit';
357 }
358
359 public function isPartialPayment(): bool
360 {
361 return $this->getPaymentMethod() === 'partial';
362 }
363
364 public function getDepositPercentage(): int
365 {
366 return (int) ($this->session['deposit_percentage'] ?? 20);
367 }
368
369 public function getPartialPercentage(): int
370 {
371 return (int) ($this->session['partial_payment_percentage'] ?? 30);
372 }
373
374 // ========== Additional Services ==========
375
376 public function getAdditionalServices(): array
377 {
378 return $this->pricingCalculation['additional_services'] ?? [];
379 }
380
381 public function getServicesTotal(): float
382 {
383 return (float) ($this->pricingCalculation['services_total'] ?? 0);
384 }
385
386 public function hasAdditionalServices(): bool
387 {
388 return !empty($this->getAdditionalServices());
389 }
390
391 // ========== Itinerary Costs ==========
392
393 public function getItineraryCosts(): array
394 {
395 return $this->pricingCalculation['itinerary_costs'] ?? [];
396 }
397
398 public function getItineraryCostsTotal(): float
399 {
400 return (float) ($this->pricingCalculation['itinerary_costs_total'] ?? 0);
401 }
402
403 public function hasItineraryCosts(): bool
404 {
405 return !empty($this->getItineraryCosts());
406 }
407
408 // ========== Currency ==========
409
410 public function getCurrency(): ?string
411 {
412 return $this->pricingCalculation['currency'] ?? null;
413 }
414
415 public function getCurrencySymbol(): string
416 {
417 return yatra_get_currency_symbol($this->getCurrency());
418 }
419
420 // ========== Formatted Prices ==========
421
422 public function formatPrice(float $amount): string
423 {
424 return yatra_format_price($amount, $this->getCurrency());
425 }
426
427 public function getFormattedPricePerPerson(): string
428 {
429 return $this->formatPrice($this->getPricePerPerson());
430 }
431
432 public function getFormattedGrossTotal(): string
433 {
434 return $this->formatPrice($this->getGrossTotal());
435 }
436
437 public function getFormattedNetTotal(): string
438 {
439 return $this->formatPrice($this->getNetTotal());
440 }
441
442 public function getFormattedAmountDue(): string
443 {
444 return $this->formatPrice($this->getAmountDue());
445 }
446
447 public function getFormattedCouponDiscount(): string
448 {
449 return $this->formatPrice($this->getCouponDiscountAmount());
450 }
451
452 public function getFormattedGroupDiscount(): string
453 {
454 return $this->formatPrice($this->getGroupDiscountAmount());
455 }
456
457 // ========== Raw Data Access (for advanced use) ==========
458
459 public function getSession(): array
460 {
461 return $this->session;
462 }
463
464 public function getPricingCalculation(): array
465 {
466 return $this->pricingCalculation;
467 }
468
469 public function getTaxCalculation(): array
470 {
471 return $this->taxCalculation;
472 }
473 }
474