В статье-инструкции расскажу, как начал монетизировать telegram бота, который присылает уведомления о необходимости продлить домен, тем самым делает напоминание, чтобы пользователь продлил домен во время и не потерял его. За 2 часа, добавил кнопку доната и настроил интеграцию с ЮKassa. Через 1 день уже начал принимать донаты.

Оглавление:

Наверное каждый хочет начать монетизацию своего telegram бота, но могут отталкивать технические сложности, или не хочется заниматься регистрацией ООО или ИП.

На практике, не так все сложно, с помощью этой статьи-инструкции можно стать самозанятым, открыть счет, интегрировать платежную систему в telegram бота и начать принимать платежи, при этом автоматически отчитываться перед налоговой.

Зачем становиться самозанятым?

Преимущества, которые дает самозанятость:

  • Позволяет работать легально и не получить штраф или судебное разбирательство за неуплату налогов и незаконную предпринимательскую деятельность;

  • Не требуется оформление юридического лица;

  • Сам плачу за себя налоги, от заказчика требуется только оплатить услуги или товар;

  • Можно официально подтвердить свои доходы, например для получения визы, кредита или ипотеки;

  • Предоставление чеков клиентам поможет повысить их доверие к продукту;

  • Предприниматели-одиночки без наемного персонала могут снизить налоговую нагрузку;

  • Юрлица — компании и ИП — могут пользоваться услугами и покупать товары напрямую у самозанятых граждан, не исполняя обязанности налогового агента.

Становимся самозанятым.

Становимся самозанятым, для того, чтобы могли заключить договор с ЮKassa и открыть счет для принятия платежей, а так же ЮKassa могла делать вывод средств на наш счет в банке. Самозанятые платят самый маленький налог:

  • 4% за доходы от физических лиц - наши донаты и платежи.

  • 6% за доходы от юридических лиц, например когда к нам придет рекламодатель и захочет оплатить интеграцию или рекламный пост.

Для оформления самозанятости необходимо наличие карты Tinkoff. Если счета нет, то его нужно открыть в банке Тинькофф.

Оформляем самозанятость через мобильное приложение Тинькофф. Откройте «Главная» → «Открыть новый продукт» → «Самозанятость» → кнопка «Оформить самозанятость».

Оформление самозанятости в мобильном приложении Тинькофф.
Оформление самозанятости в мобильном приложении Тинькофф.

Не могу сказать, можно ли оформить самозанятость через приложения других банков. Но встать на учет самозанятому можно в приложении «Мой налог» по учетной записи Госуслуг. Если у вас есть банковский счет в другом банке, вы можете подать заявление на самозанятого, в приложении «Мой налог».

Тинькофф сам подает заявление в налоговую службу. Самозанятость оформляется за 5–10 минут, хотя в моем случае постановка на учет в налоговую заняла 4 часа.

Уведомление о подаче заявления в приложении «Мой налог»
Уведомление о подаче заявления в приложении «Мой налог»
Уведомление об успешном постановлении на учет в приложении «Мой налог»
Уведомление об успешном постановлении на учет в приложении «Мой налог»

Пока ждем решения налоговой, скачиваем приложение «Мой налог». После каждой оплаты или доната, нужно заявлять наш доход и оплачивать налоги. Позже настроим автоматическую отправку чеков в налоговую из личного кабинета ЮKassa. Если у вас небольшое количество операций, то можно заявлять доход в ручную, в приложении «Мой налог».

Скачиваем приложение «Мой налог»:

Скриншот бонуса из приложения «Мой налог»
Скриншот бонуса из приложения «Мой налог»

Из приятного. Каждому налогоплательщику, который впервые регистрируется в качестве самозанятого, предоставляется налоговый вычет в размере 10 000 рублей (бонус). Бонус — это сумма, которая уменьшает налог. Он предоставляется государством единожды и расходуется постепенно. Бонус уменьшает налоговую ставку на 1% по вознаграждениям, полученным от физических лиц, и на 2% - по вознаграждениям от юридических лиц или индивидуальных предпринимателей.

Открытие счета в ЮKassa.

Регистрируемся в ЮKassa. Сразу после регистрации можно указать данные для договора и заполнить настройки. Это можно делать одновременно, но лучше сначала отправить данные — пока менеджеры их проверяют, вы сможете разобраться с настройками. На сайте ЮKassa есть инструкция по заполнению и подписанию договора.

Бот будет принимать платежи без сайта, будем выставлять счет на оплату и отправлять его пользователю. Все остальные данные договора заполняем по инструкции и отправляем на проверку. Если в заполненном договоре не будет ошибок, то мы получим уведомление на почту, что договор готов и его необходимо подписать.

Письмо на почту, после проверки данных договора.
Письмо на почту, после проверки данных договора.

После того, как мы подпишем договор и загрузим его в личном кабинете ЮKassa, можно перейти к написанию кода и интеграции платежной системы в бота. Проверка данных может занимать от одного до трех рабочих дней. После проверки мы сможем принимать платежи.

В ЮKassa есть возможность использовать тестовые данные, для разработки и тестирования, которые позволяют нам начать интеграцию платежной системы прямо сейчас. Перейдем в тестовый магазин, нам необходимы будут следующие данные, для интеграции с ЮKassa API:

YOOKASSA_SHOP_ID
YOOKASSA_SECRET_KEY

Личный кабинет ЮKassa. Тестовый YOOKASSA_SHOP_ID.
Личный кабинет ЮKassa. Тестовый YOOKASSA_SHOP_ID.
Личный кабинет ЮKassa. Тестовый YOOKASSA_SECRET_KEY.
Личный кабинет ЮKassa. Тестовый YOOKASSA_SECRET_KEY.

Когда нам откроют счет, мы сможем заменить тестовые данные на реальные и начать принимать платежи.

Добавляем автоматическую отправку чеков в налоговую.

По данной инструкции, делаем автоматическую отправку чеков в налоговую, чтобы зафиксировать доход и оплатить налог на прибыль. ЮKassa берет комиссию за отправку чека 1,2% от суммы платежа. Если на начальном этапе, у вас мало оплат, можно делать отправку чеков в ручном режиме, через приложение «Мой налог».

Запрос разрешения от ЮKassa в приложении Мой налог, для отправки чеков.
Запрос разрешения от ЮKassa в приложении Мой налог, для отправки чеков.

Сколько в итоге потеряю на налогах и комиссиях?

Комиссия ЮKassa с каждой операции 3.5% и за отправку чека 1.2% (НДС уже включён). Налог в федеральную налоговую службу составляет 4%.

Допустим наш донат 100 рублей. Со 100 рублей в ЮKassa уходит 4 рубля 70 копеек, в федеральную налоговую службу уходит 4 рубля.

Итого, от 100 рублей у нас остается 91 рубля 30 копеек. Таким образом, с каждой транзакции мы отдаем 9,7% от суммы платежа.

Интеграция ЮKassa в телеграм-бота

Устанавливаем клиент для работы с платежами по API ЮKassa

composer require yoomoney/yookassa-sdk-php

Telegram бот отправляет уведомления об истечении срока регистрации домена, под сообщением располагается кнопка доната. У меня это делается в очереди сообщений, у вас может быть любая другая реализация, например определенное событие, которое срабатывает в конкретный момент времени. Например, по расписанию в 12:00 запускается скрипт.

TrackDomainJob.php

namespace App\Jobs;

use App\Telegram;
use App\Models\Keyboard;
use App\Models\TrackDomain;

/**
 * Job, которая отправляет уведомление о необходимости продлить домен.
 */
class TrackDomainJob implements ShouldQueue
{
	/**
     * @throws Exception
     */
    public function handle(): void
	{
        // Отправляем пользователю сообщение и кнопку доната
        Telegram::sendKeyboard($message, Keyboard::getDonateButton(100), $this->trackDomain->user)
	}
}

Telegram.php Класс реализующий методы Telegram API. Статический метод Telegram::sendKeyboard() отправляет пользователю сообщение $message с набором кнопок Keyboard::getDonateButton(100) - возвращает массив набора кнопок inline_keyboard.

Telegram.php

namespace App;

use App\Models\User;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;

/**
 * Класс реализующий методы Telegram API.
 */
class Telegram
{

  /**
   * Отправить сообщение с кнопками клавиатуры
   *
   * @param string $message
   * @param array $keyboard
   * @param User $user
   * @return Response
   */
  public static function sendKeyboard(string $message, array $keyboard, User $user): Response
  {
  	$method = 'sendMessage';
      $url =(new Telegram)->getTelegramBotApiUrl() . $method;
  
      return Http::post($url,[
        'chat_id' => $user->telegram_user_id,
        'parse_mode' => 'HTML',
        'text' => $message,
        'reply_markup' => json_encode($keyboard)
      ]);
  }

  /**
    * Возвращает URL API телеграм бота
    *
    * @return string
    */
  private function getTelegramBotApiUrl(): string
  {
    return env('TELEGRAM_BOT_API_URL') . env('TELEGRAM_BOT_API_ACCESS_TOKEN') . '/';
  }
}

Keyboard.php

Класс, который отвечает за формирования и получения набора кнопок. Статический метод Keyboard::getDonateButton() возвращает кнопку доната со ссылкой на оплату выставленного счета.

namespace App\Models;

use App\Payment;
use YooKassa\Client;
use YooKassa\Common\Exceptions\ApiException;
use YooKassa\Common\Exceptions\BadApiRequestException;
use YooKassa\Common\Exceptions\ExtensionNotFoundException;
use YooKassa\Common\Exceptions\ForbiddenException;
use YooKassa\Common\Exceptions\InternalServerError;
use YooKassa\Common\Exceptions\NotFoundException;
use YooKassa\Common\Exceptions\ResponseProcessingException;
use YooKassa\Common\Exceptions\TooManyRequestsException;
use YooKassa\Common\Exceptions\UnauthorizedException;

/**
 * Класс для работы с клавиатурой бота.
 */
class Keyboard
{
/**
     * Возвращает кнопку для доната.
     *
     * @param int $price
     *
     * @return array
     *
     * @throws ApiException
     * @throws BadApiRequestException
     * @throws ExtensionNotFoundException
     * @throws ForbiddenException
     * @throws InternalServerError
     * @throws NotFoundException
     * @throws ResponseProcessingException
     * @throws TooManyRequestsException
     * @throws UnauthorizedException
     */
    public static function getDonateButton(int $price): array
    {
        $payment = (new Payment())
            ->setPrice($price)
            ->setDescription('Донат от пользователя');

        $client = new Client();
        $client->setAuth(env('YOOKASSA_SHOP_ID'), env('YOOKASSA_SECRET_KEY'));

        $YooKassaPayment = $client->createPayment(
            $payment->data,
            uniqid('', true)
        );

        return [
            'inline_keyboard' => [
                [
                    [
                        'text' => "???? Спасибо, бот. Держи " . $price . " руб.",
                        'url' => $YooKassaPayment->getConfirmation()->getConfirmationUrl()
                    ],
                ]
            ],
        ];
    }
}

Payment.php

Класс для работы с данными платежа. Создает и формирует данные платежа, чтобы дальше передать их для создания счета в ЮKassa.

namespace App;


class Payment
{
  /**
    * @var string $fullName Имя и фамилия пользователя.
    */
  private string $fullName = 'Drobushevskiy Aleksandr';

  /**
    * @var string $email Email пользователя.
    */
  private string $email = 'useremail@gmail.com';

  /**
    * @var string $phone Телефон пользователя.
    */
  private string $phone = '7905*******';

  /**
    * @var int $price Сумма платежа.
    */
  private int $price = 100;

  /**
    * @var string $currency Валюта платежа.
    */
  private string $currency = 'RUB';

  /**
    * @var string $description Описание платежа.
    */
  private string $description = 'Донат от пользователя';

  /**
    * @var array $data Данные платежа.
    */
  public array $data;

  /**
    * @var array $receiptData Данные чека.
    */
  public array $receiptData;

    /**
     * Payment constructor.
     */
    public function __construct()
    {
        $this->receiptData = [
            'customer' => [
                'full_name' => $this->fullName,
                'email' => $this->email,
                'phone' => $this->phone,
            ],
            'items' => [
                [
                    'description' => $this->description,
                    'quantity' => '1.00',
                    'amount' => [
                        'value' => $this->price,
                        'currency' => $this->currency
                    ],
                    'vat_code' => '2',
                ],
            ]
        ];

        $this->data = [
            'amount' => [
                'value' => $this->price,
                'currency' => $this->currency,
            ],
            'confirmation' => [
                'type' => 'redirect',
                'return_url' => env('TELEGRAM_BOT_URL'),
            ],
            'capture' => true,
            'description' => $this->description,
            'metadata' => [
                'order_id' => '1',
            ],
            'receipt' => $this->receiptData,
        ];
    }

    /**
     * Установить размер платежа.
     *
     * @param int $price
     * @return $this
     */
    public function setPrice(int $price): self
    {
        $this->receiptData['items']['amount']['value'] = $price;
        $this->data['amount']['value'] = $price;
        $this->price = $price;

        return $this;
    }

    /**
     * Установить описание платежа.
     *
     * @param string $description
     * @return $this
     */
    public function setDescription(string $description): self
    {
        $this->data['description'] = $description;
        $this->description = $description;

        return $this;
    }
}

Все переменные окружения добавляем в .env

# YooKassa
YOOKASSA_SHOP_ID=ВАШ_YOOKASSA_SHOP_ID
YOOKASSA_SECRET_KEY=ВАШ_YOOKASSA_SECRET_KEY

# Телеграм бот
TELEGRAM_BOT_URL=ВАШ_TELEGRAM_BOT_URL
TELEGRAM_BOT_API_URL=https://api.telegram.org/bot
TELEGRAM_BOT_API_ACCESS_TOKEN=ВАШ_TELEGRAM_BOT_API_ACCESS_TOKEN
TELEGRAM_CHAT_ID=ВАШ_TELEGRAM_CHAT_ID

Результат

В результате telegram бот будет отправлять уведомление о необходимости продлить домен, а под сообщением будет кнопка, со ссылкой на выставленный счет. Если пользователь захочет отблагодарить, за вовремя присланное напоминание, что необходимо продлить домен, чтобы не потерять его.

Уведомление о необходимости продления домена в telegram боте YourDomainBot
Уведомление о необходимости продления домена в telegram боте YourDomainBot

Мой Telegram-канал, где я делюсь идеями и процессом работы над пет-проектами.

Комментарии (5)


  1. FanatPHP
    00.00.0000 00:00

    Спасибо, очень интересно, особенно про налоги и комиссии.


    Не очень понятна структура класса Payment. Например почему данные плательщика прописаны в классе а не берутся из настроек. И почему цена и описание фактически присваиваются по два раза. Не проще вместо конструктора сделать getPaymentData, и заполнять массив в нем? Мне кажется поаккуратнее будет. А в конструкторе задавать сумму и назначение платежа. И вызывать


    $paymentData = (new Payment($price))->getPaymentData();


    1. drobushevkiy Автор
      00.00.0000 00:00
      +1

      В классе Payment дефолтные значения свойств, которые могут быть переопределены. Свойства в классе Payment они динамические и могут меняться. По-этой причине, я решил, что здесь лучше будет применить паттерн Builder, чтобы можно было легко переопределять свойства объекта. То, что вы предлагаете, тоже удобно и может выглядеть еще чище, попробую реализовать. Итак, можно было бы создать DTO, например:

      class PaymentDataDto
      {
          /**
           * @var string $fullName Имя и фамилия пользователя.
           */
          private string $fullName = 'My name is Aleksandr';
      
          /**
           * @var string $email Email пользователя.
           */
          private string $email = '****@gmail.com';
      
          /**
           * @var string $phone Телефон пользователя.
           */
          private string $phone = '7905*******';
      
          /**
           * @var int $price Сумма платежа.
           */
          private int $price = 100;
      
          /**
           * @var string $currency Валюта платежа.
           */
          private string $currency = 'RUB';
      
          /**
           * @var string $description Описание платежа.
           */
          private string $description = 'Донат от пользователя';
      
          /**
           * @var array $data Данные платежа.
           */
          public array $data;
      
          /**
           * @var array $receiptData Данные чека
           */
          public array $receiptData;
      
          public function __construct($properties = [])
          {
              if (!empty($properties)) {
                  foreach ($properties as $name => $value) {
                  $this->$name = $value;
              }
          }
      }
      

      Инициализация объекта платежа будет следующей:

      $paymentDataDto = new paymentDataDto([
        'fullName' => 'User name',
        'email' => 'habrouser2023@gmail.com',
        'phone' => '+7*********',
        'price' => 1500,
        'description' => 'Оплата услуги',
      ]);
      
      $payment = new Payment($paymentDataDto);
      
      $client = new Client();
      $client->setAuth(env('YOOKASSA_SHOP_ID'), env('YOOKASSA_SECRET_KEY'));
      
      $YooKassaPayment = $client->createPayment(
          $payment->data,
          uniqid('', true)
      );

      Я не люблю когда при создании объекта, в сигнатуре передается одно свойство, потому что, через неделю, другой разработчик захочет передать второе свойство, например скидку $discount. Через год другой разработчик захочет передать еще несколько свойств в конструктор и мы получим приблизительно следующее.

      $paymentData = (new Payment($price))->getPaymentData();
      
      // Через месяц
      $paymentData = (new Payment($price, $discount))->getPaymentData();
      
      // Через 6 месяцев
      $paymentData = (new Payment($price, $discount, $description))->getPaymentData();
      
      // Через год
      $paymentData = (new Payment($price, $discount, $description, $userId, $email))->getPaymentData();

      Выглядит не очень, куда лучше использовать инверсию зависимости и передавать обьект paymentDataDto, который в дальнейшем может имплементировать интерфейс BaseDtoInterface или расширять родительский класс BaseDto, что в будущем позволяет нам иметь чистый и понятный обьектно ориентированный код, который легко поддерживать и расширять.

      Вообщем, тут есть несколько вариантов, ваш тоже приемлем.


  1. FanatPHP
    00.00.0000 00:00
    +1

    Вот да, с DTO получается гораздо лучше. Но только с нормальным конструктором, а не с вот этим вот отростком. Как-то от этого "иф нот емпти" четвертой версией попыхивает :)


    Сейчас мне кажется для DTO вообще уже нет других вариантов кроме constructor property promotion. Тем более что кода получится в 5 раз меньше


    class PaymentDataDto
    {
        public function __construct(
            private string $fullName = '',
            private string $email = '',
            private string $phone = '',
            private int $price = 100,
            private string $currency = 'RUB',
            private string $description = 'Донат от пользователя',
            ) {}
    }


  1. OlegIva
    00.00.0000 00:00
    +1

    Для оформления самозанятости необходимо наличие карты Tinkoff

    Вы нагло врёте. Почему нагло? Потому что для оформления самозанятости Тинькофф нафиг не нужен, а у вас дальше идёт реферальная ссылка.


    1. FanatPHP
      00.00.0000 00:00

      Опа. А слона-то я и не заметил. Ну это совсем уже зашквар. Lead Software Engineer побирается реферальными ссылками.