getPaymentAmountForBooking($booking); return $payment > 0; } // $payment es la cantidad a pagar que puede venir desde el widget fijada por un custom layout // si no viene es porque hay que aplicar las reglas de pago normales public function gatewayPaymentDetails($service, $booking, $payment = null) { $restaurant = $booking->restaurant; // Calculo la cantidad a pagar segun las reglas $payment_calculated = $service->getPaymentAmountForBooking($booking); // Importe ya persistido en la reserva (se fija al pasar a pending_payment). // Las asignaciones manuales de oferta/pago no tienen offer rule, asi que el // calculo por reglas devuelve 0 — usamos el importe guardado para que la // booking view pueda mostrar el pago igualmente. $persisted_amount = ($booking->status == \Alexr\Enums\BookingStatus::PENDING_PAYMENT) ? 0.01 * intval($booking->amount) : 0; // Si no hay que pagar por las reglas entonces no hay que pagar el custom layout?? // no es cierto porque puede que no haya que pagar por dos comensales segun las reglas pero sin embargo si haya que pagar por la mesa // Por eso compruebo tambien que la mesa tiene una cantidad asignada que viene en payment if (($payment == null || $payment == 0) && $payment_calculated <= 0 && $persisted_amount <= 0) { return false; } // Aplicar reglas de pago, precio por asiento, etc. if ($payment == null || $payment == 0) { $payment = $payment_calculated; } // Desde la vista booking view cojo el que ya esta calculado // Solo aplica el override si la cantidad estaba ya guardada (>0). Para // reglas de offer con `percentage_of_menu`, el primer paso por // hold_booking tiene subtotal=0 y el booking->amount queda a 0; en // ese caso debemos usar el valor calculado, no el override de 0. if ($booking->status == \Alexr\Enums\BookingStatus::PENDING_PAYMENT && intval($booking->amount) > 0) { $payment = 0.01 * intval($booking->amount); //ray('STATUS => ' . $payment); } //ray('PAYMENT: ' . $payment); if ($payment > 0) { // Token needed for return url for gateways $token = \Evavel\Support\Str::upper(\Evavel\Support\Str::random(12)); $booking->gateway_token = $token; $booking->save(); $payment_gateways = []; // Woocommerce $woocommerce_args = $this->getWoocommercePaymentArgs($booking); if ($woocommerce_args) { $payment_gateways['woocommerce'] = $woocommerce_args; } // Stripe $stripe_args = $this->getStripePaymentArgs($booking); if ($stripe_args) { $payment_gateways['stripe'] = $stripe_args; } // Redsys $redsys_args = $this->getRedsysPaymentArgs($booking, $payment); if ($redsys_args) { $payment_gateways['redsys'] = $redsys_args; } // Paypal $paypal_args = $this->getPaypalPaymentArgs($booking); if ($paypal_args) { $payment_gateways['paypal'] = $paypal_args; } // MercadoPago $mercadopago_args = $this->getMercadopagoPaymentArgs($booking); if ($mercadopago_args) { $payment_gateways['mercadopago'] = $mercadopago_args; } // Mollie $mollie_args = $this->getMolliePaymentArgs($booking); if ($mollie_args) { $payment_gateways['mollie'] = $mollie_args; } // Square $square_args = $this->getSquarePaymentArgs($booking); if ($square_args) { $payment_gateways['square'] = $square_args; } // Xendit $xendit_args = $this->getXenditPaymentArgs($booking); if ($xendit_args) { $payment_gateways['xendit'] = $xendit_args; } // Upayments $upayments_args = $this->getUpaymentsPaymentArgs($booking); if ($upayments_args) { $payment_gateways['upayments'] = $upayments_args; } // Save amount required inside the booking if ($payment > 0) { $booking->amount = intval(100 * $payment); $booking->save(); } return [ 'payment_gateways' => $payment_gateways, 'required' => $payment > 0, // Define is the gateway has the option to capture cards 'is_capture_card' => $service->isPaymentToCapture(), 'amount' => $payment, 'currency' => strtolower($restaurant->currency), 'currency_symbol' => \Alexr\Enums\CurrencyType::symbolFor($restaurant->currency), 'message' => $service->getPaymentMessage(), 'return_url' => add_query_arg(['payment' => 'confirm'],evavel_view_booking_url($booking->uuid)), ]; } return false; } /** * Recompute the payment amount for a held booking after the customer * picks a menu in the offer step. Persists the offer menu selection on * the booking and returns the new `payment` payload (same shape as * `rr_hold_booking` returns). * * Expected POST/GET parameters: * - restaurantId * - uuid : holded booking uuid * - offer_id : selected offer id * - offer_rule_id : matched rule id (optional, informative) * - menu_lines : JSON-encoded array of selected items * - menu_subtotal : client-side subtotal (informative) * - nonce */ public function recompute_offer_payment() { $restaurant_id = intval($_REQUEST['restaurantId']); if (!$this->verify_nonce($restaurant_id)) { wp_send_json_error(['error' => __eva('Invalid nonce.')]); } $restaurant = \Alexr\Models\Restaurant::find($restaurant_id); if (!$restaurant) { wp_send_json_error(['error' => __eva('Restaurant not found.')]); } $uuid = isset($_REQUEST['uuid']) ? sanitize_text_field($_REQUEST['uuid']) : ''; if (!$uuid) { wp_send_json_error(['error' => __eva('Booking not found.')]); } $booking = \Alexr\Models\Booking::where('uuid', $uuid) ->where('status', '!=', \Alexr\Enums\BookingStatus::DELETED) ->first(); if (!$booking) { wp_send_json_error(['error' => __eva('Booking not found.')]); } $service = alexr_get_service($booking->shift_event_id); if (!$service) { wp_send_json_error(['error' => __eva('Service not found.')]); } // Sanity-check: the slot must currently match an offer rule. $rule = $service->getOfferRuleMatched($booking->date, $booking->time, $booking->party); if ($rule === null) { wp_send_json_error(['error' => __eva('No offer applies to this slot.')]); } $offer_id = isset($_REQUEST['offer_id']) ? intval($_REQUEST['offer_id']) : 0; if ($offer_id <= 0) { wp_send_json_error(['error' => __eva('Missing offer.')]); } $raw_lines = isset($_REQUEST['menu_lines']) ? $_REQUEST['menu_lines'] : null; if (is_string($raw_lines)) { $decoded = json_decode(stripslashes($raw_lines), true); $raw_lines = is_array($decoded) ? $decoded : []; } if (!is_array($raw_lines)) $raw_lines = []; // Sanitise the line items but trust the subtotal coming from the widget // (the customer reviewed it on screen before pressing Continue). $clean_lines = $service->sanitiseMenuLines($offer_id, $raw_lines, $booking->party); $rule_id_request = isset($_REQUEST['offer_rule_id']) ? sanitize_text_field($_REQUEST['offer_rule_id']) : null; $subtotal = isset($_REQUEST['menu_subtotal']) ? floatval($_REQUEST['menu_subtotal']) : 0; // Persist on the booking so subsequent payment endpoints (and // make_booking) can read the menu subtotal. $settings = $booking->settings; if (!is_array($settings)) $settings = []; $settings['offer_menu'] = [ 'offer_id' => $offer_id, 'offer_rule_id' => $rule_id_request, 'lines' => $clean_lines, 'subtotal' => $subtotal, ]; $booking->settings = $settings; $booking->save(); // Build the updated payment payload. // gatewayPaymentDetails reads booking->settings to derive the menu subtotal. $payment = $this->gatewayPaymentDetails($service, $booking); // gatewayPaymentDetails returns false when there's nothing to pay. if ($payment === false) { $payment = [ 'required' => false, 'amount' => 0, 'currency' => strtolower($restaurant->currency), 'currency_symbol' => \Alexr\Enums\CurrencyType::symbolFor($restaurant->currency), 'payment_gateways'=> [], 'is_capture_card' => false, 'message' => $service->getPaymentMessage(), ]; } wp_send_json_success([ 'payment' => $payment, 'menu_subtotal' => $subtotal, ]); } /** * Customer submits their menu selection from the booking view * (after_reservation offer flow). Persists the menu, then either defers * to the payment flow (when the rule requires payment) or just confirms. * * Uses the booking-uuid nonce ('booking-') — NOT the restaurant nonce. * * Expected POST parameters: * - restaurantId * - uuid : booking uuid * - nonce : wp_create_nonce('booking-') * - lang * - menu_lines : JSON array of { section_id, item_id, quantity } * - menu_subtotal : client-side subtotal (authoritative for percentage_of_menu) * - offer_id : optional, the offer the customer picked */ public function submit_booking_menu() { $restaurant_id = isset($_REQUEST['restaurantId']) ? intval($_REQUEST['restaurantId']) : 0; $uuid = isset($_REQUEST['uuid']) ? sanitize_text_field($_REQUEST['uuid']) : ''; $nonce = isset($_REQUEST['nonce']) ? $_REQUEST['nonce'] : ''; $lang = isset($_REQUEST['lang']) ? sanitize_text_field($_REQUEST['lang']) : 'en'; // Booking-scoped nonce, same one localized into the booking view. if (!$uuid || !wp_verify_nonce($nonce, 'booking-' . $uuid)) { wp_send_json_error(['error' => __eva('Invalid request. Please refresh the page.')]); } $booking = \Alexr\Models\Booking::where('uuid', $uuid) ->where('status', '!=', \Alexr\Enums\BookingStatus::DELETED) ->first(); if (!$booking || $booking->restaurant_id != $restaurant_id) { wp_send_json_error(['error' => __eva('Booking not found.')]); } $service = alexr_get_service($booking->shift_event_id); if (!$service || !method_exists($service, 'sanitiseMenuLines')) { wp_send_json_error(['error' => __eva('Service not found for this booking.')]); } // Stored selection (may be a MANUAL admin assignment from // BookingsController@offerPaymentSetup). $settings_pre = is_array($booking->settings) ? $booking->settings : []; $stored_offer_menu = (isset($settings_pre['offer_menu']) && is_array($settings_pre['offer_menu'])) ? $settings_pre['offer_menu'] : []; $manual_payment = (isset($stored_offer_menu['manual_payment']) && is_array($stored_offer_menu['manual_payment'])) ? $stored_offer_menu['manual_payment'] : null; $has_stored_offer = !empty($stored_offer_menu['offer_id']); // manual_payment is only ever written by the manual admin flow, so it is a // reliable fallback signal alongside the explicit 'manual' flag. $is_manual = !empty($stored_offer_menu['manual']) || $manual_payment !== null; // Re-match the offer rule (authoritative for rule-driven flows). A MANUAL // admin assignment is authoritative on its own: the stored offer + its // manual_payment win, and the shift's offer rules must NOT apply (they may // force "choose during booking" or impose a different price), so ignore // any matched rule for manual bookings. $rule = $is_manual ? null : $service->getOfferRuleMatched($booking->date, $booking->time, $booking->party); if ($rule === null && !$has_stored_offer) { wp_send_json_error(['error' => __eva('The menu selection is no longer available for this booking.')]); } // Offer id: matched rule > stored selection > request. $offer_id = ($rule !== null && isset($rule['offer_id'])) ? intval($rule['offer_id']) : 0; if ($offer_id <= 0 && $has_stored_offer) { $offer_id = intval($stored_offer_menu['offer_id']); } if ($offer_id <= 0) { $offer_id = isset($_REQUEST['offer_id']) ? intval($_REQUEST['offer_id']) : 0; } if ($offer_id <= 0) { wp_send_json_error(['error' => __eva('Missing offer.')]); } $raw_lines = isset($_REQUEST['menu_lines']) ? $_REQUEST['menu_lines'] : null; if (is_string($raw_lines)) { $decoded = json_decode(stripslashes($raw_lines), true); $raw_lines = is_array($decoded) ? $decoded : []; } if (!is_array($raw_lines)) $raw_lines = []; $clean_lines = $service->sanitiseMenuLines($offer_id, $raw_lines, $booking->party); $subtotal = isset($_REQUEST['menu_subtotal']) ? floatval($_REQUEST['menu_subtotal']) : 0; // Draft vs finalise: "Save and finish later" persists a partial selection // as 'incomplete' (no email, no payment); the default is to finalise. $finalize = isset($_REQUEST['finalize']) ? sanitize_text_field($_REQUEST['finalize']) : 'yes'; $is_finalize = ($finalize !== 'no'); // Re-hydration state for the menu selector (steppers/radios) so the // customer can resume a draft. Not used for pricing — the sanitised // lines above are the authoritative source. $raw_state = isset($_REQUEST['menu_state']) ? $_REQUEST['menu_state'] : null; if (is_string($raw_state)) { $decoded_state = json_decode(stripslashes($raw_state), true); $raw_state = is_array($decoded_state) ? $decoded_state : null; } $clean_state = $this->sanitiseMenuState($raw_state); // Persist the selection. $settings = is_array($booking->settings) ? $booking->settings : []; $offer_menu = (isset($settings['offer_menu']) && is_array($settings['offer_menu'])) ? $settings['offer_menu'] : []; $offer_menu['offer_id'] = $offer_id; $offer_menu['offer_rule_id'] = isset($rule['id']) ? $rule['id'] : (isset($offer_menu['offer_rule_id']) ? $offer_menu['offer_rule_id'] : null); $offer_menu['lines'] = $clean_lines; $offer_menu['subtotal'] = $subtotal; if ($clean_state !== null) { $offer_menu['state'] = $clean_state; } // DRAFT keeps the booking untouched (status 'incomplete', no email, no // payment); FINALISE marks it 'submitted' and runs the notify + payment // flow below. $offer_menu['menu_status'] = $is_finalize ? 'submitted' : 'incomplete'; $settings['offer_menu'] = $offer_menu; $booking->settings = $settings; $booking->save(); // DRAFT: stop here. Nothing else changes until the customer finalises. if (!$is_finalize) { wp_send_json_success([ 'requires_payment' => false, 'menu_status' => 'incomplete', 'draft' => true, ]); } // Notify the restaurant that the customer has FINISHED picking the menu // (system-wide "Menu submitted (Admin)" email; respects its Active // toggle). Guarded so it fires once even if the customer re-finalises or // the page reloads. Covers both the payment and no-payment paths. if (empty($offer_menu['submitted_admin_sent']) && method_exists($booking, 'sendEmailMenuSubmittedAdmin')) { $booking->sendEmailMenuSubmittedAdmin($lang); $offer_menu['submitted_admin_sent'] = true; $settings['offer_menu'] = $offer_menu; $booking->settings = $settings; $booking->save(); } // Deferred payment applies ONLY to percentage_of_menu (the amount depends // on the chosen menu). It comes from the matched rule, or — for a manual // admin assignment with no rule — from the stored manual_payment spec. $amount = 0; if ($rule !== null) { $required = isset($rule['payment_required']) ? $rule['payment_required'] : 'no'; $payment_type = isset($rule['payment_type']) ? $rule['payment_type'] : 'fixed'; if (($required === 'yes' || $required === 'yes_save') && $payment_type === 'percentage_of_menu') { $amount = floatval($service->getOfferRulePrice($rule, $booking->party, $subtotal)); } } else if ($manual_payment !== null && isset($manual_payment['payment_type']) && $manual_payment['payment_type'] === 'percentage_of_menu') { $pct = isset($manual_payment['price_percentage']) ? floatval($manual_payment['price_percentage']) : 0; $amount = ($pct > 0 && $subtotal > 0) ? ($subtotal * $pct / 100) : 0; } if ($amount > 0) { // Reset any persisted amount so gatewayPaymentDetails uses the freshly // computed $amount (it reuses booking->amount when already // pending_payment with amount > 0). $booking->amount = 0; $booking->status = \Alexr\Enums\BookingStatus::PENDING_PAYMENT; $booking->save(); $payment = $this->gatewayPaymentDetails($service, $booking, $amount); // After-reservation bookings have no gateway chosen at booking time // (the customer never reached a payment step). Without a gateway the // booking view can't render the "Pay now" button, so assign one from // the enabled gateways. Prefer an already-set gateway if present. if (empty($booking->gateway) && is_array($payment) && !empty($payment['payment_gateways'])) { $gateway_keys = array_keys($payment['payment_gateways']); if (!empty($gateway_keys)) { $booking->gateway = $gateway_keys[0]; $booking->save(); } } wp_send_json_success([ 'requires_payment' => true, 'payment' => $payment, 'gateway' => $booking->gateway, 'menu_status' => 'submitted', ]); } // No payment needed — record the menu, finalise the booking to the // operator-chosen status when set (manual offer setup), and notify. if (!empty($settings['final_status'])) { $booking->status = $settings['final_status']; } $booking->save(); $this->sendBookingNotifications($booking, $lang, false, false); wp_send_json_success([ 'requires_payment' => false, 'menu_status' => 'submitted', ]); } /** * Light sanitiser for the menu selector's re-hydration `state` object. * It is NOT used for pricing (lines are re-sanitised server-side); it only * repaints the steppers/radios when the customer resumes a draft. Coerces * the three quantity maps to (string section)=>(string item)=>(int qty) and * singleChoice to (string section)=>(string item|null). * * @param mixed $raw * @return array|null */ private function sanitiseMenuState($raw) { if (!is_array($raw)) { return null; } $qtyMap = function ($node) { $out = []; if (!is_array($node)) { return $out; } foreach ($node as $sectionId => $items) { if (!is_array($items)) { continue; } $sid = sanitize_text_field((string) $sectionId); $out[$sid] = []; foreach ($items as $itemId => $qty) { $iid = sanitize_text_field((string) $itemId); $out[$sid][$iid] = max(0, intval($qty)); } } return $out; }; $state = [ 'requiredSelections' => $qtyMap(isset($raw['requiredSelections']) ? $raw['requiredSelections'] : null), 'perGuestExtras' => $qtyMap(isset($raw['perGuestExtras']) ? $raw['perGuestExtras'] : null), 'bookingExtras' => $qtyMap(isset($raw['bookingExtras']) ? $raw['bookingExtras'] : null), 'singleChoice' => [], ]; if (isset($raw['singleChoice']) && is_array($raw['singleChoice'])) { foreach ($raw['singleChoice'] as $sectionId => $itemId) { $sid = sanitize_text_field((string) $sectionId); $state['singleChoice'][$sid] = ($itemId === null || $itemId === '') ? null : sanitize_text_field((string) $itemId); } } return $state; } function get_confirmed_booking_payments() { $restaurant_id = sanitize_text_field($_REQUEST['restaurantId']); if ( ! $this->verify_nonce('stripe-'.$restaurant_id) ) { wp_send_json_error(['error' => __eva('Invalid nonce. Try to refresh the page.')]); } $restaurant = \Alexr\Models\Restaurant::find($restaurant_id); if (!$restaurant){ wp_send_json_error([ 'message' => 'Wrong restaurant' ]); } $booking_uuid = sanitize_text_field($_REQUEST['uuid']); if (!$booking_uuid) { wp_send_json_error(['error' => __eva('Booking not found.')]); } $booking = \Alexr\Models\Booking::where('uuid', $booking_uuid)->first(); if (!$booking) { wp_send_json_error(['error' => __eva('Booking not found.')]); } $payments = \Alexr\Models\Payment::where('booking_id', $booking->id)->get(); if (!$payments) { wp_send_json_success(['payments' => []]); } wp_send_json_success(['payments' => $payments->toArrayEachModel()]); } public function webhook() { // @TODO para mercadopago die(); } }