Quantity-based pricing with multi-tenant and multi-scope support for any Eloquent model.
mrnewport/laravel-priceable is a Laravel package for quantity-based pricing with multi-tenant and multi-scope support for any eloquent model..
It currently has 2 GitHub stars and 3 downloads on Packagist (latest version v1.0.0).
Install it with composer require mrnewport/laravel-priceable.
Discover more Laravel packages by mrnewport
or browse all Laravel packages to compare alternatives.
Last updated
mrnewport/laravel-priceable is a Laravel package that provides quantity-based, multi-scope pricing for any Eloquent model. It allows you to define tiered pricing, attach custom price lists to specific users, and apply additional “scopes” (like region or channel) without the need for external dependencies.
This README walks you through installation, configuration, usage, advanced features, and testing.
MrNewport/LaravelPriceable offers a complete and robust solution for dynamic pricing in your Laravel application. By simply adding a trait to any Eloquent model, you can define:
Everything is self-contained, including the test suite, which uses an in-memory SQLite database and does not rely on your host application’s models or migrations.
Require via Composer:
composer require mrnewport/laravel-priceable
Optional: Publish the package config and migrations into your Laravel app (if you want to customize them in your project):
php artisan vendor:publish --provider="MrNewport\LaravelPriceable\Providers\PriceableServiceProvider" --tag=config
php artisan vendor:publish --provider="MrNewport\LaravelPriceable\Providers\PriceableServiceProvider" --tag=migrations
Run Migrations (if you published them or are auto-loading them):
php artisan migrate
Configure (Optional): If you want the package to auto-load its migrations instead of publishing them, set auto_load_migrations to true in config/priceable.php.
The package ships with a configuration file located at:
src/config/priceable.php
When published, it appears in your Laravel app’s config/priceable.php. Key settings include:
return [
'auto_load_migrations' => false,
'fallback_to_default_price' => true,
'throw_exception_if_no_price_found' => false,
'default_currency' => 'USD',
];
auto_load_migrations: If true, migrations inside src/database/migrations will be auto-loaded by the package.fallback_to_default_price: When a user’s price list doesn’t match, fallback to the “default” price (where price_list_id is null).throw_exception_if_no_price_found: If no price is found (even after fallback), throw a PriceNotFoundException instead of returning null.default_currency: The default currency used in your price records.All migration files reside in:
src/database/migrations/
If auto_load_migrations is set to false (default), you should publish and run them from your main Laravel application. Alternatively, set auto_load_migrations to true to have them loaded directly from this package.
These migrations create:
Priceable TraitAny Eloquent model you wish to have dynamic pricing must use the Priceable trait:
use MrNewport\LaravelPriceable\Models\Traits\Priceable;
class Product extends Model
{
use Priceable;
// Your other model code
}
This adds a polymorphic relationship to the prices table, letting you define multiple tiered prices for each product (or any other model).
You can define multiple quantity ranges by creating records in the prices relationship:
$product = Product::create([...]);
// For 1-9 units
$product->prices()->create([
'min_quantity' => 1,
'max_quantity' => 9,
'unit_price' => 100.00, // $100
]);
// For 10+ units (no upper bound)
$product->prices()->create([
'min_quantity' => 10,
'max_quantity' => null,
'unit_price' => 80.00, // $80
]);
If someone purchases 5 units, the price is $100; for 15 units, $80.
If you want specialized pricing for certain users:
Create a Price List:
use MrNewport\LaravelPriceable\Models\PriceList;
$vipList = PriceList::create([
'name' => 'VIP',
'description' => 'VIP Price List',
]);
Attach a user to that list:
// Assuming $user is an instance of your User model
$vipList->users()->attach($user->id);
Add special prices under that price list:
$product->prices()->create([
'price_list_id' => $vipList->id,
'min_quantity' => 1,
'max_quantity' => null,
'unit_price' => 60.00,
]);
When you call $product->priceFor(5, $user), the package checks the user’s VIP price list first, and if found, uses $60.
You can attach key-value scopes to any price. For instance, define a region=US or channel=B2B scope:
use MrNewport\LaravelPriceable\Models\PriceScope;
$scopedPrice = $product->prices()->create([
'min_quantity' => 1,
'max_quantity' => null,
'unit_price' => 95.00,
]);
// Attach scope region=US
$scopedPrice->scopes()->create([
'scope_type' => 'region',
'scope_value' => 'US',
]);
When retrieving the price, pass scopes as an associative array:
$price = $product->priceFor(10, $user, ['region' => 'US']);
Only prices containing all matching scopes (region=US) will be considered. If none match, it falls back as configured.
To get a final price, call priceFor($quantity, $user, $scopes = []):
$quantity = 12;
$user = auth()->user(); // or any user instance
$scopes = ['region' => 'US', 'channel' => 'B2B'];
$price = $product->priceFor($quantity, $user, $scopes);
Alternatively, you can use:
Facade:
use MrNewport\LaravelPriceable\Facades\Priceable;
$price = Priceable::getPrice($product, 12, $user, ['region' => 'US']);
Helper:
use function MrNewport\LaravelPriceable\Support\price_for;
$price = price_for($product, 12, $user, ['region' => 'US']);
If a user has no custom price (or no matching scope), the system can fallback to a default price (price_list_id = null). You can control this behavior via config:
fallback_to_default_price: true or false.throw_exception_if_no_price_found: If set to true, throws a PriceNotFoundException if no price is found after fallback.This ensures you never end up with an undefined or zero price unless you explicitly allow it.
Each price record can optionally have valid_from and valid_to fields:
$product->prices()->create([
'min_quantity' => 1,
'max_quantity' => 9,
'unit_price' => 100.00,
'valid_from' => now()->startOfMonth(),
'valid_to' => now()->endOfMonth(),
]);
If the current date/time is outside that range, the system ignores this price.
The tests are completely self-contained and use in-memory SQLite. No external references to your application’s models or factories.
To run tests:
vendor/bin/phpunit
tests directory, with a TestCase that sets up a test database in memory.ProductTestModel, UserTestModel) are defined, along with tables for them.src/database/migrations) are loaded to test every feature.This ensures the package is thoroughly verified in isolation.
This package is open-sourced software licensed under the MIT license.
Enjoy using MrNewport/LaravelPriceable for your advanced pricing needs!