repuve-backend-v1/app/Http/Controllers/Repuve/PackageController.php
Juan Felipe Zapata Moreno 31746867b8 feat: Agrega validaciones de autorización y nuevas clases Request
- Se agregó autorización basada en permisos en múltiples Requests.
- Nuevos Requests para motivos de cancelación y tags con validación y autorización.
- Se añadieron métodos de roles al modelo User (isDeveloper, isAdmin, isPrimary).
- Se actualizó el acceso a Telescope usando validación por roles.
- Mejora en el manejo de excepciones de autorización.
- Actualización de RoleSeeder con nuevas convenciones de permisos.
- Actualización de dependencias (composer.lock).
2026-02-23 13:05:53 -06:00

471 lines
19 KiB
PHP

<?php
namespace App\Http\Controllers\Repuve;
use Notsoweb\ApiResponse\Enums\ApiResponse;
use App\Http\Controllers\Controller;
use App\Http\Requests\Repuve\PackageStoreRequest;
use App\Http\Requests\Repuve\PackageUpdateRequest;
use App\Models\CatalogTagStatus;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\QueryException;
use App\Models\Package;
use App\Models\Tag;
use Illuminate\Routing\Controllers\HasMiddleware;
class PackageController extends Controller implements HasMiddleware
{
/**
* Middleware
*/
public static function middleware(): array
{
return [
self::can('packages.index', ['index']),
self::can('packages.show', ['show']),
self::can('packages.destroy', ['destroy']),
self::can('packages.box_tags', ['getBoxTags']),
];
}
public function index(Request $request)
{
try {
// Si NO hay filtro de caja, no cargar las relaciones de tags para optimizar
$shouldLoadTags = $request->filled('caja') || $request->filled('box_number');
$packages = Package::query();
if ($shouldLoadTags) {
$packages->with([
'tags:id,folio,tag_number,package_id,status_id,vehicle_id,module_id',
'tags.status:id,code,name',
'tags.vehicle:id,placa,niv',
'tags.module:id,name',
'user:id,name,username'
]);
} else {
$packages->with('user:id,name,username');
}
$packages->withCount('tags')->orderBy('id', 'ASC');
if ($request->filled('lote') || $request->filled('lot')) {
$loteValue = $request->input('lote') ?? $request->input('lot');
$packages->where('lot', 'LIKE', '%' . trim($loteValue) . '%');
}
if ($request->filled('caja') || $request->filled('box_number')) {
$cajaValue = $request->input('caja') ?? $request->input('box_number');
$packages->where('box_number', 'LIKE', '%' . trim($cajaValue) . '%');
}
$paginatedPackages = $packages->paginate(config('app.pagination'));
// Validación si no hay resultados
if ($paginatedPackages->isEmpty()) {
return ApiResponse::NOT_FOUND->response([
'message' => 'No se encontraron paquetes con los criterios de búsqueda proporcionados.',
'filters_applied' => [
'lote' => $request->input('lote') ?? $request->input('lot'),
'caja' => $request->input('caja') ?? $request->input('box_number'),
]
]);
}
// Si hay filtro de caja, incluir estadísticas de tags
if ($request->filled('caja') || $request->filled('box_number')) {
$paginatedPackages->getCollection()->transform(function ($package) {
return [
'id' => $package->id,
'lot' => $package->lot,
'box_number' => $package->box_number,
'starting_page' => $package->starting_page,
'ending_page' => $package->ending_page,
'created_by' => $package->user ? [
'id' => $package->user->id,
'name' => $package->user->name,
'username' => $package->user->username,
] : null,
'estadisticas' => [
'total' => $package->tags->count(),
'available' => $package->tags->filter(function($tag) {
return $tag->status && $tag->status->code === 'available';
})->count(),
'assigned' => $package->tags->filter(function($tag) {
return $tag->status && $tag->status->code === 'assigned';
})->count(),
'cancelled' => $package->tags->filter(function($tag) {
return $tag->status && $tag->status->code === 'cancelled';
})->count(),
],
'tags' => $package->tags->map(function ($tag) {
return [
'id' => $tag->id,
'folio' => $tag->folio,
'tag_number' => $tag->tag_number,
'status' => $tag->status ? [
'id' => $tag->status->id,
'code' => $tag->status->code,
'name' => $tag->status->name,
] : null,
'vehicle' => $tag->vehicle ? [
'id' => $tag->vehicle->id,
'placa' => $tag->vehicle->placa,
'niv' => $tag->vehicle->niv,
] : null,
'module' => $tag->module ? [
'id' => $tag->module->id,
'name' => $tag->module->name,
] : null,
];
}),
];
});
}
return ApiResponse::OK->response([
'Paquetes' => $paginatedPackages,
]);
} catch (\Exception $e) {
return ApiResponse::INTERNAL_ERROR->response([
'message' => 'Error al obtener los paquetes',
'error' => $e->getMessage(),
]);
}
}
public function store(PackageStoreRequest $request)
{
try {
DB::beginTransaction();
// Verificar folios en el mismo lote antes de crear
$packageIds = Package::where('lot', $request->lot)->pluck('id');
$conflicting = Tag::whereIn('package_id', $packageIds)
->whereRaw('CAST(folio AS UNSIGNED) BETWEEN ? AND ?', [$request->starting_page, $request->ending_page])
->pluck('folio');
if ($conflicting->isNotEmpty()) {
DB::rollBack();
return ApiResponse::UNPROCESSABLE_CONTENT->response([
'starting_page' => [
'Los folios ' . $conflicting->join(', ') . ' ya están registrados en el lote "' . $request->lot . '".',
],
]);
}
$form = $request->validated();
$form['user_id'] = Auth::id();
$package = Package::create($form);
$statusAvailable = CatalogTagStatus::where('code', 'available')->firstOrFail();
for ($page = $request->starting_page; $page <= $request->ending_page; $page++) {
Tag::create([
'folio' => $page,
'tag_number' => null,
'package_id' => $package->id,
'status_id' => $statusAvailable->id,
]);
}
DB::commit();
return ApiResponse::CREATED->response([
'message' => 'Paquete registrado exitosamente con sus tags',
'package' => $package->load('tags'),
'tags_created' => $package->tags()->count(),
]);
} catch (\Exception $e) {
DB::rollBack();
return ApiResponse::INTERNAL_ERROR->response([
'message' => 'Error al crear el paquete',
'error' => $e->getMessage(),
]);
}
}
public function show($id)
{
try {
$package = Package::with(['tags'])->findOrFail($id);
return ApiResponse::OK->response([
'package' => $package,
]);
} catch (ModelNotFoundException $e) {
return ApiResponse::NOT_FOUND->response([
'message' => 'Paquete no encontrado.',
]);
} catch (\Exception $e) {
return ApiResponse::INTERNAL_ERROR->response([
'message' => 'Error al obtener el paquete',
'error' => $e->getMessage(),
]);
}
}
public function update(PackageUpdateRequest $request, $id)
{
try {
$package = Package::with('tags')->findOrFail($id);
$validated = $request->validated();
// Validar si el paquete tiene tags asignados
$hasTags = $package->tags()->count() > 0;
// Si tiene tags, validar que no se cambien los rangos de páginas
if ($hasTags) {
if (isset($validated['starting_page']) && $validated['starting_page'] != $package->starting_page) {
return ApiResponse::BAD_REQUEST->response([
'message' => 'No se puede cambiar el rango inicial porque el paquete ya tiene tags asignados.',
'current_starting_page' => $package->starting_page,
'requested_starting_page' => $validated['starting_page'],
'tags_count' => $package->tags()->count(),
]);
}
if (isset($validated['ending_page']) && $validated['ending_page'] != $package->ending_page) {
return ApiResponse::BAD_REQUEST->response([
'message' => 'No se puede cambiar el rango final porque el paquete ya tiene tags asignados.',
'current_ending_page' => $package->ending_page,
'requested_ending_page' => $validated['ending_page'],
'tags_count' => $package->tags()->count(),
]);
}
}
// Validar que la combinación de lote + caja sea única
if (isset($validated['lot']) || isset($validated['box_number'])) {
$newLot = $validated['lot'] ?? $package->lot;
$newBoxNumber = $validated['box_number'] ?? $package->box_number;
$existingPackage = Package::where('lot', $newLot)
->where('box_number', $newBoxNumber)
->where('id', '!=', $package->id)
->first();
if ($existingPackage) {
return ApiResponse::BAD_REQUEST->response([
'message' => "Ya existe otro paquete con el lote '{$newLot}' y caja '{$newBoxNumber}'.",
'existing_package_id' => $existingPackage->id,
'current_lot' => $package->lot,
'current_box_number' => $package->box_number,
'requested_lot' => $newLot,
'requested_box_number' => $newBoxNumber,
]);
}
}
DB::beginTransaction();
// Guardar valores anteriores para el log
$changes = [];
foreach ($validated as $key => $value) {
if ($package->$key != $value) {
$changes[$key] = [
'old' => $package->$key,
'new' => $value,
];
}
}
// Si no hay cambios
if (empty($changes)) {
return ApiResponse::OK->response([
'message' => 'No se realizaron cambios en el paquete.',
'package' => [
'id' => $package->id,
'lot' => $package->lot,
'box_number' => $package->box_number,
'starting_page' => $package->starting_page,
'ending_page' => $package->ending_page,
],
]);
}
$package->update($validated);
DB::commit();
return ApiResponse::OK->response([
'message' => 'Paquete actualizado exitosamente',
'package' => [
'id' => $package->id,
'lot' => $package->lot,
'box_number' => $package->box_number,
'starting_page' => $package->starting_page,
'ending_page' => $package->ending_page,
'updated_at' => $package->updated_at->format('Y-m-d H:i:s'),
],
'changes' => $changes,
]);
} catch (QueryException $e) {
DB::rollBack();
if ($e->getCode() == 23000 && str_contains($e->getMessage(), 'packages_lot_box_unique')) {
return ApiResponse::BAD_REQUEST->response([
'message' => 'Ya existe un paquete con esa combinación de lote y caja.',
'error' => $e->getMessage(),
]);
}
return ApiResponse::NOT_FOUND->response([
'message' => 'Error de base de datos al actualizar el paquete',
'error' => $e->getMessage(),
]);
} catch (\Exception $e) {
DB::rollBack();
return ApiResponse::NOT_FOUND->response([
'message' => 'Error al actualizar el paquete',
'error' => $e->getMessage(),
]);
}
}
public function destroy($id)
{
try {
DB::beginTransaction();
$package = Package::findOrFail($id);
if ($package->tags()->count() > 0) {
return ApiResponse::BAD_REQUEST->response([
'message' => 'No se puede eliminar el paquete porque tiene tags asociados.',
'tags_count' => $package->tags()->count(),
]);
}
$package->delete();
DB::commit();
return ApiResponse::OK->response([
'message' => 'Paquete eliminado exitosamente.',
]);
} catch (\Exception $e) {
DB::rollBack();
return ApiResponse::INTERNAL_ERROR->response([
'message' => 'Error al eliminar el paquete.',
'error' => $e->getMessage(),
]);
}
}
/**
* Obtener tags de una caja específica con paginación
*/
public function getBoxTags(Request $request)
{
try {
// Si no se envían parámetros, obtener el primer paquete
if (!$request->has('lot') && !$request->has('box_number')) {
$package = Package::with('user')->orderBy('id', 'DESC')->first();
if (!$package) {
return ApiResponse::NOT_FOUND->response([
'message' => 'No se encontraron paquetes en el sistema.',
]);
}
} else {
// Validar parámetros si se proporcionan
$validated = $request->validate([
'lot' => 'required|string',
'box_number' => 'required|string|max:255',
]);
$lot = $validated['lot'];
$boxNumber = $validated['box_number'];
// Buscar el paquete
$package = Package::with('user')->where('lot', $lot)
->where('box_number', $boxNumber)
->first();
if (!$package) {
return ApiResponse::NOT_FOUND->response([
'message' => 'No se encontró un paquete con el lote y caja especificados.',
'lot' => $lot,
'box_number' => $boxNumber,
]);
}
}
// Obtener tags con paginación
$tags = Tag::with(['status:id,code,name', 'vehicle:id,placa,niv', 'module:id,name', 'cancellationLogs.cancellationReason'])
->where('package_id', $package->id)
->orderBy('id', 'ASC');
// Filtro adicional por status si se proporciona
if ($request->filled('status')) {
$tags->whereHas('status', function ($q) use ($request) {
$q->where('code', $request->input('status'));
});
}
if($request->filled('module_id')) {
$tags->where('module_id', $request->input('module_id'));
}
$paginatedTags = $tags->paginate($request->input('per_page', 25));
// Transformar tags
$paginatedTags->getCollection()->transform(function ($tag) {
return [
'id' => $tag->id,
'folio' => $tag->folio,
'tag_number' => $tag->tag_number,
'status' => [
'id' => $tag->status->id,
'code' => $tag->status->code,
'name' => $tag->status->name,
],
'vehicle' => $tag->vehicle ? [
'id' => $tag->vehicle->id,
'placa' => $tag->vehicle->placa,
'niv' => $tag->vehicle->niv,
] : null,
'module' => $tag->module ? [
'id' => $tag->module->id,
'name' => $tag->module->name,
] : null,
'cancellation_reason' => in_array($tag->status?->code, ['cancelled', 'damaged']) && $tag->cancellationLogs->first() ? [
'reason' => $tag->cancellationLogs->first()->cancellationReason?->name,
'observations' => $tag->cancellationLogs->first()->cancellation_observations,
'cancelled_at' => $tag->cancellationLogs->first()->cancellation_at,
] : null,
];
});
return ApiResponse::OK->response([
'package' => [
'id' => $package->id,
'lot' => $package->lot,
'box_number' => $package->box_number,
'starting_page' => $package->starting_page,
'ending_page' => $package->ending_page,
'created_by' => $package->user ? [
'id' => $package->user->id,
'name' => $package->user->name,
'username' => $package->user->username,
] : null,
],
'tags' => $paginatedTags,
]);
} catch (\Exception $e) {
return ApiResponse::INTERNAL_ERROR->response([
'message' => 'Error al obtener los tags de la caja.',
'error' => $e->getMessage(),
]);
}
}
}