Skip to content

Gateway: Card

Current version: v0.0.11

telegram-bot-essentials/gateway-card (namespace TelegramBotEssentials\GatewayCard) is a manual "pay by bank card transfer" gateway for Billing. There's no external payment processor — the buyer transfers money to a card number shown in the bot, sends a screenshot/receipt back, and an admin manually accepts or rejects the payment.

This is the simplest possible gateway implementation and a good reference for writing your own manual/offline gateway.

Installation

bash
composer require telegram-bot-essentials/gateway-card
php artisan migrate

Configure via the bot's Settings admin menu (or directly with settings()->set(...)) — there's no separate config file, everything lives in per-bot Settings:

Setting keyTypePurpose
billing.gateways.card.statusCHECKBOXMaster on/off switch
billing.gateways.card.card_numberTEXTCard number shown to buyers
billing.gateways.card.card_nameTEXTCardholder name shown to buyers
billing.gateways.card.transactions_chat_idTEXTChat where admins review submitted receipts

All four must be set for the gateway to appear — CardPaymentFeature::isCardPaymentEnabled() checks all of them, and both CardPaymentQuery and ManageCardPaymentQuery gate every method behind it via isEnabled().

Flow

Invoice screen (Billing)
      │  gateways()->getGateway('card') → inline button, only if isCardPaymentEnabled()

CardPaymentQuery::toCard(Invoice)          (CARD_PAYMENT#toCard)
      │  creates a ToCardAttempt, billing()->attemptPayment($invoice, $attempt)
      │  shows the card number/name, sets state → awaiting receipt

CardPaymentAnswer::payToCard(Invoice)      (state: CARD_PAYMENT#pay_to_card)
      │  stores the buyer's text/photo receipt on the ToCardAttempt (photo → base64 on the model)
      │  forwards/copies the receipt to `transactions_chat_id` with Accept/Reject buttons

ManageCardPaymentQuery::acceptCardPayment(ToCardAttempt)   (MANAGE_CARD_PAYMENT#accept_card_payment)
  → $toCardAttempt->attemptSucceed()  → Invoice::markAsPaid() → Billing's InvoicePaid hooks fire
                        or
ManageCardPaymentQuery::rejectCardPayment(ToCardAttempt)   (MANAGE_CARD_PAYMENT#reject_card_payment)
  → asks the admin for a rejection reason, then $toCardAttempt->attemptFailed() → Invoice::markAsFailed()

ToCardAttempt

Extends Billing's abstract PaymentAttempt. Notable columns: card_number, amount, info_text (receipt caption/text), info_photo (base64-encoded JPEG downloaded from Telegram), received_at, rejected_at.

php
protected function attemptSucceedHook(): void {}                 // no-op — Invoice::markAsPaid() does the real work
protected function attemptFailedHook(): void
{
    $this->setAttribute('rejected_at', now());
    $this->save();
}

Registering with Billing

Like every gateway, it registers itself into Billing's registry from its own service provider — nothing in Billing itself knows this gateway exists:

php
gateways()->addGateway(new Gateway(
    key: 'card',
    label: __('tbe-gateway-card::invoice.to_card.labels.gateway'),
    inlineButtonGenerator: function (Invoice $invoice) {
        if (!CardPaymentFeature::isCardPaymentEnabled()) return null;
        return Keyboard::inlineButton([
            'text' => __('tbe-gateway-card::invoice.to_card.keys.member-card_payment'),
            'callback_data' => encodeCallback('CARD_PAYMENT', 'toCard', [$invoice->id]),
        ]);
    },
));

Returning null from inlineButtonGenerator hides the payment option entirely — this is the standard way a gateway opts itself out when unconfigured.

Why this is a good template

If you're writing your own manual gateway, copy this package's shape:

  1. A *Attempt extends PaymentAttempt model with your own bookkeeping columns.
  2. A member-facing CallbackQuery that creates the attempt and calls billing()->attemptPayment().
  3. A member-facing StateAnswer that collects proof of payment and hands off to an admin chat.
  4. An admin-facing CallbackQuery that calls $attempt->attemptSucceed() / attemptFailed().
  5. A Feature::isEnabled()-style guard checked from every handler's isEnabled() and from the gateway's inlineButtonGenerator.
  6. Settings under billing.gateways.<your-key>.*, plus the shared billing / billing.gateways DIRECTORY settings (re-declared per package — Settings::addSetting() overwrites by key, so this is safe and keeps each package self-contained).