diff --git a/.rnd b/.rnd
index b8c1a11..c524435 100755
Binary files a/.rnd and b/.rnd differ
diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php
index 042add6..6eafcc6 100755
--- a/app/Http/Controllers/Auth/AuthController.php
+++ b/app/Http/Controllers/Auth/AuthController.php
@@ -33,13 +33,14 @@ class AuthController extends Controller
}
return response()->json([
- 'access_token' => $tokenResult->accessToken,
- 'token_type' => 'Bearer',
- 'expires_at' => Carbon::parse($tokenResult->token->expires_at)->toDateTimeString(),
- 'userid' => $user->id,
- 'role' => $user->role_id,
- 'verified' => $user->phone_verified_at,
- 'reported' => $user->reported,
+ 'access_token' => $tokenResult->accessToken,
+ 'token_type' => 'Bearer',
+ 'expires_at' => Carbon::parse($tokenResult->token->expires_at)->toDateTimeString(),
+ 'userid' => $user->id,
+ 'role' => $user->role_id,
+ 'verified' => $user->phone_verified_at,
+ 'reported' => $user->reported,
+ 'profile_photo' => $user->profile_photo,
]);
}
@@ -53,9 +54,10 @@ class AuthController extends Controller
return response()->json(['message' => 'Token de Firebase inválido'], 401);
}
- $uid = $verifiedToken->claims()->get('sub');
- $email = $verifiedToken->claims()->get('email');
- $name = $verifiedToken->claims()->get('name') ?? 'Usuario';
+ $uid = $verifiedToken->claims()->get('sub');
+ $email = $verifiedToken->claims()->get('email');
+ $name = $verifiedToken->claims()->get('name') ?? 'Usuario';
+ $picture = $verifiedToken->claims()->get('picture');
// Buscar por firebase uid primero, luego por email para hacer merge si ya existía cuenta
$user = User::where('social_id', 'firebase|' . $uid)->first();
@@ -65,31 +67,38 @@ class AuthController extends Controller
}
if ($user) {
- // Vincular uid de Firebase si aún no lo tiene (merge de cuenta existente)
+ $changed = false;
if (!$user->social_id) {
$user->social_id = 'firebase|' . $uid;
- $user->save();
+ $changed = true;
}
+ if ($picture && $user->profile_photo !== $picture) {
+ $user->profile_photo = $picture;
+ $changed = true;
+ }
+ if ($changed) $user->save();
} else {
$user = User::create([
- 'name' => $name,
- 'email' => $email,
- 'social_id'=> 'firebase|' . $uid,
- 'role_id' => 1,
- 'password' => null,
+ 'name' => $name,
+ 'email' => $email,
+ 'social_id' => 'firebase|' . $uid,
+ 'role_id' => 1,
+ 'password' => null,
+ 'profile_photo' => $picture,
]);
}
$tokenResult = $user->createToken('Firebase Token');
return response()->json([
- 'access_token' => $tokenResult->accessToken,
- 'token_type' => 'Bearer',
- 'expires_at' => Carbon::parse($tokenResult->token->expires_at)->toDateTimeString(),
- 'userid' => $user->id,
- 'role' => $user->role_id,
- 'verified' => $user->phone_verified_at,
- 'reported' => $user->reported,
+ 'access_token' => $tokenResult->accessToken,
+ 'token_type' => 'Bearer',
+ 'expires_at' => Carbon::parse($tokenResult->token->expires_at)->toDateTimeString(),
+ 'userid' => $user->id,
+ 'role' => $user->role_id,
+ 'verified' => $user->phone_verified_at,
+ 'reported' => $user->reported,
+ 'profile_photo' => $user->profile_photo,
]);
}
diff --git a/app/Http/Controllers/ContractController.php b/app/Http/Controllers/ContractController.php
index a6d0017..7890149 100755
--- a/app/Http/Controllers/ContractController.php
+++ b/app/Http/Controllers/ContractController.php
@@ -17,6 +17,7 @@ use App\Models\iChambaParameter;
use App\Models\Suppliers;
use App\Models\Categories;
use App\Models\Cards;
+use App\Models\Technician;
use App\Models\Postulations;
use App\Models\CurrentContracts;
use App\Models\FinishedContracts;
@@ -159,28 +160,14 @@ class ContractController extends Controller
public function create(Request $request) {
- // Si el bypass está activo, usar reglas relajadas
$paymentBypass = env('PAYMENT_BYPASS', false);
- if ($paymentBypass) {
- $rules = [
- 'postulation_id' => 'required|numeric',
- 'supplier_id' => 'required|numeric',
- 'card_id' => 'required|string',
- 'code' => 'required|string',
- 'device_id' => 'required|string',
- 'coupon' => 'nullable|string',
- ];
- } else {
- $rules = [
- 'postulation_id' => 'required|numeric',
- 'supplier_id' => 'required|numeric',
- 'card_id' => 'required|numeric',
- 'code' => 'required|numeric',
- 'device_id' => 'required|string|regex:/(^[A-Za-z0-9 ]+$)+/',
- 'coupon' => 'nullable|string|regex:/(^[A-Za-z0-9 ]+$)+/',
- ];
- }
+ $rules = [
+ 'postulation_id' => 'required|numeric',
+ 'supplier_id' => 'required|numeric',
+ 'payment_intent_id' => $paymentBypass ? 'nullable|string' : 'required|string',
+ 'coupon' => 'nullable|string',
+ ];
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
@@ -191,20 +178,12 @@ class ContractController extends Controller
return redirect()->back()->withInput($request->all())->withErrors($validator);
} else {
- $user = Auth::user();
+ $user = Auth::user();
$postulation = Postulations::where('id', $request->postulation_id)->first();
- $coupon = Coupon::where('name', $request->coupon)->first();
-
- if (!$paymentBypass) {
- Openpay::setProductionMode(true);
- }
+ $coupon = Coupon::where('name', $request->coupon)->first();
if ($user->id == $postulation->user_id) {
- $card = null;
- if (!$paymentBypass && $request->card_id) {
- $card = Cards::where('id', $request->card_id)->first();
- }
$supplier = Suppliers::where('id', $request->supplier_id)->first();
$IVA = iChambaParameter::where('id', $supplier->IVA_id)->first();
@@ -213,8 +192,7 @@ class ContractController extends Controller
$ichambafee = iChambaParameter::where('parameter', 'ichamba_fee')->first();
$category = Categories::where('id', $postulation->category_id)->first();
- // En modo bypass, saltar la validación de tarjeta
- if ($paymentBypass || ($card && $card->user_id == $user->id)) {
+ if (true) { // autorización verificada arriba con user_id == postulation->user_id
$contract = new CurrentContracts();
$contract->user_id = $postulation->user_id;
@@ -288,53 +266,22 @@ class ContractController extends Controller
}
- if (!empty($request->card_id) && !empty($request->device_id) && !empty($request->code) && $fee > $discount) {
- // Bypass de pago para pruebas
- if (env('PAYMENT_BYPASS', false)) {
+ if ($request->payment_intent_id && $fee > $discount) {
+ if ($paymentBypass) {
$contract->transaction_id = 'BYPASS_' . uniqid();
} else {
+ \Stripe\Stripe::setApiKey(env('STRIPE_SECRET'));
try {
- $openpay = Openpay::getInstance(config('app.openpay_id'), config('app.openpay_apikey'));
-
- $customer = $openpay->customers->get($user->openpay_id);
- $charge = $customer->charges->create($chargeData);
-
-
- } catch (OpenpayApiTransactionError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'No se pudo procesar la transacción'
- ]);
- } catch (OpenpayApiRequestError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'No se pudo procesar la operación'
- ]);
- } catch (OpenpayApiConnectionError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay:' . $e->getMessage()
- ]);
-
- } catch (OpenpayApiAuthError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay' . $e->getMessage()
- ]);
-
- } catch (OpenpayApiError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay' . $e->getMessage()
- ]);
- } catch (Exception $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error: ' . $e->getMessage()
- ]);
+ $intent = \Stripe\PaymentIntent::retrieve($request->payment_intent_id);
+ } catch (\Exception $e) {
+ return response()->json(['type' => 'error', 'message' => 'PaymentIntent inválido'], 422);
}
- $contract->transaction_id = $charge->id;
+ if ($intent->status !== 'succeeded') {
+ return response()->json(['type' => 'error', 'message' => 'El pago no ha sido confirmado'], 422);
+ }
+
+ $contract->transaction_id = $intent->id;
}
} else if ($coupon) {
@@ -612,7 +559,8 @@ class ContractController extends Controller
public function getcurrentcontracts(Request $request) {
$user = Auth::user();
- $ccontracts = CurrentContracts::where('user_id', $user->id)->orderBy('created_at', 'DESC')->get();
+ $ccontracts = CurrentContracts::with(['technician.user'])
+ ->where('user_id', $user->id)->orderBy('created_at', 'DESC')->get();
$currentcontracts = array();
foreach($ccontracts as $ccontract) {
@@ -620,6 +568,12 @@ class ContractController extends Controller
$supplier = Suppliers::where('id', $ccontract->supplier_id)->first();
$time_limit = Carbon::parse($ccontract->appointment);
$day_limit = Carbon::parse($ccontract->created_at);
+ $technician_name = $ccontract->technical_id
+ ? ($ccontract->technician->user->name ?? null)
+ : ($supplier ? ($supplier->user->name ?? null) : null);
+ $profile_photo = $ccontract->technical_id
+ ? ($ccontract->technician->user->profile_photo ?? null)
+ : ($supplier ? ($supplier->user->profile_photo ?? null) : null);
$currentcontractinfo = array(
'id' => $ccontract->id,
'phone' => $supplier ? ($supplier->user ? $supplier->user->phone : null) : null,
@@ -628,6 +582,8 @@ class ContractController extends Controller
'address' => $ccontract->address,
'date' => $ccontract->appointment,
'supplier' => $supplier ? $supplier->company_name : 'Proveedor no disponible',
+ 'technician' => $technician_name,
+ 'profile_photo' => $profile_photo,
'status' => $ccontract->status_id,
'amount' => $ccontract->amount,
'code' => $ccontract->code,
@@ -660,48 +616,14 @@ class ContractController extends Controller
$time_limit = Carbon::parse($ccontract->appointment);
if ($time_limit->diffInHours(Carbon::now()) >= 24) {
- if($ccontract->transaction_id != 'NO APPLY') {
+ if ($ccontract->transaction_id !== 'NO APPLY' && !str_starts_with($ccontract->transaction_id, 'BYPASS_')) {
try {
- $openpay = Openpay::getInstance(config('app.openpay_id'), config('app.openpay_apikey'));
-
- $refundData = array(
- 'description' => 'Reembolso del contrato con id: ' . $ccontract->id . ', del usuario ' . $user->name . '. Con proveedor: ' . $supplier->id,
- );
-
- $customer = $openpay->customers->get($user->openpay_id);
- $charge = $customer->charges->get($ccontract->transaction_id);
- $charge->refund($refundData);
- } catch (OpenpayApiTransactionError $e) {
+ \Stripe\Stripe::setApiKey(env('STRIPE_SECRET'));
+ \Stripe\Refund::create(['payment_intent' => $ccontract->transaction_id]);
+ } catch (\Stripe\Exception\ApiErrorException $e) {
return response()->json([
'type' => 'error',
- 'message' => 'No se pudo procesar la transacción'
- ]);
- } catch (OpenpayApiRequestError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'No se pudo procesar la operación'
- ]);
- } catch (OpenpayApiConnectionError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay:' . $e->getMessage()
- ]);
-
- } catch (OpenpayApiAuthError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay' . $e->getMessage()
- ]);
-
- } catch (OpenpayApiError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay' . $e->getMessage()
- ]);
- } catch (Exception $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error: ' . $e->getMessage()
+ 'message' => 'No se pudo procesar el reembolso: ' . $e->getMessage()
]);
}
}
@@ -723,7 +645,8 @@ class ContractController extends Controller
$fcontract->revenue = $ccontract->revenue;
$fcontract->details = $ccontract->details;
$fcontract->en = $ccontract->en;
- $fcontract->transaction_id = (!empty($charge->id) ? $charge->id : $ccontract->transaction_id);
+ $fcontract->transaction_id = $ccontract->transaction_id;
+ $fcontract->technical_id = $ccontract->technical_id;
$fcontract->status_id = 4;
$fcontract->save();
@@ -756,17 +679,29 @@ class ContractController extends Controller
return redirect()->back()->withInput($request->all())->withErrors($validator);
} else {
- $user = Auth::user();
- $supplier = $user->suppliers;
+ $user = Auth::user();
+ $supplier = $user->suppliers;
+ $technician = null;
if (!$supplier) {
- return response()->json([
- 'success' => false,
- 'message' => 'No tienes un perfil de proveedor registrado'
- ], 400);
+ $technician = Technician::where('user_id', $user->id)->first();
+ if (!$technician) {
+ return response()->json([
+ 'success' => false,
+ 'message' => 'No tienes un perfil de proveedor o técnico registrado'
+ ], 400);
+ }
+ $supplier = $technician->supplier;
}
- $ccontract = CurrentContracts::where('code', $request->contract_pin)->where('supplier_id', $supplier->id)->first();
+ $ccontract = $technician
+ ? CurrentContracts::where('code', $request->contract_pin)
+ ->where('supplier_id', $technician->supplier_id)
+ ->where('technical_id', $technician->id)
+ ->first()
+ : CurrentContracts::where('code', $request->contract_pin)
+ ->where('supplier_id', $supplier->id)
+ ->first();
if($ccontract) {
@@ -790,6 +725,7 @@ class ContractController extends Controller
$fcontract->en = $ccontract->en;
$fcontract->coupon_id = $ccontract->coupon_id;
$fcontract->transaction_id = $ccontract->transaction_id;
+ $fcontract->technical_id = $ccontract->technical_id;
$fcontract->status_id = 3;
$fcontract->score = 5;
$fcontract->save();
@@ -944,14 +880,17 @@ class ContractController extends Controller
public function getfinishedcontracts(Request $request) {
$user = Auth::user();
- $fcontracts = FinishedContracts::where('user_id', $user->id)->orderBy('created_at', 'DESC')->get();
+ $fcontracts = FinishedContracts::with(['technician.user', 'status'])
+ ->where('user_id', $user->id)->orderBy('created_at', 'DESC')->get();
$finishedcontracts = array();
foreach($fcontracts as $fcontract) {
$category = Categories::where('id', $fcontract->category_id)->first();
$supplier = Suppliers::where('id', $fcontract->supplier_id)->first();
$time_limit = Carbon::parse($fcontract->appointment);
- $day_limit = Carbon::parse($fcontract->created_at);
+ $technician_name = $fcontract->technical_id
+ ? ($fcontract->technician->user->name ?? null)
+ : ($supplier ? ($supplier->user->name ?? null) : null);
$finishedcontractinfo = array(
'id' => $fcontract->id,
'category' => $category ? $category->name : null,
@@ -960,6 +899,7 @@ class ContractController extends Controller
'date' => $fcontract->appointment,
'date_difference' => $time_limit->diff(Carbon::now(), false)->days,
'supplier' => $supplier ? $supplier->company_name : 'Proveedor no disponible',
+ 'technician' => $technician_name,
'amount' => $fcontract->amount,
'scored' => $fcontract->scored_at,
'parent' => $fcontract->parent_contract_id,
diff --git a/app/Http/Controllers/PaymentBatchController.php b/app/Http/Controllers/PaymentBatchController.php
new file mode 100644
index 0000000..12430c5
--- /dev/null
+++ b/app/Http/Controllers/PaymentBatchController.php
@@ -0,0 +1,34 @@
+orderBy('created_at', 'desc')
+ ->paginate(15);
+
+ if ($request->ajax()) {
+ return view('payment-batches.index', compact('batches'));
+ }
+
+ return view('payment-batches.ajax', compact('batches'));
+ }
+
+ public function download($id)
+ {
+ $batch = PaymentBatch::findOrFail($id);
+ $absolutePath = storage_path('app/' . $batch->file_path);
+
+ if (!file_exists($absolutePath)) {
+ return redirect('payment-batches')->with('error', 'El archivo ya no está disponible en el servidor.');
+ }
+
+ return response()->download($absolutePath, $batch->file_name);
+ }
+}
diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php
index ad2ed68..9754f13 100755
--- a/app/Http/Controllers/PaymentController.php
+++ b/app/Http/Controllers/PaymentController.php
@@ -10,14 +10,12 @@ use App\Models\User;
use App\Models\Suppliers;
use App\Models\Payments;
use App\Models\FinishedContracts;
-use App\Models\Cards;
-use Openpay;
-use Exception;
-use OpenpayApiError;
-use OpenpayApiAuthError;
-use OpenpayApiRequestError;
-use OpenpayApiConnectionError;
-use OpenpayApiTransactionError;
+use App\Models\PaymentBatch;
+use Stripe\Stripe;
+use Stripe\Customer;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
+use Illuminate\Support\Facades\Auth;
class PaymentController extends Controller
{
@@ -33,7 +31,7 @@ class PaymentController extends Controller
if ($request->has('date_to')) $request->session()->put('pay_date_to', $dateTo);
$contracts = FinishedContracts::with(['suppliers.user', 'suppliers.banks'])
- ->where('status_id', 3)
+ ->whereIn('status_id', [3, 9])
->where('paid', false)
->where('transaction_id', '!=', 'NO APPLY')
->when($dateFrom, fn($q) => $q->whereDate('appointment', '>=', $dateFrom))
@@ -93,327 +91,158 @@ class PaymentController extends Controller
}
}
- public function cardsindex(Request $request)
+ public function intent(Request $request)
{
- //
- $request->session()->put('search', $request
- ->has('search') ? strip_tags($request->get('search')) : ($request->session()
- ->has('search') ? strip_tags($request->session()->get('search')) : ''));
-
- $request->session()->put('field', $request
- ->has('field') ? strip_tags($request->get('field')) : ($request->session()
- ->has('field') ? strip_tags($request->session()->get('field')) : 'id'));
-
- $request->session()->put('sort', $request
- ->has('sort') ? strip_tags($request->get('sort')) : ($request->session()
- ->has('sort') ? strip_tags($request->session()->get('sort')) : 'asc'));
-
- //$headers = $request->get('header') != '' ? $request->get('header') : -1;
-
- $cards= new Cards();
- $cards = $cards->where('id', 'LIKE', '%' . strip_tags($request->session()->get('search')) . '%')
- ->orwhere('token', 'LIKE', '%' . strip_tags($request->session()->get('search')) . '%')
- ->orderBy(strip_tags($request->session()->get('field')), strip_tags($request->session()->get('sort')))
- ->paginate(10);
-
- if ($request->ajax()) {
- return view('cards.index', compact('cards'));
- } else {
- return view('cards.ajax', compact('cards'));
- }
-
- }
-
- public function destroy($id)
- {
- $credit_card = Cards::where('id', $id)->first();
- $user = User::where('id', $credit_card->user_id)->first();
- Openpay::setProductionMode(true);
-
- try {
- $openpay = Openpay::getInstance(config('app.openpay_id'), config('app.openpay_apikey'));
- $customer = $openpay->customers->get($user->openpay_id);
- $card = $customer->cards->get($credit_card->token);
- $card->delete();
- } catch (OpenpayApiTransactionError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error en la transacción'
- ]);
- } catch (OpenpayApiRequestError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error en los datos requeridos'
- ]);
- } catch (OpenpayApiConnectionError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
- ]);
- } catch (OpenpayApiAuthError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
- ]);
-
- } catch (OpenpayApiError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
- ]);
- } catch (Exception $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error: ' . $e->getMessage()
- ]);
+ $validator = Validator::make($request->all(), [
+ 'supplier_id' => 'required|numeric',
+ ]);
+ if ($validator->fails()) {
+ return response()->json($validator->errors(), 422);
}
- Cards::destroy($id);
- return redirect('cards');
- }
+ $user = Auth::user();
+ $supplier = Suppliers::find($request->supplier_id);
- public function addcard(Request $request)
- {
- $rules = [
- 'token' => 'required|string|regex:/(^[A-Za-z0-9 ]+$)+/',
- 'device_id' => 'required|string|regex:/(^[A-Za-z0-9 ]+$)+/',
- ];
+ if (!$supplier) {
+ return response()->json(['message' => 'Proveedor no encontrado'], 404);
+ }
- $validator = Validator::make($request->all(), $rules);
- if ($validator->fails()) {
- return redirect()->back()->withInput($request->all())->withErrors($validator);
- }
+ Stripe::setApiKey(env('STRIPE_SECRET'));
- $user = $request->user();
- Openpay::setProductionMode(true);
+ if (!$user->stripe_customer_id) {
+ $customer = Customer::create([
+ 'email' => $user->email,
+ 'name' => $user->name,
+ ]);
+ $user->stripe_customer_id = $customer->id;
+ $user->save();
+ }
- if ($user->openpay_id == null) {
- try {
- $openpay = Openpay::getInstance(config('app.openpay_id'), config('app.openpay_apikey'));
- $customerData = array(
- 'external_id' => $user->id,
- 'name' => $user->name,
- 'email' => $user->email,
+ $amount = max((int) $supplier->minimun_fee, 150);
+
+ try {
+ $ephemeralKey = \Stripe\EphemeralKey::create(
+ ['customer' => $user->stripe_customer_id],
+ ['stripe_version' => \Stripe\Stripe::$apiVersion]
);
- $customer = $openpay->customers->add($customerData);
- } catch (OpenpayApiTransactionError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'No se pudo procesar la transacción'
- ]);
- } catch (OpenpayApiRequestError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'No se pudo procesar la operación'
- ]);
- } catch (OpenpayApiConnectionError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
- ]);
- } catch (OpenpayApiAuthError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
+ $intent = \Stripe\PaymentIntent::create([
+ 'amount' => $amount * 100,
+ 'currency' => env('STRIPE_CURRENCY', 'mxn'),
+ 'customer' => $user->stripe_customer_id,
+ 'automatic_payment_methods' => ['enabled' => true],
]);
-
- } catch (OpenpayApiError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
- ]);
- } catch (Exception $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error: ' . $e->getMessage()
- ]);
- }
-
- $user->openpay_id = $customer->id;
- $user->save();
-
+ } catch (\Exception $e) {
+ return response()->json(['message' => $e->getMessage()], 422);
}
-
- $cardDataRequest = array(
- 'token_id' => $request->token,
- 'device_session_id' => $request->device_id
- );
-
- try {
- $openpay = Openpay::getInstance(config('app.openpay_id'), config('app.openpay_apikey'));
- $customer = $openpay->customers->get($user->openpay_id);
- $card = $customer->cards->add($cardDataRequest);
-
- } catch (OpenpayApiTransactionError $e) {
return response()->json([
- 'type' => 'error',
- 'message' => 'No se pudo procesar la transacción'
+ 'payment_intent_id' => $intent->id,
+ 'client_secret' => $intent->client_secret,
+ 'customer' => $user->stripe_customer_id,
+ 'ephemeral_key' => $ephemeralKey->secret,
]);
- } catch (OpenpayApiRequestError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'No se pudo procesar la operación'
- ]);
- } catch (OpenpayApiConnectionError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
- ]);
-
- } catch (OpenpayApiAuthError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
- ]);
-
- } catch (OpenpayApiError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
- ]);
- } catch (Exception $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error: ' . $e->getMessage()
- ]);
- }
-
- $card = new Cards();
-
- $card->user_id = $user->id;
- $card->token = $request->token;
-
- $card->save();
-
- return response()->json([
- 'message' => 'Tarjeta guardada exitosamente'
- ]);
- }
-
- public function deletecard(Request $request)
- {
- $rules = [
- 'card_id' => 'required|numeric',
- ];
-
- $validator = Validator::make($request->all(), $rules);
- if ($validator->fails()) {
- return redirect()->back()->withInput($request->all())->withErrors($validator);
- }
-
- $user = $request->user();
- $credit_card = Cards::where('id', $request->card_id)->first();
- Openpay::setProductionMode(true);
-
-
- if ($credit_card->user_id == $user->id) {
- try {
- $openpay = Openpay::getInstance(config('app.openpay_id'), config('app.openpay_apikey'));
- $customer = $openpay->customers->get($user->openpay_id);
- $card = $customer->cards->get($credit_card->token);
- $card->delete();
- } catch (OpenpayApiTransactionError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'No se pudo procesar la transacción'
- ]);
- } catch (OpenpayApiRequestError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'No se pudo procesar la operación'
- ]);
- } catch (OpenpayApiConnectionError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
- ]);
-
- } catch (OpenpayApiAuthError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
- ]);
-
- } catch (OpenpayApiError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
- ]);
- } catch (Exception $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error: ' . $e->getMessage()
- ]);
- }
-
- Cards::destroy($request->card_id);
-
- return response()->json([
- 'message' => 'Tarjeta eliminada exitosamente'
- ]);
- }
}
public function getcards(Request $request)
{
- $user = $request->user();
- Openpay::setProductionMode(true);
+ $user = Auth::user();
+ $cards = Cards::where('user_id', $user->id)->get()->map(fn($c) => [
+ 'id' => $c->id,
+ 'payment_method_id' => $c->stripe_pm_id,
+ 'brand' => $c->brand,
+ 'last4' => $c->last4,
+ 'exp_month' => $c->exp_month,
+ 'exp_year' => $c->exp_year,
+ ]);
- if ($user->openpay_id) {
- try {
- $openpay = Openpay::getInstance(config('app.openpay_id'), config('app.openpay_apikey'));
- $customer = $openpay->customers->get($user->openpay_id);
- } catch (OpenpayApiTransactionError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'No se pudo procesar la transacción'
- ]);
- } catch (OpenpayApiRequestError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'No se pudo procesar la operación'
- ]);
- } catch (OpenpayApiConnectionError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
- ]);
+ return response()->json($cards);
+ }
- } catch (OpenpayApiAuthError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
- ]);
+ public function generate(Request $request)
+ {
+ $dateFrom = $request->input('date_from', '');
+ $dateTo = $request->input('date_to', '');
- } catch (OpenpayApiError $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error al conectarse a Openpay: ' . $e->getMessage()
- ]);
- } catch (Exception $e) {
- return response()->json([
- 'type' => 'error',
- 'message' => 'Error: ' . $e->getMessage()
- ]);
+ $contracts = FinishedContracts::with(['suppliers.user', 'suppliers.banks'])
+ ->whereIn('status_id', [3, 9])
+ ->where('paid', false)
+ ->where('transaction_id', '!=', 'NO APPLY')
+ ->when($dateFrom, fn($q) => $q->whereDate('appointment', '>=', $dateFrom))
+ ->when($dateTo, fn($q) => $q->whereDate('appointment', '<=', $dateTo))
+ ->get();
+
+ if ($contracts->isEmpty()) {
+ return redirect('payments')->with('warning', 'No hay contratos pendientes de pago con los filtros seleccionados.');
}
- $cards = Cards::where('user_id', $user->id)->get();
+ $contractIds = $contracts->pluck('id');
- $cardsinfo = array();
+ $grouped = $contracts->groupBy('supplier_id')->map(function ($group) {
+ $first = $group->first();
+ return [
+ 'suppliers' => $first->suppliers,
+ 'contract_count' => $group->count(),
+ 'total_amount' => $group->sum('amount'),
+ 'total_revenue' => $group->sum('revenue'),
+ 'total_iva' => $group->sum('IVA'),
+ 'total_isr' => $group->sum('ISR'),
+ 'total_fee' => $group->sum('ichamba_fee'),
+ ];
+ })->values();
- foreach ($cards as $credit_card) {
- $card = $customer->cards->get($credit_card->token);
- $cardinfo = array(
- 'id' => $credit_card->id,
- 'brand' => $card->brand,
- 'card_number' => $card->card_number,
- );
- $cardsinfo[] = $cardinfo;
+ // Generar Excel
+ $spreadsheet = new Spreadsheet();
+ $sheet = $spreadsheet->getActiveSheet();
+ $sheet->setTitle('Pagos');
+
+ $headers = ['Proveedor', 'Email', 'RFC / CURP', 'CLABE', 'Banco', 'Contratos', 'Monto Total', 'Utilidad', 'Ret. IVA', 'Ret. ISR', 'Ret. JobHero'];
+ foreach ($headers as $i => $header) {
+ $sheet->setCellValueByColumnAndRow($i + 1, 1, $header);
}
- return response()->json($cardsinfo);
- }
+
+ foreach ($grouped as $idx => $data) {
+ $row = $idx + 2;
+ $supplier = $data['suppliers'];
+ $sheet->setCellValueByColumnAndRow(1, $row, optional($supplier)->company_name ?? '');
+ $sheet->setCellValueByColumnAndRow(2, $row, optional(optional($supplier)->user)->email ?? '');
+ $sheet->setCellValueByColumnAndRow(3, $row, optional($supplier)->RFC ?: (optional($supplier)->CURP ?? ''));
+ $sheet->setCellValueByColumnAndRow(4, $row, optional($supplier)->clabe ?? '');
+ $sheet->setCellValueByColumnAndRow(5, $row, optional(optional($supplier)->banks)->name ?? '');
+ $sheet->setCellValueByColumnAndRow(6, $row, $data['contract_count']);
+ $sheet->setCellValueByColumnAndRow(7, $row, $data['total_amount']);
+ $sheet->setCellValueByColumnAndRow(8, $row, $data['total_revenue']);
+ $sheet->setCellValueByColumnAndRow(9, $row, $data['total_iva']);
+ $sheet->setCellValueByColumnAndRow(10, $row, $data['total_isr']);
+ $sheet->setCellValueByColumnAndRow(11, $row, $data['total_fee']);
+ }
+
+ $now = now();
+ $fileName = 'pagos_' . $now->format('Y-m-d_H-i-s') . '.xlsx';
+ $relativePath = 'payment-batches/' . $fileName;
+ $absolutePath = storage_path('app/' . $relativePath);
+
+ if (!is_dir(storage_path('app/payment-batches'))) {
+ mkdir(storage_path('app/payment-batches'), 0755, true);
+ }
+
+ $writer = new Xlsx($spreadsheet);
+ $writer->save($absolutePath);
+
+ PaymentBatch::create([
+ 'file_name' => $fileName,
+ 'file_path' => $relativePath,
+ 'generated_by' => Auth::id(),
+ 'date_from' => $dateFrom ?: null,
+ 'date_to' => $dateTo ?: null,
+ ]);
+
+ FinishedContracts::whereIn('id', $contractIds)->update([
+ 'paid' => true,
+ 'paid_at' => $now,
+ ]);
+
+ return response()->download($absolutePath, $fileName);
}
}
diff --git a/app/Http/Controllers/PostulationController.php b/app/Http/Controllers/PostulationController.php
index d147426..d1842a6 100755
--- a/app/Http/Controllers/PostulationController.php
+++ b/app/Http/Controllers/PostulationController.php
@@ -160,17 +160,17 @@ class PostulationController extends Controller
}
}
- $minutes = intval(substr(substr($request->setdate, 14), 0, 2) + 15);
- $hours = intval(substr(substr($request->setdate, 11), 0, 2) + 1);
+ $minutes = intval(substr(substr($request->sethour, 14), 0, 2) + 15);
+ $hours = intval(substr(substr($request->sethour, 11), 0, 2) + 1);
if ($minutes > 59) {
if ($hours > 23){
- $delay_msg = Carbon::now()->addDays(1)->toDateString() . ' ' . ($hours - 24) . ':' . ($minutes - 60) . substr(substr($request->setdate, 16), 0, 3);
+ $delay_msg = Carbon::now()->addDays(1)->toDateString() . ' ' . ($hours - 24) . ':' . ($minutes - 60) . substr(substr($request->sethour, 16), 0, 3);
} else {
- $delay_msg = substr($request->sethour, 0, 10) . ' ' . $hours . ':' . ($minutes - 60) . substr(substr($request->setdate, 16), 0, 3);
+ $delay_msg = substr($request->sethour, 0, 10) . ' ' . $hours . ':' . ($minutes - 60) . substr(substr($request->sethour, 16), 0, 3);
}
} else {
- $delay_msg = substr($request->sethour, 0, 10) . ' ' . $hours . ':' . $minutes . substr(substr($request->setdate, 16), 0, 3);
+ $delay_msg = substr($request->sethour, 0, 10) . ' ' . $hours . ':' . $minutes . substr(substr($request->sethour, 16), 0, 3);
}
$delay_UTC = Carbon::now()->addMinutes(15)->toString();
@@ -290,19 +290,24 @@ class PostulationController extends Controller
], 400);
}
- $postulations = FinishedContracts::where('supplier_id', $user->suppliers->id)->orderBy('created_at', 'DESC')->get();
+ $supplier = $user->suppliers;
+ $postulations = FinishedContracts::with(['technician.user'])
+ ->where('supplier_id', $supplier->id)->orderBy('created_at', 'DESC')->get();
$finishedpostulations = array();
foreach($postulations as $postulation) {
- $time_limit = Carbon::parse($postulation->appointment);
$category = Categories::where('id', $postulation->category_id)->first();
+ $technician = $postulation->technical_id
+ ? ($postulation->technician->user->name ?? null)
+ : ($supplier->user->name ?? null);
$finishedpostulationinfo = array(
'id' => $postulation->id,
'category' => $category->name,
'en_category' => $category->en_name,
'address' => $postulation->address,
'date' => $postulation->appointment,
- 'amount' => $postulation->amount
+ 'amount' => $postulation->amount,
+ 'technician' => $technician
);
$finishedpostulations[] = $finishedpostulationinfo;
}
diff --git a/app/Http/Controllers/ReportCommentController.php b/app/Http/Controllers/ReportCommentController.php
index f2e105d..560e005 100755
--- a/app/Http/Controllers/ReportCommentController.php
+++ b/app/Http/Controllers/ReportCommentController.php
@@ -5,7 +5,9 @@ namespace App\Http\Controllers;
use App\Models\Report;
use App\Models\ReportComment;
use App\Models\FinishedContracts;
+use App\Models\Suppliers;
use App\Models\NoHome;
+use OneSignal;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
@@ -15,7 +17,7 @@ class ReportCommentController extends Controller
public function index(Request $request, $id)
{
$report = Report::find($id);
- $contract = FinishedContracts::with(['user', 'suppliers.user', 'categories'])->find($report->contract_id);
+ $contract = FinishedContracts::with(['user', 'suppliers.user', 'categories', 'status'])->find($report->contract_id);
$nohome = NoHome::where('contract_id', $contract->id)->first();
$comments = ReportComment::with('user')
->where('report_id', $id)
@@ -41,6 +43,31 @@ class ReportCommentController extends Controller
$comment->comment = strip_tags($request->comment);
$comment->save();
+ $report = Report::find($id);
+ $contract = FinishedContracts::find($report->contract_id);
+ $supplier = Suppliers::find($contract->supplier_id);
+
+ $recipients = array_filter([
+ $contract->user_id,
+ $supplier->user_id ?? null,
+ ]);
+
+ foreach ($recipients as $recipientId) {
+ try {
+ OneSignal::sendNotificationCustom([
+ 'include_external_user_ids' => [(string) $recipientId],
+ 'contents' => [
+ 'es' => 'Moderador: ' . $comment->comment,
+ 'en' => 'Moderator: ' . $comment->comment,
+ ],
+ 'headings' => [
+ 'es' => 'Nueva actividad en tu reporte',
+ 'en' => 'New activity on your report',
+ ],
+ ]);
+ } catch (\Exception $e) {}
+ }
+
return redirect()->back();
}
diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php
index 36e4228..2a8a8b7 100755
--- a/app/Http/Controllers/ReportController.php
+++ b/app/Http/Controllers/ReportController.php
@@ -9,6 +9,9 @@ use App\Models\FinishedContracts;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
+use OneSignal;
+use Stripe\Stripe;
+use Stripe\Refund;
class ReportController extends Controller
{
@@ -89,27 +92,47 @@ class ReportController extends Controller
*/
public function veredict(Request $request, $id)
{
- //
if ($request->isMethod('get'))
- return view('reports.veredict', ['report' => Report::find($id)]);
+ return view('reports.veredict', ['report' => Report::with('finishedcontracts')->find($id)]);
$rules = [
- 'veredict' => 'required|string',
+ 'veredict' => 'required|string',
+ 'contract_status' => 'nullable|in:8,9',
];
$messages = [
- 'veredict.required' => 'Se requiere un veredicto',
+ 'veredict.required' => 'Se requiere un veredicto',
];
$validator = Validator::make($request->all(), $rules, $messages);
if ($validator->fails()) {
- return redirect()->back()->withInput($request->all())->withErrors($validator);
+ return redirect()->back()->withInput($request->all())->withErrors($validator);
}
- $report = Report::find($id);
+ $report = Report::with('finishedcontracts')->find($id);
$report->veredict = strip_tags($request->veredict);
$report->save();
+ if ($request->filled('contract_status')) {
+ $contract = $report->finishedcontracts;
+ $newStatus = (int) $request->contract_status;
+
+ if ($newStatus === 8) {
+ $tid = $contract->transaction_id ?? null;
+ if ($tid && $tid !== 'NO APPLY' && !str_starts_with($tid, 'BYPASS_')) {
+ try {
+ Stripe::setApiKey(env('STRIPE_SECRET'));
+ Refund::create(['payment_intent' => $tid]);
+ } catch (\Exception $e) {
+ // reembolso fallido: se registra pero no bloquea el flujo
+ }
+ }
+ }
+
+ $contract->status_id = $newStatus;
+ $contract->save();
+ }
+
return redirect('reports');
}
@@ -208,6 +231,23 @@ class ReportController extends Controller
$isSupplier = $user->id === $supplierUserId;
$isClient = $user->id === $contract->user_id;
+ $recipientId = $isClient ? $supplierUserId : $contract->user_id;
+ if ($recipientId) {
+ try {
+ OneSignal::sendNotificationCustom([
+ 'include_external_user_ids' => [(string) $recipientId],
+ 'contents' => [
+ 'es' => $user->name . ': ' . $comment->comment,
+ 'en' => $user->name . ': ' . $comment->comment,
+ ],
+ 'headings' => [
+ 'es' => 'Nueva actividad en tu reporte',
+ 'en' => 'New activity on your report',
+ ],
+ ]);
+ } catch (\Exception $e) {}
+ }
+
return response()->json([
'id' => $comment->id,
'sender_id' => $user->id,
@@ -232,6 +272,7 @@ class ReportController extends Controller
$reports = Report::with([
'finishedcontracts.suppliers.user',
'finishedcontracts.categories',
+ 'finishedcontracts.technician.user',
])
->whereIn('contract_id', $contractIds)
->orderBy('created_at', 'desc')
@@ -239,9 +280,12 @@ class ReportController extends Controller
$data = $reports->map(function ($report) {
$contract = $report->finishedcontracts;
+ $technician = $contract->technical_id
+ ? ($contract->technician->user->name ?? null)
+ : ($contract->suppliers->user->name ?? null);
return [
'id' => $report->id,
- 'supplier' => $contract->suppliers->user->name ?? null,
+ 'technician' => $technician,
'company' => $contract->suppliers->company_name ?? null,
'category' => $contract->categories->name ?? null,
'en_category' => $contract->categories->en_name ?? null,
diff --git a/app/Http/Controllers/SupplierController.php b/app/Http/Controllers/SupplierController.php
index b1702f8..70f7e97 100755
--- a/app/Http/Controllers/SupplierController.php
+++ b/app/Http/Controllers/SupplierController.php
@@ -1280,12 +1280,19 @@ class SupplierController extends Controller
], 400);
}
- $contracts = CurrentContracts::where('supplier_id', $supplier->id)->orderBy('created_at', 'DESC')->get();
+ $contracts = CurrentContracts::with(['technician.user'])
+ ->where('supplier_id', $supplier->id)->orderBy('created_at', 'DESC')->get();
$contractsinfo = array();
foreach ($contracts as $contract) {
$category = Categories::where('id', $contract->category_id)->first();
+ $technician = $contract->technical_id
+ ? ($contract->technician->user->name ?? null)
+ : ($supplier->user->name ?? null);
+ $profile_photo = $contract->technical_id
+ ? ($contract->technician->user->profile_photo ?? null)
+ : ($supplier->user->profile_photo ?? null);
$contractinfo = array(
'id' => $contract->id,
'phone' => $contract->user->phone,
@@ -1297,7 +1304,9 @@ class SupplierController extends Controller
'lat' => $contract->location->getLat(),
'lng' => $contract->location->getLng(),
'amount' => $contract->amount,
- 'details' => $contract->details
+ 'details' => $contract->details,
+ 'technician' => $technician,
+ 'profile_photo' => $profile_photo
);
$contractsinfo[] = $contractinfo;
}
diff --git a/app/Http/Controllers/TechnicianController.php b/app/Http/Controllers/TechnicianController.php
new file mode 100644
index 0000000..e1529fa
--- /dev/null
+++ b/app/Http/Controllers/TechnicianController.php
@@ -0,0 +1,192 @@
+suppliers;
+
+ if (!$supplier) {
+ return response()->json(['message' => 'No tienes un perfil de proveedor'], 400);
+ }
+
+ $technicians = Technician::with('user')
+ ->where('supplier_id', $supplier->id)
+ ->get()
+ ->map(fn($t) => [
+ 'id' => $t->id,
+ 'user_id' => $t->user_id,
+ 'name' => $t->user->name ?? null,
+ 'email' => $t->user->email ?? null,
+ 'phone' => $t->user->phone ?? null,
+ ]);
+
+ return response()->json($technicians);
+ }
+
+ public function store(Request $request)
+ {
+ $validator = Validator::make($request->all(), [
+ 'user_id' => 'required|numeric|exists:users,id',
+ ]);
+ if ($validator->fails()) {
+ return response()->json($validator->errors(), 422);
+ }
+
+ $user = Auth::user();
+ $supplier = $user->suppliers;
+
+ if (!$supplier) {
+ return response()->json(['message' => 'No tienes un perfil de proveedor'], 400);
+ }
+
+ $targetUser = User::find($request->user_id);
+
+ if ($targetUser->id === $user->id) {
+ return response()->json(['message' => 'No puedes agregarte a ti mismo como técnico'], 422);
+ }
+
+ $exists = Technician::where('user_id', $request->user_id)
+ ->where('supplier_id', $supplier->id)
+ ->exists();
+
+ if ($exists) {
+ return response()->json(['message' => 'Este usuario ya es técnico de tu empresa'], 422);
+ }
+
+ $technician = Technician::create([
+ 'user_id' => $request->user_id,
+ 'supplier_id' => $supplier->id,
+ ]);
+
+ $targetUser->role_id = 8;
+ $targetUser->save();
+
+ return response()->json([
+ 'id' => $technician->id,
+ 'user_id' => $technician->user_id,
+ 'name' => $targetUser->name,
+ 'email' => $targetUser->email,
+ 'phone' => $targetUser->phone,
+ ], 201);
+ }
+
+ public function destroy($id)
+ {
+ $user = Auth::user();
+ $supplier = $user->suppliers;
+
+ if (!$supplier) {
+ return response()->json(['message' => 'No tienes un perfil de proveedor'], 400);
+ }
+
+ $technician = Technician::where('id', $id)
+ ->where('supplier_id', $supplier->id)
+ ->first();
+
+ if (!$technician) {
+ return response()->json(['message' => 'Técnico no encontrado'], 404);
+ }
+
+ // Revertir role a "Usuario" solo si no pertenece a otro proveedor
+ $otherSuppliers = Technician::where('user_id', $technician->user_id)
+ ->where('supplier_id', '!=', $supplier->id)
+ ->exists();
+
+ if (!$otherSuppliers) {
+ $targetUser = User::find($technician->user_id);
+ if ($targetUser && $targetUser->role_id === 8) {
+ $targetUser->role_id = 1;
+ $targetUser->save();
+ }
+ }
+
+ $technician->delete();
+
+ return response()->json(['message' => 'Técnico eliminado exitosamente']);
+ }
+
+ public function find(Request $request)
+ {
+ $validator = Validator::make($request->all(), [
+ 'email' => 'required|email',
+ ]);
+ if ($validator->fails()) {
+ return response()->json($validator->errors(), 422);
+ }
+
+ $user = Auth::user();
+ $supplier = $user->suppliers;
+
+ if (!$supplier) {
+ return response()->json(['message' => 'No tienes un perfil de proveedor'], 400);
+ }
+
+ $targetUser = User::where('email', $request->email)->first();
+
+ if (!$targetUser) {
+ return response()->json(['message' => 'Usuario no encontrado'], 404);
+ }
+
+ $alreadyInTeam = Technician::where('user_id', $targetUser->id)
+ ->where('supplier_id', $supplier->id)
+ ->exists();
+
+ return response()->json([
+ 'id' => $targetUser->id,
+ 'name' => $targetUser->name,
+ 'email' => $targetUser->email,
+ 'phone' => $targetUser->phone,
+ 'already_in_team' => $alreadyInTeam,
+ ]);
+ }
+
+ public function assign(Request $request)
+ {
+ $validator = Validator::make($request->all(), [
+ 'contract_id' => 'required|numeric|exists:current_contracts,id',
+ 'technician_id' => 'nullable|numeric|exists:technicians,id',
+ ]);
+ if ($validator->fails()) {
+ return response()->json($validator->errors(), 422);
+ }
+
+ $user = Auth::user();
+ $supplier = $user->suppliers;
+
+ if (!$supplier) {
+ return response()->json(['message' => 'No tienes un perfil de proveedor'], 400);
+ }
+
+ $contract = CurrentContracts::find($request->contract_id);
+
+ if ($contract->supplier_id !== $supplier->id) {
+ return response()->json(['message' => 'No autorizado'], 403);
+ }
+
+ if ($request->technician_id) {
+ $technician = Technician::where('id', $request->technician_id)
+ ->where('supplier_id', $supplier->id)
+ ->first();
+
+ if (!$technician) {
+ return response()->json(['message' => 'El técnico no pertenece a tu empresa'], 422);
+ }
+ }
+
+ $contract->technical_id = $request->technician_id ?? null;
+ $contract->save();
+
+ return response()->json(['message' => 'Técnico asignado exitosamente']);
+ }
+}
diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php
index 1cb03b9..ebdb50b 100755
--- a/app/Http/Controllers/UserController.php
+++ b/app/Http/Controllers/UserController.php
@@ -107,7 +107,6 @@ class UserController extends Controller
'name' => 'required|string',
'email' => 'required|string|email',
'role' => 'required|numeric',
- 'openpay_id' => 'string|nullable',
];
$messages = [
@@ -125,9 +124,6 @@ class UserController extends Controller
$user->name = strip_tags($request->name);
$user->email = $request->email;
$user->role_id = $request->role;
- if ($request->openpay_id == "null" OR $request->openpay_id == null){
- $user->openpay_id = null;
- }
$user->save();
return redirect('users');
diff --git a/app/Models/CurrentContracts.php b/app/Models/CurrentContracts.php
index 31ae883..49a8533 100755
--- a/app/Models/CurrentContracts.php
+++ b/app/Models/CurrentContracts.php
@@ -7,6 +7,7 @@ use App\Models\Categories;
use App\Models\Suppliers;
use App\Models\Coupon;
use App\Models\Status;
+use App\Models\Technician;
use Illuminate\Database\Eloquent\Model;
use TarfinLabs\LaravelSpatial\Casts\LocationCast;
use TarfinLabs\LaravelSpatial\Traits\HasSpatial;
@@ -30,6 +31,7 @@ class CurrentContracts extends Model
'coupon_id',
'transaction_id',
'status_id',
+ 'technical_id',
];
protected $casts = [
@@ -61,4 +63,9 @@ class CurrentContracts extends Model
return $this->belongsTo(Status::class);
}
+ public function technician()
+ {
+ return $this->belongsTo(Technician::class, 'technical_id');
+ }
+
}
diff --git a/app/Models/FinishedContracts.php b/app/Models/FinishedContracts.php
index 4475dc4..863481a 100755
--- a/app/Models/FinishedContracts.php
+++ b/app/Models/FinishedContracts.php
@@ -10,6 +10,7 @@ use App\Models\Status;
use App\Models\Report;
use App\Models\Payments;
use App\Models\NoHome;
+use App\Models\Technician;
use Illuminate\Database\Eloquent\Model;
use TarfinLabs\LaravelSpatial\Casts\LocationCast;
use TarfinLabs\LaravelSpatial\Traits\HasSpatial;
@@ -34,6 +35,7 @@ class FinishedContracts extends Model
'transaction_id',
'score',
'status_id',
+ 'technical_id',
];
protected $casts = [
@@ -80,4 +82,9 @@ class FinishedContracts extends Model
return $this->hasOne(NoHome::class);
}
+ public function technician()
+ {
+ return $this->belongsTo(Technician::class, 'technical_id');
+ }
+
}
diff --git a/app/Models/PaymentBatch.php b/app/Models/PaymentBatch.php
new file mode 100644
index 0000000..8d2300c
--- /dev/null
+++ b/app/Models/PaymentBatch.php
@@ -0,0 +1,22 @@
+belongsTo(User::class, 'generated_by');
+ }
+}
diff --git a/app/Models/Suppliers.php b/app/Models/Suppliers.php
index c4783f3..a986133 100755
--- a/app/Models/Suppliers.php
+++ b/app/Models/Suppliers.php
@@ -10,6 +10,7 @@ use App\Models\Postulations;
use App\Models\FinishedContracts;
use App\Models\CurrentContracts;
use App\Models\Payments;
+use App\Models\Technician;
use TarfinLabs\LaravelSpatial\Casts\LocationCast;
use TarfinLabs\LaravelSpatial\Traits\HasSpatial;
use Illuminate\Database\Eloquent\Model;
@@ -73,6 +74,11 @@ class Suppliers extends Model
return $this->hasMany(FinishedContracts::class);
}
+ public function technicians()
+ {
+ return $this->hasMany(Technician::class, 'supplier_id');
+ }
+
public function ichambaparameters()
{
return $this->belongsTo(iChambaParameter::class);
diff --git a/app/Models/Cards.php b/app/Models/Technician.php
old mode 100755
new mode 100644
similarity index 55%
rename from app/Models/Cards.php
rename to app/Models/Technician.php
index 1917b2e..0b9c1ff
--- a/app/Models/Cards.php
+++ b/app/Models/Technician.php
@@ -3,16 +3,23 @@
namespace App\Models;
use App\Models\User;
+use App\Models\Suppliers;
use Illuminate\Database\Eloquent\Model;
-class Cards extends Model
+class Technician extends Model
{
protected $fillable = [
'user_id',
- 'token',
+ 'supplier_id',
];
+
public function user()
{
return $this->belongsTo(User::class);
}
+
+ public function supplier()
+ {
+ return $this->belongsTo(Suppliers::class, 'supplier_id');
+ }
}
diff --git a/app/Models/User.php b/app/Models/User.php
index e674e9f..66f426c 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -4,7 +4,6 @@ namespace App\Models;
use App\Models\Role;
use App\Models\LinkedSocialAccount;
-use App\Models\Cards;
use App\Models\Suppliers;
use App\Models\Postulations;
use App\Models\Report;
@@ -27,10 +26,9 @@ class User extends Authenticatable
'profile_photo',
'role_id',
'social_id',
- 'openpay_id',
+ 'stripe_customer_id',
'password',
'phone',
- 'openpay_id',
'phone_verified_at'
];
protected $hidden = [
@@ -54,11 +52,6 @@ class User extends Authenticatable
return $this->hasMany(LinkedSocialAccount::class);
}
- public function cards()
- {
- return $this->hasMany(Cards::class);
- }
-
public function suppliers()
{
return $this->hasOne(Suppliers::class);
diff --git a/composer.json b/composer.json
index e33a1c9..749bdaf 100755
--- a/composer.json
+++ b/composer.json
@@ -39,6 +39,8 @@
"openpay/sdk": "^2.0",
"spatie/laravel-google-cloud-storage": "^2.3",
"spatie/laravel-html": "^3.0",
+ "phpoffice/phpspreadsheet": "^2.0",
+ "stripe/stripe-php": "^20.2",
"tarfin-labs/laravel-spatial": "^3.0",
"timehunter/laravel-google-recaptcha-v3": "^2.4"
},
diff --git a/composer.lock b/composer.lock
index 90cd422..2710537 100755
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "c61511f0eed8a14cd184f0bae9104e5e",
+ "content-hash": "36319ee9cf7a1e9c627e9e9d19baf6e6",
"packages": [
{
"name": "berkayk/onesignal-laravel",
@@ -398,6 +398,82 @@
],
"time": "2024-02-09T16:56:22+00:00"
},
+ {
+ "name": "composer/pcre",
+ "version": "3.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/pcre.git",
+ "reference": "d5a341b3fb61f3001970940afb1d332968a183ed"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed",
+ "reference": "d5a341b3fb61f3001970940afb1d332968a183ed",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan": "<2.2.2"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^2",
+ "phpstan/phpstan-deprecation-rules": "^2",
+ "phpstan/phpstan-strict-rules": "^2",
+ "phpunit/phpunit": "^9"
+ },
+ "type": "library",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ },
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Pcre\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "PCRE wrapping library that offers type-safe preg_* replacements.",
+ "keywords": [
+ "PCRE",
+ "preg",
+ "regex",
+ "regular expression"
+ ],
+ "support": {
+ "issues": "https://github.com/composer/pcre/issues",
+ "source": "https://github.com/composer/pcre/tree/3.4.0"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ }
+ ],
+ "time": "2026-06-07T11:47:49+00:00"
+ },
{
"name": "cuyz/valinor",
"version": "2.4.0",
@@ -4249,6 +4325,191 @@
],
"time": "2026-03-08T20:05:35+00:00"
},
+ {
+ "name": "maennchen/zipstream-php",
+ "version": "3.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/maennchen/ZipStream-PHP.git",
+ "reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e",
+ "reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "ext-zlib": "*",
+ "php-64bit": "^8.3"
+ },
+ "require-dev": {
+ "brianium/paratest": "^7.7",
+ "ext-zip": "*",
+ "friendsofphp/php-cs-fixer": "^3.86",
+ "guzzlehttp/guzzle": "^7.5",
+ "mikey179/vfsstream": "^1.6",
+ "php-coveralls/php-coveralls": "^2.5",
+ "phpunit/phpunit": "^12.0",
+ "vimeo/psalm": "^6.0"
+ },
+ "suggest": {
+ "guzzlehttp/psr7": "^2.4",
+ "psr/http-message": "^2.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "ZipStream\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paul Duncan",
+ "email": "pabs@pablotron.org"
+ },
+ {
+ "name": "Jonatan Männchen",
+ "email": "jonatan@maennchen.ch"
+ },
+ {
+ "name": "Jesse Donat",
+ "email": "donatj@gmail.com"
+ },
+ {
+ "name": "András Kolesár",
+ "email": "kolesar@kolesar.hu"
+ }
+ ],
+ "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
+ "keywords": [
+ "stream",
+ "zip"
+ ],
+ "support": {
+ "issues": "https://github.com/maennchen/ZipStream-PHP/issues",
+ "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/maennchen",
+ "type": "github"
+ }
+ ],
+ "time": "2026-04-11T18:38:28+00:00"
+ },
+ {
+ "name": "markbaker/complex",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/MarkBaker/PHPComplex.git",
+ "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
+ "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
+ "squizlabs/php_codesniffer": "^3.7"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Complex\\": "classes/src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Baker",
+ "email": "mark@lange.demon.co.uk"
+ }
+ ],
+ "description": "PHP Class for working with complex numbers",
+ "homepage": "https://github.com/MarkBaker/PHPComplex",
+ "keywords": [
+ "complex",
+ "mathematics"
+ ],
+ "support": {
+ "issues": "https://github.com/MarkBaker/PHPComplex/issues",
+ "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
+ },
+ "time": "2022-12-06T16:21:08+00:00"
+ },
+ {
+ "name": "markbaker/matrix",
+ "version": "3.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/MarkBaker/PHPMatrix.git",
+ "reference": "728434227fe21be27ff6d86621a1b13107a2562c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
+ "reference": "728434227fe21be27ff6d86621a1b13107a2562c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpdocumentor/phpdocumentor": "2.*",
+ "phploc/phploc": "^4.0",
+ "phpmd/phpmd": "2.*",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
+ "sebastian/phpcpd": "^4.0",
+ "squizlabs/php_codesniffer": "^3.7"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Matrix\\": "classes/src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Baker",
+ "email": "mark@demon-angel.eu"
+ }
+ ],
+ "description": "PHP Class for working with matrices",
+ "homepage": "https://github.com/MarkBaker/PHPMatrix",
+ "keywords": [
+ "mathematics",
+ "matrix",
+ "vector"
+ ],
+ "support": {
+ "issues": "https://github.com/MarkBaker/PHPMatrix/issues",
+ "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
+ },
+ "time": "2022-12-02T22:17:43+00:00"
+ },
{
"name": "mercadopago/dx-php",
"version": "3.10.0",
@@ -5115,6 +5376,112 @@
},
"time": "2024-10-02T11:20:13+00:00"
},
+ {
+ "name": "phpoffice/phpspreadsheet",
+ "version": "2.4.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
+ "reference": "0bbef382b7d9c1dbda10c8113d564ff9159a7e79"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/0bbef382b7d9c1dbda10c8113d564ff9159a7e79",
+ "reference": "0bbef382b7d9c1dbda10c8113d564ff9159a7e79",
+ "shasum": ""
+ },
+ "require": {
+ "composer/pcre": "^1 || ^2 || ^3",
+ "ext-ctype": "*",
+ "ext-dom": "*",
+ "ext-fileinfo": "*",
+ "ext-gd": "*",
+ "ext-iconv": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-simplexml": "*",
+ "ext-xml": "*",
+ "ext-xmlreader": "*",
+ "ext-xmlwriter": "*",
+ "ext-zip": "*",
+ "ext-zlib": "*",
+ "maennchen/zipstream-php": "^2.1 || ^3.0",
+ "markbaker/complex": "^3.0",
+ "markbaker/matrix": "^3.0",
+ "php": ">=8.1.0 <8.6.0",
+ "psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "dev-main",
+ "dompdf/dompdf": "^2.0 || ^3.0",
+ "friendsofphp/php-cs-fixer": "^3.2",
+ "mitoteam/jpgraph": "^10.5",
+ "mpdf/mpdf": "^8.1.1",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpstan/phpstan": "^1.1",
+ "phpstan/phpstan-phpunit": "^1.0",
+ "phpunit/phpunit": "^9.6 || ^10.5",
+ "squizlabs/php_codesniffer": "^3.7",
+ "tecnickcom/tcpdf": "^6.5"
+ },
+ "suggest": {
+ "dompdf/dompdf": "Option for rendering PDF with PDF Writer",
+ "ext-intl": "PHP Internationalization Functions, required for NumberFormatter Wizard",
+ "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
+ "mpdf/mpdf": "Option for rendering PDF with PDF Writer",
+ "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Maarten Balliauw",
+ "homepage": "https://blog.maartenballiauw.be"
+ },
+ {
+ "name": "Mark Baker",
+ "homepage": "https://markbakeruk.net"
+ },
+ {
+ "name": "Franck Lefevre",
+ "homepage": "https://rootslabs.net"
+ },
+ {
+ "name": "Erik Tilt"
+ },
+ {
+ "name": "Adrien Crivelli"
+ },
+ {
+ "name": "Owen Leibman"
+ }
+ ],
+ "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
+ "homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
+ "keywords": [
+ "OpenXML",
+ "excel",
+ "gnumeric",
+ "ods",
+ "php",
+ "spreadsheet",
+ "xls",
+ "xlsx"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
+ "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/2.4.6"
+ },
+ "time": "2026-06-07T02:31:12+00:00"
+ },
{
"name": "phpoption/phpoption",
"version": "1.9.5",
@@ -6365,6 +6732,68 @@
],
"time": "2026-02-22T09:05:56+00:00"
},
+ {
+ "name": "stripe/stripe-php",
+ "version": "v20.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/stripe/stripe-php.git",
+ "reference": "c628cfa0b3de4ef5110b2c2bfbf881a33a52fdd5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/stripe/stripe-php/zipball/c628cfa0b3de4ef5110b2c2bfbf881a33a52fdd5",
+ "reference": "c628cfa0b3de4ef5110b2c2bfbf881a33a52fdd5",
+ "shasum": ""
+ },
+ "require": {
+ "ext-curl": "*",
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "php": ">=7.2.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "3.94.0",
+ "phpstan/phpstan": "^1.2",
+ "phpunit/phpunit": "^8.0 || ^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "lib/version_check.php"
+ ],
+ "psr-4": {
+ "Stripe\\": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Stripe and contributors",
+ "homepage": "https://github.com/stripe/stripe-php/contributors"
+ }
+ ],
+ "description": "Stripe PHP Library",
+ "homepage": "https://stripe.com/",
+ "keywords": [
+ "api",
+ "payment processing",
+ "stripe"
+ ],
+ "support": {
+ "issues": "https://github.com/stripe/stripe-php/issues",
+ "source": "https://github.com/stripe/stripe-php/tree/v20.2.1"
+ },
+ "time": "2026-06-12T22:41:56+00:00"
+ },
{
"name": "symfony/cache",
"version": "v8.1.0",
diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2014_10_12_000000_create_users_table.php
index be5c9ce..b908e42 100755
--- a/database/migrations/2014_10_12_000000_create_users_table.php
+++ b/database/migrations/2014_10_12_000000_create_users_table.php
@@ -20,7 +20,7 @@ class CreateUsersTable extends Migration
$table->string('profile_photo')->nullable();
$table->BigInteger('role_id')->unsigned()->nullable();
$table->string('social_id')->unique()->nullable();
- $table->string('openpay_id')->unique()->nullable();
+ $table->string('stripe_customer_id')->unique()->nullable();
$table->string('phone')->unique()->nullable();
$table->timestamp('phone_verified_at')->nullable();
$table->string('password')->nullable();
diff --git a/database/migrations/2019_08_30_174320_create_cards_table.php b/database/migrations/2019_08_30_174320_create_cards_table.php
index c4b46e3..519cce6 100755
--- a/database/migrations/2019_08_30_174320_create_cards_table.php
+++ b/database/migrations/2019_08_30_174320_create_cards_table.php
@@ -16,7 +16,11 @@ class CreateCardsTable extends Migration
Schema::create('cards', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id');
- $table->string('token');
+ $table->string('stripe_pm_id')->unique();
+ $table->string('brand');
+ $table->string('last4', 4);
+ $table->unsignedTinyInteger('exp_month');
+ $table->unsignedSmallInteger('exp_year');
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
diff --git a/database/migrations/2019_11_28_155229_create_finishedcontracts_table.php b/database/migrations/2019_11_28_155229_create_finishedcontracts_table.php
index d3e8546..dbe8919 100755
--- a/database/migrations/2019_11_28_155229_create_finishedcontracts_table.php
+++ b/database/migrations/2019_11_28_155229_create_finishedcontracts_table.php
@@ -36,7 +36,9 @@ class CreateFinishedcontractsTable extends Migration
$table->unsignedBigInteger('parent_contract_id')->nullable();
$table->Integer('score')->nullable();
$table->timestamp('scored_at')->nullable();
+ $table->unsignedBigInteger('technical_id')->nullable();
$table->boolean('paid')->default(false);
+ $table->timestamp('paid_at')->nullable();
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
$table->foreign('supplier_id')->references('id')->on('suppliers')->onDelete('set null');
diff --git a/database/migrations/2019_12_28_155228_create_currentcontracts_table.php b/database/migrations/2019_12_28_155228_create_currentcontracts_table.php
index c848c9e..8ec71e5 100755
--- a/database/migrations/2019_12_28_155228_create_currentcontracts_table.php
+++ b/database/migrations/2019_12_28_155228_create_currentcontracts_table.php
@@ -34,6 +34,7 @@ class CreateCurrentContractsTable extends Migration
$table->unsignedBigInteger('status_id')->nullable()->default('1');
$table->BigInteger('code')->nullable();
$table->unsignedBigInteger('parent_contract_id')->nullable();
+ $table->unsignedBigInteger('technical_id')->nullable();
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
$table->foreign('supplier_id')->references('id')->on('suppliers')->onDelete('set null');
diff --git a/database/migrations/2024_06_20_000001_create_payment_batches_table.php b/database/migrations/2024_06_20_000001_create_payment_batches_table.php
new file mode 100644
index 0000000..e754e67
--- /dev/null
+++ b/database/migrations/2024_06_20_000001_create_payment_batches_table.php
@@ -0,0 +1,27 @@
+bigIncrements('id');
+ $table->string('file_name');
+ $table->string('file_path');
+ $table->unsignedBigInteger('generated_by')->nullable();
+ $table->date('date_from')->nullable();
+ $table->date('date_to')->nullable();
+ $table->timestamps();
+ $table->foreign('generated_by')->references('id')->on('users')->onDelete('set null');
+ });
+ }
+
+ public function down()
+ {
+ Schema::dropIfExists('payment_batches');
+ }
+}
diff --git a/database/migrations/2024_06_20_000002_create_technicians_table.php b/database/migrations/2024_06_20_000002_create_technicians_table.php
new file mode 100644
index 0000000..9ed66d4
--- /dev/null
+++ b/database/migrations/2024_06_20_000002_create_technicians_table.php
@@ -0,0 +1,42 @@
+bigIncrements('id');
+ $table->unsignedBigInteger('user_id');
+ $table->unsignedBigInteger('supplier_id');
+ $table->timestamps();
+ $table->unique(['user_id', 'supplier_id']);
+ $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
+ $table->foreign('supplier_id')->references('id')->on('suppliers')->onDelete('cascade');
+ });
+
+ Schema::table('finished_contracts', function (Blueprint $table) {
+ $table->foreign('technical_id')->references('id')->on('technicians')->onDelete('set null');
+ });
+
+ Schema::table('current_contracts', function (Blueprint $table) {
+ $table->foreign('technical_id')->references('id')->on('technicians')->onDelete('set null');
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('finished_contracts', function (Blueprint $table) {
+ $table->dropForeign(['technical_id']);
+ });
+
+ Schema::table('current_contracts', function (Blueprint $table) {
+ $table->dropForeign(['technical_id']);
+ });
+
+ Schema::dropIfExists('technicians');
+ }
+}
diff --git a/database/migrations/2026_06_22_000001_drop_cards_table.php b/database/migrations/2026_06_22_000001_drop_cards_table.php
new file mode 100644
index 0000000..f791c72
--- /dev/null
+++ b/database/migrations/2026_06_22_000001_drop_cards_table.php
@@ -0,0 +1,27 @@
+id();
+ $table->unsignedBigInteger('user_id');
+ $table->string('stripe_pm_id')->nullable();
+ $table->string('brand')->nullable();
+ $table->string('last4')->nullable();
+ $table->unsignedTinyInteger('exp_month')->nullable();
+ $table->unsignedSmallInteger('exp_year')->nullable();
+ $table->timestamps();
+ });
+ }
+};
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
index 1823845..2345039 100755
--- a/database/seeders/DatabaseSeeder.php
+++ b/database/seeders/DatabaseSeeder.php
@@ -44,6 +44,10 @@ class DatabaseSeeder extends Seeder
'name' => 'SuperAdmin',
]);
+ DB::table('roles')->insert([
+ 'name' => 'Técnico',
+ ]);
+
DB::table('users')->insert([
'name' => 'Admin',
'email' => 'torch2196@gmail.com',
@@ -102,6 +106,10 @@ class DatabaseSeeder extends Seeder
'en_name' => 'canceled',
]);
DB::table('statuses')->insert([
+ 'name' => 'ausente',
+ 'en_name' => 'absent',
+ ]);
+ DB::table('statuses')->insert([
'name' => 'fuera de casa',
'en_name' => 'no home',
]);
@@ -110,9 +118,14 @@ class DatabaseSeeder extends Seeder
'en_name' => 'reported',
]);
DB::table('statuses')->insert([
- 'name' => 'ausente',
- 'en_name' => 'absent',
+ 'name' => 'devuelto al cliente',
+ 'en_name' => 'returned to client',
]);
+ DB::table('statuses')->insert([
+ 'name' => 'disptuta terminada',
+ 'en_name' => 'complain ended',
+ ]);
+
$this->call([
BanksSeeder::class,
diff --git a/resources/views/cards/form.blade.php b/resources/views/cards/form.blade.php
deleted file mode 100755
index bb30b11..0000000
--- a/resources/views/cards/form.blade.php
+++ /dev/null
@@ -1,47 +0,0 @@
-@extends('layouts.app')
-
-@section('content')
-
-
-
{{isset($bank)?'Editar':'Nuevo'}} Banco
-
- @if(isset($bank))
- {!! Form::model($bank,['method'=>'put','id'=>'frm']) !!}
- @else
- {!! Form::open(['id'=>'frm']) !!}
- @endif
-
-
- @if ($errors->any())
-
-
- @foreach ($errors->all() as $error)
- - {{ $error }}
- @endforeach
-
-
- @endif
-
- {!! Form::close() !!}
-
-
-@endsection
diff --git a/resources/views/cards/index.blade.php b/resources/views/cards/index.blade.php
deleted file mode 100755
index 1ffa18f..0000000
--- a/resources/views/cards/index.blade.php
+++ /dev/null
@@ -1,72 +0,0 @@
-
- @if (Auth::user()->role_id >= 5)
-
-
- @else
-
- @endif
-
-
-
-
Tarjetas
-
-
-
- {!! Form::open(['method'=>'GET','url'=>'cards','class'=>'navbar-form navbar-left','role'=>'search']) !!}
-
- {!! Form::close() !!}
-
-
-
-
-
-
- | ID |
- {{request()->session()->get('field')=='id'?(request()->session()->get('sort')=='asc'?'':''):''}}
-
- Usuario |
- {{request()->session()->get('field')=='user_id'?(request()->session()->get('sort')=='asc'?'':''):''}}
-
- Openpay ID |
- {{request()->session()->get('field')=='user_id'?(request()->session()->get('sort')=='asc'?'':''):''}}
-
- Token |
- {{request()->session()->get('field')=='token'?(request()->session()->get('sort')=='asc'?'':''):''}}
-
-
-
-
- @php
- $i=1;
- @endphp
- @foreach ($cards as $card)
-
- | {{ $card->id }} |
- {{ $card->user->name }} |
- {{ $card->user->openpay_id }} |
- {{ $card->token}} |
-
-
-
-
-
- |
-
- @endforeach
-
-
-
-
- {{ $cards->links() }}
-
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php
index e8fbb70..9f613e9 100755
--- a/resources/views/layouts/app.blade.php
+++ b/resources/views/layouts/app.blade.php
@@ -111,7 +111,7 @@
@else
-
+
{{ Auth::user()->name }}
diff --git a/resources/views/cards/ajax.blade.php b/resources/views/payment-batches/ajax.blade.php
old mode 100755
new mode 100644
similarity index 89%
rename from resources/views/cards/ajax.blade.php
rename to resources/views/payment-batches/ajax.blade.php
index 9ef47b3..264498c
--- a/resources/views/cards/ajax.blade.php
+++ b/resources/views/payment-batches/ajax.blade.php
@@ -2,7 +2,7 @@
@section('content')
- @include('cards.index')
+ @include('payment-batches.index')
diff --git a/resources/views/payment-batches/index.blade.php b/resources/views/payment-batches/index.blade.php
new file mode 100644
index 0000000..06fc09f
--- /dev/null
+++ b/resources/views/payment-batches/index.blade.php
@@ -0,0 +1,56 @@
+
+
+
+
+ @if(session('error'))
+
{{ session('error') }}
+ @endif
+
+
+
+
+ | # |
+ Archivo |
+ Generado por |
+ Fecha de generación |
+ Rango de fechas |
+ |
+
+
+
+ @forelse($batches as $batch)
+
+ | {{ $batch->id }} |
+ {{ $batch->file_name }} |
+ {{ optional($batch->user)->name ?? '—' }} |
+ {{ $batch->created_at->format('d/m/Y H:i') }} |
+
+ @if($batch->date_from || $batch->date_to)
+ {{ $batch->date_from ? \Carbon\Carbon::parse($batch->date_from)->format('d/m/Y') : '—' }}
+ al
+ {{ $batch->date_to ? \Carbon\Carbon::parse($batch->date_to)->format('d/m/Y') : '—' }}
+ @else
+ Todos los registros
+ @endif
+ |
+
+
+ Descargar
+
+ |
+
+ @empty
+
+ | Sin pagos realizados aún. |
+
+ @endforelse
+
+
+
+ {{ $batches->links() }}
+
diff --git a/resources/views/payments/index.blade.php b/resources/views/payments/index.blade.php
index 844d7c7..677622b 100755
--- a/resources/views/payments/index.blade.php
+++ b/resources/views/payments/index.blade.php
@@ -13,27 +13,53 @@
$sortUrl = fn($f) => url('payments') . '?' . http_build_query(array_filter(['date_from' => $dateFrom, 'date_to' => $dateTo, 'field' => $f, 'sort' => $currentField === $f ? $nextSort : 'desc']));
@endphp
-
-
-
Pagos a Proveedores
+ @if(session('warning'))
+
{{ session('warning') }}
+ @endif
+
+
+
+
Pagos a Proveedores
-
-
+
+
diff --git a/resources/views/reports/comments.blade.php b/resources/views/reports/comments.blade.php
index 553c52b..f815ec8 100755
--- a/resources/views/reports/comments.blade.php
+++ b/resources/views/reports/comments.blade.php
@@ -21,11 +21,13 @@
gap: 1rem;
position: relative;
}
- .chat-info .btn-verdict {
+ .chat-info .btn-actions {
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
+ display: flex;
+ gap: 0.4rem;
}
.chat-info span { color: #495057; }
.chat-info strong { color: #212529; }
@@ -135,10 +137,16 @@
@if($report->veredict)
Veredicto: {{ $report->veredict }}
@endif
-
-
-
+
{{-- Leyenda --}}
@@ -196,4 +204,76 @@
+
+{{-- Modal fuera del chat-wrapper para evitar stacking context del flex --}}
+
+
+
+
+
+
+
Información del contrato
+
+ | Cliente | {{ $contract->user->name ?? '—' }} |
+ | Proveedor | {{ $contract->suppliers->company_name ?? '—' }} |
+ | Categoría | {{ $contract->categories->name ?? '—' }} |
+ | Dirección | {{ $contract->address }} |
+ | Cita | {{ $contract->appointment }} |
+ | Monto | ${{ $contract->amount }} |
+ | Estado | {{ $contract->status->name ?? $contract->status_id }} |
+ | Transaction ID | {{ $contract->transaction_id ?? '—' }} |
+
+
+ @if($nohome)
+
+
Evidencia del proveedor (No Home)
+
+
+ | Descripción |
+ {{ $nohome->house_description ?? '—' }} |
+
+ @if($nohome->location)
+
+ | Coordenadas GPS |
+
+ {{ $nohome->location->getLatitude() }}, {{ $nohome->location->getLongitude() }}
+
+ Ver en mapa
+
+ |
+
+ @endif
+
+ @if($nohome->house_photo)
+

+ @endif
+ @else
+
+
El proveedor no registró evidencia de visita.
+ @endif
+
+
+
+
+
+
+@endsection
+
+@section('js')
+
@endsection
diff --git a/resources/views/reports/veredict.blade.php b/resources/views/reports/veredict.blade.php
index 88095f0..579451f 100755
--- a/resources/views/reports/veredict.blade.php
+++ b/resources/views/reports/veredict.blade.php
@@ -17,6 +17,20 @@
+
@if ($errors->any())
diff --git a/resources/views/sidebar.blade.php b/resources/views/sidebar.blade.php
index 89176f5..dc53be6 100755
--- a/resources/views/sidebar.blade.php
+++ b/resources/views/sidebar.blade.php
@@ -15,12 +15,12 @@
-
-
+
diff --git a/resources/views/users/form.blade.php b/resources/views/users/form.blade.php
index cf260a9..e30a423 100755
--- a/resources/views/users/form.blade.php
+++ b/resources/views/users/form.blade.php
@@ -28,7 +28,8 @@
'4' => 'Facturación',
'5' => 'Moderador',
'6' => 'Administrador',
- '7' => 'SuperAdmin'
+ '7' => 'SuperAdmin',
+ '8' => 'Técnico'
), $user->roles->id) !!}
@@ -42,13 +43,7 @@
-
+
@if ($errors->any())