restaurant_id); if (!$gateway->enabled) return null; $exchange = floatval(str_replace(',', '.', $gateway->currency_exchange)); if (!$exchange || $exchange == 0) { $exchange = 1.0; } return [ 'id' => 'xendit', 'title' => $gateway->title, 'description' => $gateway->description, 'currency' => $gateway->currency, 'exchange' => $exchange, 'public_key' => $gateway->getPublicKey(), 'can_capture_card' => \Alexr\Payments\GatewayXendit::$hasCaptureCards ]; } public function xendit_create() { $restaurant_id = intval($_REQUEST['restaurantId']); $booking_id = intval($_REQUEST['booking_id']); $nonce = $_REQUEST['nonce']; // Desde la view booking no se necesita el nonce if ($nonce != 'not_needed') { if ( ! $this->verify_nonce($restaurant_id) && ! $this->verify_nonce('stripe-'.$restaurant_id) ) { wp_send_json_error(['error' => __eva('Invalid nonce. Try to refresh the page.')]); } } if (!defined('ALEXR_PRO_VERSION')) { wp_send_json_error(['error' => __eva('Need the PRO plugin enabled.')]); } $gateway = new \Alexr\Payments\GatewayXendit($restaurant_id); $amount = floatval($_REQUEST['amount']); $amount = intval( 100.0 * $amount); $currency = sanitize_text_field($_REQUEST['currency']); $is_capture_card = sanitize_text_field($_REQUEST['is_capture_card']) === 'yes'; // Convert amount based on exchange rate $exchange = floatval(str_replace(',', '.', $gateway->currency_exchange)); if (!$exchange || $exchange == 0) { $exchange = 1.0; } $xendit_amount = intval($amount * $exchange); $booking = \Alexr\Models\Booking::find($booking_id); if (!$booking) { wp_send_json_error(['error' => __eva('Invalid booking')]); } try { if ($is_capture_card) { // Create invoice for card capture/pre-auth flow $invoice = $gateway->createInvoice($booking, $xendit_amount/100, $gateway->currency); $data = [ 'invoice' => $invoice, 'type' => 'capture_card' ]; $payment = $gateway->create_payment($booking_id, $xendit_amount, $data, \Alexr\Enums\PaymentStatus::PENDING); wp_send_json_success([ 'invoice_url' => $invoice['invoice_url'], 'invoice_id' => $invoice['id'], 'payment_id' => $payment->id ]); } else { // Create invoice for direct payment $invoice = $gateway->createInvoice($booking, $xendit_amount/100, $gateway->currency); $data = [ 'invoice' => $invoice, 'type' => 'direct_payment' ]; $payment = $gateway->create_payment($booking_id, $xendit_amount, $data, \Alexr\Enums\PaymentStatus::PENDING); wp_send_json_success([ 'invoice_url' => $invoice['invoice_url'], 'invoice_id' => $invoice['id'], 'payment_id' => $payment->id ]); } } catch (\Exception $e) { wp_send_json_error(['error' => $e->getMessage()]); } } public function xendit_webhook() { $token = sanitize_text_field($_REQUEST['token'] ?? ''); $raw_input = file_get_contents('php://input'); $payload = json_decode($raw_input, true); if (!$payload) { http_response_code(400); exit('Invalid payload'); } // Get restaurant_id from external_id $external_id = $payload['external_id'] ?? ''; // Gift voucher purchases (addon_gift_cards) use their own namespace: // 'voucher_{voucherId}_{time}'. Handled apart, never as a booking. if (preg_match('/^voucher_(\d+)_/', $external_id, $vmatches)) { $this->xendit_webhook_voucher(intval($vmatches[1]), $external_id, $token, $payload); exit('OK'); } if (!preg_match('/booking_(\d+)_/', $external_id, $matches)) { http_response_code(400); exit('Invalid external_id'); } $booking_id = intval($matches[1]); $booking = \Alexr\Models\Booking::find($booking_id); if (!$booking) { http_response_code(404); exit('Booking not found'); } $gateway = new \Alexr\Payments\GatewayXendit($booking->restaurant_id); if (!$gateway->validateWebhook($token, $payload)) { http_response_code(401); exit('Invalid signature'); } /*$payment = \Alexr\Models\Payment::where('booking_id', $booking_id) ->where('payment_type', 'xendit') ->first();*/ $payment = \Alexr\Models\Payment::where('xendit_external_id', $external_id) ->where('payment_type', 'xendit') ->first(); if (!$payment) { http_response_code(404); exit('Payment not found'); } // Update payment status based on webhook event $status = $payload['status'] ?? ''; switch ($status) { case 'PAID': $payment->status = \Alexr\Enums\PaymentStatus::SUCCEEDED; $payment->xendit_invoice_status = 'PAID'; $booking->status = $booking->paymentStatus; $booking->save(); $booking->sendBookingEmail(); $booking->sendSmsNotification(); $booking->sendWhatsappNotification(); break; case 'EXPIRED': $payment->status = \Alexr\Enums\PaymentStatus::CANCELLED; break; case 'FAILED': //$payment->status = \Alexr\Enums\PaymentStatus::FAILED; $payment->status = \Alexr\Enums\PaymentStatus::CANCELLED; break; } $payment->settings = array_merge($payment->settings ?? [], ['webhook' => $payload]); $payment->save(); http_response_code(200); exit('OK'); } public function xendit_fetch() { $uuid = sanitize_text_field($_REQUEST['uuid']); $booking = \Alexr\Models\Booking::where('uuid', $uuid)->first(); if (!$booking) { wp_send_json_error(['error' => __eva('Invalid booking.')]); } $data = $this->getXenditPaymentArgs($booking); $data['amount'] = intval($booking->amount) / 100.0; wp_send_json_success(['payment' => $data]); } /** * Xendit webhook for a GIFT VOUCHER invoice: validates the signature with the * voucher's restaurant, finds the voucher's Payment by external_id and, on * PAID (with the invoiced amount), issues the voucher. Idempotent. */ protected function xendit_webhook_voucher($voucher_id, $external_id, $token, $payload) { $voucher = \Alexr\Models\Voucher::find($voucher_id); if (!$voucher) { http_response_code(404); exit('Voucher not found'); } $gateway = new \Alexr\Payments\GatewayXendit($voucher->restaurant_id); if (!$gateway->validateWebhook($token, $payload)) { http_response_code(401); exit('Invalid signature'); } $payment = \Alexr\Models\Payment::where('xendit_external_id', $external_id) ->where('payment_type', 'xendit') ->where('voucher_id', $voucher->id) ->first(); if (!$payment) { http_response_code(404); exit('Payment not found'); } $status = strtoupper($payload['status'] ?? ''); $paidCents = intval(round(floatval($payload['paid_amount'] ?? ($payload['amount'] ?? 0)) * 100)); switch ($status) { case 'PAID': case 'SETTLED': if ($paidCents !== intval($payment->amount)) { break; // amount mismatch: keep pending for review } $payment->status = \Alexr\Enums\PaymentStatus::SUCCEEDED; $payment->xendit_invoice_status = 'PAID'; $payment->settings = array_merge($payment->settings ?? [], ['webhook' => $payload]); $payment->save(); if (alexr_gift_cards_enabled($voucher->restaurant_id) && $voucher->markPaid($payment)) { do_action('alexr_voucher_issued', $voucher); } http_response_code(200); return; case 'EXPIRED': case 'FAILED': $payment->status = \Alexr\Enums\PaymentStatus::CANCELLED; $payment->xendit_invoice_status = $status; if ($voucher->status === \Alexr\Enums\VoucherStatus::PENDING_PAYMENT) { $voucher->status = \Alexr\Enums\VoucherStatus::CANCELLED; $voucher->save(); } break; } $payment->settings = array_merge($payment->settings ?? [], ['webhook' => $payload]); $payment->save(); http_response_code(200); } }