From abfa2fe1fd983092068a5b59b50dbf74b1b41863 Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Tue, 4 Nov 2025 23:29:34 -0600 Subject: [PATCH] ADD: Clientes y ventas --- .../Controllers/Netbien/ClientController.php | 59 ++++++ .../Netbien/PackagesController.php | 14 +- .../Controllers/Netbien/SaleController.php | 187 ++++++++++++++++++ .../Controllers/Netbien/SimCardController.php | 33 +++- .../Requests/Netbien/ClientStoreRequest.php | 47 +++++ .../Requests/Netbien/ClientUpdateRequest.php | 47 +++++ .../Requests/Netbien/PackagesStoreRequest.php | 7 +- .../Netbien/PackagesUpdateRequest.php | 10 +- .../Requests/Netbien/SaleStoreRequest.php | 58 ++++++ .../Requests/Netbien/SaleUpdateRequest.php | 41 ++++ .../Requests/Netbien/SimCardStoreRequest.php | 4 + ...uest.php => SimCardUpdateRequest copy.php} | 1 - app/Models/Client.php | 42 ++++ app/Models/ClientSim.php | 36 ++++ app/Models/PackSim.php | 6 + app/Models/Packages.php | 26 +++ app/Models/Sale.php | 34 ++++ app/Models/SaleItem.php | 38 ++++ app/Models/SimCard.php | 27 +++ ...025_11_04_132448_create_packages_table.php | 4 +- ..._add_history_fields_to_pack_sims_table.php | 30 +++ ...2025_11_04_215818_create_clients_table.php | 32 +++ ..._11_04_215846_create_client_sims_table.php | 32 +++ .../2025_11_04_221603_create_sales_table.php | 31 +++ ...5_11_04_221707_create_sale_items_table.php | 30 +++ routes/api.php | 6 + 26 files changed, 860 insertions(+), 22 deletions(-) create mode 100644 app/Http/Controllers/Netbien/ClientController.php create mode 100644 app/Http/Controllers/Netbien/SaleController.php create mode 100644 app/Http/Requests/Netbien/ClientStoreRequest.php create mode 100644 app/Http/Requests/Netbien/ClientUpdateRequest.php create mode 100644 app/Http/Requests/Netbien/SaleStoreRequest.php create mode 100644 app/Http/Requests/Netbien/SaleUpdateRequest.php rename app/Http/Requests/Netbien/{SimCardUpdateRequest.php => SimCardUpdateRequest copy.php} (92%) create mode 100644 app/Models/Client.php create mode 100644 app/Models/ClientSim.php create mode 100644 app/Models/Sale.php create mode 100644 app/Models/SaleItem.php create mode 100644 database/migrations/2025_11_04_204413_add_history_fields_to_pack_sims_table.php create mode 100644 database/migrations/2025_11_04_215818_create_clients_table.php create mode 100644 database/migrations/2025_11_04_215846_create_client_sims_table.php create mode 100644 database/migrations/2025_11_04_221603_create_sales_table.php create mode 100644 database/migrations/2025_11_04_221707_create_sale_items_table.php diff --git a/app/Http/Controllers/Netbien/ClientController.php b/app/Http/Controllers/Netbien/ClientController.php new file mode 100644 index 0000000..1b36e60 --- /dev/null +++ b/app/Http/Controllers/Netbien/ClientController.php @@ -0,0 +1,59 @@ +orderBy('id', 'asc')->paginate(config('app.pagination')); + + return ApiResponse::OK->response([ + 'data' => $clients, + ]); + } + + public function store(ClientStoreRequest $request) + { + $client = Client::create($request->validated()); + + return ApiResponse::CREATED->response([ + 'data' => $client, + ]); + } + + public function update(ClientUpdateRequest $request, Client $client) + { + $client->update($request->validated()); + + return ApiResponse::OK->response([ + 'data' => $client, + ]); + } + + public function destroy(Client $client) + { + $hasActiveSims = $client->simCards() + ->wherePivot('is_active', true) + ->exists(); + + if ($hasActiveSims) { + return ApiResponse::BAD_REQUEST->response([ + 'message' => 'No se puede eliminar el cliente porque tiene SIMs activas', + ]); + } + + $client->delete(); + + return ApiResponse::NO_CONTENT->response(); + } + +} diff --git a/app/Http/Controllers/Netbien/PackagesController.php b/app/Http/Controllers/Netbien/PackagesController.php index 3057912..602378a 100644 --- a/app/Http/Controllers/Netbien/PackagesController.php +++ b/app/Http/Controllers/Netbien/PackagesController.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Netbien\PackagesStoreRequest; +use App\Http\Requests\Netbien\PackagesUpdateRequest; use App\Models\Packages; use Notsoweb\ApiResponse\Enums\ApiResponse; @@ -33,14 +34,19 @@ public function store(PackagesStoreRequest $request) ]); } - public function update(PackagesStoreRequest $request, Packages $package) + public function update(PackagesUpdateRequest $request, Packages $package) { - $validated = $request->validated(); - - $package->update($validated); + $package->update($request->validated()); return ApiResponse::OK->response([ 'data' => $package, ]); } + + public function destroy(Packages $package) + { + $package->delete(); + + return ApiResponse::NO_CONTENT->response(); + } } diff --git a/app/Http/Controllers/Netbien/SaleController.php b/app/Http/Controllers/Netbien/SaleController.php new file mode 100644 index 0000000..9f287ae --- /dev/null +++ b/app/Http/Controllers/Netbien/SaleController.php @@ -0,0 +1,187 @@ +has('date')) { + $query->whereDate('sale_date', $request->date); + } + + // Filtro por cliente + if ($request->has('client_id')) { + $query->where('client_id', $request->client_id); + } + + // Filtro por método de pago + if ($request->has('payment_method')) { + $query->where('payment_method', $request->payment_method); + } + + $sales = $query->orderBy('id', 'asc') + ->paginate(config('app.pagination')); + + return ApiResponse::OK->response([ + 'Sale' => $sales, + ]); + } + + public function store(SaleStoreRequest $request) + { + try { + DB::beginTransaction(); + + if ($request->has('client_id')) { + $client = Client::findOrFail($request->client_id); + } else { + $client = Client::create($request->client); + } + + $total = 0; + foreach ($request->saleItems as $item) { + $package = Packages::findOrFail($item['package_id']); + $total += $package->price; + } + + $sale = Sale::create([ + 'client_id' => $client->id, + 'total_amount' => $total, + 'payment_method' => $request->payment_method, + 'sale_date' => now(), + ]); + + foreach ($request->saleItems as $item) { + $sim = SimCard::findOrFail($item['sim_card_id']); + $package = Packages::findOrFail($item['package_id']); + + if ($sim->status !== SimCardStatus::AVAILABLE) { + throw new \Exception("La SIM {$sim->msisdn} no está disponible"); + } + + SaleItem::create([ + 'sale_id' => $sale->id, + 'sim_card_id' => $sim->id, + 'package_id' => $package->id, + ]); + + ClientSim::create([ + 'client_id' => $client->id, + 'sim_card_id' => $sim->id, + 'assigned_at' => now(), + 'is_active' => true, + ]); + + $sim->packages()->attach($package->id, [ + 'activated_at' => now(), + 'is_active' => true, + ]); + + $sim->update(['status' => SimCardStatus::ASSIGNED]); + } + + DB::commit(); + + $sale->load([ + 'client', + 'saleItems.simCard', + 'saleItems.package' + ]); + + return ApiResponse::CREATED->response([ + 'Sale' => $sale, + 'message' => 'Venta registrada exitosamente', + ]); + + } catch (\Exception $e) { + DB::rollBack(); + + return ApiResponse::INTERNAL_ERROR->response([ + 'message' => 'Error al registrar la venta', + 'error' => $e->getMessage(), + ]); + } + } + + public function update(SaleUpdateRequest $request, Sale $sale) + { + $sale->update($request->only(['payment_method'])); + + return ApiResponse::OK->response([ + 'data' => $sale, + 'message' => 'Venta actualizada exitosamente', + ]); + } + + public function destroy(Sale $sale) + { + try { + DB::beginTransaction(); + + // Obtener todos los items de la venta + $items = $sale->items; + + foreach ($items as $item) { + $sim = $item->simCard; + + // Desactivar el paquete de la SIM + $sim->packages() + ->wherePivot('package_id', $item->package_id) + ->wherePivot('is_active', true) + ->update([ + 'is_active' => false, + 'deactivated_at' => now() + ]); + + //Liberar la SIM del cliente + ClientSim::where('client_id', $sale->client_id) + ->where('sim_card_id', $sim->id) + ->where('is_active', true) + ->update([ + 'is_active' => false, + 'released_at' => now() + ]); + + //Cambiar status de la SIM a disponible + $sim->update(['status' => SimCardStatus::AVAILABLE]); + } + + //Eliminar la venta (cascade) + $sale->delete(); + + DB::commit(); + + return ApiResponse::NO_CONTENT->response(); + + } catch (\Exception $e) { + DB::rollBack(); + + return ApiResponse::INTERNAL_ERROR->response([ + 'message' => 'Error al cancelar la venta', + 'error' => $e->getMessage(), + ]); + } + } +} diff --git a/app/Http/Controllers/Netbien/SimCardController.php b/app/Http/Controllers/Netbien/SimCardController.php index 0ae0207..dfe213e 100644 --- a/app/Http/Controllers/Netbien/SimCardController.php +++ b/app/Http/Controllers/Netbien/SimCardController.php @@ -7,6 +7,7 @@ use App\Http\Requests\Netbien\SimCardStoreRequest; use App\Http\Requests\Netbien\SimCardUpdateRequest; use App\Models\SimCard; +use Illuminate\Support\Facades\DB; use Notsoweb\ApiResponse\Enums\ApiResponse; /** @@ -16,7 +17,7 @@ class SimCardController extends Controller { public function index() { - $simCards = SimCard::orderBy('id', 'asc')->paginate(config('app.pagination')); + $simCards = SimCard::with('packSims.package:id,name')->orderBy('id', 'asc')->paginate(config('app.pagination')); return ApiResponse::OK->response([ 'data' => $simCards, @@ -25,11 +26,33 @@ public function index() public function store(SimCardStoreRequest $request) { - $simCard = SimCard::create($request->validated()); + try { + DB::beginTransaction(); - return ApiResponse::CREATED->response([ - 'data' => $simCard, - ]); + $simCard = SimCard::create($request->validated()); + + if ($request->has('package_id')) { + // Asignar el paquete con fecha de activación + $simCard->packages()->attach($request->package_id, [ + 'activated_at' => now(), + 'is_active' => true, + ]); + } + + DB::commit(); + + $simCard->load('activePackage'); + + return ApiResponse::CREATED->response([ + 'data' => $simCard, + ]); + } catch (\Exception $e) { + DB::rollBack(); + return ApiResponse::INTERNAL_ERROR->response([ + 'message' => 'Error al crear SIM card', + 'error' => $e->getMessage(), + ]); + } } public function update(SimCardUpdateRequest $request, SimCard $simCard) diff --git a/app/Http/Requests/Netbien/ClientStoreRequest.php b/app/Http/Requests/Netbien/ClientStoreRequest.php new file mode 100644 index 0000000..c7c4661 --- /dev/null +++ b/app/Http/Requests/Netbien/ClientStoreRequest.php @@ -0,0 +1,47 @@ + + * + * @version 1.0.0 + */ +class ClientStoreRequest extends FormRequest +{ + public function authorize(): bool + { + return true; + } + + /** + * Get the validation rules that apply to the request. + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string'], + 'paternal' => ['required', 'string'], + 'maternal' => ['required', 'string'], + 'email' => ['nullable', 'email'], + 'phone' => ['nullable', 'string', 'max:10'], + ]; + } + + public function messages() : array + { + return [ + 'name.required' => 'El nombre es obligatorio.', + 'paternal.required' => 'El apellido paterno es obligatorio.', + 'maternal.required' => 'El apellido materno es obligatorio.', + 'email.email' => 'El email debe ser válido.', + ]; + } +} diff --git a/app/Http/Requests/Netbien/ClientUpdateRequest.php b/app/Http/Requests/Netbien/ClientUpdateRequest.php new file mode 100644 index 0000000..53dcf60 --- /dev/null +++ b/app/Http/Requests/Netbien/ClientUpdateRequest.php @@ -0,0 +1,47 @@ + + * + * @version 1.0.0 + */ +class ClientUpdateRequest extends FormRequest +{ + public function authorize(): bool + { + return true; + } + + /** + * Get the validation rules that apply to the request. + */ + public function rules(): array + { + return [ + 'name' => ['sometimes', 'string', 'max:255'], + 'paternal' => ['sometimes', 'string', 'max:100'], + 'maternal' => ['sometimes', 'string', 'max:100'], + 'email' => ['sometimes', 'email'], + 'phone' => ['nullable', 'string', 'max:20'], + ]; + } + + public function messages() : array + { + return [ + 'name.required' => 'El campo Nombre es obligatorio.', + 'email.required' => 'El campo Correo Electrónico es obligatorio.', + 'email.email' => 'El campo Correo Electrónico debe ser una dirección de correo válida.', + 'phone.required' => 'El campo Teléfono es obligatorio.', + ]; + } +} diff --git a/app/Http/Requests/Netbien/PackagesStoreRequest.php b/app/Http/Requests/Netbien/PackagesStoreRequest.php index 85c03c3..3ee6b81 100644 --- a/app/Http/Requests/Netbien/PackagesStoreRequest.php +++ b/app/Http/Requests/Netbien/PackagesStoreRequest.php @@ -26,9 +26,9 @@ public function rules(): array { return [ 'name' => ['required', 'string', 'max:80'], - 'price' => ['required', 'numeric', 'min:0'], - 'period' => ['required', 'integer', 'min:1'], - 'data_limit' => ['required', 'integer', 'min:0'], + 'price' => ['required', 'float'], + 'period' => ['required', 'float'], + 'data_limit' => ['required', 'integer'], ]; } @@ -45,7 +45,6 @@ public function messages() : array 'period.required' => 'El campo Periodo es obligatorio.', 'data_limit.required' => 'El campo Límite de Datos es obligatorio.', - 'data_limit.integer' => 'El campo Límite de Datos debe ser un número entero.', 'data_limit.min' => 'El campo Límite de Datos no debe ser negativo.', ]; } diff --git a/app/Http/Requests/Netbien/PackagesUpdateRequest.php b/app/Http/Requests/Netbien/PackagesUpdateRequest.php index aecd75a..8dc4a18 100644 --- a/app/Http/Requests/Netbien/PackagesUpdateRequest.php +++ b/app/Http/Requests/Netbien/PackagesUpdateRequest.php @@ -25,10 +25,10 @@ public function authorize(): bool public function rules(): array { return [ - 'name' => ['required', 'string', 'max:80'], - 'price' => ['required', 'numeric', 'min:0'], - 'period' => ['required', 'string', 'max:20'], - 'data_limit' => ['required', 'integer', 'min:0'], + 'name' => ['sometimes', 'string', 'max:80'], + 'price' => ['sometimes', 'numeric', 'min:0'], + 'period' => ['sometimes', 'numeric', 'min:1'], + 'data_limit' => ['sometimes', 'integer', 'min:0'], ]; } @@ -44,8 +44,6 @@ public function messages() : array 'price.min' => 'El campo Precio no debe ser negativo.', 'period.required' => 'El campo Periodo es obligatorio.', - 'period.string' => 'El campo Periodo debe ser una cadena de texto.', - 'period.max' => 'El campo Periodo no debe exceder los 20 caracteres.', 'data_limit.required' => 'El campo Límite de Datos es obligatorio.', 'data_limit.integer' => 'El campo Límite de Datos debe ser un número entero.', diff --git a/app/Http/Requests/Netbien/SaleStoreRequest.php b/app/Http/Requests/Netbien/SaleStoreRequest.php new file mode 100644 index 0000000..68fee16 --- /dev/null +++ b/app/Http/Requests/Netbien/SaleStoreRequest.php @@ -0,0 +1,58 @@ + + * + * @version 1.0.0 + */ +class SaleStoreRequest extends FormRequest +{ + public function authorize(): bool + { + return true; + } + + /** + * Get the validation rules that apply to the request. + */ + public function rules(): array + { + return [ + 'client_id' => ['nullable', 'integer', 'exists:clients,id'], + 'client' => ['required_without:client_id', 'array'], + 'client.name' => ['required_with:client', 'string'], + 'client.paternal' => ['required_with:client', 'string'], + 'client.maternal' => ['required_with:client', 'string'], + 'client.email' => ['required_with:client', 'email'], + 'client.phone' => ['nullable', 'string', 'max:10'], + + 'payment_method' => ['required', 'string', 'in:cash,card,transfer'], + + 'saleItems' => ['required', 'array', 'min:1'], + 'saleItems.*.sim_card_id' => ['required', 'integer', 'exists:sim_cards,id'], + 'saleItems.*.package_id' => ['required', 'integer', 'exists:packages,id'], + ]; + } + + public function messages(): array + { + return [ + 'client_id.exists' => 'El cliente seleccionado no existe.', + 'client.required_without' => 'Debe proporcionar un cliente existente o crear uno nuevo.', + 'payment_method.required' => 'El método de pago es obligatorio.', + 'payment_method.in' => 'El método de pago debe ser: efectivo, tarjeta o transferencia.', + 'saleItems.required' => 'Debe agregar al menos un item a la venta.', + 'saleItems.*.sim_card_id.exists' => 'Una de las SIM seleccionadas no existe.', + 'saleItems.*.package_id.exists' => 'Uno de los paquetes seleccionados no existe.', + ]; + } +} diff --git a/app/Http/Requests/Netbien/SaleUpdateRequest.php b/app/Http/Requests/Netbien/SaleUpdateRequest.php new file mode 100644 index 0000000..a1f1ead --- /dev/null +++ b/app/Http/Requests/Netbien/SaleUpdateRequest.php @@ -0,0 +1,41 @@ + + * + * @version 1.0.0 + */ +class SaleUpdateRequest extends FormRequest +{ + public function authorize(): bool + { + return true; + } + + /** + * + * IMPORTANTE: Generalmente no se permite modificar los items de una venta + * ya registrada por temas de trazabilidad contable. Solo se permite + * actualizar campos administrativos como método de pago o notas. + */ + public function rules(): array + { + return [ + 'payment_method' => ['sometimes', 'string', 'in:cash,card,transfer'], + ]; + } + + public function messages(): array + { + return [ + 'payment_method.in' => 'El método de pago debe ser: efectivo, tarjeta o transferencia.', + ]; + } +} diff --git a/app/Http/Requests/Netbien/SimCardStoreRequest.php b/app/Http/Requests/Netbien/SimCardStoreRequest.php index 3bf8090..d5bb6cc 100644 --- a/app/Http/Requests/Netbien/SimCardStoreRequest.php +++ b/app/Http/Requests/Netbien/SimCardStoreRequest.php @@ -29,6 +29,7 @@ public function rules(): array return [ 'iccid' => ['required', 'string', 'max:25', 'unique:sim_cards,iccid'], 'msisdn' => ['required', 'string', 'max:10', 'unique:sim_cards,msisdn'], + 'package_id' => ['nullable', 'integer', 'exists:packages,id'], ]; } @@ -44,6 +45,9 @@ public function messages() : array 'msisdn.string' => 'El campo MSISDN debe ser una cadena de texto.', 'msisdn.max' => 'El campo MSISDN no debe exceder los 10 caracteres.', 'msisdn.unique' => 'El MSISDN ya está en uso.', + + 'package_id.integer' => 'El paquete debe ser un número entero.', + 'package_id.exists' => 'El paquete seleccionado no existe.', ]; } } diff --git a/app/Http/Requests/Netbien/SimCardUpdateRequest.php b/app/Http/Requests/Netbien/SimCardUpdateRequest copy.php similarity index 92% rename from app/Http/Requests/Netbien/SimCardUpdateRequest.php rename to app/Http/Requests/Netbien/SimCardUpdateRequest copy.php index 67763fc..384b0aa 100644 --- a/app/Http/Requests/Netbien/SimCardUpdateRequest.php +++ b/app/Http/Requests/Netbien/SimCardUpdateRequest copy.php @@ -35,7 +35,6 @@ public function messages() : array { return [ 'msisdn.required' => 'El campo MSISDN es obligatorio.', - 'msisdn.string' => 'El campo MSISDN debe ser una cadena de texto.', 'msisdn.max' => 'El campo MSISDN no debe exceder los 10 caracteres.', 'msisdn.unique' => 'El MSISDN ya está en uso.', ]; diff --git a/app/Models/Client.php b/app/Models/Client.php new file mode 100644 index 0000000..f5f766d --- /dev/null +++ b/app/Models/Client.php @@ -0,0 +1,42 @@ + + * + * @version 1.0.0 + */ +class Client extends Model +{ + protected $fillable = [ + 'name', + 'paternal', + 'maternal', + 'email', + 'phone', + ]; + + public function sales() + { + return $this->hasMany(Sale::class); + } + + public function clientSims() + { + return $this->hasMany(ClientSim::class); + } + + public function simCards() + { + return $this->belongsToMany(SimCard::class, 'client_sims') + ->withPivot('assigned_at', 'released_at', 'is_active') + ->withTimestamps(); + } +} diff --git a/app/Models/ClientSim.php b/app/Models/ClientSim.php new file mode 100644 index 0000000..d7db83b --- /dev/null +++ b/app/Models/ClientSim.php @@ -0,0 +1,36 @@ + + * + * @version 1.0.0 + */ +class ClientSim extends Model +{ + protected $fillable = [ + 'client_id', + 'sim_card_id', + 'assigned_at', + 'released_at', + 'is_active', + ]; + + + public function client() + { + return $this->belongsTo(Client::class); + } + + public function simCard() + { + return $this->belongsTo(SimCard::class); + } +} diff --git a/app/Models/PackSim.php b/app/Models/PackSim.php index 1c43682..89209c5 100644 --- a/app/Models/PackSim.php +++ b/app/Models/PackSim.php @@ -18,11 +18,17 @@ class PackSim extends Model protected $fillable = [ 'package_id', 'sim_card_id', + 'activated_at', + 'deactivated_at', + 'is_active', ]; protected $casts = [ 'package_id' => 'integer', 'sim_card_id' => 'integer', + 'activated_at' => 'datetime', + 'deactivated_at' => 'datetime', + 'is_active' => 'boolean', ]; public function package() diff --git a/app/Models/Packages.php b/app/Models/Packages.php index 5ac7c7e..d601253 100644 --- a/app/Models/Packages.php +++ b/app/Models/Packages.php @@ -29,8 +29,34 @@ class Packages extends Model 'data_limit' => 'integer', ]; + // Relación con la tabla pivote public function packSims() { return $this->hasMany(PackSim::class, 'package_id'); } + + // Relación muchos a muchos con SIM cards + public function simCards() + { + return $this->belongsToMany( + SimCard::class, + 'pack_sims', + 'package_id', + 'sim_card_id' + )->withPivot('activated_at', 'deactivated_at', 'is_active') + ->withTimestamps(); + } + + // SIM cards activas con este paquete + public function activeSimCards() + { + return $this->belongsToMany( + SimCard::class, + 'pack_sims', + 'package_id', + 'sim_card_id' + )->wherePivot('is_active', true) + ->withPivot('activated_at', 'deactivated_at', 'is_active') + ->withTimestamps(); + } } diff --git a/app/Models/Sale.php b/app/Models/Sale.php new file mode 100644 index 0000000..5949112 --- /dev/null +++ b/app/Models/Sale.php @@ -0,0 +1,34 @@ + + * + * @version 1.0.0 + */ +class Sale extends Model +{ + protected $fillable = [ + 'client_id', + 'total_amount', + 'payment_method', + 'sale_date', + ]; + + public function client() + { + return $this->belongsTo(Client::class); + } + + public function saleItems() + { + return $this->hasMany(SaleItem::class); + } +} diff --git a/app/Models/SaleItem.php b/app/Models/SaleItem.php new file mode 100644 index 0000000..a596c62 --- /dev/null +++ b/app/Models/SaleItem.php @@ -0,0 +1,38 @@ + + * + * @version 1.0.0 + */ +class SaleItem extends Model +{ + protected $fillable = [ + 'sale_id', + 'sim_card_id', + 'package_id', + ]; + + public function sale() + { + return $this->belongsTo(Sale::class); + } + + public function simCard() + { + return $this->belongsTo(SimCard::class); + } + + public function package() + { + return $this->belongsTo(Packages::class); + } +} diff --git a/app/Models/SimCard.php b/app/Models/SimCard.php index 42550e3..1eacd84 100644 --- a/app/Models/SimCard.php +++ b/app/Models/SimCard.php @@ -26,8 +26,35 @@ class SimCard extends Model 'status' => SimCardStatus::class, ]; + // Relación con la tabla pivote public function packSims() { return $this->hasMany(PackSim::class, 'sim_card_id'); } + + // Relación muchos a muchos con paquetes + public function packages() + { + return $this->belongsToMany( + Packages::class, + 'pack_sims', + 'sim_card_id', + 'package_id' + )->withPivot('activated_at', 'deactivated_at', 'is_active') + ->withTimestamps(); + } + + // Paquete actualmente activo + public function activePackage() + { + return $this->belongsToMany( + Packages::class, + 'pack_sims', + 'sim_card_id', + 'package_id' + )->wherePivot('is_active', true) + ->withPivot('activated_at', 'deactivated_at', 'is_active') + ->withTimestamps() + ->limit(1); + } } diff --git a/database/migrations/2025_11_04_132448_create_packages_table.php b/database/migrations/2025_11_04_132448_create_packages_table.php index 8265ec0..41b42f0 100644 --- a/database/migrations/2025_11_04_132448_create_packages_table.php +++ b/database/migrations/2025_11_04_132448_create_packages_table.php @@ -14,9 +14,9 @@ public function up(): void Schema::create('packages', function (Blueprint $table) { $table->id(); $table->string('name'); - $table->integer('price'); + $table->float('price'); $table->integer('period'); - $table->integer('data_limit'); + $table->float('data_limit'); $table->timestamps(); }); } diff --git a/database/migrations/2025_11_04_204413_add_history_fields_to_pack_sims_table.php b/database/migrations/2025_11_04_204413_add_history_fields_to_pack_sims_table.php new file mode 100644 index 0000000..e22d5a1 --- /dev/null +++ b/database/migrations/2025_11_04_204413_add_history_fields_to_pack_sims_table.php @@ -0,0 +1,30 @@ +timestamp('activated_at')->nullable()->after('package_id'); + $table->timestamp('deactivated_at')->nullable()->after('activated_at'); + $table->boolean('is_active')->default(true)->after('deactivated_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('pack_sims', function (Blueprint $table) { + $table->dropColumn(['activated_at', 'deactivated_at', 'is_active']); + }); + } +}; diff --git a/database/migrations/2025_11_04_215818_create_clients_table.php b/database/migrations/2025_11_04_215818_create_clients_table.php new file mode 100644 index 0000000..078aa48 --- /dev/null +++ b/database/migrations/2025_11_04_215818_create_clients_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name'); + $table->string('paternal'); + $table->string('maternal'); + $table->string('email')->unique(); + $table->string('phone')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('clients'); + } +}; diff --git a/database/migrations/2025_11_04_215846_create_client_sims_table.php b/database/migrations/2025_11_04_215846_create_client_sims_table.php new file mode 100644 index 0000000..7237acf --- /dev/null +++ b/database/migrations/2025_11_04_215846_create_client_sims_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('client_id')->constrained('clients')->onDelete('cascade'); + $table->foreignId('sim_card_id')->constrained('sim_cards')->onDelete('cascade'); + $table->date('assigned_at'); + $table->date('released_at')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('client_sims'); + } +}; diff --git a/database/migrations/2025_11_04_221603_create_sales_table.php b/database/migrations/2025_11_04_221603_create_sales_table.php new file mode 100644 index 0000000..acce9ae --- /dev/null +++ b/database/migrations/2025_11_04_221603_create_sales_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('client_id')->constrained('clients')->onDelete('cascade'); + $table->decimal('total_amount', 10, 2); + $table->string('payment_method'); + $table->date('sale_date'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('sales'); + } +}; diff --git a/database/migrations/2025_11_04_221707_create_sale_items_table.php b/database/migrations/2025_11_04_221707_create_sale_items_table.php new file mode 100644 index 0000000..ee34278 --- /dev/null +++ b/database/migrations/2025_11_04_221707_create_sale_items_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('sale_id')->constrained('sales')->onDelete('cascade'); + $table->foreignId('sim_card_id')->constrained('sim_cards')->onDelete('cascade'); + $table->foreignId('package_id')->constrained('packages')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('sale_items'); + } +}; diff --git a/routes/api.php b/routes/api.php index dc4f2ec..ae7a76c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -2,6 +2,8 @@ use App\Http\Controllers\Netbien\PackagesController; use App\Http\Controllers\Netbien\SimCardController; +use App\Http\Controllers\Netbien\ClientController; +use App\Http\Controllers\Netbien\SaleController; use Illuminate\Support\Facades\Route; /** @@ -24,6 +26,10 @@ Route::resource('sim-cards', SimCardController::class); Route::resource('packages', PackagesController::class); + + Route::resource('clients', ClientController::class); + + Route::resource('sales', SaleController::class); }); /** Rutas públicas */