first(); if ( ! $voucher ) { wp_redirect( '/' ); exit; } // Close the payment cycle if this is a gateway return. $this->processGatewayReturn( $voucher ); ob_start(); $this->renderConfirmation( $voucher ); die( ob_get_clean() ); } /** * Dispatch to the matching gateway return processor. Stripe returns with a * ?payment_intent; the redirect gateways (Redsys/Mollie/...) will come back * with ?alexr_gateway_{gw}=...&token=... once their create handlers land in * the next sub-step. */ protected function processGatewayReturn( $voucher ) { if ( $voucher->status !== \Alexr\Enums\VoucherStatus::PENDING_PAYMENT ) { return; // already issued / cancelled — nothing to process (idempotent) } if ( isset( $_GET['payment_intent'] ) ) { $this->processStripe( $voucher, sanitize_text_field( $_GET['payment_intent'] ) ); return; } // Redirect gateways come back with ?alexr_gateway_{gw}=...&token=... if ( isset( $_GET['alexr_gateway_mercadopago'] ) ) { $this->processMercadopago( $voucher ); return; } if ( isset( $_GET['alexr_gateway_mollie'] ) ) { $this->processMollie( $voucher ); return; } if ( isset( $_GET['alexr_gateway_square'] ) ) { $this->processSquare( $voucher ); return; } if ( isset( $_GET['alexr_gateway_xendit'] ) ) { $this->processXendit( $voucher ); return; } if ( isset( $_GET['alexr_gateway_upayments'] ) ) { $this->processUpayments( $voucher ); return; } if ( isset( $_GET['alexr_gateway_redsys'] ) ) { $this->processRedsys( $voucher ); return; } // PayPal is captured + issued in its ajax handler, so it needs no return processor. } /** * Stripe return: re-verify the PaymentIntent SERVER-SIDE against Stripe (we * never trust the browser for money), then issue the voucher. Idempotent. */ protected function processStripe( $voucher, $intentId ) { if ( ! defined( 'ALEXR_PRO_VERSION' ) || ! class_exists( '\Alexr\Payments\GatewayStripe' ) ) { return; } try { $gateway = new \Alexr\Payments\GatewayStripe( $voucher->restaurant_id ); \Stripe\Stripe::setApiKey( $gateway->getPrivateKey() ); $intent = \Stripe\PaymentIntent::retrieve( $intentId ); if ( ! $intent || $intent->status !== 'succeeded' ) { return; // not paid — leave pending, page shows the pending state } $payment = \Alexr\Models\Payment::where( 'stripe_payment_intent_id', $intentId ) ->where( 'voucher_id', $voucher->id ) ->first(); if ( $payment ) { $payment->status = \Alexr\Enums\PaymentStatus::SUCCEEDED; $payment->stripe_payment_status = $intent->status; $payment->save(); } $this->issue( $voucher, $payment ); } catch ( \Exception $e ) { // Leave the voucher pending; the buyer sees the pending message. } } /** * Mercadopago return: trusts the ?alexr_gateway_mercadopago status + the * anti-replay token (mirrors the booking process-mercadopago; it does not * re-query the API). */ protected function processMercadopago( $voucher ) { if ( ! $this->tokenMatches( $voucher ) ) return; $payment = $this->voucherPayment( $voucher, 'mercadopago' ); if ( ! $payment ) return; $status = sanitize_text_field( $_GET['alexr_gateway_mercadopago'] ); if ( $status === 'success' ) { $payment->status = \Alexr\Enums\PaymentStatus::SUCCEEDED; $this->storeCallback( $payment, 'mercadopago_result', array_map( 'sanitize_text_field', $_GET ) ); $payment->save(); $this->clearToken( $voucher ); $this->issue( $voucher, $payment ); } elseif ( $status === 'failure' ) { $payment->status = \Alexr\Enums\PaymentStatus::CANCELLED; $payment->save(); $this->clearToken( $voucher ); $this->cancel( $voucher ); } // 'pending': leave the voucher pending for a later return. } /** * Mollie return: re-verifies against the Mollie API by passing the voucher's * Payment to the driver's get_gateway_payment (optional $payment param). */ protected function processMollie( $voucher ) { if ( ! $this->tokenMatches( $voucher ) ) return; if ( ! class_exists( '\Alexr\Payments\GatewayMollie' ) ) return; $payment = $this->voucherPayment( $voucher, 'mollie' ); if ( ! $payment ) return; try { $gateway = new \Alexr\Payments\GatewayMollie( $voucher->restaurant_id ); $gp = $gateway->get_gateway_payment( $voucher, $payment ); if ( ! $gp ) return; if ( $gp->isPaid() ) { $payment->status = \Alexr\Enums\PaymentStatus::SUCCEEDED; $this->storeCallback( $payment, 'payment', json_decode( json_encode( $gp ), true ) ); $payment->save(); $this->clearToken( $voucher ); $this->issue( $voucher, $payment ); } elseif ( $gp->isPending() ) { // leave pending } else { $payment->status = \Alexr\Enums\PaymentStatus::CANCELLED; $payment->save(); $this->clearToken( $voucher ); $this->cancel( $voucher ); } } catch ( \Exception $e ) {} } /** * Square return: re-verifies via the driver's get_gateway_payment (passing the * voucher's Payment) and checks the order status. */ protected function processSquare( $voucher ) { if ( ! $this->tokenMatches( $voucher ) ) return; if ( ! class_exists( '\Alexr\Payments\GatewaySquare' ) ) return; $payment = $this->voucherPayment( $voucher, 'square' ); if ( ! $payment ) return; try { $gateway = new \Alexr\Payments\GatewaySquare( $voucher->restaurant_id ); $gp = $gateway->get_gateway_payment( $voucher, $payment ); if ( ! $gp ) return; $status = isset( $gp['status'] ) ? $gp['status'] : ''; if ( $status === 'COMPLETED' || $status === 'PAID' ) { $payment->status = \Alexr\Enums\PaymentStatus::SUCCEEDED; $payment->save(); $this->clearToken( $voucher ); $this->issue( $voucher, $payment ); } else { $payment->status = \Alexr\Enums\PaymentStatus::CANCELLED; $payment->save(); $this->clearToken( $voucher ); $this->cancel( $voucher ); } } catch ( \Exception $e ) {} } /** * Xendit return: re-verifies against the Xendit API (getInvoice must report * PAID) — authoritative, so no token is needed. Finds the voucher's Payment by * the external_id echoed in the return URL. */ protected function processXendit( $voucher ) { if ( ! class_exists( '\Alexr\Payments\GatewayXendit' ) ) return; $externalId = sanitize_text_field( $_GET['xendit_external_id'] ?? '' ); $status = strtolower( sanitize_text_field( $_GET['xendit_status'] ?? '' ) ); if ( ! $externalId ) return; $payment = \Alexr\Models\Payment::where( 'payment_type', 'xendit' ) ->where( 'xendit_external_id', $externalId ) ->where( 'voucher_id', $voucher->id ) ->first(); if ( ! $payment ) return; if ( $status === 'success' || $status === 'paid' ) { try { $gateway = new \Alexr\Payments\GatewayXendit( $voucher->restaurant_id ); $invoice = $gateway->getInvoice( $payment->xendit_invoice_id ); if ( $invoice && strtoupper( $invoice['status'] ?? '' ) === 'PAID' ) { $payment->status = \Alexr\Enums\PaymentStatus::SUCCEEDED; $payment->xendit_invoice_status = 'PAID'; $this->storeCallback( $payment, 'callback', [ 'external_id' => $externalId, 'status' => $status ] ); $payment->save(); $this->issue( $voucher, $payment ); } } catch ( \Exception $e ) {} } elseif ( $status === 'failed' || $status === 'failure' ) { $payment->status = \Alexr\Enums\PaymentStatus::FAILED; $payment->xendit_invoice_status = 'FAILED'; $payment->save(); $this->cancel( $voucher ); } } /** * Upayments return: trusts the ?result param (mirrors the booking flow, which * does not re-verify on the browser return) gated by our anti-replay token. */ protected function processUpayments( $voucher ) { if ( ! $this->tokenMatches( $voucher ) ) return; $orderId = sanitize_text_field( $_GET['upayments_order_id'] ?? '' ); $result = strtoupper( sanitize_text_field( $_GET['result'] ?? '' ) ); $payment = null; if ( $orderId ) { $payment = \Alexr\Models\Payment::where( 'payment_type', 'upayments' ) ->where( 'upayments_order_id', $orderId ) ->where( 'voucher_id', $voucher->id ) ->first(); } if ( ! $payment ) { $payment = \Alexr\Models\Payment::where( 'payment_type', 'upayments' ) ->where( 'voucher_id', $voucher->id ) ->last(); } if ( ! $payment ) return; if ( in_array( $result, [ 'CAPTURED', 'SUCCESS', 'PAID' ], true ) ) { $payment->status = \Alexr\Enums\PaymentStatus::SUCCEEDED; $payment->upayments_status = $result; $this->storeCallback( $payment, 'callback', [ 'order_id' => $orderId, 'result' => $result ] ); $payment->save(); $this->clearToken( $voucher ); $this->issue( $voucher, $payment ); } elseif ( in_array( $result, [ 'NOT CAPTURED', 'NOT_CAPTURED', 'FAILED', 'FAILURE' ], true ) ) { $payment->status = \Alexr\Enums\PaymentStatus::FAILED; $payment->upayments_status = $result; $payment->save(); $this->clearToken( $voucher ); $this->cancel( $voucher ); } } /** * Redsys return: trusts ?alexr_gateway_redsys=ok/ko gated by our anti-replay * token (mirrors the booking process-redsys; the signed server-to-server * notify only logs, so the browser return closes the cycle). */ protected function processRedsys( $voucher ) { if ( ! $this->tokenMatches( $voucher ) ) return; $result = sanitize_text_field( $_GET['alexr_gateway_redsys'] ?? '' ); $payment = $this->voucherPayment( $voucher, 'redsys' ); if ( ! $payment ) return; if ( $result === 'ok' ) { $payment->status = \Alexr\Enums\PaymentStatus::SUCCEEDED; $payment->save(); $this->clearToken( $voucher ); $this->issue( $voucher, $payment ); } elseif ( $result === 'ko' ) { $payment->status = \Alexr\Enums\PaymentStatus::CANCELLED; $payment->save(); $this->clearToken( $voucher ); $this->cancel( $voucher ); } } protected function voucherPayment( $voucher, $type ) { return \Alexr\Models\Payment::where( 'voucher_id', $voucher->id ) ->where( 'payment_type', $type ) ->last(); } /** * Anti-replay: the return URL carries &token=..., compared (constant-time) * against the token stashed on the voucher when the payment was created. */ protected function tokenMatches( $voucher ) { $token = isset( $_GET['token'] ) ? sanitize_text_field( $_GET['token'] ) : ''; $settings = is_array( $voucher->settings ) ? $voucher->settings : []; $stored = isset( $settings['gateway_token'] ) ? $settings['gateway_token'] : ''; return $token !== '' && $stored !== '' && hash_equals( (string) $stored, (string) $token ); } protected function clearToken( $voucher ) { $settings = is_array( $voucher->settings ) ? $voucher->settings : []; $settings['gateway_token'] = null; $voucher->settings = $settings; $voucher->save(); } protected function cancel( $voucher ) { $voucher->status = \Alexr\Enums\VoucherStatus::CANCELLED; $voucher->save(); } protected function storeCallback( $payment, $key, $value ) { $settings = is_array( $payment->settings ) ? $payment->settings : []; $settings[ $key ] = $value; $payment->settings = $settings; } /** * Mark the voucher paid/active and fire the issued hook (phase 4 sends the * email with the code). markPaid() is idempotent, so double returns are safe. */ protected function issue( $voucher, $payment = null ) { $activated = $voucher->markPaid( $payment ); if ( $activated ) { do_action( 'alexr_voucher_issued', $voucher ); } } /** * Standalone confirmation + printable gift voucher page. Active vouchers show * a print-ready certificate (code, amount, to/from, message, expiry, terms); * pending/cancelled/expired show a short status message. */ protected function renderConfirmation( $voucher ) { $restaurant = $voucher->restaurant; $symbol = \Alexr\Enums\CurrencyType::symbolFor( $voucher->currency ); $isActive = $voucher->status === \Alexr\Enums\VoucherStatus::ACTIVE; $isPending = $voucher->status === \Alexr\Enums\VoucherStatus::PENDING_PAYMENT; $amount = $symbol . number_format( 0.01 * intval( $voucher->initial_amount ), 2 ); $settings = is_array( $voucher->settings ) ? $voucher->settings : []; $product_name = isset( $settings['product_name'] ) ? $settings['product_name'] : ''; // Fetch the product for its terms (not snapshotted on the voucher). $terms = ''; if ( $voucher->product_id ) { $product = \Alexr\Settings\GiftCard::find( $voucher->product_id ); if ( $product ) { $terms = (string) $product->terms; } } $expires = $voucher->expires_at ? date_i18n( get_option( 'date_format' ), strtotime( $voucher->expires_at ) ) : __eva( 'No expiry' ); $title = $isActive ? __eva( 'Your gift voucher is ready!' ) : ( $isPending ? __eva( 'Completing your payment…' ) : __eva( 'Gift voucher' ) ); ?>
name : '' ); ?>
status ) ); ?>