Chuyển tới nội dung chính

Mua trong ứng dụng và gói thuê bao

Khi khách nhắn hỗ trợ từ app của bạn, nhân viên và trợ lý AI cần biết khách đang trả tiền cho gì: gói nào, có tự gia hạn không, hết hạn khi nào, đã hoàn tiền chưa. Hạ Tầng ghi gói thuê bao và giao dịch mua một lần (App Store, Google Play và website của bạn) vào hồ sơ khách và giữ chúng luôn đúng.

App vẫn bán hàng bằng StoreKit hoặc Google Play Billing (hoặc in_app_purchase / react-native-iap). Hạ Tầng chỉ cần biết từng lần mua, gia hạn, hết hạn và hoàn tiền.

Chọn cách đưa giao dịch vào Hạ Tầng​

A. Từ app, có kết nối cửa hàngB. Từ backend của bạn
Ai kiểm tra giao dịchHạ Tầng, với Apple và GoogleServer của bạn, như hiện nay
Bạn thêm gìpurchaseToken và reportPurchase trong app; thông báo của cửa hàng gửi về Hạ Tầngchat.subscriptions.upsert và chat.purchases.upsert từ server sau mỗi thay đổi
Gia hạn, hết hạn, hoàn tiềnTừ thông báo của cửa hàng về Hạ TầngServer của bạn gửi khi biết
Bán trên webGửi từ backend (như cách B)Có sẵn
Hợp vớiApp mà server chưa tự kiểm tra giao dịch trong cửa hàngApp mà server đã kiểm tra giao dịch, đã giữ URL thông báo của cửa hàng, hoặc bán cả trên web

Apple chỉ cho một URL App Store Server Notifications cho mỗi môi trường, nên nếu server của bạn đã nhận thông báo này thì chọn B. Có thể chạy cả hai: cùng một giao dịch từ hai phía vẫn chỉ là một bản ghi.

Cách A hoạt động thế nào​

App ──mua(appAccountToken = purchaseToken(user))──▶ App Store / Google Play
│ │
└─ reportPurchase(bằng chứng) ──▶ Hạ Tầng ◀── gia hạn, huỷ, hoàn tiền (thông báo của cửa hàng)
│
└─ xác minh với cửa hàng, ghi vào hồ sơ khách
  1. Lúc mua, app truyền purchaseToken(userId) cho cửa hàng. Cửa hàng gửi lại mã này trong mọi thông báo về giao dịch đó, nên việc gia hạn, huỷ hay hoàn tiền nhiều tháng sau vẫn về đúng khách.
  2. Sau khi mua, app gọi reportPurchase kèm bằng chứng của cửa hàng. Hạ Tầng kiểm tra với Apple hoặc Google và gói hiện ngay trên hồ sơ khách, không phải chờ thông báo của cửa hàng.
  3. Về sau, thông báo của cửa hàng giữ gói luôn đúng. Có khoá App Store Server API, Hạ Tầng còn kiểm tra lại các gói sắp gia hạn mỗi ngày.

Chuẩn bị​

Cho cả hai cách:

  • Chat trong app và website, đăng nhập khách bằng identify với user id mà backend của bạn dùng cho họ (xem Ứng dụng di động). Gói và giao dịch về đúng khách qua id này.
  • Sản phẩm trên dashboard với loại (gói thuê bao, dùng hết, mua một lần), chu kỳ thanh toán, số ngày dùng thử, mã sản phẩm và giá trên App Store / Google Play, và SKU cho những gì bán trên web. Thẻ sản phẩm trong chat khi đó hiện giá của cửa hàng, trợ lý biết các gói của bạn, và bản ghi tìm được sản phẩm. Xem Bán phần mềm và gói thuê bao.

Cho cách A:

  • SDK chat có các lệnh mua hàng: @hatang/chat-react-native 0.4.0+, hatang_chat 0.6.0+, dev.hatang:chat 0.4.0+ hoặc HatangChat 0.4.0+.
  • Cửa hàng đã kết nối với app trên dashboard (Kênh → app của bạn → Thuê bao từ App Store và Google Play):
    • App Store: dán URL thông báo vào App Store Connect cho cả Production và Sandbox; không bắt buộc nhưng nên có khoá App Store Server API (Issuer ID, Key ID, file .p8) để Hạ Tầng làm mới gói, kiểm tra mỗi ngày và nhập khách đã mua từ trước.
    • Google Play: JSON của service account có quyền xem dữ liệu tài chính trong Play Console, và một Pub/Sub push subscription tới URL mà dashboard hiện, kèm bật thông báo thời gian thực (RTDN). Thiếu service account thì Hạ Tầng không xác minh và không acknowledge được giao dịch Android.

Bài trợ giúp ở trên hướng dẫn từng bước cho cả hai cửa hàng.

Cho cách B:

  • Một API key có scope crm:write, giữ trên server.
  • SDK cho server: @hatang/sdk 0.6.0+ (Node.js) hoặc hatang 0.1.0+ (Python), hoặc REST API.

A1. Truyền mã mua hàng khi mua​

purchaseToken(userId) được tạo từ site của bạn và user id bạn truyền vào identify, nên giống nhau trên mọi thiết bị và cả ở backend (hatang.chat.subscriptions.purchaseToken(siteId, userId) trong @hatang/sdk). Dùng đúng user id bạn dùng để đăng nhập khách.

let token = HatangChat.purchaseToken(userId: currentUser.id) // nil nếu chưa configure
let result = try await product.purchase(options: token.map { [.appAccountToken($0)] } ?? [])

A2. Báo từng giao dịch​

Gọi reportPurchase mỗi khi app thấy một giao dịch hoàn tất, không chỉ ngay sau nút mua:

  • sau khi mua thành công,
  • với giao dịch đến muộn (gia hạn, giao dịch phụ huynh duyệt, thanh toán chờ xử lý vừa xong): Transaction.updates trên iOS, PurchasesUpdatedListener trên Android, purchase stream của in_app_purchase / react-native-iap,
  • khi khách khôi phục giao dịch (restore).

Truyền bằng chứng của cửa hàng: signed transaction (JWS) của StoreKit 2 trên iOS, purchase token và loại sản phẩm (subs hoặc inapp) trên Android. Hạ Tầng xác minh với cửa hàng, ghi gói thuê bao hoặc giao dịch mua một lần cho khách, acknowledge giao dịch Google Play, và để lại một ghi chú nội bộ trong hội thoại đang mở của khách. Báo lại cùng một giao dịch chỉ cập nhật bản ghi cũ, không tạo bản ghi thứ hai.

func handle(_ result: VerificationResult<Transaction>) async {
guard case .verified(let tx) = result else { return }
await HatangChat.reportPurchase(storeProductId: tx.productID,
transactionId: String(tx.originalID),
signedTransaction: result.jwsRepresentation)
await tx.finish()
}

// Sau product.purchase(…): if case .success(let result) = purchaseResult { await handle(result) }
// Khi mở app, cho gia hạn và giao dịch đến muộn:
Task { for await result in Transaction.updates { await handle(result) } }
// Khôi phục:
for await result in Transaction.currentEntitlements { await handle(result) }

reportPurchase trả về true khi giao dịch đã được ghi, false khi cửa hàng từ chối bằng chứng, app chưa kết nối cửa hàng đó, hoặc gọi lỗi. Dù kết quả thế nào, app vẫn finish hoặc acknowledge giao dịch như bình thường: Hạ Tầng là bản ghi cho hỗ trợ khách hàng, không phải hệ thống mở quyền của app, và giao dịch bị sót vẫn đến qua thông báo của cửa hàng.

Giao dịch báo lên trước khi khách đăng nhập được giữ trên khách ẩn danh, và chuyển sang tài khoản khi bạn gọi identify.

B. Gửi từ backend​

Server của bạn báo cho Hạ Tầng từng thay đổi mà nó đã biết, dùng đúng user id bạn truyền vào identify:

  1. Sau mỗi lần mua, gia hạn, hết hạn hoặc huỷ một gói, gọi chat.subscriptions.upsert(platform, id, …) với trạng thái hiện tại: status (active, trial, canceled, expired, refunded…), current_period_end, và sản phẩm (store_product_id, hoặc sku trên web).
  2. Sau mỗi giao dịch mua một lần (gói xu, mở khoá, khoá học), gọi chat.purchases.upsert(platform, id, …) với kind, amount, currency và purchased_at; khi hoàn tiền, gửi lại cùng mã với status: "refunded".
  3. Một lần, gửi lịch sử: mọi gói và giao dịch bạn đang có, để khách cũ cũng hiện.

Gửi lại cùng một bản ghi chỉ cập nhật nó (gói không đổi thì không ghi thêm vào lịch sử), nên server có thể gửi lại toàn bộ thông tin của một người sau mỗi thay đổi.

import { Hatang } from "@hatang/sdk"; // 0.6.0+

const hatang = new Hatang(process.env.HATANG_API_KEY);

// Gói bán trên website
await hatang.chat.subscriptions.upsert("web", subscription.id, {
external_user_id: user.id, sku: "PRO-1M", status: "active", auto_renew: false,
started_at: "2026-10-01T00:00:00Z", current_period_end: "2026-11-01T00:00:00Z", amount: 99000, currency: "VND",
});
// Gói App Store mà server đã xác minh
await hatang.chat.subscriptions.upsert("ios", originalTransactionId, {
external_user_id: user.id, store_product_id: "com.example.pro.monthly", status: "active", current_period_end: expiresAt,
});
// Giao dịch mua một lần; khi hoàn tiền, gửi lại cùng mã với status "refunded"
await hatang.chat.purchases.upsert("web", order.id, {
external_user_id: user.id, store_product_id: "credits.300", sku: "XU-300", kind: "consumable",
amount: 29000, currency: "VND", purchased_at: order.paidAt,
});

Gửi mã nào​

LoạiPlatformMã
Gói bán trên websitewebMã gói của riêng bạn
Gói App StoreiosoriginalTransactionId của Apple (không đổi qua các lần gia hạn)
Gói Google PlayandroidPurchase token
Mua một lần trên websitewebMã đơn của bạn (mỗi dòng của giỏ hàng một bản ghi, ví dụ order_981:0)
Mua một lần trên App StoreiostransactionId của Apple
Mua một lần trên Google PlayandroidOrder id của Play (GPA.…), hoặc purchase token

Sản phẩm được tìm theo product_id, theo sku trong danh mục của bạn, hoặc (App Store, Google Play) theo mã sản phẩm của cửa hàng. Trên web, store_product_id là mã riêng của bạn cho thứ đã bán; nó hiện ra khi không khớp sản phẩm nào trong danh mục.

Nếu app của bạn dùng cả cách A, giao dịch trong cửa hàng sẽ gặp đúng các bản ghi này: Hạ Tầng dùng cùng các mã đó làm khoá.

Đọc và nhập​

  • Đọc bản ghi của một khách: chat.subscriptions.list({ external_user_id }), chat.purchases.list({ external_user_id }), hoặc forCustomer(customerId).
  • Chỉ cho cách A: nhập khách đã mua trong cửa hàng một lần, sau khi kết nối: POST /chat/subscriptions/import với app id và tối đa 500 mã (transaction id của Apple, cần khoá App Store Server API; purchase token gói thuê bao của Google Play). Nhân viên cũng làm được việc này trên dashboard.

Xem Support REST API để biết đủ các trường.

Nhân viên và trợ lý thấy gì​

  • Khung thông tin khách trong hộp thư liệt kê các gói (App Store, Google Play hoặc Web; trạng thái như dùng thử, đang hoạt động, gia hạn trễ, đã huỷ hay đã hoàn tiền; ngày gia hạn hoặc hết hạn) và các giao dịch mua một lần. Khi đã kết nối cửa hàng (cách A), nút Làm mới hỏi lại cửa hàng (trên iOS cần khoá App Store Server API).
  • Trợ lý AI nhận một dòng về gói hiện tại của khách, nên trả lời được "khi nào gói của tôi gia hạn?" hay "tôi bị trừ tiền hai lần" bằng dữ liệu thật.
  • Giao dịch sandbox và thử nghiệm được đánh dấu thử nghiệm, không lẫn với giao dịch thật.

Thử trong sandbox​

  • iOS: mua bằng Sandbox Apple Account (file StoreKit configuration trong Xcode chỉ để thử giao diện: giao dịch StoreKit cục bộ không được Apple ký, nên Hạ Tầng không xác minh được). Trang thông báo trong App Store Connect có nút Request a Test Notification để thử URL.
  • Android: thêm license tester trong Play Console và cài app từ track internal testing. Gói thử nghiệm gia hạn vài phút một lần, tiện để xem việc gia hạn đến.
  • Mở hội thoại của khách trong hộp thư: gói phải hiện trên hồ sơ trong vài giây sau reportPurchase.
  • Cách B: gửi một gói và một giao dịch cho một người dùng thử đã mở chat, rồi xem hồ sơ của họ. Với giao dịch sandbox của cửa hàng, gửi environment: "sandbox" để được đánh dấu đúng.

Xử lý lỗi​

Bạn thấyNguyên nhân thường gặp
Hồ sơ không có gì sau khi muaKhông truyền bằng chứng (signedTransaction / purchaseToken), app chưa kết nối cửa hàng đó, hoặc chưa gọi reportPurchase cho giao dịch đó. Khi đó chỉ có ghi chú nội bộ
Gia hạn hay hoàn tiền không về, hoặc không gắn với aiGiao dịch mua không có purchaseToken(userId), hoặc dùng user id khác với id truyền vào identify. Nhập gói từ dashboard để gắn lại
Bundle id … is not this app'sBundle id trong App Store Connect khác với bundle id nhập cho app trên dashboard
Connect Google Play for this app firstApp chưa có package name và service account
Google tự hoàn tiền giao dịch Android sau ba ngàyKhông ai acknowledge: kết nối service account, hoặc acknowledge trong app
Giao dịch iOS không bao giờ được xác minh khi thửĐang thử bằng StoreKit cục bộ trong Xcode; dùng Sandbox Apple Account
Cách B: có bản ghi nhưng không gắn với kháchexternal_user_id khác với id truyền vào identify, hoặc người đó chưa từng mở chat (bản ghi hiện khi họ mở)
Cách B: khung khách hiện mã thay vì tên sản phẩmKhông có sản phẩm nào mang SKU hoặc mã cửa hàng đó: điền mã cho sản phẩm trong danh mục

Xem trang này dạng Markdown