potelo/gu-payment
Laravel package that integrates the Gerencianet (Efí) payment gateway into your application, providing a simple, configurable API for creating charges, issuing boletos, processing credit card payments, and managing payment notifications.
GuPayment is a Cashier-style wrapper around the iugu subscription API for Laravel. If you've used laravel/cashier with Stripe, the mental model is nearly 1:1 — the package gives you a Billable trait, a Subscription model, a config file for plan mapping, and a webhook controller. It targets Brazilian merchants (BRL, CPF/CNPJ, boleto + credit card).
Minimum setup:
composer require potelo/gu-payment
php artisan vendor:publish --provider="Potelo\GuPayment\GuPaymentServiceProvider"
php artisan migrate
Then in .env:
IUGU_API_TOKEN=your_api_token
IUGU_ACCOUNT_ID=your_account_id
Add the trait to app/Models/User.php:
use Potelo\GuPayment\Billable;
class User extends Authenticatable
{
use Billable;
}
Where to look first:
config/gu-payment.php — the plans map is the contract between your app and iugu. Local slugs on the left, iugu plan identifiers on the right.iugu_customer_id (and related columns) to users, the other creates a subscriptions table. Read them before running against production.vendor/potelo/gu-payment/src/ — the trait, the Subscription model, and WebhookController. This package is small enough to read end-to-end in 20 minutes, and you will need to.First use case — subscribe a user with a card token minted client-side:
$user->newSubscription('main', 'monthly')
->create($request->input('token'));
The card token must come from iugu.js on the frontend; you never send raw card data to your server.
Plan mapping lives in two places. Create the plan in the iugu dashboard first, copy its identifier into config/gu-payment.php, then reference the local slug everywhere in your code. Keeping a Plan enum or a config('gu-payment.plans') lookup in one place prevents the classic "why is swap() 404ing" afternoon.
Subscription lifecycle. The trait mirrors Cashier's verbs, which is the main reason to pick this package over hand-rolling Guzzle calls:
$user->subscribed('main'); // bool
$user->subscription('main')->onTrial();
$user->subscription('main')->swap('annual'); // prorated via iugu
$user->subscription('main')->cancel(); // cancels at period end
$user->subscription('main')->cancelNow();
$user->subscription('main')->resume();
Middleware gating. A EnsureUserIsSubscribed middleware built on $user->subscribed() is the standard pattern; return a redirect to your billing page rather than a 403 so users can recover.
Webhooks are the source of truth. The package registers a route that hits WebhookController. You should:
/gu-payment/webhook (or whichever path the published service provider registers — confirm with php artisan route:list | grep gu-payment).subscription.expired, subscription.suspended, invoice.paid, invoice.payment_failed.Invoice access. For a billing history page:
foreach ($user->invoices() as $invoice) {
$invoice->id;
$invoice->total_cents;
$invoice->status;
$invoice->secure_url(); // hosted iugu invoice page
}
Testing. iugu has no sandbox mode as clean as Stripe's. The pragmatic pattern is Http::fake() against https://api.iugu.com/v1/* in feature tests, asserting your app's reaction to the shape of the response rather than round-tripping to iugu. Keep the card-token creation step behind an interface so you can fake it.
Frontend. iugu.js generates the token. Wire it into a form that POSTs only token + plan_slug to your controller, then call newSubscription()->create($token). Do not store the token — it's single-use.
Integration tip. If you need custom webhook handling (very likely), don't fight the default controller. Publish it (php artisan vendor:publish --tag=gu-payment-views / config) or register your own route ahead of it in routes/web.php and let the package's controller fall through. The package is small enough that forking the controller into app/Http/Controllers/IuguWebhookController.php is a legitimate long-term move — see gotchas.
IUGU_API_TOKEN is the API key from "Integrações → API" in the iugu dashboard, not the account ID. Both are needed; the error when you swap them is a generic 401 that doesn't say which.name="number", stop.Stripe-Signature). Your endpoint is public. The pragmatic defense: verify the payload by fetching the referenced subscription via the API with your token before acting on it, and make the handler idempotent (dedupe on invoice/subscription id) since retries happen.users. The publish step adds iugu_customer_id to your users table. On a large table, this locks for a write. Consider ALTER TABLE ... ADD COLUMN ... NULL manually in a maintenance window, or at minimum use a zero-downtime migration strategy.monthly. Your swap() calls now fail with a plan-not-found. Keep plan identifiers in a versioned config/gu-payment.php and add a smoke test that asserts each mapped plan ID resolves via the API.cancel() vs cancelNow(). cancel() defers to period end and requires the subscription still be active to resume(). A user who calls cancel() and immediately tries to resume() from a different request path will hit a state error. Model this in your UI (show a "resume" button only when onGracePeriod()).composer.json, read the source you depend on, and vendor the two files you actually customize (webhook controller and the Subscription model) rather than monkey-patching via class_alias.user->invoices() hits the network on every call. Don't call it in a Blade loop inside a list view without caching — it's one API request per render. Cache the result for a few minutes in Redis.invoice.secure_url() redirect). If you support boleto, newSubscription()->create() without a token may behave differently than expected — check the package's Subscription::create signature against iugu's docs before shipping that path.How can I help you explore Laravel packages today?