Google Apps Marketplace Billing APIについて調べてみた

Google Apps Marketplaceの課金の仕組みについて調べてみました。
Google Apps Marketplaceを知らない方は、以下の記事を読んでください。

表題の件について、簡単なスライドを作って某所で説明したので、資料を公開してしまいます。
正しいのかはだいぶ不安です。詳しい人がいたら、ぜひコメントお願いします。

なお、公式ドキュメントはBilling Overview - Google Apps Marketplace - Google Codeです。

実装について

スライド内では実装については説明していません。簡単に書いておきます。
でも、実際には動いていません。動くはずのサンプルコードです^^;


基本的にはGoogle Apps Marketplaceにアプリケーションを登録する方法 - Startup Realityで使用しているサンプルアプリケーションを使っています。
それに対して以下のような修正を加えています。

manifest.xmlに以下を追加

Application Manifestと言われるものです。実際のところこのmanifest.xmlと次のlisting.xmlはファイルとして存在する必要はなくて、Google Apps Marketplaceにアプリを登録する際の入力フォームにこのXMLの内容を記述します。

  <!-- Configures the default_edition for existing users -->
  <Edition id="free">
    <Name>Default Edition</Name>
    <Extension ref="navLink"/>
    <Extension ref="realm"/>
  </Edition>

  <!-- Configures extensions available for the standard edition of the application. -->
  <Edition id="standard">
    <Name>Standard Edition</Name>
    <Extension ref="navLink"/>
    <Extension ref="realm"/>
  </Edition>

  <!-- Configures extensions available for the pro edition of the application -->
  <Edition id="premium">
    <Name>Premium Edition</Name>
    <Extension ref="navLink"/>
    <Extension ref="realm"/>
  </Edition>
listing.xmlを新規作成

Listing Manifestと呼ばれるものです。無料のMarketplaceアプリの場合は空欄にしていた項目です。

<?xml version="1.0" encoding="UTF-8" ?>
<ListingManifest>
  <SubListings>
    <SubListing>
      <DisplayName>Invoicer Free</DisplayName>
      <EditionId>free</EditionId>
      <PaymentModel>FREE</PaymentModel>
    </SubListing>
    <SubListing>
      <DisplayName>Invoicer Standard $5/year</DisplayName>
      <PaymentModel>PAID</PaymentModel>
      <PurchaseUrl>http://billing-test.appspot.com/purchase?edition=standard</PurchaseUrl>
    </SubListing>
    <SubListing>
      <DisplayName>Invoicer Premium $10/year</DisplayName>
      <PaymentModel>PAID</PaymentModel>
      <PurchaseUrl>http://billing-test.appspot.com/purchase?edition=premium</PurchaseUrl>
    </SubListing>
  </SubListings>
  <Merchants>
    <Merchant>
      <CurrencyCode>USD</CurrencyCode>
      <MerchantEmailAddress>xxxxxx@gmail.com</MerchantEmailAddress>
    </Merchant>
  </Merchants>
</ListingManifest>
PurchaseServlet.javaを新規作成

これはBillling APIを使って課金情報をGoogle Apps Marketplaceに送信するために使われるサーブレットです。Listing ManifestでPurchaseUrlとして指定している本体です。

package com.google.code.samples.apps.marketplace;

import java.io.IOException;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.api.client.sample.appsmarket.AppsMarketService;
import com.google.api.client.sample.appsmarket.model.Cart;
import com.google.api.client.sample.appsmarket.model.InitialCart;
import com.google.api.client.sample.appsmarket.model.LineItem;
import com.google.api.client.sample.appsmarket.model.PurchasingCustomerInfo;
import com.google.api.client.sample.appsmarket.model.RecurringCart;
import com.google.api.client.sample.appsmarket.model.Subscription;

public class PurchaseServlet extends HttpServlet {
	private static final String YOUR_APPLICATION_ID = "999999999999";
	private static final String YOUR_APPLICATION_NAME = "Billing API Test";
	private static final String YOUR_CONSUMER_SECRET = "abcdefghijklmnopqrstuvwxyz";
	
	Logger logger = Logger.getLogger(this.getClass().getName());

	@Override
	public void init(ServletConfig config) throws ServletException {
		super.init(config);
	}

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		String token = req.getParameter("appsmarket.purchaseToken");
		String edition = req.getParameter("edition");

		String redirectUrl = sendShoppingCard(token, edition);
		logger.warning("redirectUrl=" + redirectUrl);
		resp.sendRedirect(redirectUrl);

		// req.getRequestDispatcher("/WEB-INF/jsp/purchase.jsp")
		// .forward(req, resp);
	}

	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		resp.sendError(405);
	}

	private String sendShoppingCard(String purchaseToken, String edition)
			throws IOException {
		AppsMarketService service = new AppsMarketService();
		service.appId = YOUR_APPLICATION_ID;
		service.appName = YOUR_APPLICATION_NAME;
		service.endpoint = "https://www.googleapis.com/appsmarket/v2sandbox/";
		service.consumerKey = service.appId + ".apps.googleusercontent.com";
		service.consumerSecret = YOUR_CONSUMER_SECRET;
		service.authorize();

		PurchasingCustomerInfo info = service
				.getPurchasingCustomerInfo(purchaseToken);
		String customerId = info.customerLicense.customerId;
		String developerSku = edition;

		// Configure the base subscription.
		Subscription sub = new Subscription();
		sub.applicationId = service.appId;
		sub.customerId = customerId;
		sub.purchaseToken = purchaseToken;
		sub.name = service.appName + " Subscription";
		sub.description = "Example subscription description";
		sub.currencyCode = "USD";

		LineItem item;
		// Configure the initial cart (up front fees).
		sub.initialCart = new InitialCart();
		sub.initialCart.cart = new Cart();
		sub.initialCart.cart.receiptName = "Initial cart";
		sub.initialCart.cart.receiptDescription = "Initial cart description";
		item = new LineItem();
		item.name = "Setup fee";
		item.description = "Setup fee description";
		item.developerSku = "setupfee";
		item.price = 99000000L; // 99 USD, in micro-dollars
		item.seatCount = 1;
		sub.initialCart.cart.items = new ArrayList();
		sub.initialCart.cart.items.add(item);

		// Configure the recurring cart (recurring subscription).
		sub.recurringCart = new RecurringCart();
		sub.recurringCart.cart = new Cart();
		sub.recurringCart.cart.receiptName = "Recurring subscription";
		sub.recurringCart.cart.receiptDescription = "Recurring cart description";
		item = new LineItem();
		item.name = edition + " edition";
		item.description = edition + " edition description";
		item.editionId = edition;
		item.developerSku = developerSku;
		item.seatCount = 5;
		item.price = 250000000L; // 250 USD (50/seat), in micro-dollars
		sub.recurringCart.cart.items = new ArrayList();
		sub.recurringCart.cart.items.add(item);
		sub.recurringCart.firstChargeDays = 0; // Begin charging immediately
		sub.recurringCart.frequency = "MONTHLY"; // Charge once a month

		// Send the subscription to Google.
		Subscription response = service.insertSubscription(sub);
		String redirectUrl = response.redirectUrl;
		return redirectUrl;
	}
}
web.xmlに以下を追加
    <servlet>
        <servlet-name>PurchaseServlet</servlet-name>
        <servlet-class>com.google.code.samples.apps.marketplace.PurchaseServlet</servlet-class>
    </servlet>
    ...
    <servlet-mapping>
        <servlet-name>PurchaseServlet</servlet-name>
        <url-pattern>/purchase</url-pattern>
    </servlet-mapping>
Billing APIに必要なjarファイルを追加

Billing and Licensing Developer's Guide v2 - Google Apps - Google Codeからjarファイルをダウンロードして追加してください。
google-api-java-clientは最新を落とさず、1.2.1を落としてください。最新だとクラスがないと言われます。
私はgoogle-api-client-1.2.1-alpha.jarとappsmarket-2010_12_3.jarの組み合わせで試しました。*1

問題のテスト環境設定

Setting up your test environmentにテスト環境の構築方法が書いてあるんですが、これがどうしてもうまく行きませんでした。
一応苦闘の記録を書いておくと、最初のGoogle Checkoutのsandboxでテストアカウントを作るところからうまく行かない。

まず、ここで日本を選択するとダメなようなので、アラバマ州にする。でも、郵便番号と電話番号がアラバマ州っぽくないとだめらしくて、アラバマ州っぽくすると成功。ここはなんとか突破。なお、キャプチャは電話番号がアラバマになっていません。
次が法人情報設定画面みたいなところに行くので、入力してみる。

すると、こんなメッセージ。いや、どうしろと。

There was an error processing your request. Refresh the page in a few minutes and try again. If you continue to experience this, please contact support, referencing this error code: SERVER_ERROR.

何かわかる人いればぜひ教えてください!


というわけで、結局うまくいかずあまり参考にならないかもしれませんが、何かの役に立てば幸いです。
やたらと情報少ないので、調べている方が他にいればぜひ情報共有したいです。

*1:いや、動いてないんですけどね>