「Php/アプリストア連携/返金API/GooglePlayStore」の版間の差分
提供: 初心者エンジニアの簡易メモ
(ページの作成:「==GooglePlayAPIのvoided-purchasesを使用== https://developers.google.com/android-publisher/voided-purchases?hl=ja https://developers.google.com/android-publisher/api-...」) |
|||
| 行4: | 行4: | ||
https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.voidedpurchases/list?hl=ja | https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.voidedpurchases/list?hl=ja | ||
| + | ===phpサンプルコード=== | ||
| + | <pre> | ||
| + | class GooglePlayRefundChecker { | ||
| + | private $serviceAccountFile; | ||
| + | private $packageName; | ||
| + | private $accessToken; | ||
| + | |||
| + | public function __construct($serviceAccountFile, $packageName) { | ||
| + | $this->serviceAccountFile = $serviceAccountFile; | ||
| + | $this->packageName = $packageName; | ||
| + | } | ||
| + | |||
| + | // アクセストークンを取得 | ||
| + | public function authenticate() { | ||
| + | $credentials = json_decode(file_get_contents($this->serviceAccountFile), true); | ||
| + | |||
| + | $jwtHeader = base64_encode(json_encode([ | ||
| + | 'alg' => 'RS256', | ||
| + | 'typ' => 'JWT' | ||
| + | ])); | ||
| + | |||
| + | $now = time(); | ||
| + | $jwtClaimSet = base64_encode(json_encode([ | ||
| + | 'iss' => $credentials['client_email'], | ||
| + | 'scope' => 'https://www.googleapis.com/auth/androidpublisher', | ||
| + | 'aud' => 'https://oauth2.googleapis.com/token', | ||
| + | 'iat' => $now, | ||
| + | 'exp' => $now + 3600 | ||
| + | ])); | ||
| + | |||
| + | $signatureInput = "$jwtHeader.$jwtClaimSet"; | ||
| + | openssl_sign($signatureInput, $signature, $credentials['private_key'], 'SHA256'); | ||
| + | $jwtSignature = base64_encode($signature); | ||
| + | |||
| + | $jwt = "$signatureInput.$jwtSignature"; | ||
| + | |||
| + | $response = $this->httpPost('https://oauth2.googleapis.com/token', [ | ||
| + | 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', | ||
| + | 'assertion' => $jwt | ||
| + | ]); | ||
| + | |||
| + | $this->accessToken = $response['access_token']; | ||
| + | return $this->accessToken; | ||
| + | } | ||
| + | |||
| + | // 返金情報を取得 | ||
| + | public function getVoidedPurchases($startTime = null) { | ||
| + | if (!$this->accessToken) { | ||
| + | throw new Exception('Access token not available. Call authenticate() first.'); | ||
| + | } | ||
| + | |||
| + | $url = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{$this->packageName}/purchases/voidedpurchases"; | ||
| + | $params = ['type' => 'subscription']; | ||
| + | |||
| + | if ($startTime) { | ||
| + | $params['startTime'] = $startTime; | ||
| + | } | ||
| + | |||
| + | $allVoidedPurchases = []; | ||
| + | $nextPageToken = null; | ||
| + | |||
| + | do { | ||
| + | if ($nextPageToken) { | ||
| + | $params['token'] = $nextPageToken; | ||
| + | } | ||
| + | |||
| + | $response = $this->httpGet($url, $params); | ||
| + | $data = json_decode($response, true); | ||
| + | |||
| + | if (isset($data['voidedPurchases'])) { | ||
| + | $allVoidedPurchases = array_merge($allVoidedPurchases, $data['voidedPurchases']); | ||
| + | } | ||
| + | |||
| + | $nextPageToken = $data['tokenPagination']['nextPageToken'] ?? null; | ||
| + | |||
| + | } while ($nextPageToken); | ||
| + | |||
| + | return $allVoidedPurchases; | ||
| + | } | ||
| + | |||
| + | // 返金処理を実行 | ||
| + | public function processRefunds($voidedPurchases) { | ||
| + | foreach ($voidedPurchases as $purchase) { | ||
| + | try { | ||
| + | $orderId = $purchase['orderId']; | ||
| + | $purchaseToken = $purchase['purchaseToken']; | ||
| + | $voidedTime = date('Y-m-d H:i:s', $purchase['voidedTimeMillis'] / 1000); | ||
| + | |||
| + | // ここでデータベース更新やサービス停止処理を実装 | ||
| + | $this->updateOrderStatus($orderId, 'REFUNDED'); | ||
| + | $this->revokeUserAccess($purchaseToken); | ||
| + | |||
| + | echo "Processed refund for order: $orderId (voided at: $voidedTime)\n"; | ||
| + | } catch (Exception $e) { | ||
| + | error_log("Error processing refund for order {$orderId}: " . $e->getMessage()); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | |||
| + | private function httpPost($url, $data) { | ||
| + | $ch = curl_init($url); | ||
| + | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | ||
| + | curl_setopt($ch, CURLOPT_POST, true); | ||
| + | curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); | ||
| + | $response = curl_exec($ch); | ||
| + | curl_close($ch); | ||
| + | return json_decode($response, true); | ||
| + | } | ||
| + | |||
| + | private function httpGet($url, $params = []) { | ||
| + | $query = http_build_query($params); | ||
| + | $fullUrl = $url . ($query ? "?{$query}" : ''); | ||
| + | |||
| + | $ch = curl_init(); | ||
| + | curl_setopt($ch, CURLOPT_URL, $fullUrl); | ||
| + | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | ||
| + | curl_setopt($ch, CURLOPT_HTTPHEADER, [ | ||
| + | 'Authorization: Bearer ' . $this->accessToken, | ||
| + | 'Content-Type: application/json' | ||
| + | ]); | ||
| + | $response = curl_exec($ch); | ||
| + | curl_close($ch); | ||
| + | return $response; | ||
| + | } | ||
| + | |||
| + | private function updateOrderStatus($orderId, $status) { | ||
| + | // データベース更新処理を実装 | ||
| + | // 例: DB::table('orders')->where('order_id', $orderId)->update(['status' => $status]); | ||
| + | } | ||
| + | |||
| + | private function revokeUserAccess($purchaseToken) { | ||
| + | // ユーザーアクセス無効化処理を実装 | ||
| + | // 例: $user = User::findByPurchaseToken($purchaseToken); $user->revokePremiumAccess(); | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // 使用例 | ||
| + | try { | ||
| + | $checker = new GooglePlayRefundChecker( | ||
| + | '/path/to/service-account.json', | ||
| + | 'com.your.app.package' | ||
| + | ); | ||
| + | |||
| + | // 認証 | ||
| + | $accessToken = $checker->authenticate(); | ||
| + | echo "Access Token: $accessToken\n"; | ||
| + | |||
| + | // 返金情報取得(前回処理から1時間前までのデータを取得) | ||
| + | $lastCheckTime = strtotime('-1 hour') * 1000; // ミリ秒単位 | ||
| + | $voidedPurchases = $checker->getVoidedPurchases($lastCheckTime); | ||
| + | |||
| + | // 返金処理実行 | ||
| + | $checker->processRefunds($voidedPurchases); | ||
| + | |||
| + | echo "Refund processing completed. Total processed: " . count($voidedPurchases) . "\n"; | ||
| + | |||
| + | } catch (Exception $e) { | ||
| + | die("Error: " . $e->getMessage()); | ||
| + | } | ||
| + | </pre> | ||
===正常レスポンス=== | ===正常レスポンス=== | ||
2025年6月4日 (水) 15:29時点における版
GooglePlayAPIのvoided-purchasesを使用
https://developers.google.com/android-publisher/voided-purchases?hl=ja
https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.voidedpurchases/list?hl=ja
phpサンプルコード
class GooglePlayRefundChecker {
private $serviceAccountFile;
private $packageName;
private $accessToken;
public function __construct($serviceAccountFile, $packageName) {
$this->serviceAccountFile = $serviceAccountFile;
$this->packageName = $packageName;
}
// アクセストークンを取得
public function authenticate() {
$credentials = json_decode(file_get_contents($this->serviceAccountFile), true);
$jwtHeader = base64_encode(json_encode([
'alg' => 'RS256',
'typ' => 'JWT'
]));
$now = time();
$jwtClaimSet = base64_encode(json_encode([
'iss' => $credentials['client_email'],
'scope' => 'https://www.googleapis.com/auth/androidpublisher',
'aud' => 'https://oauth2.googleapis.com/token',
'iat' => $now,
'exp' => $now + 3600
]));
$signatureInput = "$jwtHeader.$jwtClaimSet";
openssl_sign($signatureInput, $signature, $credentials['private_key'], 'SHA256');
$jwtSignature = base64_encode($signature);
$jwt = "$signatureInput.$jwtSignature";
$response = $this->httpPost('https://oauth2.googleapis.com/token', [
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $jwt
]);
$this->accessToken = $response['access_token'];
return $this->accessToken;
}
// 返金情報を取得
public function getVoidedPurchases($startTime = null) {
if (!$this->accessToken) {
throw new Exception('Access token not available. Call authenticate() first.');
}
$url = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{$this->packageName}/purchases/voidedpurchases";
$params = ['type' => 'subscription'];
if ($startTime) {
$params['startTime'] = $startTime;
}
$allVoidedPurchases = [];
$nextPageToken = null;
do {
if ($nextPageToken) {
$params['token'] = $nextPageToken;
}
$response = $this->httpGet($url, $params);
$data = json_decode($response, true);
if (isset($data['voidedPurchases'])) {
$allVoidedPurchases = array_merge($allVoidedPurchases, $data['voidedPurchases']);
}
$nextPageToken = $data['tokenPagination']['nextPageToken'] ?? null;
} while ($nextPageToken);
return $allVoidedPurchases;
}
// 返金処理を実行
public function processRefunds($voidedPurchases) {
foreach ($voidedPurchases as $purchase) {
try {
$orderId = $purchase['orderId'];
$purchaseToken = $purchase['purchaseToken'];
$voidedTime = date('Y-m-d H:i:s', $purchase['voidedTimeMillis'] / 1000);
// ここでデータベース更新やサービス停止処理を実装
$this->updateOrderStatus($orderId, 'REFUNDED');
$this->revokeUserAccess($purchaseToken);
echo "Processed refund for order: $orderId (voided at: $voidedTime)\n";
} catch (Exception $e) {
error_log("Error processing refund for order {$orderId}: " . $e->getMessage());
}
}
}
private function httpPost($url, $data) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
private function httpGet($url, $params = []) {
$query = http_build_query($params);
$fullUrl = $url . ($query ? "?{$query}" : '');
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $fullUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $this->accessToken,
'Content-Type: application/json'
]);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
private function updateOrderStatus($orderId, $status) {
// データベース更新処理を実装
// 例: DB::table('orders')->where('order_id', $orderId)->update(['status' => $status]);
}
private function revokeUserAccess($purchaseToken) {
// ユーザーアクセス無効化処理を実装
// 例: $user = User::findByPurchaseToken($purchaseToken); $user->revokePremiumAccess();
}
}
// 使用例
try {
$checker = new GooglePlayRefundChecker(
'/path/to/service-account.json',
'com.your.app.package'
);
// 認証
$accessToken = $checker->authenticate();
echo "Access Token: $accessToken\n";
// 返金情報取得(前回処理から1時間前までのデータを取得)
$lastCheckTime = strtotime('-1 hour') * 1000; // ミリ秒単位
$voidedPurchases = $checker->getVoidedPurchases($lastCheckTime);
// 返金処理実行
$checker->processRefunds($voidedPurchases);
echo "Refund processing completed. Total processed: " . count($voidedPurchases) . "\n";
} catch (Exception $e) {
die("Error: " . $e->getMessage());
}
正常レスポンス
{
"voidedPurchases": [
{
"kind": "androidpublisher#voidedPurchase",
"purchaseToken": "abcdef123456",
"orderId": "GPA.1234-5678-9012-34567",
"voidedTimeMillis": "1610000000000",
"voidedSource": 1,
"voidedReason": 2
}
],
"tokenPagination": {
"nextPageToken": "next_page_token_123"
}
}
