Skip to main content

Hướng Dẫn Google Play Android Developer API

MỤC LỤC


PHẦN 1: CẤU HÌNH VÀ THÊM QUYỀN TÀI KHOẢN

Bước 1: ENABLE GOOGLE PLAY ANDROID DEVELOPER API

1.Truy cập Google Cloud Console

🔗 https://console.cloud.google.com
  • Đăng nhập bằng tài khoản Google
  • Chọn Project Firebase của bạn từ dropdown góc trên bên trái

2. Mở API Library

🔗 https://console.cloud.google.com/apis/library

Hoặc: Menu bên trái → APIs & ServicesLibrary

3. Tìm và Enable API

  • Gõ vào ô tìm kiếm: Google Play Android Developer API
  • Click vào kết quả đầu tiên
  • Click nút ENABLE (màu xanh)
  • Đợi 5-10 giây → Thấy "API enabled" ✅

Enable Google Play API


BƯỚC 2: TẠO VÀ LẤY THÔNG TIN SERVICE ACCOUNT

1. Truy cập Service Accounts

🔗 https://console.cloud.google.com/iam-admin/serviceaccounts

Hoặc: Menu → IAM & AdminService Accounts

2. Tìm hoặc Tạo Service Account

Option A: Sử dụng Service Account có sẵn

  • Tìm Service Account có tên chứa firebase hoặc firebase-adminsdk
  • Email dạng: firebase-adminsdk-xxxxx@your-project.iam.gserviceaccount.com
  • Copy email này (cần dùng ở bước tiếp theo)

Option B: Tạo Service Account mới (nếu chưa có)

  • Click + CREATE SERVICE ACCOUNT
  • Service account name: play-api-checker
  • Service account ID: Tự động tạo (có thể tùy chỉnh)
  • Click CREATE AND CONTINUE
  • Role: Bỏ qua (không cần chọn) → Click CONTINUE
  • Click DONE
  • Copy email Service Account vừa tạo

Service Account

3. Tạo Private Key (JSON)

Quan trọng: File này dùng để xác thực API từ code

  • Tại danh sách Service Accounts, click vào email Service Account
  • Chọn tab KEYS (ở menu trên)
  • Click ADD KEYCreate new key
  • Chọn JSON → Click CREATE
  • File JSON tự động tải về máy
  • Đổi tên file thành service-account.json

⚠️ Bảo mật:

  • Không đẩy file này lên Git
  • Không chia sẻ với người khác
  • Lưu ở nơi an toàn

Cấu trúc file JSON:

{
"type": "service_account",
"project_id": "your-project-id",
"private_key_id": "abc123...",
"private_key": "-----BEGIN PRIVATE KEY-----\n...",
"client_email": "play-api-checker@your-project.iam.gserviceaccount.com",
"client_id": "123456789",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token"
}

BƯỚC 3: CẤP QUYỀN CHO SERVICE ACCOUNT TRÊN GOOGLE PLAY CONSOLE

1. Truy cập Google Play Console

🔗 https://play.google.com/console
  • Đăng nhập bằng tài khoản có quyền quản lý app
  • Chọn ứng dụng của bạn

2. Vào Users and Permissions

🔗 https://play.google.com/console/users-and-permissions

Hoặc: Menu bên trái → Users and permissions

Mời Service Account

  • Click Invite new users (góc trên phải)
  • Email address: Paste email Service Account (từ Bước 5)
  • Expiry date: Chọn None (không hết hạn)

Invite User

3. Cấp quyền cho App cụ thể

  • Phần App permissions: Click Add app
  • Chọn ứng dụng của bạn từ danh sách
  • Click Apply

App Permissions

4. Cấp quyền Account (QUAN TRỌNG ⚠️)

Scroll xuống phần Account permissions và mở rộng các mục sau:

A. Financial data (Click để mở rộng):

  • ✅ Tích: View financial data, orders, and cancellation survey responses

B. Order management (Click để mở rộng):

  • ✅ Tích: Manage orders and subscriptions

Account Permissions

📝 Lưu ý:

  • Quyền View financial data cho phép đọc thông tin giao dịch
  • Quyền Manage orders cho phép đọc danh sách giao dịch bị hủy (voided purchases)

Hoàn tất

  • Click Invite user (nút màu xanh)
  • Thấy thông báo "User invited successfully" ✅

Invite User Success


BƯỚC 4: XÁC MINH CẤU HÌNH

1. Kiểm tra quyền đã cấp

🔗 https://play.google.com/console/users-and-permissions
  • Tìm email Service Account trong danh sách users
  • Click vào email để xem chi tiết
  • Kiểm tra:
    • App access: Có tên ứng dụng
    • Financial data: View financial data, orders...
    • Order management: Manage orders and subscriptions

Đợi đồng bộ quyền

⏱️ Thời gian đồng bộ: 30 phút đến 24 giờ

Google cần thời gian để đồng bộ quyền giữa Play Console và API backend. Trong thời gian này:

  • API có thể trả về lỗi 403 Forbidden
  • Đây là hiện tượng bình thường
  • Hãy đợi và thử lại sau

✅ HOÀN TẤT PHẦN 1

Checklist cấu hình:

  • ✅ Google Play Android Developer API: Đã Enable
  • ✅ Service Account: Đã tạo/chọn
  • ✅ Service Account JSON Key: Đã tải về
  • ✅ Email Service Account: Đã copy
  • ✅ Quyền trên Play Console: Đã cấp (Financial data + Order management)
  • ✅ App: Đã thêm vào App permissions

Thông tin cần lưu lại:

Service Account Email: [email@project.iam.gserviceaccount.com]
Package Name: [vn.vgp.kiem.khach.phong.lang]
File JSON: [service-account.json]

➡️ Tiếp theo: Chuyển sang PHẦN 2 để test API


PHẦN 2: TEST TRỰC TIẾP TRÊN GOOGLE API EXPLORER

2.1. GIỚI THIỆU API EXPLORER

Google API Explorer là công cụ web cho phép test API trực tiếp mà không cần viết code.

Ưu điểm:

  • ✅ Không cần cài đặt gì
  • ✅ Test nhanh các parameters
  • ✅ Xem request/response trực quan
  • ✅ Không cần Service Account JSON (dùng OAuth)

Link API Explorer:

🔗 https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.voidedpurchases/list

2.2. CÁC BƯỚC TEST API

Bước 1: Truy cập API Explorer

Mở link sau trong trình duyệt:

https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.voidedpurchases/list

Hoặc tìm kiếm Google: android publisher api voidedpurchases list

Bước 2: Kéo xuống phần "Try this API"

Tìm panel bên phải có tiêu đề "Try this API" hoặc "Try this method"

API Endpoint


2.3. CẤU HÌNH PARAMETERS

A. REQUIRED PARAMETERS (Bắt buộc)

ParameterGiá trịMô tả
packageNamevn.vgp.kiem.khach.phong.langPackage name của app trên Google Play

Cách lấy packageName:

  • Mở Google Play Console → Chọn app
  • Xem ở URL: https://play.google.com/console/app/[PACKAGE_NAME]/...
  • Hoặc: Dashboard → App details → Package name

B. OPTIONAL PARAMETERS (Tùy chọn)

1. startTime (Thời gian bắt đầu)

  • Format: Milliseconds (số nguyên 13 chữ số)
  • Mô tả: Chỉ lấy giao dịch hủy SAU thời điểm này
  • Giới hạn: Không được quá 60 ngày so với hiện tại

Ví dụ:

1701187200000  →  29/11/2023 00:00:00 GMT
1704067200000 → 01/01/2024 00:00:00 GMT

Cách tính:

2. endTime (Thời gian kết thúc)

  • Format: Milliseconds (số nguyên 13 chữ số)
  • Mô tả: Chỉ lấy giao dịch hủy TRƯỚC thời điểm này
  • Mặc định: Nếu không điền = thời gian hiện tại

3. maxResults (Số lượng kết quả)

  • Format: Số nguyên
  • Giá trị: 1 - 1000
  • Mặc định: 100
  • Mô tả: Số lượng giao dịch tối đa trả về trong 1 request

4. includeQuantityBasedPartialRefund (Bao gồm refund một phần)

  • Format: Boolean
  • Giá trị: true hoặc false
  • Mặc định: false
  • Mô tả: Có bao gồm giao dịch hoàn tiền một phần theo số lượng không

Khuyến nghị: Luôn dùng true để lấy đầy đủ dữ liệu

5. type (Loại sản phẩm)

  • Format: Integer
  • Giá trị:
    • 0 = In-app products (mua trong app 1 lần)
    • 1 = Subscriptions (đăng ký định kỳ)
  • Mặc định: Lấy cả 2 loại

6. startIndex (Vị trí bắt đầu - Phân trang)

  • Format: Integer
  • Giá trị: >= 0
  • Mặc định: 0
  • Mô tả: Bỏ qua N kết quả đầu tiên (dùng cho phân trang)

7. token (Token phân trang)

  • Format: String
  • Mô tả: Dùng để lấy trang tiếp theo (từ response trước)
  • Cách dùng: Copy nextPageToken từ response trước, paste vào đây

2.4. CONFIGURATION MẪU

packageName: vn.vgp.kiem.khach.phong.lang
startTime: 1701187200000 (30 ngày trước)
endTime: [để trống hoặc thời gian hiện tại]
maxResults: 100
includeQuantityBasedPartialRefund: true

Cấu hình lấy tất cả:

packageName: vn.vgp.kiem.khach.phong.lang
maxResults: 1000
includeQuantityBasedPartialRefund: true

Cấu hình chỉ subscriptions:

packageName: vn.vgp.kiem.khach.phong.lang
type: 1
includeQuantityBasedPartialRefund: true

2.5. XÁC THỰC VÀ EXECUTE

Bước 3: Xác thực OAuth 2.0

  1. Kéo xuống phần "Google OAuth 2.0"
  2. Click nút "Authorize" hoặc "Sign in with Google"
  3. Chọn tài khoản Google có quyền truy cập Play Console
  4. Cho phép các quyền:
    • View and manage your Google Play Developer account
  5. Click "Allow"

⚠️ Lưu ý: Phải dùng tài khoản Google đã được thêm vào Play Console

Bước 4: Điền Parameters

Điền các giá trị vào form:

packageName: vn.vgp.kiem.khach.phong.lang
includeQuantityBasedPartialRefund: true

Bước 5: Execute Request

  1. Kiểm tra lại tất cả parameters
  2. Click nút "EXECUTE" (màu xanh)
  3. Đợi 2-5 giây
  4. Xem kết quả ở phần "Response" bên dưới

2.6. ĐỌC KẾT QUẢ RESPONSE

Response thành công (200 OK):

{
"voidedPurchases": [
{
"kind": "androidpublisher#voidedPurchase",
"purchaseToken": "abcdefghijklmnopqrstuvwxyz...",
"purchaseTimeMillis": "1702345678000",
"voidedTimeMillis": "1702445678000",
"orderId": "GPA.1234-5678-9012-34567",
"voidedReason": 1,
"voidedSource": 0,
"voidedQuantity": 1
}
],
"tokenPagination": {
"nextPageToken": "CAoQABgB"
}
}

Giải thích các trường:

TrườngGiải thích
kindLoại object (luôn là androidpublisher#voidedPurchase)
purchaseTokenToken định danh giao dịch (dùng để xác thực)
purchaseTimeMillisThời gian mua (milliseconds)
voidedTimeMillisThời gian hủy (milliseconds)
orderIdOrder ID từ Google Play
voidedReasonLý do hủy (xem bảng bên dưới)
voidedSourceNguồn hủy (0=User, 1=Developer, 2=Google)
voidedQuantitySố lượng bị hủy (với partial refund)

Bảng mã voidedReason:

CodeÝ nghĩaGiải thích
0OTHERLý do khác
1REMORSEĐổi ý, hối hận mua hàng
2NOT_RECEIVEDKhông nhận được sản phẩm
3DEFECTIVESản phẩm lỗi
4ACCIDENTAL_PURCHASEMua nhầm
5FRAUDGian lận
6FRIENDLY_FRAUDGian lận từ người quen
7CHARGEBACKHoàn tiền qua ngân hàng
8UNACKNOWLEDGED_PURCHASEKhông xác nhận mua hàng

Bảng mã voidedSource:

CodeÝ nghĩa
0USER (Người dùng tự hủy)
1DEVELOPER (Developer hủy qua API)
2GOOGLE (Google tự động hủy)

Response rỗng (Không có giao dịch hủy):

{
"voidedPurchases": []
}

2.7. XỬ LÝ LỖI THƯỜNG GẶP

❌ Lỗi 403 Forbidden

Nguyên nhân: Quyền chưa đồng bộ
Giải pháp: Đợi thêm 2-24 giờ

❌ Lỗi 401 Unauthorized

Nguyên nhân: Access Token không hợp lệ
Giải pháp: Kiểm tra Service Account đúng chưa

❌ Lỗi 400 Bad Request

Nguyên nhân: Package name sai
Giải pháp: Copy chính xác từ Google Play Console


✅ HOÀN TẤT PHẦN 2

Checklist test API Explorer:

  • ✅ Đã truy cập API Explorer
  • ✅ Đã xác thực OAuth 2.0
  • ✅ Đã điền packageName
  • ✅ Đã điền includeQuantityBasedPartialRefund = true
  • ✅ Đã Execute và nhận response thành công
  • ✅ Đã hiểu cách đọc response

➡️ Tiếp theo: Chuyển sang PHẦN 3 để test bằng Node.js


PHẦN 3: TEST BẰNG NODE.JS

3.1. CHUẨN BỊ MÔI TRƯỜNG

Bước 1: Kiểm tra Node.js

Mở Terminal/CMD và chạy:

node --version

Yêu cầu: Node.js >= 14.0.0

Chưa có Node.js? Tải tại: https://nodejs.org/

Bước 2: Tạo thư mục project

mkdir google-play-api-test
cd google-play-api-test

Bước 3: Khởi tạo project

npm init -y

Bước 4: Cài đặt dependencies

npm install googleapis

3.2. CHUẨN BỊ FILE SERVICE ACCOUNT

Bước 5: Copy file JSON

  1. Copy file service-account.json (từ PHẦN 1, Bước 6)
  2. Paste vào thư mục google-play-api-test
  3. Đảm bảo tên file chính xác: service-account.json

Cấu trúc thư mục:

google-play-api-test/
├── service-account.json ← File này
├── package.json
└── test-voided-purchases.js (tạo ở bước sau)

3.3. TẠO FILE TEST

Bước 6: Tạo file test-voided-purchases.js

Tạo file test-voided-purchases.js với nội dung đầy đủ:

#!/usr/bin/env node
const {google} = require('googleapis');
const path = require('path');

// ============================================
// CẤU HÌNH - THAY ĐỔI THEO PROJECT CỦA BẠN
// ============================================
const serviceKeyPath = path.join(__dirname, 'service-account.json');
const packageName = process.argv[2] || 'vn.vgp.kiem.khach.phong.lang';

async function main() {
console.log('╔══════════════════════════════════════════════════════════════════════╗');
console.log('║ GOOGLE PLAY ANDROID DEVELOPER API - VOIDED PURCHASES TEST ║');
console.log('╚══════════════════════════════════════════════════════════════════════╝');
console.log('');
console.log('🚀 Bắt đầu test Google Play Android Developer API...\n');

try {
// ============================================
// BƯỚC 1: XÁC THỰC
// ============================================
console.log('📝 Bước 1: Đang xác thực với Google...');
const auth = new google.auth.GoogleAuth({
keyFile: serviceKeyPath,
scopes: ['https://www.googleapis.com/auth/androidpublisher'],
});

const authClient = await auth.getClient();
console.log('✅ Xác thực thành công!\n');

// ============================================
// BƯỚC 2: KHỞI TẠO API CLIENT
// ============================================
console.log('📝 Bước 2: Khởi tạo Android Publisher API client...');
const androidpublisher = google.androidpublisher({
version: 'v3',
auth: authClient
});
console.log('✅ API client đã sẵn sàng!\n');

// ============================================
// BƯỚC 3: TÍNH TOÁN THỜI GIAN
// ============================================
const nowMs = Date.now();
const startMs = nowMs - 29 * 24 * 60 * 60 * 1000; // 29 ngày (không quá 30)

console.log('📝 Bước 3: Chuẩn bị parameters...');
console.log(`📦 Package Name: ${packageName}`);
console.log(`⏰ Start Time: ${new Date(startMs).toLocaleString('vi-VN')}`);
console.log(`⏰ End Time: ${new Date(nowMs).toLocaleString('vi-VN')}`);
console.log(`📊 Max Results: 100\n`);

// ============================================
// BƯỚC 4: CẤU HÌNH PARAMETERS
// ============================================
const baseParams = {
packageName,
type: 0, // 0 = in-app products, 1 = subscriptions
includeQuantityBasedPartialRefund: true,
startTime: String(startMs),
endTime: String(nowMs),
maxResults: 100,
};

// ============================================
// BƯỚC 5: GỌI API VỚI PHÂN TRANG
// ============================================
console.log('📝 Bước 4: Đang gọi API...\n');

const results = [];
let token = null;
let pageCount = 0;

do {
pageCount++;
console.log(` 📄 Đang lấy trang ${pageCount}...`);

const params = Object.assign({}, baseParams);
if (token) params.token = token;

const res = await androidpublisher.purchases.voidedpurchases.list(params);
const items = (res.data && res.data.voidedPurchases) || [];

console.log(` ✅ Tìm thấy ${items.length} giao dịch\n`);

// Xử lý từng giao dịch
for (const vp of items) {
results.push({
kind: vp.kind,
orderId: vp.orderId,
purchaseTimeMillis: vp.purchaseTimeMillis ? Number(vp.purchaseTimeMillis) : null,
purchaseToken: vp.purchaseToken,
voidedQuantity: vp.voidedQuantity,
voidedReason: vp.voidedReason,
voidedReasonText: getVoidedReasonText(vp.voidedReason),
voidedTimeMillis: vp.voidedTimeMillis ? Number(vp.voidedTimeMillis) : null,
voidedSource: vp.voidedSource,
voidedSourceText: getVoidedSourceText(vp.voidedSource)
});
}

// Lấy token trang tiếp theo
token = (res.data && res.data.tokenPagination && res.data.tokenPagination.nextPageToken) || null;

} while (token);

// ============================================
// BƯỚC 6: HIỂN THỊ KẾT QUẢ
// ============================================
console.log('='.repeat(70));
console.log('📊 KẾT QUẢ CUỐI CÙNG');
console.log('='.repeat(70));
console.log(`Tổng số trang: ${pageCount}`);
console.log(`Tổng số giao dịch bị hủy: ${results.length}\n`);

if (results.length === 0) {
console.log('✨ Không có giao dịch nào bị hủy trong khoảng thời gian này');
} else {
console.log('📋 CHI TIẾT GIAO DỊCH:');
console.log('-'.repeat(70));

results.forEach((purchase, index) => {
console.log(`\n[${index + 1}] Order ID: ${purchase.orderId || 'N/A'}`);
console.log(` Purchase Token: ${purchase.purchaseToken.substring(0, 40)}...`);
console.log(` Purchase Time: ${purchase.purchaseTimeMillis ? new Date(purchase.purchaseTimeMillis).toLocaleString('vi-VN') : 'N/A'}`);
console.log(` Voided Time: ${purchase.voidedTimeMillis ? new Date(purchase.voidedTimeMillis).toLocaleString('vi-VN') : 'N/A'}`);
console.log(` Voided Reason: ${purchase.voidedReasonText} (${purchase.voidedReason})`);
console.log(` Voided Source: ${purchase.voidedSourceText} (${purchase.voidedSource})`);
console.log(` Voided Quantity: ${purchase.voidedQuantity || 1}`);
});

console.log('\n' + '-'.repeat(70));
console.log('\n💾 Full JSON Output:\n');
console.log(JSON.stringify(results, null, 2));
}

console.log('\n' + '='.repeat(70));
console.log('✅ Test hoàn tất thành công!');
console.log('='.repeat(70));

} catch (err) {
// ============================================
// XỬ LÝ LỖI
// ============================================
console.error('\n❌ LỖI XẢY RA:');
console.error('='.repeat(70));

if (err.response) {
console.error(`HTTP Status: ${err.response.status}`);
console.error(`Status Text: ${err.response.statusText}`);
console.error(`Error Message: ${err.response.data?.error?.message || 'Unknown error'}`);

// Gợi ý khắc phục theo mã lỗi
console.error('\n💡 GỢI Ý KHẮC PHỤC:');

if (err.response.status === 403) {
console.error(' ⚠️ Lỗi 403 Forbidden - Không có quyền truy cập');
console.error(' - Kiểm tra Google Play Android Developer API đã enable chưa');
console.error(' - Kiểm tra Service Account đã được thêm vào Play Console chưa');
console.error(' - Kiểm tra đã cấp quyền Financial data và Order management chưa');
console.error(' - Đợi 2-24 giờ để quyền đồng bộ hoàn toàn');
} else if (err.response.status === 401) {
console.error(' ⚠️ Lỗi 401 Unauthorized - Xác thực thất bại');
console.error(' - Kiểm tra file service-account.json có đúng không');
console.error(' - Kiểm tra file có bị hỏng hoặc thiếu thông tin không');
console.error(' - Thử tạo lại Service Account Key mới');
} else if (err.response.status === 400) {
console.error(' ⚠️ Lỗi 400 Bad Request - Request không hợp lệ');
console.error(' - Kiểm tra packageName có đúng không');
console.error(' - Kiểm tra startTime và endTime hợp lệ (không quá 60 ngày)');
console.error(' - Kiểm tra các parameters khác');
} else if (err.response.status === 404) {
console.error(' ⚠️ Lỗi 404 Not Found');
console.error(' - Package name không tồn tại trên Google Play');
console.error(' - Hoặc Service Account không có quyền truy cập app này');
}
} else if (err.code === 'ENOENT') {
console.error(`File không tồn tại: ${serviceKeyPath}`);
console.error('\n💡 GỢI Ý KHẮC PHỤC:');
console.error(' - Kiểm tra file service-account.json có trong thư mục không');
console.error(' - Kiểm tra tên file có chính xác không (phân biệt hoa thường)');
} else {
console.error(err.message);
if (err.stack) console.error('\n' + err.stack);
}

console.error('='.repeat(70));
process.exitCode = 1;
}
}

// ============================================
// HÀM PHỤ TRỢ
// ============================================

function getVoidedReasonText(reason) {
const reasonMap = {
0: 'Khác',
1: 'Đổi ý',
2: 'Không nhận được',
3: 'Lỗi/defective',
4: 'Mua nhầm',
5: 'Gian lận',
6: 'Friendly fraud',
7: 'Chargeback',
8: 'Không xác nhận mua (unacknowledged)'
};
return reasonMap[reason] || `Unknown (${reason})`;
}

function getVoidedSourceText(source) {
const sourceMap = {
0: 'Người dùng',
1: 'Developer',
2: 'Google'
};
return sourceMap[source] || `Unknown (${source})`;
}

// ============================================
// CHẠY CHƯƠNG TRÌNH
// ============================================
main();

3.4. CHẠY TEST

Bước 7: Chạy với package mặc định

node test-voided-purchases.js

Bước 8: Chạy với package tùy chỉnh

node test-voided-purchases.js com.example.myapp

3.5. ĐỌC KẾT QUẢ

Kết quả thành công (có giao dịch hủy):

╔══════════════════════════════════════════════════════════════════════╗
║ GOOGLE PLAY ANDROID DEVELOPER API - VOIDED PURCHASES TEST ║
╚══════════════════════════════════════════════════════════════════════╝

🚀 Bắt đầu test Google Play Android Developer API...

📝 Bước 1: Đang xác thực với Google...
✅ Xác thực thành công!

📝 Bước 2: Khởi tạo Android Publisher API client...
✅ API client đã sẵn sàng!

📝 Bước 3: Chuẩn bị parameters...
📦 Package Name: vn.vgp.kiem.khach.phong.lang
⏰ Start Time: 30/11/2024, 00:00:00
⏰ End Time: 29/12/2024, 14:30:00
📊 Max Results: 100

📝 Bước 4: Đang gọi API...

📄 Đang lấy trang 1...
✅ Tìm thấy 3 giao dịch

======================================================================
📊 KẾT QUẢ CUỐI CÙNG
======================================================================
Tổng số trang: 1
Tổng số giao dịch bị hủy: 3

📋 CHI TIẾT GIAO DỊCH:
----------------------------------------------------------------------

[1] Order ID: GPA.1234-5678-9012-34567
Purchase Token: abcdefghijklmnopqrstuvwxyz123456789...
Purchase Time: 15/12/2024, 10:30:00
Voided Time: 20/12/2024, 14:15:00
Voided Reason: Đổi ý (1)
Voided Source: Người dùng (0)
Voided Quantity: 1

[2] Order ID: GPA.2345-6789-0123-45678
Purchase Token: bcdefghijklmnopqrstuvwxyz234567890...
Purchase Time: 18/12/2024, 15:45:00
Voided Time: 22/12/2024, 09:30:00
Voided Reason: Không nhận được (2)
Voided Source: Người dùng (0)
Voided Quantity: 1

----------------------------------------------------------------------

💾 Full JSON Output:

[
{
"kind": "androidpublisher#voidedPurchase",
"orderId": "GPA.1234-5678-9012-34567",
"purchaseTimeMillis": 1702638600000,
"purchaseToken": "abcdefghijklmnopqrstuvwxyz...",
"voidedQuantity": 1,
"voidedReason": 1,
"voidedReasonText": "Đổi ý",
"voidedTimeMillis": 1703077500000,
"voidedSource": 0,
"voidedSourceText": "Người dùng"
}
]

======================================================================
✅ Test hoàn tất thành công!
======================================================================

Kết quả thành công (không có giao dịch hủy):

======================================================================
📊 KẾT QUẢ CUỐI CÙNG
======================================================================
Tổng số trang: 1
Tổng số giao dịch bị hủy: 0

✨ Không có giao dịch nào bị hủy trong khoảng thời gian này

======================================================================
✅ Test hoàn tất thành công!
======================================================================

3.6. XỬ LÝ LỖI

❌ Lỗi 403 Forbidden

❌ LỖI XẢY RA:
======================================================================
HTTP Status: 403
Status Text: Forbidden
Error Message: The caller does not have permission

💡 GỢI Ý KHẮC PHỤC:
⚠️ Lỗi 403 Forbidden - Không có quyền truy cập
- Kiểm tra Google Play Android Developer API đã enable chưa
- Kiểm tra Service Account đã được thêm vào Play Console chưa
- Kiểm tra đã cấp quyền Financial data và Order management chưa
- Đợi 2-24 giờ để quyền đồng bộ hoàn toàn
======================================================================

Cách khắc phục:

  1. Quay lại PHẦN 1, Bước 13 để kiểm tra quyền
  2. Đợi 24 giờ nếu vừa mới cấp quyền
  3. Thử lại

❌ Lỗi 401 Unauthorized

❌ LỖI XẢY RA:
======================================================================
HTTP Status: 401
Status Text: Unauthorized
Error Message: Request is missing required authentication credential

💡 GỢI Ý KHẮC PHỤC:
⚠️ Lỗi 401 Unauthorized - Xác thực thất bại
- Kiểm tra file service-account.json có đúng không
- Kiểm tra file có bị hỏng hoặc thiếu thông tin không
- Thử tạo lại Service Account Key mới
======================================================================

Cách khắc phục:

  1. Kiểm tra file service-account.json có trong thư mục không
  2. Mở file JSON, kiểm tra có đầy đủ các trường không
  3. Tạo lại Key mới (PHẦN 1, Bước 6)

❌ Lỗi 400 Bad Request

❌ LỖI XẢY RA:
======================================================================
HTTP Status: 400
Status Text: Bad Request
Error Message: Invalid package name

💡 GỢI Ý KHẮC PHỤC:
⚠️ Lỗi 400 Bad Request - Request không hợp lệ
- Kiểm tra packageName có đúng không
- Kiểm tra startTime và endTime hợp lệ (không quá 60 ngày)
- Kiểm tra các parameters khác
======================================================================

Cách khắc phục:

  1. Copy chính xác packageName từ Play Console
  2. Kiểm tra startTime không quá 60 ngày so với hiện tại
  3. Kiểm tra format các parameters

❌ File không tồn tại

❌ LỖI XẢY RA:
======================================================================
File không tồn tại: /path/to/google-play-api-test/service-account.json

💡 GỢI Ý KHẮC PHỤC:
- Kiểm tra file service-account.json có trong thư mục không
- Kiểm tra tên file có chính xác không (phân biệt hoa thường)
======================================================================

Cách khắc phục:

  1. Kiểm tra file có trong thư mục không: ls -la
  2. Đảm bảo tên file là service-account.json (chữ thường)
  3. Copy lại file từ Google Cloud Console

3.7. TÙY CHỈNH SCRIPT

Thay đổi khoảng thời gian

Sửa dòng này trong code:

// Lấy 29 ngày trước
const startMs = nowMs - 29 * 24 * 60 * 60 * 1000;

// Lấy 7 ngày trước
const startMs = nowMs - 7 * 24 * 60 * 60 * 1000;

// Lấy 60 ngày trước (max)
const startMs = nowMs - 60 * 24 * 60 * 60 * 1000;

Chỉ lấy subscriptions

Sửa dòng này:

type: 0, // 0 = in-app products, 1 = subscriptions

// Đổi thành:
type: 1, // Chỉ lấy subscriptions

Tăng số lượng kết quả

maxResults: 100, // Mặc định

// Đổi thành:
maxResults: 1000, // Tối đa

3.8. LƯU Ý BẢO MẬT

⚠️ QUAN TRỌNG: Bảo vệ file service-account.json

Tạo file .gitignore:

echo "service-account.json" >> .gitignore
echo "node_modules/" >> .gitignore

Nội dung file .gitignore:

# Service Account - KHÔNG BAO GIỜ COMMIT FILE NÀY
service-account.json
*.json

# Dependencies
node_modules/
package-lock.json

# Logs
*.log

Nếu vô tình đã commit:

git rm --cached service-account.json
git commit -m "Remove service account key"
git push

Sau đó:

  1. Vào Google Cloud Console
  2. Xóa Key cũ
  3. Tạo Key mới

✅ HOÀN TẤT PHẦN 3

Checklist test Node.js:

  • ✅ Đã cài Node.js và npm
  • ✅ Đã tạo project và cài googleapis
  • ✅ Đã copy file service-account.json
  • ✅ Đã tạo file test-voided-purchases.js
  • ✅ Đã chạy test thành công
  • ✅ Đã hiểu cách đọc kết quả
  • ✅ Đã thêm .gitignore để bảo mật

🎉 TỔNG KẾT

Bạn đã hoàn thành cả 3 phần:

✅ PHẦN 1: Cấu hình và cấp quyền tài khoản

  • Enable Google Play Android Developer API
  • Tạo Service Account và tải Key JSON
  • Cấp quyền Financial data + Order management trên Play Console
  • Xác minh cấu hình

✅ PHẦN 2: Test trực tiếp trên Google API Explorer

  • Sử dụng API Explorer để test nhanh
  • Hiểu rõ các parameters (required và optional)
  • Xác thực OAuth 2.0
  • Đọc và phân tích response

✅ PHẦN 3: Test bằng Node.js

  • Code hoàn chỉnh với xử lý lỗi
  • Hiển thị kết quả chi tiết bằng tiếng Việt
  • Hỗ trợ phân trang tự động
  • Bảo mật file JSON

📚 TÀI LIỆU THAM KHẢO


🆘 HỖ TRỢ

Nếu gặp vấn đề:

  1. Kiểm tra lại từng bước trong PHẦN 1
  2. Đợi 24 giờ để quyền đồng bộ
  3. Xem phần "Xử lý lỗi" trong từng phần
  4. Kiểm tra logs chi tiết từ script Node.js

Lỗi phổ biến nhất: Lỗi 403 do quyền chưa đồng bộ → Đợi thêm thời gian


⚡ TIPS

  • Luôn test trên API Explorer trước khi code
  • Lưu service-account.json ở nơi an toàn
  • Đặt cron job chạy mỗi 6-12 giờ (không cần quá thường xuyên)
  • Log mọi giao dịch hủy để phân tích xu hướng
  • Xem xét refund policy dựa trên voidedReason

Chúc bạn thành công! 🎉