Appearance
Billing
Current version: v0.0.23
telegram-bot-essentials/billing (namespace TelegramBotEssentials\Billing) adds invoices, payment attempts, a pluggable gateway registry, and currency conversion on top of Essence. It does not talk to any payment provider itself — that's what Gateway: Card and Gateway: Zibal are for. Billing defines the contract they implement, plus the invoice lifecycle and admin UI that everything else plugs into.
Installation
bash
composer require telegram-bot-essentials/billing
php artisan migratePublish config if you need to customize supported currencies:
bash
php artisan vendor:publish --tag=tbe-billing-configphp
// config/tbe-billing.php
return [
'supported_currencies' => [
['name' => 'USD', 'symbol' => '$'],
['name' => 'IRR', 'symbol' => '﷼'],
['name' => 'IRT', 'symbol' => 'تومان'],
],
];Depends on Settings — it registers a billing.currency setting (per-bot) using this list.
Core Concepts
Order (abstract)
Your own sellable thing — a subscription, a product purchase, whatever — extends TelegramBotEssentials\Billing\Models\Abstract\Order. It brings in HasInvoice and requires you to implement:
php
abstract class Order extends Model
{
abstract public function getPaidAtAttribute(): ?Carbon;
abstract public function getAmountAttribute(): string;
abstract public function getDescriptionAttribute(): string;
}HasInvoice (a trait, also usable standalone) adds the polymorphic invoice() relation and two hook methods you must implement plus two optional ones:
php
trait HasInvoice
{
public function invoice(): MorphOne; // -> Invoice, latest()
abstract public function invoicePaidHook(): void; // required
abstract public function cancelOrderHook(): void; // required — invoice was revoked (e.g. un-paid after being paid)
public function invoicePendingHook(): void {} // optional
public function invoiceFailedHook(): void {} // optional
}Invoice
TelegramBotEssentials\Billing\Models\Invoice — tenant-scoped, soft-deletes, morphs to both a payable (your Order) and a paymentAttempt (a gateway's attempt model). Statuses: pending, paid, failed. A public, unguessable public_token is lazily generated via uniqid() for building payment-return URLs.
Creating a new invoice for the same payable soft-deletes any prior undeleted invoice for it (see Invoice::booted()), so a payable never has more than one live invoice at a time.
php
$invoice = billing()->createInvoice($order); // order->invoice()->create(['bot_user_id' => ..., 'price' => $order->amount])Status transitions go through dedicated methods, never a raw update(['status' => ...]), because each one fires an event and dispatches inside wHook()->runForUser() so the webhook context is correct even if called from a webhook callback for a different bot user (e.g. a payment gateway callback):
php
$invoice->markAsPaid(); // fires InvoicePaid (no-op if already paid)
$invoice->markAsFailed(); // fires InvoiceFailed
$invoice->markAsPending(); // fires InvoicePendingSetting status back away from 'paid' directly (e.g. via a raw attribute set) fires InvoiceRevoked with the previous status — this is how a paid order gets automatically cancelled if its invoice is later marked unpaid.
PaymentAttempt (abstract)
Each gateway module (Card, Zibal, ...) defines its own concrete PaymentAttempt model extending TelegramBotEssentials\Billing\Models\Abstract\PaymentAttempt, morph-related back to Invoice:
php
abstract class PaymentAttempt extends Model
{
public function invoice(): MorphOne;
public function attemptSucceed(): void; // calls attemptSucceedHook(), sets status='succeed', invoice->markAsPaid()
public function attemptFailed(): void; // calls attemptFailedHook(), sets status='failed', invoice->markAsFailed()
abstract protected function attemptSucceedHook(): void; // gateway-specific bookkeeping
abstract protected function attemptFailedHook(): void;
}Link an attempt to an invoice with:
php
billing()->attemptPayment($invoice, $paymentAttempt);
// $invoice->paymentAttempt()->associate($paymentAttempt); $invoice->save();Invoice status → Order hooks
You never call $order->invoicePaidHook() yourself. TelegramBotEssentials\Billing\Listeners\Invoices\InvokeInvoiceHooks listens for all four status events and calls the matching hook on the invoice's payable automatically:
| Event | Hook called on the Order |
|---|---|
InvoicePaid | invoicePaidHook() |
InvoiceFailed | invoiceFailedHook() |
InvoicePending | invoicePendingHook() |
InvoiceRevoked | cancelOrderHook() |
Separately, DispatchInvoice{Paid,Failed,Pending}Hooks / DispatchInvoiceRevokedHooks listeners send the corresponding Telegram message to the buyer (translated via tbe-billing::invoice.hooks.*) and update any locked invoice_view-tagged MessageMeta to reflect the new status. Both sets of listeners are wired in Providers/EventServiceProvider.php — you get user notification and order fulfillment for free just by extending Order/HasInvoice correctly.
Gateways registry
Gateways (helper: gateways()) is a simple keyed registry of TelegramBotEssentials\Billing\DTOs\Gateway — Billing itself registers none; each gateway package registers its own in its service provider:
php
gateways()->addGateway(new Gateway(
key: 'zibal',
label: 'Zibal',
inlineButtonGenerator: fn (Invoice $invoice) => Keyboard::inlineButton([
'text' => 'Pay with Zibal',
'url' => route('gateway-zibal.pay', $invoice->public_token),
]),
));
gateways()->getGateways(); // Collection<string, Gateway>
gateways()->getGateway('zibal');
gateways()->getGateway('zibal')->getInlineKeyboard($invoice); // calls inlineButtonGenerator($invoice)This is what lets InvoiceFeature render a "choose your payment method" screen without knowing which gateway packages happen to be installed — see Gateway: Card and Gateway: Zibal for concrete registrations.
Currency
Two related but distinct helpers:
currency()→Currencyservice (per-bot currency helpers backed by thebilling.currencysetting)priceIn($price)→CurrencyFather::from(settings()->get('billing.currency'))->amount($price)— a fluent converter
CurrencyFather converts between USD, IRR, and IRT using a live exchange-rate API (currency.servicefather.ir), caching each (from, to, amount) triple for 6 hours:
php
priceIn('10')->toIRT(); // convert 10 units of the bot's configured currency to Tomans
CurrencyFather::USD()->amount('5')->toIRR();Admin UI
ManageInvoicesQuery / ManageInvoicesAnswer / ManageInvoicesFeature / ManageInvoicesKey give admins an invoice list/detail screen out of the box. InvoiceFeature (member-facing) renders the invoice + gateway choice screen shown to the buyer.
Scheduled Maintenance
MarkOverdueInvoicesAsFailed runs hourly (registered against Laravel's scheduler in TbeBillingServiceProvider::boot()) to fail invoices that were never completed within the expected window — make sure php artisan schedule:work (or your server's cron → schedule:run) is actually running in production.
Settings registered
| Key | Type | Notes |
|---|---|---|
billing | DIRECTORY | Groups the settings below in the admin UI |
billing.gateways | DIRECTORY | Groups gateway-specific settings (populated by gateway packages) |
billing.currency | ENUM | Options from config('tbe-billing.supported_currencies') + USD; default USD |