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

Free Payment Bundle Laravel Package

paymentsuite/free-payment-bundle

Adds a free payment method to PaymentSuite, letting Symfony/PHP e-commerce apps complete zero-total orders without touching a real gateway. Lightweight and easy to configure, it's a handy fallback for testing, promotions, or fully discounted carts.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: it is not a Laravel package. paymentsuite/free-payment-bundle is a read-only subtree split of the FreePayment component from the PaymentSuite monorepo. There is no ServiceProvider, no config/paymentsuite.php, and nothing for package discovery to find. What you actually get is roughly three files of real logic: a PaymentMethodInterface implementation, a controller, and a DI extension + routing. For a Laravel shop, the value is the pattern, not the dependency.

Read in this order before writing any code:

  1. README.md — it points back to PaymentSuite/PaymentSuite; issues and PRs do not live here.
  2. PaymentMethod/FreePaymentMethod.php — the entire point: it returns a PaymentMethodView that redirects to its own execute route instead of an external gateway.
  3. DependencyInjection/Configuration.php — usually just an enabled flag under the paymentsuite tree.
  4. Resources/config/routing.* — route names are namespaced paymentsuite_free_payment_*; check them before wiring redirects.

Check installability in seconds, then stop:

composer why-not paymentsuite/free-payment-bundle

If it demands --ignore-platform-reqs or pulls symfony/http-kernel into your app, that is your kill signal. Do not bridge a Symfony bundle into Laravel via a shim — the DI + event-dispatcher graph costs more than the behavior is worth.

Minimal Laravel port (this is the actual "getting started" for a Laravel team):

// config/payment.php
return [
    'methods' => [
        'free' => ['enabled' => env('PAYMENT_FREE_ENABLED', false)],
    ],
];
// app/Payments/FreePaymentMethod.php
final class FreePaymentMethod implements PaymentMethod
{
    public function process(Order $order): PaymentResult
    {
        abort_unless(config('payment.methods.free.enabled'), 404);
        abort_unless($order->total()->isZero(), 422, 'Free method is zero-amount only.');

        return PaymentResult::success($order, provider: 'free');
    }
}

First use case: a zero-total cart — free product, 100%-off coupon, trial signup, comped B2B seat, or a dev/staging checkout — where you want the existing order state machine to run without touching a gateway.

Implementation Patterns

1. Staging/test checkout driver (highest day-to-day value). Enable it only in local/testing so feature tests drive the full checkout path without HTTP fakes:

$this->post('/checkout')->assertRedirect('/orders/1/complete');

$this->assertDatabaseHas('orders', [
    'id' => 1,
    'status' => 'paid',
    'payment_method' => 'free',
]);

2. Explicit zero-total branch — you decide, not the package. The bundle just accepts; it does not choose when it applies. Put the decision in a resolver, not scattered conditionals:

final class PaymentMethodResolver
{
    public function resolve(Order $order): PaymentMethod
    {
        if ($order->total()->isZero() && config('payment.methods.free.enabled')) {
            return app(FreePaymentMethod::class);
        }

        return app($order->preferredGateway());
    }
}

3. Never expose it as a user-selectable radio button. Gate it server-side: either the order total is zero (computed by your code, not a request parameter) or the caller is an admin route. A "free" method in the public method list is a revenue bypass waiting to happen.

4. Emit the same domain events as a real gateway. Downstream listeners (receipts, entitlements, provisioning) should not care which provider ran:

PaymentSucceeded::dispatch($order, transaction: Transaction::synthetic(
    orderId: $order->id,
    provider: 'free',
    amount: 0,
));

5. Idempotency on retry. Zero-amount orders still get double-submitted (back button, retried job). Guard on state, not on the gateway:

if ($order->status === OrderStatus::Paid) {
    return PaymentResult::alreadyPaid($order);
}

6. Admin/comped flow. For B2B comped seats and manually approved carts, expose an admin action that flips the order to the free path rather than letting the customer pick it:

Route::post('/admin/orders/{order}/comp', CompOrderController::class)
    ->middleware(['auth', 'can:comp,order']);

7. Observability from day one. Every invocation logs order ID, resolved amount, and actor, so a finance reconciliation gap is traceable:

Log::info('free_payment.invoked', [
    'order_id' => $order->id,
    'amount' => $order->total()->minorUnits(),
    'user_id' => auth()->id(),
    'source' => $source, // 'checkout' | 'admin'
]);

8. Parallel run in staging. Keep your existing paid gateways wired and selectable; free sits alongside behind a flag. Run both through the same contract tests so the checkout UI, emails, and fulfilment paths are provider-agnostic.

9. Port the semantics, not the code. The reusable asset is the state-transition shape: an order is marked paid with a synthetic transaction, events fire, and the wrapper exposes the result. Reproduce that shape against your own PaymentMethod contract; do not copy the Symfony service tags.

Gotchas and Tips

  • Silent success is the critical risk. A free method returning success without asserting the payable amount is zero is a direct revenue-bypass vector. Verify the assertion yourself in your port — do not assume the upstream does it, and make it fail closed (throw/422) on non-zero.
  • Config drift is the second risk. A staging config inherited into production silently processes free orders. Default PAYMENT_FREE_ENABLED=false, set it explicitly per environment, and add a deploy-time assertion that production has it off unless intentionally on.
  • --ignore-platform-reqs is a trap. If you needed it once, it becomes permanent and masks every other version conflict in your lock file. Prefer the port over the override.
  • State desync breaks finance. Marking an order paid without a settlement record (even a synthetic zero-value one) breaks reconciliation and refund flows later. Write the transaction row; the amount being zero does not mean the record does not matter.
  • No webhooks, no refunds, no partials. This is a no-op gateway. Do not plan around reversing a free payment; plan around reversing the order.
  • Free ≠ pending approval. A comped order awaiting sign-off is not the same as a zero-total cart. Keep approval state in your own model; the free method only marks payment.
  • Zero total has edge cases. Shipping, tax, and currency rounding can make "zero" a moving target between cart and checkout. Recompute server-side at the moment of payment, from persisted line items — never trust a submitted amount.
  • Guard the route, not just the button. Hiding the radio button is UI; the checkout POST handler must re-check zero total and the config flag. The bundle's execute route is directly reachable in Symfony — assume yours will be too.
  • Vendor it if you adopt it. The split is a read-only artifact with effectively no upstream traffic. Record the upstream commit SHA, and budget for owning patches on every PHP/Symfony bump — or skip it entirely.
  • Extension points are thin by design. The only real knob is the enabled flag. If you need per-customer eligibility, amount caps, or audit trails, those belong in your own resolver and policy classes around the gateway, not inside it.
  • Debugging tip: when a free order stalls, check the redirect target (paymentsuite_free_payment_execute) and routing import first — a missing route import looks identical to a failed payment in logs.
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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource