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