PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.6
Yatra – Travel Booking & Tour Operator Software v3.0.6
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.6, at app/Models/Checkout.php

471 lines 14.1 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 'subtotal' => $categoryPrice * $count,
154 'pricing_mode' => $pt->pricing_mode ?? 'per_person',
155 ];
156 }
157 }
158
159 return $breakdown;
160 }
161
162 // ========== Pricing Amounts ==========
163
164 public function getPricePerPerson(): float
165 {
166 // Use unit_price from pricing calculation, or fallback to trip's discounted/original price
167 if (isset($this->pricingCalculation['unit_price']) && $this->pricingCalculation['unit_price'] > 0) {
168 return (float) $this->pricingCalculation['unit_price'];
169 }
170
171 // Fallback to trip pricing via centralized TripPricingService
172 return \Yatra\Services\TripPricingService::resolveRegularCurrentPrice($this->trip);
173 }
174
175 public function getBaseAmount(): float
176 {
177 return (float) ($this->pricingCalculation['base_amount'] ?? 0);
178 }
179
180 public function getGrossTotal(): float
181 {
182 return (float) ($this->pricingCalculation['gross_total'] ?? $this->getBaseAmount());
183 }
184
185 public function getSubtotal(): float
186 {
187 return $this->getGrossTotal();
188 }
189
190 public function getNetTotal(): float
191 {
192 return (float) ($this->pricingCalculation['final_total'] ?? 0);
193 }
194
195 public function getFinalTotal(): float
196 {
197 return $this->getNetTotal();
198 }
199
200 public function getAmountDue(): float
201 {
202 return (float) ($this->pricingCalculation['amount_due'] ?? $this->getNetTotal());
203 }
204
205 // ========== Discounts ==========
206
207 public function getGroupDiscount(): ?array
208 {
209 $discount = $this->pricingCalculation['group_discount'] ?? null;
210 if (empty($discount) || empty($discount['amount'])) {
211 return null;
212 }
213 return $discount;
214 }
215
216 public function getGroupDiscountAmount(): float
217 {
218 $discount = $this->getGroupDiscount();
219 return $discount ? (float) ($discount['amount'] ?? 0) : 0.0;
220 }
221
222 public function getGroupDiscountLabel(): string
223 {
224 $discount = $this->getGroupDiscount();
225 return $discount ? ($discount['label'] ?? __('Group Discount', 'yatra')) : '';
226 }
227
228 public function getCouponDiscount(): ?array
229 {
230 $discount = $this->pricingCalculation['coupon_discount'] ?? null;
231 if (empty($discount) || empty($discount['calculated_amount'])) {
232 return null;
233 }
234 return $discount;
235 }
236
237 public function getCouponDiscountAmount(): float
238 {
239 $discount = $this->getCouponDiscount();
240 return $discount ? (float) ($discount['calculated_amount'] ?? 0) : 0.0;
241 }
242
243 public function getCouponDiscountLabel(): string
244 {
245 $discount = $this->getCouponDiscount();
246 return $discount ? ($discount['label'] ?? __('Coupon Discount', 'yatra')) : '';
247 }
248
249 public function getCouponCode(): string
250 {
251 $discount = $this->getCouponDiscount();
252 return $discount ? ($discount['code'] ?? '') : '';
253 }
254
255 public function hasCoupon(): bool
256 {
257 return !empty($this->getCouponCode());
258 }
259
260 public function getTotalDiscountAmount(): float
261 {
262 return $this->getGroupDiscountAmount() + $this->getCouponDiscountAmount();
263 }
264
265 // ========== Dynamic Pricing ==========
266 //
267 // These accessors expose the dynamic-pricing breakdown that CalculationService
268 // collects via the `yatra_price_breakdown` filter. The pricing-summary
269 // template uses them to render a "Dynamic Pricing" line item with the
270 // original price, applied rules, and savings, rather than silently folding
271 // the adjustment into the trip subtotal.
272
273 public function getDynamicPricing(): ?array
274 {
275 $dp = $this->pricingCalculation['dynamic_pricing'] ?? null;
276 if (!is_array($dp) || empty($dp['rules'])) {
277 return null;
278 }
279 return $dp;
280 }
281
282 public function hasDynamicPricing(): bool
283 {
284 return $this->getDynamicPricing() !== null;
285 }
286
287 /**
288 * Total dollar impact dynamic pricing has on the booking. Negative means
289 * a discount, positive means a markup. For traveler-based trips the DP
290 * filter fires per-category so we don't have a single per-unit delta; in
291 * that case fall back to the breakdown's savings field.
292 */
293 public function getDynamicPricingTotalAdjustment(): float
294 {
295 $delta = (float) ($this->pricingCalculation['dp_total_adjustment'] ?? 0);
296 if ($delta !== 0.0) {
297 return $delta;
298 }
299 $dp = $this->getDynamicPricing();
300 if ($dp && isset($dp['savings'])) {
301 return -1.0 * (float) $dp['savings']; // savings positive = discount, so negate for signed delta
302 }
303 return 0.0;
304 }
305
306 public function getDynamicPricingLabel(): string
307 {
308 $dp = $this->getDynamicPricing();
309 return $dp ? (string) ($dp['label'] ?? __('Dynamic Pricing', 'yatra')) : '';
310 }
311
312 // ========== Tax Information ==========
313
314 public function isTaxEnabled(): bool
315 {
316 return !empty($this->taxCalculation['enable_tax']);
317 }
318
319 public function getTaxBreakdown(): array
320 {
321 return $this->taxCalculation['tax_breakdown'] ?? [];
322 }
323
324 public function getTotalTaxAmount(): float
325 {
326 return (float) ($this->taxCalculation['total_tax_amount'] ?? 0);
327 }
328
329 public function isTaxInclusive(): bool
330 {
331 return !empty($this->taxCalculation['tax_inclusive']);
332 }
333
334 public function hasTaxes(): bool
335 {
336 return $this->isTaxEnabled() && !empty($this->getTaxBreakdown());
337 }
338
339 public function getTaxableAmount(): float
340 {
341 return (float) ($this->pricingCalculation['taxable_amount'] ?? 0);
342 }
343
344 // ========== Payment Methods ==========
345
346 public function getPaymentMethod(): string
347 {
348 return $this->session['payment_method'] ?? 'full';
349 }
350
351 public function isDepositPayment(): bool
352 {
353 return $this->getPaymentMethod() === 'deposit';
354 }
355
356 public function isPartialPayment(): bool
357 {
358 return $this->getPaymentMethod() === 'partial';
359 }
360
361 public function getDepositPercentage(): int
362 {
363 return (int) ($this->session['deposit_percentage'] ?? 20);
364 }
365
366 public function getPartialPercentage(): int
367 {
368 return (int) ($this->session['partial_payment_percentage'] ?? 30);
369 }
370
371 // ========== Additional Services ==========
372
373 public function getAdditionalServices(): array
374 {
375 return $this->pricingCalculation['additional_services'] ?? [];
376 }
377
378 public function getServicesTotal(): float
379 {
380 return (float) ($this->pricingCalculation['services_total'] ?? 0);
381 }
382
383 public function hasAdditionalServices(): bool
384 {
385 return !empty($this->getAdditionalServices());
386 }
387
388 // ========== Itinerary Costs ==========
389
390 public function getItineraryCosts(): array
391 {
392 return $this->pricingCalculation['itinerary_costs'] ?? [];
393 }
394
395 public function getItineraryCostsTotal(): float
396 {
397 return (float) ($this->pricingCalculation['itinerary_costs_total'] ?? 0);
398 }
399
400 public function hasItineraryCosts(): bool
401 {
402 return !empty($this->getItineraryCosts());
403 }
404
405 // ========== Currency ==========
406
407 public function getCurrency(): ?string
408 {
409 return $this->pricingCalculation['currency'] ?? null;
410 }
411
412 public function getCurrencySymbol(): string
413 {
414 return yatra_get_currency_symbol($this->getCurrency());
415 }
416
417 // ========== Formatted Prices ==========
418
419 public function formatPrice(float $amount): string
420 {
421 return yatra_format_price($amount, $this->getCurrency());
422 }
423
424 public function getFormattedPricePerPerson(): string
425 {
426 return $this->formatPrice($this->getPricePerPerson());
427 }
428
429 public function getFormattedGrossTotal(): string
430 {
431 return $this->formatPrice($this->getGrossTotal());
432 }
433
434 public function getFormattedNetTotal(): string
435 {
436 return $this->formatPrice($this->getNetTotal());
437 }
438
439 public function getFormattedAmountDue(): string
440 {
441 return $this->formatPrice($this->getAmountDue());
442 }
443
444 public function getFormattedCouponDiscount(): string
445 {
446 return $this->formatPrice($this->getCouponDiscountAmount());
447 }
448
449 public function getFormattedGroupDiscount(): string
450 {
451 return $this->formatPrice($this->getGroupDiscountAmount());
452 }
453
454 // ========== Raw Data Access (for advanced use) ==========
455
456 public function getSession(): array
457 {
458 return $this->session;
459 }
460
461 public function getPricingCalculation(): array
462 {
463 return $this->pricingCalculation;
464 }
465
466 public function getTaxCalculation(): array
467 {
468 return $this->taxCalculation;
469 }
470 }
471