Hướng dẫn cách tích hợp Paypal vào trang thanh toán với Golang

Có thể nói Golang là một mã nguồn tuyệt vời trong việc phát triển các chức năng cho Web Application, xây dựng các Backend với tốc độ nhanh, dễ viết để tách bạch với nền tảng Frontend hiện đại như Flutter, React, Vue...Một trong những lợi thế tuyệt vời của Golang đó là thư viện go/html được tích hợp sẵn giúp việc render ra các web tĩnh trở nên dễ dàng hơn bao giờ hết. 

Tận dụng thế mạnh đó, Nam Digital đã áp dụng để xây dựng rất nhiều dự án với thời gian phát triển ngắn nhưng lại mang lại tốc độ tuyệt vời cũng như trải nghiệm đơn giản. Trong đó Nam phát triển 3 website

Tạm thời thì Nam vẫn đang rất ấn tượng với tốc độ xử lý Backend cũng như Render ra các page rất nhanh của Go, điều tuyệt vời cũng nằm ở tính ổn định của các sản phẩm được khởi tạo, chạy hàng năm trời cũng rất ổn áp bởi các hệ thống được tách bạch hoàn toàn bằng Docker Container.

Quên mất, hãy bắt đầu chủ đề chính thôi, cùng tới với cách tích hợp Paypal vào trang thanh toán với Golang

Hàm checkout của Web tĩnh sẽ có dạng nào?

Hàm Checkout của Web tĩnh sẽ thường là action của Form trước đó, tức là nó sẽ parse nội dung của form như:

  • Họ tên người đặt hàng
  • Số tiền 
  • Tổng cộng (total)
  • Địa chỉ thanh toán...

Bỏ qua các yếu tố râu ria thì cứ xem như chúng ta đã get được total order rồi, giờ hãy đem nó đi xử lý về mặt Logic thôi

Trường hợp 1: Sử dụng thư viện Plutov/paypal để tạo thanh toán xử lý đơn hàng 

Nam đánh giá đây là một thư viện rất tốt trong việc điều hướng tới các Endpoint của Paypal nhằm gửi đơn hàng, bạn cũng cần có tài khoản Sandbox của Paypal để thực hiện Test nhé, cập nhật 1 chút là trường hợp này các hàm đang áp dụng cho  Plutov/paypal/v4.7 (Hiện latest là 4.11 nên sẽ khác 1 chút)

Dưới đây là ví dụ của việc gửi đơn hàng cho Paypal theo Total đã format từ tiền Việt sang 

 

// Hàm xử lý thanh toán PayPal
func HandlePaypalPayment(w http.ResponseWriter, r *http.Request) {

    // Cấu hình PayPal
    var Client *paypal.Client

    // Sử dụng môi trường sandbox cho việc phát triển
    Client, err := paypal.NewClient("Client ID", "Client Secret", paypal.APIBaseSandBox)
    if err != nil {
        log.Fatal(err)
    }
    Client.SetLog(os.Stdout) // Đặt log để theo dõi các request và response

    // // Get the cart data from the session
    session, _ := store.Get(r, "mysession")
    strcart := session.Values["cart"].(string)
    var cart []models.Item
    json.Unmarshal([]byte(strcart), &cart)

    // Retrieve the new total from the session
    var total float64
    if newTotal, ok := session.Values["newTotal"].(float64); ok {
        total = newTotal
    } else {
        total = Product_total(cart)
    }
    total_usd := ConvertToUSD(total)
    // Tạo context
    ctx := context.Background()

    // Tạo payment request đến PayPal
    payment := paypal.PurchaseUnitRequest{
        ReferenceID: "ref-id",
        Amount: &paypal.PurchaseUnitAmount{
            Currency: "USD",
            Value:    fmt.Sprintf("%.2f", total_usd),
        },
    }

    // Tạo payer
    payer := &paypal.CreateOrderPayer{
        Name: &paypal.CreateOrderPayerName{
            GivenName: "PayPal",
        },
    }

    // Tạo appContext
    appContext := &paypal.ApplicationContext{
        BrandName: "YourBrandName",
    }

    // Tạo đơn hàng PayPal
    resp, err := Client.CreateOrder(ctx, paypal.OrderIntentCapture, []paypal.PurchaseUnitRequest{payment}, payer, appContext)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // Chuyển hướng người dùng đến trang thanh toán của PayPal
    for _, link := range resp.Links {
        if link.Rel == "approve" {
            http.Redirect(w, r, link.Href, http.StatusFound)
            return
        }
    }

    http.Error(w, "Không tìm thấy URL thanh toán từ PayPal", http.StatusInternalServerError)
}

Trường hợp 2: Sử dụng Plutov/paypal để xử lý nạp tiền đổi điểm dùng trong các dịch vụ của nền tảng

Một trường hợp khác mà Nam thấy khá phổ biến đó là người dùng sẽ nạp tiền lấy Credit để sử dụng các dịch vụ trong nền tảng (như viết bài chuẩn SEO chẳng hạn), thì logic đó sẽ xử lý như sau (Áp dụng với Paypal Sandbox thôi nhé, còn Production thì để Nam tìm hiểu thêm)

Tạo kết nối Paypal (ta sẽ tách thành 1 hàm riêng để dễ quản lý)

func NewPaypalClient() (*paypal.Client, error) {
	client, err := paypal.NewClient(os.Getenv("PAYPAL_SECRET"), os.Getenv("PAYPAL_SECRET_2"), paypal.APIBaseSandBox)
	if err != nil {
		return nil, err
	}
	client.SetLog(os.Stdout) // Ghi log để debug nếu cần
	return client, nil
}

Xây dựng 1 route để Submit Form nạp điểm, kết nối trực tiếp với Paypal

 

// Hàm xử lý thanh toán PayPal
func HandlePaypalPayment(w http.ResponseWriter, r *http.Request) {
	client, err := NewPaypalClient()
	if err != nil {
		http.Error(w, "Lỗi kết nối PayPal", http.StatusInternalServerError)
		return
	}

	// Parse form để lấy dữ liệu từ request
	err = r.ParseForm()
	if err != nil {
		http.Error(w, "Lỗi khi xử lý form", http.StatusBadRequest)
		return
	}

	// Lấy giá trị amount từ form
	amount, err := strconv.ParseFloat(r.FormValue("amount"), 64)
	if err != nil || amount <= 0 {
		http.Error(w, "Số tiền không hợp lệ", http.StatusBadRequest)
		return
	}

	// Get User hiện tại
	u := user.GetUser(w, r)

	// Chuyển đổi sang USD
	totalUSD := ConvertToUSD(amount)

	// Lưu hóa đơn vào DB trước khi tạo đơn hàng PayPal
	invoice := models.Invoice{
		UserID: u.ID,
		Amount: amount,
		Status: "pending",
	}

	if err := config.Db.Create(&invoice).Error; err != nil {
		http.Error(w, "Không thể tạo hóa đơn", http.StatusInternalServerError)
		return
	}

	// Tạo đơn hàng PayPal với invoice ID trong ReferenceID
	payment := paypal.PurchaseUnitRequest{
		ReferenceID: fmt.Sprintf("invoice-%d", invoice.ID), // Để có thể lấy lại trong webhook
		Amount: &paypal.PurchaseUnitAmount{
			Currency: "USD",
			Value:    fmt.Sprintf("%.2f", totalUSD),
		},
		CustomID: fmt.Sprintf("%d", invoice.ID), // Đảm bảo webhook có thể lấy invoice ID
	}

	payer := &paypal.PaymentSource{
		Paypal: &paypal.PaymentSourcePaypal{
			ExperienceContext: paypal.PaymentSourcePaypalExperienceContext{
				PaymentMethodPreference: "IMMEDIATE_PAYMENT_REQUIRED",
				ShippingPreference:      "NO_SHIPPING",
				ReturnURL:               "http://localhost/paypal/success",
				CancelURL:               "http://localhost/paypal/cancel",
				UserAction:              "CONTINUE",
				Locale:                  "en-US",
				LandingPage:             "LOGIN",
				BrandName:               "YourBrandName",
			},
		},
	}

	// Tạo đơn hàng
	resp, err := client.CreateOrder(context.Background(), paypal.OrderIntentCapture, []paypal.PurchaseUnitRequest{payment}, payer, nil)
	if err != nil {
		http.Error(w, "Không thể tạo đơn hàng PayPal", http.StatusInternalServerError)
		return
	}

	// Chuyển hướng người dùng đến PayPal để thanh toán
	for _, link := range resp.Links {
		if link.Rel == "payer-action" {
			http.Redirect(w, r, link.Href, http.StatusFound)
			return
		}
	}

	// Nếu không tìm thấy link thanh toán
	http.Error(w, "Không tìm thấy đường dẫn thanh toán", http.StatusInternalServerError)
}

Chỗ này thì bản chất ta sẽ lấy giá trị số tiền từ Form (do người dùng xác định) và lấy user.ID từ cookie hoặc session (tùy bạn), tiếp đó xây dựng 1 invoice thanh toán với trạng thái chờ. Sau đó chỉ thị cho Paypal để người dùng thanh toán số tiền mong muốn (thường mình quy ra $ cho dễ). Sau đó triển khai tạo đơn hàng Paypal, phần Return URL sẽ là nơi để truyền tới hàm mong muốn, khi đẩy lên môi trường production thì nên thay tên miền của ta vào nhé. 

Chuyển hướng tới API thành công, tạo đơn hàng và xử lý dữ liệu

// Xử lý Route Handle Paypal Success
func HandlePaypalSuccess(w http.ResponseWriter, r *http.Request) {
	client, err := NewPaypalClient()
	if err != nil {
		http.Error(w, "Lỗi kết nối PayPal", http.StatusInternalServerError)
		return
	}

	// Lấy orderID từ query params
	orderID := r.URL.Query().Get("token")
	if orderID == "" {
		http.Error(w, "Order ID không hợp lệ", http.StatusBadRequest)
		return
	}

	// Lấy thông tin đơn hàng từ PayPal
	order, err := client.GetOrder(context.Background(), orderID)
	if err != nil {
		http.Error(w, "Không thể lấy thông tin đơn hàng", http.StatusInternalServerError)
		return
	}

	// Kiểm tra trạng thái đơn hàng
	if order.Status != "COMPLETED" {
		http.Error(w, "Thanh toán chưa hoàn tất", http.StatusPaymentRequired)
		return
	}

	// Lấy ReferenceID để truy xuất invoiceID
	if len(order.PurchaseUnits) == 0 {
		http.Error(w, "Dữ liệu đơn hàng không hợp lệ", http.StatusInternalServerError)
		return
	}

	referenceID := order.PurchaseUnits[0].ReferenceID
	if !strings.HasPrefix(referenceID, "invoice-") {
		http.Error(w, "Reference ID không hợp lệ", http.StatusBadRequest)
		return
	}

	// Chuyển invoice ID từ ReferenceID
	invoiceIDStr := referenceID[8:] // Cắt bỏ "invoice-"
	invoiceID, err := strconv.Atoi(invoiceIDStr)
	if err != nil {
		http.Error(w, "Invoice ID không hợp lệ", http.StatusBadRequest)
		return
	}

	// Lấy thông tin invoice từ database
	var invoice models.Invoice
	if err := config.Db.Where("id = ?", invoiceID).First(&invoice).Error; err != nil {
		http.Error(w, "Không tìm thấy hóa đơn", http.StatusNotFound)
		return
	}

	// Chỉ hiển thị thông tin hóa đơn, không cập nhật trạng thái
	config.Tpl.ExecuteTemplate(w, "payment-confirm.html", invoice)
}

Đoạn này mình sẽ tiến hành cập nhật hóa đơn thanh toán thành công, sau đó cho người dùng biết thanh toán đã thành công, tuy vậy đây vẫn chỉ là xác minh 1 chiều, ta sẽ cần 1 Route nữa để verify liệu chúng ta đã nhận được tiền chưa, từ đó mới tiến hành nạp điểm cho khách hàng. 

Đăng ký Webhook để báo Paypal trả về Route mong muốn, từ đó xử lý logic theo thông tin Paypal trả về

 

func HandlePaypalWebhook(w http.ResponseWriter, r *http.Request) {
	// Đọc toàn bộ nội dung body và log lại để kiểm tra payload
	body, err := io.ReadAll(r.Body)
	if err != nil {
		log.Println("❌ Lỗi khi đọc request body:", err)
		http.Error(w, "Lỗi khi đọc request", http.StatusInternalServerError)
		return
	}
	log.Println("🔍 Payload nhận được:", string(body))

	// Giải mã JSON từ body vào struct WebhookEvent
	var notification WebhookEvent
	err = json.Unmarshal(body, ¬ification)
	if err != nil {
		log.Println("❌ Lỗi khi parse JSON:", err)
		http.Error(w, "Invalid webhook request", http.StatusBadRequest)
		return
	}

	log.Println("🔔 Nhận event:", notification.EventType)

	// Kiểm tra loại sự kiện
	if notification.EventType != "CHECKOUT.ORDER.APPROVED" {
		log.Println("⚠️ Bỏ qua event không phù hợp:", notification.EventType)
		w.WriteHeader(http.StatusOK)
		return
	}

	// Kiểm tra resource
	resource, ok := notification.Resource.(map[string]interface{})
	if !ok {
		log.Println("❌ Webhook không có resource hợp lệ")
		http.Error(w, "Webhook không hợp lệ", http.StatusBadRequest)
		return
	}

	// Truy xuất `custom_id` từ `purchase_units[0]`
	purchaseUnits, ok := resource["purchase_units"].([]interface{})
	if !ok || len(purchaseUnits) == 0 {
		log.Println("❌ Không tìm thấy purchase_units")
		http.Error(w, "Không tìm thấy purchase_units", http.StatusBadRequest)
		return
	}

	firstUnit, ok := purchaseUnits[0].(map[string]interface{})
	if !ok {
		log.Println("❌ purchase_units không hợp lệ")
		http.Error(w, "purchase_units không hợp lệ", http.StatusBadRequest)
		return
	}

	customID, ok := firstUnit["custom_id"].(string)
	if !ok || customID == "" {
		log.Println("❌ Không tìm thấy custom_id trong purchase_units")
		http.Error(w, "Không tìm thấy custom_id", http.StatusBadRequest)
		return
	}

	log.Println("✅ Lấy được custom_id:", customID)

	// Chuyển customID thành invoiceID
	invoiceID, err := strconv.Atoi(customID)
	if err != nil {
		log.Println("❌ Invoice ID không hợp lệ:", customID, err)
		http.Error(w, "Invoice ID không hợp lệ", http.StatusBadRequest)
		return
	}

	log.Println("📌 Invoice ID hợp lệ:", invoiceID)

	// Cập nhật hóa đơn trong database
	result := config.Db.Model(&models.Invoice{}).Where("id = ?", invoiceID).Update("status", "paid")
	if result.Error != nil {
		log.Println("❌ Lỗi khi cập nhật hóa đơn:", result.Error)
		http.Error(w, "Lỗi khi cập nhật hóa đơn", http.StatusInternalServerError)
		return
	}

	log.Println("💰 Hóa đơn", invoiceID, "đã được cập nhật thành PAID")

	// Nạp credit cho user
	AddCreditToUser(invoiceID)

	log.Println("✅ Webhook xử lý thành công cho Invoice ID:", invoiceID)

	w.WriteHeader(http.StatusOK)
}

Hết rồi, hy vọng Code này sẽ hữu ích cho bạn, bạn có thể đặt điều kiện nếu phương thức thanh toán (select name="payment-method" là paypal thì sẽ xử lý logic phía trên), chúc bạn thành công

 

 

Bình luận cho Nam qua zalo nhé!