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:
| Contract | Config key | Default | Responsibility |
|---|---|---|---|
Contracts\PaymentGateway | shops.payment.gateway | Payments\NullPaymentGateway | charge(Order): PaymentResult, refund(Order, Money): PaymentResult |
Contracts\ShippingMethod | shops.shipping.method | Shipping\FreeShippingMethod | quote(Order, Address): Money, label(): string |
Contracts\TaxResolver | shops.tax.resolver | Support\Tax\DatabaseTaxResolver | rateFor(?Shop, string $taxClass, ?string $country): TaxRateValue |
Contracts\DiscountResolver | shops.discounts.resolver | Discounts\CouponPackageDiscountResolver | resolve(string $code, Money $goods): DiscountResult |
Orders\NumberGenerators\NumberGenerator | shops.orders.number_generator | Orders\NumberGenerators\DefaultNumberGenerator | generate(Order): string |
Reviews\Contracts\VerifiedPurchaseResolver | shops.reviews.verified_purchase_resolver | Reviews\NullVerifiedPurchaseResolver | verified(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
| Event | Properties | Fired by |
|---|---|---|
OrderPlaced | Order $order | Checkout (PlaceOrderAction), after the transaction commits |
OrderStatusChanged | Order $order, Status $from, Status $to | Every transition |
OrderPaid | Order $order | → Paid |
OrderFulfilled | Order $order | → Fulfilled |
OrderCanceled | Order $order | → Canceled |
OrderRefunded | Order $order | → Refunded (the package listens with GrantStoreCreditOnRefund) |
StockAdjusted | ProductVariant $variant, StockAdjustment $adjustment | Every stock write (AdjustStockAction) |
StockRanLow | ProductVariant $variant, int $threshold | Once, 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):
| Exception | Thrown when |
|---|---|
Exceptions\ForeignItemException | A cart line updated or removed through another cart, or a return booked against an order the variant was never on — nothing is written. |
Exceptions\InvalidQuantityException | A quantity outside 1..32767, a fraction, or a stock delta that is zero or has the wrong sign for its reason. |
Inventory\Exceptions\InsufficientStockException | Reserving or selling past available stock on a tracked variant. |
Orders\Exceptions\CheckoutRefusedException | A 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\IllegalStatusTransitionException | A disallowed status move, or a charge of an order that is not New or InProgress — nothing changes. |
Payments\Exceptions\StoreCreditAlreadyAppliedException | Store credit applied twice to one order. |
Payments\Exceptions\StoreCreditBucketNotDenominated | The store-credit bucket has no currency in credits.currencies. |
Payments\Exceptions\StoreCreditCurrencyMismatch | The 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 cryptoBy 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.