Быстрый старт
Введение
Эта инструкция — о том, как организовать проведение платежей через программный интерфейс Gate платёжной платформы ecommpay. С тем, чтобы использовать свой пользовательский интерфейс и обеспечивать максимальное взаимодействие с пользователями на стороне веб-сервиса, а все взаимодействия с платёжной платформой выполнять на программном уровне, „под капотом“. И чтобы при этом можно было применять проверенные быстрые решения — с чёткими инструкциями и примерами кода на популярных языках программирования — PHP и Go.
В рамках этой инструкции рассматриваются действия, необходимые для проведения наиболее востребованного вида платежей — оплат в одну стадию с прямым использованием платёжных карт, — а также действия, необходимые для выполнения возвратов по таким оплатам. Поддержка этих функциональных возможностей, как правило, оптимальна для первичного тестирования и быстрого запуска платёжных проектов в работу. Кроме того, поддержавПоддержав эти возможности, можно довольно легко поддержать и любые другие, поскольку в основе работы с любыми типами платежей и платёжными методами, поддерживаемыми в платформе, лежат те же принципы и во многом те же процедуры, что и в основе работы с одностадийными карточными оплатами. Кроме того, эта инструкция может быть полезна, даже если вам не актуальны оплаты через Gate, но актуальны возвраты или выплаты.
Если вам не актуальны оплаты через Gate и требуется настроить лишь работу с возвратами или выплатами, можно освоить с помощью этой инструкции работу с подписью, ответами и оповещениями и перейти после этого к реализации соответствующих дополнений. Если актуально использовать Gate лишь для контроля состояния отдельных платежей, можно освоить здесь работу с подписью и перейти к статье Получение информации о состоянии платежа. И наконец, если актуально что-то принципиально другое, можно сориентироваться в соответствующих вариантах.
- Если надо настроить решения с использованием платёжной формы ecommpay на сайтах и в мобильных приложениях, можно обратиться к разделам Payment Page и Интеграция с использованием SDK.
- Если надо настроить оплаты с вызовом платёжной формы ecommpay по платёжным ссылкам, можно освоить с помощью этой инструкции работу с подписью, ответами и оповещениями и перейти после этого к соответствующей статье: Оплата по платёжной ссылке. Кроме того, оплаты по ссылкам можно инициировать и вручную, через интерфейс Dashboard, предназначенный для сотрудников мерчантов (подробнее).
- Если надо настроить получение через программный интерфейс информации об операциях, опротестованиях и балансах, следует обратиться к разделу о работе с интерфейсом Data API.
- Наконец, если актуально что-то ещё, можно обратиться к другим разделам настоящей документации и к специалистам ecommpay.
На этом с вводными всё. Можно переходить к делу.
Краткая теория
Проекты и ключи
Работу с платёжной платформой ecommpay можно сравнить с использованием услуг гостиницы. Так, для заселения в гостиницу обычно необходимо получить номер и ключ от него, а для начала работы с платформой надо получить... проект и ключ от него. И как с номерами в гостиницах, в платформе может предоставляться разное количество проектов для одного клиента — под разные цели и задачи — при этом для каждого проекта (как и для каждого гостиничного номера) необходим свой ключ.
Как правило, для работы достаточно одного тестового и одного рабочего проектов. Это типичный случай, и в рамках быстрого старта мы исходим из него. Если вам по какой-либо причине необходимо больше проектов, это стоит обсудить с курирующим менеджером, но начать всё также можно с одного тестового проекта.
Если у вас уже есть идентификатор тестового проекта (project_id
) и секретный ключ для него (secret_key
), можно приготовиться к их использованию и переходить дальше. Если же вы ещё не бронировали тестовый проект, самое время сделать это
через заявку на основном сайте компании и вернуться сюда.
Для начала работы с платформой следует получить проект и ключ от него. Если у вас уже есть идентификатор (project_id
) и ключ (secret_key
) тестового проекта, можно приготовиться к их использованию и переходить дальше. Если же вы ещё не оформляли тестовый проект, самое время это сделать
через заявку на основном сайте компании и вернуться сюда.
Схема работы
Чтобы корректно проводить платежи через Gate, необходимо обеспечить сбор актуальных параметров, формирование и отправку к платёжной платформе соответствующих запросов и приём и обработку ответной информации от платформы. При этом все взаимодействия с пользователями (для сбора и отображения релевантной информации) должны обеспечиваться на стороне веб-сервиса с использованием собственных решений, в то время как все остальные процедуры (для обработки используемой информации) можно реализовывать на основе представленных далее примеров кода.
Если сфокусировать внимание на технических аспектах для веб-сервиса и платёжной платформы, то проведение оплаты можно представить следующим образом.
В веб‑сервисе | В платёжной платформе | |
---|---|---|
1 | При готовности пользователя оплатить заказ получаем все необходимые данные, фиксируем параметры платежа и подписываем их, после чего формируем запрос на проведение платежа и отправляем его в платёжную платформу | – |
2 | Информируем пользователя о том, что платёж проводится | Принимаем запрос и работаем по нему, чтобы провести платёж. Если актуально, отправляем оповещение о необходимых действиях |
3 | Если актуально, принимаем оповещение о необходимых действиях, выполняем эти действия (с участием пользователя или без него) и отправляем в платёжную платформу запрос на продолжение платежа | – |
4 | Информируем пользователя о том, что платёж проводится | Если актуально, принимаем дополнительный запрос и выполняем необходимые действия для продолжения платежа, после чего отправляем к веб-сервису оповещение о результате платежа |
5 | Принимаем оповещение с информацией о результате платежа и отображаем пользователю необходимые сведения | – |
При работе с платежами других типов выполняемые действия могут отличаться, но в целом схема соответствует приведённой. Реализовывать её на стороне веб-сервиса можно самыми разными способами. Здесь, в рамках быстрого старта, описываются базовые процедуры, которые можно использовать и адаптировать в соответствии со спецификой вашего веб-сервиса.
Параметры запросов
Обязательный набор параметров, необходимых для проведения конкретного платежа, может отличаться в зависимости от типа этого платежа, специфики платёжного метода и платёжной системы, региональных особенностей и других факторов. Так, в каких-то случаях может требоваться указывать назначение платежа, адрес пользователя или другую информацию, в то время как в других случаях эти сведения могут быть необязательны. Поэтому при настройке работы с разными типами платежей и платёжными методами следует обращаться к соответствующим статьям настоящей документации и к спецификации Gate API.
Обязательный набор параметров, необходимых для проведения конкретного платежа, может отличаться в зависимости от различных факторов, поэтому при настройке работы с разными типами платежей и платёжными методами следует обращаться к соответствующим статьям настоящей документации и к спецификации Gate API.
Для проведения карточной оплаты в базовом случае следует определиться с её суммой и валютой, добавить к этим двум параметрам три идентификатора (проекта, платежа и пользователя) и данные платёжной карты, а затем сформировать подпись к этим параметрам.
Если данные карты указываются в явном виде, то к числу обязательных параметров относятся следующие.
Параметр | Описание |
---|---|
general — объект, содержащий параметры с основными идентификационными сведениями запроса |
|
|
Идентификатор проекта. Его вместе с ключом выдаёт ecommpay, и его важно точно указывать даже в тестовых запросах. Иначе… стоит ждать реакцию, как при попытке зайти в чужой гостиничный номер, полученный от ecommpay. |
|
Идентификатор платежа. Он может быть произвольным, но каждый раз должен быть уникальным в рамках используемого проекта. Иначе стоит ждать ошибку обращения, формируемый на стороне веб-сервиса. |
|
Подпись к параметрам запроса. Она формируется в соответствии со специальным алгоритмом, описанным далее. При этом для тестового проекта следует использовать тестовый ключ, для рабочего — рабочий, формируемая описанным далее способом. |
payment — объект, содержащий параметры с основными сведениями о платеже |
|
|
Сумма платежа. В тестовых запросах может быть произвольной, а в реальных должна точно соответствовать сумме заказа. Приводится, в дробных единицах валюты. |
|
Код валюты платежа. Приводится, в трёхбуквенном формате ISO 4217 alpha-3. В тестовых запросах могут использоваться любые из действующих кодов, а в реальных каждый раз должен использоваться код той валюты, в которой инициируется платёж. Для сверки можно использовать справочник валют. |
customer — объект, содержащий параметры с основными сведениями о пользователе |
|
|
Идентификатор пользователя в веб-сервисе. Может быть произвольным и повторяемым в разных запросах, но для каждого реального пользователя должен быть однозначно сопоставляемым с его учётной записью в веб-сервисе и уникальным в рамках проекта. Иначе возможны различные коллизии, в том числе при оценке рисков проведения платежей. |
ip_address |
IP-адрес пользователя. В тестовых запросах можно указывать IP-адрес мерчанта, в рабочих запросах каждый раз должен указываться IP-адрес, с которого пользователь инициирует оплату. |
card — объект, содержащий параметры со сведениями о платёжной карте пользователя |
|
|
Номер платёжной карты. В тестовых запросах можно указывать любые реалистичные значения, в том числе специальные, приведённые далее; в рабочих запросах должны использоваться реальные данные. |
|
Порядковый номер года, в котором истекает срок действия карты, в формате |
|
Порядковый номер месяца, в котором истекает срок действия карты, в виде числа от 1 до 12. В тестовых запросах можно указывать произвольные значения, но в корректном формате и так, чтобы срок действия был актуален; в рабочих запросах должны использоваться реальные данные. |
|
Имя пользователя, в соответствии с указанным на карте и с учётом применяемых ограничений. В тестовых запросах можно указывать произвольные значения; в рабочих запросах должны использоваться реальные данные. |
|
Код проверки подлинности карты, в соответствии с указанным на карте или полученным держателем от эмитента. В тестовых запросах можно указывать произвольные значения из трёх цифр; в рабочих запросах должны использоваться реальные данные. |
В запросе такой набор параметров может выглядеть следующим образом.
{ "general": { "project_id": 42, "payment_id": "Cosmoshop_purchase_2025-01-01_000001", "signature": "rdCiqlibt8SUMe3OVPNfKMYnQjQ6dkEvRQhMkJRg9ZJULdsKJEZU21E5Y/ISdv0FtXi3oJE5n4hSNK3Owo7Axw==" }, "customer": { "ip_address": "248.121.176.220", "id": "17008" }, "payment": { "amount": 8855, "currency": "USD" }, "card": { "pan": "4242424242424243", "year": 2025, "month": 5, "card_holder": "SONYA KOVALEVSKY", "cvv": "123" } }
Вместе с тем, при проведении платежей могут требоваться и другие параметры:
- Параметры, необходимые в различных случаях, исходя из специфики конкретных платёжных систем и региональных особенностей. Если такие параметры не были переданы изначально, платёж может быть отклонён либо эти параметры могут быть запрошены в процессе его проведения.
- Параметры, необходимые в различных случаях, исходя из специфики веб-сервиса, например при выполнении аутентификации 3‑D Secure на стороне веб-сервиса или для отправки уведомления о результате платежа пользователю. Если такие параметры не были переданы, то соответствующие возможности могут не поддерживаться.
Для работы с такими случаями может потребоваться настроить дополнительные процедуры. Они частично затронуты далее, а также отдельно разобраны в разделе Дополнения, к которому лучше переходить после настройки и тестирования всех базовых процедур.
В целом, сбор всех необходимых параметров можно настраивать как удобно, с оглядкой на архитектуру вашего веб-сервиса и иные факторы (например, с задействованием используемых справочников и баз данных). Здесь же, определившись с обязательными параметрами, можно приступать к реализации.
Базовая реализация
Введение
Реализовывать функции веб-сервиса для проведения платежей можно по-разному, в том числе за счёт создания своих программных решений. В рамках этого быстрого старта мы рассматриваем реализацию с использованием готового кода от ecommpay для подписывания данных, отправки запросов, приёма ответов на эти запросы и программных оповещений, и, вместе с тем, с использованием ваших решений на стороне веб-сервиса для всех остальных действий, включая получение информации от пользователей и предоставление им информации о проведении платежа.
Подписывание данных
Когда для всех необходимых параметров определены их значения (и только в таком случае), можно формировать подпись к ним и готовить запрос.
*/ class Signer { const ITEMS_DELIMITER = ';'; const ALGORITHM = 'sha512'; const IGNORED_KEYS = ['frame_mode']; /** * Secret key * * @var string */ private $secretKey; /** * __construct * * @param string $secretKey */ public function __construct(string $secretKey) { $this->secretKey = $secretKey; } /** * Check signature * * @param array $params * @param string $signature * @return boolean */ public function check(array $params, string $signature): bool { return $this->sign($params) === $signature; } /** * Return signature * * @param array $params * @return string */ public function sign(array $params): string { $stringToSign = implode(self::ITEMS_DELIMITER, $this->getParamsToSign($params, self::IGNORED_KEYS)); return base64_encode(hash_hmac(self::ALGORITHM, $stringToSign, $this->secretKey, true)); } /** * Get parameters to sign * * @param array $params * @param array $ignoreParamKeys * @param string $prefix * @param bool $sort * @return array */ private function getParamsToSign( array $params, array $ignoreParamKeys = [], string $prefix = '', bool $sort = true ): array { $projectId = 42; $paymentId = '12345'; $customerId = '123'; $ip = '192.168.1.1'; $paymentAmount = '1000'; $paymentCurrency = 'USD'; $cardPan = '*4567'; $cardYear = '2029'; $cardMonth = '09'; $cardHolder = 'SONYA KOVALEVSKY'; $cardCvv = '123'; $paramsToSign = [ 'general' => [ 'project_id' => $projectId, 'payment_id' => $paymentId, ], 'customer' => [ 'id' => $customerId, 'ip_address' => $ip, ], 'payment' => [ 'payment_amount' => $paymentAmount, 'payment_currency' => $paymentCurrency, ], 'card' => [ 'pan' => $cardPan, 'year' => $cardYear, 'month' => $cardMonth, 'card_holder' => $cardHolder, 'cvv' => $cardCvv, ], ]; foreach ($params as $key => $value) { if (in_array($key, $ignoreParamKeys, true)) { continue; } $paramKey = ($prefix ? $prefix . ':' : '') . $key; if (is_array($value)) { $subArray = $this->getParamsToSign($value, $ignoreParamKeys, $paramKey, false); $paramsToSign = array_merge($paramsToSign, $subArray); } else { if (is_bool($value)) { $value = $value ? '1' : '0'; } else { $value = (string)$value; } $paramsToSign[$paramKey] = $paramKey . ':' . $value; } } if ($sort) { ksort($paramsToSign, SORT_NATURAL); } return $paramsToSign; } }
package main
import (
"crypto/hmac"
"crypto/sha512"
"encoding/base64"
"sort"
)
const itemsDelimiter = ";"
var ignoredKeys = map[string]struct{}{"frame_mode": struct{}{}}
var secretKey string
func SetSecretKey(key string) {
secretKey = key
}
func Check(params map[string]map[string]string, signature string) bool {
return Sign(params) == signature
}
func Sign(params map[string]map[string]string) string {
var stringToSign string
paramsToSign := getParamsToSign(params, ignoredKeys, "", true)
for _, value := range paramsToSign {
for _, v := range value {
stringToSign = stringToSign + itemsDelimiter + v
}
}
mac := hmac.New(sha512.New, []byte(secretKey))
mac.Write([]byte(stringToSign))
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}
func getParamsToSign(params map[string]map[string]string, ignoreParamKeys map[string]struct{},
prefix string, sorted bool) map[string]map[string]string {
var paymentId, customerId, ip, amount, currency, cardPan, cardYear, cardMonth, cardHolder, cardCvv string
paramsToSign := map[string]map[string]string{
"general": {
"project_id": projectId,
"payment_id": paymentId,
},
"customer": {
"id": customerId,
"ip_address": ip,
},
"payment": {
"payment_amount": amount,
"payment_currency": currency,
},
"card": {
"pan": cardPan,
"year": cardYear,
"month": cardMonth,
"card_holder": cardHolder,
"cvv": cardCvv,
},
}
for key, value := range params {
for k, v := range value {
if _, ok := ignoreParamKeys[k]; ok {
continue
}
paramKey := k
if prefix != "" {
paramKey = prefix + ":" + k
}
paramsToSign[key][paramKey] = paramKey + ":" + v
}
}
if sorted == true {
return sortParams(paramsToSign)
}
return paramsToSign
}
func sortParams(params map[string]map[string]string) map[string]map[string]string {
var sortedParams map[string]map[string]string
keys := make([]string, 0, len(params))
for kp := range params {
keys = append(keys, kp)
}
sort.Strings(keys)
for _, ks := range keys {
sortedParams[ks] = params[ks]
}
return sortedParams
}
Отправка запросов и приём ответов на них
Когда все данные собраны и подписаны, можно отправлять запрос к требуемой конечной точке (их перечень представлен в спецификации Gate API). В нашем случае это /v2/payment/card/sale. При приёме запроса (как правило, в течение 100 мс) со стороны платёжной платформы к веб-сервису направляетсяПосле этого стоит ждать от платёжной платформы синхронный HTTP-ответ — с информацией о приёме запроса в обработку или об ошибках, из-за которых запрос не был принят.
В ответах от платформы используются следующие коды:
200 OK
— запрос принят в обработку. В таком случае можно ждать дальнейших оповещений о проведении платежа. Работу с такими оповещениями рассматриваем уже в следующем разделе.400 Bad Request
— запрос не принят из-за того, что в нём не указан по крайней мере один из обязательных параметров или указана некорректная подпись. В таком случае можно дополнить запрос недостающими параметрами и обновить подпись (или просто обновить подпись, проверив при этом корректность используемых идентификатора и ключа проекта) и повторить отправку запроса.403 Forbidden
— запрос не принят из-за отказа в доступе к конечной точке. В таком случае можно обратиться к специалистам технической поддержки ecommpay для добавления IP-адреса отправителя в список доверенных адресов.422 Unprocessable Entity
— запрос не принят из-за синтаксической ошибки (например, из-за пропущенной запятой). В таком случае можно исправить ошибку и повторить отправку запроса.500 Internal Error
— запрос не принят из-за сбоя в платёжной платформе. В таком случае можно повторить отправку запроса позднее.
Чтобы получать информацию из ответов от платформы, следует настроить их приём и обработку.
$headers = []; $headerCallback = function ($curl, $header_line) use (&$headers) { if (strpos($header_line, ":") === false) { return strlen($header_line); } list($key, $value) = explode(":", trim($header_line), 2); $headers[trim($key)] = trim($value); return strlen($header_line); }; $absUrl = 'https://abs.url'; $curl = curl_init(); $opts[CURLOPT_URL] = $absUrl; $opts[CURLOPT_RETURNTRANSFER] = true; $opts[CURLOPT_CONNECTTIMEOUT] = $this->connectTimeout; $opts[CURLOPT_TIMEOUT] = $this->timeout; $opts[CURLOPT_HEADERFUNCTION] = $headerCallback; curl_setopt_array($curl, $opts); $rbody = curl_exec($curl); $errno = curl_errno($curl); if ($rbody === false) { $errno = curl_errno($curl); $message = curl_error($curl); curl_close($curl); $this->handleCurlError($absUrl, $errno, $message); } $rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl); echo "http code = ".$rcode."\n"; echo "http response = ".$rbody."\n";
package main
import (
"fmt"
"net/http"
"time"
)
type conf struct {
connectTimeout, timeout time.Duration
returnTransfer bool
}
func main() {
absUrl := "https://abs.url"
var cf = conf{10 * time.Second, 20 * time.Second, true}
req, err := http.NewRequest(http.MethodGet, absUrl, nil)
if err != nil {
fmt.Printf("client: could not create request: %s\n", err)
return
}
client := http.Client{
Timeout: cf.timeout,
}
res, err := client.Do(req)
if err != nil {
fmt.Printf("client: error making http request: %s\n", err)
handleError(err)
return
}
fmt.Printf("http code = %d\n", res.StatusCode)
fmt.Printf("http response = %s\n", res.Body)
}
Приём оповещений и отправка ответов на них
В рамках проведения отдельного платежа от платформы к веб-сервису могут отправляться предписывающие и уведомительные оповещения.
Предписывающие оповещения требуют отправки каких-либо сведений в платёжную платформу, предоставления определённой информации пользователю, перенаправления его к сторонним сервисам или выполнения иных действий. Это промежуточные оповещения, на которые необходимо своевременно реагировать для корректного проведения платежей.
Уведомительные оповещения позволяют оперативно узнавать о результатах платежей и получать другую значимую информацию. Эту информацию можно использовать для оперативного обновления статусов заказов в веб-сервисе, информирования пользователей и других целей — в соответствии с моделью работы вашего сервиса. Уведомительные оповещения могут быть как промежуточными, с информацией о значимых событиях при проведении платежей, так и итоговыми, с информацией о результатах. И эту информацию можно использовать по мере необходимости.
Для приёма оповещений (как предписывающих, так и уведомительных) следует:
- Определить и задать в платёжной платформе адрес, предназначенный для приёма в веб-сервисе оповещений по проекту. Для этого можно открыть раздел Проекты интерфейса Dashboard и использовать инструменты на вкладке Оповещения.
- Настроить проверку целостности и разбор оповещений, ожидаемых по указанному адресу, так, чтобы не использовать в работе оповещения с некорректной подписью. Для такой настройки можно использовать приведённый далее пример кода.
- Настроить отправку синхронных HTTP-ответов о приёме оповещений:
200 ОК
, если подпись корректна, и400 Bad Request
, если подпись некорректна.
{ "account": { "number": "424242******4243", "token": "f365bb1729f9b72fd9c09703a751c979f3becc679f29c3e35c91d18070d15654", "type": "visa", "card_holder": "SONYA KOVALEVSKY", "id": 45678, "expiry_month": "05", "expiry_year": "2025" }, "customer": { "id": "17008", }, "payment": { "date": "2023-01-11T13:02:42+0000", "id": "Cosmoshop_purchase_2025-01-01_000001", "method": "card", "status": "success", "sum": { "amount": 8855, "currency": "USD" }, "type": "purchase", "description": "" }, "project_id": 42, "operation": { "id": 969000002636, "type": "sale", "status": "success", "date": "2023-01-11T13:02:42+0000", "created_date": "2023-01-11T13:01:45+0000", "request_id": "c6eed1eb14c629b4ef20b3b8086d...d04132c34b0088cbc0be4667c", "sum_initial": { "amount": 8855, "currency": "USD" }, "sum_converted": { "amount": 8855, "currency": "USD" }, "provider": { "id": 408, "payment_id": "330157196", "date": "2023-01-11T13:02:32+0000", "auth_code": "", "endpoint_id": "612266625" }, "code": "0", "message": "Success", "eci": "07" }, "signature": "v7KNMpfogAxwRIL9tVftZ1ZZ5D/aZAeb0VMdeR+CqGrNxYyilUwSm...==" }
require_once __DIR__ . 'signature.php'; //@todo net set to merchant project https://api.merchant.com/callback.php //@todo set projectId $projectId = null; //@todo set paymentId $paymentId = ''; //@todo set secretKey $secretKey = ''; $response = json_decode(file_get_contents('php://input'), true); $rsignature = $response['signature']; unset($response['signature']); if ((new Signer($secretKey))->check($response, $rsignature)) { header('HTTP/1.1 200 OK'); header('Status: 200 OK'); echo "signature is correct\n"; $projectId = $response['project_id']; $paymentId = $response['payment']['id']; //@todo save necessary info to the merchant system } else { header('Status: 400 Bad Request'); header('HTTP/1.1 400 Bad Request'); echo "signature is invalid\n"; }
package main
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
)
//@todo net set to merchant project https://api.merchant.com/callback.go
//@todo set project
type project struct{}
type request struct {
Signature string `json:"signature"`
}
var prj project
//@todo set secretKey
var secretKey string
//@todo set paymentId
var paymentId string
//@todo get request
var r *http.Request
func main() {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Error reading body: %v", err)
}
var req request
err = json.Unmarshal(body, &req)
if err != nil {
log.Printf("Error parsing request: %v", err)
}
var w http.ResponseWriter
if checkSignature(req, secretKey, req.Signature) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("signature is correct\n"))
} else {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("signature is invalid\n"))
}
}
func checkSignature(req request, secretKey string, signature interface{}) bool {
}
На этом можно остановиться и выполнить минимальное тестирование либо продолжить разбираться со спецификой реагирования на различные оповещения и перейти к тестированию после.
Реагирование на оповещения
Общая информация
Для реагирования на уведомительные оповещения, если это актуально, можно настроить использование необходимой информации из таких оповещений в соответствии со спецификой веб-сервисапри её получении.
Для реагирования на предписывающие оповещения обязательно следует разобраться с выполнением действий, которые могут быть необходимы для проведения платежа.
Вместе с тем, настроить реагирование на оповещения можно и позже, после реализации основных действий и их тестирования.
К действиям, необходимым при получении предписывающих оповещений, могут относиться:
- Сбор дополнительных сведений о платеже и их отправка в платформу.
В таких случаях в оповещениях передаётся объект
clarification_fields
со списком параметров, которые необходимо отправить в последующем запросе к платёжной платформе. Как правило, запрашивается информация о пользователе: его имя и фамилия, дата рождения, платёжный адрес и иные подобные сведения.Рис. 10. Пример данных из оповещения со списком запрашиваемых параметров "clarification_fields":{ "avs_data": [ "avs_post_code", "avs_street_address" ] }
Сбор требуемых сведений в таких ситуациях можно осуществлять любым удобным способом, в том числе из имеющейся базы данных или через заполнение пользователем соответствующих полей в интерфейсе веб-сервиса. Подробнее о работе с такими оповещениями — в разговоре о дополнениях, после первичной настройки и тестирования. Здесь же можно отметить, что такие ситуации бывают и требуют оперативного решения на стороне веб-сервиса.
- Перенаправление пользователя к стороннему сервису.
В таких случаях оповещения, как правило, содержат объект
acs
илиredirectData
с адресом страницы сервиса, к которому необходимо перенаправить пользователя, и дополнительными сведениями. При получении таких оповещений следует перенаправлять пользователей по указанным адресам. Это можно делать с помощью HTML-форм.Рис. 11. Пример данных из оповещения с адресом для перенаправления пользователя "acs":{ "pa_req":"eJxVUtluwyAQ/BUrH2DA...n8/4htjT7Em", "acs_url":"https://example.com/ACS", "md":"eyJfto7jg456ZCI6IiJ9" }
Рис. 12. Пример скрипта для перенаправления пользователя <form id="3dsform" action="https://example.com/ACS" method="post" enctype="application/ x-www-form-urlencoded"> <input type="hidden" name="PaReq" value="eJxVUtluwyAQ/BUrH2DA...n8/4htjT7Em" /> <input type="hidden" name="TermUrl" value="http://example.com/termurl" /> <input type="hidden" name="MD" value="eyJfto7jg456ZCI6IiJ9" /> </form> <script type="text/javascript"> setTimeout(function() { document.getElementById("3dsform").submit(); }, 1000); </script>
После перенаправления пользователя согласно полученному оповещению может понадобиться принять какую-либо дополнительную информацию от целевого сервиса (например, от сервиса Access Control Server при аутентификации 3‑D Secure) или ожидать следующего оповещения от платформы — в соответствии со схемой проведения платежа и спецификой используемого метода. Подробнее о работе с такими оповещениями — в разговоре о дополнениях, после первичной настройки и тестирования.
- Отображение пользователю какой-либо информации.
В таких случаях оповещения, как правило, содержат объект
display_data
с информацией, которую необходимо отобразить пользователю (например, в виде текста или QR-кода), и дополнительными сведениями.Рис. 13. Пример данных из оповещения с информацией для отображения пользователю "display_data": [ { "type": "qr_data", "title": "QR Code", "data": "https://example.com/paygate/union/pay/MDAyMDUxNjk0OTMwNjYyMTQzMTgwOHwwNHw0ODgxMDAwMA==" }, { "type": "add_info", "title": "QR Code Timeout", "data": "600" } ]
Такие оповещения могут быть актуальны для платежей с использованием различных альтернативных платёжных методов. Работа с этими методами рассматривается в разделе Платёжные методы, и знакомиться с ней стоит после первичной настройки и тестирования.
Работа с карточными оплатами
При работе с карточными оплатами наиболее актуальными можно считать промежуточные оповещения для выполнения аутентификации 3‑D Secure. В таких оповещениях, как правило, содержатся сведения для перенаправления пользователя напрямую к сервису эмитента (если в оповещении получен объект acs
) или к сервису провайдера (если получен объект redirectData
). Перенаправить пользователя на требуемую страницу можно с помощью такой же HTML-формы, которая была представлена ранее (и повторена здесь).
<form id="3dsform" action="https://example.com/ACS" method="post" enctype="application/ x-www-form-urlencoded"> <input type="hidden" name="PaReq" value="eJxVUtluwyAQ/BUrH2DA...n8/4htjT7Em" /> <input type="hidden" name="TermUrl" value="http://example.com/termurl" /> <input type="hidden" name="MD" value="eyJfto7jg456ZCI6IiJ9" /> </form> <script type="text/javascript"> setTimeout(function() { document.getElementById("3dsform").submit(); }, 1000); </script>
При возвращении пользователя от сервиса эмитента следует принять от этого сервиса информацию о результате аутентификации, отправить эту информацию в HTTP-POST-запросе к платформе и принять синхронный HTTP-ответ. В случае возвращения пользователя от сервиса провайдера такая информация не передаётся, и со стороны веб-сервиса следует ожидать от платформы оповещение с информацией о результате платежа.
require_once __DIR__ . 'signature.php'; //@todo set projectId $projectId = null; //@todo set paymentId $paymentId = ''; //@todo set secretKey $secretKey = ''; $params = [ 'general' => [ 'project_id' => $projectId, 'payment_id' => $paymentId, ], "pares" => "", "md" => "" ]; $absUrl = 'https://api.ecommpay.com/v2/payment/card/3ds_result'; $params['general']['signature'] = (new Signer($secretKey))->sign($params); $request = json_encode($params); $curl = curl_init(); $opts = []; $opts[CURLOPT_POST] = 1; $opts[CURLOPT_POSTFIELDS] = $request; $opts[CURLOPT_HTTPHEADER] = ['Content-Type: application/json', 'Content-Length: '.strlen($request)]; $headers = []; $headerCallback = function ($curl, $header_line) use (&$headers) { if (strpos($header_line, ":") === false) { return strlen($header_line); } list($key, $value) = explode(":", trim($header_line), 2); $headers[trim($key)] = trim($value); return strlen($header_line); }; $opts[CURLOPT_URL] = $absUrl; $opts[CURLOPT_RETURNTRANSFER] = true; $opts[CURLOPT_CONNECTTIMEOUT] = $this->connectTimeout; $opts[CURLOPT_TIMEOUT] = $this->timeout; $opts[CURLOPT_HEADERFUNCTION] = $headerCallback; curl_setopt_array($curl, $opts); $rbody = curl_exec($curl); $errno = curl_errno($curl); if ($rbody === false) { $errno = curl_errno($curl); $message = curl_error($curl); curl_close($curl); $this->handleCurlError($absUrl, $errno, $message); } $rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl); echo "http code = ".$rcode."\n"; echo "http response = ".$rbody."\n";
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
type general struct{ project_id, payment_id, signature string }
type params struct {
gnrl general
pares, md string
}
type conf struct {
connectTimeout, timeout time.Duration
}
func main() {
//@todo set project
project := ""
//@todo set secretKey
var secretKey string
//@todo set paymentId
var paymentId string
var cf = conf{10 * time.Second, 20 * time.Second}
prms := params{
gnrl: general{project_id: project, payment_id: paymentId, signature: ""},
pares: "",
md: "",
}
signature := sign(prms, secretKey)
prms.gnrl.signature = signature
absUrl := "https://api.ecommpay.com/v2/payment/card/3ds_result"
reqBody, err := json.Marshal(prms)
bodyReader := bytes.NewReader(reqBody)
req, err := http.NewRequest(http.MethodPost, absUrl, bodyReader)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Content-Length", string(rune(bodyReader.Len())))
client := http.Client{
Timeout: cf.timeout,
}
res, err := client.Do(req)
if err != nil {
fmt.Printf("client: error making http request: %s\n", err)
handleError(err)
os.Exit(1)
}
defer res.Body.Close()
fmt.Printf("http code = %d\n", res.StatusCode)
fmt.Printf("http response = %s\n", res.Body)
}
func sign(par params, secretKey string) string {
//@todo implement sign function
}
Более подробно работа с аутентификацией 3‑D Secure рассматривается в отдельной статье, к которой можно обращаться уже после тестирования первичного решения.
Тестирование
Когда работа с подписью, ответами и оповещениями организована, можно приступать к тестированию проведения платежей. При работе с тестовым проектом можно использовать два вида платёжных реквизитов: специальные тестовые, позволяющие тестировать заданные сценарии работы, и произвольные реалистичные, позволяющие дополнительно проверять работу с платежами в разных случаях.
В базовом случае можно использовать следующие тестовые номера платёжных карт (для проведения платежей по заданным кратчайшим сценариям).
Эмулируемый сценарий | Конечный статус платежа | |
---|---|---|
Платёж проведён | Платёж отклонён | |
Оплата без аутентификации 3‑D Secure | 4000 0000 0000 0077 |
4111 1111 1111 1111 |
Оплата с аутентификацией 3‑D Secure | 4314 2200 0000 0056 |
5544 3300 0000 0045 |
Для более масштабного тестирования можно использовать расширенный набор тестовых данных для карточных и различных альтернативных платежей, а также произвольные данные, включая реквизиты реальных карт, кошельков и других платёжных инструментов. Такой набор позволяет тестировать различные сценарии проведения платежей, например с выполнением аутентификации 3‑D Secure без каких-либо действий пользователя. И это является безопаснымЭто безопасно, поскольку для всех данных в тестовой среде обеспечивается тот же уровень защиты, что и в рабочей, но реальные платежи при этом не проводятся.
По итогам тестирования можно считать реализованными базовые функции по проведению платежей и при желании переходить дальше — к различным дополнениям, которые могут быть полезны уже на первых порах работы с платёжной платформой, и к запуску решения в работу.
Дополнения
Общий контроль проведения платежей
После проведения нескольких тестовых платежей можно разобраться с тем, как контролировать ситуацию по ним. Для общего контроля платежей в платформе предусмотрены пользовательский интерфейс Dashboard и программный интерфейс Data API. С их помощью можно получать различными способами сводную информацию о суммах, статусах и других атрибутах проводимых платежей, но с задержкой вплоть до нескольких минут. Для работы с этими интерфейсами необходимо предварительно получить доступ к интерфейсу Dashboard, настроить права доступа к текстовому проекту и, при необходимости работы с Data API, сформировать соответствующие токен и секретный ключ. После этого можно переходить к работе с информацией о платежах.
В интерфейсе Dashboard для контроля состояния платежей предусмотрены раздел Платежи (с информацией о платежах всех типов) и специализированные разделы с информацией о платежах различных типов, а также карточки с информацией об отдельных платежах. Справочная информация об использовании этих разделов представлена в соответствующем разделе документации.
В Data API предусмотрены различные конечные точки, через запросы к которым можно получать информацию о группах платежей или отдельных платежах. Работа с такими запросами описана в соответствующем разделе документации.
Оперативный контроль состояния отдельных платежей
Чтобы получать оперативную информацию о состоянии отдельных платежей через Gate API, следует использовать HTTP-POST-запросы к конечной точке /v2/payment/status (с указанием идентификаторов проекта и платежа) и принимать синхронные HTTP-ответы со сведениями по этим запросам на них.
require_once __DIR__ . 'signature.php'; //@todo set projectId $projectId = null; //@todo set paymentId $paymentId = ''; //@todo set secretKey $secretKey = ''; $params = [ 'general' => [ 'project_id' => $projectId, 'payment_id' => $paymentId, ] ]; $absUrl = 'https://api.ecommpay.com/v2/payment/status'; $params['general']['signature'] = (new Signer($secretKey))->sign($params); $request = json_encode($params); $curl = curl_init(); $opts = []; $opts[CURLOPT_POST] = 1; $opts[CURLOPT_POSTFIELDS] = $request; $opts[CURLOPT_HTTPHEADER] = ['Content-Type: application/json', 'Content-Length: '.strlen($request)]; $headers = []; $headerCallback = function ($curl, $header_line) use (&$headers) { if (strpos($header_line, ":") === false) { return strlen($header_line); } list($key, $value) = explode(":", trim($header_line), 2); $headers[trim($key)] = trim($value); return strlen($header_line); }; $opts[CURLOPT_URL] = $absUrl; $opts[CURLOPT_RETURNTRANSFER] = true; $opts[CURLOPT_CONNECTTIMEOUT] = $this->connectTimeout; $opts[CURLOPT_TIMEOUT] = $this->timeout; $opts[CURLOPT_HEADERFUNCTION] = $headerCallback; curl_setopt_array($curl, $opts); $rbody = curl_exec($curl); $errno = curl_errno($curl); if ($rbody === false) { $errno = curl_errno($curl); $message = curl_error($curl); curl_close($curl); $this->handleCurlError($absUrl, $errno, $message); } $rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE); curl_close($curl); echo "http code = ".$rcode."\n"; echo "http response = ".$rbody."\n"; $response = json_decode($rbody, true); $rsignrature = $response['signature']; unset($response['signature']); if ((new Signer($secretKey))->check($response, $rsignrature)) { echo "signature is correct\n"; } else { echo "signature is invalid\n"; }
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
"time"
)
//@todo net set to merchant project https://api.merchant.com/callback.go
type general struct{ project_id, payment_id, signature string }
type params struct{ gnrl general }
type response struct {
Signature string `json:"signature"`
}
func main() {
//@todo set project
project := ""
//@todo set secretKey
var secretKey string
//@todo set paymentId
var paymentId string
prms := params{
gnrl: general{project_id: project, payment_id: paymentId, signature: ""},
}
signature := sign(prms, secretKey)
prms.gnrl.signature = signature
absUrl := "https://api.ecommpay.com/v2/payment/status"
reqBody, err := json.Marshal(prms)
bodyReader := bytes.NewReader(reqBody)
req, err := http.NewRequest(http.MethodPost, absUrl, bodyReader)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Content-Length", string(rune(bodyReader.Len())))
res, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Printf("client: error making http request: %s\n", err)
handleError(err)
os.Exit(1)
}
defer res.Body.Close()
fmt.Printf("http code = %d\n", res.StatusCode)
fmt.Printf("http response = %s\n", res.Body)
var respBody response
b, err := io.ReadAll(res.Body)
err = json.Unmarshal(b, &respBody)
if err != nil {
log.Printf("Error parsing response: %v", err)
}
rsign := respBody.Signature
if checkSignature(respBody, secretKey, rsign) {
fmt.Printf("signature is correct\n")
} else {
fmt.Printf("signature is invalid\n")
}
}
func sign(par params, secretKey string) string {
//@todo implement sign function
}
func checkSignature(resp response, secretKey string, signature interface{}) bool {
//@todo implement checkSignature function
}
При возникновении вопросов, касающихся получения информации о платежах через Gate, можно обращаться к соответствующей статье.
Подробнее о работе с такими запросами — в отдельной статье.
Возврат средств
Если по каким-либо из проведённых оплат необходимо выполнить возвраты, для этого можно использовать Dashboard и Gate.
При работе через Dashboard можно открывать карточки целевых оплат и использовать расположенную в них кнопку Возврат, а также применять пакетную отправку запросов через файлы (подробнее).
При работе через Gate следует использовать запросы к соответствующим конечным точкам (с учётом специфики платёжных методов). В случае карточных оплат это запросы к конечной точке /v2/payment/card/refund. Для более глубокого освоения работы с возвратами через Gate можно использовать отдельную статью.
В каждом запросе на возврат по карточной оплате необходимо указывать целевые идентификаторы проекта и платежа, а также описание причины возврата. Вместе с этими обязательными сведениями можно указывать также сумму и код валюты возврата, если возвратить необходимо только часть суммы. В таких случаях код валюты должен соответствовать исходному в платеже, иначе запрос на возврат отклоняется. Для более глубокого освоения работы с возвратами через Gate можно использовать отдельную статью.
{ "general": { "project_id": 42, "payment_id": "Cosmoshop_purchase_2025-01-01_000001", "signature": "of8k9xeKJ7KLTZYO56lCv+f1M0Sf/7eg==" }, "payment": { "description": "Item return" } }
{ "project_id":42, "payment":{ "id":"Cosmoshop_purchase_2025-01-01_000001", "type":"purchase", // Тип платежа — разовая оплата "status":"refunded", // Статус платежа после полного возврата "date":"2023-01-12T15:20:36+0000", "method":"card", "sum":{ "amount":0, // Актуальная сумма платежа после полного возврата "currency":"USD" // Код валюты платежа } }, "account":{ "number":"424242******4243", "token":"14c24c8a5384b413f11b2956a82ddaeea609ea49", "type":"visa", "card_holder":"SONYA KOVALEVSKY", "expiry_month":"05", "expiry_year":"2025" }, "customer":{ "id":"17008" }, "operation_description":"Item return", // Описание причины возврата "operation":{ "id":3861, "type":"refund", // Тип операции "status":"success", // Статус операции "date":"2023-01-12T15:21:00+0000", "created_date":"2023-01-12T15:20:58+0000", "request_id":"67a97cd6b14f1aa0543c81e18cd270b66-aadc6e790206d5-00038611", "sum_initial":{ "amount":8855, // Сумма возврата "currency":"USD" // Код валюты возврата (в соответствии с валютой платежа) }, "sum_converted":{ "amount":8855, "currency":"USD" }, "code":"0", "message":"Success", "provider":{ "id":414, "payment_id":"", "endpoint_id":414 } }, "signature":"of8k9xerKSK4XL1QFaDH3p9Mh0CIcjmOwSwKJ7KLTZYO56lCv+f1M0Sf/7eg==" }
Организация работы с другими типами платежей
Настроив проведение карточных оплат, можно настроить работу и с другими типами платежей и платёжными методами. Чтобы сориентироваться в том, как в платформе проводятся платежи разных типов, и настроить работу с необходимыми типами платежей и платёжными методами, можно использовать модель проведения платежей (с описанием поддерживаемых типов платежей и их статусов) и материалы о методах (с описанием специфики проведения платежей с использованием различных методов).
Использование вспомогательных процедур и дополнительных возможностей
При проведении платежей через платёжную платформу ecommpay могут применяться различные процедуры и возможности. Вспомогательные процедуры требуются в отдельных случаях для решения специфических задач, например по аутентификации пользователей. Когда такие процедуры актуальны, они обязательны для проведения платежей, поэтому важно уметь с ними работать. В свою очередь, дополнительные возможности не блокируют проведение платежей и могут выполняться по желанию мерчанта. Они рассчитаны на улучшение качества предоставляемого сервиса.
После настройки проведения оплат, описанных в этом быстром старте, может быть полезно настроить выполнение процедур и возможностей, актуальных в соответствии со спецификой вашего веб-сервиса и задействуемых платёжных методов. Среди них можно выделить следующие:
- Дополнение информации о платеже — чтобы своевременно предоставлять дополнительные данные, которые могут запрашиваться платёжными системами;
- Аутентификация 3‑D Secure — чтобы обеспечивать аутентификацию пользователей для проведения платежей с использованием платёжных карт;
- Отправка уведомлений пользователям — чтобы информировать пользователей через электронную почту о проведении платежей и других событиях.
Помимо этих процедур и возможностей можно настраивать и другие, описанные в разделах Вспомогательные процедуры и Дополнительные возможности.
Запуск
После реализации базовых функций, тестирования актуальных возможностей и настройки необходимых вам сценариев работы можно переходить к запуску рабочего проекта. Важно, чтобы к этому моменту были решены основные организационные вопросы. В таком случае вопросы технические сводятся к настройке свойств проекта на стороне платёжной платформы и к началу использования идентификатора и ключа рабочего проекта на стороне веб-сервиса.
Также после запуска можно продолжать настраивать работу с различными типами платежей, платёжными методами, процедурами и возможностями — с учётом ваших потребностей — и обращаться с вопросами и обратной связью к нашим специалистам.
Успехов!