├── .gitignore ├── .travis.yml ├── README.md ├── composer.json ├── config ├── .gitkeep └── config.php ├── migrations ├── 2015_06_02_202821_create_products_table.php ├── 2015_06_02_202822_create_products_options_table.php ├── 2015_06_02_202823_create_products_options_values_table.php ├── 2015_06_02_202824_create_products_variants_table.php ├── 2015_06_02_202825_create_products_properties_table.php ├── 2015_06_02_202826_create_products_properties_values_table.php ├── 2015_06_02_202827_create_products_images_table.php ├── 2015_06_02_202923_create_orders_table.php └── 2015_06_02_202924_create_orders_items_table.php ├── phpunit.xml ├── src ├── Cart │ ├── CartServiceProvider.php │ ├── Context │ │ ├── CartContext.php │ │ └── CartContextInterface.php │ ├── Facades │ │ └── Cart.php │ ├── Models │ │ ├── Cart.php │ │ ├── CartInterface.php │ │ ├── CartItem.php │ │ └── CartItemInterface.php │ ├── Resolvers │ │ ├── ItemResolver.php │ │ ├── ItemResolverInterface.php │ │ └── ItemResolvingException.php │ └── Storage │ │ └── StorageInterface.php ├── Inventory │ └── Models │ │ └── StockableInterface.php ├── Order │ └── Models │ │ ├── Order.php │ │ ├── OrderInterface.php │ │ ├── OrderItem.php │ │ └── OrderItemInterface.php ├── Pricing │ └── Models │ │ └── PriceableInterface.php ├── Product │ ├── Builder │ │ ├── ProductBuilder.php │ │ └── ProductBuilderInterface.php │ ├── Facades │ │ └── ProductBuilder.php │ ├── Models │ │ ├── Image.php │ │ ├── ImageInterface.php │ │ ├── Option.php │ │ ├── OptionInterface.php │ │ ├── OptionTranslation.php │ │ ├── OptionValue.php │ │ ├── OptionValueInterface.php │ │ ├── OptionValueTranslation.php │ │ ├── Product.php │ │ ├── ProductInterface.php │ │ ├── ProductTranslation.php │ │ ├── Property.php │ │ ├── PropertyInterface.php │ │ ├── PropertySubjectInterface.php │ │ ├── PropertyTranslation.php │ │ ├── PropertyValue.php │ │ ├── PropertyValueInterface.php │ │ ├── VariableInterface.php │ │ ├── Variant.php │ │ └── VariantInterface.php │ └── Observer │ │ ├── ProductObserver.php │ │ ├── ProductTranslationObserver.php │ │ └── PropertyValueObserver.php ├── Promotion │ └── Models │ │ ├── ActionInterface.php │ │ ├── CouponInterface.php │ │ ├── PromotionInterface.php │ │ └── RuleInterface.php └── StoreServiceProvider.php └── tests ├── Product ├── Builder │ └── ProductBuilderTest.php └── Models │ ├── OptionTest.php │ ├── OptionValueTest.php │ ├── ProductTest.php │ └── VariantTest.php └── TestBase.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /bin 3 | 4 | composer.phar 5 | composer.lock 6 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | 8 | before_script: 9 | - composer self-update 10 | - composer install --dev --prefer-source 11 | 12 | #script: vendor/bin/phpspec run -fpretty --verbose 13 | script: phpunit --coverage-text 14 | 15 | notifications: 16 | email: "adrian@blunier.net" 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel e-commerce [![Build Status](https://travis-ci.org/ablunier/laravel-ecommerce.svg?branch=master)](https://travis-ci.org/ablunier/laravel-ecommerce) 2 | 3 | This package aims to be a full featured e-commerce platform written for the Laravel PHP framework. It is designed to make programming commerce applications easier by making several assumptions about what most developers needs to get started. 4 | 5 | > **Note:** This package isn't finished and NOT ready for production. Currently can't dedicate time to it, so consider forking or sending PR to it. Maybe you can use it as a starting point for your e-commerce project. 6 | 7 | ### Features 8 | * Full featured flexible e-commerce. 9 | * Simple API. 10 | 11 | ### Requirements 12 | * PHP 5.4 or higher. 13 | * Laravel 5. 14 | 15 | ## Installation 16 | 17 | Require this package in your `composer.json` and update composer: 18 | 19 | ```json 20 | "ablunier/laravel-ecommerce": "dev-master" 21 | ``` 22 | 23 | After updating composer, add the ServiceProvider to the providers array in `config/app.php`: 24 | 25 | ```php 26 | 'ANavallaSuiza\Ecommerce\StoreServiceProvider', 27 | ``` 28 | 29 | To publish the config settings and migrations in Laravel 5 use: 30 | 31 | ```php 32 | php artisan vendor:publish 33 | ``` 34 | 35 | ## Documentation 36 | 37 | Visit the [wiki](https://github.com/ablunier/laravel-ecommerce/wiki) for more information about understanding the core concepts and how to use this package. 38 | 39 | ## License 40 | 41 | This software is published under the MIT License 42 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ablunier/laravel-ecommerce", 3 | "description": "", 4 | "authors": [ 5 | { 6 | "name": "Adrian P. Blunier", 7 | "email": "adrian@blunier.net" 8 | } 9 | ], 10 | "require": { 11 | "php": ">=5.4.0", 12 | "illuminate/support": "5.0.*", 13 | "illuminate/database": "5.0.*", 14 | "dimsav/laravel-translatable": "5.1", 15 | "javiereguiluz/easyslugger": "1.0.*" 16 | }, 17 | "require-dev": { 18 | "orchestra/testbench": "~3.0", 19 | "phpunit/phpunit": "~4.0", 20 | "mockery/mockery": "0.9.*" 21 | }, 22 | "config": { 23 | "bin-dir": "bin/" 24 | }, 25 | "autoload": { 26 | "classmap": [ 27 | "migrations", 28 | "tests" 29 | ], 30 | "psr-4": { 31 | "ANavallaSuiza\\Ecommerce\\": "src" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "ANavallaSuiza\\Tests\\": "tests" 37 | } 38 | }, 39 | "minimum-stability": "dev" 40 | } 41 | -------------------------------------------------------------------------------- /config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ablunier/laravel-ecommerce/8a69f0ce79d6e1e40efd43cb6da430356ca31f21/config/.gitkeep -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | 'ANavallaSuiza\Ecommerce\Product\Models\Product' 13 | ]; 14 | -------------------------------------------------------------------------------- /migrations/2015_06_02_202821_create_products_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | 18 | $table->datetime('available_on'); 19 | 20 | $table->timestamps(); 21 | $table->softDeletes(); 22 | }); 23 | 24 | Schema::create('products_translations', function (Blueprint $table) { 25 | $table->increments('id'); 26 | 27 | $table->unsignedInteger('product_id'); 28 | $table->string('locale')->index(); 29 | 30 | $table->string('slug'); 31 | $table->string('name'); 32 | $table->text('short_description')->nullable(); 33 | $table->text('description')->nullable(); 34 | $table->string('meta_keywords')->nullable(); 35 | $table->string('meta_description')->nullable(); 36 | 37 | $table->unique(['product_id','locale']); 38 | }); 39 | 40 | Schema::table('products_translations', function (Blueprint $table) { 41 | $table->foreign('product_id') 42 | ->references('id')->on('products') 43 | ->onDelete('cascade'); 44 | }); 45 | } 46 | 47 | /** 48 | * Reverse the migrations. 49 | * 50 | * @return void 51 | */ 52 | public function down() 53 | { 54 | Schema::dropIfExists('products_translations'); 55 | Schema::dropIfExists('products'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /migrations/2015_06_02_202822_create_products_options_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | 18 | $table->string('name'); 19 | 20 | $table->unsignedInteger('product_id'); 21 | 22 | $table->timestamps(); 23 | }); 24 | 25 | Schema::create('products_options_translations', function (Blueprint $table) { 26 | $table->increments('id'); 27 | 28 | $table->unsignedInteger('product_option_id'); 29 | $table->string('locale')->index(); 30 | 31 | $table->string('presentation'); 32 | 33 | $table->unique(['product_option_id','locale']); 34 | }); 35 | 36 | Schema::table('products_options', function (Blueprint $table) { 37 | $table->foreign('product_id') 38 | ->references('id')->on('products') 39 | ->onDelete('cascade'); 40 | }); 41 | 42 | Schema::table('products_options_translations', function (Blueprint $table) { 43 | $table->foreign('product_option_id') 44 | ->references('id')->on('products_options') 45 | ->onDelete('cascade'); 46 | }); 47 | } 48 | 49 | /** 50 | * Reverse the migrations. 51 | * 52 | * @return void 53 | */ 54 | public function down() 55 | { 56 | Schema::dropIfExists('products_options_translations'); 57 | Schema::dropIfExists('products_options'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /migrations/2015_06_02_202823_create_products_options_values_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | 18 | $table->unsignedInteger('product_option_id'); 19 | }); 20 | 21 | Schema::create('products_options_values_translations', function (Blueprint $table) { 22 | $table->increments('id'); 23 | 24 | $table->unsignedInteger('product_option_value_id'); 25 | $table->string('locale')->index(); 26 | 27 | $table->string('value'); 28 | 29 | $table->unique(['product_option_value_id','locale'], 'options_values_translations_id_locale_unique'); 30 | }); 31 | 32 | Schema::table('products_options_values', function (Blueprint $table) { 33 | $table->foreign('product_option_id') 34 | ->references('id')->on('products_options') 35 | ->onDelete('cascade'); 36 | }); 37 | 38 | Schema::table('products_options_values_translations', function (Blueprint $table) { 39 | $table->foreign('product_option_value_id', 'option_value_translation_id_foreign') 40 | ->references('id')->on('products_options_values') 41 | ->onDelete('cascade'); 42 | }); 43 | } 44 | 45 | /** 46 | * Reverse the migrations. 47 | * 48 | * @return void 49 | */ 50 | public function down() 51 | { 52 | Schema::dropIfExists('products_options_values_translations'); 53 | Schema::dropIfExists('products_options_values'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /migrations/2015_06_02_202824_create_products_variants_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | 18 | $table->boolean('master'); 19 | $table->string('sku'); 20 | $table->integer('price'); 21 | $table->integer('on_hand'); 22 | $table->boolean('available_on_demand'); 23 | $table->decimal('weight')->nullable(); 24 | $table->decimal('width')->nullable(); 25 | $table->decimal('height')->nullable(); 26 | $table->decimal('depth')->nullable(); 27 | 28 | $table->unsignedInteger('product_id'); 29 | 30 | $table->timestamps(); 31 | $table->softDeletes(); 32 | }); 33 | 34 | Schema::create('products_variants_options_values', function (Blueprint $table) { 35 | $table->unsignedInteger('product_variant_id'); 36 | $table->unsignedInteger('product_option_value_id'); 37 | }); 38 | 39 | Schema::table('products_variants', function (Blueprint $table) { 40 | $table->foreign('product_id') 41 | ->references('id')->on('products') 42 | ->onDelete('cascade'); 43 | }); 44 | 45 | Schema::table('products_variants_options_values', function (Blueprint $table) { 46 | $table->foreign('product_variant_id') 47 | ->references('id')->on('products_variants') 48 | ->onDelete('cascade'); 49 | 50 | $table->foreign('product_option_value_id') 51 | ->references('id')->on('products_options_values'); 52 | }); 53 | } 54 | 55 | /** 56 | * Reverse the migrations. 57 | * 58 | * @return void 59 | */ 60 | public function down() 61 | { 62 | Schema::dropIfExists('products_variants_options_values'); 63 | Schema::dropIfExists('products_variants'); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /migrations/2015_06_02_202825_create_products_properties_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | 18 | $table->string('name'); 19 | }); 20 | 21 | Schema::create('products_properties_translations', function (Blueprint $table) { 22 | $table->increments('id'); 23 | 24 | $table->unsignedInteger('property_id'); 25 | $table->string('locale')->index(); 26 | 27 | $table->string('presentation'); 28 | 29 | $table->unique(['property_id','locale']); 30 | }); 31 | 32 | Schema::table('products_properties_translations', function (Blueprint $table) { 33 | $table->foreign('property_id') 34 | ->references('id')->on('products_properties') 35 | ->onDelete('cascade'); 36 | }); 37 | } 38 | 39 | /** 40 | * Reverse the migrations. 41 | * 42 | * @return void 43 | */ 44 | public function down() 45 | { 46 | Schema::dropIfExists('products_properties_translations'); 47 | Schema::dropIfExists('products_properties'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /migrations/2015_06_02_202826_create_products_properties_values_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | 18 | $table->string('value'); 19 | 20 | $table->unsignedInteger('product_id'); 21 | $table->unsignedInteger('property_id'); 22 | }); 23 | 24 | Schema::table('products_properties_values', function (Blueprint $table) { 25 | $table->foreign('product_id') 26 | ->references('id')->on('products') 27 | ->onDelete('cascade'); 28 | 29 | $table->foreign('property_id') 30 | ->references('id')->on('products_properties'); 31 | }); 32 | } 33 | 34 | /** 35 | * Reverse the migrations. 36 | * 37 | * @return void 38 | */ 39 | public function down() 40 | { 41 | Schema::dropIfExists('products_properties_values'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /migrations/2015_06_02_202827_create_products_images_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | 18 | $table->string('path'); 19 | 20 | $table->unsignedInteger('product_variant_id'); 21 | }); 22 | 23 | Schema::table('products_images', function (Blueprint $table) { 24 | $table->foreign('product_variant_id') 25 | ->references('id')->on('products_variants') 26 | ->onDelete('cascade'); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('products_images'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /migrations/2015_06_02_202923_create_orders_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | 18 | $table->datetime('completed_at'); 19 | $table->string('number'); 20 | $table->integer('items_total'); 21 | $table->integer('total'); 22 | $table->boolean('confirmed'); 23 | $table->string('confirmation_token'); 24 | $table->string('state'); 25 | $table->string('email'); 26 | 27 | $table->timestamps(); 28 | $table->softDeletes(); 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | * 35 | * @return void 36 | */ 37 | public function down() 38 | { 39 | Schema::dropIfExists('orders'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /migrations/2015_06_02_202924_create_orders_items_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | 18 | $table->unsignedInteger('orders_id'); 19 | $table->integer('quantity'); 20 | $table->integer('unit_price'); 21 | $table->integer('total'); 22 | 23 | }); 24 | 25 | Schema::table('orders_items', function (Blueprint $table) { 26 | $table->foreign('orders_id') 27 | ->references('id')->on('orders') 28 | ->onDelete('cascade'); 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | * 35 | * @return void 36 | */ 37 | public function down() 38 | { 39 | Schema::dropIfExists('orders_items'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | 20 | 21 | ./src/ 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Cart/CartServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind('ANavallaSuiza\Ecommerce\Cart\Models\CartInterface', function () { 34 | return new Cart; 35 | }); 36 | } 37 | 38 | /** 39 | * Get the services provided by the provider. 40 | * 41 | * @return array 42 | */ 43 | public function provides() 44 | { 45 | return ['ANavallaSuiza\Ecommerce\Cart\Models\CartInterface']; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Cart/Context/CartContext.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 18 | } 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function getCurrentCart() 24 | { 25 | return $this->storage->getData(self::STORAGE_KEY); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function setCurrentCart(CartInterface $cart) 32 | { 33 | $this->storage->setData(self::STORAGE_KEY, $cart); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function resetCurrentCart() 40 | { 41 | $this->storage->removeData(self::STORAGE_KEY); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Cart/Context/CartContextInterface.php: -------------------------------------------------------------------------------- 1 | incrementExpiresAt(); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function getIdentifier() 30 | { 31 | return $this->id; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getExpiresAt() 38 | { 39 | return $this->expiresAt; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function setExpiresAt(\DateTime $expiresAt = null) 46 | { 47 | $this->expiresAt = $expiresAt; 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function incrementExpiresAt() 56 | { 57 | $expiresAt = new \DateTime(); 58 | $expiresAt->add(new \DateInterval('PT3H')); 59 | 60 | $this->expiresAt = $expiresAt; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function isExpired() 69 | { 70 | return $this->getExpiresAt() < new \DateTime(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Cart/Models/CartInterface.php: -------------------------------------------------------------------------------- 1 | productModel = $product; 14 | } 15 | 16 | /** 17 | * {@inheritdoc} 18 | */ 19 | public function resolve($id, $quantity) 20 | { 21 | $product = $this->productModel->findOrFail($id); 22 | 23 | $cartItem = new CartItem; 24 | 25 | $cartItem->product()->associate($product); 26 | $cartItem->setQuantity($quantity); 27 | 28 | return $cartItem; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Cart/Resolvers/ItemResolverInterface.php: -------------------------------------------------------------------------------- 1 | state = OrderInterface::STATE_CART; 29 | $this->items = new Collection(); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function items() 36 | { 37 | return $this->hasMany('ANavallaSuiza\Ecommerce\Order\Models\OrderItem'); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function getEmail() 44 | { 45 | return $this->email; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function setEmail($email) 52 | { 53 | $this->email = $email; 54 | 55 | return $this; 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function isCompleted() 62 | { 63 | return null !== $this->completed_at; 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function complete() 70 | { 71 | $this->completed_at = new \DateTime(); 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function getCompletedAt() 80 | { 81 | return $this->completed_at; 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | public function setCompletedAt(\DateTime $completedAt = null) 88 | { 89 | $this->completed_at = $completedAt; 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | public function getItems() 98 | { 99 | return $this->items; 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | */ 105 | public function setItems(Collection $items) 106 | { 107 | $this->items = $items; 108 | 109 | return $this; 110 | } 111 | 112 | /** 113 | * {@inheritdoc} 114 | */ 115 | public function countItems() 116 | { 117 | return $this->items->count(); 118 | } 119 | 120 | /** 121 | * {@inheritdoc} 122 | */ 123 | public function clearItems() 124 | { 125 | // $this->items->clear(); ?? 126 | 127 | return $this; 128 | } 129 | 130 | /** 131 | * {@inheritdoc} 132 | */ 133 | public function addItem(OrderItemInterface $item) 134 | { 135 | if ($this->hasItem($item)) { 136 | return $this; 137 | } 138 | 139 | $this->items->push($item); 140 | 141 | return $this; 142 | } 143 | 144 | /** 145 | * {@inheritdoc} 146 | */ 147 | public function removeItem(OrderItemInterface $item) 148 | { 149 | if ($this->hasItem($item)) { 150 | // $this->items->forget($item); ?? 151 | } 152 | 153 | return $this; 154 | } 155 | 156 | /** 157 | * {@inheritdoc} 158 | */ 159 | public function hasItem(OrderItemInterface $item) 160 | { 161 | return false; 162 | } 163 | 164 | /** 165 | * {@inheritdoc} 166 | */ 167 | public function isEmpty() 168 | { 169 | 170 | } 171 | 172 | /** 173 | * {@inheritdoc} 174 | */ 175 | public function getItemsTotal() 176 | { 177 | 178 | } 179 | 180 | /** 181 | * {@inheritdoc} 182 | */ 183 | public function calculateItemsTotal() 184 | { 185 | 186 | } 187 | 188 | /** 189 | * {@inheritdoc} 190 | */ 191 | public function getTotal() 192 | { 193 | 194 | } 195 | 196 | /** 197 | * {@inheritdoc} 198 | */ 199 | public function setTotal($total) 200 | { 201 | 202 | } 203 | 204 | /** 205 | * {@inheritdoc} 206 | */ 207 | public function calculateTotal() 208 | { 209 | 210 | } 211 | 212 | /** 213 | * {@inheritdoc} 214 | */ 215 | public function getTotalQuantity() 216 | { 217 | 218 | } 219 | 220 | /** 221 | * {@inheritdoc} 222 | */ 223 | public function getState() 224 | { 225 | return $this->state; 226 | } 227 | 228 | /** 229 | * {@inheritdoc} 230 | */ 231 | public function setState($state) 232 | { 233 | $this->state = $state; 234 | 235 | return $this; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/Order/Models/OrderInterface.php: -------------------------------------------------------------------------------- 1 | belongsTo('ANavallaSuiza\Ecommerce\Order\Models\Order'); 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function product() 29 | { 30 | return $this->belongsTo('ANavallaSuiza\Ecommerce\Product\Models\Product'); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function getQuantity() 37 | { 38 | return $this->quantity; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function setQuantity($quantity) 45 | { 46 | $this->quantity = $quantity; 47 | 48 | return $this; 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function getUnitPrice() 55 | { 56 | return $this->unit_price; 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function setUnitPrice($unitPrice) 63 | { 64 | $this->unit_price = $unitPrice; 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | public function getTotal() 73 | { 74 | return $this->total; 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function setTotal($total) 81 | { 82 | $this->total = $total; 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | */ 90 | public function calculateTotal() 91 | { 92 | 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Order/Models/OrderItemInterface.php: -------------------------------------------------------------------------------- 1 | product, $method)) { 30 | throw new \BadMethodCallException(sprintf('Product has no "%s()" method.', $method)); 31 | } 32 | 33 | call_user_func_array(array($this->product, $method), $arguments); 34 | 35 | return $this; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function build($name, $sku, $price, $stockQty) 42 | { 43 | DB::beginTransaction(); 44 | 45 | $this->product = Product::firstOrCreateByName($name); 46 | 47 | // Set Base Varient 48 | $variant = new Variant; 49 | 50 | $variant->sku = $sku; 51 | $variant->price = $price; 52 | $variant->on_hand = $stockQty; 53 | $variant->product_id = $this->product->id; 54 | $variant->master = true; 55 | $variant->available_on_demand = false; 56 | 57 | $variant->save(); 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function addAttribute($name, $value) 66 | { 67 | $this->product->setAttribute($name, $value); 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function addProperty($name, $value, $presentation = null) 76 | { 77 | $property = Property::firstOrCreate(['name' => $name]); 78 | 79 | $property->presentation = is_null($presentation) ? $name : $presentation; 80 | 81 | $propertyValue = new PropertyValue; 82 | 83 | $propertyValue->value = $value; 84 | $propertyValue->property()->associate($property); 85 | $propertyValue->product()->associate($this->product); 86 | 87 | $propertyValue->save(); 88 | 89 | $this->product->addProperty($propertyValue); 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | public function save() 98 | { 99 | $this->product->save(); 100 | 101 | DB::commit(); 102 | 103 | return $this->product; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Product/Builder/ProductBuilderInterface.php: -------------------------------------------------------------------------------- 1 | hasMany('ANavallaSuiza\Ecommerce\Product\Models\OptionValue', 'product_option_id'); 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function getValues() 28 | { 29 | return $this->values; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function setValues(Collection $optionValues) 36 | { 37 | $this->values = $optionValues; 38 | 39 | return $this; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function addValue(OptionValueInterface $optionValue) 46 | { 47 | if (! $this->hasValue($optionValue)) { 48 | $optionValue->setOption($this); 49 | $this->values->push($optionValue); 50 | } 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function removeValue(OptionValueInterface $optionValue) 59 | { 60 | if ($this->hasValue($optionValue)) { 61 | foreach ($this->values as $key => $item) { 62 | if ($item->getKey() === $optionValue->getKey()) { 63 | $this->values->forget($key); 64 | } 65 | } 66 | } 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * {@inheritdoc} 73 | */ 74 | public function hasValue(OptionValueInterface $optionValue) 75 | { 76 | return $this->values->contains($optionValue); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Product/Models/OptionInterface.php: -------------------------------------------------------------------------------- 1 | belongsTo('ANavallaSuiza\Ecommerce\Product\Models\Option', 'product_option_id'); 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function getOption() 31 | { 32 | return $this->option; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function setOption(OptionInterface $option = null) 39 | { 40 | $this->option()->associate($option); 41 | 42 | return $this; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Product/Models/OptionValueInterface.php: -------------------------------------------------------------------------------- 1 | available_on = new \DateTime(); 39 | 40 | parent::__construct($attributes); 41 | } 42 | 43 | public static function firstOrCreateByName($name) 44 | { 45 | $instance = static::whereHas('translations', function ($query) use ($name) { 46 | $query->where('name', 'LIKE', $name); 47 | })->first(); 48 | 49 | if (! is_null($instance)) { 50 | return $instance; 51 | } 52 | 53 | return static::create([ 54 | App::getLocale() => ['name' => $name] 55 | ]); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function isAvailable() 62 | { 63 | return new \DateTime() >= $this->available_on; 64 | } 65 | 66 | public function variants() 67 | { 68 | return $this->hasMany('ANavallaSuiza\Ecommerce\Product\Models\Variant'); 69 | } 70 | 71 | public function options() 72 | { 73 | return $this->hasMany('ANavallaSuiza\Ecommerce\Product\Models\Option'); 74 | } 75 | 76 | public function properties() 77 | { 78 | return $this->hasMany('ANavallaSuiza\Ecommerce\Product\Models\PropertyValue'); 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | public function getSku() 85 | { 86 | return $this->getMasterVariant()->getSku(); 87 | } 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function setSku($sku) 92 | { 93 | $this->getMasterVariant()->setSku($sku); 94 | 95 | return $this; 96 | } 97 | 98 | /** 99 | * {@inheritdoc} 100 | */ 101 | public function getImages() 102 | { 103 | return $this->getMasterVariant()->getImages(); 104 | } 105 | 106 | /** 107 | * {@inheritdoc} 108 | */ 109 | public function getImage() 110 | { 111 | return $this->getMasterVariant()->getImages()->first(); 112 | } 113 | 114 | /** 115 | * {@inheritdoc} 116 | */ 117 | public function getPrice() 118 | { 119 | return $this->getMasterVariant()->getPrice(); 120 | } 121 | 122 | /** 123 | * {@inheritdoc} 124 | */ 125 | public function setPrice($price) 126 | { 127 | $this->getMasterVariant()->setPrice($price); 128 | 129 | return $this; 130 | } 131 | 132 | 133 | /** 134 | * {@inheritdoc} 135 | */ 136 | public function getMasterVariant() 137 | { 138 | foreach ($this->variants as $variant) { 139 | if ($variant->isMaster()) { 140 | return $variant; 141 | } 142 | } 143 | 144 | return null; 145 | } 146 | 147 | /** 148 | * {@inheritdoc} 149 | */ 150 | public function setMasterVariant(VariantInterface $variant) 151 | { 152 | $variant->setMaster(true); 153 | 154 | if (! $this->variants->contains($variant)) { 155 | $variant->setProduct($this); 156 | $this->variants->push($variant); 157 | } 158 | 159 | return $this; 160 | } 161 | 162 | /** 163 | * {@inheritdoc} 164 | */ 165 | public function hasVariants() 166 | { 167 | $items = $this->variants->filter(function (VariantInterface $variant) { 168 | return ! $variant->isMaster(); 169 | }); 170 | 171 | if ($items->isEmpty()) { 172 | return false; 173 | } 174 | 175 | return true; 176 | } 177 | 178 | /** 179 | * {@inheritdoc} 180 | */ 181 | public function getVariants() 182 | { 183 | return $this->variants; 184 | } 185 | 186 | /** 187 | * {@inheritdoc} 188 | */ 189 | public function setVariants(Collection $variants) 190 | { 191 | $this->variants = $variants; 192 | 193 | return $this; 194 | } 195 | 196 | /** 197 | * {@inheritdoc} 198 | */ 199 | public function addVariant(VariantInterface $variant) 200 | { 201 | if (! $this->hasVariant($variant)) { 202 | $variant->setProduct($this); 203 | $this->variants->push($variant); 204 | } 205 | 206 | return $this; 207 | } 208 | 209 | /** 210 | * {@inheritdoc} 211 | */ 212 | public function removeVariant(VariantInterface $variant) 213 | { 214 | if ($this->hasVariant($variant)) { 215 | foreach ($this->variants as $key => $item) { 216 | if ($item->getKey() === $variant->getKey()) { 217 | $this->variants->forget($key); 218 | } 219 | } 220 | } 221 | 222 | return $this; 223 | } 224 | 225 | /** 226 | * {@inheritdoc} 227 | */ 228 | public function hasVariant(VariantInterface $variant) 229 | { 230 | return $this->variants->contains($variant); 231 | } 232 | 233 | /** 234 | * {@inheritdoc} 235 | */ 236 | public function hasOptions() 237 | { 238 | return (! $this->options->isEmpty()); 239 | } 240 | 241 | /** 242 | * {@inheritdoc} 243 | */ 244 | public function getOptions() 245 | { 246 | return $this->options; 247 | } 248 | 249 | /** 250 | * {@inheritdoc} 251 | */ 252 | public function setOptions(Collection $options) 253 | { 254 | $this->options = $options; 255 | 256 | return $this; 257 | 258 | } 259 | 260 | /** 261 | * {@inheritdoc} 262 | */ 263 | public function addOption(OptionInterface $option) 264 | { 265 | if (! $this->hasOption($option)) { 266 | $this->options->push($option); 267 | } 268 | 269 | return $this; 270 | } 271 | 272 | /** 273 | * {@inheritdoc} 274 | */ 275 | public function removeOption(OptionInterface $option) 276 | { 277 | if ($this->hasOption($option)) { 278 | foreach ($this->options as $key => $item) { 279 | if ($item->getKey() === $option->getKey()) { 280 | $this->options->forget($key); 281 | } 282 | } 283 | } 284 | 285 | return $this; 286 | } 287 | 288 | /** 289 | * {@inheritdoc} 290 | */ 291 | public function hasOption(OptionInterface $option) 292 | { 293 | return $this->options->contains($option); 294 | } 295 | 296 | /** 297 | * {@inheritdoc} 298 | */ 299 | public function getProperties() 300 | { 301 | return $this->properties; 302 | } 303 | 304 | /** 305 | * {@inheritdoc} 306 | */ 307 | public function setProperties(Collection $properties) 308 | { 309 | $this->properties = $properties; 310 | } 311 | 312 | /** 313 | * {@inheritdoc} 314 | */ 315 | public function addProperty(PropertyValueInterface $property) 316 | { 317 | if (! $this->hasProperty($property)) { 318 | $property->setSubject($this); 319 | $this->properties->push($property); 320 | } 321 | 322 | return $this; 323 | } 324 | 325 | /** 326 | * {@inheritdoc} 327 | */ 328 | public function removeProperty(PropertyValueInterface $property) 329 | { 330 | if ($this->hasProperty($property)) { 331 | foreach ($this->properties as $key => $item) { 332 | if ($item->getKey() === $property->getKey()) { 333 | $this->properties->forget($key); 334 | } 335 | } 336 | } 337 | 338 | return $this; 339 | } 340 | 341 | /** 342 | * {@inheritdoc} 343 | */ 344 | public function hasProperty(PropertyValueInterface $property) 345 | { 346 | return $this->properties->contains($property); 347 | } 348 | 349 | /** 350 | * {@inheritdoc} 351 | */ 352 | public function hasPropertyByName($propertyName) 353 | { 354 | foreach ($this->properties as $item) { 355 | if ($item->getProperty()->name === $propertyName) { 356 | return true; 357 | } 358 | } 359 | 360 | return false; 361 | } 362 | 363 | /** 364 | * {@inheritdoc} 365 | */ 366 | public function getPropertyByName($propertyName) 367 | { 368 | foreach ($this->properties as $item) { 369 | if ($item->getProperty()->name === $propertyName) { 370 | return $item; 371 | } 372 | } 373 | 374 | return null; 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /src/Product/Models/ProductInterface.php: -------------------------------------------------------------------------------- 1 | belongsTo('ANavallaSuiza\Ecommerce\Product\Models\Product'); 28 | } 29 | 30 | public function property() 31 | { 32 | return $this->belongsTo('ANavallaSuiza\Ecommerce\Product\Models\Property'); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function getSubject() 39 | { 40 | return $this->product; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function setSubject(PropertySubjectInterface $subject) 47 | { 48 | $this->product()->associate($subject); 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function getProperty() 57 | { 58 | return $this->property; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function setProperty(PropertyInterface $property) 65 | { 66 | $this->property()->associate($property); 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | public function getValue() 73 | { 74 | return $this->value; 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function setValue($value) 81 | { 82 | $this->value = $value; 83 | 84 | return $this; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Product/Models/PropertyValueInterface.php: -------------------------------------------------------------------------------- 1 | belongsTo('ANavallaSuiza\Ecommerce\Product\Models\Product'); 23 | } 24 | 25 | public function options() 26 | { 27 | return $this->belongsToMany('ANavallaSuiza\Ecommerce\Product\Models\OptionValue', 'products_variants_options_values', 'product_variant_id', 'product_option_value_id'); 28 | } 29 | 30 | public function images() 31 | { 32 | return $this->hasMany('ANavallaSuiza\Ecommerce\Product\Models\Image'); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function isMaster() 39 | { 40 | return is_null($this->master) ? false : $this->master; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function setMaster($master) 47 | { 48 | $this->master = (Boolean) $master; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function getProduct() 57 | { 58 | return $this->product; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function setProduct(ProductInterface $product = null) 65 | { 66 | $this->product()->associate($product); 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * {@inheritdoc} 73 | */ 74 | public function getOptions() 75 | { 76 | return $this->options; 77 | } 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | public function setOptions(Collection $options) 83 | { 84 | $this->options = $options; 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | public function addOption(OptionValueInterface $option) 93 | { 94 | if (! $this->hasOption($option)) { 95 | $this->options->push($option); 96 | } 97 | 98 | return $this; 99 | } 100 | 101 | /** 102 | * {@inheritdoc} 103 | */ 104 | public function removeOption(OptionValueInterface $option) 105 | { 106 | if ($this->hasOption($option)) { 107 | foreach ($this->options as $key => $item) { 108 | if ($item->getKey() === $option->getKey()) { 109 | $this->options->forget($key); 110 | } 111 | } 112 | } 113 | 114 | return $this; 115 | } 116 | 117 | /** 118 | * {@inheritdoc} 119 | */ 120 | public function hasOption(OptionValueInterface $option) 121 | { 122 | return $this->options->contains($option); 123 | } 124 | 125 | /** 126 | * {@inheritdoc} 127 | */ 128 | public function setDefaults(VariantInterface $masterVariant) 129 | { 130 | if (! $masterVariant->isMaster()) { 131 | throw new \InvalidArgumentException('Cannot inherit values from non master variant.'); 132 | } 133 | 134 | if ($this->isMaster()) { 135 | throw new \LogicException('Master variant cannot inherit from another master variant.'); 136 | } 137 | 138 | return $this; 139 | } 140 | 141 | /** 142 | * {@inheritdoc} 143 | */ 144 | public function getImages() 145 | { 146 | return $this->images; 147 | } 148 | 149 | /** 150 | * {@inheritdoc} 151 | */ 152 | public function getImage() 153 | { 154 | if ($this->images->isEmpty()) { 155 | return $this->getProduct()->getImage(); 156 | } 157 | 158 | return $this->images->first(); 159 | } 160 | 161 | /** 162 | * {@inheritdoc} 163 | */ 164 | public function hasImage(ImageInterface $image) 165 | { 166 | return $this->images->contains($image); 167 | } 168 | 169 | /** 170 | * {@inheritdoc} 171 | */ 172 | public function addImage(ImageInterface $image) 173 | { 174 | if (! $this->hasImage($image)) { 175 | $this->images->push($image); 176 | } 177 | 178 | return $this; 179 | } 180 | 181 | /** 182 | * {@inheritdoc} 183 | */ 184 | public function removeImage(ImageInterface $image) 185 | { 186 | if ($this->hasImage($image)) { 187 | foreach ($this->images as $key => $item) { 188 | if ($item->getKey() === $image->getKey()) { 189 | $this->images->forget($key); 190 | } 191 | } 192 | } 193 | 194 | return $this; 195 | } 196 | 197 | /** 198 | * {@inheritdoc} 199 | */ 200 | public function getSku() 201 | { 202 | return $this->sku; 203 | } 204 | 205 | /** 206 | * {@inheritdoc} 207 | */ 208 | public function getInventoryName() 209 | { 210 | return $this->getProduct()->getName(); 211 | } 212 | 213 | /** 214 | * {@inheritdoc} 215 | */ 216 | public function isInStock() 217 | { 218 | return 0 < $this->on_hand; 219 | } 220 | 221 | /** 222 | * {@inheritdoc} 223 | */ 224 | public function isAvailableOnDemand() 225 | { 226 | return $this->available_on_demand; 227 | } 228 | 229 | /** 230 | * {@inheritdoc} 231 | */ 232 | public function getOnHand() 233 | { 234 | return $this->on_hand; 235 | } 236 | 237 | /** 238 | * {@inheritdoc} 239 | */ 240 | public function setOnHand($onHand) 241 | { 242 | $this->on_hand = $onHand; 243 | 244 | if (0 > $this->on_hand) { 245 | $this->on_hand = 0; 246 | } 247 | 248 | return $this; 249 | } 250 | 251 | /** 252 | * {@inheritdoc} 253 | */ 254 | public function getPrice() 255 | { 256 | return $this->price; 257 | } 258 | 259 | /** 260 | * {@inheritdoc} 261 | */ 262 | public function setPrice($price) 263 | { 264 | if (! is_int($price)) { 265 | throw new \InvalidArgumentException('Price must be an integer.'); 266 | } 267 | 268 | $this->price = $price; 269 | 270 | return $this; 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/Product/Models/VariantInterface.php: -------------------------------------------------------------------------------- 1 | slug)) { 11 | $model->slug = $model->name; 12 | } 13 | 14 | $model->slug = Slugger::slugify($model->slug); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Product/Observer/ProductTranslationObserver.php: -------------------------------------------------------------------------------- 1 | slug)) { 11 | $model->slug = $model->name; 12 | } 13 | 14 | $model->slug = Slugger::slugify($model->slug); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Product/Observer/PropertyValueObserver.php: -------------------------------------------------------------------------------- 1 | property->save(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Promotion/Models/ActionInterface.php: -------------------------------------------------------------------------------- 1 | publishes([ 25 | __DIR__.'/../config/config.php' => config_path('ans-ecommerce.php') 26 | ], 'config'); 27 | 28 | $this->publishes([ 29 | __DIR__ . '/../migrations/' => base_path('/database/migrations') 30 | ], 'migrations'); 31 | 32 | // Register Model Events 33 | Product::observe(new ProductObserver()); 34 | } 35 | 36 | /** 37 | * Register the service provider. 38 | * 39 | * @return void 40 | */ 41 | public function register() 42 | { 43 | $this->mergeConfigFrom(__DIR__.'/../config/config.php', 'ans-ecommerce'); 44 | 45 | $this->app->bind('ANavallaSuiza\Ecommerce\Product\Models\ProductInterface', function () { 46 | return $this->app->make(config('ans-ecommerce.product_model')); 47 | }); 48 | 49 | $this->app->bind('ANavallaSuiza\Ecommerce\Product\Builder\ProductBuilderInterface', 'ANavallaSuiza\Ecommerce\Product\Builder\ProductBuilder'); 50 | 51 | $this->app->register('Dimsav\Translatable\TranslatableServiceProvider'); 52 | } 53 | 54 | /** 55 | * Get the services provided by the provider. 56 | * 57 | * @return array 58 | */ 59 | public function provides() 60 | { 61 | return [ 62 | 'ANavallaSuiza\Ecommerce\Product\Models\ProductInterface', 63 | 'ANavallaSuiza\Ecommerce\Product\Builder\ProductBuilderInterface' 64 | ]; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Product/Builder/ProductBuilderTest.php: -------------------------------------------------------------------------------- 1 | productBuilder = $this->getInstance(); 21 | } 22 | 23 | private function getInstance() 24 | { 25 | return App::make('ANavallaSuiza\Ecommerce\Product\Builder\ProductBuilder'); 26 | } 27 | 28 | public function test_creates_product() 29 | { 30 | $productName = 'GitHub T-Shirt'; 31 | $SKU = 'SKU99'; 32 | $price = 100; 33 | $stock = 100; 34 | 35 | $this->productBuilder->build($productName, $SKU, $price, $stock) 36 | ->addAttribute('description', 'Awesome description') 37 | ->save(); 38 | 39 | $this->product = Product::firstOrCreateByName($productName); 40 | 41 | $this->assertEquals($productName, $this->product->name); 42 | $this->assertEquals($price, $this->product->getPrice()); 43 | $this->assertEquals($SKU, $this->product->getSku()); 44 | $this->assertEquals(true, $this->product->getMasterVariant()->isInStock()); 45 | } 46 | 47 | public function test_creates_property_if_it_does_not_exist() 48 | { 49 | $productName = 'GitHub T-Shirt'; 50 | $SKU = 'SKU99'; 51 | $price = 100; 52 | $stock = 100; 53 | 54 | $this->product = $this->productBuilder->build($productName, $SKU, $price, $stock) 55 | ->addAttribute('description', 'Awesome description') 56 | ->addProperty('Collection', 2015) 57 | ->save(); 58 | 59 | $properties = $this->product->getProperties(); 60 | 61 | $this->assertEquals(2015, $properties->first()->getValue()); 62 | $this->assertEquals('Collection', $properties->first()->getProperty()->name); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Product/Models/OptionTest.php: -------------------------------------------------------------------------------- 1 | option = $this->getInstance(); 17 | } 18 | 19 | public function tearDown() 20 | { 21 | Mockery::close(); 22 | } 23 | 24 | private function getInstance($id = null) 25 | { 26 | if (isset($id)) { 27 | return Option::find($id); 28 | } 29 | 30 | return new Option; 31 | } 32 | 33 | public function test_implement_option_value_interface() 34 | { 35 | $this->assertInstanceOf('ANavallaSuiza\Ecommerce\Product\Models\OptionInterface', $this->option); 36 | } 37 | 38 | public function test_should_initialize_values_collection_by_default() 39 | { 40 | $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $this->option->getValues()); 41 | } 42 | 43 | public function test_adds_removes_value() 44 | { 45 | $value = Mockery::mock('ANavallaSuiza\Ecommerce\Product\Models\OptionValue'); 46 | $value->shouldReceive('setOption')->once()->with($this->option); 47 | $value->shouldReceive('getKey')->andReturn(1); 48 | 49 | $this->option->addValue($value); 50 | 51 | $this->assertTrue($this->option->hasValue($value)); 52 | 53 | $this->option->removeValue($value); 54 | 55 | $this->assertFalse($this->option->hasValue($value)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Product/Models/OptionValueTest.php: -------------------------------------------------------------------------------- 1 | optionValue = $this->getInstance(); 17 | } 18 | 19 | public function tearDown() 20 | { 21 | Mockery::close(); 22 | } 23 | 24 | private function getInstance($id = null) 25 | { 26 | if (isset($id)) { 27 | return OptionValue::find($id); 28 | } 29 | 30 | return new OptionValue; 31 | } 32 | 33 | public function test_implement_option_interface() 34 | { 35 | $this->assertInstanceOf('ANavallaSuiza\Ecommerce\Product\Models\OptionValueInterface', $this->optionValue); 36 | } 37 | 38 | public function test_should_allow_assigning_itself_to_an_option() 39 | { 40 | $option = Mockery::mock('ANavallaSuiza\Ecommerce\Product\Models\Option')->makePartial(); 41 | 42 | $this->optionValue->setOption($option); 43 | 44 | $this->assertEquals($option, $this->optionValue->getOption()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Product/Models/ProductTest.php: -------------------------------------------------------------------------------- 1 | product = $this->getInstance(); 17 | } 18 | 19 | public function tearDown() 20 | { 21 | Mockery::close(); 22 | } 23 | 24 | private function getInstance($id = null) 25 | { 26 | if (isset($id)) { 27 | return Product::find($id); 28 | } 29 | 30 | return new Product; 31 | } 32 | 33 | private function getVariantMock() 34 | { 35 | return Mockery::mock('ANavallaSuiza\Ecommerce\Product\Models\VariantInterface'); 36 | } 37 | 38 | public function test_implements_product_interface() 39 | { 40 | $this->assertInstanceOf('ANavallaSuiza\Ecommerce\Product\Models\ProductInterface', $this->product); 41 | } 42 | 43 | public function test_initializes_availability_date_by_default() 44 | { 45 | $this->assertInstanceOf('\DateTime', $this->product->available_on); 46 | } 47 | 48 | public function test_is_available_by_default() 49 | { 50 | $this->assertTrue($this->product->isAvailable()); 51 | } 52 | 53 | public function test_availability_date_is_mutable() 54 | { 55 | $availableOn = new \DateTime('yesterday'); 56 | $this->product->available_on = $availableOn; 57 | 58 | $this->assertEquals($availableOn, $this->product->available_on); 59 | } 60 | 61 | public function test_is_available_only_if_availability_date_is_in_past() 62 | { 63 | $this->product->available_on = new \DateTime('yesterday'); 64 | $this->assertTrue($this->product->isAvailable()); 65 | 66 | $this->product->available_on = new \DateTime('tomorrow'); 67 | $this->assertFalse($this->product->isAvailable()); 68 | } 69 | 70 | public function test_initializes_property_collection_by_default() 71 | { 72 | $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $this->product->getProperties()); 73 | } 74 | 75 | public function test_adds_removes_property() 76 | { 77 | $property = Mockery::mock('ANavallaSuiza\Ecommerce\Product\Models\PropertyValue'); 78 | $property->shouldReceive('setSubject')->once()->with($this->product); 79 | $property->shouldReceive('getKey')->andReturn(1); 80 | 81 | $this->product->addProperty($property); 82 | 83 | $this->assertTrue($this->product->hasProperty($property)); 84 | 85 | $this->product->removeProperty($property); 86 | 87 | $this->assertFalse($this->product->hasProperty($property)); 88 | } 89 | 90 | public function test_should_not_have_master_variant_by_default() 91 | { 92 | $this->assertNull($this->product->getMasterVariant()); 93 | } 94 | 95 | public function test_master_variant_should_be_mutable_and_define_given_variant_as_master() 96 | { 97 | $variant = $this->getVariantMock(); 98 | $variant->shouldReceive('setProduct')->once()->with($this->product); 99 | $variant->shouldReceive('setMaster')->once()->with(true); 100 | $variant->shouldReceive('isMaster')->once()->andReturn(true); 101 | 102 | $this->product->setMasterVariant($variant); 103 | 104 | $this->assertEquals($variant, $this->product->getMasterVariant()); 105 | } 106 | 107 | public function test_should_not_add_master_variant_twice_to_collection() 108 | { 109 | $variant = $this->getVariantMock(); 110 | $variant->shouldReceive('isMaster')->times(2)->andReturn(true); 111 | $variant->shouldReceive('setProduct')->times(2)->with($this->product); 112 | $variant->shouldReceive('setMaster')->times(2)->with(true); 113 | $variant->shouldReceive('getKey')->once()->andReturn(null); 114 | 115 | $this->product->setMasterVariant($variant); 116 | $this->product->setMasterVariant($variant); 117 | 118 | $this->assertFalse($this->product->hasVariants()); 119 | } 120 | 121 | public function test_hasVariants_should_return_false_if_no_variants_defined() 122 | { 123 | $this->assertFalse($this->product->hasVariants()); 124 | } 125 | 126 | public function test_adds_removes_variant() 127 | { 128 | $variant = Mockery::mock('ANavallaSuiza\Ecommerce\Product\Models\Variant'); 129 | $variant->shouldReceive('isMaster')->once()->andReturn(false); 130 | $variant->shouldReceive('setProduct')->once()->with($this->product); 131 | $variant->shouldReceive('getKey')->andReturn(1); 132 | 133 | $this->product->addVariant($variant); 134 | 135 | $this->assertTrue($this->product->hasVariants()); 136 | 137 | $this->product->removeVariant($variant); 138 | 139 | $this->assertFalse($this->product->hasVariants()); 140 | } 141 | 142 | public function test_should_initialize_variants_collection_by_default() 143 | { 144 | $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $this->product->getVariants()); 145 | } 146 | 147 | public function test_should_initialize_option_collection_by_default() 148 | { 149 | $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $this->product->getOptions()); 150 | } 151 | 152 | public function test_hasOptions_should_return_false_if_no_options_defined() 153 | { 154 | $this->assertFalse($this->product->hasOptions()); 155 | } 156 | 157 | public function test_hasOptions_should_return_true_only_if_any_options_defined() 158 | { 159 | $option = Mockery::mock('ANavallaSuiza\Ecommerce\Product\Models\Option'); 160 | $option->shouldReceive('getKey')->andReturn(1); 161 | 162 | $this->product->addOption($option); 163 | 164 | $this->assertTrue($this->product->hasOptions()); 165 | } 166 | 167 | public function test_adds_removes_option() 168 | { 169 | $option = Mockery::mock('ANavallaSuiza\Ecommerce\Product\Models\Option'); 170 | $option->shouldReceive('getKey')->andReturn(1); 171 | 172 | $this->product->addOption($option); 173 | 174 | $this->assertTrue($this->product->hasOption($option)); 175 | 176 | $this->product->removeOption($option); 177 | 178 | $this->assertFalse($this->product->hasOption($option)); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /tests/Product/Models/VariantTest.php: -------------------------------------------------------------------------------- 1 | variant = $this->getInstance(); 17 | } 18 | 19 | public function tearDown() 20 | { 21 | Mockery::close(); 22 | } 23 | 24 | private function getInstance($id = null) 25 | { 26 | if (isset($id)) { 27 | return Variant::find($id); 28 | } 29 | 30 | return new Variant; 31 | } 32 | 33 | public function test_implements_variant_interface() 34 | { 35 | $this->assertInstanceOf('ANavallaSuiza\Ecommerce\Product\Models\VariantInterface', $this->variant); 36 | } 37 | 38 | public function test_should_allow_assigning_itself_to_a_product() 39 | { 40 | $product = Mockery::mock('ANavallaSuiza\Ecommerce\Product\Models\Product')->makePartial(); 41 | 42 | $this->variant->setProduct($product); 43 | 44 | $this->assertEquals($product, $this->variant->getProduct()); 45 | } 46 | 47 | public function test_should_not_be_master_variant_by_default() 48 | { 49 | $this->assertFalse($this->variant->isMaster()); 50 | } 51 | 52 | public function test_is_master_variant_when_marked_so() 53 | { 54 | $this->assertFalse($this->variant->isMaster()); 55 | 56 | $this->variant->setMaster(true); 57 | 58 | $this->assertTrue($this->variant->isMaster()); 59 | } 60 | 61 | public function test_should_initialize_option_values_collection_by_default() 62 | { 63 | $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $this->variant->getOptions()); 64 | } 65 | 66 | public function test_should_add_option_value_properly() 67 | { 68 | $option = Mockery::mock('ANavallaSuiza\Ecommerce\Product\Models\OptionValue')->makePartial(); 69 | 70 | $this->variant->addOption($option); 71 | 72 | $this->assertTrue($this->variant->hasOption($option)); 73 | } 74 | 75 | public function test_should_remove_option_value_properly() 76 | { 77 | $option = Mockery::mock('ANavallaSuiza\Ecommerce\Product\Models\OptionValue')->makePartial(); 78 | 79 | $this->variant->addOption($option); 80 | $this->assertTrue($this->variant->hasOption($option)); 81 | 82 | $this->variant->removeOption($option); 83 | $this->assertFalse($this->variant->hasOption($option)); 84 | } 85 | 86 | public function test_throws_exception_if_trying_to_inherit_values_and_being_a_master_variant() 87 | { 88 | $this->setExpectedException('\LogicException'); 89 | 90 | $variant = Mockery::mock('ANavallaSuiza\Ecommerce\Product\Models\Variant')->makePartial(); 91 | 92 | $this->variant->setMaster(true); 93 | $this->variant->setDefaults($variant); 94 | } 95 | 96 | public function test_throws_exception_if_trying_to_inherit_values_from_non_master_variant() 97 | { 98 | $this->setExpectedException('\InvalidArgumentException'); 99 | 100 | $variant = Mockery::mock('ANavallaSuiza\Ecommerce\Product\Models\Variant')->makePartial(); 101 | 102 | $this->variant->setDefaults($variant); 103 | } 104 | 105 | public function test_price_should_accept_only_integer() 106 | { 107 | $this->variant->setPrice(410); 108 | 109 | $this->isType('int', $this->variant->getPrice()); 110 | 111 | $this->setExpectedException('\InvalidArgumentException'); 112 | 113 | $this->variant->setPrice(4.1 * 100); 114 | $this->variant->setPrice('410'); 115 | $this->variant->setPrice(round(4.1 * 100)); 116 | $this->variant->setPrice(array(410)); 117 | $this->variant->setPrice(new \stdClass()); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/TestBase.php: -------------------------------------------------------------------------------- 1 | resetDatabase(); 16 | } 17 | 18 | protected function getPackageProviders($app) 19 | { 20 | return ['ANavallaSuiza\Ecommerce\StoreServiceProvider']; 21 | } 22 | protected function getEnvironmentSetUp($app) 23 | { 24 | $app['path.base'] = __DIR__.'/..'; 25 | 26 | $app['config']->set('database.default', 'sqlite'); 27 | 28 | $app['config']->set('database.connections.sqlite', [ 29 | 'driver' => 'sqlite', 30 | 'database' => ':memory:', 31 | 'prefix' => '' 32 | ]); 33 | 34 | $app['config']->set('translatable.locales', ['en']); 35 | } 36 | 37 | private function resetDatabase() 38 | { 39 | $artisan = $this->app->make('Illuminate\Contracts\Console\Kernel'); 40 | 41 | // Makes sure the migrations table is created 42 | $artisan->call('migrate', [ 43 | '--path' => self::MIGRATIONS_PATH, 44 | ]); 45 | 46 | // We empty all tables 47 | $artisan->call('migrate:reset'); 48 | 49 | // Migrate 50 | $artisan->call('migrate', [ 51 | '--path' => self::MIGRATIONS_PATH, 52 | ]); 53 | } 54 | 55 | public function test_running_migration() 56 | { 57 | $migrations = DB::select('SELECT * FROM migrations'); 58 | 59 | $fi = new \FilesystemIterator(self::MIGRATIONS_PATH, \FilesystemIterator::SKIP_DOTS); 60 | 61 | $this->assertCount(iterator_count($fi), $migrations); 62 | } 63 | } 64 | --------------------------------------------------------------------------------