Locations represent physical places where you store inventory and ship orders from: warehouses, retail stores, fulfillment centers, or even your apartment. Each location has its own address and can track stock independently, so a product can have 50 units in your Paris warehouse and 20 in your New York store.
When Shopper is first installed, the setup wizard creates a default location using your store’s address. All stock mutations target this location unless you specify another one. If your business ships from multiple places, you can add more locations and assign stock to each one.
Model
The model used is Shopper\Core\Models\Inventory. It implements Shopper\Core\Models\Contracts\Inventory and is configurable via config/shopper/models.php.
Extending the Model
To add custom behavior, extend the model and update your configuration:
namespace App\Models;
use Shopper\Core\Models\Inventory as BaseInventory;
class Inventory extends BaseInventory
{
}
Update config/shopper/models.php:
return [
'inventory' => \App\Models\Inventory::class,
];
Database Schema
| Column | Type | Nullable | Default | Description |
|---|
id | bigint | no | auto | Primary key |
name | string | no | - | Location name |
code | string | no | - | Unique identifier code |
description | text | yes | null | Location description |
email | string | no | - | Location contact email (unique) |
street_address | string | no | - | Street address |
street_address_plus | string | yes | null | Additional address line |
postal_code | string | no | - | Postal/ZIP code |
city | string | no | - | City |
state | string | yes | null | State/province/region |
phone_number | string | yes | null | Phone number |
priority | integer | no | 0 | Fulfillment priority order |
latitude | decimal(11,5) | yes | null | GPS latitude |
longitude | decimal(10,5) | yes | null | GPS longitude |
is_default | boolean | no | false | Default location for stock |
country_id | bigint | no | - | FK to countries table |
created_at | timestamp | yes | null | Creation timestamp |
updated_at | timestamp | yes | null | Last update timestamp |
Relationships
Country
Each location belongs to a country:
$location->country; // Country model
Inventory Histories
Every stock mutation (increase, decrease, set) on any product or variant creates an InventoryHistory record tied to a specific location. This is how Shopper tracks stock per location.
$location->histories; // Collection of InventoryHistory records
Query Scopes
The default scope returns the location marked as the default inventory. This is the location used by InitialQuantityInventory when setting stock during product creation.
use Shopper\Core\Models\Inventory;
$defaultLocation = Inventory::query()->default()->first();
How Locations Connect to Stock
When you call mutateStock(), decreaseStock(), or setStock() on a product or variant, you pass an $inventoryId. This ties the stock mutation to a specific location:
$product->mutateStock(
inventoryId: $warehouse->id,
quantity: 100,
event: 'Shipment received',
);
To get stock for a product at a specific location:
$product->stockInventory($warehouse->id);
To batch-load stock for a specific location across multiple products (avoiding N+1):
use Shopper\Models\Product;
$products = Product::query()->get();
Product::loadStockForInventory($products, $warehouse->id);
Creating Locations
To create a new warehouse location:
use Shopper\Core\Models\Inventory;
use Shopper\Core\Models\Country;
$france = Country::query()->where('cca2', 'FR')->first();
$warehouse = Inventory::query()->create([
'name' => 'Paris Warehouse',
'code' => 'WH-PARIS',
'email' => 'paris@mystore.com',
'street_address' => '15 Rue du Commerce',
'city' => 'Paris',
'postal_code' => '75015',
'country_id' => $france->id,
'priority' => 1,
'latitude' => 48.8466,
'longitude' => 2.2958,
]);
Retrieving Locations
To get the default location:
$default = Inventory::query()->default()->first();
To get all locations ordered by priority:
$locations = Inventory::query()
->with('country')
->orderBy('priority')
->get();
To find the nearest location to a customer (if latitude/longitude are set):
$locations = Inventory::query()
->whereNotNull('latitude')
->whereNotNull('longitude')
->get()
->sortBy(function ($location) use ($customerLat, $customerLng) {
return sqrt(
pow($location->latitude - $customerLat, 2)
+ pow($location->longitude - $customerLng, 2)
);
});
$nearest = $locations->first();
Default Location
The default location is used as the origin address for shipping calculations and as the fallback when stock operations do not specify a location. Only one location can be the default.
Deleting a location removes all inventory history records tied to it. This affects stock levels for every product that had stock in that location. Proceed with caution.
Components
To customize the admin UI for location management:
php artisan shopper:component:publish setting
Location-related components in config/shopper/components/setting.php:
use Shopper\Livewire;
return [
'pages' => [
'location-index' => Livewire\Pages\Settings\Locations\Index::class,
'location-create' => Livewire\Pages\Settings\Locations\Create::class,
'location-edit' => Livewire\Pages\Settings\Locations\Edit::class,
],
'components' => [
'settings.locations.form' => Livewire\Components\Settings\Locations\InventoryForm::class,
],
];