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/Controllers/TripController.php +169 -31 3.0.3 → 3.0.16 View file →
@@ -61,23 +61,23 @@
61 61 register_rest_route($namespace, '/' . $base, [
62 62 [
63 63 'methods' => \WP_REST_Server::READABLE,
64 64 'callback' => [$this, 'get_items'],
65 - 'permission_callback' => [$this, 'check_read_permission'],
65 + 'permission_callback' => [$this, 'check_view_permission'],
66 66 ],
67 67 [
68 68 'methods' => \WP_REST_Server::CREATABLE,
69 69 'callback' => [$this, 'create_item'],
70 - 'permission_callback' => [$this, 'check_permission'],
70 + 'permission_callback' => [$this, 'check_create_permission'],
71 71 ],
72 72 ]);
73 73
74 - // Duplicate trip: POST /trips/{id}/duplicate
74 + // Duplicate is a create — produces a new trip row.
75 75 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/duplicate', [
76 76 [
77 77 'methods' => \WP_REST_Server::CREATABLE,
78 78 'callback' => [$this, 'duplicate_item'],
79 - 'permission_callback' => [$this, 'check_permission'],
79 + 'permission_callback' => [$this, 'check_create_permission'],
80 80 ],
81 81 ]);
82 82
83 83 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)', [
@@ -83,46 +83,54 @@
83 83 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)', [
84 84 [
85 85 'methods' => \WP_REST_Server::READABLE,
86 86 'callback' => [$this, 'get_item'],
87 - 'permission_callback' => [$this, 'check_read_permission'],
87 + 'permission_callback' => [$this, 'check_view_permission'],
88 88 ],
89 89 [
90 + // EDITABLE covers both content edits AND publish/unpublish
91 + // state changes (the React form sends both via PUT).
92 + // We accept either edit OR publish cap — handlers should
93 + // refuse to change `status` when the user holds only the
94 + // edit cap, but the route gate lets both through.
90 95 'methods' => \WP_REST_Server::EDITABLE,
91 96 'callback' => [$this, 'update_item'],
92 - 'permission_callback' => [$this, 'check_permission'],
97 + 'permission_callback' => [$this, 'check_edit_or_publish_permission'],
93 98 ],
94 99 [
100 + // Soft-delete (trash) → edit cap. Trash is reversible
101 + // and is the day-to-day "remove from catalogue" action.
95 102 'methods' => \WP_REST_Server::DELETABLE,
96 103 'callback' => [$this, 'delete_item'],
97 - 'permission_callback' => [$this, 'check_permission'],
104 + 'permission_callback' => [$this, 'check_edit_permission'],
98 105 ],
99 106 ]);
100 107
101 - // Permanent delete endpoint
108 + // Permanent delete — bypasses trash. High-sensitivity action,
109 + // gated on the dedicated delete cap.
102 110 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/permanent-delete', [
103 111 [
104 112 'methods' => \WP_REST_Server::DELETABLE,
105 113 'callback' => [$this, 'permanent_delete_item'],
106 - 'permission_callback' => [$this, 'check_permission'],
114 + 'permission_callback' => [$this, 'check_delete_permission'],
107 115 ],
108 116 ]);
109 117
110 - // Search endpoint
118 + // Search endpoint — view cap.
111 119 register_rest_route($namespace, '/' . $base . '/search', [
112 120 [
113 121 'methods' => \WP_REST_Server::READABLE,
114 122 'callback' => [$this, 'search_items'],
115 - 'permission_callback' => [$this, 'check_read_permission'],
123 + 'permission_callback' => [$this, 'check_view_permission'],
116 124 ],
117 125 ]);
118 126
119 - // Revisions endpoints
127 + // Revisions list — view cap.
120 128 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/revisions', [
121 129 [
122 130 'methods' => \WP_REST_Server::READABLE,
123 131 'callback' => [$this, 'get_revisions'],
124 - 'permission_callback' => [$this, 'check_read_permission'],
132 + 'permission_callback' => [$this, 'check_view_permission'],
125 133 ],
126 134 ]);
127 135
128 136 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/revisions/(?P<revision_id>[\d]+)', [
@@ -128,14 +136,15 @@
128 136 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/revisions/(?P<revision_id>[\d]+)', [
129 137 [
130 138 'methods' => \WP_REST_Server::READABLE,
131 139 'callback' => [$this, 'get_revision'],
132 - 'permission_callback' => [$this, 'check_read_permission'],
140 + 'permission_callback' => [$this, 'check_view_permission'],
133 141 ],
134 142 [
143 + // Restoring a revision overwrites the live trip → edit cap.
135 144 'methods' => \WP_REST_Server::EDITABLE,
136 145 'callback' => [$this, 'restore_revision'],
137 - 'permission_callback' => [$this, 'check_permission'],
146 + 'permission_callback' => [$this, 'check_edit_permission'],
138 147 ],
139 148 ]);
140 149
141 150 // Availability template endpoint (public, no auth required)
@@ -165,34 +174,35 @@
165 174 'permission_callback' => '__return_true', // Public endpoint
166 175 ],
167 176 ]);
168 177
169 - // Status statistics for admin views
178 + // Status statistics for admin views — view cap.
170 179 register_rest_route($namespace, '/' . $base . '/stats', [
171 180 [
172 181 'methods' => \WP_REST_Server::READABLE,
173 182 'callback' => [$this, 'getStats'],
174 - 'permission_callback' => [$this, 'check_permission'],
183 + 'permission_callback' => [$this, 'check_view_permission'],
175 184 ],
176 185 ]);
177 186
178 - // Trip attributes endpoints (admin only — never expose unauthenticated read/write)
187 + // Test endpoint — view cap (read-only diagnostic).
179 188 register_rest_route($namespace, '/' . $base . '/test', [
180 189 'methods' => \WP_REST_Server::READABLE,
181 190 'callback' => [$this, 'test_endpoint'],
182 - 'permission_callback' => [$this, 'check_permission'],
191 + 'permission_callback' => [$this, 'check_view_permission'],
183 192 ]);
184 193
194 + // Trip-attribute assignments — trip-taxonomy edits go here.
185 195 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/attributes', [
186 196 [
187 197 'methods' => \WP_REST_Server::READABLE,
188 198 'callback' => [$this, 'get_trip_attributes'],
189 - 'permission_callback' => [$this, 'check_read_permission'],
199 + 'permission_callback' => [$this, 'check_view_permission'],
190 200 ],
191 201 [
192 202 'methods' => \WP_REST_Server::CREATABLE,
193 203 'callback' => [$this, 'update_trip_attributes'],
194 - 'permission_callback' => [$this, 'check_permission'],
204 + 'permission_callback' => [$this, 'check_taxonomy_permission'],
195 205 ],
196 206 ]);
197 207
198 208 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/attributes/(?P<attribute_id>[\d]+)', [
@@ -198,14 +208,58 @@
198 208 register_rest_route($namespace, '/' . $base . '/(?P<id>[\d]+)/attributes/(?P<attribute_id>[\d]+)', [
199 209 [
200 210 'methods' => \WP_REST_Server::DELETABLE,
201 211 'callback' => [$this, 'delete_trip_attribute'],
202 - 'permission_callback' => [$this, 'check_permission'],
212 + 'permission_callback' => [$this, 'check_taxonomy_permission'],
203 213 ],
204 214 ]);
205 215 }
206 216
207 217 /**
218 + * Granular cap checks for every Trip endpoint. Overrides the
219 + * BaseController defaults (which gate everything on `manage_options`
220 + * and locked out every yatra_* role from the trips REST surface).
221 + * WP admins pass via the Team module's admin-fallback filter.
222 + */
223 + public function check_view_permission(?WP_REST_Request $request = null): bool
224 + {
225 + return current_user_can('yatra_view_trips');
226 + }
227 +
228 + public function check_create_permission(?WP_REST_Request $request = null): bool
229 + {
230 + return current_user_can('yatra_create_trips');
231 + }
232 +
233 + public function check_edit_permission(?WP_REST_Request $request = null): bool
234 + {
235 + return current_user_can('yatra_edit_trips');
236 + }
237 +
238 + /**
239 + * EDITABLE / PUT routes that may carry either a content edit or a
240 + * status change pass when the caller holds EITHER cap. The actual
241 + * handler should refuse to change `status` when only `edit` is
242 + * held — that's a future hardening, but the route gate already
243 + * keeps non-trip-staff out.
244 + */
245 + public function check_edit_or_publish_permission(?WP_REST_Request $request = null): bool
246 + {
247 + return current_user_can('yatra_edit_trips')
248 + || current_user_can('yatra_publish_trips');
249 + }
250 +
251 + public function check_delete_permission(?WP_REST_Request $request = null): bool
252 + {
253 + return current_user_can('yatra_delete_trips');
254 + }
255 +
256 + public function check_taxonomy_permission(?WP_REST_Request $request = null): bool
257 + {
258 + return current_user_can('yatra_manage_trip_taxonomies');
259 + }
260 +
261 + /**
208 262 * Get statistics for admin trip views (status counts)
209 263 */
210 264 public function getStats(WP_REST_Request $request)
211 265 {
@@ -888,10 +942,15 @@
888 942 */
889 943 public function restore_revision(WP_REST_Request $request)
890 944 {
891 945 try {
892 - // Check permissions
893 - if (!current_user_can('yatra_edit_trips')) {
946 + // Check permissions — admin fallback ensures site owners
947 + // always pass even when the Team module isn't active and
948 + // the yatra_edit_trips cap isn't on the admin role.
949 + if (
950 + !current_user_can('manage_options')
951 + && !current_user_can('yatra_edit_trips')
952 + ) {
894 953 return $this->error_response(__('You do not have permission to restore revisions', 'yatra'), 403);
895 954 }
896 955
897 956 $id = (int) $request->get_param('id');
@@ -998,8 +1057,9 @@
998 1057 'transportation_included',
999 1058 'international_flights_included',
1000 1059 'domestic_flights_included',
1001 1060 'is_featured',
1061 + 'has_default_time_slots',
1002 1062 ];
1003 1063
1004 1064 foreach ($booleanFields as $field) {
1005 1065 if (isset($data[$field])) {
@@ -1269,8 +1329,20 @@
1269 1329 $images = is_array($decoded) ? $decoded : [];
1270 1330 }
1271 1331 }
1272 1332
1333 + // Decode gallery JSON column on the entry so the React form
1334 + // can re-populate the gallery picker without an extra fetch.
1335 + $gallery = [];
1336 + if (isset($entry->gallery)) {
1337 + if (is_array($entry->gallery)) {
1338 + $gallery = $entry->gallery;
1339 + } elseif (is_string($entry->gallery) && $entry->gallery !== '') {
1340 + $decoded = json_decode($entry->gallery, true);
1341 + $gallery = is_array($decoded) ? $decoded : [];
1342 + }
1343 + }
1344 +
1273 1345 return [
1274 1346 'id' => isset($entry->id) ? (int) $entry->id : null,
1275 1347 'day_id' => isset($entry->day_id) ? (int) $entry->day_id : null,
1276 1348 'time' => $entry->time ?? '',
@@ -1279,8 +1351,16 @@
1279 1351 'time_type' => $entry->time_type ?? 'exact',
1280 1352 'title' => $entry->title ?? '',
1281 1353 'description' => $entry->description ?? '',
1282 1354 'location' => $entry->location ?? '',
1355 + // The entries table has lat/lng/gallery/video_url + an `order`
1356 + // smallint column — but until this serializer included them, the
1357 + // /trips/{id} response never carried them. The React activity
1358 + // load mapper sorts by `entry.order`; without it, every entry
1359 + // arrived with order=null, the sort fell through to id-order,
1360 + // and drag-sort reorders never appeared to persist on reload.
1361 + 'location_latitude' => isset($entry->location_latitude) ? $entry->location_latitude : null,
1362 + 'location_longitude' => isset($entry->location_longitude) ? $entry->location_longitude : null,
1283 1363 'duration' => $entry->duration ?? '',
1284 1364 'cost' => isset($entry->cost) ? (float) $entry->cost : null,
1285 1365 'cost_per_person' => isset($entry->cost_per_person) ? (bool) $entry->cost_per_person : false,
1286 1366 'notes' => $entry->notes ?? '',
@@ -1286,8 +1366,11 @@
1286 1366 'notes' => $entry->notes ?? '',
1287 1367 'item_type_id' => isset($entry->item_type_id) ? (int) $entry->item_type_id : null,
1288 1368 'item_id' => isset($entry->item_id) ? (int) $entry->item_id : null,
1289 1369 'status' => $entry->status ?? 'active',
1370 + 'order' => isset($entry->order) ? (int) $entry->order : 0,
1371 + 'gallery' => $gallery,
1372 + 'video_url' => $entry->video_url ?? '',
1290 1373 'created_at' => $entry->created_at ?? '',
1291 1374 'updated_at' => $entry->updated_at ?? '',
1292 1375 'included_items' => $includedItems,
1293 1376 'excluded_items' => $excludedItems,
@@ -1432,13 +1515,15 @@
1432 1515
1433 1516 // Fetch availability dates using centralized resolution service
1434 1517 $resolutionService = new \Yatra\Services\AvailabilityResolutionService();
1435 1518
1436 - // Always show all dates from today onwards (selected_date is only for highlighting)
1519 + // Always show all dates from today onwards (selected_date is only for
1520 + // highlighting), up to the configurable booking horizon (Settings →
1521 + // Booking; 12 months unless changed — the previous hard-coded value).
1437 1522 $fromDate = date('Y-m-d');
1438 - $toDate = date('Y-m-d', strtotime('+12 months'));
1523 + $toDate = yatra_get_availability_horizon_date($fromDate);
1439 1524
1440 - $availability_dates = $resolutionService->getAllAvailabilityDates($id, $fromDate, $toDate);
1525 + $availability_dates = $resolutionService->getAllAvailabilityDates($id, $fromDate, $toDate, \Yatra\Services\SettingsService::isEnabled('show_sold_out'));
1441 1526
1442 1527 // Determine if this is a day trip
1443 1528 $is_single_day = ($trip->duration_days ?? 1) <= 1;
1444 1529
@@ -1510,8 +1595,11 @@
1510 1595 'discounted_price' => isset($trip->discounted_price) ? (float) $trip->discounted_price : 0,
1511 1596 'sale_price' => isset($trip->sale_price) ? (float) $trip->sale_price : 0,
1512 1597 'currency' => SettingsService::getCurrency(),
1513 1598 'duration_days' => isset($trip->duration_days) ? (int) $trip->duration_days : 1,
1599 + // Hour-based day tours show "8 Hours" instead of "1 Day" on the
1600 + // availability cards. 0 for every existing (day-based) trip.
1601 + 'duration_hours' => isset($trip->duration_hours) ? (int) $trip->duration_hours : 0,
1514 1602 'max_travelers' => isset($trip->max_travelers) ? (int) $trip->max_travelers : 20,
1515 1603 'min_travelers' => isset($trip->min_travelers) ? (int) $trip->min_travelers : 1,
1516 1604 'pricing_type' => $trip->pricing_type ?? 'regular',
1517 1605 'price_types' => $trip->price_types ?? [], // Include price_types for traveler-based pricing
@@ -1651,8 +1739,36 @@
1651 1739
1652 1740 // Build cards from real availability data or use sample data
1653 1741 $availability_cards = [];
1654 1742 $month_filters = [];
1743 +
1744 + // Availability priority (same as the resolver):
1745 + // 1) manual availability dates, 2) recurring rules, 3) trip defaults.
1746 + // For UI counts + filters we want the list to reflect that priority (not a mixed set).
1747 + $availability_dates_for_render = $has_availability ? $trip_data->availability_dates : [];
1748 + if ($has_availability) {
1749 + $by_source = [
1750 + 'availability_date' => [],
1751 + 'recurring_rule' => [],
1752 + 'trip_default' => [],
1753 + ];
1754 + foreach ($trip_data->availability_dates as $a) {
1755 + if (!is_object($a)) {
1756 + continue;
1757 + }
1758 + $src = strtolower(trim((string) ($a->source ?? '')));
1759 + if (isset($by_source[$src])) {
1760 + $by_source[$src][] = $a;
1761 + }
1762 + }
1763 + if (!empty($by_source['availability_date'])) {
1764 + $availability_dates_for_render = $by_source['availability_date'];
1765 + } elseif (!empty($by_source['recurring_rule'])) {
1766 + $availability_dates_for_render = $by_source['recurring_rule'];
1767 + } elseif (!empty($by_source['trip_default'])) {
1768 + $availability_dates_for_render = $by_source['trip_default'];
1769 + }
1770 + }
1655 1771
1656 1772 // Determine if this is a day trip (duration <= 1 day)
1657 1773 $is_single_day = ($trip_data->duration_days ?? 1) <= 1;
1658 1774
@@ -1691,9 +1807,9 @@
1691 1807 $add_category_ids($trip_data->price_types);
1692 1808 }
1693 1809
1694 1810 if ($has_availability) {
1695 - foreach ($trip_data->availability_dates as $avail_for_cats) {
1811 + foreach ($availability_dates_for_render as $avail_for_cats) {
1696 1812 if (!empty($avail_for_cats->price_types)) {
1697 1813 $add_category_ids($avail_for_cats->price_types);
1698 1814 }
1699 1815 if (!empty($avail_for_cats->traveler_pricing)) {
@@ -1724,8 +1840,9 @@
1724 1840 'age_min' => isset($meta['age_min']) ? (int) $meta['age_min'] : null,
1725 1841 'age_max' => isset($meta['age_max']) ? (int) $meta['age_max'] : null,
1726 1842 'min_pax' => isset($meta['min_pax']) ? (int) $meta['min_pax'] : null,
1727 1843 'max_pax' => isset($meta['max_pax']) ? (int) $meta['max_pax'] : null,
1844 + 'group_overflow' => isset($meta['group_overflow']) && in_array($meta['group_overflow'], ['block', 'per_block'], true) ? $meta['group_overflow'] : 'block',
1728 1845 ];
1729 1846 }
1730 1847 }
1731 1848
@@ -1775,8 +1892,9 @@
1775 1892 if (!isset($pt['age_min'])) $pt['age_min'] = $meta['age_min'];
1776 1893 if (!isset($pt['age_max'])) $pt['age_max'] = $meta['age_max'];
1777 1894 if (!isset($pt['min_pax'])) $pt['min_pax'] = $meta['min_pax'];
1778 1895 if (!isset($pt['max_pax'])) $pt['max_pax'] = $meta['max_pax'];
1896 + $pt['group_overflow'] = $meta['group_overflow'] ?? 'block';
1779 1897 }
1780 1898
1781 1899 // Payable amount (honors price / sale_price / discounted_price like TripPricingService)
1782 1900 if (!isset($pt['effective_price'])) {
@@ -1812,9 +1930,9 @@
1812 1930
1813 1931 if ($has_availability) {
1814 1932 $current_time = time();
1815 1933
1816 - foreach ($trip_data->availability_dates as $avail) {
1934 + foreach ($availability_dates_for_render as $avail) {
1817 1935 if (empty($avail->departure_date)) {
1818 1936 // Skip entries without a valid departure date
1819 1937 continue;
1820 1938 }
@@ -1892,9 +2010,9 @@
1892 2010
1893 2011 // Use month-based filters for both day trips and multi-day trips for better navigation
1894 2012 // This prevents overwhelming users with too many individual date filters
1895 2013 $month_key = strtolower(date('M-Y', $departure_date));
1896 - $month_filters[$month_key] = date('M Y', $departure_date);
2014 + $month_filters[$month_key] = date_i18n('M Y', $departure_date);
1897 2015
1898 2016 $from_location = !empty($avail->from_location) ? $avail->from_location : ($trip_data->starting_location ?? '');
1899 2017 $to_location = !empty($avail->to_location) ? $avail->to_location : ($trip_data->ending_location ?? $from_location);
1900 2018
@@ -1922,8 +2040,22 @@
1922 2040 $from_label = __('Departure', 'yatra');
1923 2041 $to_label = __('Return', 'yatra');
1924 2042 }
1925 2043
2044 + // Per-card duration: derive from THIS card's departure→return span
2045 + // so the displayed "X Days" always matches the departure/return
2046 + // dates shown on the same card. When no custom arrival is stored,
2047 + // $return_date is departure + (duration_days - 1), so the span
2048 + // equals the trip's duration_days (no visible change). Only when an
2049 + // operator stored an arrival that disagrees with the trip default
2050 + // does this diverge — and then the customer sees a self-consistent
2051 + // card (e.g. "10 Days" over a Jun 25 → Jul 04 span) instead of a
2052 + // "9 Days" badge contradicting the dates. round() (not floor())
2053 + // absorbs any ±1h DST drift between two local-midnight timestamps.
2054 + $card_duration_days = $is_single_day
2055 + ? max(1, (int) ($trip_data->duration_days ?? 1))
2056 + : max(1, (int) round(($return_date - $departure_date) / DAY_IN_SECONDS) + 1);
2057 +
1926 2058 // Use month-based keys for filtering for both day trips and multi-day trips
1927 2059 $filter_key = strtolower(date('M-Y', $departure_date));
1928 2060
1929 2061 // Must match {@see TripPricingService::resolveCardPricing}: trip-level mode wins; do not
@@ -1943,8 +2075,9 @@
1943 2075 'to_label' => $to_label,
1944 2076 'to_date' => $to_display,
1945 2077 'to_location' => $to_location,
1946 2078 'date_display' => $date_display, // For day trips: "Saturday, 30 Nov 2025"
2079 + 'duration_days' => $card_duration_days, // Inclusive span of THIS card's dates
1947 2080 'date' => $avail->departure_date, // Raw date for dynamic pricing
1948 2081 'spots_remaining' => $seats, // For dynamic pricing
1949 2082 'seats' => $seats > 10 ? '10+' : (string) $seats,
1950 2083 'seats_available' => $seats,
@@ -1967,8 +2100,10 @@
1967 2100 'is_sold_out' => $is_sold_out,
1968 2101 // Card-specific pricing
1969 2102 'pricing_type' => $card_pricing_type,
1970 2103 'traveler_pricing' => $card_traveler_pricing,
2104 + 'price_unit' => $cardPricing['price_unit'] ?? 'per_person',
2105 + 'price_unit_label' => $cardPricing['price_unit_label'] ?? __('per person', 'yatra'),
1971 2106 'is_recurring' => !empty($avail->is_recurring),
1972 2107 'rule_id' => $avail->rule_id ?? null,
1973 2108 ] + $dp_card_fields;
1974 2109 }
@@ -2051,9 +2186,9 @@
2051 2186 'is_recurring' => false,
2052 2187 'rule_id' => null,
2053 2188 ] + $sample_dp_fields,
2054 2189 ];
2055 - $month_filters[strtolower(date('M-Y', strtotime('+7 days')))] = date('M Y', strtotime('+7 days'));
2190 + $month_filters[strtolower(date('M-Y', strtotime('+7 days')))] = date_i18n('M Y', strtotime('+7 days'));
2056 2191 }
2057 2192
2058 2193 $sorted_cards = $this->sortAvailabilityCards($availability_cards, $sort_key);
2059 2194
@@ -2131,8 +2266,9 @@
2131 2266
2132 2267 if ($dynamic_pricing_enabled && $B > $eps && $F > $B + $eps) {
2133 2268 $p = (int) round((($F - $B) / $B) * 100);
2134 2269
2270 + /* translators: %d: dynamic pricing increase percentage. */
2135 2271 return $p > 0 ? sprintf(__('+%d%%', 'yatra'), $p) : '';
2136 2272 }
2137 2273
2138 2274 if ($O > $eps && $F < $O - $eps) {
@@ -2137,8 +2273,9 @@
2137 2273
2138 2274 if ($O > $eps && $F < $O - $eps) {
2139 2275 $p = (int) round((($O - $F) / $O) * 100);
2140 2276
2277 + /* translators: %d: discount percentage. */
2141 2278 return $p > 0 ? sprintf(__('%d%% OFF', 'yatra'), $p) : '';
2142 2279 }
2143 2280
2144 2281 if ($O <= $eps && $B > $eps && $F < $B - $eps) {
@@ -2143,8 +2280,9 @@
2143 2280
2144 2281 if ($O <= $eps && $B > $eps && $F < $B - $eps) {
2145 2282 $p = (int) round((($B - $F) / $B) * 100);
2146 2283
2284 + /* translators: %d: discount percentage. */
2147 2285 return $p > 0 ? sprintf(__('%d%% OFF', 'yatra'), $p) : '';
2148 2286 }
2149 2287
2150 2288 return '';