milirezai/milipay
Milipay is a Laravel/PHP package for integrating online payment gateways. It offers a straightforward API for creating payments, handling gateway callbacks, and verifying transactions, so developers can add checkout flows to their applications with minimal configuration.
1. Install and pin
composer require milirezai/milipay
The last release is dated 2026-09-18, so pin an explicit constraint ("milirezai/milipay": "^1.0" or whatever the tag scheme is) rather than relying on *. With a 10-star package, upstream churn is real and you want upgrades to be deliberate.
2. Publish and read the config first
php artisan vendor:publish --provider="Milirezai\Milipay\MilipayServiceProvider"
Open config/milipay.php before writing any code. This is the single most important file in a small payment package — it tells you:
sandbox/test toggle,Cross-reference the config keys against src/ — small packages often document fewer options than they support, and sometimes support fewer than the config implies.
3. Set credentials in .env, never in the repo
Mirror each config key into an env var and reference it from the config file (the standard env('...') pattern). Payment credentials differ per gateway, so keep one block per gateway and only populate the one you actually use.
4. First use case: a single-gateway checkout
The canonical first flow is request → redirect → callback → verify:
Read the package's own tests and any examples/ or README code block before designing — the method signatures (especially argument order for amount/currency/callback) are where you'll waste the most time guessing.
Wrap the package in one application-level service
Don't call Milipay::... directly from controllers. Put a thin PaymentService (or per-gateway actions) between the package and your app:
This matters more than usual here: if the package's API shifts on upgrade, or if you outgrow it, you replace one class instead of grepping 30 call sites.
Driver/gateway selection per request
Most Laravel payment packages of this shape expose something like a manager with gateway('name') or a driver() chain. Two viable patterns:
config/milipay.php. Simplest; right for most apps.If the package supports runtime selection, pass the gateway explicitly at both request and verify time; don't rely on the default being the same on the callback as it was on the redirect (it usually is, but not always — e.g. multi-gateway callback routes).
Transaction ledger as your source of truth
Keep your own payments/transactions table. Minimum columns: id, user_id (or order id), gateway, gateway_token/authority, amount, currency, status, reference_id/trace, paid_at, raw_response (JSON), timestamps.
The package may or may not persist anything. Assume it doesn't. Your table is what you reconcile, refund, and report against, and it's what survives a package swap.
Status machine, not booleans
pending → redirected → verified | failed | canceled. Write the transition after a successful verify call, inside a DB transaction with a SELECT ... FOR UPDATE (or lockForUpdate()) on the payment row. Duplicate callbacks are normal; the row lock plus a "already verified" short-circuit is what prevents double fulfillment.
Callback route hygiene
auth middleware (the gateway has no session).VerifyCsrfToken::$except if the gateway POSTs back.OK or a 200 with specific body) — not your app's HTML error page.Verify amounts server-side
Never trust the amount in the callback. Compare the verified amount (from the verify response) against the amount you stored on the transaction. Mismatch = log loudly, mark failed, don't fulfill.
Test with fakes, not live calls
If the package ships a fake/testing driver, use it in feature tests. If not, bind your own PaymentService double in the container for tests and reserve real gateway calls for a staging smoke test against sandbox credentials. Do not put live calls in CI.
Read src/ before trusting the README. At this size, the README is often a wish-list. The actual contract is the service provider's bindings, the manager class, and whatever the tests assert. Budget an hour for this; it saves a day of guessing method signatures.
Amount units are the #1 bug. Iranian and regional gateways vary between Rial and Toman, and some expect the smallest unit as an integer while others take a decimal string. Decide on one internal representation (integer minor units is safest), convert at the boundary, and write a test asserting the exact value sent for a known price. Getting this wrong is silent — payments succeed at 1/10 the price.
Callback/return URL must match exactly. Trailing slashes, http vs https, and query-string ordering all matter to some gateways. Generate it with route() / url() rather than hardcoding, and don't let it drift between local and production .env files.
Sandbox mode is per-gateway, not global. Check whether the toggle changes the endpoint host, the credentials, or both. Some packages toggle the host but still validate live credentials, which fails confusingly.
Verify is not idempotent on the gateway side. A second verify call on the same token often returns "already verified" as an error. Treat that specific error as success, or you'll fail users who hit refresh on the callback page. Your own row lock should handle most of this, but plan for the gateway-side behavior too.
Timeouts and retries. Payment HTTP calls should have explicit timeouts (10–15s) and no automatic retry on the request step — retrying a "create payment" call can create two payable tokens. Retries are safe on verify only if you treat duplicates correctly. If the package uses Laravel's HTTP client internally, check whether it exposes timeout config; if not, wrap calls or fork.
Logging and masking. Log the full response on failure, but mask API keys, card numbers, and full tokens in anything that reaches persistent logs or an error tracker. Payment payloads are exactly what gets screenshotted into tickets.
Reconciliation job. Schedule a daily job that re-queries the gateway for payments stuck in pending past some threshold. Callbacks get lost — dropped deploys, gateway outages, users closing the tab. Without this you'll have paid-but-unfulfilled orders and no way to find them.
Extension points to look for. If the package has a driver interface or a base gateway class, that's your escape hatch: implement a custom driver for a gateway it doesn't cover instead of fighting the abstraction. If it doesn't have one, that's a strong signal to keep the package behind your own service interface so replacing it is a one-file change.
Lock down the dependency. Because the package sits on the money path and has a small maintainer base: pin the version, review the diff before upgrading, and consider vendoring or forking if you hit an unfixable bug — a PR upstream may not get reviewed quickly. Add the payment integration to your critical-path test suite so an upgrade can't silently break checkout.
How can I help you explore Laravel packages today?