get_charset_collate(); $create[] = "CREATE TABLE `{$wpdb->base_prefix}formipay_paypal_trx` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `created_date` datetime DEFAULT CURRENT_TIMESTAMP, `form_id` int DEFAULT 0, `order_id` int DEFAULT 0, `currency_code` text, `total` float(10, 2) DEFAULT 0, `status` text, `meta_data` text, PRIMARY KEY (`id`) ) $charset_collate;"; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; dbDelta($create); } public function webhook_endpoint() { register_rest_route('formipay/v1', '/paypal-webhook', [ 'methods' => 'POST', 'callback' => [$this, 'handle_paypal_webhook'], 'permission_callback' => '__return_true' ]); } public function add_transaction($order_data) { $form_id = absint($order_data['form_id'] ?? 0); $formipay_settings = get_option('formipay_settings'); $timeout = isset($formipay_settings['paypal_timeout']) ? (int) $formipay_settings['paypal_timeout'] : 1440; $currency_meta = formipay_get_post_meta($form_id, 'product_currency'); $currency_parts = explode(':::', $currency_meta); $currency = !empty($currency_parts[0]) ? sanitize_text_field($currency_parts[0]) : ''; if($this->gateway === ($order_data['payment_gateway'] ?? '')) { $submit_args = [ 'created_date' => formipay_date('Y-m-d H:i:s', strtotime($order_data['created_date'] ?? 'now')), 'form_id' => $form_id, 'order_id' => absint($order_data['id'] ?? 0), 'currency_code' => $currency, 'total' => floatval($order_data['total'] ?? 0), 'status' => 'PENDING', 'meta_data' => maybe_serialize([ 'request_response' => maybe_serialize($order_data['payment_callback']), 'payer_action_url' => esc_url_raw($order_data['redirect_url'] ?? '') ]) ]; global $wpdb; $table = $wpdb->prefix . 'formipay_paypal_trx'; $format = [ '%s', // created_date '%d', // form_id '%d', // order_id '%s', // currency_code '%f', // total '%s', // status '%s' // meta_data ]; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->insert($table, $submit_args, $format); wp_schedule_single_event( (int) formipay_date('timestamp') + $timeout, 'formipay/order/payment-timeout', [ 'order_id' => $order_data['id'], 'payment_gateway' => 'paypal' ] ); } } public function auto_cancel_order_on_timeout($order_id, $payment_gateway) { if($payment_gateway == 'paypal'){ global $wpdb; $order = formipay_get_order($order_id); if($order['status'] !== 'completed'){ Order::update($order_id, [ 'status' => 'cancelled' ]); $this->cancel_payment($order_id); } } } public function get_transaction($order_id = 0) { global $wpdb; $table = $wpdb->prefix .'formipay_paypal_trx'; if($order_id == 0){ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $get = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i", $table ) ); }else{ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $get = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM %i WHERE `order_id` = %d", $table, $order_id ) ); } return $get; } public function update_transaction($order_id, $status, $callback) { $args = [ 'status' => sanitize_text_field( $status ), 'meta_data' => maybe_serialize( $callback ) ]; global $wpdb; $table = $wpdb->prefix . 'formipay_paypal_trx'; $where = ['order_id' => $order_id]; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $update = $wpdb->update( $table, $args, $where ); return $update; } public function add_gateway($gateways) { $formipay_settings = get_option('formipay_settings'); if(isset($formipay_settings['paypal_toggle']) && $formipay_settings['paypal_toggle'] == 'yes') { $gateways[] = [ 'id' => $this->gateway, 'gateway' => __( 'Paypal', 'formipay' ) ]; } return $gateways; } public function add_settings($fields) { $payment_instruction = file_get_contents( FORMIPAY_PATH . 'admin/templates/paypal-instruction.html' ); $paypal_settings = array( $this->gateway.'_toggle' => array( 'type' => 'checkbox', 'label' => __( 'Activate Paypal', 'formipay' ), 'submenu' => __( 'Paypal', 'formipay' ) ), $this->gateway.'_account_group' => array( 'type' => 'group_title', 'label' => __( 'Paypal Account', 'formipay' ), 'submenu' => __( 'Paypal', 'formipay' ), 'dependency' => array( 'key' => $this->gateway.'_toggle', 'value' => 'not_empty' ), 'group' => 'started' ), $this->gateway.'_instruction' => array( 'type' => 'notification_message', 'description' => $payment_instruction, 'buttons' => array( 'show_instruction' => array( 'text' => __( 'Show Instruction', 'formipay' ), 'class' => 'show-instruction' ) ), 'dependency' => array( 'key' => $this->gateway.'_toggle', 'value' => 'not_empty' ), 'submenu' => __( 'Paypal', 'formipay' ) ), $this->gateway.'_client_id' => array( 'type' => 'text', 'label' => __( 'Client ID', 'formipay' ), 'submenu' => __( 'Paypal', 'formipay' ), 'dependency' => array( 'key' => $this->gateway.'_toggle', 'value' => 'not_empty' ) ), $this->gateway.'_secret' => array( 'type' => 'text', 'label' => __( 'Secret', 'formipay' ), 'submenu' => __( 'Paypal', 'formipay' ), 'dependency' => array( 'key' => $this->gateway.'_toggle', 'value' => 'not_empty' ) ), $this->gateway.'_environment' => array( 'type' => 'select', 'label' => __( 'Environment', 'formipay' ), 'options' => array( 'sandbox' => __( 'Sandbox', 'formipay' ), 'production' => __( 'Production', 'formipay' ) ), 'value' => 'sandbox', 'submenu' => __( 'Paypal', 'formipay' ), 'dependency' => array( 'key' => $this->gateway.'_toggle', 'value' => 'not_empty' ) ), $this->gateway.'_merchant_email' => array( 'type' => 'text', 'label' => __( 'Merchant Email', 'formipay' ), 'submenu' => __( 'Paypal', 'formipay' ), 'dependency' => array( 'key' => $this->gateway.'_toggle', 'value' => 'not_empty' ), ), $this->gateway.'_timeout' => array( 'type' => 'number', 'label' => __( 'Payment Timeout (minute)', 'formipay' ), 'description' => __( 'Set a timeout to wait for payment. After this timeout is up, the order status automatically changes to cancelled.', 'formipay' ), 'value' => 120, 'group' => 'ended', 'submenu' => __( 'Paypal', 'formipay' ), 'dependency' => array( 'key' => $this->gateway.'_toggle', 'value' => 'not_empty' ), 'group' => 'ended' ), ); foreach($paypal_settings as $key => $value) { $fields[$key] = $value; } return $fields; } public function add_paypal_settings($fields) { $paypal_settings = array( $this->gateway.'_confirmation_notice_message_group' => array( 'type' => 'group_title', 'label' => __( 'Confirmation Notice Messages', 'formipay' ), 'description' => __( 'Set notice message on every condition about attempting to confirm the payment', 'formipay' ), 'submenu' => __( 'Paypal', 'formipay' ), 'group' => 'started' ), $this->gateway.'_confirmation_update_to_status' => array( 'type' => 'select', 'label' => __( 'Change status order to', 'formipay' ), 'options' => formipay_order_status_list(), 'value' => 'payment-confirm', 'submenu' => __( 'Paypal', 'formipay' ), ), $this->gateway.'_confirmation_message_success' => array( 'type' => 'hint_textarea', 'label' => __( 'Successfully Confirmed Message', 'formipay' ), 'hints' => array( 'order_id' => __('Order ID', 'formipay') ), 'value' => __('Successfully confirmed payment for #{{order_id}}', 'formipay'), 'description' => __('When successfully change the order status to the preferred one', 'formipay'), 'submenu' => __( 'Paypal', 'formipay' ), ), $this->gateway.'_confirmation_message_already_confirmed' => array( 'type' => 'hint_textarea', 'label' => __( 'Has Been Confirmed Message', 'formipay' ), 'hints' => array( 'order_id' => __('Order ID', 'formipay') ), 'value' => __('#{{order_id}} has been confirmed', 'formipay'), 'description' => __('When the order status has been the preferred one', 'formipay'), 'submenu' => __( 'Paypal', 'formipay' ), ), $this->gateway.'_confirmation_message_error' => array( 'type' => 'hint_textarea', 'label' => __( 'Failed to Confirm Message', 'formipay' ), 'hints' => array( 'order_id' => __('Order ID', 'formipay'), 'system_error_message' => __('System Error Message', 'formipay') ), 'value' => __('Failed to proceed. Error: {{system_error_message}}', 'formipay'), 'description' => __('When error to change the order status to the preferred one', 'formipay'), 'submenu' => __( 'Paypal', 'formipay' ), 'group' => 'ended' ) ); foreach($paypal_settings as $key => $value) { $fields[$key] = $value; } return $fields; } public function set_frontend_label($label){ $label = __( 'Paypal', 'formipay' ); return $label; } public function set_frontend_logo($logo){ if($logo == false){ $logo = FORMIPAY_URL . 'public/assets/img/paypal.png'; } return $logo; } public function prepare_payload($order_data) { // error_log('current order'. $order_data['id'] . ': ' . print_r($order_data, true)); $form_id = intval($order_data['form_id']); $order_details = $order_data['items']; if($this->gateway == $order_data['payment_gateway']){ $formipay_settings = get_option('formipay_settings'); $grand_total = floatval($order_data['total']); // Currency $currency = explode(':::', formipay_get_post_meta($form_id, 'product_currency')); $currency = $currency[0]; $currency_decimal_digits = formipay_get_post_meta($form_id, 'product_currency_decimal_digits'); $currency_decimal_symbol = formipay_get_post_meta($form_id, 'product_currency_decimal_symbol'); $currency_thousand_separator = formipay_get_post_meta($form_id, 'product_currency_thousand_separator'); $slug = 'thankyou'; $formipay_settings = get_option('formipay_settings'); if(!empty($formipay_settings['thankyou_link'])){ $slug = $formipay_settings['thankyou_link']; } if(formipay_get_post_meta($form_id, 'product_type') == 'digital'){ $shipping_preference = 'NO_SHIPPING'; }else{ $shipping_preference = 'SET_PROVIDED_ADDRESS'; } // Setup items $items = []; foreach($order_details as $item){ $qty = 1; if(!empty($item['qty'])){ $qty = $item['qty']; } if(floatval($item['amount']) >= 0){ $items[] = [ 'name' => $item['item'], 'unit_amount' => [ 'currency_code' => $currency, 'value' => floatval($item['amount']) ], 'quantity' => $qty ]; } } $items = apply_filters('formipay/order/paypal/payload/items', $items, $currency, $order_data); $breakdown = array( "item_total" => array( "currency_code" => $currency, "value" => $grand_total ) ); $breakdown = apply_filters('formipay/order/paypal/payload/breakdown', $breakdown, $currency, $order_data); $unique_id = $order_data['meta_data']['session_id']['value']; $thankyou_link = 'thankyou'; if(isset($formipay_settings['thankyou_link']) && !empty($formipay_settings['thankyou_link'])){ $thankyou_link = $formipay_settings['thankyou_link']; } $token_manager = new \Formipay\Token(); $token = $token_manager->generate( $order_data['id'], $form_id, 900 // 15-minute expiration ); $access_link = site_url('/'.$thankyou_link.'/' . $token); // Prepare the payload $payload = json_encode(array( "intent" => "CAPTURE", "purchase_units" => array( array( "custom_id" => (string)$order_data['id'], "amount" => array( "currency_code" => $currency, "value" => $grand_total, // Format amount "breakdown" => $breakdown ), "items" => $items ) ), 'payment_source' => array( 'paypal' => array( 'experience_context' => array( 'return_url' => $access_link . '?paypal-status=success', 'cancel_url' => $access_link . '?paypal-status=cancel', 'user_action' => 'PAY_NOW', 'brand_name' => 'FORMIPAY INC', 'local' => 'en-US', 'landing_page' => 'LOGIN', 'payment_method_preference' => 'IMMEDIATE_PAYMENT_REQUIRED', 'shipping_preference' => $shipping_preference ) ) ) )); return $payload; } return false; } public function process_payment( $order_data ) { $formipay_settings = get_option('formipay_settings'); if($order_data['payment_gateway'] == 'paypal') { $paypal_client_id = $formipay_settings['paypal_client_id']; $paypal_secret = $formipay_settings['paypal_secret']; // Set your authentication header $auth = base64_encode($paypal_client_id . ':' . $paypal_secret); // Define arguments for wp_remote_post $args = array( 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Basic ' . $auth, ), 'method' => 'POST', 'data_format' => 'body' ); $url = 'https://api-m.sandbox.paypal.com/v2'; if(isset($formipay_settings[$this->gateway.'_environment']) && $formipay_settings[$this->gateway.'_environment'] == 'production') { $url = 'https://api-m.paypal.com/v2'; } $payload = $this->prepare_payload($order_data); // error_log('paypal payload:'.print_r($payload, true)); $args['body'] = $payload; $url = $url.'/checkout/orders'; $response = wp_remote_retrieve_body( wp_remote_post($url, $args) ); $response = json_decode($response); // error_log('paypal response:'.print_r($response, true)); $payer_action_link = null; foreach ($response->links as $link) { if ($link->rel === 'payer-action') { $payer_action_link = $link->href; break; } } if($payer_action_link !== null){ $order_data['payment_callback'] = $response; $order_data['redirect_url'] = $payer_action_link; } } return $order_data; } public function cancel_payment( $order_data ) { $formipay_settings = get_option('formipay_settings'); if($order_data['payment_gateway'] == 'paypal') { $paypal_client_id = $formipay_settings['paypal_client_id']; $paypal_secret = $formipay_settings['paypal_secret']; // Set your authentication header $auth = base64_encode($paypal_client_id . ':' . $paypal_secret); // Define arguments for wp_remote_post $args = array( 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Basic ' . $auth, ), 'method' => 'POST', 'data_format' => 'body' ); $url = 'https://api-m.sandbox.paypal.com/v2'; if(isset($formipay_settings[$this->gateway.'_environment']) && $formipay_settings[$this->gateway.'_environment'] == 'production') { $url = 'https://api-m.paypal.com/v2'; } $payload = $this->prepare_payload($order_data); // error_log('paypal payload:'.print_r($payload, true)); $args['body'] = $payload; $url = $url.'/checkout/orders'; $response = wp_remote_retrieve_body( wp_remote_post($url, $args) ); $response = json_decode($response); // error_log('paypal response:'.print_r($response, true)); $payer_action_link = null; foreach ($response->links as $link) { if ($link->rel === 'payer-action') { $payer_action_link = $link->href; break; } } if($payer_action_link !== null){ $order_data['redirect_url'] = $payer_action_link; } do_action('formipay/notification/order', $order_data); } } public function check_parse_query() { if (is_admin()) { return; } global $wp_query; $formipay_settings = get_option('formipay_settings'); // Validate query vars early $query = $wp_query->query; if ( is_array($query) && !empty($query['formipay-payment-confirm']) && filter_var($query['formipay-payment-confirm'], FILTER_VALIDATE_BOOLEAN) && !empty($query['gateway']) && $query['gateway'] === 'paypal' ) { // Sanitize and validate token $token_raw = isset($query['formipay-token']) ? $query['formipay-token'] : ''; $token_decoded = base64_decode($token_raw, true); if (!$token_decoded) { return; } $token = explode(':::', $token_decoded); if (count($token) < 2) { return; } $form_id = absint($token[0]); $order_id = absint($token[1]); if (!$form_id || !$order_id) { return; } $check_order = $this->get_transaction($order_id); // Check PayPal status and process payment if (!empty($check_order) && // phpcs:ignore WordPress.Security.NonceVerification.Recommended isset($_GET['paypal-status']) && sanitize_text_field(wp_unslash($_GET['paypal-status'])) === 'success' && strtoupper($check_order->status) === 'PENDING' ) { $paypal_client_id = isset($formipay_settings['paypal_client_id']) ? $formipay_settings['paypal_client_id'] : ''; $paypal_secret = isset($formipay_settings['paypal_secret']) ? $formipay_settings['paypal_secret'] : ''; $environment = isset($formipay_settings[$this->gateway . '_environment']) ? $formipay_settings[$this->gateway . '_environment'] : 'sandbox'; if ($paypal_client_id && $paypal_secret) { $auth = base64_encode($paypal_client_id . ':' . $paypal_secret); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $paypal_order_id = isset($_GET['token']) ? sanitize_text_field(wp_unslash($_GET['token'])) : ''; $base_url = ($environment === 'production') ? 'https://api-m.paypal.com/v2' : 'https://api-m.sandbox.paypal.com/v2'; $url = $base_url . '/checkout/orders/' . $paypal_order_id . '/capture'; $args = [ 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => 'Basic ' . $auth, ], 'method' => 'POST', 'data_format' => 'body', 'timeout' => 20, ]; $response = wp_remote_post($url, $args); $body = is_wp_error($response) ? null : wp_remote_retrieve_body($response); $response_data = json_decode($body, true); if (isset($response_data['status']) && $response_data['status'] === 'COMPLETED') { $update_order_status = formipay_update_order_status([ 'form_id' => $form_id, 'order_id' => $order_id, 'payment_gateway' => $this->gateway, 'status' => 'completed' ]); if (!empty($update_order_status['valid'])) { $capture_details = $response_data['purchase_units'][0]['payments']['captures'][0] ?? []; $this->update_transaction($order_id, $response_data['status'], $capture_details); } } } } // Determine redirect action $action_type = formipay_get_post_meta($form_id, 'submit_action_type'); $params = isset($_SERVER['QUERY_STRING']) ? sanitize_text_field(wp_unslash($_SERVER['QUERY_STRING'])) : ''; if ($action_type === 'thankyou') { $thankyou_slug = !empty($formipay_settings['thankyou_link']) ? sanitize_title($formipay_settings['thankyou_link']) : 'thankyou'; wp_safe_redirect(site_url('/' . $thankyou_slug . '/' . $token_raw . '?' . $params)); exit; } elseif ($action_type === 'redirect') { $url = formipay_get_post_meta($form_id, 'redirect_url'); $url = esc_url_raw($url); if (strpos($url, '?') !== false) { wp_safe_redirect($url . '&' . $params); } else { wp_safe_redirect($url . '?' . $params); } exit; } elseif ($action_type === 'whatsapp') { $order = formipay_get_order($order_id); $order_data = []; if (is_array($order)) { foreach ($order as $key => $data) { $order_data[$key] = maybe_unserialize($data); } } $admin_number = formipay_get_post_meta($form_id, 'whatsapp_admin'); $shortcodes = [ '{{product_name}}' => get_the_title($form_id), '{{order_id}}' => $order_id, '{{order_date}}' => $order['created_date'] ?? '', '{{order_total}}' => $order['total'] ?? '', '{{order_status}}' => $order['status'] ?? '', '{{order_details}}' => $this->render_order_details($form_id, $order_id, maybe_unserialize($order['items'] ?? '')), '{{payment_details}}' => $this->render_payment_details('', $form_id, $order_data, 'whatsapp'), ]; $message_template = wp_kses_post(formipay_get_post_meta($form_id, 'whatsapp_message')); $message = strtr($message_template, $shortcodes); $message = str_replace('%250A', '%0A', rawurlencode($message)); $whatsapp_url = 'https://api.whatsapp.com/send/?phone=' . urlencode($admin_number) . '&text=' . $message; wp_safe_redirect($whatsapp_url); exit; } } } public function handle_paypal_webhook(\WP_REST_Request $request) { $body = $request->get_body(); $event = json_decode($body, true); if (!$event || empty($event['event_type'])) { return new \WP_Error('invalid_webhook', 'Invalid webhook payload'); } // Example: get order_id from custom_id or invoice_number in resource $resource = $event['resource']; $order_id = null; if (!empty($resource['custom_id'])) { $order_id = intval($resource['custom_id']); } elseif (!empty($resource['invoice_number'])) { $order_id = intval($resource['invoice_number']); } if (!$order_id) { return new \WP_Error('missing_order_id', 'Cannot find order ID in webhook'); } // Map PayPal event to order status switch ($event['event_type']) { case 'PAYMENT.CAPTURE.COMPLETED': $status = 'completed'; break; case 'PAYMENT.CAPTURE.PENDING': $status = 'pending'; break; case 'PAYMENT.CAPTURE.REFUNDED': case 'PAYMENT.CAPTURE.PARTIALLY_REFUNDED': $status = 'refunded'; break; case 'PAYMENT.CAPTURE.DENIED': case 'PAYMENT.CAPTURE.REVERSED': case 'PAYMENT.ORDER.CANCELLED': $status = 'cancelled'; break; default: // Ignore unhandled events return rest_ensure_response(['ignored' => true]); } // Update order and transaction formipay_update_order_status([ 'order_id' => $order_id, 'payment_gateway' => $this->gateway, 'status' => $status ]); $this->update_transaction($order_id, $status, $resource); return rest_ensure_response(['updated' => true, 'order_id' => $order_id, 'status' => $status]); } public function render_order_details($form_id, $order_id, $items) { $order = formipay_get_order($order_id); $target_length = 40; $message_format = formipay_get_post_meta($form_id, 'whatsapp_message'); $order_details_message = '```'; if(!empty($items)){ foreach($items as $detail){ $qty = ''; if(isset($detail['qty']) && !empty($detail['qty'])){ $qty = $detail['qty'].'×'; } $subtotal = formipay_price_format($detail['subtotal'], $form_id); $qty_length = strlen($qty); $subtotal_length = strlen($subtotal); if(floatval($detail['subtotal']) < 0){ $subtotal_length = $subtotal_length + 1; } $dot_length = $target_length - ($qty_length + $subtotal_length); $dots = str_repeat('.', intval($dot_length)); $order_details_message .= '%0A'.$detail['item']; $order_details_message .= '%0A'.$qty.$dots.$subtotal; } $total = formipay_price_format($order['total'], $form_id); $total_length = strlen($total); $dot_length = $target_length - ($total_length + 6); $dots = str_repeat('.', intval($dot_length)); $divider = str_repeat('_', intval($target_length)); $order_details_message .= '%0A'.$divider; $order_details_message .= '%0A'.''; $order_details_message .= '%0A'.'Total'.$dots.$total; } $order_details_message .= '%0A```'; $content = $order_details_message; return $content; } public function render_payment_details($render, $form_id, $order_data, $submit_action_type) { if($order_data['status'] == 'completed'){ return $render; } $formipay_settings = get_option('formipay_settings'); $get_trx = $this->get_transaction($order_data['id']); if(!empty($get_trx)){ $meta = maybe_unserialize($get_trx->meta_data); if(isset($meta['payer_action_url']) && !empty($meta['payer_action_url'])){ if($formipay_settings[$this->gateway.'_toggle'] !== false){ if($submit_action_type == 'thankyou'){ ob_start(); ?> Payment Link: *%s*', 'formipay'), $meta['payer_action_url'] ); $render = $message; } } } } return $render; } public function render_email_action_button($button, $recipient, $order_data){ if( $order_data['payment_gateway'] == $this->gateway && $order_data['status'] == 'on-hold' && $recipient !== 'admin' ){ $formipay_settings = get_option('formipay_settings'); $get_trx = $this->get_transaction($order_data['id']); if(!empty($get_trx)){ $meta = maybe_unserialize($get_trx->meta_data); if(isset($meta['payer_action_url']) && !empty($meta['payer_action_url'])){ $button = [ 'url' => $meta['payer_action_url'], 'label' => __( 'Pay Now', 'formipay' ) ]; } } } return $button; } }