| @@ -192,10 +192,10 @@ | ||
| 192 | 192 | if (empty($data['slug'])) { |
| 193 | 193 | throw new \InvalidArgumentException('Trip slug is required'); |
| 194 | 194 | } |
| 195 | 195 | |
| 196 | - if (!preg_match('/^[a-z0-9-]+$/', $data['slug'])) { | |
| 197 | - throw new \InvalidArgumentException('Trip slug can only contain lowercase letters, numbers, and hyphens'); | |
| 196 | + if (!preg_match('/^[\pL\pN-]+$/u', $data['slug'])) { | |
| 197 | + throw new \InvalidArgumentException('Trip slug can only contain letters, numbers, and hyphens'); | |
| 198 | 198 | } |
| 199 | 199 | |
| 200 | 200 | // Check if slug is unique (exclude current trip when updating) |
| 201 | 201 | $existing = $this->repository->findBySlug($data['slug']); |
| @@ -275,10 +275,10 @@ | ||
| 275 | 275 | * Process before create |
| 276 | 276 | */ |
| 277 | 277 | protected function processBeforeCreate(array $data): array |
| 278 | 278 | { |
| 279 | - if (!preg_match('/^[a-z0-9-]+$/', $data['slug'])) { | |
| 280 | - throw new \InvalidArgumentException('Trip slug can only contain lowercase letters, numbers, and hyphens'); | |
| 279 | + if (!preg_match('/^[\pL\pN-]+$/u', $data['slug'])) { | |
| 280 | + throw new \InvalidArgumentException('Trip slug can only contain letters, numbers, and hyphens'); | |
| 281 | 281 | } |
| 282 | 282 | |
| 283 | 283 | if (empty($data['status'])) { |
| 284 | 284 | $data['status'] = 'draft'; |
| @@ -356,8 +356,13 @@ | ||
| 356 | 356 | |
| 357 | 357 | // Set updated_by |
| 358 | 358 | $data['updated_by'] = get_current_user_id(); |
| 359 | 359 | |
| 360 | + // Merge custom_fields: keep existing keys that the incoming payload doesn't touch | |
| 361 | + if (isset($data['custom_fields']) && is_array($data['custom_fields'])) { | |
| 362 | + $data['custom_fields'] = $this->mergeCustomFields($id, $data['custom_fields']); | |
| 363 | + } | |
| 364 | + | |
| 360 | 365 | // Process JSON fields |
| 361 | 366 | $data = $this->processJsonFields($data); |
| 362 | 367 | |
| 363 | 368 | // Ensure difficulty_level is stored as integer ID |
| @@ -368,8 +373,32 @@ | ||
| 368 | 373 | return $data; |
| 369 | 374 | } |
| 370 | 375 | |
| 371 | 376 | /** |
| 377 | + * Merge incoming custom_fields with what is already stored in the DB for a trip. | |
| 378 | + * Only keys present in the incoming array are changed; all other existing keys are | |
| 379 | + * kept intact. This is the "append, never override" contract the UI depends on. | |
| 380 | + */ | |
| 381 | + private function mergeCustomFields(int $id, array $incoming): array | |
| 382 | + { | |
| 383 | + $existing = $this->repository->find($id); | |
| 384 | + if (!$existing || empty($existing->custom_fields)) { | |
| 385 | + return $incoming; | |
| 386 | + } | |
| 387 | + | |
| 388 | + $storedRaw = $existing->custom_fields; | |
| 389 | + if (is_string($storedRaw)) { | |
| 390 | + $decoded = maybe_unserialize($storedRaw); | |
| 391 | + if (!is_array($decoded)) { | |
| 392 | + $decoded = json_decode($storedRaw, true); | |
| 393 | + } | |
| 394 | + $storedRaw = is_array($decoded) ? $decoded : []; | |
| 395 | + } | |
| 396 | + | |
| 397 | + return array_merge($storedRaw, $incoming); | |
| 398 | + } | |
| 399 | + | |
| 400 | + /** | |
| 372 | 401 | * Process JSON fields - serialize arrays |
| 373 | 402 | */ |
| 374 | 403 | private function processJsonFields(array $data): array |
| 375 | 404 | { |
| @@ -1232,9 +1261,14 @@ | ||
| 1232 | 1261 | * Count departures by date |
| 1233 | 1262 | */ |
| 1234 | 1263 | public function countDeparturesByDate(int $tripId, string $date): int |
| 1235 | 1264 | { |
| 1236 | - return $this->repository->countDeparturesByDate($tripId, $date); | |
| 1265 | + // The December 2025 service/controller refactor moved this query to | |
| 1266 | + // TripAvailabilityRepository::countAvailableDeparturesByDate() but left | |
| 1267 | + // this call pointing at TripRepository, where no such method exists — | |
| 1268 | + // so the storefront's date-pricing request (fired whenever a customer | |
| 1269 | + // picks a date) has returned a fatal 500 ever since. | |
| 1270 | + return $this->availabilityRepository->countAvailableDeparturesByDate($tripId, $date); | |
| 1237 | 1271 | } |
| 1238 | 1272 | |
| 1239 | 1273 | /** |
| 1240 | 1274 | * Get trip with availability |
| @@ -1251,8 +1285,49 @@ | ||
| 1251 | 1285 | * @return array ['min_price' => float, 'max_price' => float] |
| 1252 | 1286 | */ |
| 1253 | 1287 | public function getTripPriceRange(int $tripId): array |
| 1254 | 1288 | { |
| 1255 | - $tripPriceTypeRepository = new \Yatra\Repositories\TripPriceTypeRepository(); | |
| 1256 | - return $tripPriceTypeRepository->getPriceRangeByTripId($tripId); | |
| 1289 | + // Traveler-based pricing stores per-category prices in `trips.price_types` JSON. | |
| 1290 | + // The old implementation referenced a repository that may not exist in all builds; | |
| 1291 | + // compute the range directly from the persisted JSON so list endpoints can't fatal. | |
| 1292 | + $tripRepository = new \Yatra\Repositories\TripRepository(); | |
| 1293 | + $priceTypes = $tripRepository->getPriceTypes($tripId); | |
| 1294 | + | |
| 1295 | + $min = PHP_FLOAT_MAX; | |
| 1296 | + $max = 0.0; | |
| 1297 | + | |
| 1298 | + foreach ($priceTypes as $pt) { | |
| 1299 | + if (!is_array($pt)) { | |
| 1300 | + continue; | |
| 1301 | + } | |
| 1302 | + | |
| 1303 | + $discounted = isset($pt['discounted_price']) ? (float) $pt['discounted_price'] : 0.0; | |
| 1304 | + $sale = isset($pt['sale_price']) ? (float) $pt['sale_price'] : 0.0; | |
| 1305 | + $original = isset($pt['original_price']) ? (float) $pt['original_price'] : 0.0; | |
| 1306 | + $legacyPrice = isset($pt['price']) ? (float) $pt['price'] : 0.0; | |
| 1307 | + | |
| 1308 | + $effective = 0.0; | |
| 1309 | + if ($discounted > 0) { | |
| 1310 | + $effective = $discounted; | |
| 1311 | + } elseif ($sale > 0) { | |
| 1312 | + $effective = $sale; | |
| 1313 | + } elseif ($original > 0) { | |
| 1314 | + $effective = $original; | |
| 1315 | + } elseif ($legacyPrice > 0) { | |
| 1316 | + $effective = $legacyPrice; | |
| 1317 | + } | |
| 1318 | + | |
| 1319 | + if ($effective <= 0) { | |
| 1320 | + continue; | |
| 1321 | + } | |
| 1322 | + | |
| 1323 | + $min = min($min, $effective); | |
| 1324 | + $max = max($max, $effective); | |
| 1325 | + } | |
| 1326 | + | |
| 1327 | + if ($min === PHP_FLOAT_MAX) { | |
| 1328 | + return ['min_price' => 0.0, 'max_price' => 0.0]; | |
| 1329 | + } | |
| 1330 | + | |
| 1331 | + return ['min_price' => (float) $min, 'max_price' => (float) $max]; | |
| 1257 | 1332 | } |
| 1258 | 1333 | } |