WIP (#1)
Co-authored-by: Juan Felipe Zapata Moreno <zapata_pipe@hotmail.com> Reviewed-on: #1
This commit is contained in:
parent
e8da447b68
commit
af8749abcd
@ -1,7 +1,7 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /var/www/golscontrols/public;
|
||||
root /var/www/netbien/public;
|
||||
index index.php index.html;
|
||||
|
||||
# Logging
|
||||
@ -17,7 +17,7 @@ server {
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass golscontrols:9000;
|
||||
fastcgi_pass netbien-backend:9000;
|
||||
fastcgi_index index.php;
|
||||
|
||||
# Timeouts importantes para evitar errores 500
|
||||
@ -45,17 +45,17 @@ server {
|
||||
|
||||
# Handle storage files (Laravel storage link)
|
||||
location /storage {
|
||||
alias /var/www/golscontrols/storage/app;
|
||||
alias /var/www/netbien/storage/app;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location /profile {
|
||||
alias /var/www/golscontrols/storage/app/profile;
|
||||
alias /var/www/netbien/storage/app/profile;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location /images {
|
||||
alias /var/www/golscontrols/storage/app/images;
|
||||
alias /var/www/netbien/storage/app/images;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
|
||||
28
app/Enums/SimCardStatus.php
Normal file
28
app/Enums/SimCardStatus.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum SimCardStatus: string
|
||||
{
|
||||
case AVAILABLE = 'available';
|
||||
case ASSIGNED = 'assigned';
|
||||
|
||||
/**
|
||||
* Get all possible values
|
||||
*/
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable label
|
||||
*/
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::AVAILABLE => 'Disponible',
|
||||
self::ASSIGNED => 'Asignada',
|
||||
};
|
||||
}
|
||||
}
|
||||
296
app/Http/Controllers/Netbien/CashCloseController.php
Normal file
296
app/Http/Controllers/Netbien/CashCloseController.php
Normal file
@ -0,0 +1,296 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Netbien;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CashClose;
|
||||
use App\Models\Sale;
|
||||
use App\Models\SaleItem;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
use phpseclib3\Crypt\RC2;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class CashCloseController extends Controller
|
||||
{
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = CashClose::with('user:id,name')->withCount('sales');
|
||||
|
||||
if ($request->has('status')) {
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
if ($request->has('date')) {
|
||||
$query->whereDate('close_date', $request->date);
|
||||
}
|
||||
|
||||
$cashCloses = $query->orderBy('id', 'asc')
|
||||
->paginate(config('app.pagination'));
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'cash_closes' => $cashCloses,
|
||||
]);
|
||||
}
|
||||
|
||||
public function closeCashClose(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'exit' => 'sometimes|numeric|min:0',
|
||||
]);
|
||||
|
||||
$cashClose = CashClose::open()->first();
|
||||
|
||||
if (!$cashClose) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'No hay un corte de caja abierto para cerrar.',
|
||||
]);
|
||||
}
|
||||
|
||||
$totalSales = Sale::where('cash_close_id', $cashClose->id)->sum('total_amount');
|
||||
|
||||
$paymentMethods = Sale::where('cash_close_id', $cashClose->id)
|
||||
->select('payment_method', DB::raw('SUM(total_amount) as total'))
|
||||
->groupBy('payment_method')
|
||||
->pluck('total', 'payment_method');
|
||||
|
||||
$exit = $request->input('exit', 0);
|
||||
|
||||
$cashClose->update([
|
||||
'closed_at' => now(),
|
||||
'income' => $totalSales,
|
||||
'exit' => $exit,
|
||||
'income_cash' => $paymentMethods->get('cash', 0),
|
||||
'income_card' => $paymentMethods->get('card', 0),
|
||||
'income_transfer' => $paymentMethods->get('transfer', 0),
|
||||
'status' => 'closed',
|
||||
]);
|
||||
|
||||
$balanceFinal = $cashClose->initial_balance + $totalSales - $exit;
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Corte de caja cerrado exitosamente.',
|
||||
'cash_close' => $cashClose->fresh('user'),
|
||||
'resumen' => [
|
||||
'periodo' => [
|
||||
'apertura' => $cashClose->opened_at,
|
||||
'cierre' => $cashClose->closed_at,
|
||||
],
|
||||
'totales' => [
|
||||
'fondo_inicial' => $cashClose->initial_balance,
|
||||
'total_ventas' => $totalSales,
|
||||
'efectivo' => $paymentMethods->get('cash', 0),
|
||||
'tarjeta' => $paymentMethods->get('card', 0),
|
||||
'transferencia' => $paymentMethods->get('transfer', 0),
|
||||
'egresos' => $exit,
|
||||
'balance_final' => $balanceFinal,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function report(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'start_date' => 'nullable|date',
|
||||
'end_date' => 'nullable|date|after_or_equal:start_date',
|
||||
]);
|
||||
|
||||
$query = CashClose::with('user:id,name')->withCount('sales');
|
||||
|
||||
if ($request->has('start_date') && $request->has('end_date')) {
|
||||
$query->whereBetween('close_date', [$request->start_date, $request->end_date]);
|
||||
} elseif ($request->has('start_date')) {
|
||||
$query->whereDate('close_date', '>=', $request->start_date);
|
||||
} elseif ($request->has('end_date')) {
|
||||
$query->whereDate('close_date', '<=', $request->end_date);
|
||||
} else {
|
||||
$query->closed()->orderBy('id', 'desc')->limit(1);
|
||||
}
|
||||
|
||||
// Información del corte de caja
|
||||
$cashCloses = $query->orderBy('id', 'desc')->get();
|
||||
|
||||
if ($cashCloses->isEmpty()) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'No se encontraron cortes de caja en el rango de fechas especificado.',
|
||||
]);
|
||||
}
|
||||
|
||||
$cashCloseIds = $cashCloses->pluck('id')->toArray();
|
||||
|
||||
// Estadísticas por paquete (Total de Paquetes Vendidos por Tipo)
|
||||
$packageStats = DB::table('sale_items')
|
||||
->join('sales', 'sale_items.sale_id', '=', 'sales.id')
|
||||
->join('packages', 'sale_items.package_id', '=', 'packages.id')
|
||||
->whereIn('sales.cash_close_id', $cashCloseIds)
|
||||
->select(
|
||||
'packages.name as paquete',
|
||||
DB::raw('COUNT(*) as total_vendidos'),
|
||||
DB::raw('SUM(packages.price) as total_ingresos')
|
||||
)
|
||||
->groupBy('packages.id', 'packages.name')
|
||||
->get();
|
||||
|
||||
// Estadísticas por duración (Total de Ventas por Duración)
|
||||
$durationStats = DB::table('sale_items')
|
||||
->join('sales', 'sale_items.sale_id', '=', 'sales.id')
|
||||
->join('packages', 'sale_items.package_id', '=', 'packages.id')
|
||||
->whereIn('sales.cash_close_id', $cashCloseIds)
|
||||
->select(
|
||||
'packages.period as duracion_dias',
|
||||
DB::raw('COUNT(DISTINCT sales.id) as total_ventas')
|
||||
)
|
||||
->groupBy('packages.period')
|
||||
->orderBy('packages.period', 'asc')
|
||||
->get();
|
||||
|
||||
// Reporte detallado de ventas
|
||||
$detailedSales = SaleItem::whereHas('sale', function ($query) use ($cashCloseIds) {
|
||||
$query->whereIn('cash_close_id', $cashCloseIds);
|
||||
})
|
||||
->with([
|
||||
'sale.client:id,name,paternal,maternal',
|
||||
'sale:id,client_id,payment_method',
|
||||
'simCard:id,iccid,msisdn',
|
||||
'package:id,name,price'
|
||||
])
|
||||
->orderBy('id', 'asc')
|
||||
->paginate(config('app.pagination'))
|
||||
->through(function ($item) {
|
||||
return [
|
||||
'nombre_comprador' => $item->sale->client->full_name,
|
||||
'id_sim' => $item->simCard->iccid,
|
||||
'numero_asignado' => $item->simCard->msisdn,
|
||||
'paquete' => $item->package->name,
|
||||
'costo' => $item->package->price,
|
||||
'medio_pago' => $item->sale->payment_method
|
||||
];
|
||||
});
|
||||
|
||||
$totalIncome = $cashCloses->sum('income');
|
||||
$totalExit = $cashCloses->sum('exit');
|
||||
$totalCash = $cashCloses->sum('income_cash');
|
||||
$totalCard = $cashCloses->sum('income_card');
|
||||
$totalTransfer = $cashCloses->sum('income_transfer');
|
||||
$balanceFinal = $cashCloses->sum('initial_balance') + $totalIncome - $totalExit;
|
||||
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'cash_closes' => $cashCloses,
|
||||
'periodo' => [
|
||||
'inicio' => $cashCloses->last()?->opened_at,
|
||||
'fin' => $cashCloses->first()?->closed_at,
|
||||
],
|
||||
'resumen_financiero' => [
|
||||
'total_ventas' => $totalIncome,
|
||||
'efectivo' => $totalCash,
|
||||
'tarjeta' => $totalCard,
|
||||
'transferencia' => $totalTransfer,
|
||||
'egresos' => $totalExit,
|
||||
'balance_final' => $balanceFinal,
|
||||
],
|
||||
'ventas_paquete' => $packageStats,
|
||||
'ventas_duracion' => $durationStats,
|
||||
'ventas_detalladas' => $detailedSales,
|
||||
]);
|
||||
}
|
||||
|
||||
public function exportReport(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'start_date' => 'nullable|date',
|
||||
'end_date' => 'nullable|date|after_or_equal:start_date',
|
||||
]);
|
||||
|
||||
$query = CashClose::with('user:id,name')->withCount('sales');
|
||||
|
||||
if ($request->has('start_date') && $request->has('end_date')) {
|
||||
$query->whereBetween('close_at', [$request->start_date, $request->end_date]);
|
||||
} elseif ($request->has('start_date')) {
|
||||
$query->whereDate('close_at', '>=', $request->start_date);
|
||||
} elseif ($request->has('end_date')) {
|
||||
$query->whereDate('close_at', '<=', $request->end_date);
|
||||
} else {
|
||||
$query->closed()->orderBy('id', 'desc')->limit(1);
|
||||
}
|
||||
|
||||
$cashCloses = $query->orderBy('id', 'desc')->get();
|
||||
|
||||
if ($cashCloses->isEmpty()) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'No se encontraron cortes de caja en el rango de fechas especificado.',
|
||||
]);
|
||||
}
|
||||
|
||||
$cashCloseIds = $cashCloses->pluck('id')->toArray();
|
||||
|
||||
// Obtener ventas detalladas
|
||||
$detailedSales = SaleItem::whereHas('sale', function ($query) use ($cashCloseIds) {
|
||||
$query->whereIn('cash_close_id', $cashCloseIds);
|
||||
})
|
||||
->with([
|
||||
'sale.client:id,name,paternal,maternal',
|
||||
'sale:id,client_id,payment_method',
|
||||
'simCard:id,iccid,msisdn',
|
||||
'package:id,name,price'
|
||||
])
|
||||
->orderBy('id', 'asc')
|
||||
->get();
|
||||
|
||||
// Calcular totales
|
||||
$totalIncome = $cashCloses->sum('income');
|
||||
$totalExit = $cashCloses->sum('exit');
|
||||
$totalCash = $cashCloses->sum('income_cash');
|
||||
$totalCard = $cashCloses->sum('income_card');
|
||||
$totalTransfer = $cashCloses->sum('income_transfer');
|
||||
|
||||
// Crear el CSV
|
||||
$filename = 'reporte_corte_caja_' . date('Y-m-d_His') . '.csv';
|
||||
|
||||
$headers = [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
||||
];
|
||||
|
||||
$callback = function () use ($cashCloses, $detailedSales, $totalIncome, $totalExit, $totalCash, $totalCard, $totalTransfer) {
|
||||
$file = fopen('php://output', 'w');
|
||||
|
||||
fprintf($file, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||||
|
||||
// RESUMEN FINANCIERO
|
||||
fputcsv($file, ['RESUMEN FINANCIERO', '']);
|
||||
fputcsv($file, ['Periodo Inicio', $cashCloses->last()?->opened_at]);
|
||||
fputcsv($file, ['Periodo Fin', $cashCloses->first()?->closed_at]);
|
||||
fputcsv($file, ['Total Ventas', number_format($totalIncome, 2)]);
|
||||
fputcsv($file, ['Efectivo', number_format($totalCash, 2)]);
|
||||
fputcsv($file, ['Tarjeta', number_format($totalCard, 2)]);
|
||||
fputcsv($file, ['Transferencia', number_format($totalTransfer, 2)]);
|
||||
fputcsv($file, ['Egresos', number_format($totalExit, 2)]);
|
||||
fputcsv($file, []);
|
||||
|
||||
// VENTAS DETALLADAS
|
||||
fputcsv($file, ['VENTAS DETALLADAS']);
|
||||
fputcsv($file, ['Nombre Comprador', 'ID SIM', 'Número Asignado', 'Paquete', 'Costo', 'Medio de Pago']);
|
||||
|
||||
foreach ($detailedSales as $item) {
|
||||
fputcsv($file, [
|
||||
$item->sale->client->full_name,
|
||||
"'" . $item->simCard->iccid . "'",
|
||||
"'" . $item->simCard->msisdn . "'",
|
||||
$item->package->name,
|
||||
number_format($item->package->price, 2),
|
||||
ucfirst($item->sale->payment_method)
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($file);
|
||||
};
|
||||
|
||||
return response()->stream($callback, 200, $headers);
|
||||
}
|
||||
}
|
||||
91
app/Http/Controllers/Netbien/ClientController.php
Normal file
91
app/Http/Controllers/Netbien/ClientController.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?php namespace App\Http\Controllers\Netbien;
|
||||
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Client;
|
||||
use App\Http\Requests\Netbien\ClientStoreRequest;
|
||||
use App\Http\Requests\Netbien\ClientUpdateRequest;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class ClientController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$clients = Client::with('simCards:id,msisdn')->orderBy('id', 'asc')->paginate(config('app.pagination'));
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'data' => $clients,
|
||||
]);
|
||||
}
|
||||
|
||||
public function search(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'filter' => 'required|string|min:2',
|
||||
]);
|
||||
|
||||
$search = $request->input('filter');
|
||||
|
||||
$clients = Client::with('simCards:id,msisdn')
|
||||
->where(function($q) use ($search) {
|
||||
$q->whereRaw("CONCAT(name, ' ', paternal, ' ', maternal) LIKE ?", ["%{$search}%"])
|
||||
->orWhere('rfc', 'like', "%{$search}%")
|
||||
->orWhere('email', 'like', "%{$search}%")
|
||||
->orWhere('phone', 'like', "%{$search}%");
|
||||
})
|
||||
->orderBy('id', 'asc')
|
||||
->paginate(config('app.pagination'));
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'data' => $clients,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Client $client)
|
||||
{
|
||||
$client->load('simCards:id,msisdn');
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'data' => $client,
|
||||
]);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
59
app/Http/Controllers/Netbien/PackagesController.php
Normal file
59
app/Http/Controllers/Netbien/PackagesController.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php namespace App\Http\Controllers\Netbien;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class PackagesController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$packages = Packages::OrderBy('id', 'asc')->paginate(config('app.pagination'));
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'data' => $packages,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Packages $package)
|
||||
{
|
||||
return ApiResponse::OK->response([
|
||||
'data' => $package,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(PackagesStoreRequest $request)
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
$package = Packages::create($validated);
|
||||
|
||||
return ApiResponse::CREATED->response([
|
||||
'data' => $package,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(PackagesUpdateRequest $request, Packages $package)
|
||||
{
|
||||
$package->update($request->validated());
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'data' => $package,
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(Packages $package)
|
||||
{
|
||||
$package->delete();
|
||||
|
||||
return ApiResponse::NO_CONTENT->response();
|
||||
}
|
||||
}
|
||||
191
app/Http/Controllers/Netbien/SaleController.php
Normal file
191
app/Http/Controllers/Netbien/SaleController.php
Normal file
@ -0,0 +1,191 @@
|
||||
<?php namespace App\Http\Controllers\Netbien;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Sale;
|
||||
use App\Models\SaleItem;
|
||||
use App\Models\Client;
|
||||
use App\Models\Packages;
|
||||
use App\Models\SimCard;
|
||||
use App\Models\ClientSim;
|
||||
use App\Enums\SimCardStatus;
|
||||
use App\Http\Requests\Netbien\SaleStoreRequest;
|
||||
use App\Http\Requests\Netbien\SaleUpdateRequest;
|
||||
use App\Services\CashCloseService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
|
||||
/**
|
||||
*/
|
||||
class SaleController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = Sale::with([
|
||||
'client:id,name,paternal,maternal,email',
|
||||
'saleItems.simCard:id,iccid,msisdn',
|
||||
'saleItems.package:id,name,price'
|
||||
]);
|
||||
|
||||
// Filtro por fecha
|
||||
if ($request->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;
|
||||
}
|
||||
|
||||
$cashClose = CashCloseService::getOrCreateOpenCashClose();
|
||||
|
||||
$sale = Sale::create([
|
||||
'client_id' => $client->id,
|
||||
'cash_close_id' => $cashClose->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->saleItems;
|
||||
|
||||
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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
96
app/Http/Controllers/Netbien/SimCardController.php
Normal file
96
app/Http/Controllers/Netbien/SimCardController.php
Normal file
@ -0,0 +1,96 @@
|
||||
<?php namespace App\Http\Controllers\Netbien;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
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;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class SimCardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$simCards = SimCard::with('packSims.package:id,name')->orderBy('id', 'asc')->paginate(config('app.pagination'));
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'data' => $simCards,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(SimCard $simCard)
|
||||
{
|
||||
$simCard->load('packSims.package:id,name');
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'data' => $simCard,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(SimCardStoreRequest $request)
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$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)
|
||||
{
|
||||
$simCard->update($request->validated());
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'data' => $simCard,
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(SimCard $simCard)
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$simCard->delete();
|
||||
|
||||
DB::commit();
|
||||
|
||||
return ApiResponse::NO_CONTENT->response();
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
'message' => 'Error al eliminar SIM card',
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
48
app/Http/Requests/Netbien/ClientStoreRequest.php
Normal file
48
app/Http/Requests/Netbien/ClientStoreRequest.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php namespace App\Http\Requests\Netbien;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* Almacenar rol
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @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'],
|
||||
'rfc' => ['required', 'string', 'max:13'],
|
||||
];
|
||||
}
|
||||
|
||||
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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
48
app/Http/Requests/Netbien/ClientUpdateRequest.php
Normal file
48
app/Http/Requests/Netbien/ClientUpdateRequest.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php namespace App\Http\Requests\Netbien;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* Almacenar rol
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @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'],
|
||||
'rfc' => ['sometimes', 'string', 'max:13'],
|
||||
];
|
||||
}
|
||||
|
||||
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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
51
app/Http/Requests/Netbien/PackagesStoreRequest.php
Normal file
51
app/Http/Requests/Netbien/PackagesStoreRequest.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php namespace App\Http\Requests\Netbien;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
* Almacenar rol
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class PackagesStoreRequest 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', 'max:80'],
|
||||
'price' => ['required', 'integer'],
|
||||
'period' => ['required', 'integer'],
|
||||
'data_limit' => ['required', 'integer'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages() : array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'El campo Nombre es obligatorio.',
|
||||
'name.string' => 'El campo Nombre debe ser una cadena de texto.',
|
||||
'name.max' => 'El campo Nombre no debe exceder los 80 caracteres.',
|
||||
|
||||
'price.required' => 'El campo Precio es obligatorio.',
|
||||
'price.min' => 'El campo Precio no debe ser negativo.',
|
||||
|
||||
'period.required' => 'El campo Periodo es obligatorio.',
|
||||
|
||||
'data_limit.required' => 'El campo Límite de Datos es obligatorio.',
|
||||
'data_limit.min' => 'El campo Límite de Datos no debe ser negativo.',
|
||||
];
|
||||
}
|
||||
}
|
||||
53
app/Http/Requests/Netbien/PackagesUpdateRequest.php
Normal file
53
app/Http/Requests/Netbien/PackagesUpdateRequest.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php namespace App\Http\Requests\Netbien;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
* Almacenar rol
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class PackagesUpdateRequest 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:80'],
|
||||
'price' => ['sometimes', 'numeric', 'min:0'],
|
||||
'period' => ['sometimes', 'numeric', 'min:1'],
|
||||
'data_limit' => ['sometimes', 'integer', 'min:0'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages() : array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'El campo Nombre es obligatorio.',
|
||||
'name.string' => 'El campo Nombre debe ser una cadena de texto.',
|
||||
'name.max' => 'El campo Nombre no debe exceder los 80 caracteres.',
|
||||
|
||||
'price.required' => 'El campo Precio es obligatorio.',
|
||||
'price.numeric' => 'El campo Precio debe ser un número.',
|
||||
'price.min' => 'El campo Precio no debe ser negativo.',
|
||||
|
||||
'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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
58
app/Http/Requests/Netbien/SaleStoreRequest.php
Normal file
58
app/Http/Requests/Netbien/SaleStoreRequest.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php namespace App\Http\Requests\Netbien;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* Almacenar rol
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
41
app/Http/Requests/Netbien/SaleUpdateRequest.php
Normal file
41
app/Http/Requests/Netbien/SaleUpdateRequest.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php namespace App\Http\Requests\Netbien;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
/**
|
||||
* Request para actualizar una venta
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
53
app/Http/Requests/Netbien/SimCardStoreRequest.php
Normal file
53
app/Http/Requests/Netbien/SimCardStoreRequest.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php namespace App\Http\Requests\Netbien;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* Almacenar rol
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class SimCardStoreRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
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'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages() : array
|
||||
{
|
||||
return [
|
||||
'iccid.required' => 'El campo ICCID es obligatorio.',
|
||||
'iccid.string' => 'El campo ICCID debe ser una cadena de texto.',
|
||||
'iccid.max' => 'El campo ICCID no debe exceder los 25 caracteres.',
|
||||
'iccid.unique' => 'El ICCID ya está en uso.',
|
||||
|
||||
'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.',
|
||||
|
||||
'package_id.integer' => 'El paquete debe ser un número entero.',
|
||||
'package_id.exists' => 'El paquete seleccionado no existe.',
|
||||
];
|
||||
}
|
||||
}
|
||||
42
app/Http/Requests/Netbien/SimCardUpdateRequest.php
Normal file
42
app/Http/Requests/Netbien/SimCardUpdateRequest.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php namespace App\Http\Requests\Netbien;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/**
|
||||
* Almacenar rol
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class SimCardUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'msisdn' => ['required', 'string', 'max:10', 'unique:sim_cards,msisdn'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages() : array
|
||||
{
|
||||
return [
|
||||
'msisdn.required' => 'El campo MSISDN es obligatorio.',
|
||||
'msisdn.max' => 'El campo MSISDN no debe exceder los 10 caracteres.',
|
||||
'msisdn.unique' => 'El MSISDN ya está en uso.',
|
||||
];
|
||||
}
|
||||
}
|
||||
62
app/Models/CashClose.php
Normal file
62
app/Models/CashClose.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CashClose extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'opened_at',
|
||||
'closed_at',
|
||||
'initial_balance',
|
||||
'income',
|
||||
'exit',
|
||||
'income_cash',
|
||||
'income_card',
|
||||
'income_transfer',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'initial_balance' => 'decimal:2',
|
||||
'income' => 'decimal:2',
|
||||
'exit' => 'decimal:2',
|
||||
'income_cash' => 'decimal:2',
|
||||
'income_card' => 'decimal:2',
|
||||
'income_transfer' => 'decimal:2',
|
||||
'opened_at' => 'datetime',
|
||||
'closed_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Relación con el usuario que realizó el corte
|
||||
*/
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relación con las ventas asociadas a este corte
|
||||
*/
|
||||
public function sales()
|
||||
{
|
||||
return $this->hasMany(Sale::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope para cortes abiertos
|
||||
*/
|
||||
public function scopeOpen($query)
|
||||
{
|
||||
return $query->where('status', 'open');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope para cortes cerrados
|
||||
*/
|
||||
public function scopeClosed($query)
|
||||
{
|
||||
return $query->where('status', 'closed');
|
||||
}
|
||||
}
|
||||
51
app/Models/Client.php
Normal file
51
app/Models/Client.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php namespace App\Models;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class Client extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'paternal',
|
||||
'maternal',
|
||||
'email',
|
||||
'phone',
|
||||
'rfc',
|
||||
];
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public function fullName(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->name . ' ' . $this->paternal . ' ' . $this->maternal,
|
||||
);
|
||||
}
|
||||
}
|
||||
36
app/Models/ClientSim.php
Normal file
36
app/Models/ClientSim.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php namespace App\Models;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
43
app/Models/PackSim.php
Normal file
43
app/Models/PackSim.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php namespace App\Models;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
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()
|
||||
{
|
||||
return $this->belongsTo(Packages::class, 'package_id');
|
||||
}
|
||||
|
||||
public function simCard()
|
||||
{
|
||||
return $this->belongsTo(SimCard::class, 'sim_card_id');
|
||||
}
|
||||
}
|
||||
62
app/Models/Packages.php
Normal file
62
app/Models/Packages.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php namespace App\Models;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class Packages extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'price',
|
||||
'period',
|
||||
'data_limit',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'name' => 'string',
|
||||
'price' => 'float',
|
||||
'period' => 'integer',
|
||||
'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();
|
||||
}
|
||||
}
|
||||
40
app/Models/Sale.php
Normal file
40
app/Models/Sale.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php namespace App\Models;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class Sale extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'client_id',
|
||||
'cash_close_id',
|
||||
'total_amount',
|
||||
'payment_method',
|
||||
'sale_date',
|
||||
];
|
||||
|
||||
public function client()
|
||||
{
|
||||
return $this->belongsTo(Client::class);
|
||||
}
|
||||
|
||||
public function saleItems()
|
||||
{
|
||||
return $this->hasMany(SaleItem::class);
|
||||
}
|
||||
|
||||
public function cashClose()
|
||||
{
|
||||
return $this->belongsTo(CashClose::class);
|
||||
}
|
||||
}
|
||||
38
app/Models/SaleItem.php
Normal file
38
app/Models/SaleItem.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php namespace App\Models;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
60
app/Models/SimCard.php
Normal file
60
app/Models/SimCard.php
Normal file
@ -0,0 +1,60 @@
|
||||
<?php namespace App\Models;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
|
||||
use App\Enums\SimCardStatus;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Modelo para tarjetas SIM
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class SimCard extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'iccid',
|
||||
'msisdn',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'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);
|
||||
}
|
||||
}
|
||||
33
app/Services/CashCloseService.php
Normal file
33
app/Services/CashCloseService.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\CashClose;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class CashCloseService
|
||||
{
|
||||
/**
|
||||
* Obtiene el corte de caja abierto del día o crea uno nuevo
|
||||
*/
|
||||
public static function getOrCreateOpenCashClose()
|
||||
{
|
||||
$cashClose = CashClose::open()->first();
|
||||
|
||||
if (!$cashClose) {
|
||||
$cashClose = CashClose::create([
|
||||
'user_id' => Auth::id(),
|
||||
'opened_at' => now(),
|
||||
'initial_balance' => 0,
|
||||
'income' => 0,
|
||||
'exit' => 0,
|
||||
'income_cash' => 0,
|
||||
'income_card' => 0,
|
||||
'income_transfer' => 0,
|
||||
'status' => 'open',
|
||||
]);
|
||||
}
|
||||
|
||||
return $cashClose;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\SimCardStatus;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('sim_cards', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('iccid')->unique();
|
||||
$table->string('msisdn')->unique();
|
||||
$table->enum('status', SimCardStatus::values())->default(SimCardStatus::AVAILABLE->value);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sim_cards');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('packages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->float('price');
|
||||
$table->integer('period');
|
||||
$table->float('data_limit');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('packages');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('pack_sims', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$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('pack_sims');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('pack_sims', function (Blueprint $table) {
|
||||
$table->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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('clients', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('client_sims', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
31
database/migrations/2025_11_04_221603_create_sales_table.php
Normal file
31
database/migrations/2025_11_04_221603_create_sales_table.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('sales', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('sale_items', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cash_closes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->nullable()->constrained('users')->onDelete('set null');
|
||||
$table->decimal('balance', 10, 2);
|
||||
$table->decimal('income', 10, 2);
|
||||
$table->decimal('exit', 10, 2)->default(0);
|
||||
$table->enum('status', ['open', 'closed'])->default('open')->comment('Estado del corte');
|
||||
$table->timestamp('close_date');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cash_closes');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('sales', function (Blueprint $table) {
|
||||
$table->foreignId('cash_close_id')->nullable()->after('client_id')->constrained('cash_closes')->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('sales', function (Blueprint $table) {
|
||||
$table->dropForeign(['cash_close_id']);
|
||||
$table->dropColumn('cash_close_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('clients', function (Blueprint $table) {
|
||||
$table->string('rfc')->nullable()->after('phone');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('clients', function (Blueprint $table) {
|
||||
$table->dropColumn('rfc');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('clients', function (Blueprint $table) {
|
||||
$table->string('rfc', 13)->unique()->change();
|
||||
$table->string('phone')->unique()->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('cash_closes', function (Blueprint $table) {
|
||||
$table->timestamp('opened_at')->nullable()->after('user_id');
|
||||
|
||||
$table->decimal('initial_balance', 10, 2)->default(0)->after('user_id');
|
||||
|
||||
$table->dropColumn('balance');
|
||||
|
||||
$table->decimal('income_cash', 10, 2)->default(0)->after('income');
|
||||
$table->decimal('income_card', 10, 2)->default(0)->after('income_cash');
|
||||
$table->decimal('income_transfer', 10, 2)->default(0)->after('income_card');
|
||||
});
|
||||
|
||||
// Renombrar y hacer nullable en una segunda operación
|
||||
Schema::table('cash_closes', function (Blueprint $table) {
|
||||
$table->renameColumn('close_date', 'closed_at');
|
||||
});
|
||||
|
||||
// Modificar closed_at para que sea nullable
|
||||
Schema::table('cash_closes', function (Blueprint $table) {
|
||||
$table->timestamp('closed_at')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('cash_closes', function (Blueprint $table) {
|
||||
$table->timestamp('closed_at')->nullable(false)->change();
|
||||
});
|
||||
|
||||
Schema::table('cash_closes', function (Blueprint $table) {
|
||||
$table->renameColumn('closed_at', 'close_date');
|
||||
});
|
||||
|
||||
Schema::table('cash_closes', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'opened_at',
|
||||
'initial_balance',
|
||||
'income_cash',
|
||||
'income_card',
|
||||
'income_transfer',
|
||||
]);
|
||||
$table->decimal('balance', 10, 2)->after('user_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
61
database/seeders/ClientSeeder.php
Normal file
61
database/seeders/ClientSeeder.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php namespace Database\Seeders;
|
||||
|
||||
use App\Models\Client;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class ClientSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Ejecutar sembrado de base de datos
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$clients = [
|
||||
[
|
||||
'name' => 'Juan',
|
||||
'paternal' => 'Pérez',
|
||||
'maternal' => 'Gómez',
|
||||
'email' => 'juan.perez@example.com',
|
||||
'phone' => '551234567890',
|
||||
'rfc' => 'JUAP890123XXX',
|
||||
],
|
||||
[
|
||||
'name' => 'María',
|
||||
'paternal' => 'López',
|
||||
'maternal' => 'Hernández',
|
||||
'email' => 'maria.lopez@example.com',
|
||||
'phone' => '551234567891',
|
||||
'rfc' => 'MALO910203XXX',
|
||||
],
|
||||
[
|
||||
'name' => 'María',
|
||||
'paternal' => 'Hernández',
|
||||
'maternal' => 'Cruz',
|
||||
'email' => 'maria.hernandez@example.com',
|
||||
'phone' => '555-1003',
|
||||
'rfc' => 'MAHC910203XXX',
|
||||
],
|
||||
[
|
||||
'name' => 'Carlos',
|
||||
'paternal' => 'Sánchez',
|
||||
'maternal' => 'Ruiz',
|
||||
'email' => 'carlos.sanchez@example.com',
|
||||
'phone' => '555-1004',
|
||||
'rfc' => 'CASR910203XXX',
|
||||
],
|
||||
[
|
||||
'name' => 'Laura',
|
||||
'paternal' => 'Gómez',
|
||||
'maternal' => 'Flores',
|
||||
'email' => 'laura.gomez@example.com',
|
||||
'phone' => '555-1005',
|
||||
'rfc' => 'LAGF910203XXX',
|
||||
]
|
||||
];
|
||||
|
||||
foreach ($clients as $client) {
|
||||
Client::create($client);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,9 +7,9 @@
|
||||
|
||||
/**
|
||||
* Seeder de desarrollo
|
||||
*
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class DevSeeder extends Seeder
|
||||
@ -22,5 +22,9 @@ public function run(): void
|
||||
$this->call(RoleSeeder::class);
|
||||
$this->call(UserSeeder::class);
|
||||
$this->call(SettingSeeder::class);
|
||||
|
||||
$this->call(ClientSeeder::class);
|
||||
$this->call(SimCardSeeder::class);
|
||||
$this->call(PackageSeeder::class);
|
||||
}
|
||||
}
|
||||
|
||||
25
database/seeders/PackageSeeder.php
Normal file
25
database/seeders/PackageSeeder.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php namespace Database\Seeders;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class PackageSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Ejecutar sembrado de base de datos
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
42
database/seeders/PackagesSeeder.php
Normal file
42
database/seeders/PackagesSeeder.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use App\Models\Packages;
|
||||
|
||||
class PackagesSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$packages = [
|
||||
[
|
||||
'name' => 'Paquete 1',
|
||||
'price' => 100.00,
|
||||
'period' => 15,
|
||||
'data_limit' => 5,
|
||||
],
|
||||
[
|
||||
'name' => 'Paquete 2',
|
||||
'price' => 150.00,
|
||||
'period' => 20,
|
||||
'data_limit' => 10,
|
||||
],
|
||||
[
|
||||
'name' => 'Paquete 3',
|
||||
'price' => 200.00,
|
||||
'period' => 25,
|
||||
'data_limit' => 15,
|
||||
],
|
||||
[
|
||||
'name' => 'Paquete Premium',
|
||||
'price' => 250.00,
|
||||
'period' => 30,
|
||||
'data_limit' => 20,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($packages as $package) {
|
||||
Packages::create($package);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
database/seeders/SimCardSeeder.php
Normal file
45
database/seeders/SimCardSeeder.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php namespace Database\Seeders;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use App\Models\SimCard;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class SimCardSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Ejecutar sembrado de base de datos
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$availables=[
|
||||
['iccid'=>'8986002212345678901','msisdn'=>'551234567890','status'=>'available'],
|
||||
['iccid'=>'8986002212345678902','msisdn'=>'551234567891','status'=>'available'],
|
||||
['iccid'=>'8986002212345678903','msisdn'=>'551234567892','status'=>'available'],
|
||||
['iccid'=>'8986002212345678904','msisdn'=>'551234567893','status'=>'available'],
|
||||
['iccid'=>'8986002212345678905','msisdn'=>'551234567894','status'=>'available'],
|
||||
['iccid'=>'8986002212345678906','msisdn'=>'551234567895','status'=>'available'],
|
||||
['iccid'=>'8986002212345678907','msisdn'=>'551234567896','status'=>'available'],
|
||||
['iccid'=>'8986002212345678908','msisdn'=>'551234567897','status'=>'available'],
|
||||
['iccid'=>'8986002212345678909','msisdn'=>'551234567898','status'=>'available'],
|
||||
['iccid'=>'8986002212345678910','msisdn'=>'551234567899','status'=>'available'],
|
||||
];
|
||||
|
||||
foreach ($availables as $simcard) {
|
||||
SimCard::create([
|
||||
'iccid' => $simcard['iccid'],
|
||||
'msisdn' => $simcard['msisdn'],
|
||||
'status' => $simcard['status'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,37 +1,40 @@
|
||||
services:
|
||||
repuve-backend:
|
||||
netbien-backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: dockerfile.dev
|
||||
working_dir: /var/www/repuve-v1
|
||||
dockerfile: dockerfile
|
||||
working_dir: /var/www/netbien
|
||||
environment:
|
||||
- DB_HOST=mysql
|
||||
- DB_HOST=${DB_HOST}
|
||||
- DB_USERNAME=${DB_USERNAME}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- DB_DATABASE=${DB_DATABASE}
|
||||
- DB_PORT=${DB_PORT}
|
||||
volumes:
|
||||
- ./:/var/www/repuve-v1
|
||||
- /var/www/repuve-v1/vendor
|
||||
- ./:/var/www/netbien
|
||||
- ./vendor:/var/www/netbien/vendor
|
||||
- /var/www/netbien/node_modules
|
||||
networks:
|
||||
- repuve-network
|
||||
- netbien-network
|
||||
mem_limit: 512m
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "${NGINX_PORT}:80"
|
||||
volumes:
|
||||
- ./public:/var/www/repuve-v1/public
|
||||
- ./:/var/www/netbien
|
||||
- ./Docker/nginx/nginx.conf:/etc/nginx/conf.d/default.conf
|
||||
networks:
|
||||
- repuve-network
|
||||
- netbien-network
|
||||
mem_limit: 512m
|
||||
depends_on:
|
||||
- repuve-backend
|
||||
- netbien-backend
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
@ -41,11 +44,12 @@ services:
|
||||
MYSQL_PASSWORD: ${DB_PASSWORD}
|
||||
MYSQL_USER: ${DB_USERNAME}
|
||||
ports:
|
||||
- ${DB_PORT}:${DB_PORT}
|
||||
- "${DB_PORT}:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
networks:
|
||||
- repuve-network
|
||||
- netbien-network
|
||||
mem_limit: 512m
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
timeout: 15s
|
||||
@ -57,31 +61,17 @@ services:
|
||||
PMA_HOST: mysql
|
||||
PMA_PORT: 3306
|
||||
ports:
|
||||
- '${PMA_PORT}:80'
|
||||
- "${PMA_PORT}:80"
|
||||
depends_on:
|
||||
- mysql
|
||||
- mysql
|
||||
networks:
|
||||
- repuve-network
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
ports:
|
||||
- "${REDIS_PORT}:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- repuve-network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
- netbien-network
|
||||
mem_limit: 512m
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
repuve-network:
|
||||
netbien-network:
|
||||
driver: bridge
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
FROM php:8.3-fpm
|
||||
|
||||
RUN mkdir -p /var/www/repuve-v1
|
||||
RUN mkdir -p /var/www/netbien
|
||||
|
||||
WORKDIR /var/www/repuve-v1
|
||||
WORKDIR /var/www/netbien
|
||||
|
||||
RUN apt-get update && apt-get install -y\
|
||||
git \
|
||||
@ -31,8 +31,8 @@ RUN chmod +x /usr/local/bin/entrypoint-dev.sh
|
||||
|
||||
RUN mkdir -p storage/app/keys storage/logs bootstrap/cache
|
||||
|
||||
RUN chown -R www-data:www-data /var/www/repuve-v1/storage /var/www/repuve-v1/bootstrap/cache
|
||||
RUN chmod -R 775 /var/www/repuve-v1/storage /var/www/repuve-v1/bootstrap/cache
|
||||
RUN chown -R www-data:www-data /var/www/netbien/storage /var/www/netbien/bootstrap/cache
|
||||
RUN chmod -R 775 /var/www/netbien/storage /var/www/netbien/bootstrap/cache
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
git config --global --add safe.directory /var/www/repuve-v1
|
||||
git config --global --add safe.directory /var/www/netbien
|
||||
|
||||
echo "=== Iniciando entrypoint DESARROLLO ==="
|
||||
|
||||
# Variables desde Docker environment
|
||||
DB_HOST=${DB_HOST:-mysql}
|
||||
DB_USERNAME=${DB_USERNAME:-root}
|
||||
DB_PASSWORD=${DB_PASSWORD:-}
|
||||
DB_DATABASE=${DB_DATABASE:-laravel}
|
||||
DB_HOST=${DB_HOST}
|
||||
DB_USERNAME=${DB_USERNAME}
|
||||
DB_PASSWORD=${DB_PASSWORD}
|
||||
DB_DATABASE=${DB_DATABASE}
|
||||
MAX_RETRIES=30
|
||||
RETRY_COUNT=0
|
||||
|
||||
|
||||
105
entrypoint.sh
105
entrypoint.sh
@ -1,105 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== Iniciando entrypoint ==="
|
||||
|
||||
# Variables desde Docker environment
|
||||
DB_HOST=${DB_HOST:-mysql}
|
||||
DB_USERNAME=${DB_USERNAME:-root}
|
||||
DB_PASSWORD=${DB_PASSWORD:-}
|
||||
DB_DATABASE=${DB_DATABASE:-laravel}
|
||||
MAX_RETRIES=30
|
||||
RETRY_COUNT=0
|
||||
|
||||
echo "Configuración de BD: Host=${DB_HOST}, Usuario=${DB_USERNAME}, Base=${DB_DATABASE}"
|
||||
|
||||
# Función para verificar conectividad con MySQL usando PHP
|
||||
check_mysql() {
|
||||
php -r "
|
||||
try {
|
||||
\$pdo = new PDO('mysql:host=${DB_HOST};port=3306', '${DB_USERNAME}', '${DB_PASSWORD}');
|
||||
\$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
exit(0);
|
||||
} catch (Exception \$e) {
|
||||
exit(1);
|
||||
}
|
||||
"
|
||||
}
|
||||
|
||||
# Esperar a que MySQL esté disponible
|
||||
echo "Esperando conexión a MySQL..."
|
||||
until check_mysql; do
|
||||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||||
if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
|
||||
echo "ERROR: No se pudo conectar a MySQL después de $MAX_RETRIES intentos"
|
||||
exit 1
|
||||
fi
|
||||
echo "Intento $RETRY_COUNT/$MAX_RETRIES - Esperando a MySQL..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "✓ MySQL está disponible"
|
||||
|
||||
# Comandos de inicialización
|
||||
echo "Ejecutando comandos de inicialización..."
|
||||
|
||||
echo "Ejecutando package:discover..."
|
||||
php artisan package:discover --ansi
|
||||
|
||||
echo "Creando enlaces simbólicos..."
|
||||
php artisan storage:link --force || true
|
||||
|
||||
echo "Ejecutando configuración de producción..."
|
||||
composer run env:prod
|
||||
|
||||
echo "Creando directorio de claves Passport..."
|
||||
mkdir -p storage/app/keys
|
||||
|
||||
echo "Generando claves de Passport..."
|
||||
php artisan passport:keys --force || true
|
||||
|
||||
# Verificar que las claves se crearon
|
||||
if [ ! -f "storage/app/keys/oauth-private.key" ] || [ ! -f "storage/app/keys/oauth-public.key" ]; then
|
||||
echo "ERROR: Las claves de Passport no se generaron correctamente"
|
||||
echo "Intentando generar manualmente..."
|
||||
|
||||
# Generar claves manualmente usando OpenSSL
|
||||
openssl genrsa -out storage/app/keys/oauth-private.key 4096
|
||||
openssl rsa -in storage/app/keys/oauth-private.key -pubout -out storage/app/keys/oauth-public.key
|
||||
|
||||
echo "✓ Claves generadas manualmente"
|
||||
fi
|
||||
|
||||
# Establecer permisos correctos para las claves
|
||||
chmod 600 storage/app/keys/oauth-private.key
|
||||
chmod 644 storage/app/keys/oauth-public.key
|
||||
chown www-data:www-data storage/app/keys/oauth-*.key
|
||||
|
||||
echo "✓ Claves de Passport verificadas"
|
||||
|
||||
# Archivo de control para primera ejecución
|
||||
FIRST_RUN_FLAG="/var/www/holos.backend/.first_run_completed"
|
||||
|
||||
# Solo en la primera ejecución
|
||||
if [ ! -f "$FIRST_RUN_FLAG" ]; then
|
||||
echo "=== PRIMERA EJECUCIÓN DETECTADA ==="
|
||||
|
||||
echo "Ejecutando migraciones y seeders..."
|
||||
if composer run db:prod; then
|
||||
echo "✓ db:prod completado"
|
||||
else
|
||||
echo "ERROR: Falló db:prod"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Marcar como completado
|
||||
touch "$FIRST_RUN_FLAG"
|
||||
echo "✓ Primera ejecución completada exitosamente"
|
||||
else
|
||||
echo "✓ No es primera ejecución, omitiendo setup inicial"
|
||||
fi
|
||||
|
||||
echo "=== Iniciando PHP-FPM ==="
|
||||
|
||||
# Iniciar PHP-FPM
|
||||
exec "$@"
|
||||
@ -1,23 +1,42 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Netbien\CashCloseController;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Rutas del núcleo de la aplicación.
|
||||
*
|
||||
*
|
||||
* Se recomienda que no se modifiquen estas rutas a menos que sepa lo que está haciendo.
|
||||
*/
|
||||
require('core.php');
|
||||
|
||||
/**
|
||||
* Rutas de tu aplicación.
|
||||
*
|
||||
*
|
||||
* Estas rutas son de la aplicación AP I que desarrollarás. Siéntete libre de agregar lo que consideres necesario.
|
||||
* Procura revisar que no existan rutas que entren en conflicto con las rutas del núcleo.
|
||||
*/
|
||||
|
||||
/** Rutas protegidas (requieren autenticación) */
|
||||
Route::middleware('auth:api')->group(function() {
|
||||
// Tus rutas protegidas
|
||||
|
||||
Route::resource('sim-cards', SimCardController::class);
|
||||
|
||||
Route::resource('packages', PackagesController::class);
|
||||
|
||||
Route::get('clients/search', [ClientController::class, 'search']);
|
||||
Route::resource('clients', ClientController::class);
|
||||
|
||||
Route::resource('sales', SaleController::class);
|
||||
|
||||
Route::get('cash-closes', [CashCloseController::class, 'index']);
|
||||
Route::put('cash-closes/close', [CashCloseController::class, 'CloseCashClose']);
|
||||
Route::get('cash-closes/report', [CashCloseController::class, 'report']);
|
||||
Route::get('cash-closes/export', [CashCloseController::class, 'exportReport']);
|
||||
});
|
||||
|
||||
/** Rutas públicas */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user