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 +112 -65 3.0.2.8 → 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,48 +902,41 @@
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 -
895 - // Debug: Log traveler counts and total
896 - if (WP_DEBUG && WP_DEBUG_LOG) {
897 - error_log('Yatra Debug - Group Discount Calculation: ' . print_r([
898 - 'total_travelers' => $totalTravelers,
899 - 'traveler_counts' => $travelerCounts,
900 - 'price_by_category' => $priceByCategory
901 - ], true));
902 - }
903 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 + };
928 +
929 +
904 930 foreach ($groupDiscounts as $discount) {
905 931 $discountMode = $discount->discount_mode ?? 'total';
906 -
907 - // Debug: Log discount details
908 - if (WP_DEBUG && WP_DEBUG_LOG) {
909 - error_log('Yatra Debug - Processing discount: ' . print_r([
910 - 'discount_code' => $discount->code ?? 'unknown',
911 - 'discount_mode' => $discountMode,
912 - 'discount_amount' => $discount->amount ?? $discount->discount_amount ?? 'not_set',
913 - 'discount_type' => $discount->type ?? $discount->discount_type ?? 'not_set',
914 - 'category_discounts' => !empty($discount->category_discounts),
915 - 'group_discount_ranges' => !empty($discount->group_discount_ranges)
916 - ], true));
917 - }
918 -
932 +
919 933 // Category-based discounts: check each category's count and apply to that category's subtotal
920 934 if ($discountMode === 'category_based' && !empty($discount->category_discounts)) {
921 935 $totalDiscountAmount = 0;
922 936 $appliedCategories = [];
923 937
924 - $categoryDiscounts = is_array($discount->category_discounts) ? $discount->category_discounts : unserialize($discount->category_discounts);
938 + $categoryDiscounts = $this->decodeStoredList($discount->category_discounts ?? null);
925 939 foreach ($categoryDiscounts as $catDiscount) {
926 940 $catDiscount = (object) $catDiscount;
927 941 $categoryId = $catDiscount->traveler_category_id ?? null;
928 942 if ($categoryId === null) continue;
@@ -932,9 +946,9 @@
932 946 if ($categoryCount <= 0) continue;
933 947
934 948 // Check if this category's count falls within any range
935 949 if (!empty($catDiscount->ranges)) {
936 - $ranges = is_array($catDiscount->ranges) ? $catDiscount->ranges : unserialize($catDiscount->ranges);
950 + $ranges = $this->decodeStoredList($catDiscount->ranges ?? null);
937 951 foreach ($ranges as $range) {
938 952 $range = (object) $range;
939 953 $minSize = (int) ($range->min_group_size ?? 0);
940 954 $maxSize = !empty($range->max_group_size) ? (int) $range->max_group_size : PHP_INT_MAX;
@@ -943,10 +957,10 @@
943 957 $discountType = $range->discount_type ?? 'percentage';
944 958 $discountValue = (float) ($range->discount_amount ?? 0);
945 959
946 960 // Calculate discount for this category's subtotal only
947 - $categoryPrice = $priceByCategory[$categoryId] ?? 0;
948 - $categorySubtotal = $categoryPrice * $categoryCount;
961 + // (flat for per-group, price × count for per-person).
962 + $categorySubtotal = $catSubtotal($categoryId, $categoryCount);
949 963
950 964 if ($discountType === 'percentage') {
951 965 $categoryDiscount = $categorySubtotal * ($discountValue / 100);
952 966 } else {
@@ -973,10 +987,12 @@
973 987 $discountInfo = '';
974 988 if (!empty($appliedCategories)) {
975 989 $firstCat = $appliedCategories[0];
976 990 if ($firstCat['discount_type'] === 'percentage') {
991 + /* translators: %s: discount percentage value. */
977 992 $discountInfo = sprintf(__('Group Discount (%s%%)', 'yatra'), $firstCat['discount_value']);
978 993 } else {
994 + /* translators: %s: formatted discount amount. */
979 995 $discountInfo = sprintf(__('Group Discount (%s)', 'yatra'), yatra_format_price($firstCat['discount_value']));
980 996 }
981 997 } else {
982 998 $discountInfo = __('Group Discount', 'yatra');
@@ -992,9 +1008,9 @@
992 1008 }
993 1009 }
994 1010 // Total-based discounts: check total travelers and apply to total
995 1011 elseif (!empty($discount->group_discount_ranges)) {
996 - $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);
997 1013 foreach ($groupDiscountRanges as $range) {
998 1014 $range = (object) $range;
999 1015 $minSize = (int) ($range->min_group_size ?? 0);
1000 1016 $maxSize = !empty($range->max_group_size) ? (int) $range->max_group_size : PHP_INT_MAX;
@@ -1005,10 +1021,9 @@
1005 1021
1006 1022 // Calculate total subtotal from all categories
1007 1023 $totalSubtotal = 0;
1008 1024 foreach ($travelerCounts as $catId => $count) {
1009 - $categoryPrice = $priceByCategory[$catId] ?? 0;
1010 - $totalSubtotal += $categoryPrice * $count;
1025 + $totalSubtotal += $catSubtotal($catId, $count);
1011 1026 }
1012 1027
1013 1028 // Calculate the actual discount amount
1014 1029 $calculatedAmount = $discountType === 'percentage'
@@ -1019,10 +1034,12 @@
1019 1034 'type' => $discountType,
1020 1035 'value' => $discountValue,
1021 1036 'amount' => round($calculatedAmount, 2),
1022 1037 'code' => $discount->code ?? null,
1023 - 'label' => $discountType === 'percentage'
1038 + 'label' => $discountType === 'percentage'
1039 + /* translators: %s: discount percentage value. */
1024 1040 ? sprintf(__('Group Discount (%s%%)', 'yatra'), $discountValue)
1041 + /* translators: %s: formatted discount amount. */
1025 1042 : sprintf(__('Group Discount (%s)', 'yatra'), yatra_format_price($discountValue)),
1026 1043 ];
1027 1044 }
1028 1045 }
@@ -1040,27 +1057,15 @@
1040 1057 // Handle different field names for discount type and value
1041 1058 $discountType = $discount->type ?? $discount->discount_type ?? 'percentage';
1042 1059 $discountValue = (float) ($discount->amount ?? $discount->discount_amount ?? 0);
1043 1060
1044 - // Debug: Log legacy format processing
1045 - if (WP_DEBUG && WP_DEBUG_LOG) {
1046 - error_log('Yatra Debug - Legacy format processing: ' . print_r([
1047 - 'min_size' => $minSize,
1048 - 'max_size' => $maxSize,
1049 - 'total_travelers' => $totalTravelers,
1050 - 'discount_type' => $discountType,
1051 - 'discount_value' => $discountValue,
1052 - 'meets_criteria' => $totalTravelers >= $minSize && $totalTravelers <= $maxSize
1053 - ], true));
1054 - }
1055 -
1061 +
1056 1062 if ($totalTravelers >= $minSize && $totalTravelers <= $maxSize) {
1057 1063
1058 1064 // Calculate total subtotal from all categories
1059 1065 $totalSubtotal = 0;
1060 1066 foreach ($travelerCounts as $catId => $count) {
1061 - $categoryPrice = $priceByCategory[$catId] ?? 0;
1062 - $totalSubtotal += $categoryPrice * $count;
1067 + $totalSubtotal += $catSubtotal($catId, $count);
1063 1068 }
1064 1069
1065 1070 // Calculate the actual discount amount
1066 1071 $calculatedAmount = $discountType === 'percentage'
@@ -1071,10 +1076,12 @@
1071 1076 'type' => $discountType,
1072 1077 'value' => $discountValue,
1073 1078 'amount' => round($calculatedAmount, 2),
1074 1079 'code' => $discount->code ?? null,
1075 - 'label' => $discountType === 'percentage'
1080 + 'label' => $discountType === 'percentage'
1081 + /* translators: %s: discount percentage value. */
1076 1082 ? sprintf(__('Group Discount (%s%%)', 'yatra'), $discountValue)
1083 + /* translators: %s: formatted discount amount. */
1077 1084 : sprintf(__('Group Discount (%s)', 'yatra'), yatra_format_price($discountValue)),
1078 1085 ];
1079 1086 }
1080 1087 }
@@ -1081,9 +1088,9 @@
1081 1088 }
1082 1089
1083 1090 return null;
1084 1091 }
1085 -
1092 +
1086 1093 /**
1087 1094 * Calculate coupon discount for booking
1088 1095 *
1089 1096 * @param string $coupon_code Coupon code to apply
@@ -1147,9 +1154,10 @@
1147 1154 'code' => $coupon_code,
1148 1155 'type' => $discount->type,
1149 1156 'amount' => (float) $discount->amount,
1150 1157 'calculated_amount' => round($calculated_discount, 2),
1151 - 'label' => $discount->type === 'percentage'
1158 + 'label' => $discount->type === 'percentage'
1159 + /* translators: %s: discount percentage value. */
1152 1160 ? sprintf(__('Coupon (%s%%)', 'yatra'), $discount->amount)
1153 1161 : __('Coupon Discount', 'yatra'),
1154 1162 ];
1155 1163 }
@@ -1154,8 +1162,46 @@
1154 1162 ];
1155 1163 }
1156 1164
1157 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 + /**
1158 1204 * Validate coupon for booking
1159 1205 *
1160 1206 * @param \stdClass $discount Discount object
1161 1207 * @param int $trip_id Trip ID
@@ -1191,10 +1237,11 @@
1191 1237
1192 1238 // Check minimum amount
1193 1239 if (!empty($discount->min_amount) && $total < (float) $discount->min_amount) {
1194 1240 return [
1195 - 'valid' => false,
1241 + 'valid' => false,
1196 1242 'message' => sprintf(
1243 + /* translators: %s: formatted minimum amount. */
1197 1244 __('Minimum amount of %s required for this coupon.', 'yatra'),
1198 1245 yatra_format_price((float) $discount->min_amount)
1199 1246 )
1200 1247 ];