amidesfahani/filament-payment-manager
Filament Payment Manager is a Laravel/Filament package for administering payments from your admin panel. It provides Filament resources to view and manage payment records, monitor transaction statuses, and keep payment data organized in a familiar, ready-to-use interface.
The package wires Shetabit/Payment into a Filament panel, so it assumes two things up front: a Filament panel provider and a Laravel app already comfortable with the underlying Shetabit driver model. Read the Shetabit docs before the plugin docs — the plugin is mostly a UI shell around Shetabit\Multipay\Invoice, Payment::config(), and Payment::callbackUrl().
Minimal setup:
composer require amidesfahani/filament-payment-manager
php artisan vendor:publish --tag="filament-payment-manager-config"
php artisan vendor:publish --tag="filament-payment-manager-migrations"
php artisan migrate
Then register the plugin on the panel you want it visible on. In app/Providers/Filament/AdminPanelProvider.php:
use Amidesfahani\FilamentPaymentManager\FilamentPaymentManagerPlugin;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(FilamentPaymentManagerPlugin::make());
}
First use case: log into /admin, open the created Payment Gateways (or similarly named) resource, and add a gateway record — driver name (zarinpal, idpay, zibal, payir, …), API key / merchant ID, sandbox toggle, and a callback URL. That row is what your frontend checkout should read from instead of hard-coding config/payment.php values.
Where to look first:
config/filament-payment-manager.php — the driver list, model bindings, and table names.Resources/ folder — that's the actual surface area; almost every customization is a Resource or Form override.Treat the plugin as an admin CRUD layer, not a payment processor. The package manages gateway configuration. The actual Payment::make($invoice, $callbackUrl, $driver) call still belongs in your own service or controller. A clean split:
// app/Services/CheckoutService.php
public function start(Order $order, Gateway $gateway): string
{
$invoice = (new Invoice)->amount($order->total);
$invoice->detail(['description' => "Order #{$order->id}", 'mobile' => $order->user->mobile]);
return Payment::config($gateway->toShetabitConfig())
->callbackUrl(route('payments.callback', $order))
->purchase($invoice, function ($driver, $transactionId) use ($order) {
$order->update(['transaction_id' => $transactionId]);
})
->pay()
->render();
}
Add a toShetabitConfig() accessor on your gateway model (or use the plugin's model if it publishes one) that maps the stored columns to Shetabit's expected array — merchantId, sandbox, callbackUrl, etc. This keeps the admin UI decoupled from the runtime call.
Multi-panel strategy. If you have a tenant or vendor panel where merchants manage their own gateways, register the plugin there with a scoped query. Filament plugin classes rarely ship tenant scoping out of the box — override getEloquentQuery() in a subclassed Resource, or use a modifyQueryUsing on the plugin if supported:
FilamentPaymentManagerPlugin::make()
->usingModel(MerchantGateway::class)
Integrate with a payment/transaction ledger. The plugin is config-oriented; your transactions table should be separate and reference the gateway row by FK. Then hook the Shetabit callback (verify, paid events) into your own listener and update your ledger — don't expect the plugin to do this for you.
Form customization. Because Filament Resources are extensible, the day-to-day pattern is extend and override rather than fork:
class GatewayResource extends \Amidesfahani\FilamentPaymentManager\Resources\GatewayResource
{
public static function form(Form $form): Form
{
return parent::form($form)->schema([
...parent::form($form)->getSchema(),
TextInput::make('webhook_secret')->password()->revealable(),
]);
}
}
Register the subclass via the plugin's ->resource() method if exposed, or by disabling the plugin's default resource and adding your own in the panel's ->resources([]).
Seed gateway rows via migrations/seeders in CI, not through the panel, for reproducible environments — the panel is for operator changes, seeders for baselines. Store credentials in .env and reference them in the seeder so production keys never live in a migration file.
Invoice::amount() expects Rials, while many Iranian UIs display Toman (1 Toman = 10 Rials). The plugin won't enforce this. Pick one convention, write it down in a code comment near the form's amount field if the plugin exposes one, and stick to it — off-by-10x bugs here are common and expensive.route('payments.callback') and ensure APP_URL is the public domain. Shetabit's drivers are strict about trailing slashes on some (Zarinpal), lenient on others (IDPay). Prefer url()->route() without query strings.mode => 'sandbox' for Zarinpal vs sandbox => true for others). A shared "sandbox" checkbox in the UI that doesn't map to the driver's actual key is a silent failure mode.encrypted casts. If it doesn't, add them via a subclass. Filament's TextInput::password()->revealable() is a UI nicety, not encryption — anything stored plaintext in gateways.api_key is exposed to anyone with DB read access.Form::schema() vs Form::schema()->components(), ->plugin() signature, Section API). Pin to the Filament major the package was built against in composer.json ("filament/filament": "^3.2" typically) and don't bump casually.composer require then copy into app/Filament/) if you need to fork for driver support, and that's often the better path for a package this size.shetabit/payment occasionally changes driver config keys (notably zarinpal moving between merchantId and merchant_id, and idpay between api_key and key). If the plugin's form labels the field "Merchant ID" but Shetabit expects merchantId, verify in config/payment.php from the vendor folder — do not trust the label.Select with ->options() backed by a static list, and validate in save() that the chosen driver exists under config('payment.drivers'). Otherwise the resource happily stores driver => 'zarinpall' and the failure surfaces at checkout, not at save time.Log::info('payment.callback', $request->all()) at the top of your callback route is the fastest way to tell whether the problem is your app or the gateway. Iranian gateways (especially Zibal and Pay.ir) get tetchy about GET vs POST on verify — check $request->method() in the log, not just the body.->navigationGroup(), ->navigationLabel(), ->navigationIcon() (or whatever fluent setters it exposes) are the low-friction knobs; model binding (->usingModel()) and resource replacement are the high-leverage ones. If a needed setter isn't there, subclass the plugin class rather than editing vendor/.How can I help you explore Laravel packages today?