관리-도구
편집 파일: class-kkart-gateway-yoco.php
<?php /** * YOCO Payment Gateway * @class KKART_Gateway_Yoco * @package Kkart * @version 1.0.0 * @category Payment Gateways * @author Kkart */ if ( ! defined( 'ABSPATH' ) ) { exit; } define( 'KKART_GATEWAY_YOCO_VERSION', KKART_VERSION ); define( 'KKART_GATEWAY_YOCO_URL', untrailingslashit( plugin_dir_url(__FILE__) ) ); define( 'KKART_YOCO_ONLINE_CHECKOUT_URL', 'https://payments.yoco.com/api/checkouts' ); class KKART_Gateway_Yoco extends KKART_Payment_Gateway { /** * Version * * @var string */ public $version; public $mode; /** * Constructor */ public function __construct() { //NOTE:: Update payment method description according to supported payment methods $this->version = KKART_GATEWAY_YOCO_VERSION; $this->credentials = $this->get_credential(); $this->id = 'yoco'; $this->enabled = $this->get_option( 'enabled' ); $this->icon = KKART_GATEWAY_YOCO_URL . '/assets/yoco-2024.svg'; $this->has_fields = false; $this->init_form_fields(); $this->init_settings(); // Supported functionality. $this->supports = array( 'products', 'pre-orders', 'refunds'); $this->title = $this->get_option( 'title', esc_html__( 'Yoco', 'kkart' ) ); $this->description = $this->get_option( 'description', esc_html__( 'Pay securely using a credit/debit card or other payment methods via Yoco.', 'kkart' ) ); $this->mode = $this->get_option( 'mode' ); $this->method_title = esc_html__( 'Yoco', 'kkart' ); $this->method_description = esc_html__( 'Yoco redirects customers to PayPal to enter their payment information.', 'kkart' ); $this->available_currencies = (array)apply_filters('kkart_gateway_yoco_available_currencies', array( 'ZAR' ) ); add_action( 'kkart_update_options_payment_gateways_' . $this->id, array( &$this, 'process_admin_options' ) ); add_action( 'admin_notices', array( $this, 'admin_notices' ) ); add_action( 'yoco_payment_gateway/checkout/created', array( $this, 'updateOrderCheckoutMeta' ), 10, 2 ); add_action( 'yoco_payment_gateway/order/refunded', array( $this, 'updateOrderRefundId' ), 10, 2 ); } public function init_form_fields() { $this->form_fields = array( 'enabled' => array( 'title' => __( 'Enable/Disable', 'kkart' ), 'label' => __( 'Enable Yoco Payments', 'kkart' ), 'type' => 'checkbox', 'description' => '', 'default' => 'no', ), 'title' => array( 'title' => __( 'Title', 'kkart' ), 'type' => 'text', 'description' => __( 'This controls the title which the user sees during checkout.', 'kkart' ), 'default' => __( 'Yoco', 'kkart' ), 'desc_tip' => true, ), 'description' => array( 'title' => __( 'Description', 'kkart' ), 'type' => 'text', 'description' => __( 'This controls the description which the user sees during checkout.', 'kkart' ), 'default' => __( 'Pay securely using a credit/debit card or other payment methods via Yoco.', 'kkart' ), 'css' => 'max-width:400px;', ), 'mode' => array( 'title' => __( 'Mode', 'kkart' ), 'label' => __( 'Mode', 'kkart' ), 'type' => 'select', 'description' => __( 'Test mode allow you to test the plugin without processing money.<br>Set the plugin to Live mode and click on "Save changes" for real customers to use it.', 'kkart' ), 'default' => 'Test', 'options' => array( 'live' => 'Live', 'test' => 'Test', ), ), 'live_secret_key' => array( 'title' => __( 'Live Secret Key', 'kkart' ), 'type' => 'password', 'description' => __( 'Live Secret Key', 'kkart' ), 'class' => 'input password-input', ), 'test_secret_key' => array( 'title' => __( 'Test Secret Key', 'kkart' ), 'type' => 'password', 'description' => __( 'Test Secret Key', 'kkart' ), 'class' => 'input password-input', ), ); } public function process_payment( $order_id ): ?array { $order = kkart_get_order( $order_id ); try { $checkoutUrl = $order->get_meta( 'kkart_yoco_order_checkout_url', true, 'kkart' ); if( ! empty( $checkoutUrl ) ){ return $this->createSuccessRedirectResponse( $checkoutUrl ); } $response = $this->send($order, 'payment'); if( ! in_array( (int) $response['code'], array( 200, 201, 202 ), true )){ $error_message = isset( $response['body']['errorMessage'] ) ? $response['body']['errorMessage'] : ''; $error_code = isset( $response['body']['errorCode'] ) ? $response['body']['errorCode'] : ''; $response_message = isset( $response['message'] ) ? $response['message'] : ''; throw new Exception( sprintf( 'Failed to request checkout. %s', $response_message ) ); } do_action( 'yoco_payment_gateway/checkout/created', $order, $response['body']); return $this->createSuccessRedirectResponse( $response['body']['redirectUrl'] ); } catch ( \Throwable $th ){ kkart_add_notice( __( 'Your order could not be processed by Yoco - please try again later.', 'kkart' ), 'error' ); return null; } } public function get_icon() { $icon = '<img class="yoco-payment-method-icon" style="max-height:1em;width:100%;" alt="' . esc_attr( $this->title ) . '" width="100" height="24" src="' . esc_url( $this->icon ) . '"/>'; return apply_filters( 'kkart_gateway_icon', $icon, $this->id ); } public function process_refund( $order_id, $amount = null, $reason = '' ){ $order = kkart_get_order( $order_id ); try{ $response = $this->send($order, 'refund'); if ( isset( $response['body']['description'] ) ) { return new WP_Error( 400, $response['body']['description'] ); } if ( isset( $response['body']['status'] ) && 'successful' === $response['body']['status'] ) { do_action( 'yoco_payment_gateway/order/refunded', $order, $response['body'] ); } return new WP_Error( 200, $response['body']['message'] ?? '' ); }catch( \Throwable $th ){ return new WP_Error( $th->getCode(), $th->getMessage() ); } } private function createSuccessRedirectResponse( string $redirectUrl ): array { return array( 'result' => 'success', 'redirect' => $redirectUrl, ); } public function send($order, string $request_type): array { try { $url = $this->getUrl($order, $request_type); $args = $this->getArgs($order,$request_type); return $this->post( $url, $args ); } catch ( \Throwable $th ) { throw $th; } } public function updateOrderCheckoutMeta( KKART_Order $order, array $data ): void { $this->updateOrderMeta( $order, 'kkart_yoco_order_checkout_id', $data['id'] ); $this->updateOrderMeta( $order, 'kkart_yoco_order_checkout_mode', $data['processingMode'] ); $this->updateOrderMeta( $order, 'kkart_yoco_order_checkout_url', $data['redirectUrl'] ); } public function updateOrderPaymentId( KKART_Order $order, $payload ): void { $this->updateOrderMeta( $order, 'kkart_yoco_order_payment_id', $payload->getPaymentId() ); } public function updateOrderRefundId( KKART_Order $order, array $data ): void { $this->updateOrderMeta( $order, 'kkart_yoco_order_refund_id', $data['refundId'] ); } public function updateOrderMeta( KKART_Order $order, string $key, string $value ): void { $order->update_meta_data( $key, $value ); $order->save_meta_data(); } private function getUrl($order, string $request_type): string { if($request_type === 'refund'){ $url = $this->getCheckoutApiUrl(); return trailingslashit( $url ) . $this->getOrderCheckoutId($order) . '/refund'; } return $this->getCheckoutApiUrl(); } public function getCheckoutApiUrl(): ?string { if(! defined( 'KKART_YOCO_ONLINE_CHECKOUT_URL' ) ){ return ''; } return KKART_YOCO_ONLINE_CHECKOUT_URL; } public function getOrderCheckoutId( KKART_Order $order ): string { return $this->getOrderMeta( $order, 'kkart_yoco_order_checkout_id' ); } public function getOrderMeta( KKART_Order $order, string $key ): string { $meta = $order->get_meta( $key, true, 'yoco' ); return is_string( $meta ) ? $meta : ''; } private function getArgs($order, string $request_type): array { if($request_type === 'refund'){ return array( 'headers' => $this->getHeaders($request_type), ); } return array( 'headers' => $this->getHeaders($request_type), 'body' => $this->getBody($order) ); } public function getHeaders($request_type) { $headers = array( 'Content-Type' => 'application/json', 'Authorization' => $this->getApiBearer(), 'X-Product' => 'kkart', ); if($request_type === 'refund'){ return apply_filters( 'yoco_payment_gateway/refund/request/headers', $headers ); } return apply_filters( 'yoco_payment_gateway/payment/request/headers', $headers ); } public function getHeadersForMode($order) { $headers = array( 'Content-Type' => 'application/json', 'Authorization' => $this->getApiBearer( $order->get_meta( 'kkart_yoco_order_payment_mode', true ) ), 'X-Product' => 'kkart', ); return apply_filters( 'yoco_payment_gateway/payment/request/headers', $headers ); } public function getApiBearer( string $mode = '' ): string { return 'Bearer ' . $this->getSecretKey( $mode ); } public function getSecretKey( string $mode = '' ) { $mode = ( 'live' === $mode || 'test' === $mode ) ? $mode : $this->mode; return $this->get_option($mode . '_secret_key' ) ? $this->get_option($mode . '_secret_key' ) : ''; } public function getBody($order) { $body = $this->buildPayload($order); $body = apply_filters( 'yoco_payment_gateway/payment/request/body', $body ); return json_encode( $body, JSON_UNESCAPED_SLASHES ); } public function getOrderCheckoutPaymentUrl( string $status, $order ): string { return add_query_arg( array( 'kkart_yoco_checkout_status' => $status, ), $order->get_checkout_order_received_url() ); } // This function checked move on to the next public function buildPayload($order){ $payload = array(); $payload['amount'] = $this->format($order->get_total()); $payload['currency'] = $order->get_currency(); $payload['successUrl'] = $order->get_checkout_order_received_url(); $payload['cancelUrl'] = $order->get_checkout_payment_url(); $payload['failureUrl'] = $this->getOrderCheckoutPaymentUrl( 'failed', $order ); $payload['totalDiscount'] = $order->get_total_discount(); $payload['totalTaxAmount'] = $order->get_total_tax(); $payload['subtotalAmount'] = $order->get_subtotal(); if (!isset($payload['metadata'])) { $payload['metadata'] = array(); } $metadata = $this->buildMetadata($order); $payload['metadata'] = array_merge($payload['metadata'], $metadata); $payload['productType'] = 'kkart'; return $payload; } public function buildMetadata( KKART_Order $order ){ $custom_info = array(); $note = join(' ', array( __( 'order', 'kkart' ), $order->get_id(), __( 'from', 'kkart' ), $order->get_billing_first_name(), $order->get_billing_last_name(), '(' . $order->get_billing_email() . ')', ) ); $custom_info['billNote'] = $note; $custom_info['customerEmailAddress'] = $order->get_billing_email(); $custom_info['customerLastName'] = $order->get_billing_last_name(); $custom_info['customerFirstName'] = $order->get_billing_first_name(); return $custom_info; } public function post( string $url, array $args ){ if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) { throw new Exception( __( 'Invalid URL for POST request.', 'yoco_wc_payment_gateway' ) ); } $response = wp_remote_post( $url, $args ); if ( is_wp_error( $response ) ) { throw new Exception( $response->get_error_message(), 0 ); } $code = wp_remote_retrieve_response_code( $response ); //start from here $message = wp_remote_retrieve_response_message( $response ); $body = wp_remote_retrieve_body( $response ); return array( 'code' => $code, 'message' => $message, 'body' => (array) json_decode( $body ), ); } public function format( $value, array $options = array() ): int { $options = wp_parse_args( $options, array( 'decimals' => 2, 'rounding_mode' => PHP_ROUND_HALF_UP, ) ); $decimals = absint( $options['decimals'] ); $rounding_mode = min( absint( $options['rounding_mode'] ), 4 ); return intval( round( ( (float) kkart_format_decimal( $value ) ) * ( 10 ** $decimals ), 0, $rounding_mode ) ); } public function get_credential(){ $cred_arr = array(); $cred_arr['livePublic'] = $this->mode === "live" ? $this->get_option( 'live_public_key', '' ) : ''; $cred_arr['liveSecret'] = $this->mode === "live" ? $this->get_option( 'live_secret_key', '' ) : ''; $cred_arr['testPublic'] = $this->mode === "test" ? $this->get_option( 'test_public_key', '' ) : ''; $cred_arr['testSecret'] = $this->mode === "test" ? $this->get_option( 'test_secret_key', '' ) : ''; return $cred_arr; } public function check_requirements() { $errors = []; // Check if the store currency is supported by YOCO if (!in_array(get_kkart_currency(), $this->available_currencies)) { $errors[] = 'kkart-gateway-yoco-error-invalid-currency'; } if ($this->mode === 'live' && empty($this->get_option('live_secret_key'))) { $errors[] = 'kkart-gateway-yoco-error-missing-livekey'; } if ($this->mode === 'test' && empty($this->get_option('test_secret_key'))) { $errors[] = 'kkart-gateway-yoco-error-missing-testkey'; } if ($this->mode === 'test' && !preg_match('/^sk_test/', $this->get_option('test_secret_key'))) { $errors[] = 'kkart-gateway-yoco-error-wrong-testkey'; } if ($this->mode === 'live' && !preg_match('/^sk_live/', $this->get_option('live_secret_key'))) { $errors[] = 'kkart-gateway-yoco-error-wrong-livekey'; } if($this->is_rest_api_enabled() === false){ $errors[] = 'rest-api-disabled'; } return array_filter( $errors ); } public function is_rest_api_enabled(){ $rest_api = get_option('sitepad_rest_api'); if(empty($rest_api)){ return false; } return true; } public function is_available() { if ( 'yes' === $this->enabled ) { $errors = $this->check_requirements(); return 0 === count( $errors ); } return parent::is_available(); } public function admin_notices() { // Get requirement errors. $errors_to_show = $this->check_requirements(); // If everything is in place, don't display it. if( ! count( $errors_to_show ) ){ return; } // If the gateway isn't enabled, don't show it. if ( "no" === $this->enabled ) { return; } // Use transients to display the admin notice once after saving values. if ( ! get_transient( 'kkart-gateway-YOCO-admin-notice-transient' ) ) { set_transient( 'kkart-gateway-YOCO-admin-notice-transient', 1, 1); echo '<div class="notice notice-error is-dismissible"><p>' . __( 'To use YOCO as a payment provider, you need to fix the problems below:', 'kkart' ) . '</p>' . '<ul style="list-style-type: disc; list-style-position: inside; padding-left: 2em;">' . array_reduce( $errors_to_show, function( $errors_list, $error_item ) { $errors_list = $errors_list . PHP_EOL . ( '<li>' . $this->get_error_message($error_item) . '</li>' ); return $errors_list; }, '' ) . '</ul></p></div>'; } } public function get_error_message( $key ) { $error_msg = ''; switch ( $key ) { case 'kkart-gateway-yoco-error-invalid-currency': $error_msg = __( 'Your store uses a currency that YOCO doesnt support yet.', 'kkart' ); break; case 'kkart-gateway-yoco-error-missing-livekey': $error_msg = __( 'You forgot to fill your live key.', 'kkart' ); break; case 'kkart-gateway-yoco-error-missing-testkey': $error_msg = __( 'You forgot to fill your test key.', 'kkart' ); break; case 'kkart-gateway-yoco-error-wrong-testkey': $error_msg = __( 'Check the formatting for the test key.', 'kkart' ); break; case 'rest-api-disabled' : $error_msg = sprintf( __('SitePad REST API is disabled. Please <a href="%s">enable</a> it first.', 'kkart'), esc_url(admin_url('options-general.php')) ); break; case 'kkart-gateway-yoco-error-wrong-livekey': $error_msg = __( 'Check the formatting for the live key.', 'kkart' ); break; } return $error_msg; } public function process_admin_options() { parent::process_admin_options(); $mode = $this->get_option('mode', 'test'); $secret_key_option = $mode === 'live' ? 'live_secret_key' : 'test_secret_key'; $secret_key = $this->get_option($secret_key_option); if (!empty($secret_key)) { $this->register_yoco_webhook($secret_key); } } public function register_yoco_webhook($secret_key) { $webhook_url = get_rest_url(null, 'kkart/v3/webhook'); $mode = $this->get_option('mode', 'test'); $payload = array( 'name' => 'kkart-yoco-webhook', 'url' => $webhook_url, ); $args = array( 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $secret_key, ), 'body' => json_encode($payload), ); $response = wp_remote_post('https://payments.yoco.com/api/webhooks', $args); $response_params= wp_remote_retrieve_body( $response ); $response_data = json_decode($response_params, true); if(is_wp_error($response)){ $error_message = $response->get_error_message(); error_log('Yoco Webhook Registration Error: ' . $error_message); }else{ $response_code = wp_remote_retrieve_response_code($response); if($response_code === 200 || $response_code === 201){ $key = 'live' === $mode ? 'yoco_payment_gateway_live_webhook_secret' : 'yoco_payment_gateway_test_webhook_secret'; $current_secret = get_option( $key ); if ( $current_secret === $response_data['secret'] ) { return; } update_option( $key, $response_data['secret']); }else{ error_log('Yoco Webhook Registration Failed: ' . wp_remote_retrieve_body($response)); } } } }