---
title: "Payments & shipping — Shops for Laravel | Roundly"
description: "Payment and shipping driver contracts with zero-config defaults — implement a gateway, charge orders, refund on your terms and quote shipping."
url: https://roundly-consulting.com/open-source/docs/shops-for-laravel/payments-shipping
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Shops for Laravel](https://roundly-consulting.com/open-source/docs/shops-for-laravel.md)

# Payments & shipping

Payment and shipping are driver contracts; the package ships no vendor SDK. The defaults work zero-config — a null gateway that always succeeds, and free shipping. Bind your own implementation for production.

## Payment gateway

```php
use RoundlyConsulting\Money\Money;
use RoundlyConsulting\Shops\Contracts\PaymentGateway;
use RoundlyConsulting\Shops\Orders\Order;
use RoundlyConsulting\Shops\Payments\PaymentResult;

final class AcmePayGateway implements PaymentGateway
{
    public function charge(Order $order): PaymentResult
    {
        $due = $order->gatewayAmount();   // total minus store credit — never getFinalPrice()

        // … charge $due->minor() in $due->currency()->code with your provider …

        return PaymentResult::success($due, reference: $chargeId);
    }

    public function refund(Order $order, Money $amount): PaymentResult
    {
        // … refund with your provider …

        return PaymentResult::success($amount, reference: $refundId);
    }
}
```

```php
// config/shops.php
'payment' => [
    'gateway' => AcmePayGateway::class,
    // ...
],
'shipping' => [
    'method' => FlatRateShipping::class,
],

// Or bind the contract in a service provider — a later binding wins:
$this->app->bind(\RoundlyConsulting\Shops\Contracts\PaymentGateway::class, AcmePayGateway::class);
```

## Charging

```php
use RoundlyConsulting\Shops\Facades\Shops;

$result = Shops::order($order)->charge();   // action form: Actions\Orders\ChargeOrderAction

if (! $result->successful) {
    return back()->withErrors(['payment' => $result->message]);
}

// The order is now Paid — do not call markPaid() again.
$result->amount;      // Money charged through the gateway
$result->reference;   // your provider's reference
```

- Only a New or InProgress order is charged, decided under the order’s row lock before any store credit or gateway call: charging a Paid, Fulfilled, Canceled or Refunded order — a double-clicked “Pay”, or a second request holding a stale copy — throws IllegalStatusTransitionException with nothing charged.
- When store credit is enabled and not yet applied, and the customer is Creditable, credit is debited first.
- A zero balance (credit covered the order, or it is free) skips the gateway and succeeds with a zero amount.
- Otherwise the gateway’s charge() runs. It receives the Order and must charge $order->gatewayAmount() — never the final price, which would charge the credit share twice.
- On success the order moves New → InProgress (if still New) → Paid. The whole charge — credit, gateway call, move to Paid — runs in one transaction holding the order’s lock, so a concurrent second charge waits and is then refused. Keep your gateway’s charge() to the payment call itself; OrderPaid listeners run after the commit.
- A failed (declined) charge leaves the status unchanged and keeps no store credit. The amount charged includes the order’s snapshotted shipping.

```php
use RoundlyConsulting\Shops\Payments\PaymentResult;

new PaymentResult(bool $successful, Money $amount, ?string $reference = null, ?string $message = null);

PaymentResult::success($amount, reference: 'ch_123');
PaymentResult::failure($amount, message: 'Card declined');
```

## Refunds

Refunds are host-driven by design: the package never calls the gateway’s refund(). You decide the amount (at most $order->gatewayAmount()) and whether money goes back to the card at all — then transition the order:

```php
use RoundlyConsulting\Shops\Contracts\PaymentGateway;
use RoundlyConsulting\Shops\Facades\Shops;

// The gateway only took gatewayAmount() — the store-credit share never went through it.
$result = app(PaymentGateway::class)->refund($order, $order->gatewayAmount());

if ($result->successful) {
    Shops::order($order)->refund();   // Paid|Fulfilled → Refunded, fires OrderRefunded
}

// Give back any store-credit share yourself (when refund_to_store_credit is off):
$customer->modifyCreditsMoney($order->store_credit_applied, bucket: 'store_credit');
```

With payment.refund\_to\_store\_credit on, skip the gateway refund — Shops::order($order)->refund() alone credits the whole order total back as store credit, and doing both refunds the buyer twice.

## Shipping

A ShippingMethod quotes a cost for an order and a destination. The default FreeShippingMethod quotes zero in the order currency with the label Free shipping:

```php
use RoundlyConsulting\Money\Money;
use RoundlyConsulting\Shops\Contracts\ShippingMethod;
use RoundlyConsulting\Shops\Orders\DataTransferObjects\Address;
use RoundlyConsulting\Shops\Orders\Order;

final class FlatRateShipping implements ShippingMethod
{
    public function quote(Order $order, Address $destination): Money
    {
        return $destination->countryIso === 'DE'
            ? Money::ofMinor(490, $order->currency)
            : Money::ofMinor(1290, $order->currency);
    }

    public function label(): string
    {
        return 'Standard delivery';
    }
}
```

```php
use RoundlyConsulting\Shops\Facades\Shops;

$cost = Shops::order($order)->quoteShipping($order->shipping_address);   // Money

// The action form: app(Actions\Orders\QuoteShippingAction::class)->execute($order, $address)
```

Checkout quotes the shipping address through the bound method and snapshots the result onto the order, where it is part of the final price and the charge — see Placing orders. quoteShipping() prices a destination on demand without changing the order.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/shops-for-laravel.md)
