python & paypalで一括送金 [Mass Pay & ipn]

今回は決済サービスを提供しているpaypalのAPIを利用して一括送金を行ってみたいと思います。
使用するpaypalのサービスは「mass pay API」と「ipn（即時支払い通知）」です。

#送金処理イメージ
![Untitled.png](https://qiita-image-store.s3.amazonaws.com/0/98305/8b7a876f-f1a7-e5df-03fd-4220ae59910c.png)


#paypalとは？
個人および事業主の間で簡単にオンライン決済を行えるようにしたサービスです。

#mass payとは？
paypalが提供している非同期送金API。ビジネスアカウントでのみ利用可能。
指定のpaypal登録メールアドレスをに対して送金を行うAPIです。
複数paypalアカウントに対して送金が行え、最大250件同時に送金が行えます。
手数料は送金者が支払い、指定金額が指定paypalアカウントへ送金されます。
mass payの送金状況はpaypalで提供しているipnを利用する事で確認できる。

#ipnとは？
Instant Payment Notification(即時支払い通知)。ビジネスアカウントでのみ利用可能。
paypalアカウントで支払いやAPIを利用した際に、そのアクション情報を指定urlへ送信するpaypalのサービスです。
sandboxではIPN Simulatorが提供されております。
IPNを利用する場合はpaypal側からの情報かをチェックするためのIPN authentication protocol(IPN認証プロトコル)を実装する必要があります。

#paypalアカウント
https://www.paypal.com/

paypalアカウントは3種類あります。
下記に各アカウント概要をまとめてみました。

- パーソナル
一般ユーザ向けアカウント
ebayツールの使用制限有り
mass pay利用不可

- プレミア
取引数が多くクレジットカードでの支払いを
受け取る必要があるユーザ向けアカウント
ebayツールの使用制限無し

- ビジネス
個人事業主or法人向けアカウント
ebayツールの使用制限無し
mass pay利用可能

mass pay APIを使用するためにはビジネスアカウントが必要です。
ただ、パーソナルアカウントでもsandboxを利用できるので、そちらでAPIテストを行いビジネスアカウントを申請する事も出来ます。

#準備するもの
- paypalアカウント
sandboxのAPIサーバへアクセ薄するのでパーソナルアカウントでも可能。

- python2.7.9以上の環境（SSLプロトコルTSL1.2対応必須）
homebrewでpythonをインストールしてください。python.orgからダウンロードしてインストールすると2.7.9以上でもsslにPROTOCOL_TLSv1_2が無い事が有ります。

- pythonの動くサーバ
今回はmac 10.9で実装しました。ipn simulaterを利用する際はFreeBSDのレンタルサーバを少し利用しました。

#今回解説する部分
- mass pay [APIのrequest & respons]
- ipn　[request & authentication protocol & respons]

#フォルダ構成
```
cgiserver.py
cgi-bin
├─mass_pay.py
└─ipn.py
```

#簡易サーバ立ち上げ
pythonのCGIHTTPServerです。
簡単に動的処理を実行できるサーバを立ち上げてくれる標準ライブラリ。

```lang:cgi_server.py
#!/usr/local/Cellar/python/2.7.11/bin/python
# -*- coding: utf-8 -*-
import CGIHTTPServer
CGIHTTPServer.test()
```

起動コマンドは
`python cgi_server.py`

いちいちpyファイルを作るのも面倒な場合はそのままコマンドでも起動可能。
`python -m CGIHTTPServer 8000`


#mass pay
```lang:curlコマンド
# curl -s --insecure —tlsv1 https://api-3t.sandbox.paypal.com/nvp -d "PWD=xxxxxxxxxxxxx&VERSION=90&L_UNIQUEID0=xxxxxxx&USER=xxxxxxxxxxx_api1.xxxxx.com&SIGNATURE=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&CURRENCYCODE=JPY&RECEIVERTYPE=EmailAddress&L_EMAIL0=xxxxxxxxx@xxxxxxxxxx.com&METHOD=MassPay&L_AMT0=100"

# 成功時レスポンス例
TIMESTAMP=2016%2d03%2d02T07%3a24%3a50Z&CORRELATIONID=xxxxxxxxxxxx&ACK=Success&VERSION=90&BUILD=18316154

# 失敗時レスポンス例
TIMESTAMP=2016%2d03%2d02T07%3a29%3a45Z&CORRELATIONID=xxxxxxxxxxxx&ACK=Failure&VERSION=90&BUILD=18316154&L_ERRORCODE0=10002&L_SHORTMESSAGE0=Security%20error&L_LONGMESSAGE0=Security%20header%20is%20not%20valid&L_SEVERITYCODE0=Error
```

```lang:mass_pay.py
# -*- coding: utf-8 -*-
import ssl
import urllib
import httplib

# SSLプロトコルTLS1.2で通信
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
# APIサーバドメインを指定
conn = httplib.HTTPSConnection('api-3t.sandbox.paypal.com', context=context)
params = urllib.urlencode(
    {"USER": "xxx_api1.xxx.com",                        # APIユーザname
     "PWD": "xxxxxxxxxxxxx",                            # APIユーザパスワード
     "SIGNATURE": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", # API署名
     "METHOD": "MassPay",
     "VERSION": 90,
     "RECEIVERTYPE": "EmailAddress",
     "CURRENCYCODE": "USD",                             # 送金通貨（USDは米ドル）
     "L_EMAIL0": "xxxxxxx-paypal-buyer@xxx.xx.jp",      # 送金先paypalアカウントEメールアドレス
     "L_AMT0": "0.01",                                  # 送金額
     "L_UNIQUEID0": "test1",                            # unique ID for txn
     "L_EMAIL1": "xxxxxxx2-paypal-buyer@xxx.xx.jp",     # 2件目の送金先
     "L_AMT1": "0.01",
     "L_UNIQUEID1": "test2"})
# mass pay APIを叩く 
conn.request('POST', '/nvp', params)                    # request送信
res = conn.getresponse()                                # respons取得
data = res.read()
print data
```


```lang:respons例
# 送金処理成功時のrespons例
TIMESTAMP=2016%2d03%2d01T01%3a06%3a32Z&CORRELATIONID=xxxxxxxxxxxxx&ACK=Success&VERSION=90&BUILD=xxxxxxxx

# 送金処理失敗時（残高不足）のrespons例
TIMESTAMP=2016%2d03%2d01T03%3a22%3a48Z&CORRELATIONID=xxxxxxxxxxxx&ACK=Failure&VERSION=90&BUILD=xxxxxxxx&L_ERRORCODE0=10321&L_SHORTMESSAGE0=Insufficient%20funds&L_LONGMESSAGE0=The%20account%20does%20not%20have%20sufficient%20funds%20to%20do%20this%20masspay&L_SEVERITYCODE0=Error
```

#ipn
ipnはpaypal側からのアクションで動くのでipn simulatorでipn.pyへ送信してみて下さい。またipnからの情報を元にcurlでapiを叩いても同様のテストが行えます。

```lang:curlコマンドイメージ例
# ipn simulatorからのpostデータを抜き出してcurlでテストしてみた
# curl -X POST http://127.0.0.1:8080/cgi-bin/ipn.py -d "txn_type=masspay&payment_gross_1=1500.00&payment_date=09:52:38 Dec 23, 2012 PST&last_name=Angell&mc_fee_1=1.00&masspay_txn_id_1=XXXXXXXXXXXXXXXXX&receiver_email_1=xxxx@xxxxxxxxxx.com&residence_country=US&verify_sign=XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx&payer_status=verified&test_ipn=1&payer_email=xxx@xxxxxxxx.com&first_name=Drew&payment_fee_1=1.00&payer_id=XXXXXXXXXXXXX&payer_business_name=Drew Angell's Test Store&payment_status=Processed&status_1=Unclaimed&mc_gross_1=1500.00&charset=windows-1252&notify_version=3.7&mc_currency_1=USD&unique_id_1=3&ipn_track_id=xxxxxxxxxxxxx"

# ipn成功時レスポンス
{"last_name": "Angell", "receiver_email_1": "xxxx@xxxxxxxxxx.com", "payer_business_name": "Drew Angell's Test Store", "payment_status": "Processed", "payer_id": "XXXXXXXXXXXXX", "residence_country": "US", "unique_id_1": "3", "payer_status": "verified", "txn_type": "masspay", "verify_sign": "XxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx", "masspay_txn_id_1": "XXXXXXXXXXXXXXXXX", "status_1": "Unclaimed", "mc_gross_1": "1500.00", "payment_gross_1": "1500.00", "payment_date": "09:52:38 Dec 23, 2012 PST", "first_name": "Drew", "payment_fee_1": "1.00", "payer_email": "xxx@xxxxxxxx.com", "charset": "windows-1252", "mc_fee_1": "1.00", "notify_version": "3.7", "mc_currency_1": "USD", "ipn_track_id": "xxxxxxxxxxxxx", "test_ipn": "1"}
# ipn失敗時レスポンス
{"error": "ipn authentication check error"}
```

```lang:ipn.py
#!/usr/local/Cellar/python/2.7.11/bin/python
# -*- coding: utf-8 -*-
import sys
import json
import cgi
import ssl
import urllib
import httplib

form=cgi.FieldStorage()
post = {key: form[key].value for key in form.keys()}
print "application/json\n"

def ipn(post):
    context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
    conn = httplib.HTTPSConnection('www.sandbox.paypal.com', context=context)
    payload = 'cmd=_notify-validate&' + \
        '&'.join(['{}={}'.format(key, urllib.quote_plus(val))
                  for key, val in post.items()])

    conn.request('POST', '/cgi-bin/webscr', payload)
    res = conn.getresponse()
    result = res.read()
    if not result == 'VERIFIED':
        print json.dumps({'error': 'ipn authentication check error'})
        sys.exit()
    return result


ipn(post)
data = dict(post)

print json.dumps(data)
```

```lang:respons例
# ipn認証成功時
{"last_name": "Angell", "receiver_email_1": "xxxx@xxxxxxxx.com", "payer_business_name": "Drew Angell's Test Store", "payment_status": "Processed", "payer_id": "XXXXXXXXXXXXX", "residence_country": "US", "unique_id_1": "3", "payer_status": "verified", "txn_type": "masspay", "verify_sign": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", "masspay_txn_id_1": "XXXXXXXXXXXXXXXXX", "status_1": "Completed", "mc_gross_1": "1500.00", "payment_gross_1": "1500.00", "payment_date": "09:52:38 Dec 23, 2012 PST", "first_name": "Drew", "payment_fee_1": "1.00", "payer_email": "xxxx@xxxxxxxx.com", "charset": "windows-1252", "mc_fee_1": "1.00", "notify_version": "3.7", "mc_currency_1": "USD", "ipn_track_id": "xxxxxxxxxxxxx", "test_ipn": "1"}

# ipn承認失敗時の出力内容
{"error": "ipn authentication check error"}
```

#注意
**masspayの利用に関しましてはpaypal社側での厳しい審査に通過する必要が有るそうです。個人事業主等のビジネスアカウントでは利用は難しいようです。ざっくりお問い合わせで聞いてみた所3ヶ月間5000米ドル以上支払いがあるビジネスアカウントに対して申請の方法をメールで案内するそうです。**