NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Shops for Laravel

Extending, events & exceptions

The provider binds six contracts from config. Point the config key at your class, or bind the contract in your own service provider (a later binding wins). All classes live under RoundlyConsulting\Shops:

ContractConfig keyDefaultResponsibility
Contracts\PaymentGatewayshops.payment.gatewayPayments\NullPaymentGatewaycharge(Order): PaymentResult, refund(Order, Money): PaymentResult
Contracts\ShippingMethodshops.shipping.methodShipping\FreeShippingMethodquote(Order, Address): Money, label(): string
Contracts\TaxResolvershops.tax.resolverSupport\Tax\DatabaseTaxResolverrateFor(?Shop, string $taxClass, ?string $country): TaxRateValue
Contracts\DiscountResolvershops.discounts.resolverDiscounts\CouponPackageDiscountResolverresolve(string $code, Money $goods): DiscountResult
Orders\NumberGenerators\NumberGeneratorshops.orders.number_generatorOrders\NumberGenerators\DefaultNumberGeneratorgenerate(Order): string
Reviews\Contracts\VerifiedPurchaseResolvershops.reviews.verified_purchase_resolverReviews\NullVerifiedPurchaseResolververified(Model $author, Product $product): bool
// AppServiceProvider::register()
$this->app->bind(\RoundlyConsulting\Shops\Contracts\PaymentGateway::class, AcmePayGateway::class);

Also shipped: Support\Tax\ConfigTaxResolver and Reviews\DatabaseVerifiedPurchaseResolver. Swappable models: shops.shop_model (the tenant) and shops.discounts.coupon_model (a coupons Coupon subclass).

Traits for your own models

BelongsToShop, HasPublishing and HasTranslations work on any model. For translations, implement Translatable and cast each translatable attribute to array:

use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\Shops\Concerns\BelongsToShop;
use RoundlyConsulting\Shops\Concerns\HasPublishing;
use RoundlyConsulting\Shops\Concerns\HasTranslations;
use RoundlyConsulting\Shops\Contracts\Translatable;

final class Banner extends Model implements Translatable
{
    use BelongsToShop, HasPublishing, HasTranslations;

    public function translatableAttributes(): array
    {
        return ['headline'];
    }

    protected function casts(): array
    {
        return ['headline' => 'array', 'published_at' => 'datetime'];
    }
}

Banner::query()->forShop($shop)->published()->get();

Service classes

  • Orders\AddressBook (Shops::addresses()) — maps addresses-for-laravel addresses to order Address objects (defaults() returns OrderAddresses, map()).
  • Payments\StoreCreditTender — applies store credit (apply()) and returns it (restore(), which cancel() runs).
  • Shops\CurrentShop (Shops::current()) — the active tenant, scoped per request and per queued job.
  • Support\ShopModel and Support\CouponModel — resolve the configured models (class(), and query() on ShopModel).
  • Support\Casts\AddressCast — a JSON ⇄ Address cast, reusable on your own models.

Events

EventPropertiesFired by
OrderPlacedOrder $orderCheckout (PlaceOrderAction), after the transaction commits
OrderStatusChangedOrder $order, Status $from, Status $toEvery transition
OrderPaidOrder $order→ Paid
OrderFulfilledOrder $order→ Fulfilled
OrderCanceledOrder $order→ Canceled
OrderRefundedOrder $order→ Refunded (the package listens with GrantStoreCreditOnRefund)
StockAdjustedProductVariant $variant, StockAdjustment $adjustmentEvery stock write (AdjustStockAction)
StockRanLowProductVariant $variant, int $thresholdOnce, when an adjustment takes a tracked variant’s available stock from above the threshold to at or below it

During place-order, coupons-for-laravel fires its own CouponRedeemed, CouponRedemptionFailed and CouponExhausted events.

Exceptions

Package exceptions extend the abstract RoundlyConsulting\Shops\Exceptions\ShopsException (a RuntimeException):

ExceptionThrown when
Exceptions\ForeignItemExceptionA cart line updated or removed through another cart, or a return booked against an order the variant was never on — nothing is written.
Exceptions\InvalidQuantityExceptionA quantity outside 1..32767, a fraction, or a stock delta that is zero or has the wrong sign for its reason.
Inventory\Exceptions\InsufficientStockExceptionReserving or selling past available stock on a tracked variant.
Orders\Exceptions\CheckoutRefusedExceptionA checkout of an empty cart (also a double-submitted second checkout), of a line whose variant was deleted since it was added ($cartItem names it), or with a negative shipping cost — thrown before anything is written.
Orders\Exceptions\IllegalStatusTransitionExceptionA disallowed status move, or a charge of an order that is not New or InProgress — nothing changes.
Payments\Exceptions\StoreCreditAlreadyAppliedExceptionStore credit applied twice to one order.
Payments\Exceptions\StoreCreditBucketNotDenominatedThe store-credit bucket has no currency in credits.currencies.
Payments\Exceptions\StoreCreditCurrencyMismatchThe bucket’s currency differs from the order’s.

Money errors come from money-for-laravel and do not extend ShopsException — CurrencyMismatch (a variant in another currency, re-denominating a price column, mixed-currency math), UnknownCurrency and InvalidMoneyValue among others. Sluggable, coupons and credits throw their own exceptions.

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

By donating, you agree to our donation terms.

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.