| @@ -304,8 +304,134 @@ | ||
| 304 | 304 | return self::countryNames()[$code] ?? $code; |
| 305 | 305 | } |
| 306 | 306 | |
| 307 | 307 | /** |
| 308 | + * Build a customer's postal address as display lines, for invoices and any | |
| 309 | + * other document that has to show where the customer actually is. | |
| 310 | + * | |
| 311 | + * Two sources, in order of authority: | |
| 312 | + * 1. The booking's own contact data — what the customer entered at the time | |
| 313 | + * of purchase, which is what an invoice should reflect. | |
| 314 | + * 2. The linked customer record, which carries the structured city / state / | |
| 315 | + * postal code / country that the booking form does not always collect. | |
| 316 | + * | |
| 317 | + * @param object|array|null $booking Booking (or payment joined to one) carrying | |
| 318 | + * contact_data / contact_country / customer_id. | |
| 319 | + * @return string[] Non-empty address lines, ready to render one per line. | |
| 320 | + */ | |
| 321 | + public static function customerAddressLines($booking): array | |
| 322 | + { | |
| 323 | + $get = static function ($source, string $key) { | |
| 324 | + if (is_array($source)) { | |
| 325 | + return $source[$key] ?? null; | |
| 326 | + } | |
| 327 | + if (is_object($source)) { | |
| 328 | + return $source->$key ?? null; | |
| 329 | + } | |
| 330 | + return null; | |
| 331 | + }; | |
| 332 | + | |
| 333 | + $parts = ['address' => '', 'city' => '', 'state' => '', 'postal_code' => '', 'country' => '']; | |
| 334 | + | |
| 335 | + // 1. What the customer typed when booking. | |
| 336 | + $contactData = $get($booking, 'contact_data'); | |
| 337 | + if (is_string($contactData) && $contactData !== '') { | |
| 338 | + $contactData = json_decode($contactData, true); | |
| 339 | + } | |
| 340 | + if (is_array($contactData)) { | |
| 341 | + foreach (array_keys($parts) as $key) { | |
| 342 | + if (!empty($contactData[$key]) && is_scalar($contactData[$key])) { | |
| 343 | + $parts[$key] = trim((string) $contactData[$key]); | |
| 344 | + } | |
| 345 | + } | |
| 346 | + // The booking form stores the postcode under either name. | |
| 347 | + if ($parts['postal_code'] === '' && !empty($contactData['zip'])) { | |
| 348 | + $parts['postal_code'] = trim((string) $contactData['zip']); | |
| 349 | + } | |
| 350 | + } | |
| 351 | + | |
| 352 | + if ($parts['country'] === '') { | |
| 353 | + $parts['country'] = trim((string) ($get($booking, 'contact_country') ?? '')); | |
| 354 | + } | |
| 355 | + | |
| 356 | + // 2. Fill the gaps from the customer record. | |
| 357 | + $customerId = (int) ($get($booking, 'customer_id') ?? 0); | |
| 358 | + if ($customerId > 0 && in_array('', $parts, true)) { | |
| 359 | + $customer = (new \Yatra\Repositories\CustomerRepository())->find($customerId); | |
| 360 | + | |
| 361 | + if ($customer) { | |
| 362 | + foreach (array_keys($parts) as $key) { | |
| 363 | + if ($parts[$key] === '' && !empty($customer->$key)) { | |
| 364 | + $parts[$key] = trim((string) $customer->$key); | |
| 365 | + } | |
| 366 | + } | |
| 367 | + } | |
| 368 | + } | |
| 369 | + | |
| 370 | + // "City, State 12345" reads as one line; street and country get their own. | |
| 371 | + $locality = trim(implode(', ', array_filter([$parts['city'], $parts['state']]))); | |
| 372 | + if ($parts['postal_code'] !== '') { | |
| 373 | + $locality = trim($locality . ' ' . $parts['postal_code']); | |
| 374 | + } | |
| 375 | + | |
| 376 | + $country = $parts['country'] !== '' ? self::getCountryName($parts['country']) : ''; | |
| 377 | + | |
| 378 | + return array_values(array_filter([$parts['address'], $locality, $country], static function ($line) { | |
| 379 | + return $line !== ''; | |
| 380 | + })); | |
| 381 | + } | |
| 382 | + | |
| 383 | + /** | |
| 384 | + * The operator's own business address, one entry per line. | |
| 385 | + * | |
| 386 | + * Settings hold the address in separate fields (street, city, state, postcode, | |
| 387 | + * country) but documents only ever printed `company_address` — so everything | |
| 388 | + * after the street silently vanished and the address looked truncated at its | |
| 389 | + * final line. Composed the same way as {@see customerAddressLines()} so both | |
| 390 | + * sides of a document read identically. | |
| 391 | + * | |
| 392 | + * @return list<string> | |
| 393 | + */ | |
| 394 | + public static function companyAddressLines(): array | |
| 395 | + { | |
| 396 | + $get = static function (string $key): string { | |
| 397 | + return trim((string) \Yatra\Services\SettingsService::get($key, '')); | |
| 398 | + }; | |
| 399 | + | |
| 400 | + $street = $get('company_address'); | |
| 401 | + $city = $get('company_city'); | |
| 402 | + $state = $get('company_state'); | |
| 403 | + $zip = $get('company_zip'); | |
| 404 | + $countryRaw = $get('company_country'); | |
| 405 | + | |
| 406 | + // "City, State 12345" reads as one line; street and country get their own. | |
| 407 | + $locality = trim(implode(', ', array_filter([$city, $state]))); | |
| 408 | + if ($zip !== '') { | |
| 409 | + $locality = trim($locality . ' ' . $zip); | |
| 410 | + } | |
| 411 | + | |
| 412 | + $country = $countryRaw !== '' ? self::getCountryName($countryRaw) : ''; | |
| 413 | + | |
| 414 | + // The street field may itself carry line breaks (an operator pasting a full | |
| 415 | + // address), so keep those as separate lines rather than one run-on entry. | |
| 416 | + $lines = []; | |
| 417 | + foreach (preg_split('/\R/', $street) ?: [] as $streetLine) { | |
| 418 | + $streetLine = trim($streetLine); | |
| 419 | + if ($streetLine !== '') { | |
| 420 | + $lines[] = $streetLine; | |
| 421 | + } | |
| 422 | + } | |
| 423 | + | |
| 424 | + foreach ([$locality, $country] as $line) { | |
| 425 | + if ($line !== '') { | |
| 426 | + $lines[] = $line; | |
| 427 | + } | |
| 428 | + } | |
| 429 | + | |
| 430 | + return array_values($lines); | |
| 431 | + } | |
| 432 | + | |
| 433 | + /** | |
| 308 | 434 | * Canonical country list — single source of truth used by every |
| 309 | 435 | * country / nationality dropdown in both Free and Pro plugins. |
| 310 | 436 | * |
| 311 | 437 | * Returns the full ISO-3166-1 alpha-2 set (sovereign states + |
| @@ -674,10 +800,20 @@ | ||
| 674 | 800 | */ |
| 675 | 801 | public static function formatRatingStars(float $rating, bool $showNumber = false): string |
| 676 | 802 | { |
| 677 | 803 | $rating = max(0, min(5, $rating)); |
| 678 | - $fullStars = (int) floor($rating); | |
| 679 | - $hasHalfStar = ($rating - $fullStars) >= 0.5; | |
| 804 | + | |
| 805 | + // Rounds to the nearest half star (see yatra_rating_star_parts), so this | |
| 806 | + // renderer agrees with the confirmation page and reviews block. | |
| 807 | + if (function_exists('yatra_rating_star_parts')) { | |
| 808 | + $parts = yatra_rating_star_parts($rating); | |
| 809 | + $fullStars = $parts['full']; | |
| 810 | + $hasHalfStar = $parts['half']; | |
| 811 | + } else { | |
| 812 | + $halves = (int) round($rating * 2); | |
| 813 | + $fullStars = intdiv($halves, 2); | |
| 814 | + $hasHalfStar = ($halves % 2) === 1; | |
| 815 | + } | |
| 680 | 816 | |
| 681 | 817 | $html = '<span class="yatra-rating-stars">'; |
| 682 | 818 | |
| 683 | 819 | for ($i = 1; $i <= 5; $i++) { |