Всем привет! Мы разрабатываем сервис для сбора, доставки и анализа логов, серверная часть которого написана на Go. В этой статье мы расскажем о проблеме, с которой мы столкнулись при подключении нашего проекта к платежной системе PayPal и о решении, которое мы разработали и успешно внедрили.
Итак, у многих есть опыт работы с API PayPal, использовать OAuth 2.0 довольно просто: подключаем библиотеку-клиент в свой проект и начинаем реализацию.
Для PHP, Java и Python существуют официальные SDK библиотеки, но наш сервис написан на GO, и в этом случае поиск SDK не дал нам приемлемых результатов(https://github.com/search?q=paypal+golang). В итоге найдено пять проектов на github, два из которых выглядят достойно, но имеют ограниченный функционал:
- leebenson/paypal (неполное покрытие API)
- crowdmob/paypal (реализовывает только Express Checkout)
Нам был необходим клиент с возможностью расширения и улучшения функционала, поэтому было принято решение написать свой велосипед.
OAuth 2.0
На этапе разработки мы использовали PayPal sandbox, где проводили тестирование всех видов запросов API.
Первый этап — это работа с протоколом PayPal и авторизация. PayPal использует OAuth версии 2.0. Для начала нам необходимо получить приватные ключи (client_id и secret_key).
Авторизация осуществляется следующим образом: после получения client_id и secret_key необходимо сделать запрос в PayPal на получение access_token, который действителен в течении заданного времени. Далее все запросы в PayPal API должны сопровождаться этим токеном в заголовке запроса (-u ":").
Реализация с использованием нашего клиента:
import "github.com/logpacker/PayPal-Go-SDK"
// ...
// Create a client instance
c, err := paypalsdk.NewClient("clientID", "secretID", paypalsdk.APIBaseSandBox)
accessToken, err := c.GetAccessToken()
Далее объект клиента будет иметь все доступные методы для работы с API. Например, чтобы создать платеж нам необходимо выполнить следующее:
paymentResponse, err := client.CreatePayment(p)
Мы работаем над тем, чтобы предоставить и описать все доступные операции API, при этом есть возможность вызвать любой конечный метод посредством базовых функций:
req, err := c.NewRequest(method, url, payload)
c.SendWithAuth(req, &resp)
Все запросы в PayPal можно логировать в файл, полный дамп запроса сохраняется вместе с заголовками:
c.SetLogFile("/tpm/paypal-debug.log")
Доступные функции API
Полный список функций PayPal API представлен в спецификации, все они делятся на группы, Payments, Orders, Vault. В клиенте мы реализовали встроенные функции для основных операций API:
POST /v1/oauth2/token — получение временного access_token
accessToken, err := c.GetAccessToken()
За сохранение ключа отвечает приложение, поэтому вместо получения нового ключа можно установить сохраненный.
token := "abcdef"
c.SetAccessToken(token)
POST /v1/payments/payment — создание платежа в PayPal. Мы предоставили две функции для создания платежа.
Внутренний PayPal платеж:
amount := paypalsdk.Amount{
Total: "7.00",
Currency: "USD",
}
redirectURI := "http://example.com/redirect-uri"
cancelURI := "http://example.com/cancel-uri"
description := "Description for this payment"
paymentResult, err := c.CreateDirectPaypalPayment(amount, redirectURI, cancelURI, description)
2. Платеж любого типа:
p := paypalsdk.Payment{
Intent: "sale",
Payer: &paypalsdk.Payer{
PaymentMethod: "credit_card",
FundingInstruments: []paypalsdk.FundingInstrument{paypalsdk.FundingInstrument{
CreditCard: &paypalsdk.CreditCard{
Number: "4111111111111111",
Type: "visa",
ExpireMonth: "11",
ExpireYear: "2020",
CVV2: "777",
FirstName: "John",
LastName: "Doe",
},
}},
},
Transactions: []paypalsdk.Transaction{paypalsdk.Transaction{
Amount: &paypalsdk.Amount{
Currency: "USD",
Total: "7.00",
},
Description: "My Payment",
}},
RedirectURLs: &paypalsdk.RedirectURLs{
ReturnURL: "http://...",
CancelURL: "http://...",
},
}
paymentResponse, err := client.CreatePayment(p)
GET /v1/payments/payment/ID — получение информации о платеже
payment, err := c.GetPayment(paymentID)
GET /v1/payments/payment — список всех платежей
payments, err := c.GetPayments()
GET /v1/payments/authorization/ID — получение информации об авторизации
authID := "2DC87612EK520411B"
auth, err := c.GetAuthorization(authID)
POST /v1/payments/authorization/ID/capture — блокировка авторизации
capture, err := c.CaptureAuthorization(authID, &paypalsdk.Amount{Total: "7.00", Currency: "USD"}, true)
POST /v1/payments/authorization/ID/void — отмена авторизации
auth, err := c.VoidAuthorization(authID)
POST /v1/payments/authorization/ID/reauthorize — реавторизация
auth, err := c.ReauthorizeAuthorization(authID, &paypalsdk.Amount{Total: "7.00", Currency: "USD"})
GET /v1/payments/sale/ID — получение объекта продажи
saleID := "36C38912MN9658832"
sale, err := c.GetSale(saleID)
POST /v1/payments/sale/ID/refund — возврат средств для объекта продажи. Можно сделать как полный возврат платежа, так и частичный.
// Full
refund, err := c.RefundSale(saleID, nil)
// Partial
refund, err := c.RefundSale(saleID, &paypalsdk.Amount{Total: "7.00", Currency: "USD"})
GET /v1/payments/refund/ID — получение информации о возврате
orderID := "O-4J082351X3132253H"
refund, err := c.GetRefund(orderID)
GET /v1/payments/orders/ID — получение информации о заказе
order, err := c.GetOrder(orderID)
POST /v1/payments/orders/ID/authorize — авторизация заказа
auth, err := c.AuthorizeOrder(orderID, &paypalsdk.Amount{Total: "7.00", Currency: "USD"})
POST /v1/payments/orders/ID/capture — блокировка заказа (может быть частичной или полной, в зависимости от переданных Amount и IsFinalTransaction)
capture, err := c.CaptureOrder(orderID, &paypalsdk.Amount{Total: "7.00", Currency: "USD"}, true, nil)
POST /v1/payments/orders/ID/do-void — отмена заказа
order, err := c.VoidOrder(orderID)
Также можно воспользоваться godoc документацией для ознакомления со всеми функциями клиента: https://godoc.org/github.com/logpacker/PayPal-Go-SDK
Тестирование и CI
В проекте реализованы два типа тестов: Unit и Integration. Unit тесты позволяют проверить работоспособность внутренних условий и валидацию.
Пример проверки входных параметров в функции NewClient:
_, err := NewClient("", "", "")
if err == nil {
t.Errorf("All arguments are required in NewClient()")
} else {
fmt.Println(err.Error())
}
Интеграционные тесты работают непосредственно с тестовыми данными на PayPal Sandbox, проверяют ответы сервера и их преобразования в go-структуры.
Данный процесс представлен на схеме ниже:
Пример проверки ответа функции CreateDirectPaypalPayment:
c, _ := NewClient(testClientID, testSecret, APIBaseSandBox)
c.GetAccessToken()
amount := Amount{
Total: "15.11",
Currency: "USD",
}
p, err := c.CreateDirectPaypalPayment(amount, "http://example.com", "http://example.com", "test payment")
if err != nil || p.ID == "" {
t.Errorf("Test paypal payment is not created")
}
Мы создали тестовый аккаунт в песочнице PayPal и используем тестовые ID для каждого вида запроса. Например, на платеже с ID PAY-5YK922393D847794YKER7MUI можно тестировать получение информации о нем. Для того, чтобы сообщить клиенту, что вы работаете с Sandbox, вам необходимо установить базовый URL API (и после тестирования поменять его на Live URL):
c, err := paypalsdk.NewClient("clientID", "secretID", paypalsdk.APIBaseSandBox)
Тесты могут быть запущены локально командой go test, но нельзя быть всегда уверенным, что код в репозитории будет всегда стабильным. Поэтому мы используем Continuous Integration (CI) для автоматического запуска теста при каждом пуше в репозиторий. Мы используем TravisCI, он легко интегрируется с GitHub репозиторием, в корне нашего проекта лежит конфигурация .travis.yml:
language: go
go:
- 1.5
install:
- export PATH=$PATH:$HOME/gopath/bin
script:
- go test -v
Open Source и ближайшие планы
Все наши наработки вы можете найти на GitHub, они опубликованы под MIT лицнзией. В планах создать некую стандартную библиотеку для Go, обеспечить полное покрытие API (+webapps и т.д.)
Актуальную документацию можно найти на странице проекта в GitHub.
Ждем ваших коммитов и pull-реквестов на logpacker/PayPal-Go-SDK.
Автор: LogPacker