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/Services/DiscountService.php +109 -31 3.0.2.9 → 3.0.16 View file →
@@ -40,9 +40,9 @@
40 40 // Check both is_group_discount=1 OR discount_mode IN ('group', 'both') for backward compatibility
41 41 $query = $wpdb->prepare(
42 42 "SELECT * FROM `{$table}`
43 43 WHERE (is_group_discount = 1 OR discount_mode IN ('group', 'both'))
44 - AND status = 'publish'
44 + AND status IN ('publish', 'active')
45 45 AND (valid_from IS NULL OR valid_from <= %s)
46 46 AND (expiry_date IS NULL OR expiry_date >= %s)
47 47 ORDER BY created_at DESC",
48 48 $today,
@@ -53,17 +53,34 @@
53 53
54 54 // Filter by trip_ids in PHP since it's stored as serialized array
55 55 $filtered = [];
56 56 foreach ($results as $discount) {
57 - // If applicable_to is 'all', include it
58 - if (empty($discount->trip_ids) || $discount->applicable_to === 'all') {
57 + $applicable = (string) ($discount->applicable_to ?? 'all');
58 + if ($applicable === '' || $applicable === 'all') {
59 59 $filtered[] = $discount;
60 60 continue;
61 61 }
62 -
63 - // If applicable_to is 'specific_trips', check if trip is in the list
64 - $trip_ids = maybe_unserialize($discount->trip_ids);
65 - if (is_array($trip_ids) && in_array($tripId, array_map('intval', $trip_ids), true)) {
62 + if ($applicable !== 'specific_trips') {
63 + continue;
64 + }
65 +
66 + // specific_trips: include only when this trip is in the configured list
67 + $rawIds = $discount->trip_ids;
68 + $trip_ids = [];
69 + if (is_string($rawIds) && $rawIds !== '') {
70 + $t = trim($rawIds);
71 + if ($t !== '' && ($t[0] === '[' || $t[0] === '{')) {
72 + $decoded = json_decode($t, true);
73 + $trip_ids = is_array($decoded) ? $decoded : [];
74 + } else {
75 + $unser = maybe_unserialize($rawIds);
76 + $trip_ids = is_array($unser) ? $unser : array_map('trim', explode(',', $t));
77 + }
78 + } elseif (is_array($rawIds)) {
79 + $trip_ids = $rawIds;
80 + }
81 + $trip_ids = array_values(array_unique(array_map('absint', $trip_ids)));
82 + if (in_array($tripId, $trip_ids, true)) {
66 83 $filtered[] = $discount;
67 84 }
68 85 }
69 86
@@ -86,9 +103,9 @@
86 103 if (isset($data['type']) && !in_array($data['type'], $allowed_types, true)) {
87 104 throw new \InvalidArgumentException('Invalid discount type. Must be one of: ' . implode(', ', $allowed_types));
88 105 }
89 106
90 - $allowed_statuses = ['draft', 'publish', 'trash'];
107 + $allowed_statuses = ['draft', 'publish', 'trash', 'expired'];
91 108 if (isset($data['status']) && !in_array($data['status'], $allowed_statuses, true)) {
92 109 throw new \InvalidArgumentException('Invalid status. Must be one of: ' . implode(', ', $allowed_statuses));
93 110 }
94 111
@@ -153,9 +170,9 @@
153 170 $data['expiry_date'] = null;
154 171 }
155 172
156 173 if (isset($data['status'])) {
157 - $allowed_statuses = ['draft', 'publish', 'trash'];
174 + $allowed_statuses = ['draft', 'publish', 'trash', 'expired'];
158 175 $data['status'] = in_array($data['status'], $allowed_statuses, true) ? $data['status'] : 'draft';
159 176 } else {
160 177 $data['status'] = 'draft';
161 178 }
@@ -404,9 +421,9 @@
404 421 $data['expiry_date'] = null;
405 422 }
406 423
407 424 if (isset($data['status'])) {
408 - $allowed_statuses = ['draft', 'publish', 'trash'];
425 + $allowed_statuses = ['draft', 'publish', 'trash', 'expired'];
409 426 $data['status'] = in_array($data['status'], $allowed_statuses, true) ? $data['status'] : 'draft';
410 427 }
411 428
412 429 if (isset($data['applicable_to'])) {
@@ -851,12 +868,16 @@
851 868 */
852 869 private function formatDiscountAmount($discount): string {
853 870 $amount = $discount->group_discount_amount ?? 0;
854 871 if ($discount->group_discount_type === 'percentage') {
855 - return "{$amount}% off";
856 - } else {
857 - return "$" . number_format((float) $amount, 2) . " off";
872 + /* translators: %s: discount percentage. */
873 + return sprintf(__('%s%% off', 'yatra'), $amount);
858 874 }
875 +
876 + // Was a hardcoded "$" with default separators, so every non-dollar site
877 + // showed the wrong currency (and the label could not be translated).
878 + /* translators: %s: discount amount, already formatted with the site currency. */
879 + return sprintf(__('%s off', 'yatra'), yatra_format_price((float) $amount, null, false));
859 880 }
860 881
861 882 /**
862 883 * @param array $travelerCounts Array of category_id => count (e.g., ['3' => 4, '5' => 1])
@@ -881,19 +902,32 @@
881 902
882 903
883 904 $totalTravelers = array_sum(array_map('intval', $travelerCounts));
884 905
885 - // Build price lookup by category_id
906 + // Build price + price-type lookup by category_id
886 907 $priceByCategory = [];
908 + $ptByCategory = [];
887 909 foreach ($priceTypes as $pt) {
888 - $pt = (object) $pt;
889 - $categoryId = $pt->category_id ?? null;
910 + $pt = (array) $pt;
911 + $categoryId = $pt['category_id'] ?? null;
890 912 if ($categoryId !== null) {
891 - $priceByCategory[$categoryId] = (float) ($pt->effective_price ?? $pt->sale_price ?? $pt->original_price ?? 0);
913 + $priceByCategory[$categoryId] = (float) ($pt['effective_price'] ?? $pt['sale_price'] ?? $pt['original_price'] ?? 0);
914 + $ptByCategory[$categoryId] = $pt;
892 915 }
893 916 }
894 917
918 + // Effective subtotal for a category — delegate to the single source of
919 + // truth so the discount base ALWAYS matches CalculationService's charge
920 + // (per-person × count, flat per-group, or per-block group pricing).
921 + $catSubtotal = function ($categoryId, $count) use ($priceByCategory, $ptByCategory): float {
922 + return \Yatra\Services\TripPricingService::categoryLineSubtotal(
923 + $ptByCategory[$categoryId] ?? [],
924 + (int) $count,
925 + (float) ($priceByCategory[$categoryId] ?? 0)
926 + );
927 + };
895 928
929 +
896 930 foreach ($groupDiscounts as $discount) {
897 931 $discountMode = $discount->discount_mode ?? 'total';
898 932
899 933 // Category-based discounts: check each category's count and apply to that category's subtotal
@@ -900,9 +934,9 @@
900 934 if ($discountMode === 'category_based' && !empty($discount->category_discounts)) {
901 935 $totalDiscountAmount = 0;
902 936 $appliedCategories = [];
903 937
904 - $categoryDiscounts = is_array($discount->category_discounts) ? $discount->category_discounts : unserialize($discount->category_discounts);
938 + $categoryDiscounts = $this->decodeStoredList($discount->category_discounts ?? null);
905 939 foreach ($categoryDiscounts as $catDiscount) {
906 940 $catDiscount = (object) $catDiscount;
907 941 $categoryId = $catDiscount->traveler_category_id ?? null;
908 942 if ($categoryId === null) continue;
@@ -912,9 +946,9 @@
912 946 if ($categoryCount <= 0) continue;
913 947
914 948 // Check if this category's count falls within any range
915 949 if (!empty($catDiscount->ranges)) {
916 - $ranges = is_array($catDiscount->ranges) ? $catDiscount->ranges : unserialize($catDiscount->ranges);
950 + $ranges = $this->decodeStoredList($catDiscount->ranges ?? null);
917 951 foreach ($ranges as $range) {
918 952 $range = (object) $range;
919 953 $minSize = (int) ($range->min_group_size ?? 0);
920 954 $maxSize = !empty($range->max_group_size) ? (int) $range->max_group_size : PHP_INT_MAX;
@@ -923,10 +957,10 @@
923 957 $discountType = $range->discount_type ?? 'percentage';
924 958 $discountValue = (float) ($range->discount_amount ?? 0);
925 959
926 960 // Calculate discount for this category's subtotal only
927 - $categoryPrice = $priceByCategory[$categoryId] ?? 0;
928 - $categorySubtotal = $categoryPrice * $categoryCount;
961 + // (flat for per-group, price × count for per-person).
962 + $categorySubtotal = $catSubtotal($categoryId, $categoryCount);
929 963
930 964 if ($discountType === 'percentage') {
931 965 $categoryDiscount = $categorySubtotal * ($discountValue / 100);
932 966 } else {
@@ -953,10 +987,12 @@
953 987 $discountInfo = '';
954 988 if (!empty($appliedCategories)) {
955 989 $firstCat = $appliedCategories[0];
956 990 if ($firstCat['discount_type'] === 'percentage') {
991 + /* translators: %s: discount percentage value. */
957 992 $discountInfo = sprintf(__('Group Discount (%s%%)', 'yatra'), $firstCat['discount_value']);
958 993 } else {
994 + /* translators: %s: formatted discount amount. */
959 995 $discountInfo = sprintf(__('Group Discount (%s)', 'yatra'), yatra_format_price($firstCat['discount_value']));
960 996 }
961 997 } else {
962 998 $discountInfo = __('Group Discount', 'yatra');
@@ -972,9 +1008,9 @@
972 1008 }
973 1009 }
974 1010 // Total-based discounts: check total travelers and apply to total
975 1011 elseif (!empty($discount->group_discount_ranges)) {
976 - $groupDiscountRanges = is_array($discount->group_discount_ranges) ? $discount->group_discount_ranges : unserialize($discount->group_discount_ranges);
1012 + $groupDiscountRanges = $this->decodeStoredList($discount->group_discount_ranges ?? null);
977 1013 foreach ($groupDiscountRanges as $range) {
978 1014 $range = (object) $range;
979 1015 $minSize = (int) ($range->min_group_size ?? 0);
980 1016 $maxSize = !empty($range->max_group_size) ? (int) $range->max_group_size : PHP_INT_MAX;
@@ -985,10 +1021,9 @@
985 1021
986 1022 // Calculate total subtotal from all categories
987 1023 $totalSubtotal = 0;
988 1024 foreach ($travelerCounts as $catId => $count) {
989 - $categoryPrice = $priceByCategory[$catId] ?? 0;
990 - $totalSubtotal += $categoryPrice * $count;
1025 + $totalSubtotal += $catSubtotal($catId, $count);
991 1026 }
992 1027
993 1028 // Calculate the actual discount amount
994 1029 $calculatedAmount = $discountType === 'percentage'
@@ -999,10 +1034,12 @@
999 1034 'type' => $discountType,
1000 1035 'value' => $discountValue,
1001 1036 'amount' => round($calculatedAmount, 2),
1002 1037 'code' => $discount->code ?? null,
1003 - 'label' => $discountType === 'percentage'
1038 + 'label' => $discountType === 'percentage'
1039 + /* translators: %s: discount percentage value. */
1004 1040 ? sprintf(__('Group Discount (%s%%)', 'yatra'), $discountValue)
1041 + /* translators: %s: formatted discount amount. */
1005 1042 : sprintf(__('Group Discount (%s)', 'yatra'), yatra_format_price($discountValue)),
1006 1043 ];
1007 1044 }
1008 1045 }
@@ -1026,10 +1063,9 @@
1026 1063
1027 1064 // Calculate total subtotal from all categories
1028 1065 $totalSubtotal = 0;
1029 1066 foreach ($travelerCounts as $catId => $count) {
1030 - $categoryPrice = $priceByCategory[$catId] ?? 0;
1031 - $totalSubtotal += $categoryPrice * $count;
1067 + $totalSubtotal += $catSubtotal($catId, $count);
1032 1068 }
1033 1069
1034 1070 // Calculate the actual discount amount
1035 1071 $calculatedAmount = $discountType === 'percentage'
@@ -1040,10 +1076,12 @@
1040 1076 'type' => $discountType,
1041 1077 'value' => $discountValue,
1042 1078 'amount' => round($calculatedAmount, 2),
1043 1079 'code' => $discount->code ?? null,
1044 - 'label' => $discountType === 'percentage'
1080 + 'label' => $discountType === 'percentage'
1081 + /* translators: %s: discount percentage value. */
1045 1082 ? sprintf(__('Group Discount (%s%%)', 'yatra'), $discountValue)
1083 + /* translators: %s: formatted discount amount. */
1046 1084 : sprintf(__('Group Discount (%s)', 'yatra'), yatra_format_price($discountValue)),
1047 1085 ];
1048 1086 }
1049 1087 }
@@ -1050,9 +1088,9 @@
1050 1088 }
1051 1089
1052 1090 return null;
1053 1091 }
1054 -
1092 +
1055 1093 /**
1056 1094 * Calculate coupon discount for booking
1057 1095 *
1058 1096 * @param string $coupon_code Coupon code to apply
@@ -1116,9 +1154,10 @@
1116 1154 'code' => $coupon_code,
1117 1155 'type' => $discount->type,
1118 1156 'amount' => (float) $discount->amount,
1119 1157 'calculated_amount' => round($calculated_discount, 2),
1120 - 'label' => $discount->type === 'percentage'
1158 + 'label' => $discount->type === 'percentage'
1159 + /* translators: %s: discount percentage value. */
1121 1160 ? sprintf(__('Coupon (%s%%)', 'yatra'), $discount->amount)
1122 1161 : __('Coupon Discount', 'yatra'),
1123 1162 ];
1124 1163 }
@@ -1123,8 +1162,46 @@
1123 1162 ];
1124 1163 }
1125 1164
1126 1165 /**
1166 + * Decode list-shaped discount DB fields: JSON (current storage), array, or legacy PHP serialized.
1167 + *
1168 + * @param mixed $raw
1169 + * @return array<int|string, mixed>
1170 + */
1171 + private function decodeStoredList($raw): array
1172 + {
1173 + if ($raw === null || $raw === '') {
1174 + return [];
1175 + }
1176 + if (is_array($raw)) {
1177 + return $raw;
1178 + }
1179 + if (is_object($raw)) {
1180 + $asArray = json_decode(wp_json_encode($raw), true);
1181 +
1182 + return is_array($asArray) ? $asArray : [];
1183 + }
1184 + if (!is_string($raw)) {
1185 + return [];
1186 + }
1187 + $trimmed = trim($raw);
1188 + if ($trimmed === '') {
1189 + return [];
1190 + }
1191 + $first = $trimmed[0];
1192 + if ($first === '[' || $first === '{') {
1193 + $decoded = json_decode($trimmed, true);
1194 +
1195 + return is_array($decoded) ? $decoded : [];
1196 + }
1197 +
1198 + $maybe = maybe_unserialize($trimmed);
1199 +
1200 + return is_array($maybe) ? $maybe : [];
1201 + }
1202 +
1203 + /**
1127 1204 * Validate coupon for booking
1128 1205 *
1129 1206 * @param \stdClass $discount Discount object
1130 1207 * @param int $trip_id Trip ID
@@ -1160,10 +1237,11 @@
1160 1237
1161 1238 // Check minimum amount
1162 1239 if (!empty($discount->min_amount) && $total < (float) $discount->min_amount) {
1163 1240 return [
1164 - 'valid' => false,
1241 + 'valid' => false,
1165 1242 'message' => sprintf(
1243 + /* translators: %s: formatted minimum amount. */
1166 1244 __('Minimum amount of %s required for this coupon.', 'yatra'),
1167 1245 yatra_format_price((float) $discount->min_amount)
1168 1246 )
1169 1247 ];