Добавляем платежную систему FreeKassa в проект на Go
Привет! Хочу поделиться гайдом по интеграции FreeKassa в проект на Golang.
В данной статье будут рассмотрены:
Создание инвойса.
Обработка оповещения об успешной оплате.
Регистрация и создание магазина
Регистрируемся на https://freekassa.net.
После регистрации на странице вы увидеть кнопку "Добавить кассу":
Интерфейс главной страницы FreeKassa Нажимаем, чтобы создать кассу (магазин).
В открывшемся окне выбираем тип нашего магазина, в моем случае - это TG-бот. После жмем продолжить:
Название сайта - это то, что будет видеть пользователь на странице оплаты.
Далее потребуется какое-то время на активацию созданной кассы. Если вы пишите новый проект, и он находится в процессе разработки, то необходимо будет сообщить об этом в поддержку, чтобы вам активировали ТЕСТОВЫЙ РЕЖИМ. Иначе касса активирована не будет.
После активации на главной странице вы увидите вашу кассу.
Настройка кассы
На главной странице переходим в настройки:
Вы увидите настройки вашего магазина (кассы):
Нас интересуют:
Секретные слова - необходимы для формирования подписей. Можете придумать их сами, либо воспользоваться автогенерацией.
URL оповещения - это url, на который FreeKassa будет отправлять callback в случае успешной оплаты.
URL успешной оплаты - url, на который будет произведен редирект пользователя в случае успешного подтверждения оплаты от нас, то есть, если наш URL оповещения отдал 200.
URL возврата в случае неудачи - url, на который будет произведен редирект пользователя в случае неуспешного подтверждения платежа от нас.
ВАЖНО: все URL должны быть "на домене не ниже второго уровня". Поэтому наличие DNS у сервера обязательно, не получится отправить запрос на что-то вроде: http://47.99.123.89:8080/callback.
URL успешной и неуспешной оплаты
С телеграмм-ботом все просто - они банально не нужны. Пользователь в любом случае будет перенаправлен на https://t.me/ваш_бот.
Если у вас не ТГ-бот, можно сделать 2 базовых странички, например:
/payments/success - успешный платеж.
/payments/failure - неуспешный платеж.
К URL оповещения вернемся чуть позже, сейчас пока будем писать интеграцию.
Интеграция FreeKassa в Go
Ура! Наконец-то мы дошли до кода.
Подробная API-документация лежит тут.
Cоздание инвойса
Чтобы создать инвойс, необходимо отправить GET-запрос на https://pay.fk.money/.
Что принимает запрос можно посмотреть здесь. Я опишу минимально необходимые данные для создания инвойса. Данные передаются в query params:
Название | Семантика | Тип данных |
m | ID вашего магазина (merchant id) - можно посмотреть на главной странице | int |
o | ID заказа (order id) - формируем сами | string |
oa | Сумма заказа | int (в моем случае) |
currency | Валюта платежа (RUB,USD,EUR,UAH,KZT) | string |
s | Подпись - формируем сами | string |
us_<key> | Важная штука - payload. Формируем сами. Payload, отправленный в инвойсе, вернется нам на callback-метод (URL оповещения). Пример: мы отправили us_user_id=123, тогда на URL оповещения нам вернется us_user_id=123 | string |
Переходим непосредственно к написанию кода.
Создадим файл currency.go
:
package freekassa
type Currency string
const (
RUB Currency = "RUB"
USD Currency = "USD"
EUR Currency = "EUR"
UAH Currency = "UAH"
KZT Currency = "KZT"
)
func (c Currency) String() string {
return string(c)
}
Создадим файл payment.go
:
package freekassa
import (
"fmt"
"strings"
)
// Payment - данные о платеже.
type Payment struct {
OrderID string
Currency Currency
Amount int64
Signature string
Payload Payload
}
// Payload - key=value.
type Payload map[string]string
// Generate - преобразует Payload в query-parameters.
func (p Payload) Generate() string {
if p == nil {
return ""
}
builder := &strings.Builder{}
builder.WriteString("&")
for key, value := range p {
param := fmt.Sprintf("us_%s=%s&", key, value)
builder.WriteString(param)
}
return builder.String()[:builder.Len()-1]
}
Структура Payment
содержит минимально необходимый набор данных для создания инвойса.
Payload
- это наши us_key (s), которые будут отправляться вместе с инвойсом и потом возвращаться на наш URL-оповещения.
Для формирования подписей необходимо иметь функцию, которая будет возвращать MD5
хэш от переданной строки. Можно создать файлик utils.go
:
package freekassa
import (
"crypto/md5"
"encoding/hex"
)
func md5Hash(text string) string {
hash := md5.Sum([]byte(text))
return hex.EncodeToString(hash[:])
}
Осталось самое важное: создание самого инвойса. У нас почти все есть для этого. Ключевой момент - формирование подписи для инвойса и для подтверждения платежа в URL-оповещения:
Подпись в инвойсе - MD5-Хэш от строки:
"ID_магазина:Сумма_платежа:Секретное_слово_1:Валюта_платежа:Номер_заказа"
Подпись в URL оповещения - MD5-Хэш от строки:
"ID_магазина:Сумма_платежа:Секретное_слово_2:Номер_заказа"
Теперь оздадим файл freekassa.go
, где будет лежать основная логика:
package freekassa
import (
"fmt"
)
const InvoiceBaseURL = `https://pay.fk.money`
type Client interface {
GenerateInvoice(p *Payment) string
GenerateInvoiceSignature(amount int64, currency Currency, orderID string) string
GenerateConfirmSignature(amount int64, orderID string) string
}
type client struct {
MerchantID int64
SecretKey1 string
SecretKey2 string
}
func NewClient(merchantID int64, secretKey1 string, secretKey2 string) Client {
return &client{
MerchantID: merchantID,
SecretKey1: secretKey1,
SecretKey2: secretKey2,
}
}
func (c *client) GenerateInvoice(p *Payment) string {
if p == nil {
return ""
}
return fmt.Sprintf(
"%s/?m=%d&o=%s&oa=%d¤cy=%s&s=%s%s",
InvoiceBaseURL,
c.MerchantID,
p.OrderID,
p.Amount,
p.Currency.String(),
p.Signature,
p.Payload.Generate(),
)
}
func (c *client) GenerateInvoiceSignature(amount int64, currency Currency, orderID string) string {
signData := fmt.Sprintf("%d:%d:%s:%s:%s", c.MerchantID, amount, c.SecretKey1, currency.String(), orderID)
return md5Hash(signData)
}
func (c *client) GenerateConfirmSignature(amount int64, orderID string) string {
signData := fmt.Sprintf("%d:%d:%s:%s", c.MerchantID, amount, c.SecretKey2, orderID)
return md5Hash(signData)
}
Для создания инвойса все готово, можно попробовать его создать:
package main
import (
"fmt"
"github.com/zenorachi/freekassa-sdk-go"
)
func main() {
merchantID, key1, key2 := int64(123), "key1", "key2"
fk := freekassa.NewClient(merchantID, key1, key2)
order, amount := "test_order", int64(100)
invoice := fk.GenerateInvoice(&freekassa.Payment{
OrderID: order,
Currency: freekassa.RUB,
Amount: amount,
Signature: fk.GenerateInvoiceSignature(amount, freekassa.RUB, order),
Payload: map[string]string{
"user_id": "3493920",
},
})
fmt.Println(invoice)
}
Запустив код, получим наш инвойс: https://pay.fk.money/?m=123&o=test_order&oa=100¤cy=RUB&s=7e30da3dc8ef8498415f25e90c18cbb3&us_user_id=3493920.
Переходить по нему не стоит, данные тестовые, будет ошибка, что Merchant ID (ID магазина) не найден.
URL оповещения
Инвойс создан, время настраивать callback.
Создавать router и полноценный сервер в примерах я не буду, покажу сразу функцию-хэндлер и middleware, который стоит добавить.
Начнем с MW. В данном случае он нужен, чтобы проверять, с какого IP нам прилетел запрос. Об этом говорит официальная документация FK:
"Рекомендуем так же проверять IP сервера отправляющего Вам информацию, наши IP - 168.119.157.136, 168.119.60.227, 178.154.197.79, 51.250.54.238."
Пример middleware с использованием gin:
func middleware() gin.HandlerFunc {
whitelist := map[string]struct{}{
"168.119.157.136": {},
"168.119.60.227": {},
"178.154.197.79": {},
"51.250.54.238": {},
}
return func(c *gin.Context) {
if _, found := whitelist[c.ClientIP()]; !found {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"message": "access denied"})
return
}
c.Next()
}
}
Что отправляет FK на URL оповещения можно посмотреть тут. Я буду принимать сумму платежа, номер заказа, подпись и payload, который мы генерировали ранее.
Функция callback:
type confirmCallbackRequest struct {
Amount int64 `form:"AMOUNT" binding:"required"`
OrderID string `form:"MERCHANT_ORDER_ID" binding:"required"`
Sign string `form:"SIGN" binding:"required"`
UserID int64 `form:"us_user_id" binding:"required,gt=0"`
}
// Предполагается, что у вас уже есть структура Handler, в которой лежит клиент FK.
func (h *Handler) callback(c *gin.Context) {
var (
req confirmCallbackRequest
err error
)
// Маршаллим запрос в структуру
if err = c.ShouldBind(&req); err != nil {
c.Status(http.StatusBadRequest)
return
}
// Самое важное - проверяем подпись, которую нам прислала FreeKassa
if sign := h.fk.GenerateConfirmSignature(req.Amount, req.OrderID); sign != req.Sign {
c.Status(http.StatusForbidden)
return
}
// Здесь ваша логика (пополнение баланса, подписка и т.д.)
c.Status(http.StatusOK)
}
Заключение
Что остается? По сути - ничего, у нас все готово. Мы умеем создавать инвойс, умеем обрабатывать callback. Все, что нужно - указать URL-оповещения на сайте FK (это как раз наш метод callback).
Если хотите протестировать платеж, не платя реальных денег, то в настройках кассы можно активировать тестовый режим:
И еще важный момент: если вы хотите быть уверены, что обработали запрос верно, то необходимо включить "Подтверждение платежа". В таком случае нужно будет отправлять "YES" в случае, если все пошло по плану. Если FK не получит от вас "YES" при включенном режиме подтверждения платежа, то FK будет слать запрос до тех пор, пока не получит эту заветную строку в ответ.
P.S. Полный код можно посмотреть тут. Можете использовать в качестве примера в своих проектах. Он неидеальный, там есть, что поправить, оставляю с целью, что, может, кому-то будет полезно.