feat: branch betos — propiedades, oferta de proveedores, chat en contratos y flujo de postulacion renovado

- Sistema de propiedades: tabla, modelo, CRUD API para que el cliente guarde direcciones nombradas
- Postulacion renovada: cliente elige propiedad sin precio ni fecha, sube fotos a GCS/S3
- Proveedores ofertan date_1, date_2 y amount al postularse via pivot; sort por fecha/monto/membresia
- Contratacion: selected_date recibe date_1 o date_2, amount desde pivot (depreca minimun_fee)
- Cancelacion/repostulacion: archive con status=perdido, related_postulation_id para vincular
- Chat en contratos activos: tabla contract_comments, controller, rutas web+API, vista panel, notificaciones bilingues
- PaymentIntent calcula amount desde pivot en vez de recibirlo del front
- getpendingcontracts: sin filtro de tiempo, filtra por status=active, agrega time_created
- Expiracion de postulaciones reducida a 1000 min

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 11:26:10 -06:00
parent 0ed98c96ee
commit 700ddafba4
21 changed files with 944 additions and 180 deletions

View File

@@ -9,6 +9,8 @@ use Carbon\Carbon;
use App\Models\User;
use App\Models\Suppliers;
use App\Models\Payments;
use App\Models\Postulations;
use App\Models\Coupon;
use App\Models\FinishedContracts;
use App\Models\PaymentBatch;
use Stripe\Stripe;
@@ -94,19 +96,43 @@ class PaymentController extends Controller
public function intent(Request $request)
{
$validator = Validator::make($request->all(), [
'supplier_id' => 'required|numeric',
'postulation_id' => 'required|numeric|exists:postulations,id',
'supplier_id' => 'required|numeric|exists:suppliers,id',
'coupon' => 'nullable|string',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$user = Auth::user();
$supplier = Suppliers::find($request->supplier_id);
$user = Auth::user();
$postulation = Postulations::find($request->postulation_id);
if (!$supplier) {
return response()->json(['message' => 'Proveedor no encontrado'], 404);
if ($postulation->user_id !== $user->id) {
return response()->json(['message' => 'No autorizado'], 403);
}
$pivotSupplier = $postulation->suppliers()
->where('suppliers.id', $request->supplier_id)
->first();
if (!$pivotSupplier) {
return response()->json(['message' => 'El proveedor no se ha postulado a este servicio'], 404);
}
$minFee = 150;
$pivotAmount = $pivotSupplier->pivot->amount ?? $minFee;
$finalAmount = max($pivotAmount, $minFee);
$discount = 0;
if ($request->coupon) {
$coupon = Coupon::where('name', $request->coupon)->first();
if ($coupon && $coupon->limit > 0) {
$discount = ($finalAmount * (($coupon->percentage ?? 0) / 100)) + ($coupon->amount ?? 0);
}
}
$chargeAmount = max($finalAmount - $discount, 0);
Stripe::setApiKey(env('STRIPE_SECRET'));
if (!$user->stripe_customer_id) {
@@ -118,8 +144,6 @@ class PaymentController extends Controller
$user->save();
}
$amount = max((int) $supplier->minimun_fee, 150);
try {
$ephemeralKey = \Stripe\EphemeralKey::create(
['customer' => $user->stripe_customer_id],
@@ -127,7 +151,7 @@ class PaymentController extends Controller
);
$intent = \Stripe\PaymentIntent::create([
'amount' => $amount * 100,
'amount' => (int) ($chargeAmount * 100),
'currency' => env('STRIPE_CURRENCY', 'mxn'),
'customer' => $user->stripe_customer_id,
'automatic_payment_methods' => ['enabled' => true],
@@ -141,6 +165,7 @@ class PaymentController extends Controller
'client_secret' => $intent->client_secret,
'customer' => $user->stripe_customer_id,
'ephemeral_key' => $ephemeralKey->secret,
'amount' => $chargeAmount,
]);
}