Hướng dẫn tích hợp Checkout SDK Zalo (COD)
Tags: zalo, checkoutsdk
Published: 2024-07-14 09:37:35

Mình viết bài này để ghi lại quá trình tích hợp [Checkout SDK](https://mini.zalo.me/docs/payment/) của Zalo. Không rõ các bạn ở Zalo bị dí deadline nhiều không :D mà tài liệu hướng dẫn của các bạn hơi thiếu thốn, không được cập nhật thường xuyên. Nhiều thông tin phải search ở trên trang community của Zalo chứ doc cũng không có luôn. Hi vọng bài này có thể giúp các bạn né được các pain point mà mình đã trải qua.

> Nhớ là phải đọc hết bài trước khi đặt câu hỏi nhé

Bài viết này mình sẽ hướng dẫn với phương thức thanh toán COD trước.

[Tham khảo tài liệu hướng dẫn của Zalo](https://mini.zalo.me/docs/payment/integration-setting/cod-setting/)

# 1. Tạo đơn hàng bằng CheckoutSDK
Giả sử rằng mini app của bạn đã hoàn thành xong chức năng tạo đơn hàng, giờ chúng ta cần gửi thông tin đơn hàng đó lên CheckoutSDK của Zalo. Để tạo được đơn hàng trên CheckoutSDK chúng ta cần tạo đơn hàng trên server của chúng ta, sau đó để nó ở trạng thái "Chờ thanh toán", rồi dùng ID của đơn hàng đó để định danh với CheckoutSDK.
Cài đặt thư viện ở mini app:
```
"dependencies": {
    "zmp-sdk": "^2.39.1",
}
```
Tạo đơn hàng:
```
import { Payment } from "zmp-sdk";

async function createOrder() {
    const cart = {
        items: [
            {
                product: { id: 1, name: "Xa phong", price: 10000},
                quantity: 2,
            }
        ],
        receiverName: "HuyLV"
        address: "toa nha FPT, so 10 Pham Van Bach",
        finalPrice: 20000,
    }
    const order = await createOrderOnYourServer(cart);

    // build data để tạo order trên CheckoutSDK
    const item = cart.items.map((item) => ({
      id: String(item.product.id),
      amount: item.quantity * item.product.price,
    }));
    const paymentMethod = {
      id: "COD_SANDBOX",
      isCustom: false,
    };
    const extraData = {
      storeName: "Kho tổng",
      storeId: "1",
      orderId: order.id, // id mà chúng ta đã tạo ở server của mình
      notes: "",
    };
    const orderData: any = {
      desc: `Thanh toan ${cart.finalPrice}`,
      item,
      amount: body.finalPrice,
      extradata: JSON.stringify(extraData),
      method: JSON.stringify(paymentMethod),
    };
    // ở bước này cần tạo 1 chuỗi mac từ orderData để đảm bảo tính toàn vẹn của dữ liệu
    // để tạo được mac, chúng ta cần API ở bước 2
    const mac = await createMac(orderData);
    orderData.mac = mac;
    return new Promise((resolve, reject) => {
        Payment.createOrder({
            ...orderData,
            success: resolve,
            fail: reject,
        });
    });
}
```
Chú ý: Ở bước tạo mac của orderData, chúng ta bắt buộc phải gọi lên server để tạo mac chứ ko được tạo mac ở client side. Đoạn này [tài liệu của Zalo](https://mini.zalo.me/docs/payment/createOrder/) có nhắc đến nhưng rất dễ bị miss.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ini1ee3kjt0btpvjifz5.png)


# 2. Xây dựng các API payment phục vụ cho việc thanh toán
### 2.1. API tạo mac
Mac là 1 string lưu thông tin xác thực của dữ liệu đơn hàng, dùng PrivateKey được cung cấp để chứng thực toàn bộ dữ liệu. Các dữ liệu được sắp xếp theo thứ tự từ điển tăng dần để tạo mã hash cho thông tin. Code API tạo mac như sau:
```
const CryptoJS = require('crypto-js');

createMac: async (body) => {
    try {
      const dataMac = Object.keys(body)
        .sort()
        .map(
          (key) =>
            `${key}=${
              typeof body[key] === 'object'
                ? JSON.stringify(body[key])
                : body[key]
            }`
        )
        .join('&');
      // biến môi trường ZALO_CHECKOUT_SECRET_KEY lấy ở bước 3
      const mac = CryptoJS.HmacSHA256(
        dataMac,
        process.env.ZALO_CHECKOUT_SECRET_KEY
      ).toString();
      return mac;
    } catch (e) {
      console.log(e);
    }
  }
```
### 2.2. API nhận dữ liệu PTTT
Chúng ta cần build 1 API mở để server của Zalo gọi vào, và chúng ta sẽ phản hồi dữ liệu đó có toàn vẹn hay không. Đối với 2 phương thức: `Thanh toán khi nhận hàng (COD)` và `Chuyển khoản ngân hàng`, chúng ta cần hiện thực API này khi người dùng chọn một trong hai phương thức này.

> Chú ý API này không require authentication nhé
```
const CryptoJS = require('crypto-js');

zaloNotify: async (body) => {
    try {
      const { data, mac } = body || {};
      if (!data || !mac) {
        return {
          returnCode: 0,
          returnMessage: 'Missing data or mac',
        };
      }
      const { method, orderId, appId } = data || {};
      if (!method || !orderId || !appId) {
        return {
          returnCode: 0,
          returnMessage: 'Missing method or orderId or appId',
        };
      }
      const str = `appId=${appId}&orderId=${orderId}&method=${method}`;
      const reqMac = CryptoJS.HmacSHA256(
        str,
        process.env.ZALO_CHECKOUT_SECRET_KEY
      ).toString();
      if (reqMac == mac) {
        return {
          returnCode: 1,
          returnMessage: 'Success',
        };
      } else {
        return {
          returnCode: 0,
          returnMessage: 'Fail',
        };
      }
    } catch (e) {
      console.log(e);
      return {
        returnCode: 0,
        returnMessage: 'Fail',
      };
    }
  }
```
OK, deploy 2 API này lên test server nhé.

> Server của bạn cần whitelist cho danh sách địa chỉ IP của CheckoutSDK Server:
> 118.102.2.29
> 49.213.78.2

# 3. Cài đặt trên trang quản lý mini app
- Truy cập trang quản lý mini app: 
https://mini.zalo.me/developers -> Chọn ZaloApp -> chọn mini app -> ở menu bên trái chọn CheckoutSDK -> cấu hình chung
- Chỉnh `AppStatus` thành `ACTIVE`, sử dụng `Private Key` cho biến môi trường `ZALO_CHECKOUT_SECRET_KEY`
- Thêm phương thức thanh toán mới, chọn `Thanh toán khi nhận hàng - Sandbox`
 - Notify Url: điền API ở bước 2.2

 - Redirect path: đường dẫn mà mini app sẽ redirect đến khi thanh toán xong
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sblsv4hczgnjz6vwre2d.png)

# 4. Thêm màn hình kết quả thanh toán
Sau thanh toán xong thì chắc chắn là phải mở ra màn kết quả thanh toán rồi. Ở đây sẽ xảy ra 2 trường hợp phụ thuộc vào phiên bản Zalo mà user sử dụng.

1. Với phiên bản Zalo đã hỗ trợ event open app, bạn cần lắng nghe sự kiện OpenApp và kiểm tra kết quả từ hệ thống thanh toán để xử lý.
> iOS: từ 22.02.01
> Android: từ 22.03.02

2. Với những phiên bản Zalo chưa hỗ trợ event OpenApp, sau khi thanh toán, hệ thống sẽ điều hướng người dùng tới redirect path đã cấu hình trên web.

Để lắng nghe sự kiện open app, bạn cần khai báo hook `useHandlePayment` và gọi nó khi khởi tạo app.
```
import { events, EventName } from "zmp-sdk";

export const useHandlePayment = () => {
  const navigate = useNavigate();

  useEffect(() => {
    events.on(EventName.OpenApp, (data) => {
      // data.path = /checkout-result?env=DEVELOPMENT&version=zdev-655f4b9a&appTransID=240601_1923887902773438459351224118883
      // checkout-result là path mà chúng ta khai báo ở bước 3
      if (data?.path) {
        navigate(data?.path, {
          state: data,
        });
      }
    });
  });
}
```
Do thanh toán COD thì kết quả luôn là thành công rồi nên màn kết quả thanh toán cũng đơn giản thôi:
```
// CheckoutResult.tsx
export default function CheckoutResult() {
  const navigate = useNavigate();
  
  return (
    <div>
      <h1>Thanh toán thành công</h1>
    </div>
  );
}
```
OK, everything well done, hãy build lên **máy thật** và chạy thử thôi nào. Phần thanh toán này chỉ có thể test trên máy thật thôi nhé.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/koibniji4o3buqhvsp4x.png)

# 5. Go live
Chú ý: lên môi trường production cần phải tạo thêm phương thức thanh toán `Thanh toán khi nhận hàng` (ko Sandbox).

Like ủng hộ mình để mình làm tiếp bài hướng dẫn tích hợp VNPAY nha :D

Nếu có bất kỳ thắc mắc nào, có thể comment bên dưới hoặc contact mình qua [đây](https://www.facebook.com/huylv.177), mình sẽ support bạn trong khả năng của mình ;)