Сегодняшний вечер можно с гордостью назвать "вечером ебли с палкой", потому что он полностью был проведен в пейпеловской песочнице. Как бы итогом всего будет этот пост для гиков.
Имеем: Pylons (да не важно, любой python-фреймворк), PayPal Business Account (вот такой дают только по кредиткам США), кофе. Два. Ну и еще сервер, торчащий наружу, потому что нужно будет обрабатывать ответы от PayPal.
Хотим: прикрутить кнопочку "платить через PayPal" на сайт, чтобы пользователи несли нам денюжки со своих кредиток не задумываясь. Кстати, платить, можно с любой карточки, в том числе и российской. Лишь бы правильного типа (не Electron, как всем дают). Classic вполне подойдет. А еще можно оформить за 500 рублей MasterCard Classic в Связном. 500 рублей сразу на счет идут. Привязывается и к WM и к PayPal. Поцаны пробовали, вон как прутся.
Для начала как это работает, если кто как и я, работал с системами оплаты первый раз: мы формируем post-запрос к скрипту на сайте PayPal (легче всего это сделать через форму с кучей hidden-полей), обратно на URL уже нашего скрипта (переданный в форме, либо настроенный на сайте) поступает ответ. Так как и на первом и на втором шаге злоумышленники могут нам дать пизды, мы вручную шлем на PayPal еще один post-запрос с полученными данными. Типа спросили "это твое говно?" Если получаем в ответ VERIFIED, значит его, если INVALID, то не его, либо уже старо, оплачено, да в любом остальном случае. Это называется IPN (Instant Payment Notification). Затем проверяем полученные данные (вдруг заплатили меньше, чем мы хотели) и записываем в базу флажок "оплачено" и всю информацию. Ну вот и все. Так работают, кстати, почти все. WebMoney, с некоторыми отступлениями, тоже.
Для начала форма. Как-то так:
<form action="https://www.sandbox.paypal.com/cgi-bin/webscr" method="post">
<input type="hidden" name="cmd" value="_xclick">
<input type="hidden" name="business" value="mail@gmail.com">
<input type="hidden" name="lc" value="US">
<input type="hidden" name="item_name" value="My Cool Shop. Order #12345">
<input type="hidden" name="item_number" value="12345">
<input type="hidden" name="amount" value="200">
<input type="hidden" name="no_note" value="1">
<input type="hidden" name="no_shipping" value="1">
<input type="hidden" name="rm" value="1">
<input type="hidden" name="return" value="http://site.ru/order/12345">
<input type="hidden" name="cancel_return" value="http://site.ru/order/12345">
<input type="hidden" name="currency_code" value="USD">
<input type="hidden" name="notify_url" value="http://site.ru/paypal_listener">
<input type="submit" value="Платить через PayPal">
</form>
Здесь:
action = sandbox. Не забудьте сменить на просто paypal.com, когда будете выкладывать в продакшен.
cmd - чтобы PayPal понял, что это за запрос. Просто клик.
bussiness - email, он же ваш логин на PayPal. Не забудьте про бизнесс-аккаунт.
lc - страна, которая будет стоять по-умолчанию при оплате
item_name - название покупаемого продукта. Я просто хранил № заказа и название магазина.
item_number - ID заказа. Либо артикул товара. Любые символы, но я предпочел номером.
amount - любимое поле. Цена!
no_note, no_shipping - всякие плюшки пейпала типа рассчета доставки. Нам не нужны.
return, cancel_return - куда переадресовать в случае успешной или отмененной операции.
currency_code - что за валюта у нас в поле anount
notify_url - URL того самого IPN-листенера, код которого описан далее.
Теперь IPN-листенер. Тот самый, что по http://site.ru/paypal_listener. Простой пайлонсовский контроллер:
def paypal_confirm(self):
""" PayPal IPN listener """
import urllib2, urllib
# Тот самый POST-запрос к PayPal'у, чтобы подтвердить правильность
params = urllib.urlencode(request.POST)
url = "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_notify-validate&" + params
response = urllib2.urlopen(url).read()
if response == "INVALID":
# И значит кто-то все сделал не правильно
return "FFFFFUUUUU"
# А если мы здесь, значит запрос был правильный.
if request.params.get("payment_status", "") == "Completed":
# Помните мы передали его в форме? Вот он и вернулся. ID заказа.
id = int(request.params.get("item_number", 0))
# Вдруг такого заказа вообще не существует в нашей базе
order = self.connection.shop.orders.find_one({ "id": id })
if not order:
return "FFFFUUUU"
# Но злые хакеры могли подменить цену в нашей форме.
# Обязательно проверьте, последствия сами понимаете
if float(request.params.get("mc_gross", -1)) >= float(order.get("price", 0) + order.get("tax", 0)):
# Ну а если мы добрались до сюда, значит все круто!
# Делаем что хотим с заказом
self.connection.shop.orders.update({ "id": id }, ... })
return "OK"
По комментариям должно быть понятно назначение каждого блока. PayPal'у откровенно похуй, что мы ответим (лишь бы хедер 200 OK), поэтому дальше дело наше - заносить в базу, проверять, что угодно. Хотя я бы для пущей секкурности потребовал бы еще какой-нибудь ответ для PayPal. Типа "данные успешно занесены в базу, списывай бабло". Хотя это их не интересует, так что тут особо аккуратно, чтобы не потерять заказы и не восстанавливать по письмам клиентов и логам PayPal'а.
И я бы рекомендовал класть в базу все полученные данные. Чтобы если вдруг что, оправдаться перед покупателями куда делись деньги.
Кстати, есть в песочнице сервис для теста IPN-запросов. Вы указываете что слать и куда, PayPal шлет, а вы смотрите что у вас не работает (для этого ведите логи). Вот тут: https://developer.paypal.com/devscr?cmd=_ipn-link-session
И это только со стороны кажется так просто. А у PayPal'а гигабайты документации по этому поводу, и отыскать то, что нужно нам - довольно сложно. Только чтобы освоить этот самый IPN-интерфейс мне пришлось прочитать 67-страничный pdf.
А вообще, главная прелесть, что вам даже не нужно спрашивать у заказчиков Business аккаунт, все можно сделать из песочницы. Хотя, честно сказать, она сделана очень "в духе PayPal". То есть нихрена не понятно, куча кнопок-ссылок, менюшки с 20 уровнями вложенности, и тормозит.