Быстрый старт
Введение
Эта инструкция — о том, как организовать проведение платежей через программный интерфейс 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_addressIP-адрес пользователя. В тестовых запросах можно указывать 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 — чтобы обеспечивать аутентификацию пользователей для проведения платежей с использованием платёжных карт;
- Отправка уведомлений пользователям — чтобы информировать пользователей через электронную почту о проведении платежей и других событиях.
Помимо этих процедур и возможностей можно настраивать и другие, описанные в разделах Вспомогательные процедуры и Дополнительные возможности.
Запуск
После реализации базовых функций, тестирования актуальных возможностей и настройки необходимых вам сценариев работы можно переходить к запуску рабочего проекта. Важно, чтобы к этому моменту были решены основные организационные вопросы. В таком случае вопросы технические сводятся к настройке свойств проекта на стороне платёжной платформы и к началу использования идентификатора и ключа рабочего проекта на стороне веб-сервиса.
Также после запуска можно продолжать настраивать работу с различными типами платежей, платёжными методами, процедурами и возможностями — с учётом ваших потребностей — и обращаться с вопросами и обратной связью к нашим специалистам.
Успехов!