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/Helpers/FormatHelper.php +506 -59 3.0.3 → 3.0.16 View file →
@@ -22,61 +22,108 @@
22 22
23 23 /**
24 24 * Country codes to names mapping
25 25 */
26 - private const COUNTRY_NAMES = [
27 - 'AF' => 'Afghanistan', 'AL' => 'Albania', 'DZ' => 'Algeria', 'AD' => 'Andorra',
28 - 'AO' => 'Angola', 'AG' => 'Antigua and Barbuda', 'AR' => 'Argentina', 'AM' => 'Armenia',
29 - 'AU' => 'Australia', 'AT' => 'Austria', 'AZ' => 'Azerbaijan', 'BS' => 'Bahamas',
30 - 'BH' => 'Bahrain', 'BD' => 'Bangladesh', 'BB' => 'Barbados', 'BY' => 'Belarus',
31 - 'BE' => 'Belgium', 'BZ' => 'Belize', 'BJ' => 'Benin', 'BT' => 'Bhutan',
32 - 'BO' => 'Bolivia', 'BA' => 'Bosnia and Herzegovina', 'BW' => 'Botswana', 'BR' => 'Brazil',
33 - 'BN' => 'Brunei', 'BG' => 'Bulgaria', 'BF' => 'Burkina Faso', 'BI' => 'Burundi',
34 - 'KH' => 'Cambodia', 'CM' => 'Cameroon', 'CA' => 'Canada', 'CV' => 'Cape Verde',
35 - 'CF' => 'Central African Republic', 'TD' => 'Chad', 'CL' => 'Chile', 'CN' => 'China',
36 - 'CO' => 'Colombia', 'KM' => 'Comoros', 'CG' => 'Congo', 'CD' => 'DR Congo',
37 - 'CR' => 'Costa Rica', 'CI' => 'Ivory Coast', 'HR' => 'Croatia', 'CU' => 'Cuba',
38 - 'CY' => 'Cyprus', 'CZ' => 'Czech Republic', 'DK' => 'Denmark', 'DJ' => 'Djibouti',
39 - 'DM' => 'Dominica', 'DO' => 'Dominican Republic', 'EC' => 'Ecuador', 'EG' => 'Egypt',
40 - 'SV' => 'El Salvador', 'GQ' => 'Equatorial Guinea', 'ER' => 'Eritrea', 'EE' => 'Estonia',
41 - 'SZ' => 'Eswatini', 'ET' => 'Ethiopia', 'FJ' => 'Fiji', 'FI' => 'Finland',
42 - 'FR' => 'France', 'GA' => 'Gabon', 'GM' => 'Gambia', 'GE' => 'Georgia',
43 - 'DE' => 'Germany', 'GH' => 'Ghana', 'GR' => 'Greece', 'GD' => 'Grenada',
44 - 'GT' => 'Guatemala', 'GN' => 'Guinea', 'GW' => 'Guinea-Bissau', 'GY' => 'Guyana',
45 - 'HT' => 'Haiti', 'HN' => 'Honduras', 'HU' => 'Hungary', 'IS' => 'Iceland',
46 - 'IN' => 'India', 'ID' => 'Indonesia', 'IR' => 'Iran', 'IQ' => 'Iraq',
47 - 'IE' => 'Ireland', 'IL' => 'Israel', 'IT' => 'Italy', 'JM' => 'Jamaica',
48 - 'JP' => 'Japan', 'JO' => 'Jordan', 'KZ' => 'Kazakhstan', 'KE' => 'Kenya',
49 - 'KI' => 'Kiribati', 'KP' => 'North Korea', 'KR' => 'South Korea', 'KW' => 'Kuwait',
50 - 'KG' => 'Kyrgyzstan', 'LA' => 'Laos', 'LV' => 'Latvia', 'LB' => 'Lebanon',
51 - 'LS' => 'Lesotho', 'LR' => 'Liberia', 'LY' => 'Libya', 'LI' => 'Liechtenstein',
52 - 'LT' => 'Lithuania', 'LU' => 'Luxembourg', 'MG' => 'Madagascar', 'MW' => 'Malawi',
53 - 'MY' => 'Malaysia', 'MV' => 'Maldives', 'ML' => 'Mali', 'MT' => 'Malta',
54 - 'MH' => 'Marshall Islands', 'MR' => 'Mauritania', 'MU' => 'Mauritius', 'MX' => 'Mexico',
55 - 'FM' => 'Micronesia', 'MD' => 'Moldova', 'MC' => 'Monaco', 'MN' => 'Mongolia',
56 - 'ME' => 'Montenegro', 'MA' => 'Morocco', 'MZ' => 'Mozambique', 'MM' => 'Myanmar',
57 - 'NA' => 'Namibia', 'NR' => 'Nauru', 'NP' => 'Nepal', 'NL' => 'Netherlands',
58 - 'NZ' => 'New Zealand', 'NI' => 'Nicaragua', 'NE' => 'Niger', 'NG' => 'Nigeria',
59 - 'MK' => 'North Macedonia', 'NO' => 'Norway', 'OM' => 'Oman', 'PK' => 'Pakistan',
60 - 'PW' => 'Palau', 'PS' => 'Palestine', 'PA' => 'Panama', 'PG' => 'Papua New Guinea',
61 - 'PY' => 'Paraguay', 'PE' => 'Peru', 'PH' => 'Philippines', 'PL' => 'Poland',
62 - 'PT' => 'Portugal', 'QA' => 'Qatar', 'RO' => 'Romania', 'RU' => 'Russia',
63 - 'RW' => 'Rwanda', 'KN' => 'Saint Kitts and Nevis', 'LC' => 'Saint Lucia',
64 - 'VC' => 'Saint Vincent and the Grenadines', 'WS' => 'Samoa', 'SM' => 'San Marino',
65 - 'ST' => 'Sao Tome and Principe', 'SA' => 'Saudi Arabia', 'SN' => 'Senegal',
66 - 'RS' => 'Serbia', 'SC' => 'Seychelles', 'SL' => 'Sierra Leone', 'SG' => 'Singapore',
67 - 'SK' => 'Slovakia', 'SI' => 'Slovenia', 'SB' => 'Solomon Islands', 'SO' => 'Somalia',
68 - 'ZA' => 'South Africa', 'SS' => 'South Sudan', 'ES' => 'Spain', 'LK' => 'Sri Lanka',
69 - 'SD' => 'Sudan', 'SR' => 'Suriname', 'SE' => 'Sweden', 'CH' => 'Switzerland',
70 - 'SY' => 'Syria', 'TW' => 'Taiwan', 'TJ' => 'Tajikistan', 'TZ' => 'Tanzania',
71 - 'TH' => 'Thailand', 'TL' => 'Timor-Leste', 'TG' => 'Togo', 'TO' => 'Tonga',
72 - 'TT' => 'Trinidad and Tobago', 'TN' => 'Tunisia', 'TR' => 'Turkey', 'TM' => 'Turkmenistan',
73 - 'TV' => 'Tuvalu', 'UG' => 'Uganda', 'UA' => 'Ukraine', 'AE' => 'United Arab Emirates',
74 - 'GB' => 'United Kingdom', 'US' => 'United States', 'UY' => 'Uruguay', 'UZ' => 'Uzbekistan',
75 - 'VU' => 'Vanuatu', 'VA' => 'Vatican City', 'VE' => 'Venezuela', 'VN' => 'Vietnam',
76 - 'YE' => 'Yemen', 'ZM' => 'Zambia', 'ZW' => 'Zimbabwe',
77 - ];
26 + /**
27 + * Country code => translated display name.
28 + *
29 + * Names are wrapped in __() at the definition so every consumer (booking
30 + * form, admin, confirmation) shows localized names, and so all of them are
31 + * extractable into the translation catalog. Cached per request.
32 + */
33 + private static ?array $countryNamesCache = null;
78 34
35 + private static function countryNames(): array
36 + {
37 + if (self::$countryNamesCache !== null) {
38 + return self::$countryNamesCache;
39 + }
40 +
41 + return self::$countryNamesCache = [
42 +
43 + 'AF' => __('Afghanistan', 'yatra'), 'AL' => __('Albania', 'yatra'), 'DZ' => __('Algeria', 'yatra'), 'AD' => __('Andorra', 'yatra'),
44 + 'AO' => __('Angola', 'yatra'), 'AG' => __('Antigua and Barbuda', 'yatra'), 'AR' => __('Argentina', 'yatra'), 'AM' => __('Armenia', 'yatra'),
45 + 'AU' => __('Australia', 'yatra'), 'AT' => __('Austria', 'yatra'), 'AZ' => __('Azerbaijan', 'yatra'), 'BS' => __('Bahamas', 'yatra'),
46 + 'BH' => __('Bahrain', 'yatra'), 'BD' => __('Bangladesh', 'yatra'), 'BB' => __('Barbados', 'yatra'), 'BY' => __('Belarus', 'yatra'),
47 + 'BE' => __('Belgium', 'yatra'), 'BZ' => __('Belize', 'yatra'), 'BJ' => __('Benin', 'yatra'), 'BT' => __('Bhutan', 'yatra'),
48 + 'BO' => __('Bolivia', 'yatra'), 'BA' => __('Bosnia and Herzegovina', 'yatra'), 'BW' => __('Botswana', 'yatra'), 'BR' => __('Brazil', 'yatra'),
49 + 'BN' => __('Brunei', 'yatra'), 'BG' => __('Bulgaria', 'yatra'), 'BF' => __('Burkina Faso', 'yatra'), 'BI' => __('Burundi', 'yatra'),
50 + 'KH' => __('Cambodia', 'yatra'), 'CM' => __('Cameroon', 'yatra'), 'CA' => __('Canada', 'yatra'), 'CV' => __('Cape Verde', 'yatra'),
51 + 'CF' => __('Central African Republic', 'yatra'), 'TD' => __('Chad', 'yatra'), 'CL' => __('Chile', 'yatra'), 'CN' => __('China', 'yatra'),
52 + 'CO' => __('Colombia', 'yatra'), 'KM' => __('Comoros', 'yatra'), 'CG' => __('Congo', 'yatra'), 'CD' => __('DR Congo', 'yatra'),
53 + 'CR' => __('Costa Rica', 'yatra'), 'CI' => __('Ivory Coast', 'yatra'), 'HR' => __('Croatia', 'yatra'), 'CU' => __('Cuba', 'yatra'),
54 + 'CY' => __('Cyprus', 'yatra'), 'CZ' => __('Czech Republic', 'yatra'), 'DK' => __('Denmark', 'yatra'), 'DJ' => __('Djibouti', 'yatra'),
55 + 'DM' => __('Dominica', 'yatra'), 'DO' => __('Dominican Republic', 'yatra'), 'EC' => __('Ecuador', 'yatra'), 'EG' => __('Egypt', 'yatra'),
56 + 'SV' => __('El Salvador', 'yatra'), 'GQ' => __('Equatorial Guinea', 'yatra'), 'ER' => __('Eritrea', 'yatra'), 'EE' => __('Estonia', 'yatra'),
57 + 'SZ' => __('Eswatini', 'yatra'), 'ET' => __('Ethiopia', 'yatra'), 'FJ' => __('Fiji', 'yatra'), 'FI' => __('Finland', 'yatra'),
58 + 'FR' => __('France', 'yatra'), 'GA' => __('Gabon', 'yatra'), 'GM' => __('Gambia', 'yatra'), 'GE' => __('Georgia', 'yatra'),
59 + 'DE' => __('Germany', 'yatra'), 'GH' => __('Ghana', 'yatra'), 'GR' => __('Greece', 'yatra'), 'GD' => __('Grenada', 'yatra'),
60 + 'GT' => __('Guatemala', 'yatra'), 'GN' => __('Guinea', 'yatra'), 'GW' => __('Guinea-Bissau', 'yatra'), 'GY' => __('Guyana', 'yatra'),
61 + 'HT' => __('Haiti', 'yatra'), 'HN' => __('Honduras', 'yatra'), 'HU' => __('Hungary', 'yatra'), 'IS' => __('Iceland', 'yatra'),
62 + 'IN' => __('India', 'yatra'), 'ID' => __('Indonesia', 'yatra'), 'IR' => __('Iran', 'yatra'), 'IQ' => __('Iraq', 'yatra'),
63 + 'IE' => __('Ireland', 'yatra'), 'IL' => __('Israel', 'yatra'), 'IT' => __('Italy', 'yatra'), 'JM' => __('Jamaica', 'yatra'),
64 + 'JP' => __('Japan', 'yatra'), 'JO' => __('Jordan', 'yatra'), 'KZ' => __('Kazakhstan', 'yatra'), 'KE' => __('Kenya', 'yatra'),
65 + 'KI' => __('Kiribati', 'yatra'), 'KP' => __('North Korea', 'yatra'), 'KR' => __('South Korea', 'yatra'), 'KW' => __('Kuwait', 'yatra'),
66 + 'KG' => __('Kyrgyzstan', 'yatra'), 'LA' => __('Laos', 'yatra'), 'LV' => __('Latvia', 'yatra'), 'LB' => __('Lebanon', 'yatra'),
67 + 'LS' => __('Lesotho', 'yatra'), 'LR' => __('Liberia', 'yatra'), 'LY' => __('Libya', 'yatra'), 'LI' => __('Liechtenstein', 'yatra'),
68 + 'LT' => __('Lithuania', 'yatra'), 'LU' => __('Luxembourg', 'yatra'), 'MG' => __('Madagascar', 'yatra'), 'MW' => __('Malawi', 'yatra'),
69 + 'MY' => __('Malaysia', 'yatra'), 'MV' => __('Maldives', 'yatra'), 'ML' => __('Mali', 'yatra'), 'MT' => __('Malta', 'yatra'),
70 + 'MH' => __('Marshall Islands', 'yatra'), 'MR' => __('Mauritania', 'yatra'), 'MU' => __('Mauritius', 'yatra'), 'MX' => __('Mexico', 'yatra'),
71 + 'FM' => __('Micronesia', 'yatra'), 'MD' => __('Moldova', 'yatra'), 'MC' => __('Monaco', 'yatra'), 'MN' => __('Mongolia', 'yatra'),
72 + 'ME' => __('Montenegro', 'yatra'), 'MA' => __('Morocco', 'yatra'), 'MZ' => __('Mozambique', 'yatra'), 'MM' => __('Myanmar', 'yatra'),
73 + 'NA' => __('Namibia', 'yatra'), 'NR' => __('Nauru', 'yatra'), 'NP' => __('Nepal', 'yatra'), 'NL' => __('Netherlands', 'yatra'),
74 + 'NZ' => __('New Zealand', 'yatra'), 'NI' => __('Nicaragua', 'yatra'), 'NE' => __('Niger', 'yatra'), 'NG' => __('Nigeria', 'yatra'),
75 + 'MK' => __('North Macedonia', 'yatra'), 'NO' => __('Norway', 'yatra'), 'OM' => __('Oman', 'yatra'), 'PK' => __('Pakistan', 'yatra'),
76 + 'PW' => __('Palau', 'yatra'), 'PS' => __('Palestine', 'yatra'), 'PA' => __('Panama', 'yatra'), 'PG' => __('Papua New Guinea', 'yatra'),
77 + 'PY' => __('Paraguay', 'yatra'), 'PE' => __('Peru', 'yatra'), 'PH' => __('Philippines', 'yatra'), 'PL' => __('Poland', 'yatra'),
78 + 'PT' => __('Portugal', 'yatra'), 'QA' => __('Qatar', 'yatra'), 'RO' => __('Romania', 'yatra'), 'RU' => __('Russia', 'yatra'),
79 + 'RW' => __('Rwanda', 'yatra'), 'KN' => __('Saint Kitts and Nevis', 'yatra'), 'LC' => __('Saint Lucia', 'yatra'),
80 + 'VC' => __('Saint Vincent and the Grenadines', 'yatra'), 'WS' => __('Samoa', 'yatra'), 'SM' => __('San Marino', 'yatra'),
81 + 'ST' => __('Sao Tome and Principe', 'yatra'), 'SA' => __('Saudi Arabia', 'yatra'), 'SN' => __('Senegal', 'yatra'),
82 + 'RS' => __('Serbia', 'yatra'), 'SC' => __('Seychelles', 'yatra'), 'SL' => __('Sierra Leone', 'yatra'), 'SG' => __('Singapore', 'yatra'),
83 + 'SK' => __('Slovakia', 'yatra'), 'SI' => __('Slovenia', 'yatra'), 'SB' => __('Solomon Islands', 'yatra'), 'SO' => __('Somalia', 'yatra'),
84 + 'ZA' => __('South Africa', 'yatra'), 'SS' => __('South Sudan', 'yatra'), 'ES' => __('Spain', 'yatra'), 'LK' => __('Sri Lanka', 'yatra'),
85 + 'SD' => __('Sudan', 'yatra'), 'SR' => __('Suriname', 'yatra'), 'SE' => __('Sweden', 'yatra'), 'CH' => __('Switzerland', 'yatra'),
86 + 'SY' => __('Syria', 'yatra'), 'TW' => __('Taiwan', 'yatra'), 'TJ' => __('Tajikistan', 'yatra'), 'TZ' => __('Tanzania', 'yatra'),
87 + 'TH' => __('Thailand', 'yatra'), 'TL' => __('Timor-Leste', 'yatra'), 'TG' => __('Togo', 'yatra'), 'TO' => __('Tonga', 'yatra'),
88 + 'TT' => __('Trinidad and Tobago', 'yatra'), 'TN' => __('Tunisia', 'yatra'), 'TR' => __('Turkey', 'yatra'), 'TM' => __('Turkmenistan', 'yatra'),
89 + 'TV' => __('Tuvalu', 'yatra'), 'UG' => __('Uganda', 'yatra'), 'UA' => __('Ukraine', 'yatra'), 'AE' => __('United Arab Emirates', 'yatra'),
90 + 'GB' => __('United Kingdom', 'yatra'), 'US' => __('United States', 'yatra'), 'UY' => __('Uruguay', 'yatra'), 'UZ' => __('Uzbekistan', 'yatra'),
91 + 'VU' => __('Vanuatu', 'yatra'), 'VA' => __('Vatican City', 'yatra'), 'VE' => __('Venezuela', 'yatra'), 'VN' => __('Vietnam', 'yatra'),
92 + 'YE' => __('Yemen', 'yatra'), 'ZM' => __('Zambia', 'yatra'), 'ZW' => __('Zimbabwe', 'yatra'),
93 + // ISO-3166-1 territories and special regions added so customers
94 + // from common tourism markets (Hong Kong, Puerto Rico, Greenland,
95 + // Gibraltar, Faroe Islands, etc.) can select their location.
96 + 'HK' => __('Hong Kong', 'yatra'), 'MO' => __('Macao', 'yatra'), 'PR' => __('Puerto Rico', 'yatra'),
97 + 'GI' => __('Gibraltar', 'yatra'), 'GL' => __('Greenland', 'yatra'), 'FO' => __('Faroe Islands', 'yatra'),
98 + 'GG' => __('Guernsey', 'yatra'), 'IM' => __('Isle of Man', 'yatra'), 'JE' => __('Jersey', 'yatra'),
99 + 'AX' => __('Aland Islands', 'yatra'), 'SJ' => __('Svalbard and Jan Mayen', 'yatra'),
100 + 'BM' => __('Bermuda', 'yatra'), 'KY' => __('Cayman Islands', 'yatra'), 'AI' => __('Anguilla', 'yatra'),
101 + 'AW' => __('Aruba', 'yatra'), 'CW' => __('Curacao', 'yatra'), 'SX' => __('Sint Maarten', 'yatra'),
102 + 'BQ' => __('Bonaire, Sint Eustatius and Saba', 'yatra'),
103 + 'MS' => __('Montserrat', 'yatra'), 'VG' => __('British Virgin Islands', 'yatra'),
104 + 'TC' => __('Turks and Caicos Islands', 'yatra'),
105 + 'BL' => __('Saint Barthelemy', 'yatra'), 'MF' => __('Saint Martin (French)', 'yatra'),
106 + 'PM' => __('Saint Pierre and Miquelon', 'yatra'),
107 + 'SH' => __('Saint Helena, Ascension and Tristan da Cunha', 'yatra'),
108 + 'FK' => __('Falkland Islands', 'yatra'),
109 + 'GS' => __('South Georgia and the South Sandwich Islands', 'yatra'),
110 + 'PF' => __('French Polynesia', 'yatra'), 'NC' => __('New Caledonia', 'yatra'),
111 + 'WF' => __('Wallis and Futuna', 'yatra'), 'CK' => __('Cook Islands', 'yatra'),
112 + 'NU' => __('Niue', 'yatra'), 'TK' => __('Tokelau', 'yatra'),
113 + 'GP' => __('Guadeloupe', 'yatra'), 'MQ' => __('Martinique', 'yatra'),
114 + 'RE' => __('Reunion', 'yatra'), 'YT' => __('Mayotte', 'yatra'),
115 + 'AS' => __('American Samoa', 'yatra'), 'GU' => __('Guam', 'yatra'),
116 + 'MP' => __('Northern Mariana Islands', 'yatra'), 'VI' => __('U.S. Virgin Islands', 'yatra'),
117 + 'UM' => __('U.S. Minor Outlying Islands', 'yatra'),
118 + 'TF' => __('French Southern Territories', 'yatra'),
119 + 'IO' => __('British Indian Ocean Territory', 'yatra'), 'BV' => __('Bouvet Island', 'yatra'),
120 + 'HM' => __('Heard Island and McDonald Islands', 'yatra'),
121 + 'AQ' => __('Antarctica', 'yatra'), 'PN' => __('Pitcairn', 'yatra'),
122 + 'EH' => __('Western Sahara', 'yatra'), 'XK' => __('Kosovo', 'yatra'),
123 + ];
124 + }
125 +
79 126 /**
80 127 * Format price with currency
81 128 *
82 129 * @param float $amount Amount to format
@@ -253,22 +300,412 @@
253 300 */
254 301 public static function getCountryName(string $code): string
255 302 {
256 303 $code = strtoupper($code);
257 - return self::COUNTRY_NAMES[$code] ?? $code;
304 + return self::countryNames()[$code] ?? $code;
258 305 }
259 306
260 307 /**
261 - * Get all countries as options array
262 - *
263 - * @return array [code => name]
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.
264 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 + /**
434 + * Canonical country list — single source of truth used by every
435 + * country / nationality dropdown in both Free and Pro plugins.
436 + *
437 + * Returns the full ISO-3166-1 alpha-2 set (sovereign states +
438 + * dependent territories + commonly-accepted regions). Sorted
439 + * alphabetically by name so the rendered dropdown is browseable
440 + * without operators having to scan a code-ordered list.
441 + *
442 + * Filterable via `yatra_countries_list` for operators that want
443 + * to:
444 + * - Prepend "popular" entries (US, GB, IN, etc.) above an
445 + * `---` separator for faster picking
446 + * - Remove territories that don't apply to their market
447 + * - Rename a region (e.g. business-language preferences)
448 + *
449 + * @return array<string, string> [ISO-3166 code => display name]
450 + */
265 451 public static function getCountries(): array
266 452 {
267 - return self::COUNTRY_NAMES;
453 + $countries = self::countryNames();
454 +
455 + // Stable alphabetical sort by display name. Operators expect
456 + // "Argentina" before "Australia" before "Austria" — code-order
457 + // (AR/AU/AT) is computer-friendly but not human-friendly.
458 + asort($countries, SORT_STRING | SORT_FLAG_CASE);
459 +
460 + if (\function_exists('apply_filters')) {
461 + $filtered = apply_filters('yatra_countries_list', $countries);
462 + if (\is_array($filtered) && $filtered !== []) {
463 + return $filtered;
464 + }
465 + }
466 + return $countries;
268 467 }
269 468
270 469 /**
470 + * International dialing codes, keyed by ISO-3166-1 alpha-2 (digits only,
471 + * no leading "+"). Aligned to the {@see countryNames()} set. Multiple
472 + * countries legitimately share a code (all NANP countries use "1", RU/KZ
473 + * use "7", GB/GG/IM/JE use "44") — {@see dialingCodePriority()} resolves
474 + * which one an incoming "+code" auto-selects.
475 + *
476 + * @return array<string, string> [ISO code => dialing code]
477 + */
478 + private static function dialingCodes(): array
479 + {
480 + return [
481 + 'AF' => '93', 'AL' => '355', 'DZ' => '213', 'AD' => '376', 'AO' => '244', 'AI' => '1', 'AQ' => '672',
482 + 'AG' => '1', 'AR' => '54', 'AM' => '374', 'AW' => '297', 'AU' => '61', 'AT' => '43', 'AZ' => '994',
483 + 'BS' => '1', 'BH' => '973', 'BD' => '880', 'BB' => '1', 'BY' => '375', 'BE' => '32', 'BZ' => '501',
484 + 'BJ' => '229', 'BM' => '1', 'BT' => '975', 'BO' => '591', 'BQ' => '599', 'BA' => '387', 'BW' => '267',
485 + 'BR' => '55', 'IO' => '246', 'BN' => '673', 'BG' => '359', 'BF' => '226', 'BI' => '257', 'KH' => '855',
486 + 'CM' => '237', 'CA' => '1', 'CV' => '238', 'KY' => '1', 'CF' => '236', 'TD' => '235', 'CL' => '56',
487 + 'CN' => '86', 'CO' => '57', 'KM' => '269', 'CG' => '242', 'CD' => '243', 'CK' => '682', 'CR' => '506',
488 + 'CI' => '225', 'HR' => '385', 'CU' => '53', 'CW' => '599', 'CY' => '357', 'CZ' => '420', 'DK' => '45',
489 + 'DJ' => '253', 'DM' => '1', 'DO' => '1', 'EC' => '593', 'EG' => '20', 'SV' => '503', 'GQ' => '240',
490 + 'ER' => '291', 'EE' => '372', 'SZ' => '268', 'ET' => '251', 'FK' => '500', 'FO' => '298', 'FJ' => '679',
491 + 'FI' => '358', 'FR' => '33', 'GF' => '594', 'PF' => '689', 'GA' => '241', 'GM' => '220', 'GE' => '995',
492 + 'DE' => '49', 'GH' => '233', 'GI' => '350', 'GR' => '30', 'GL' => '299', 'GD' => '1', 'GP' => '590',
493 + 'GU' => '1', 'GT' => '502', 'GG' => '44', 'GN' => '224', 'GW' => '245', 'GY' => '592', 'HT' => '509',
494 + 'HN' => '504', 'HK' => '852', 'HU' => '36', 'IS' => '354', 'IN' => '91', 'ID' => '62', 'IR' => '98',
495 + 'IQ' => '964', 'IE' => '353', 'IM' => '44', 'IL' => '972', 'IT' => '39', 'JM' => '1', 'JP' => '81',
496 + 'JE' => '44', 'JO' => '962', 'KZ' => '7', 'KE' => '254', 'KI' => '686', 'KP' => '850', 'KR' => '82',
497 + 'KW' => '965', 'KG' => '996', 'LA' => '856', 'LV' => '371', 'LB' => '961', 'LS' => '266', 'LR' => '231',
498 + 'LY' => '218', 'LI' => '423', 'LT' => '370', 'LU' => '352', 'MO' => '853', 'MK' => '389', 'MG' => '261',
499 + 'MW' => '265', 'MY' => '60', 'MV' => '960', 'ML' => '223', 'MT' => '356', 'MH' => '692', 'MQ' => '596',
500 + 'MR' => '222', 'MU' => '230', 'YT' => '262', 'MX' => '52', 'FM' => '691', 'MD' => '373', 'MC' => '377',
501 + 'MN' => '976', 'ME' => '382', 'MS' => '1', 'MA' => '212', 'MZ' => '258', 'MM' => '95', 'NA' => '264',
502 + 'NR' => '674', 'NP' => '977', 'NL' => '31', 'NC' => '687', 'NZ' => '64', 'NI' => '505', 'NE' => '227',
503 + 'NG' => '234', 'NU' => '683', 'NF' => '672', 'MP' => '1', 'NO' => '47', 'OM' => '968', 'PK' => '92',
504 + 'PW' => '680', 'PS' => '970', 'PA' => '507', 'PG' => '675', 'PY' => '595', 'PE' => '51', 'PH' => '63',
505 + 'PN' => '64', 'PL' => '48', 'PT' => '351', 'PR' => '1', 'QA' => '974', 'RE' => '262', 'RO' => '40',
506 + 'RU' => '7', 'RW' => '250', 'BL' => '590', 'SH' => '290', 'KN' => '1', 'LC' => '1', 'MF' => '590',
507 + 'PM' => '508', 'VC' => '1', 'WS' => '685', 'SM' => '378', 'ST' => '239', 'SA' => '966', 'SN' => '221',
508 + 'RS' => '381', 'SC' => '248', 'SL' => '232', 'SG' => '65', 'SX' => '1', 'SK' => '421', 'SI' => '386',
509 + 'SB' => '677', 'SO' => '252', 'ZA' => '27', 'GS' => '500', 'SS' => '211', 'ES' => '34', 'LK' => '94',
510 + 'SD' => '249', 'SR' => '597', 'SJ' => '47', 'SE' => '46', 'CH' => '41', 'SY' => '963', 'TW' => '886',
511 + 'TJ' => '992', 'TZ' => '255', 'TH' => '66', 'TL' => '670', 'TG' => '228', 'TK' => '690', 'TO' => '676',
512 + 'TT' => '1', 'TN' => '216', 'TR' => '90', 'TM' => '993', 'TC' => '1', 'TV' => '688', 'UG' => '256',
513 + 'UA' => '380', 'AE' => '971', 'GB' => '44', 'US' => '1', 'UY' => '598', 'UZ' => '998', 'VU' => '678',
514 + 'VA' => '39', 'VE' => '58', 'VN' => '84', 'VG' => '1', 'VI' => '1', 'WF' => '681', 'EH' => '212',
515 + 'YE' => '967', 'ZM' => '260', 'ZW' => '263', 'XK' => '383', 'AS' => '1', 'AX' => '358',
516 + 'UM' => '1', 'CX' => '61', 'CC' => '61', 'TA' => '290',
517 + ];
518 + }
519 +
520 + /**
521 + * Preferred country for a dialing code that several countries share. When
522 + * an operator types a raw "+<code><number>" we must pick one country to
523 + * auto-select; this is the conventional primary (e.g. "1" => US, not one of
524 + * the ~20 other NANP countries). Codes not listed here fall back to the
525 + * first ISO that declares them.
526 + *
527 + * @return array<string, string> [dialing code => primary ISO]
528 + */
529 + private static function dialingCodePriority(): array
530 + {
531 + return [
532 + '1' => 'US', '7' => 'RU', '39' => 'IT', '44' => 'GB', '47' => 'NO', '61' => 'AU',
533 + '212' => 'MA', '262' => 'RE', '290' => 'SH', '358' => 'FI', '500' => 'FK',
534 + '590' => 'GP', '599' => 'CW', '64' => 'NZ', '672' => 'AQ',
535 + ];
536 + }
537 +
538 + /**
539 + * Preferred ISO per shared dialing code (dial => ISO), for the front-end
540 + * detector to resolve which country a "+code" auto-selects. Mirrors the
541 + * server-side {@see dialingCodePriority()}.
542 + *
543 + * @return array<string, string>
544 + */
545 + public static function getPhonePriority(): array
546 + {
547 + return self::dialingCodePriority();
548 + }
549 +
550 + /**
551 + * Dialing code (digits only, no "+") for one ISO code, or '' if unknown.
552 + */
553 + public static function getDialingCode(string $iso): string
554 + {
555 + $codes = self::dialingCodes();
556 + return $codes[strtoupper($iso)] ?? '';
557 + }
558 +
559 + /**
560 + * Country list for the phone-number field: ISO code, display name and
561 + * dialing code, sorted by display name. Filterable via
562 + * `yatra_phone_countries` (e.g. to pin popular countries to the top).
563 + *
564 + * @return array<int, array{iso:string, name:string, dial:string}>
565 + */
566 + public static function getPhoneCountries(): array
567 + {
568 + $names = self::getCountries(); // [iso => name], already sorted/filtered
569 + $dial = self::dialingCodes();
570 + $out = [];
571 + foreach ($names as $iso => $name) {
572 + $iso = strtoupper((string) $iso);
573 + if (!isset($dial[$iso]) || $dial[$iso] === '') {
574 + continue;
575 + }
576 + $out[] = ['iso' => $iso, 'name' => (string) $name, 'dial' => $dial[$iso]];
577 + }
578 + if (\function_exists('apply_filters')) {
579 + $filtered = apply_filters('yatra_phone_countries', $out);
580 + if (\is_array($filtered) && $filtered !== []) {
581 + return $filtered;
582 + }
583 + }
584 + return $out;
585 + }
586 +
587 + /**
588 + * Detect the country/dialing-code from a raw phone value. Only a value that
589 + * begins with "+" is treated as international; anything else returns null so
590 + * an existing bare national number is left untouched (backward compatible).
591 + *
592 + * Uses longest-prefix matching so "+9779806015400" → NP (977), not a shorter
593 + * false match, then applies {@see dialingCodePriority()} for shared codes.
594 + *
595 + * @return array{iso:string, dial:string, national:string}|null
596 + */
597 + public static function detectPhoneCountry(string $raw): ?array
598 + {
599 + $raw = trim($raw);
600 + if ($raw === '' || $raw[0] !== '+') {
601 + return null;
602 + }
603 + $digits = preg_replace('/\D+/', '', $raw);
604 + if ($digits === '' || $digits === null) {
605 + return null;
606 + }
607 + $codes = self::dialingCodes();
608 + $priority = self::dialingCodePriority();
609 + $maxLen = min(4, strlen($digits));
610 + for ($len = $maxLen; $len >= 1; $len--) {
611 + $prefix = substr($digits, 0, $len);
612 + $matches = array_keys($codes, $prefix, true);
613 + if ($matches === []) {
614 + continue;
615 + }
616 + $iso = $priority[$prefix] ?? (string) $matches[0];
617 + return [
618 + 'iso' => $iso,
619 + 'dial' => $prefix,
620 + 'national' => substr($digits, $len),
621 + ];
622 + }
623 + return null;
624 + }
625 +
626 + /**
627 + * Default country pre-selected in the phone field. Priority: the
628 + * `default_phone_country` setting → the site locale suffix (en_US → US,
629 + * ne_NP → NP) → "US". Filterable via `yatra_default_phone_country`.
630 + */
631 + public static function getDefaultPhoneCountry(): string
632 + {
633 + $codes = self::dialingCodes();
634 + $default = '';
635 +
636 + if (\class_exists(SettingsService::class)) {
637 + $setting = strtoupper((string) SettingsService::getString('default_phone_country', ''));
638 + if ($setting !== '' && isset($codes[$setting])) {
639 + $default = $setting;
640 + }
641 + }
642 + if ($default === '' && \function_exists('get_locale')) {
643 + if (preg_match('/_([A-Z]{2})$/', (string) get_locale(), $m) && isset($codes[$m[1]])) {
644 + $default = $m[1];
645 + }
646 + }
647 + if ($default === '') {
648 + $default = 'US';
649 + }
650 + if (\function_exists('apply_filters')) {
651 + $default = (string) apply_filters('yatra_default_phone_country', $default);
652 + }
653 + return isset($codes[$default]) ? $default : 'US';
654 + }
655 +
656 + /**
657 + * Combine a national number with a country (ISO) into the stored value.
658 + *
659 + * - Empty number → '' (nothing to store).
660 + * - Already-international ("+...") → returned unchanged (a pasted full number
661 + * or the no-JS fallback where the customer typed the "+code" themselves).
662 + * - National number + known ISO → "+<dial><digits>" (spaces/dashes stripped).
663 + * - No/unknown ISO → the number left as typed (legacy behavior, backward
664 + * compatible — nothing is invented).
665 + */
666 + public static function combineInternationalPhone(string $number, string $iso): string
667 + {
668 + $number = trim($number);
669 + if ($number === '') {
670 + return '';
671 + }
672 + if ($number[0] === '+') {
673 + return $number;
674 + }
675 + $iso = strtoupper(trim($iso));
676 + $dial = $iso !== '' ? self::getDialingCode($iso) : '';
677 + if ($dial === '') {
678 + return $number;
679 + }
680 + $digits = preg_replace('/\D+/', '', $number);
681 + if ($digits === '' || $digits === null) {
682 + return $number;
683 + }
684 + return '+' . $dial . $digits;
685 + }
686 +
687 + /**
688 + * Format a stored phone value for human display. A value saved with a
689 + * country code ("+9779806015400") is shown as "+977 9806015400"; a legacy
690 + * bare value is returned unchanged (never corrupted). Optionally accepts a
691 + * known ISO to disambiguate shared codes.
692 + */
693 + public static function formatPhoneForDisplay(string $stored, string $iso = ''): string
694 + {
695 + $stored = trim($stored);
696 + if ($stored === '') {
697 + return '';
698 + }
699 + $detected = self::detectPhoneCountry($stored);
700 + if ($detected === null) {
701 + return $stored; // legacy / national-only value — leave as-is
702 + }
703 + $national = $detected['national'] !== '' ? ' ' . $detected['national'] : '';
704 + return '+' . $detected['dial'] . $national;
705 + }
706 +
707 + /**
271 708 * Format phone number for display
272 709 *
273 710 * @param string $phone Phone number
274 711 * @return string Formatted phone
@@ -363,10 +800,20 @@
363 800 */
364 801 public static function formatRatingStars(float $rating, bool $showNumber = false): string
365 802 {
366 803 $rating = max(0, min(5, $rating));
367 - $fullStars = (int) floor($rating);
368 - $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 + }
369 816
370 817 $html = '<span class="yatra-rating-stars">';
371 818
372 819 for ($i = 1; $i <= 5; $i++) {