|
|
| (同じ利用者による、間の8版が非表示) |
| 行1: |
行1: |
| − | ==準備==
| + | [[Php/アプリストア連携/返金API/GooglePlayStore/基本]] |
| − | [[Php/GooglePlayApi]] [ショートカット] | + | |
| | | | |
| − | 上記ページの以下処理を行う。
| + | [[Php/アプリストア連携/返金API/GooglePlayStore/RTDN]] |
| − | #gcpでサービスアカウントを作成し鍵を作る
| + | |
| − | #Google Play Android Developer APIを有効に
| + | |
| − | #PlayConsoleにサービスアカウントの権限を追加
| + | |
| | | | |
| − | ==GooglePlayAPIのvoided-purchasesを使用==
| + | [[Php/アプリストア連携/返金API/GooglePlayStore/voided-purchases]] |
| − | このAPIは、自サーバからdevelopers.googleのAPIへ、問い合わせして、返金情報をリストで取得するもの
| + | |
| − | | + | |
| − | 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
| + | |
| − | | + | |
| − | https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.voidedpurchases?hl=ja#VoidedPurchase
| + | |
| − | | + | |
| − | <pre>
| + | |
| − | voidedSource:取り消し済みの購入の開始者。有効な値は 0 です。ユーザー 1.デベロッパー 2.Google
| + | |
| − | voidedReason:購入が取り消された理由。有効な値は 0 です。その他 1.購入者都合 2.Not_received 3.欠陥 4.Accidental_purchase 5.不正行為 6.フレンドリーな不正行為 7.チャージバック 8.Unacknowledged_purchase
| + | |
| − | </pre>
| + | |
| − | | + | |
| − | ===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>
| + | |
| − | | + | |
| − | ===正常レスポンス===
| + | |
| − | <pre>
| + | |
| − | {
| + | |
| − | "voidedPurchases": [
| + | |
| − | {
| + | |
| − | "kind": "androidpublisher#voidedPurchase",
| + | |
| − | "purchaseToken": "abcdef123456",
| + | |
| − | "orderId": "GPA.1234-5678-9012-34567",
| + | |
| − | "voidedTimeMillis": "1610000000000",
| + | |
| − | "voidedSource": 1,
| + | |
| − | "voidedReason": 2
| + | |
| − | }
| + | |
| − | ],
| + | |
| − | "tokenPagination": {
| + | |
| − | "nextPageToken": "next_page_token_123"
| + | |
| − | }
| + | |
| − | }
| + | |
| − | </pre>
| + | |