├── .gitattributes
├── .github
└── workflows
│ └── laravel.yml
├── .gitignore
├── .styleci.yml
├── LICENSE
├── composer.json
├── config
└── laravel-inventory.php
├── contributing.md
├── database
└── migrations
│ └── create_inventories_table.php.stub
├── phpunit.xml
├── pint.json
├── readme.md
├── src
├── Events
│ └── InventoryUpdate.php
├── Exeptions
│ ├── InvalidInventory.php
│ └── InvalidInventoryModel.php
├── HasInventory.php
├── Inventory.php
└── LaravelInventoryServiceProvider.php
└── tests
├── HasInventoryTest.php
├── InventoryModel.php
├── Pest.php
└── TestCase.php
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/workflows/laravel.yml:
--------------------------------------------------------------------------------
1 | name: Laravel-Inventory Test
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | laravel-tests:
7 | runs-on: ${{ matrix.os }}
8 | strategy:
9 | fail-fast: false
10 | matrix:
11 | os: [ubuntu-latest]
12 | php: [8.2, 8.1, 8.0]
13 | laravel: [9.*, 10.*]
14 | dependency-vesrion: [prefer-lowest, prefer-stable]
15 | include:
16 | - laravel: 10.*
17 | testbench: ^8.0
18 | - laravel: 9.*
19 | testbench: 7.*
20 | exclude:
21 | - laravel: 10.*
22 | php: 8.0
23 |
24 | name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }}
25 | steps:
26 | - name: Checkout Code
27 | uses: actions/checkout@v3
28 |
29 | - name: Cache Composer packages
30 | id: composer-cache
31 | uses: actions/cache@v2
32 | with:
33 | path: vendor
34 | key: ${{ matrix.os }}-php-${{ hashFiles('**/composer.lock') }}
35 | restore-keys: |
36 | ${{ matrix.os }}-php-
37 |
38 | - name: Setup PHP
39 | uses: shivammathur/setup-php@v2
40 | with:
41 | php-version: ${{ matrix.php }}
42 | extensions: dom, curl, libxml, mbstring, zip
43 | tools: composer:v2
44 | coverage: none
45 |
46 | - name: Install dependencies
47 | run: |
48 | composer require "illuminate/contracts=${{ matrix.laravel }}" --no-update
49 | composer update --prefer-dist --no-interaction --no-progress
50 |
51 | - name: Execute Tests
52 | run: vendor/bin/pest
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /app
3 | /routes
4 | routes
5 | app
6 |
7 | .phpunit.result.cache
8 | composer.lock
9 | .DS_Store
10 | .php-cs-fixer.php
11 | .php-cs-fixer.cache
12 |
--------------------------------------------------------------------------------
/.styleci.yml:
--------------------------------------------------------------------------------
1 | preset: laravel
2 | version: 8.1
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Caryley
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "caryley/laravel-inventory",
3 | "type": "project",
4 | "description": "Inventory feature for laravel models.",
5 | "keywords": [
6 | "Laravel",
7 | "Laravel Inventory"
8 | ],
9 | "license": "MIT",
10 | "authors": [
11 | {
12 | "name": "Tal Elmishali",
13 | "email": "tal@caryley.dev",
14 | "homepage": "https://caryley.dev"
15 | }
16 | ],
17 | "homepage": "https://github.com/caryley/laravel-inventory",
18 | "require": {
19 | "php": "^8.0|^8.1",
20 | "illuminate/support": "^9.0|^10.0"
21 | },
22 | "require-dev": {
23 | "orchestra/testbench": "^7.0|^8.0",
24 | "pestphp/pest": "^1.22",
25 | "laravel/pint": "^1.4"
26 | },
27 | "autoload": {
28 | "psr-4": {
29 | "Caryley\\LaravelInventory\\": "src/"
30 | }
31 | },
32 | "autoload-dev": {
33 | "psr-4": {
34 | "Caryley\\LaravelInventory\\Tests\\": "tests"
35 | }
36 | },
37 | "extra": {
38 | "laravel": {
39 | "providers": [
40 | "Caryley\\LaravelInventory\\LaravelInventoryServiceProvider"
41 | ],
42 | "aliases": {
43 | "LaravelInventory": "Caryley\\LaravelInventory\\Facades\\LaravelInventory"
44 | }
45 | }
46 | },
47 | "minimum-stability": "dev",
48 | "prefer-stable": true,
49 | "config": {
50 | "allow-plugins": {
51 | "pestphp/pest-plugin": true
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/config/laravel-inventory.php:
--------------------------------------------------------------------------------
1 | Caryley\LaravelInventory\Inventory::class,
16 |
17 | /*
18 | |--------------------------------------------------------------------------
19 | | Default field attribute
20 | |--------------------------------------------------------------------------
21 | /
22 | / The name of the column which holds the key for the relationship with the model related to the inventory.
23 | / You can change this value if you have set a different name in the migration for the inventories
24 | / table. You might decide to go with the SKU field instead of the ID field.
25 | /
26 | */
27 | 'model_primary_field_attribute' => 'inventoriable_id',
28 | ];
29 |
--------------------------------------------------------------------------------
/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Contributions are welcome and will be fully credited.
4 |
5 | Contributions are accepted via Pull Requests on [Github](https://github.com/caryley/laravel-inventory).
6 |
7 | ## Pull Requests
8 |
9 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests.
10 |
11 | - **Document any change in behaviour** - Make sure the `readme.md` and any other relevant documentation are kept up-to-date.
12 |
13 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
14 |
15 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.
16 |
17 | **Happy coding**!
18 |
--------------------------------------------------------------------------------
/database/migrations/create_inventories_table.php.stub:
--------------------------------------------------------------------------------
1 | id();
18 | $table->integer('quantity')->default(0);
19 | $table->text('description')->nullable();
20 | $table->string('inventoriable_type');
21 | $table->unsignedBigInteger('inventoriable_id');
22 | $table->index(['inventoriable_type', 'inventoriable_id']);
23 | $table->timestamps();
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | *
30 | * @return void
31 | */
32 | public function down()
33 | {
34 | Schema::dropIfExists('inventories');
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 |
16 |
17 | ./tests/
18 |
19 |
20 |
21 |
22 | src/
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/pint.json:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "laravel"
3 | }
4 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Laravel Inventory
2 |
3 | ![GitHub Workflow Status][link-tests]
4 | [![Latest Version on Packagist][ico-version]][link-packagist]
5 | [![Total Downloads][ico-downloads]][link-downloads]
6 | [![StyleCI][ico-styleci]][link-styleci]
7 |
8 | The Laravel Inventory package helps track an inventory on any Laravel model.
9 |
10 |
11 |
12 | The package offers the following functionality:
13 |
14 | - Create and set a new inventory
15 | - Retrieve the current inventory
16 | - Manage inventory quantity
17 | - Clear an inventory
18 | - Determine if the model is in inventory or not.
19 | - Determine if the model has a valid inventory
20 | - Query scopes for inventoriable model
21 |
22 | ## Installation
23 |
24 | ```bash
25 | composer require caryley/laravel-inventory
26 | ```
27 |
28 | Publish the migration with:
29 |
30 | ```bash
31 | php artisan vendor:publish --provider="Caryley\LaravelInventory\LaravelInventoryServiceProvider" --tag="migrations"
32 | ```
33 |
34 | Or optionaly publish togther with `config` file:
35 |
36 | ```bash
37 | php artisan vendor:publish --provider="Caryley\LaravelInventory\LaravelInventoryServiceProvider"
38 | ```
39 |
40 | Migrate `inventories` table:
41 |
42 | ```bash
43 | php artisan migrate
44 | ```
45 |
46 | ## Usage
47 |
48 | Add the `HasInventory` trait to the model.
49 |
50 | ```php
51 | ...
52 | use Caryley\LaravelInventory\HasInventory;
53 |
54 | class Product extends Model
55 | {
56 | use HasInventory;
57 |
58 | ...
59 | }
60 | ```
61 |
62 | #### hasValidInventory()
63 |
64 | ```php
65 | $product->hasValidInventory(); // Determine if the model has a valid inventory.
66 | ```
67 |
68 | #### setInventory()
69 |
70 | ```php
71 | $product->setInventory(10); // $product->currentInventory()->quantity; (Will result in 10) | Not allowed to use negative numbers.
72 | ```
73 |
74 | #### currentInventory()
75 |
76 | ```php
77 | $product->currentInventory() //Return inventory instance if one exists, if not it will return null.
78 | ```
79 |
80 | #### addInventory()
81 |
82 | ```php
83 | $product->addInventory(); // Will increment inventory by 1.
84 |
85 | $product->addInventory(10); // Will increment inventory by 10.
86 | ```
87 |
88 | #### incrementInventory()
89 |
90 | ```php
91 | $product->incrementInventory(10); // Will increment inventory by 10.
92 | ```
93 |
94 | #### subtractInventory()
95 |
96 | ```php
97 | $product->subtractInventory(5); // Will subtract 5 from current inventory.
98 |
99 | $product->subtractInventory(-5); // Will subtract 5 from current inventory.
100 | ```
101 |
102 | #### decrementInventory()
103 |
104 | ```php
105 | $product->decrementInventory(5); // Will subtract 5 from current inventory.
106 |
107 | $product->decrementInventory(-5); // Will subtract 5 from current inventory.
108 | ```
109 |
110 | #### inInventory()
111 |
112 | ```php
113 | $product->inInventory(); // Will return a boolean if model inventory greater than 0.
114 |
115 | $product->inInventory(10); // Will return a boolean if model inventory greater than 10.
116 | ```
117 |
118 | #### notInInventory()
119 |
120 | ```php
121 | $product->notInInventory(); // Determine if model inventory is less than 0.
122 | ```
123 |
124 | #### clearInventory()
125 |
126 | ```php
127 | $product->clearInventory(); // Will clear all inventory for the model **Will delete all records, not only last record.
128 |
129 | $product->clearInventory(10); // Will clear the inventory for the model and will set new inventory of 10.
130 | ```
131 |
132 | ### Scopes
133 |
134 | #### InventoryIs()
135 |
136 | - The scope accepts the first argument as quantity, the second argument as the operator, and the third argument as a model id or array of ids.
137 |
138 | ```php
139 | Product::InventoryIs(10)->get(); // Return all products with inventory of 10.
140 |
141 | Product::InventoryIs(10, '<=')->get(); // Return all products with inventory of 10 or less.
142 |
143 | Product::InventoryIs(10, '>=', [1,2,3])->get(); // Return all products with inventory of 10 or greater where product id is 1,2,3
144 | ```
145 |
146 | #### InventoryIsNot()
147 |
148 | - The scope accepts a first argument of a quantity and a second argument of a model id or array of ids
149 |
150 | ```php
151 | Proudct::InventoryIsNot(10)->get(); // Return all products where inventory is not 10
152 |
153 | Proudct::InventoryIsNot(10, [1,2,3])->get(); // Return all products where inventory is not 10 where product id is 1,2,3
154 | ```
155 |
156 | ## Change log
157 |
158 | Please see the [changelog](changelog.md) for more information on what has changed recently.
159 |
160 | ## Testing
161 |
162 | ```bash
163 | composer test
164 | ```
165 |
166 | ## Contributing
167 |
168 | Please see [contributing.md](contributing.md) for details and a todolist.
169 |
170 | ## Security
171 |
172 | If you discover any security related issues, please email author email instead of using the issue tracker.
173 |
174 | ## Credits
175 |
176 | - [Tal Elmishali][link-author]
177 | - [All Contributors][link-contributors]
178 |
179 | ## Acknowledgements
180 |
181 | Laravel-Inventory draws inspiration from spatie/laravel-model-status & appstract/laravel-stock (even though it doesn't rely on any of them).
182 |
183 | ## License
184 |
185 | license. Please see the [license file](license.md) for more information.
186 |
187 | [ico-version]: https://img.shields.io/packagist/v/caryley/laravel-inventory.svg?style=flat-square
188 | [ico-downloads]: https://img.shields.io/packagist/dt/caryley/laravel-inventory.svg?style=flat-square
189 | [ico-styleci]: https://github.styleci.io/repos/334772924/shield?branch=master
190 | [link-packagist]: https://packagist.org/packages/caryley/laravel-inventory
191 | [link-downloads]: https://packagist.org/packages/caryley/laravel-inventory
192 | [link-tests]: https://github.com/Caryley/Laravel-Inventory/workflows/Laravel-Inventory%20Test/badge.svg
193 | [link-styleci]: https://github.styleci.io/repos/334772924?branch=master
194 | [link-author]: https://github.com/talelmishali
195 | [link-contributors]: ../../contributors
196 |
--------------------------------------------------------------------------------
/src/Events/InventoryUpdate.php:
--------------------------------------------------------------------------------
1 | morphMany($this->getInventoryModelClassName(), 'inventoriable')->latest('id');
27 | }
28 |
29 | /**
30 | * Return the current inventory on the model.
31 | *
32 | * @return \Illuminate\Database\Eloquent\Relations\MorphOne
33 | */
34 | public function latestInventory(): MorphOne
35 | {
36 | return $this->morphOne($this->getInventoryModelClassName(), 'inventoriable')->latestOfMany();
37 | }
38 |
39 | /**
40 | * @return Inventory|null
41 | */
42 | public function currentInventory(): Inventory|null
43 | {
44 | return $this->relationLoaded('inventories') ? $this->inventories->first() : $this->latestInventory;
45 | }
46 |
47 | /**
48 | * @return Inventory|null
49 | */
50 | public function inventory(): Inventory|null
51 | {
52 | return $this->currentInventory();
53 | }
54 |
55 | /**
56 | * @return bool
57 | */
58 | public function hasValidInventory(): bool
59 | {
60 | return (bool) $this->currentInventory();
61 | }
62 |
63 | /**
64 | * @param int $quantity
65 | * @return bool
66 | */
67 | public function inInventory(?int $quantity = 1): bool
68 | {
69 | if ($this->notInInventory()) {
70 | return false;
71 | }
72 |
73 | if ($this->currentInventory()->quantity < $quantity) {
74 | return false;
75 | }
76 |
77 | return true;
78 | }
79 |
80 | /**
81 | * @return bool
82 | */
83 | public function notInInventory(): bool
84 | {
85 | if (! $this->hasValidInventory()) {
86 | return true;
87 | }
88 |
89 | return $this->currentInventory()->quantity <= 0;
90 | }
91 |
92 | /**
93 | * Create or update model inventory.
94 | *
95 | * @param int $quantity
96 | * @param string $description
97 | * @return Inventory
98 | */
99 | public function setInventory(int $quantity, ?string $description = null): Inventory
100 | {
101 | $this->isValidInventory($quantity, $description);
102 |
103 | return $this->createInventory($quantity, $description);
104 | }
105 |
106 | /**
107 | * @param int $quantity
108 | * @param string $description
109 | * @return bool
110 | */
111 | public function incrementInventory(int $quantity = 1, ?string $description = null): Inventory
112 | {
113 | return $this->addInventory($quantity, $description);
114 | }
115 |
116 | /**
117 | * Add or create an inventory.
118 | *
119 | * @param int $addQuantity
120 | * @param string $description
121 | * @return Inventory
122 | */
123 | public function addInventory(int $addQuantity = 1, ?string $description = null): Inventory
124 | {
125 | $this->isValidInventory($addQuantity, $description);
126 |
127 | if ($this->notInInventory()) {
128 | return $this->createInventory($addQuantity, $description);
129 | }
130 |
131 | $newQuantity = $this->currentInventory()->quantity + $addQuantity;
132 |
133 | return $this->createInventory($newQuantity, $description);
134 | }
135 |
136 | /**
137 | * @param int $quantity
138 | * @param string $description
139 | * @return bool
140 | */
141 | public function decrementInventory(int $quantity = 1, ?string $description = null): Inventory
142 | {
143 | return $this->subtractInventory($quantity, $description);
144 | }
145 |
146 | /**
147 | * Subtract a given amount from the model inventory.
148 | *
149 | * @param int $subtractQuantity
150 | * @param string $description
151 | * @return Inventory
152 | */
153 | public function subtractInventory(int $subtractQuantity = 1, ?string $description = null): Inventory
154 | {
155 | $subtractQuantity = abs($subtractQuantity);
156 |
157 | $this->isValidInventory($subtractQuantity, $description);
158 |
159 | if ($this->notInInventory()) {
160 | throw InvalidInventory::subtract($subtractQuantity);
161 | }
162 |
163 | $newQuantity = $this->currentInventory()->quantity - abs($subtractQuantity);
164 |
165 | if ($newQuantity < 0) {
166 | throw InvalidInventory::negative($subtractQuantity);
167 | }
168 |
169 | return $this->createInventory($newQuantity, $description);
170 | }
171 |
172 | /**
173 | * Create a new inventory.
174 | */
175 | protected function createInventory(int $quantity, ?string $description = null): Inventory
176 | {
177 | $oldInventory = $this->currentInventory();
178 |
179 | $newInventory = $this->inventories()->create([
180 | 'quantity' => abs($quantity),
181 | 'description' => $description,
182 | ]);
183 |
184 | event(new InventoryUpdate($oldInventory, $newInventory, $this));
185 |
186 | return $newInventory;
187 | }
188 |
189 | /**
190 | * Delete the inventory from the model.
191 | *
192 | * @param int|null $newStock
193 | * @return Inventory|bool
194 | */
195 | public function clearInventory(?int $newStock = -1): Inventory|bool
196 | {
197 | $this->inventories()->delete();
198 |
199 | // Will return new Inventory instance of new inventory has been set
200 | return $newStock >= 0 ? $this->setInventory($newStock) : true;
201 | }
202 |
203 | /**
204 | * @throws InvalidInventory
205 | */
206 | protected function isValidInventory(int $quantity, ?string $description = null): ?bool
207 | {
208 | if (gmp_sign($quantity) === -1) {
209 | throw InvalidInventory::value($quantity);
210 | }
211 |
212 | return true;
213 | }
214 |
215 | /**
216 | * Scope inventory model for a givin quantity and operatior.
217 | *
218 | * @param \Illuminate\Database\Eloquent\Builder $builder
219 | * @param int $quantity
220 | * @param string $operator (<,>,<=,>=,=,<>)
221 | * @param array $inventoriableId
222 | * @return void
223 | */
224 | public function scopeInventoryIs(Builder $builder, $quantity = 0, string $operator = '=', array ...$inventoriableId): void
225 | {
226 | $inventoriableId = is_array($inventoriableId) ? Arr::flatten($inventoriableId) : func_get_args();
227 |
228 | $builder->whereHas('inventories', function (Builder $query) use ($operator, $quantity, $inventoriableId) {
229 | $query->when($inventoriableId, function ($query, $inventoriableId) {
230 | return $query->whereIn($this->getModelKeyColumnName(), $inventoriableId);
231 | })->where('quantity', $operator, $quantity)->whereIn('id', function (QueryBuilder $query) {
232 | $query->select(DB::raw('max(id)'))
233 | ->from($this->getInventoryTableName())
234 | ->where('inventoriable_type', $this->getInventoryModelType())
235 | ->whereColumn($this->getModelKeyColumnName(), $this->getQualifiedKeyName());
236 | });
237 | });
238 | }
239 |
240 | /**
241 | * Scope inventory model to everything other than given quantity.
242 | *
243 | * @param \Illuminate\Database\Eloquent\Builder $builder
244 | * @param int $quantity
245 | * @param array $inventoriableId
246 | * @return void
247 | */
248 | public function scopeInventoryIsNot(Builder $builder, int $quantity = 0, array ...$inventoriableId): void
249 | {
250 | $inventoriableId = is_array($inventoriableId) ? Arr::flatten($inventoriableId) : func_get_args();
251 |
252 | $builder->whereHas('inventories', function (Builder $query) use ($quantity, $inventoriableId) {
253 | $query->when($inventoriableId, function ($query, $inventoriableId) {
254 | return $query->whereIn($this->getModelKeyColumnName(), $inventoriableId);
255 | })->where('quantity', '<>', $quantity)->whereIn('id', function (QueryBuilder $query) {
256 | $query->select(DB::raw('max(id)'))
257 | ->from($this->getInventoryTableName())
258 | ->where('inventoriable_type', $this->getInventoryModelType())
259 | ->whereColumn($this->getModelKeyColumnName(), $this->getQualifiedKeyName());
260 | });
261 | });
262 | }
263 |
264 | /**
265 | * Return the table name for the inventory model.
266 | *
267 | * @return string
268 | */
269 | protected function getInventoryTableName(): string
270 | {
271 | $modelClass = $this->getInventoryModelClassName();
272 |
273 | return (new $modelClass)->getTable();
274 | }
275 |
276 | /**
277 | * @return string
278 | */
279 | protected function getInventoryModelType(): string
280 | {
281 | return array_search(static::class, Relation::morphMap()) ?: static::class;
282 | }
283 |
284 | /**
285 | * @return string
286 | */
287 | protected function getModelKeyColumnName(): string
288 | {
289 | return config('laravel-inventory.model_primary_field_attribute') ?? 'inventoriable_id';
290 | }
291 |
292 | /**
293 | * @return string
294 | */
295 | protected function getInventoryModelClassName(): string
296 | {
297 | return config('laravel-inventory.inventory_model');
298 | }
299 | }
300 |
--------------------------------------------------------------------------------
/src/Inventory.php:
--------------------------------------------------------------------------------
1 | 'integer',
16 | ];
17 |
18 | /**
19 | * @return \Illuminate\Database\Eloquent\Relations\MorphTo
20 | */
21 | public function model(): MorphTo
22 | {
23 | return $this->morphTo();
24 | }
25 |
26 | public function __toString()
27 | {
28 | return $this->name;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/LaravelInventoryServiceProvider.php:
--------------------------------------------------------------------------------
1 | mergeConfigFrom(__DIR__.'/../config/laravel-inventory.php', 'laravel-inventory');
18 | }
19 |
20 | /**
21 | * Bootstrap any application services.
22 | *
23 | * @return void
24 | */
25 | public function boot(): void
26 | {
27 | // Publishing is only necessary when using the CLI.
28 | if ($this->app->runningInConsole()) {
29 | $this->bootForConsole();
30 | }
31 |
32 | if (! class_exists('CreateInventoriesTable')) {
33 | $this->publishes([
34 | __DIR__.'/../database/migrations/create_inventories_table.php.stub' => database_path('migrations/2021_01_30_100000_create_inventories_table.php'),
35 | ], 'migrations');
36 | }
37 |
38 | // Publishing the configuration file.
39 | $this->publishes([
40 | __DIR__.'/../config/laravel-inventory.php' => config_path('laravel-inventory.php'),
41 | ], 'laravel-inventory.config');
42 |
43 | $this->guardAgainstInvalidInventoryModel();
44 | }
45 |
46 | /**
47 | * Console-specific booting.
48 | *
49 | * @return void
50 | */
51 | protected function bootForConsole(): void
52 | {
53 | // Publishing the configuration file.
54 | $this->publishes([
55 | __DIR__.'/../config/laravel-inventory.php' => config_path('laravel-inventory.php'),
56 | ], 'laravel-inventory.config');
57 |
58 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
59 | }
60 |
61 | /**
62 | * Guard against invalid inventory models.
63 | *
64 | * @return void
65 | */
66 | public function guardAgainstInvalidInventoryModel(): void
67 | {
68 | $modelClassName = config('laravel-inventory.inventory_model');
69 |
70 | if (! is_a($modelClassName, Inventory::class, true)) {
71 | throw InvalidInventoryModel::create($modelClassName);
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/tests/HasInventoryTest.php:
--------------------------------------------------------------------------------
1 | inventoryModel->currentInventory()->quantity)->toBe(0);
11 | expect($this->inventoryModel->notInInventory())->toBeTrue();
12 | });
13 |
14 | it('can set inventory', function () {
15 | expect($this->inventoryModel->currentInventory()->quantity)->toBe(0);
16 |
17 | $this->inventoryModel->setInventory(10);
18 | $this->inventoryModel->refresh();
19 |
20 | expect($this->inventoryModel->currentInventory()->quantity)->toBe(10);
21 | });
22 |
23 | it('can not set negative inventory', function () {
24 | $this->inventoryModel->setInventory(-1);
25 | })->throws(InvalidInventory::class, '-1 is an invalid quantity for an inventory.');
26 |
27 | it('can set inventory on a model without any inventory', function () {
28 | expect($this->secondInventoryModel->notInInventory())->toBeTrue();
29 |
30 | $this->secondInventoryModel->setInventory(10);
31 | $this->secondInventoryModel->refresh();
32 |
33 | expect($this->secondInventoryModel->currentInventory()->quantity)->toBe(10);
34 | });
35 |
36 | it('return true when inventory is existing and quantity match', function () {
37 | expect($this->inventoryModel->inInventory())->toBeFalse();
38 | $this->inventoryModel->setInventory(1);
39 |
40 | expect($this->inventoryModel->refresh()->currentInventory()->quantity)->toBe(1);
41 | expect($this->inventoryModel->inInventory())->toBeTrue();
42 |
43 | $this->inventoryModel->setInventory(3);
44 | $this->inventoryModel->refresh();
45 |
46 | expect($this->inventoryModel->inInventory(4))->toBeFalse();
47 | expect($this->inventoryModel->inInventory(3))->toBeTrue();
48 | });
49 |
50 | it('return false when inventory does not exist and checking inIventory', function () {
51 | expect($this->secondInventoryModel->inInventory())->toBeFalse();
52 | });
53 |
54 | it('return false when calling hasValidInventory on a model without inventory', function () {
55 | expect($this->secondInventoryModel->hasValidInventory())->toBeFalse();
56 | });
57 |
58 | it('return true when calling hasValidInventory on a model with inventory', function () {
59 | expect($this->inventoryModel->hasValidInventory())->toBeTrue();
60 | });
61 |
62 | it('prevent inventory from being set to a negative number', function () {
63 | $this->inventoryModel->setInventory(-1);
64 |
65 | expect($this->inventoryModel->currentInventory()->quantity)->toBe(0);
66 | })->throws(InvalidInventory::class, '-1 is an invalid quantity for an inventory.');
67 |
68 | it('inventory can be positive number', function () {
69 | $this->inventoryModel->setInventory(1);
70 | expect($this->inventoryModel->refresh()->currentInventory()->quantity)->toBe(1);
71 | });
72 |
73 | it('inventory can be incremented', function () {
74 | expect($this->inventoryModel->currentInventory()->quantity)->toBe(0);
75 |
76 | $this->inventoryModel->addInventory();
77 |
78 | $this->inventoryModel->refresh();
79 | expect($this->inventoryModel->currentInventory()->quantity)->toBe(1);
80 |
81 | $this->inventoryModel->addInventory();
82 |
83 | $this->inventoryModel->refresh();
84 | expect($this->inventoryModel->currentInventory()->quantity)->toBe(2);
85 | });
86 |
87 | it('inventory can be incremented by positive number', function () {
88 | expect($this->inventoryModel->currentInventory()->quantity)->toBe(0);
89 |
90 | $this->inventoryModel->addInventory(10);
91 |
92 | $this->inventoryModel->refresh();
93 | expect($this->inventoryModel->currentInventory()->quantity)->toBe(10);
94 | });
95 |
96 | it('increment inventory by using incrementInventory', function () {
97 | expect($this->inventoryModel->currentInventory()->quantity)->toBe(0);
98 |
99 | $this->inventoryModel->incrementInventory();
100 |
101 | $this->inventoryModel->refresh();
102 | expect($this->inventoryModel->currentInventory()->quantity)->toBe(1);
103 | });
104 |
105 | it('add to a non existing inventory', function () {
106 | $this->inventoryModel->clearInventory();
107 | expect($this->inventoryModel->inventory())->toBeNull();
108 |
109 | $this->inventoryModel->addInventory(5);
110 | $this->inventoryModel->refresh();
111 |
112 | expect($this->inventoryModel->currentInventory()->quantity)->toBe(5);
113 | });
114 |
115 | it('inventory can be decremented', function () {
116 | $this->inventoryModel->setInventory(1);
117 | expect($this->inventoryModel->inventories->first()->quantity)->toBe(1);
118 |
119 | $this->inventoryModel->subtractInventory();
120 |
121 | $this->inventoryModel->refresh();
122 | expect($this->inventoryModel->inventories->first()->quantity)->toBe(0);
123 | });
124 |
125 | it('decrement inventory by using decrementInventory', function () {
126 | $this->inventoryModel->setInventory(1);
127 | expect($this->inventoryModel->refresh()->currentInventory()->quantity)->toBe(1);
128 |
129 | $this->inventoryModel->decrementInventory();
130 |
131 | $this->inventoryModel->refresh();
132 | expect($this->inventoryModel->currentInventory()->quantity)->toBe(0);
133 | });
134 |
135 | it('converts inventory subtraction to absolute numbers', function () {
136 | $this->inventoryModel->setInventory(5);
137 | expect($this->inventoryModel->refresh()->currentInventory()->quantity)->toBe(5);
138 |
139 | $this->inventoryModel->subtractInventory(-4);
140 | $this->inventoryModel->refresh();
141 | expect($this->inventoryModel->refresh()->currentInventory()->quantity)->toBe(1);
142 | });
143 |
144 | test('inventory subtraction can not be negative', function () {
145 | $this->inventoryModel->setInventory(1);
146 | expect($this->inventoryModel->refresh()->currentInventory()->quantity)->toBe(1);
147 |
148 | $this->inventoryModel->subtractInventory(-2);
149 |
150 | $this->inventoryModel->refresh();
151 | expect($this->inventoryModel->currentInventory()->quantity)->toBe(1);
152 | })->throws(InvalidInventory::class, 'The inventory quantity is less than 0, unable to set quantity negative by the amount of: 2.');
153 |
154 | test('inventory subtraction can not be bellow 0', function () {
155 | $this->inventoryModel->clearInventory();
156 |
157 | $this->inventoryModel->subtractInventory(-2);
158 | $this->inventoryModel->refresh();
159 |
160 | expect($this->inventoryModel->currentInventory())->toBeNull();
161 | })->throws(InvalidInventory::class, 'The inventory quantity is 0 and unable to set quantity negative by the amount of: 2.');
162 |
163 | it('return current inventory', function () {
164 | expect($this->inventoryModel->inventories)
165 | ->count()->toBe(1)
166 | ->first()->toBeInstanceOf(Inventory::class);
167 |
168 | $this->inventoryModel->setInventory(20);
169 | $this->inventoryModel->refresh();
170 |
171 | expect($this->inventoryModel)
172 | ->currentInventory()->quantity->toBe(20)
173 | ->inventories->count()->toBe(2)
174 | ->fresh()->currentInventory()->quantity->toBe(20);
175 | });
176 |
177 | test('scope to find where inventory match the parameters passed to the scope', function () {
178 | expect($this->inventoryModel->currentInventory()->quantity)->toBe(0);
179 | expect($this->inventoryModel->id)->toBe(InventoryModel::InventoryIs(0)->get()->first()->id);
180 |
181 | $this->inventoryModel->setInventory(10);
182 | $this->inventoryModel->refresh();
183 |
184 | expect($this->inventoryModel)
185 | ->currentInventory()->quantity->toBe(10)
186 | ->id->toBe(InventoryModel::InventoryIs(10)->get()->first()->id)
187 | ->id->toBe(InventoryModel::InventoryIs(10, '>=')->get()->first()->id)
188 | ->id->toBe(InventoryModel::InventoryIs(9, '>')->get()->first()->id)
189 | ->id->toBe(InventoryModel::InventoryIs(9, '>=')->get()->first()->id)
190 | ->id->toBe(InventoryModel::InventoryIs(10, '<=')->get()->first()->id);
191 |
192 | expect(InventoryModel::InventoryIs(9, '<')->get()->first())->toBeNull();
193 | expect(InventoryModel::InventoryIs(9, '<=')->get()->first())->toBeNull();
194 | });
195 |
196 | test('scope to find where inventory is the parameters passed to the scope in multiple models', function () {
197 | $this->secondInventoryModel->setInventory(20);
198 | $this->secondInventoryModel->refresh();
199 |
200 | expect($this->inventoryModel->currentInventory()->quantity)->toBe(0);
201 | expect(InventoryModel::InventoryIs(0, '=', [1, 2])->get())
202 | ->first()->id->toBe($this->inventoryModel->id)
203 | ->count()->toBe(1);
204 |
205 | expect($this->secondInventoryModel->currentInventory()->quantity)->toBe(20);
206 | expect(InventoryModel::InventoryIs(20, '=', [1, 2])->get())
207 | ->first()->id->toBe($this->secondInventoryModel->id)
208 | ->count()->toBe(1);
209 | });
210 |
211 | test('scope to find where inventory is the opposite then parameters passed to the scope', function () {
212 | expect($this->inventoryModel->currentInventory()->quantity)->toBe(0);
213 |
214 | expect($this->inventoryModel->id)
215 | ->toBe(InventoryModel::InventoryIsNot(10, [1])->get()->first()->id)
216 | ->toBe(InventoryModel::InventoryIs(0)->get()->first()->id)
217 | ->toBe(InventoryModel::InventoryIsNot(1)->get()->first()->id);
218 | });
219 |
220 | it('clear inventory and destroy all inventory records', function () {
221 | expect($this->inventoryModel->inventories)->count()->toBe(1);
222 |
223 | $this->inventoryModel->clearInventory();
224 | $this->inventoryModel->refresh();
225 |
226 | expect($this->inventoryModel)
227 | ->currentInventory()->toBeNull()
228 | ->inventories->count()->toBe(0);
229 | });
230 |
231 | it('clear inventory and set new inventory at the same time', function () {
232 | $this->inventoryModel->clearInventory(10);
233 | $this->inventoryModel->refresh();
234 |
235 | expect($this->inventoryModel)
236 | ->inventories->count()->toBe(1)
237 | ->currentInventory()->quantity->toBe(10);
238 | });
239 |
240 | it('dispatch an event when inventory is maniuplated', function () {
241 | Event::fake();
242 |
243 | $this->inventoryModel->setInventory(2);
244 |
245 | Event::assertDispatched(InventoryUpdate::class);
246 | });
247 |
--------------------------------------------------------------------------------
/tests/InventoryModel.php:
--------------------------------------------------------------------------------
1 | in(__DIR__);
6 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | set('database.default', 'testbench');
28 | $app['config']->set('database.connections.testbench', [
29 | 'driver' => 'sqlite',
30 | 'database' => ':memory:',
31 | 'prefix' => '',
32 | ]);
33 | }
34 |
35 | protected function useSqliteConnection($app): void
36 | {
37 | $app->config->set('database.default', 'sqlite');
38 | }
39 |
40 | protected function setUp(): void
41 | {
42 | parent::setUp();
43 |
44 | $this->setUpDatabase($this->app);
45 |
46 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
47 |
48 | $this->inventoryModel = InventoryModel::first();
49 |
50 | $this->secondInventoryModel = InventoryModel::find(2);
51 | }
52 |
53 | protected function setUpDatabase($app): void
54 | {
55 | $builder = $app['db']->connection()->getSchemaBuilder();
56 |
57 | $builder->create('inventory_model', function (Blueprint $table) {
58 | $table->increments('id');
59 | $table->string('name');
60 | $table->timestamps();
61 | });
62 |
63 | $builder->create('inventories', function (Blueprint $table) {
64 | $table->id();
65 | $table->integer('quantity')->default(0);
66 | $table->text('description')->nullable();
67 | $table->string('inventoriable_type');
68 | $table->unsignedBigInteger('inventoriable_id');
69 | $table->index(['inventoriable_type', 'inventoriable_id']);
70 | $table->timestamps();
71 | });
72 |
73 | InventoryModel::create([
74 | 'name' => 'InventoryModel',
75 | ]);
76 |
77 | InventoryModel::create([
78 | 'name' => 'SecondInventoryModel',
79 | ]);
80 |
81 | Inventory::create([
82 | 'quantity' => '0',
83 | 'description' => 'Inventory description',
84 | 'inventoriable_type' => 'Caryley\LaravelInventory\Tests\InventoryModel',
85 | 'inventoriable_id' => '1',
86 | ]);
87 | }
88 |
89 | /**
90 | * Get package providers.
91 | *
92 | * @param Application $app
93 | * @return array
94 | */
95 | protected function getPackageProviders($app): array
96 | {
97 | return [
98 | LaravelInventoryServiceProvider::class,
99 | ];
100 | }
101 | }
102 |
--------------------------------------------------------------------------------