- vừa được xem lúc

Tích hợp VNPAY vào Rails

1 1 84

Người đăng: Quang Vo

Theo Viblo Asia

Xin chào 500 ae năm mới nhé! ??

Hôm nay là ngày đẹp nên mình đã chọn làm ngày khai bút cho năm 2021 luôn. ??

Tiếp nối câu chuyện về Thanh toán online mà mình có chia sẽ ở 2 bài trước, mọi người chưa đọc thì có thể vào xem ở đây nhé. Lần trước mình có chia sẻ về 2 cổng thanh toán online quốc tế lớn hiện nay đó là PaypalStripe, trong quá trình làm việc với 2 ông lớn này thì mình thấy rằng nó chưa thực sự hỗ trợ nhiều cho Việt Nam mình đặc biệt là về mặt đăng kí tài khoản bussiness, hỗ trợ giao dịch với VNĐ. Thế nên mình quyết định dùng hàng nội địa để phục vụ cho người nội địa xem thế nào.

Sau một hồi tìm hiểu thì thấy có 2 cổng nội địa có thể tích hợp vào rails app đó là momovnpay. Thế là mình tiến hành liên hệ với cả 2, sau một vài hôm thì vnpay đã liên hệ mình trước và thế là mình đã chọn luôn vnpay để tiến hành tích hợp vào ứng dụng. Vậy quá trình mình tích hợp vnpay vào rails như thế nào và đã gặp được những hố gì nào?

Bài viết tập trung chủ yếu vào cách thức tích hợp vnpay nên mình xin phép bỏ qua phần giới thiệu về VNPAY nhé, mn có thể đọc hiểu thêm ở gg.

1. Chuẩn bị

=> Sau khi có được thông tin tài khoản test, ta bắt tay vào tích hợp thôi nào

2. Tiến hành tích hợp

Về cách thức tích hợp thì bên VNPAY cũng có viết document cũng khá là chi tiết, mn có thể xem ở đây https://sandbox.vnpayment.vn/apis/docs/huong-dan-tich-hop/. Hiên tại code mẫu tích hợp không có ruby thế nên để tích hợp mình phải convert code mẫu từ php sang ruby.

Có 3 endpoint mình cần thực thi:

  • URL thanh toán: URL này sẽ do bên VNPAY cung cấp cho chúng ta, khi tiến hành thanh toán thì mình sẽ gửi các thông tin mua hàng đến URL này và việc còn lại sẽ do vnpay xử lí , URL có dạng như sau:

    http://sandbox.vnpayment.vn/paymentv2/vpcpay.html?vnp_Amount=10000000&vnp_BankCode=NCB&vnp_Command=pay&vnp_CreateDate=20170829103111&vnp_CurrCode=VND&vnp_IpAddr=172.16.68.68&vnp_Locale=vn&vnp_Merchant=DEMO&vnp_OrderInfo=Nap+tien+cho+thue+bao+0123456789.+So+tien+100%2c000&vnp_OrderType=topup&vnp_ReturnUrl=http%3a%2f%2fsandbox.vnpayment.vn%2ftryitnow%2fHome%2fVnPayReturn&vnp_TmnCode=2QXUI4J4&vnp_TxnRef=23554&vnp_Version=2&vnp_SecureHashType=SHA256&vnp_SecureHash=e6ce09ae6695ad034f8b6e6aadf2726f
    
    • Danh sách tham số:
  • URL Return: URL này do mình tạo ra và sẽ gửi kèm qua URL thanh toán với key là vnp_ReturnUrl, mục đích của URL này là dùng để nhận thông tin giao dịch từ phía VNPAY trả về để hiển thị ra cho END_USER và không dùng cho việc cập nhật dữ liệu. Và có dạng sau:

    https://mypage/return_url?vnp_Amount=1000000&vnp_BankCode=NCB&vnp_BankTranNo=20170829152730&vnp_CardType=ATM&vnp_OrderInfo=Thanh+toan+don+hang+thoi+gian%3A+2017-08-29+15%3A27%3A02&vnp_PayDate=20170829153052&vnp_ResponseCode=00&vnp_TmnCode=2QXUI4J4&vnp_TransactionNo=12996460&vnp_TxnRef=23597&vnp_SecureHashType=SHA256&vnp_SecureHash=20081f0ee1cc6b524e273b6d4050fefd
    
  • URL IPN: URL này bên mình sẽ tạo ra và gửi sang VNPAY để họ config ở phía server VNPAY, sau khi VNPAY tiếp nhận thông tin thanh toán từ thanh toán URL và xử lí các thông tin giao dịch hoàn thành thì đầu VNPAY sẽ gửi thông tin giao dịch như mã giao dịch, trang thái giao dịch,... về URL IPN do mình cung cấp. URL này mình sẽ dùng để cập nhật thông tin giao dịch vào DB. URL có dạng sau

    http://mypage/vnpay_ipn?vnp_Amount=1000000&vnp_BankCode=NCB&vnp_BankTranNo=20170829152730&vnp_CardType=ATM&vnp_OrderInfo=Thanh+toan+don+hang+thoi+gian%3A+2017-08-29+15%3A27%3A02&vnp_PayDate=20170829153052&vnp_ResponseCode=00&vnp_TmnCode=2QXUI4J4&vnp_TransactionNo=12996460&vnp_TxnRef=23597&vnp_SecureHashType=SHA256&vnp_SecureHash=20081f0ee1cc6b524e273b6d4050fefd
    

Dưới dây là work flow thanh toán bên phía VNPAY(nguồn vnpay):

a) URL thanh toán

  • Dựa vào document của VNPAY mình sẽ có được code mẫu, từ code mẫu mình tiến hành convert sang RUBY như dưới

    # checkouts_controller.rb
    def execute order = Order.new ... vnp_url = get_payment_url(order, fallback_checkouts_url) redirect_to vnp_url
    end def fallback # this is URL return # todo here
    end private def get_payment_url(order_object, return_url) # Mã website bên VNPAY cung cấp ban đầu cho mình vnp_tmncode = ENV["VNP_TMNCODE"] # Chuỗi kí tự dùng cho việc mã hóa tham số do bên VNPAY cung cấp vnp_hash_secret = ENV["VNP_HASH_SECRET"] vnp_url = ENV["VNP_URL"] vnp_txnref = order_object.id vnp_order_info = "Thanh toan mua hang" vnp_order_type = "190000" vnp_amount = order_object.total_price_cents * 100 vnp_local = "vn" vnp_ipadd = request.remote_ip input_data = { "vnp_Amount" => vnp_amount, "vnp_Command" => "pay", "vnp_CreateDate" => DateTime.current.strftime("%Y%m%d%H%M%S"), "vnp_CurrCode" => "VND", "vnp_IpAddr" => vnp_ipadd, "vnp_Locale" => vnp_local, "vnp_OrderInfo" => vnp_order_info, "vnp_OrderType" => vnp_order_type, "vnp_ReturnUrl" => return_url, "vnp_TmnCode" => vnp_tmncode, "vnp_TxnRef" => vnp_txnref, "vnp_Version" => "2.0.0", } original_data = input_data.map do |key, value| "#{key}=#{value}" end.join("&") vnp_url = vnp_url + "?" + input_data.to_query vnp_security_hash = Digest::SHA256.hexdigest(vnp_hash_secret + original_data) vnp_url += '&vnp_SecureHashType=SHA256&vnp_SecureHash=' + vnp_security_hash vnp_url end
    
  • Ở trên mình có tạo hàm get_payment_url(order_object, return_url) dùng để tạo ra URL thanh toán với các tham số ứng với yêu cầu của VNPAY. Dựa vào các thông tin liên quan đến website và bảo mật do bên VNPAY cung cấp, cộng với các thông tin khách hàng mình có thể dễ dàng tạo ra được các params thông tin mua hàng cần thiết đó là input_data. Sau khi có được input_data mình sẽ đem nó đi tạo chuỗi mã hóa vnp_security_hash. Cuối cùng mình kết hợp input_datavnp_security_hash để tạo nên vnp_url.

  • Một điểm lưu ý quan trọng là các tham số có trong URL trước khi đưa vào mã hóa piải được sort theo thứ tự Alphabet, ở bài viết của mình thì chính là các key-value có trong biến input_data. Vì đã bỏ qua điểm nhỏ này mà mình đã tốn kha khá thời gian để giải quyết được.

b) URL return

  • Khi tiếp nhận thông tin từ VNPAY bước đầu tiên mình cần làm đó phải validate xem giữ liệu có trong param có hợp lệ hay không. Bằng cách lấy các thông tin params: "vnp_Amount", "vnp_BankCode", "vnp_BankTranNo", "vnp_CardType", "vnp_OrderInfo", "vnp_PayDate", "vnp_ResponseCode", "vnp_TmnCode", "vnp_TransactionNo", "vnp_TxnRef" có trong URL đem đi tạo chuỗi mã hóa, sau đó đem đi so sánh với chuỗi mã hóa trong params : params["vnp_SecureHash"]. Nếu 2 chuỗi này giống nhau thì là valid.

  • Như mình đã nói ở trên URL này chỉ dùng để hiển thị thông tin giao dịch cho END_USER biết chứ không dùng để cập nhật thông tin vào cơ sở dữ liệu.

    # checkouts_controller.rb def fallback if checksum_valid! if params["vnp_ResponseCode"] == "00" current_user.cart.clear flash[:success] = t('.payment_success') redirect_to books_path else flash[:error] = t('.payment_failed') redirect_to checkouts_path end else flash[:error] = t('.payment_failed') redirect_to checkouts_path end
    end private def checksum_valid! vnp_secure_hash = params["vnp_SecureHash"] data = response_params.to_h.map do |key, value| "#{key}=#{value}" end.join("&") secure_hash = Digest::SHA256.hexdigest(ENV["VNP_HASH_SECRET"] + data) vnp_secure_hash == secure_hash end def response_params params.permit("vnp_Amount", "vnp_BankCode", "vnp_BankTranNo", "vnp_CardType", "vnp_OrderInfo", "vnp_PayDate", "vnp_ResponseCode", "vnp_TmnCode", "vnp_TransactionNo", "vnp_TxnRef") end
    

c) URL IPN

  • Cũng tương tự Return URL, URL IPN cũng cần phải check thông tin hợp lệ trước khi tiếp tục xử lí
  • URL này sẽ dùng vào mục đích cập nhật dữ liệu vào db, khi implement mn cần thực thi đủ 8 case bên dưới. 8 case này do bên VNPAY cung cấp và sau mình tích hợp xong họ cũng sẽ cùng mình tiến hành test đủ 8 case, nếu pass hết thì sau đó mới tiên hành đưa lên mt production.
def vnp_ipn logger = Logger.new("#{Rails.root}/log/payment_#{Date.today.to_s}.log") response_data = [] if checksum_valid! permit_params = response_params order_object = Order.find_by_id(permit_params["vnp_TxnRef"]) if order_object if order_object.total_price_cents == (permit_params["vnp_Amount"].to_i / 100) if order_object.pending? if permit_params["vnp_ResponseCode"] == "00" order_object.set_paid else order_object.set_failed end # save vnp_TransactionNo order_object.transaction_id = permit_params['vnp_TransactionNo'] order_object.save! code = "00" message = "Confirm Success" else code = "02" message = "Order already confirmed" end else code = "04" message = "Invalid amount" end else code = "01"; message = "Order not found" end else code = "97"; message = "Invalid Checksum" end logger.info("VNPAY with params: " + permit_params.to_s + ", code: #{code}, message: #{message}") render json: { "RspCode": code, "Message": message }
rescue => e logger.error("VNPAY with params: " + permit_params.to_s + ", " + e.message) render json: { "RspCode": "99", "Message": "Unknow error" }
end

=> Tổng kết:

  • Cần 3 URL: URL thanh toán, URL Return, URL IPN
  • Check thông tin valid dựa vào chuỗi bảo mật trước khi tiếp tục xử lí cho URL Return và URL IPN
  • Cần sắp xếp thứ tự các key trong params theo thứ tự ALPHABET trước khi tạo chuỗi bảo mật
  • Implment đủ các case do bên VNPAY cung cấp.

3. Tài liệu tham khảo

Bình luận

Bài viết tương tự

- vừa được xem lúc

Docker: Chưa biết gì đến biết dùng (Phần 3: Docker-compose)

1. Mở đầu. . .

0 0 106

- vừa được xem lúc

Tìm hiểu Adapter Pattern trong Rails

. Nếu là một web developer chắc hẳn chúng ta đã không ít lần đọc qua về các Design patterns hay cách áp dụng chúng để làm cho code trở nên hướng đối tượng hơn, dễ đọc, dễ hiểu, dễ maintain, dễ mở rộng, … Các design patterns được áp dụng khá nhiều trong các Rails projects như Service Object, Decorato

0 0 35

- vừa được xem lúc

Sử dụng Searchkick để tìm kiếm thông minh trên Rails và Elasticsearch

Bạn đã bao giờ tự hỏi, ứng dụng web của mình có thể mở rộng quy mô bằng cách học được các từ khóa mà người dùng tìm kiếm? Có giải pháp nào cung cấp công cụ tìm kiếm tự động nhanh chóng với chỉ 1 từ khóa bất kì? Thật may khi có Searchkick và Elasticsearch là các công cụ hỗ trợ công việc tìm kiếm trở

0 0 90

- vừa được xem lúc

Những sai lầm bạn có thể mắc phải khi code Rails

. Chào các bạn, chào các bạn. Đừng vội đóng tab nha.

0 0 35

- vừa được xem lúc

Một số lưu ý cải thiện performance khi làm việc với Rails

Khi làm việc với ruby on rails chắc hẳn chúng ta sẽ làm việc với active record rất nhiều. Tuy nhiên có nhiều điều có thể ta vẫn chưa thực sự hiểu, ví dụ như ActiveRecord execute SQL query như thế nào? Và cũng còn khá nhiều lập trình viên khác cũng không để ý tới điều này.

0 0 98

- vừa được xem lúc

Insert_all và Upsert_all trong Rails 6

Có rất nhiều trường hợp chúng ta cần thêm một loạt bản ghi ví dụ như khi có một danh sách các user bằng CSV và cần phải import vào ứng dụng của chúng ta. Rails 6 đã thêm 3 method insert_all, insert_all! và upsert_all vào ActiveRecord::Persistence, để giải quyết vấn đề trên.

0 0 36