| @@ -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 | ]; |