Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Billing Laravel Package

telegram-bot-essentials/billing

Billing essentials for Telegram bots in Laravel/PHP applications. This package helps you add payment and subscription support, track charges, and manage billing flows alongside your bot workflows. It is designed to fit into existing bot projects. Part of the telegram-bot-essentials ecosystem.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and pin hard. The package is on a 0.0.x line with no human-readable changelog and zero dependents, so treat every upgrade as a potential breaking change:

composer require telegram-bot-essentials/billing:0.0.45

Pin the exact patch version (no ^, no ~). Add a composer.lock review step to your PR template so a composer update can't silently move this dependency.

2. Publish and read before you run.

php artisan vendor:publish --provider="TelegramBotEssentials\Billing\BillingServiceProvider"

Publish the config and migrations, then read both files before running migrate. On a pre-1.0 package this is your only reliable documentation. Look for:

  • Table names and whether they collide with anything you already have (invoices, payments, plans, subscriptions are common names — check for prefixes).
  • Money column types. If amounts are stored as floats, stop and wrap them yourself; if they're integer minor units, note that everywhere in your own code.
  • Status columns — are they strings, enums, or a state machine class? That tells you how much lifecycle logic you get for free.
  • The gateway registry config block: which driver keys exist, and which one is the default.

3. Where to look first, in this order:

Look at Why
src/Contracts/* The gateway contract is the seam you'll actually integrate against — payment provider, webhook handling, and probably refund/status querying. Read this before anything else.
src/BillingServiceProvider.php Shows what's bound in the container, what's configurable, and whether models are swappable.
config/billing.php Gateway registry, default driver, currency, plan definitions if any.
Migrations The real data model. Descriptions lie; schemas don't.
Existing tests in the repo Best available substitute for documentation. Read the feature tests to learn the intended happy path.

4. Your first use case: gate one premium command. Do not start with subscriptions. Start with the narrowest possible slice:

  1. Seed a single plan (or a single one-off "credit pack").
  2. Create an invoice for a known test user.
  3. Drive it through your gateway's sandbox/test mode.
  4. Assert that an entitlement check returns false before payment and true after.

If you can't get from "invoice created" to "entitlement granted" in under a day, that's your go/no-go signal.

5. Wire the entitlement check into the bot handler, not into the controller. Whatever middleware or guard the package ships, your command handler should call one internal method — $billing->userCan($telegramUser, 'premium_feature') — so you have a single place to swap implementations later.


Implementation Patterns

Pattern 1: Hide the package behind your own interface

This is the single most important pattern given the maturity level. Do not let TelegramBotEssentials\Billing\Models\Subscription appear in your application code, your API resources, or your bot handlers.

// app/Billing/BillingGateway.php  (your interface, not theirs)
interface BillingGateway
{
    public function startCheckout(int $telegramUserId, string $planKey): CheckoutResult;
    public function isEntitled(int $telegramUserId, string $feature): bool;
    public function handleProviderCallback(array $payload): void;
    public function cancel(int $telegramUserId): void;
}

Then a single adapter class imports the package. Everything else — controllers, commands, jobs, tests — depends on your interface. When 0.1.0 renames Subscription to Entitlement (or removes the gateway registry entirely), you change one file.

Pattern 2: Plans as config, not rows edited by hand

Keep the commercial definition of a plan in your own config or a domain enum, and treat the package's Plan record as a projection of it. You want pricing changes to go through code review and a deploy, not through someone editing a row in production.

// config/plans.php
return [
    'premium_monthly' => [
        'price'      => 500,        // minor units — confirm package convention
        'currency'   => 'XTR',      // Telegram Stars, or your fiat code
        'interval'   => 'month',
        'features'   => ['premium_commands', 'priority_queue'],
    ],
];

A seeder or an artisan billing:sync-plans command reconciles config → package rows idempotently. This also makes tests deterministic.

Pattern 3: Invoice lifecycle as an explicit state machine on your side

Whatever status strings the package uses, define your own enum and map to it. Billing bugs are almost always "an invoice got confirmed twice" or "a paid invoice got reverted by a late callback."

enum InvoiceState: string {
    case Draft = 'draft';
    case AwaitingPayment = 'awaiting_payment';
    case Paid = 'paid';
    case Expired = 'expired';
    case Refunded = 'refunded';
}

Guard transitions in one place and log every transition with the provider payload attached. When something goes wrong at 2am, that log is the only thing that will save you.

Pattern 4: Idempotent provider callbacks — non-negotiable

Telegram will re-deliver updates. It also sends pre_checkout_query (which you must answer within 10 seconds or the payment is cancelled) separately from successful_payment. Structure your handler as:

public function handleProviderCallback(array $payload): void
{
    DB::transaction(function () use ($payload) {
        $event = $this->recordEventOnce($payload['id'] ?? null, $payload); // unique index on provider event id
        if ($event->already_processed) {
            return;
        }

        $invoice = $this->invoices->findByProviderReference($payload['invoice_ref']);
        $invoice->transitionTo(InvoiceState::Paid, $payload); // no-op if already Paid

        $this->entitlements->grant($invoice->telegram_user_id, $invoice->plan_key);

        $event->markProcessed();
    });
}

Also answer pre_checkout_query before doing any database work — acknowledge first, reconcile after. Telegram's timeout is unforgiving and a cancelled checkout looks like a bug to the user.

Pattern 5: Entitlement check as a first-class bot guard

In a Telegram bot, the natural choke point is the command/handler dispatcher, not HTTP middleware.

// In your command registry
$this->register('/export', ExportCommand::class)->requires('premium_commands');

Resolution order in your adapter: explicit grant → active subscription → trial window → false. Cache the result per user per request (not across requests — revocation must be immediate when you cancel a subscription).

Pattern 6: Test against a fake gateway, always

Implement your own BillingGateway interface with an in-memory fake for the test suite. If the package's contracts are clean, you can also implement their gateway interface with a fake and register it through the registry — that gives you end-to-end coverage of their invoice logic without touching a payment provider.

$this->app->instance(BillingGateway::class, new FakeBillingGateway());
$this->post('/telegram/webhook', $this->successfulPaymentUpdate());
$this->assertTrue($billing->isEntitled($userId, 'premium_commands'));

Also write contract tests around exactly the flows you depend on (create invoice, confirm payment, grant entitlement, expire). These are what will catch an unannounced behavior change in 0.0.46.

Pattern 7: Expiry and renewal explicitly, on your schedule

Do not assume the package runs its own scheduler. Register a command yourself and make it idempotent:

// app/Console/Kernel.php
$schedule->command('billing:sweep-entitlements')->hourly()->withoutOverlapping();

Sweep both directions: expire stale subscriptions, and expire invoices that have been AwaitingPayment past the provider's window. Otherwise your invoice table fills with zombie rows and your reporting is meaningless.


Gotchas and Tips

Maturity is the headline risk. Zero stars, zero dependents, 0.0.x, and a release note that is literally just a compare link. Read the actual diff before every bump (git diff v0.0.44..v0.0.45 -- src/) — the changelog will not tell you anything. Budget for one surprise per bump.

Don't put it on a revenue-critical path yet. This is fine for a pilot tier, an internal tool, or a "premium commands" toggle. It is not fine for contracted SLAs, enterprise invoicing, or anything your finance team reconciles. There is no evidence of tax handling, multi-currency, dunning, refunds, or chargeback flows — and those are exactly the things that turn billing from a weekend project into a quarter.

Money types. Verify whether amounts are integer minor units or floats. If floats anywhere, coerce to integers at your boundary and never do arithmetic on the package's values directly. Also confirm the currency column's width — XTR (Telegram Stars) is three characters, but fiat codes plus symbols sometimes overflow naive string(3) columns.

Telegram-specific traps:

  • pre_checkout_query must be answered in 10 seconds. Acknowledge first, persist later.
  • With Telegram Stars, the provider token is an empty string — if the package's gateway requires a non-empty provider token, that's a wall you'll hit at integration time, not at design time. Check this early.
  • successful_payment can arrive more than once. Idempotency is not optional (see Pattern 4).
  • Test and live provider tokens are different; make sure the config supports both per-environment without code changes.

Migration collisions. Generic table names (invoices, payments, plans) will collide with anything you already have. Check before running migrate, and if there's no configurable table prefix, either add one in a follow-up migration or accept the names and document them.

Timestamp and timezone drift. Expiry comparisons across Carbon instances in different timezones are a classic silent bug. Store UTC, compare UTC, and assert in tests that an entitlement expires at the boundary you expect — not one hour early.

Revocation must beat caching. If you cache entitlement checks (you should, for bot throughput), cache with a short TTL and bust it explicitly on cancel/refund. Users notice within seconds when a cancelled subscription still works, and they notice loudly when a paid one stops working.

Don't trust the models to be final. If the package uses Laravel models, they may not have $fillable guards, casts, or relationships you'd expect. Don't extend them; wrap them. And don't let Subscription::where(...) leak into your controllers.

Extension points to look for, in priority order: (1) a gateway contract you can implement — this is the escape hatch that makes the package worth adopting at all; (2) a config-driven driver registry so you can register a fake in tests; (3) swappable model bindings in the service provider. If (1) or (2) is missing, the exit cost is high and the spike should probably fail.

Debugging playbook. When an entitlement doesn't grant: log the raw provider payload first, then the invoice row, then the resolved entitlement call. Nine times out of ten it's a callback that returned 200 but threw after the response was sent, or an idempotency key that was never written because the transaction rolled back.

Have an exit plan written down before you ship. One page: which tables you own, which are the package's, and what the migration to a homegrown or Stripe-based implementation looks like. With a 0.0.x dependency and no maintainer track record, "we'll deal with it later" is not a plan — it's a future incident.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console