Viết plugin tạo cổng thanh toán VietQR cho Woocommerce

Trước giờ mình thường chỉ phát triển website WordPress dựa trên theme và plugin có sẵn. Sau đó mình sẽ viết thêm code snippets để bổ sung tính năng hoặc chỉnh sửa css để thay đổi giao diện, tùy theo yêu cầu của khách hàng.

Trong hai tuần qua, mình đã ngồi mò cách viết plugin cho WordPress để tích lũy kinh nghiệm, sau này có thể tự bào chế tính năng theo nhu cầu, không bị phụ thuộc vào plugin có sẵn nữa.

Dự án đầu tiên mình thực hiện là viết plugin tạo cổng thanh toán VietQR cho Woocommerce. Plugin được phát triển dựa trên mã nguồn của VietQR của Casso Team, với những chỉnh sửa sau:

  • Xóa bớt những function không cần thiết.
  • Đơn giản hóa form fields.
  • Cập nhật giao diện hiển thị QR và số tài khoản trên trang Thank You.
  • Hỗ trợ khai báo nhiều ngân hàng để tạo nhiều mã VietQR (dự định sẽ làm trong bản cập nhật sau)

Mục đích của bài viết này là để lưu lại cách triển khai plugin, đề phòng sau này có cần làm tiếp mình có thể mở ra tham khảo lại.

1. Tạo thư mục chứa plugin

Mình tạo repository mới trên Github: https://github.com/10h30/vietnam-payment-gateways/ và sử dụng Codespace có sẵn để viết code trực tiếp trên trình duyệt. Sau đó tải plugin về thư mục plugins của WordPress bằng lệnh git clone.

git clone https://github.com/10h30/vietnam-payment-gateways.git

Nếu không quen dùng Github, bạn có thao tác trực tiếp bằng tạo thư mục mới với tên gọi vietnam-payment-gateways trong thư mục plugins của WordPress, và tạo thêm 1 file php vietnam-payment-gateways.php

cd wp-content/plugins/
mkdir vietnam-payment-gateways
nano vietnam-payment-gateways.php

2. Khai báo thông tin plugin

Chỉnh sửa file vietnam-payment-gateways.php, nhập vào thông tin sau để khai báo thông tin về plugin.

Bạn cũng có thể dùng công cụ WordPress plugin boilerplate generator để tạo plugin tự động dựa trên thông tin khai báo.

Truy cập vào trang Installed Plugins, plugin mới của mình đã hiện ra. Bấm Activate để kích hoạt luôn. Nó chưa có tính năng gì cả nên sẽ không ảnh hưởng gì đến hoạt động của web.

3. Khai báo cổng thanh toán

Theo tài liệu Payment Gateway API của Woocommerce, để tạo cổng thanh toán mới, mình cần thêm đoạn code sau vào file vietnam-payment-gateways.php

/*
* This action hook registers our PHP class as a WooCommerce payment gateway
*/
add_filter( 'woocommerce_payment_gateways', 'vnpg_add_gateway_class' );
function vnpg_add_gateway_class( $gateways ) {
	$gateways[] = 'WC_VNPG_YCB'; // your class name is here
	return $gateways;
}

/*
 * The class itself, please note that it is inside plugins_loaded action hook
 */
add_action( 'plugins_loaded', 'vnpg_init_gateway_class' );
function vnpg_init_gateway_class() {
	/**
	 * Constructor for the gateway.
	 */
	class WC_VNPG_YCB extends WC_Payment_Gateway {}

    }

}

Tiếp theo, cần khai báo thông tin cho class WC_VNPG_YCB – cổng thanh toán mới cho Woocommerce

class WC_VNPG_YCB extends WC_Payment_Gateway {
 		public function __construct() {
            $this->id                 = 'vnpg';
            $this->icon               = apply_filters( 'woocommerce_vnpg_icon', '' );
            $this->has_fields         = false;
            $this->method_title       = __( 'Vietnam Bank Transfer (VietQR)', 'vnpg' );
            $this->method_description = __( 'Take payments by scanning QR code with Vietnamese banking App.', 'vnpg' );

            // Load the settings.
		    $this->init_form_fields();
		    $this->init_settings();

            // Actions.
            add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
            //add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'save_account_details' ) );
            add_action( 'woocommerce_thankyou_' . $this->id, array( $this, 'thankyou_page' ) );

		    // Customer Emails.
		    add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 );
	
 		}

 		/**
 		* Initialise Gateway Settings Form Fields.
 		*/
 		public function init_form_fields(){

	 	}
		 /**
         * Output for the order received page.
         *
         * @param int $order_id Order ID.
         */
        public function thankyou_page( $order_id ) {
        
        }
        
        /**
         * Add content to the WC emails.
         *
         * @param WC_Order $order Order object.
         * @param bool     $sent_to_admin Sent to admin.
         * @param bool     $plain_text Email format: plain text or HTML.
         */
        public function email_instructions( $order, $sent_to_admin, $plain_text = false ) {
        
        }

        /**
         * Process the payment and return the result.
         *
         * @param int $order_id Order ID.
         * @return array
         */
        public function process_payment( $order_id ) {
    
            $order = wc_get_order( $order_id );
    
            if ( $order->get_total() > 0 ) {
                // Mark as on-hold (we're awaiting the payment).
                $order->update_status( apply_filters( 'woocommerce_' .$this->id. '_process_payment_order_status', 'on-hold', $order ), __( 'Awaiting BACS payment', 'woocommerce' ) );
            } else {
                $order->payment_complete();
            }
    
            // Remove cart.
            WC()->cart->empty_cart();
    
            // Return thankyou redirect.
            return array(
                'result'   => 'success',
                'redirect' => $this->get_return_url( $order ),
            );
    
        }

    }

Giải thích

  • Dòng 2-21: Hàm __construct() để khai báo thông tin chung cho class WC_VNPG_YCB: id, tên và chú thích cổng thanh toán, khai báo form nhập liệu và gọi các hàm liên quan sau khi khách thanh toán (hiển thị thông tin trên trang Thankyou, gửi thông tin qua Email,..)
  • Dòng 26-28: Hàm init_form_fields() để tạo form khai báo thông tin cho cổng thanh toán (sẽ được cập nhật ở bước sau)
  • Dòng 34-36: Hàm thankyou_page( $order_id ) để hiển thị thông tin cổng thanh toán. cho khách hàng trên trang Thank you sau khi bấm nút Thanh Toán. (sẽ được cập nhật ở bước sau)
  • Dòng 45-47: Hàm email_instructions( $order, $sent_to_admin, $plain_text = false ) để hiển thị thông tin về cổng thanh toán trong email gửi cho khách. (sẽ được cập nhật ở bước sau)
  • Dòng 55-75: Hàm process_payment( $order_id ) để xử lý đơn hàng.

Đoạn code trên được mình chỉnh sửa lại dựa trên mã nguồn của cổng thanh toán BACS có sẵn: https://woocommerce.github.io/code-reference/files/woocommerce-includes-gateways-bacs-class-wc-gateway-bacs.html#source-view.22

Toàn bộ code sau bước 3 như sau

id                 = 'vnpg';
            $this->icon               = apply_filters( 'woocommerce_vnpg_icon', '' );
            $this->has_fields         = false;
            $this->method_title       = __( 'Vietnam Bank Transfer (VietQR)', 'vnpg' );
            $this->method_description = __( 'Take payments by scanning QR code with Vietnamese banking App.', 'vnpg' );

            // Load the settings.
		    $this->init_form_fields();
		    $this->init_settings();

            // Actions.
            add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );
            //add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'save_account_details' ) );
            add_action( 'woocommerce_thankyou_' . $this->id, array( $this, 'thankyou_page' ) );

		    // Customer Emails.
		    add_action( 'woocommerce_email_before_order_table', array( $this, 'email_instructions' ), 10, 3 );
	
 		}

 		/**
 		* Initialise Gateway Settings Form Fields.
 		*/
 		public function init_form_fields(){

	 	}
		 /**
         * Output for the order received page.
         *
         * @param int $order_id Order ID.
         */
        public function thankyou_page( $order_id ) {
        
        }
        
        /**
         * Add content to the WC emails.
         *
         * @param WC_Order $order Order object.
         * @param bool     $sent_to_admin Sent to admin.
         * @param bool     $plain_text Email format: plain text or HTML.
         */
        public function email_instructions( $order, $sent_to_admin, $plain_text = false ) {
        
        }

        /**
         * Process the payment and return the result.
         *
         * @param int $order_id Order ID.
         * @return array
         */
        public function process_payment( $order_id ) {
    
            $order = wc_get_order( $order_id );
    
            if ( $order->get_total() > 0 ) {
                // Mark as on-hold (we're awaiting the payment).
                $order->update_status( apply_filters( 'woocommerce_' .$this->id. '_process_payment_order_status', 'on-hold', $order ), __( 'Awaiting BACS payment', 'woocommerce' ) );
            } else {
                $order->payment_complete();
            }
    
            // Remove cart.
            WC()->cart->empty_cart();
    
            // Return thankyou redirect.
            return array(
                'result'   => 'success',
                'redirect' => $this->get_return_url( $order ),
            );
    
        }

    }
}

Tên cổng thanh toán giờ đã hiện ra trong mục Setting -> Payments của Woocommerce. Ở bước tiếp theo, mình sẽ bổ sung các form nhập liệu cho cổng thanh toán.

4. Tạo form fields

public function init_form_fields(){

            //Tự động sinh prefix đơn hàng cho website.
            $server_domain = $_SERVER['SERVER_NAME'];
            $shopname = preg_replace('#^.+://[^/]+#', '', $server_domain);
            $shopname = str_replace(".","",$shopname);

		    $this->form_fields = array(
                'enabled'         => array(
                    'title'   => __( 'Enable/Disable', 'woocommerce' ),
                    'type'    => 'checkbox',
                    'label'   => __( 'Enable Vietnam Payment Gateway', 'vnpg' ),
                    'default' => 'no',
                ),
                'title'           => array(
                    'title'       => __( 'Title', 'woocommerce' ),
                    'type'        => 'text',
                    'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
                    'default'     => __( 'Direct Bank Transfer via Vietcombank (VietQR)', 'vnpg' ),
                    'desc_tip'    => true,
                ),
                'description'     => array(
                    'title'       => __( 'Description', 'woocommerce' ),
                    'type'        => 'textarea',
                    'description' => __( 'Payment method description that the customer will see on your checkout.', 'woocommerce' ),
                    'default'     => __( 'Make your payment directly into our bank account. Please use your Order ID as the payment reference. Your order will not be shipped until the funds have cleared in our account.', 'woocommerce' ),
                    'desc_tip'    => true,
                ),
                'bank'           => array(
                    'title'       => __('Bank Name', 'vnpg'),
                    'type'        => 'text',
                  ),
                'account_number' => array(
                    'title' => __( 'Account Number', 'vnpg'),
                    'type' => 'text',
                  ),
                 'account_name' => array(
                    'title' => __( 'Account Name', 'vnpg'),
                    'type' => 'text'
                  ),
                 'prefix'           => array(
                    'title'       => __('Prefix', 'vnpg'),
                    'type'        => 'text',
                    'description' => __('Prefix used to combine with order code to create money transfer content, Set rules: no spaces, no more than 15 characters and no special characters. Violations will be deleted', 'vnpg'),
                    'default'     => $shopname,
                    'desc_tip'    => true,
                  ),
                  'template_id' => array(
                    'title' => __( 'VietQR Template ID', 'vnpg'),
                    'type' => 'text',
                    'default' => 'compact'
                  ),
                  
            
            );
	
	 	}

Form sẽ gồm các mục sau

  • Enable/Disable: Tắt mở cổng thanh toán
  • Title: Tên cổng thanh toán.
  • Description: Chú thích, sẽ hiện ra ở trang thanh toán.
  • Bank Name: Tên ngân hàng.
  • Account Number: Số tài khoản ngân hàng.
  • Account Name: Tên chủ tài khoản.
  • Prefix: Tiền tố để ghép với mã đơn hàng, dùng để làm nội dung chuyển tiền.
  • VietQR Template ID: mặc định là compact, hoặc thay đổi thành template ID tự tạo ra ở đây.

Dưới đây là form hiện ra khi vào cấu hình cổng thanh toán Vietnam Bank Transfer

Ngoài ra mình bổ sung thêm đoạn code này vào hàm __construct() để gán các giá trị của form vào biến số.

// Define user set variables.
            $this->title        = $this->get_option( 'title' );
            $this->description  = $this->get_option( 'description' );
            $this->account_name = $this->get_option( 'account_name' );
            $this->account_number = $this->get_option( 'account_number' );
            $this->template_id = $this->get_option( 'template_id' );
            $this->prefix = $this->get_option('prefix');
            $this->bank = $this->get_option('bank');

Toàn bộ file plugin sau bước 4 có nội dung như dưới đây

5. Hiển thị thông tin thanh toán trên trang Thank you và email

Để hiển thị thông tin thanh toán, mình chỉnh sửa lại hàm thankyou_pageemail_instructions, thêm vào dòng $this->payment_details( $order_id);

        public function thankyou_page( $order_id ) {
            $this->payment_details( $order_id );
        }
        
        /**
         * Add content to the WC emails.
         *
         * @param WC_Order $order Order object.
         * @param bool     $sent_to_admin Sent to admin.
         * @param bool     $plain_text Email format: plain text or HTML.
         */
        public function email_instructions( $order, $sent_to_admin, $plain_text = false ) {
            if (!$sent_to_admin && 'vnpg' === $order->get_payment_method() && $order->has_status('on-hold')) {
                $this->payment_details($order->get_id());
            }
        }

Sau đó khai báo hàm mới payment_details nằm trong class WC_VNPG_YCB như dưới đây.

       private function payment_details($order_id) {

            // Get order and store in $order.
		    $order = wc_get_order($order_id);

            $html  = '

Thông tin thanh toán

'; $html .= '
Bạn vui lòng chuyển khoản theo thông tin dưới đây
'; $html .= '
    '; $html .= '
  • Số tiền: '. $order->get_total() . '
  • '; $html .= '
  • Ngân hàng: '. $this->bank . '
  • '; $html .= ''; $html .= ''; $html.= '
  • Nội dung: '. $this->prefix . $order_id .'
  • '; $html .= '
'; echo $html; }

Toàn bộ file plugin sau bước 5 có nội dung như dưới đây

Đặt thử 1 đơn hàng trên web, thông tin thanh toán đã hiện ra trên trang Thank you lẫn trong email gửi cho khách.

Thông tin thanh toán hiện ra trên trang Thank you
Thông tin thanh toán trong email

6. Hiển thị VietQR Barcode

Bước kế tiếp là bổ sung mã VietQR vào nội dung thanh toán.

Tạo hàm mới với tên gọi get_vietqr_img_url, hàm này sẽ nhận biến số $order_id và trả về 1 array chứa link hình và link thanh toán của QR Code.

        public function get_vietqr_img_url($order_id) {

            // Get order and store in $order.
		    $order = wc_get_order($order_id);

            $accountNo = $this->account_number;
            $accountName = $this->account_name;
            $bank = $this->bank;
            $amount = $order->get_total();
            $info = $account_fields['memo']['2value'];
            
            $template = $this->template_id;

            $img_url = "https://img.vietqr.io/image/{$bank}-{$accountNo}-{$template}.jpg?amount={$amount}&addInfo={$info}&accountName={$accountName}";
            $pay_url = "https://api.vietqr.io/{$bank}/{$accountNo}/{$amount}/{$info}";

            return array(
                "img_url" => $img_url,
                "pay_url" => $pay_url,
            );
	    }

Cập nhật lại hàm payment_details, bổ sung phần hiển thị barcode

        private function payment_details($order_id) {

            // Get order and store in $order.
		    $order = wc_get_order($order_id);

            // Get VietQR Image URL and Pay URL
            $data = $this->get_vietqr_img_url($order_id);
			$qrcode_image_url  = $data['img_url'];
			$qrcode_page_url = $data['pay_url'];

            $html  = '

Thông tin thanh toán

'; $html .= '
Bạn vui lòng chuyển khoản theo thông tin dưới đây
'; $html .= '
VietQR QR Image
'; $html .= '
    '; $html .= '
  • Số tiền: '. $order->get_total() . '
  • '; $html .= '
  • Ngân hàng: '. $this->bank . '
  • '; $html .= ''; $html .= ''; $html.= '
  • Nội dung: '. $this->prefix . $order_id .'
  • '; $html .= '
'; echo $html; }

Đặt lại đơn hàng mới, VietQR giờ đã hiện ra. Là lá la!

7. Tạo form lựa chọn danh sách ngân hàng hỗ trợ VietQR

Hiện tại, mục Bank Name của cổng thanh toán đang là dạng Text, sẽ dễ gây ra lỗi nếu điền sai tên ngân hàng, ví dụ “Vietconbank”, hoặc điền tên ngân hàng không hỗ trợ VietQR. Do đó mình sẽ đổi form lại thành dạng dropdown, danh sách sẽ là các ngân hàng hỗ trợ VietQR.

Tạo hàm get_vietqr_bank_list() để nhận danh sách ngân hàng từ API của VietQR.

        public function get_vietqr_bank_list() {

            $body = get_transient( 'vietqr_banklist' );
            
            if ( false === $body ) {
                $url = "https://api.vietqr.io/v2/banks";
                $response = wp_remote_get($url );
            
                if (200 !== wp_remote_retrieve_response_code($response)) {
                    return;
                }
            
                $body = wp_remote_retrieve_body($response);
                set_transient( 'vietqr_banklist', $body, DAY_IN_SECONDS );
            }
            $bank_list = json_decode($body, true);
            return $bank_list;
        }

Trong đoạn code trên, mình có sử dụng thêm Transient API để lưu cache trong vòng 24 giờ, hạn chế gửi truy vấn liên tục về VietQR.

Bổ sung thêm dòng này vào hàm __construct() của class WC_VietQR_YCB

            //Get bank list from VietQR API
            $this->bank_list = $this->get_vietqr_bank_list();

Chỉnh sửa lại hàm init_form_fields():

  • Dòng 73-77: tạo array chứa danh sách tên ngân hàng hỗ trợ VietQR
  • Dòng 37: sửa type của bank từ text thành select
  • Dòng 39: bổ sung thêm option
        public function init_form_fields(){

            //Tự động sinh prefix đơn hàng cho website.
            $server_domain = $_SERVER['SERVER_NAME'];
            $shopname = preg_replace('#^.+://[^/]+#', '', $server_domain);
            $shopname = str_replace(".","",$shopname);

            //Tạo danh sách tên ngân hàng cho select form
            $bank_name = [];
            foreach ($this->bank_list['data'] as $bank) {
                $bank_name[$bank['short_name']] = $bank['short_name'];
            }

		    $this->form_fields = array(
                'enabled'         => array(
                    'title'   => __( 'Enable/Disable', 'woocommerce' ),
                    'type'    => 'checkbox',
                    'label'   => __( 'Enable Vietnam Payment Gateway', 'vnpg' ),
                    'default' => 'no',
                ),
                'title'           => array(
                    'title'       => __( 'Title', 'woocommerce' ),
                    'type'        => 'text',
                    'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
                    'default'     => __( 'Direct Bank Transfer via Vietcombank (VietQR)', 'vnpg' ),
                    'desc_tip'    => true,
                ),
                'description'     => array(
                    'title'       => __( 'Description', 'woocommerce' ),
                    'type'        => 'textarea',
                    'description' => __( 'Payment method description that the customer will see on your checkout.', 'woocommerce' ),
                    'default'     => __( 'Make your payment directly into our bank account. Please use your Order ID as the payment reference. Your order will not be shipped until the funds have cleared in our account.', 'woocommerce' ),
                    'desc_tip'    => true,
                ),
                'bank'           => array(
                    'title'       => __('Bank Name', 'vnpg'),
                    'type'        => 'selectg',
                    'options'     => $bank_name,
                  ),
                'account_number' => array(
                    'title' => __( 'Account Number', 'vnpg'),
                    'type' => 'text',
                  ),
                 'account_name' => array(
                    'title' => __( 'Account Name', 'vnpg'),
                    'type' => 'text'
                  ),
                 'prefix'           => array(
                    'title'       => __('Prefix', 'vnpg'),
                    'type'        => 'text',
                    'description' => __('Prefix used to combine with order code to create money transfer content, Set rules: no spaces, no more than 15 characters and no special characters. Violations will be deleted', 'vnpg'),
                    'default'     => $shopname,
                    'desc_tip'    => true,
                  ),
                  'template_id' => array(
                    'title' => __( 'VietQR Template ID', 'vnpg'),
                    'type' => 'text',
                    'default' => 'compact'
                  ),
                  
            
            );
	
	 	}

Quay lại trang cấu hình, mục Tên ngân hàng giờ đã hiện ra danh sách để chọn lựa.

8. Những việc cần làm kế tiếp

Plugin hiện tại tuy chưa hoàn chỉnh lắm ở phần hiển thị ngoài front-end, nhưng nói chung đã đủ dùng cho nhu cầu căn bản. Toàn bộ code mình chia sẻ từ đầu đến giờ có thể được tải ở đây: Vietnam Payment Gateways.

Những việc cần làm kế tiếp (nếu có thời gian rãnh)

  • Mông má lại phần hiển thị thông tin nhìn xinh đẹp, thân thiện hơn.
  • Hiển thị logo ngân hàng ở phần chọn lựa phương thức thanh toán.
  • Hỗ trợ khai báo nhiều tài khoản ngân hàng.
  • Hỗ trợ quét mã Momo / ZaloPay / ShopeePay / …
  • Hỗ trợ cổng thanh toán thẻ VNPay, OnePay,…

Nếu bài viết của mình mang đến thông tin, kiến thức hữu ích cho bạn, đừng ngại mời mình ly bia để có thêm động lực chia sẻ nhiều hơn nữa. Cám ơn bạn!

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *