golang

Добавляем платежную систему FreeKassa в проект на Go

  • суббота, 17 мая 2025 г. в 00:00:09
https://habr.com/ru/articles/909816/

Привет! Хочу поделиться гайдом по интеграции FreeKassa в проект на Golang.
В данной статье будут рассмотрены:

  • Создание инвойса.

  • Обработка оповещения об успешной оплате.

Регистрация и создание магазина

  1. Регистрируемся на https://freekassa.net.

    После регистрации на странице вы увидеть кнопку "Добавить кассу":

    Интерфейс главной страницы FreeKassa
    Интерфейс главной страницы FreeKassa

    Нажимаем, чтобы создать кассу (магазин).

  2. В открывшемся окне выбираем тип нашего магазина, в моем случае - это TG-бот. После жмем продолжить:

    Название сайта - это то, что будет видеть пользователь на странице оплаты.

  3. Далее потребуется какое-то время на активацию созданной кассы. Если вы пишите новый проект, и он находится в процессе разработки, то необходимо будет сообщить об этом в поддержку, чтобы вам активировали ТЕСТОВЫЙ РЕЖИМ. Иначе касса активирована не будет.

  4. После активации на главной странице вы увидите вашу кассу.

Настройка кассы

На главной странице переходим в настройки:

Главная страница FK.
Главная страница FK.

Вы увидите настройки вашего магазина (кассы):

Настройки кассы.
Настройки кассы.

Нас интересуют:

  1. Секретные слова - необходимы для формирования подписей. Можете придумать их сами, либо воспользоваться автогенерацией.

  2. URL оповещения - это url, на который FreeKassa будет отправлять callback в случае успешной оплаты.

  3. URL успешной оплаты - url, на который будет произведен редирект пользователя в случае успешного подтверждения оплаты от нас, то есть, если наш URL оповещения отдал 200.

  4. 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&currency=%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&currency=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).

Если хотите протестировать платеж, не платя реальных денег, то в настройках кассы можно активировать тестовый режим:

Меню c тестовым режимом.
Меню c тестовым режимом.

И еще важный момент: если вы хотите быть уверены, что обработали запрос верно, то необходимо включить "Подтверждение платежа". В таком случае нужно будет отправлять "YES" в случае, если все пошло по плану. Если FK не получит от вас "YES" при включенном режиме подтверждения платежа, то FK будет слать запрос до тех пор, пока не получит эту заветную строку в ответ.

P.S. Полный код можно посмотреть тут. Можете использовать в качестве примера в своих проектах. Он неидеальный, там есть, что поправить, оставляю с целью, что, может, кому-то будет полезно.