mirror of
https://github.com/wearepixel/laravel-cart.git
synced 2026-11-14 22:01:48 +00:00
Compare commits
74
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1169f502f | ||
|
|
bfdea648ae | ||
|
|
01189993f2 | ||
|
|
3375518354 | ||
|
|
c493c73497 | ||
|
|
7427ce953a | ||
|
|
6ba50a1c48 | ||
|
|
c50fc70688 | ||
|
|
c9ffd8b8a4 | ||
|
|
f3c4ddae6e | ||
|
|
3f1f5fb4d2 | ||
|
|
fce7125c5f | ||
|
|
834e211443 | ||
|
|
2ced1038af | ||
|
|
9b198edb7d | ||
|
|
49959faace | ||
|
|
e61ffae5fe | ||
|
|
10ab30f4e8 | ||
|
|
34626eef5e | ||
|
|
6020b655e2 | ||
|
|
2c56062f97 | ||
|
|
88a118d81b | ||
|
|
4f87bd645c | ||
|
|
bab304c620 | ||
|
|
fd77347542 | ||
|
|
ce51042edf | ||
|
|
c2b2563cf1 | ||
|
|
15d4397968 | ||
|
|
76fba35643 | ||
|
|
b8140166c9 | ||
|
|
fc16e6c877 | ||
|
|
731aaa070d | ||
|
|
0efd4378a8 | ||
|
|
a485bfb6f0 | ||
|
|
8d04d18b48 | ||
|
|
61863e0bd5 | ||
|
|
c3a734bd7f | ||
|
|
2513e07436 | ||
|
|
b5edbb8ec0 | ||
|
|
867daaf328 | ||
|
|
e41739c436 | ||
|
|
4877911cdd | ||
|
|
f3e5e958b7 | ||
|
|
adf4bca74f | ||
|
|
0c27ac4d9a | ||
|
|
9abf6a7a1b | ||
|
|
e0103aadaa | ||
|
|
90ea8c44e4 | ||
|
|
3b8fa4c838 | ||
|
|
2f2d268e0f | ||
|
|
71213d51f5 | ||
|
|
83aa9a1431 | ||
|
|
1705c23205 | ||
|
|
9fcd50f81f | ||
|
|
4702cf98ab | ||
|
|
8927ef08a0 | ||
|
|
c9f48d7e88 | ||
|
|
11e23a946e | ||
|
|
b3c4c53e63 | ||
|
|
c9f3d76366 | ||
|
|
b2a4864675 | ||
|
|
aaa3360d1f | ||
|
|
05729b7865 | ||
|
|
7e6ee749bf | ||
|
|
17532f1b05 | ||
|
|
d058ad5521 | ||
|
|
3f5e9374c3 | ||
|
|
fcdd356718 | ||
|
|
3500fa43a9 | ||
|
|
28a72581fc | ||
|
|
ddcf38dab0 | ||
|
|
e7bf1ba3a3 | ||
|
|
ebf5a41e01 | ||
|
|
863ef52e8d |
+21
-5
@@ -1,17 +1,33 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
target-branch: "master"
|
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
groups:
|
||||||
|
actions:
|
||||||
|
patterns: ["*"]
|
||||||
|
commit-message:
|
||||||
|
prefix: "chore"
|
||||||
|
include: "scope"
|
||||||
labels:
|
labels:
|
||||||
- "deps"
|
- "dependencies"
|
||||||
|
|
||||||
- package-ecosystem: "composer"
|
- package-ecosystem: "composer"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
target-branch: "master"
|
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
day: "tuesday"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
groups:
|
||||||
|
php-minor-patch:
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
- "patch"
|
||||||
|
commit-message:
|
||||||
|
prefix: "chore"
|
||||||
|
include: "scope"
|
||||||
labels:
|
labels:
|
||||||
- "deps"
|
- "dependencies"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Dependabot metadata
|
- name: Dependabot metadata
|
||||||
id: metadata
|
id: metadata
|
||||||
uses: dependabot/fetch-metadata@dbb049abf0d677abbd7f7eee0375145b417fdd34 # v2.2.0
|
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
|
||||||
with:
|
with:
|
||||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
|
||||||
|
|||||||
@@ -9,19 +9,19 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
name: Tests
|
name: Tests
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
matrix:
|
matrix:
|
||||||
php: ['8.3', '8.4', '8.5']
|
php: ["8.3", "8.4", "8.5"]
|
||||||
stability: [prefer-stable]
|
stability: [prefer-stable]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # v2
|
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php }}
|
php-version: ${{ matrix.php }}
|
||||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd, redis, memcached
|
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd, redis, memcached
|
||||||
@@ -29,12 +29,15 @@ jobs:
|
|||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3
|
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 5
|
timeout_minutes: 5
|
||||||
max_attempts: 5
|
max_attempts: 5
|
||||||
command: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress ${{ matrix.flags }}
|
command: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress ${{ matrix.flags }}
|
||||||
|
|
||||||
|
|
||||||
- name: Execute tests
|
- name: Execute tests
|
||||||
run: vendor/bin/pest
|
run: vendor/bin/pest
|
||||||
|
|
||||||
|
- name: Static analysis
|
||||||
|
run: vendor/bin/phpstan analyse --no-progress
|
||||||
|
if: matrix.php == '8.3'
|
||||||
|
|||||||
@@ -4,3 +4,5 @@
|
|||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
READMETODO.md
|
READMETODO.md
|
||||||
build
|
build
|
||||||
|
docs/superpowers/
|
||||||
|
.claude/
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [3.0.0] - TBD
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Storage driver system** (`CartDriver` interface, `CartManager`, `SessionDriver`, `DatabaseDriver`, `RedisDriver`, `NullDriver`, `MultiDriver`)
|
||||||
|
- **`Cart::fake()`** - swaps the active driver for a `NullDriver` and returns a `CartFactory` for fluent test state seeding
|
||||||
|
- **Assertion methods** on `Cart`: `assertContains()`, `assertCount()`, `assertTotalQuantity()`, `assertSubTotal()`, `assertTotal()`, `assertConditionApplied()`, `assertEmpty()`, `assertNotEmpty()`
|
||||||
|
- **`CartFactory`** - fluent builder for seeding test cart state (`withItems()`, `withCondition()`, `withCoupon()`)
|
||||||
|
- **`Coupon` abstract class** - extend to create first-class coupon objects that convert to `CartCondition`
|
||||||
|
- **`TaxRule` abstract class** - extend to create first-class tax rules that convert to `CartCondition`
|
||||||
|
- **`ShippingRate` abstract class** - extend to create first-class shipping rates that convert to `CartCondition`
|
||||||
|
- **`Cart::coupon()`**, **`Cart::tax()`**, **`Cart::shipping()`** - accept first-class objects or raw `CartCondition`
|
||||||
|
- **`HasCart` trait** - Livewire-aware trait with reactive `$cartItems`, `$cartTotal`, `$cartSubTotal`, `$cartCount` and proxy methods
|
||||||
|
- **`ShareCartWithInertia`** - static class for sharing cart state via `HandleInertiaRequests::share()`
|
||||||
|
- **`cart:install`** Artisan command - publishes config and scaffolds `app/Cart/` directory structure
|
||||||
|
- **`cart:make:coupon`** Artisan command - generates a `Coupon` subclass stub
|
||||||
|
- **`cart:make:tax`** Artisan command - generates a `TaxRule` subclass stub
|
||||||
|
- **`cart:make:shipping`** Artisan command - generates a `ShippingRate` subclass stub
|
||||||
|
- **`cart:make:driver`** Artisan command - generates a `CartDriver` implementation stub
|
||||||
|
- **`cart:debug`** Artisan command - dumps current cart state (non-production only)
|
||||||
|
- Laravel 13 support
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `Cart` constructor now accepts a `CartDriver` instance instead of raw session/database storage
|
||||||
|
- Config publish tag renamed from `config` to `cart-config`
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- `Wearepixel\Cart\CartSession` - replaced by the driver system
|
||||||
|
|
||||||
|
## [2.1.1] - 2026-05-27
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- PHP 8.4 deprecation warnings from Mockery implicit-nullable parameters in test bootstrap
|
||||||
|
- Upgraded CI runners from deprecated `ubuntu-20.04` to `ubuntu-latest`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `pestphp/pest` upgraded from `^3.0` to `^4.7`
|
||||||
|
|
||||||
|
## [2.1.0] - 2024-01-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Laravel 12 support
|
||||||
|
|
||||||
|
## [2.0.0] - 2023-01-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Initial v2 release
|
||||||
@@ -9,11 +9,14 @@
|
|||||||
|
|
||||||
A Cart Implementation for Laravel.
|
A Cart Implementation for Laravel.
|
||||||
|
|
||||||
Supported Laravel Versions: 10, 11, and 12.
|
Supported Laravel Versions: 10, 11, 12, and 13.
|
||||||
|
|
||||||
For Laravel 9.0 and below, please use version [1.0](https://github.com/wearepixel/laravel-cart/releases/tag/1.0.12)
|
For Laravel 9.0 and below, please use version [1.0](https://github.com/wearepixel/laravel-cart/releases/tag/1.0.12)
|
||||||
|
|
||||||
|
> **Upgrading from v2?** See the [Upgrade Guide](UPGRADE.md) for breaking changes and recommended v3 patterns.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Getting Started](#-getting-started)
|
- [Getting Started](#-getting-started)
|
||||||
- [Demo](#-deni)
|
- [Demo](#-deni)
|
||||||
- [Documentation](#-documentation)
|
- [Documentation](#-documentation)
|
||||||
@@ -21,12 +24,17 @@ ## Table of Contents
|
|||||||
- [Basic Usage](#basic-usage)
|
- [Basic Usage](#basic-usage)
|
||||||
- [Conditions](#conditions)
|
- [Conditions](#conditions)
|
||||||
- [Cart Items](#cart-items)
|
- [Cart Items](#cart-items)
|
||||||
- [Database Support](#database-support)
|
- [Storage Drivers](#storage-drivers)
|
||||||
|
- [Artisan Generators](#artisan-generators)
|
||||||
|
- [First-class Objects](#first-class-objects)
|
||||||
|
- [Testing](#testing)
|
||||||
|
- [Livewire / Inertia](#livewire--inertia)
|
||||||
- [Events](#events)
|
- [Events](#events)
|
||||||
- [Credits](#-credits)
|
- [Credits](#-credits)
|
||||||
- [License](#-license)
|
- [License](#-license)
|
||||||
|
|
||||||
## 🚀 Getting Started
|
## 🚀 Getting Started
|
||||||
|
|
||||||
### 🔥 Installing
|
### 🔥 Installing
|
||||||
|
|
||||||
Install the package through [Composer](http://getcomposer.org/).
|
Install the package through [Composer](http://getcomposer.org/).
|
||||||
@@ -71,7 +79,7 @@ ### ⚙️ Configuration
|
|||||||
You can publish the configuration file to customize various options.
|
You can publish the configuration file to customize various options.
|
||||||
|
|
||||||
```php
|
```php
|
||||||
php artisan vendor:publish --provider="Wearepixel\Cart\CartServiceProvider" --tag="config"
|
php artisan vendor:publish --provider="Wearepixel\Cart\CartServiceProvider" --tag="cart-config"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Formatting Numbers
|
#### Formatting Numbers
|
||||||
@@ -107,7 +115,6 @@ #### Round Mode
|
|||||||
'round_mode' => env('LARAVEL_CART_ROUND_MODE', 'down'),
|
'round_mode' => env('LARAVEL_CART_ROUND_MODE', 'down'),
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Basic Usage
|
### Basic Usage
|
||||||
|
|
||||||
The cart has a default sessionKey that holds the cart data and stores it in the session, so you can have multiple carts for multiple users.
|
The cart has a default sessionKey that holds the cart data and stores it in the session, so you can have multiple carts for multiple users.
|
||||||
@@ -600,6 +607,42 @@ #### Conditions on items
|
|||||||
Cart::add($item);
|
Cart::add($item);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### Limiting a condition to a specific quantity of items
|
||||||
|
|
||||||
|
By default, an item condition applies to every unit of the item. Use `applies_to` to limit it to the first N units:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// 5% off the first item only - for 2x $50 items: (50*0.95) + 50 = $97.50
|
||||||
|
$firstItemDiscount = new CartCondition([
|
||||||
|
'name' => '5% Off First Item',
|
||||||
|
'type' => 'discount',
|
||||||
|
'value' => '-5%',
|
||||||
|
'applies_to' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Cart::add([
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Widget',
|
||||||
|
'price' => 50.00,
|
||||||
|
'quantity' => 2,
|
||||||
|
'attributes' => [],
|
||||||
|
'conditions' => $firstItemDiscount,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Cart::getSubTotal(); // 97.50
|
||||||
|
```
|
||||||
|
|
||||||
|
When combining conditions, those without `applies_to` apply to every unit while those with `applies_to` only apply to the first N:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$limitedDiscount = new CartCondition(['name' => '10% Off First', 'type' => 'discount', 'value' => '-10%', 'applies_to' => 1]);
|
||||||
|
$globalDiscount = new CartCondition(['name' => '5% Off All', 'type' => 'discount', 'value' => '-5%']);
|
||||||
|
|
||||||
|
// Item 1: 100 * 0.90 * 0.95 = 85.50
|
||||||
|
// Item 2: 100 * 0.95 = 95.00
|
||||||
|
// Subtotal: 180.50
|
||||||
|
```
|
||||||
|
|
||||||
> NOTE: All cart per-item conditions should be added before calling **Cart::getSubTotal()**
|
> NOTE: All cart per-item conditions should be added before calling **Cart::getSubTotal()**
|
||||||
|
|
||||||
Then Finally you can call **Cart::getSubTotal()** to get the Cart sub total with the applied conditions on each of the items.
|
Then Finally you can call **Cart::getSubTotal()** to get the Cart sub total with the applied conditions on each of the items.
|
||||||
@@ -692,55 +735,282 @@ ### Cart Items
|
|||||||
$item->getPriceSumWithConditions();
|
$item->getPriceSumWithConditions();
|
||||||
```
|
```
|
||||||
|
|
||||||
### Storage Options
|
### Storage Drivers
|
||||||
|
|
||||||
By default the cart is stored in the session, but there are times you may want to store the cart in the database.
|
The cart supports multiple storage backends via a driver system.
|
||||||
|
|
||||||
For instance, you may want to store the cart in the database so that the cart can be retrieved even after the user logs out or closes the browser, or you may want to add cart timeouts and support for multiple computers.
|
#### Session (default)
|
||||||
|
|
||||||
#### Session
|
Cart data is stored in the Laravel session. No configuration needed.
|
||||||
|
|
||||||
The cart is stored in the session by default, using Laravel's in-built SessionManager.
|
#### Database
|
||||||
|
|
||||||
#### Database Support
|
Store the cart in a database table for persistence across sessions.
|
||||||
|
|
||||||
To get started, you'll need to create a new model for your Cart, the package requires only 3 columns, but you're free to extend this as you wish and add more columns.
|
**1. Create a migration:**
|
||||||
|
|
||||||
You can utilise the [events](#events) provided by the package to store additional information alongside the cart.
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$table->string('session_id'); // this handles the session id of the cart
|
$table->string('session_id');
|
||||||
$table->text('items'); // this will store the cart items
|
$table->text('items');
|
||||||
$table->text('conditions'); // this will store the cart level conditions
|
$table->text('conditions');
|
||||||
```
|
```
|
||||||
|
|
||||||
Then add some json casts to your model and fillable columns:
|
**2. Add casts to your model:**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'items' => 'array',
|
'items' => 'array',
|
||||||
'conditions' => 'array',
|
'conditions' => 'array',
|
||||||
];
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
Then update the configuration file to use the database driver:
|
**3. Update `config/cart.php`:**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
'driver' => 'database',
|
'driver' => 'database',
|
||||||
|
|
||||||
'storage' => [
|
'drivers' => [
|
||||||
'session',
|
|
||||||
'database' => [
|
'database' => [
|
||||||
'model' => \App\Models\Cart::class, // your model here
|
'model' => \App\Models\Cart::class,
|
||||||
'id' => 'session_id',
|
'id' => 'session_id',
|
||||||
'items' => 'items',
|
'items' => 'items',
|
||||||
'conditions' => 'conditions',
|
'conditions' => 'conditions',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Redis
|
||||||
|
|
||||||
|
Store the cart in Redis with an optional TTL.
|
||||||
|
|
||||||
|
```php
|
||||||
|
'driver' => 'redis',
|
||||||
|
|
||||||
|
'drivers' => [
|
||||||
|
'redis' => [
|
||||||
|
'connection' => 'default',
|
||||||
|
'ttl' => 604800, // 7 days in seconds
|
||||||
|
],
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Multi-driver
|
||||||
|
|
||||||
|
Write to multiple drivers simultaneously. Reads come from the first driver listed.
|
||||||
|
|
||||||
|
```php
|
||||||
|
'driver' => 'multi',
|
||||||
|
|
||||||
|
'drivers' => [
|
||||||
|
'multi' => ['session', 'database'],
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Custom driver
|
||||||
|
|
||||||
|
Generate a custom driver stub with `php artisan cart:make:driver MyDriver` then register it in `CartManager`.
|
||||||
|
|
||||||
|
### Artisan Generators
|
||||||
|
|
||||||
|
#### Install: **cart:install**
|
||||||
|
|
||||||
|
Publishes the config file and scaffolds `app/Cart/` with subdirectories for your coupons, tax rules, shipping rates, and custom drivers.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan cart:install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Generate a Coupon: **cart:make:coupon**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan cart:make:coupon TenPercentOff
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates `app/Cart/Coupons/TenPercentOff.php` extending `Wearepixel\Cart\Coupons\Coupon`.
|
||||||
|
|
||||||
|
#### Generate a Tax Rule: **cart:make:tax**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan cart:make:tax GstTaxRule
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates `app/Cart/Tax/GstTaxRule.php` extending `Wearepixel\Cart\Tax\TaxRule`.
|
||||||
|
|
||||||
|
#### Generate a Shipping Rate: **cart:make:shipping**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan cart:make:shipping FlatRateShipping
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates `app/Cart/Shipping/FlatRateShipping.php` extending `Wearepixel\Cart\Shipping\ShippingRate`.
|
||||||
|
|
||||||
|
#### Generate a Custom Driver: **cart:make:driver**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan cart:make:driver ElasticsearchDriver
|
||||||
|
```
|
||||||
|
|
||||||
|
Creates `app/Cart/Drivers/ElasticsearchDriver.php` implementing `CartDriver`.
|
||||||
|
|
||||||
|
#### Debug: **cart:debug**
|
||||||
|
|
||||||
|
Dumps the current cart state. Not available in production.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan cart:debug
|
||||||
|
```
|
||||||
|
|
||||||
|
### First-class Objects
|
||||||
|
|
||||||
|
Instead of building raw `CartCondition` arrays, you can extend the provided base classes.
|
||||||
|
|
||||||
|
#### Coupon
|
||||||
|
|
||||||
|
```php
|
||||||
|
namespace App\Cart\Coupons;
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Coupons\Coupon;
|
||||||
|
|
||||||
|
class TenPercentOff extends Coupon
|
||||||
|
{
|
||||||
|
protected string $code = 'SAVE10';
|
||||||
|
protected string $value = '-10%';
|
||||||
|
protected string $target = 'subtotal';
|
||||||
|
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
return true; // add your own validation logic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply to the cart
|
||||||
|
Cart::coupon(new TenPercentOff);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TaxRule
|
||||||
|
|
||||||
|
```php
|
||||||
|
namespace App\Cart\Tax;
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Tax\TaxRule;
|
||||||
|
|
||||||
|
class GstTaxRule extends TaxRule
|
||||||
|
{
|
||||||
|
protected string $name = 'GST';
|
||||||
|
protected string $value = '10%';
|
||||||
|
protected string $target = 'subtotal';
|
||||||
|
|
||||||
|
public function isApplicable(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart::tax(new GstTaxRule);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ShippingRate
|
||||||
|
|
||||||
|
```php
|
||||||
|
namespace App\Cart\Shipping;
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Shipping\ShippingRate;
|
||||||
|
|
||||||
|
class FlatRateShipping extends ShippingRate
|
||||||
|
{
|
||||||
|
protected string $name = 'Standard Shipping';
|
||||||
|
protected string $value = '+10';
|
||||||
|
protected string $target = 'total';
|
||||||
|
|
||||||
|
public function isApplicable(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart::shipping(new FlatRateShipping);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
#### Cart::fake()
|
||||||
|
|
||||||
|
`Cart::fake()` replaces the active driver with an in-memory `NullDriver` and returns a `CartFactory` for seeding test state.
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Wearepixel\Cart\Cart;
|
||||||
|
|
||||||
|
// In your test
|
||||||
|
$factory = app('cart')->fake();
|
||||||
|
|
||||||
|
$factory->withItems(3);
|
||||||
|
$factory->withCondition(new CartCondition(['name' => 'GST', 'type' => 'tax', 'value' => '10%', 'target' => 'subtotal']));
|
||||||
|
|
||||||
|
// Or chain it
|
||||||
|
app('cart')->fake()->withItems(2)->withCondition($condition);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Assertion Methods
|
||||||
|
|
||||||
|
After `fake()`, the `Cart` instance exposes assertion methods:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$cart = app('cart');
|
||||||
|
$cart->fake();
|
||||||
|
$cart->add(1, 'Widget', 50.00, 2);
|
||||||
|
|
||||||
|
$cart->assertContains(1); // item exists
|
||||||
|
$cart->assertCount(1); // 1 distinct item
|
||||||
|
$cart->assertTotalQuantity(2); // 2 units
|
||||||
|
$cart->assertSubTotal(100.00); // subtotal
|
||||||
|
$cart->assertTotal(100.00); // total
|
||||||
|
$cart->assertNotEmpty(); // cart has items
|
||||||
|
$cart->assertEmpty(); // cart is empty
|
||||||
|
$cart->assertConditionApplied('GST'); // condition present
|
||||||
|
```
|
||||||
|
|
||||||
|
### Livewire / Inertia
|
||||||
|
|
||||||
|
#### HasCart trait (Livewire)
|
||||||
|
|
||||||
|
Add `HasCart` to any Livewire component to get reactive cart state and proxy methods.
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Wearepixel\Cart\Concerns\HasCart;
|
||||||
|
|
||||||
|
class CartComponent extends Component
|
||||||
|
{
|
||||||
|
use HasCart;
|
||||||
|
|
||||||
|
// $cartItems, $cartCount, $cartSubTotal, $cartTotal are auto-populated
|
||||||
|
|
||||||
|
public function addToCart(int $id, string $name, float $price): void
|
||||||
|
{
|
||||||
|
$this->cartAdd($id, $name, $price, 1);
|
||||||
|
// $cartCount, $cartSubTotal etc. update automatically
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ShareCartWithInertia (Inertia)
|
||||||
|
|
||||||
|
Share cart state on every Inertia response from your `HandleInertiaRequests` middleware:
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Wearepixel\Cart\Concerns\ShareCartWithInertia;
|
||||||
|
|
||||||
|
public function share(Request $request): array
|
||||||
|
{
|
||||||
|
return array_merge(parent::share($request), [
|
||||||
|
'cart' => fn() => ShareCartWithInertia::data(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This shares `cart.items`, `cart.subtotal`, `cart.total`, and `cart.count` with every page.
|
||||||
|
|
||||||
### Events
|
### Events
|
||||||
|
|
||||||
The package provides a few events that you can listen to in order to manipulate the cart or take actions based on cart events.
|
The package provides a few events that you can listen to in order to manipulate the cart or take actions based on cart events.
|
||||||
|
|||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
**PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, [SEE BELOW](#reporting-a-vulnerability).**
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you discover a security vulnerability, please report it privately using the below channel:
|
||||||
|
|
||||||
|
**GitHub Private Vulnerability Reporting** — go to the repository's **Security** tab and click **"Report a vulnerability"**. This creates a private advisory visible only to maintainers and provides a structured workflow for triage, fix coordination, and CVE assignment.
|
||||||
|
|
||||||
|
All security vulnerabilities will be promptly addressed.
|
||||||
+165
@@ -0,0 +1,165 @@
|
|||||||
|
# Upgrade Guide
|
||||||
|
|
||||||
|
## v2.x to v3.0
|
||||||
|
|
||||||
|
### Breaking changes
|
||||||
|
|
||||||
|
#### Constructor signature
|
||||||
|
|
||||||
|
`Cart` no longer accepts a raw session/database storage object as its first constructor argument. It now requires a `CartDriver` instance. If you were resolving `Cart` directly (not via the service container), update your instantiation:
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
new Cart($sessionStorage, $events, 'cart', $sessionKey, $config);
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||||
|
|
||||||
|
new Cart(new SessionDriver($session, $sessionKey), $events, 'cart', $config);
|
||||||
|
```
|
||||||
|
|
||||||
|
Applications that use the service container (the normal case) are unaffected - `CartServiceProvider` handles this automatically.
|
||||||
|
|
||||||
|
#### Publish tag renamed
|
||||||
|
|
||||||
|
The config publish tag changed from `config` to `cart-config` to avoid conflicts with other packages:
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan vendor:publish --provider="Wearepixel\Cart\CartServiceProvider" --tag=config
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan vendor:publish --provider="Wearepixel\Cart\CartServiceProvider" --tag=cart-config
|
||||||
|
# or simply:
|
||||||
|
php artisan cart:install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CartSession removed
|
||||||
|
|
||||||
|
`Wearepixel\Cart\CartSession` has been removed. It is replaced by the driver system. If you extended or referenced `CartSession` directly, switch to the appropriate driver (`SessionDriver`, `DatabaseDriver`, etc.).
|
||||||
|
|
||||||
|
### Non-breaking additions
|
||||||
|
|
||||||
|
The following are additive and require no changes to existing code:
|
||||||
|
|
||||||
|
- **Storage drivers:** `SessionDriver`, `DatabaseDriver`, `RedisDriver`, `NullDriver`, `MultiDriver` - configure via `config/cart.php`
|
||||||
|
- **CartManager:** resolves drivers by name, accessible via `app('cart.manager')`
|
||||||
|
- **First-class objects:** `Coupon`, `TaxRule`, `ShippingRate` abstract base classes - optional, existing raw `CartCondition` usage continues to work
|
||||||
|
- **Cart::fake():** returns a `CartFactory` backed by an in-memory `NullDriver` for testing
|
||||||
|
- **Assertion methods:** `Cart::assertContains()`, `assertCount()`, `assertTotal()`, etc.
|
||||||
|
- **HasCart trait:** Livewire reactive cart state
|
||||||
|
- **ShareCartWithInertia:** static class for Inertia middleware integration
|
||||||
|
- **Artisan generators:** `cart:install`, `cart:make:coupon`, `cart:make:tax`, `cart:make:shipping`, `cart:make:driver`, `cart:debug`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Recommended v3 patterns
|
||||||
|
|
||||||
|
v3 is fully backwards compatible - your existing code will continue to work unchanged. The following patterns are recommended for new code and worth migrating to when you touch existing cart logic.
|
||||||
|
|
||||||
|
#### 1. Publish the updated config
|
||||||
|
|
||||||
|
Run the installer to get the new `driver` key and storage configuration in your `config/cart.php`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan cart:install
|
||||||
|
```
|
||||||
|
|
||||||
|
The default driver is `session`, so behaviour is identical to v2 unless you change it.
|
||||||
|
|
||||||
|
#### 2. Replace raw CartCondition arrays with first-class objects
|
||||||
|
|
||||||
|
Generate typed classes with Artisan instead of constructing arrays inline:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan cart:make:coupon TenPercentOff
|
||||||
|
php artisan cart:make:tax GstTaxRule
|
||||||
|
php artisan cart:make:shipping FlatRateShipping
|
||||||
|
```
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
$condition = new CartCondition([
|
||||||
|
'name' => 'SAVE10',
|
||||||
|
'type' => 'coupon',
|
||||||
|
'target' => 'subtotal',
|
||||||
|
'value' => '-10%',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Cart::condition($condition);
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
// app/Cart/Coupons/TenPercentOff.php
|
||||||
|
class TenPercentOff extends Coupon
|
||||||
|
{
|
||||||
|
protected string $code = 'SAVE10';
|
||||||
|
protected string $value = '-10%';
|
||||||
|
protected string $target = 'subtotal';
|
||||||
|
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart::coupon(new TenPercentOff);
|
||||||
|
Cart::tax(new GstTaxRule);
|
||||||
|
Cart::shipping(new FlatRateShipping);
|
||||||
|
```
|
||||||
|
|
||||||
|
The same pattern applies to `TaxRule` (implement `isApplicable()`) and `ShippingRate` (implement `isApplicable()`).
|
||||||
|
|
||||||
|
#### 3. Use Cart::fake() in tests instead of mocking
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Mocking the session or binding a fake driver manually
|
||||||
|
$this->mock(CartDriver::class, function ($mock) {
|
||||||
|
$mock->shouldReceive(...);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
$factory = app('cart')->fake();
|
||||||
|
|
||||||
|
$factory->withItems(3)->withCondition(new GstTaxRule);
|
||||||
|
|
||||||
|
$cart = app('cart');
|
||||||
|
$cart->assertCount(3);
|
||||||
|
$cart->assertConditionApplied('GST');
|
||||||
|
$cart->assertTotal(33.00);
|
||||||
|
```
|
||||||
|
|
||||||
|
`Cart::fake()` swaps the driver to an in-memory `NullDriver` for the duration of the test - no session, database, or Redis needed.
|
||||||
|
|
||||||
|
#### 4. Switch to a persistent driver (optional)
|
||||||
|
|
||||||
|
If you want cart state to survive beyond a session, update `config/cart.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
'driver' => 'database', // or 'redis'
|
||||||
|
```
|
||||||
|
|
||||||
|
For the database driver, publish and run the migration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan cart:install
|
||||||
|
php artisan migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [Storage Drivers](README.md#storage-drivers) section of the README for full configuration options.
|
||||||
+4
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wearepixel/laravel-cart",
|
"name": "wearepixel/laravel-cart",
|
||||||
"version": "2.1.0",
|
"version": "3.0.0",
|
||||||
"description": "A cart implementation for Laravel",
|
"description": "A cart implementation for Laravel",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"laravel",
|
"laravel",
|
||||||
@@ -29,8 +29,9 @@
|
|||||||
"require-dev": {
|
"require-dev": {
|
||||||
"mockery/mockery": "^2.0",
|
"mockery/mockery": "^2.0",
|
||||||
"laravel/pint": "^1.21",
|
"laravel/pint": "^1.21",
|
||||||
"pestphp/pest": "^3.0",
|
"pestphp/pest": "^4.7",
|
||||||
"larapack/dd": "dev-master"
|
"larapack/dd": "dev-master",
|
||||||
|
"phpstan/phpstan": "2.2.x-dev"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|||||||
Generated
+1062
-641
File diff suppressed because it is too large
Load Diff
+18
-4
@@ -16,16 +16,30 @@
|
|||||||
* ---------------------------------------------------------------
|
* ---------------------------------------------------------------
|
||||||
* Storage
|
* Storage
|
||||||
* ---------------------------------------------------------------
|
* ---------------------------------------------------------------
|
||||||
|
* Supported: "session", "database", "redis", "null", "multi"
|
||||||
*/
|
*/
|
||||||
'driver' => 'session',
|
'driver' => 'session',
|
||||||
|
|
||||||
'storage' => [
|
'storage' => [
|
||||||
'session',
|
|
||||||
'database' => [
|
'database' => [
|
||||||
'model' => '',
|
'model' => '',
|
||||||
'id' => '',
|
'id' => '',
|
||||||
'items' => '',
|
'items' => '',
|
||||||
'conditions' => '',
|
'conditions' => '',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* Driver-specific options
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
'drivers' => [
|
||||||
|
'redis' => [
|
||||||
|
'connection' => 'default',
|
||||||
|
'ttl' => 604800, // 7 days in seconds
|
||||||
|
],
|
||||||
|
|
||||||
|
'multi' => ['session', 'database'],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PHPStan bootstrap — stubs for Laravel global helpers that are always
|
||||||
|
* available at runtime in a Laravel application but are not present in the
|
||||||
|
* standalone illuminate/* packages this package depends on.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (! function_exists('app')) {
|
||||||
|
function app(string $abstract = null, array $parameters = []): mixed
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! function_exists('config')) {
|
||||||
|
function config(string|null $key = null, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! function_exists('config_path')) {
|
||||||
|
function config_path(string $path = ''): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! function_exists('app_path')) {
|
||||||
|
function app_path(string $path = ''): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! function_exists('session')) {
|
||||||
|
function session(string|array|null $key = null, mixed $default = null): mixed
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
parameters:
|
||||||
|
level: 5
|
||||||
|
phpVersion: 80300
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
bootstrapFiles:
|
||||||
|
- phpstan-bootstrap.php
|
||||||
|
ignoreErrors:
|
||||||
|
# HasCart is a trait for external use in Livewire components — not used within this package.
|
||||||
|
- identifier: trait.unused
|
||||||
|
# PHPStan 2.x bundles PHP 8.4 stubs where PHP_ROUND_HALF_UP/DOWN map to 1/2 instead of 0/1.
|
||||||
|
# The code is correct at runtime on PHP 8.3. Remove once PHPStan stubs are corrected.
|
||||||
|
- message: '#Parameter \#3 \$mode of function round expects#'
|
||||||
|
path: src/Helpers/Helpers.php
|
||||||
+3
-7
@@ -1,15 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="tests/bootstrap.php" colors="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.5/phpunit.xsd">
|
||||||
bootstrap="vendor/autoload.php"
|
<source ignoreIndirectDeprecations="true">
|
||||||
colors="true"
|
|
||||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.4/phpunit.xsd"
|
|
||||||
>
|
|
||||||
<source>
|
|
||||||
<include>
|
<include>
|
||||||
<directory suffix=".php">./src</directory>
|
<directory suffix=".php">./src</directory>
|
||||||
</include>
|
</include>
|
||||||
</source>
|
</source>
|
||||||
<testsuites>
|
<testsuites>
|
||||||
<testsuite name="Laravel Cart Test Suite">
|
<testsuite name="Laravel Cart Test Suite">
|
||||||
<directory>tests</directory>
|
<directory>tests</directory>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
|
|||||||
+291
-439
File diff suppressed because it is too large
Load Diff
+10
-6
@@ -78,6 +78,14 @@ public function getMaximum(): int|float|null
|
|||||||
return $this->args['maximum'] ?? null;
|
return $this->args['maximum'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of items this condition applies to (null = all items)
|
||||||
|
*/
|
||||||
|
public function getAppliesToQuantity(): int|null
|
||||||
|
{
|
||||||
|
return isset($this->args['applies_to']) ? (int) $this->args['applies_to'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get the additional attributes of a condition
|
* get the additional attributes of a condition
|
||||||
*
|
*
|
||||||
@@ -105,13 +113,11 @@ public function setValue($value): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the order to apply this condition. If no argument order is applied we return 0 as
|
* Set the order to apply this condition.
|
||||||
* indicator that no assignment has been made
|
|
||||||
*
|
*
|
||||||
* @param int $order
|
* @param int $order
|
||||||
* @return int
|
|
||||||
*/
|
*/
|
||||||
public function setOrder($order = 1)
|
public function setOrder($order = 1): void
|
||||||
{
|
{
|
||||||
$this->args['order'] = $order;
|
$this->args['order'] = $order;
|
||||||
}
|
}
|
||||||
@@ -139,8 +145,6 @@ public function applyCondition($totalOrSubTotalOrPrice)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* get the calculated value of this condition supplied by the subtotal|price
|
* get the calculated value of this condition supplied by the subtotal|price
|
||||||
*
|
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
public function getCalculatedValue($totalOrSubTotalOrPrice = null): int|float
|
public function getCalculatedValue($totalOrSubTotalOrPrice = null): int|float
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart;
|
||||||
|
|
||||||
|
use Illuminate\Support\Manager;
|
||||||
|
use Wearepixel\Cart\Drivers\MultiDriver;
|
||||||
|
use Wearepixel\Cart\Drivers\NullDriver;
|
||||||
|
use Wearepixel\Cart\Drivers\RedisDriver;
|
||||||
|
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||||
|
use Wearepixel\Cart\Drivers\DatabaseDriver;
|
||||||
|
use Wearepixel\Cart\Drivers\Contracts\CartDriver;
|
||||||
|
|
||||||
|
class CartManager extends Manager
|
||||||
|
{
|
||||||
|
public function getDefaultDriver(): string
|
||||||
|
{
|
||||||
|
return $this->config->get('cart.driver', 'session');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sessionKey(): string
|
||||||
|
{
|
||||||
|
return $this->container['session']->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createSessionDriver(): CartDriver
|
||||||
|
{
|
||||||
|
return new SessionDriver(
|
||||||
|
$this->container['session'],
|
||||||
|
$this->sessionKey(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createDatabaseDriver(): CartDriver
|
||||||
|
{
|
||||||
|
$config = $this->config->get('cart.storage.database');
|
||||||
|
|
||||||
|
return new DatabaseDriver(
|
||||||
|
$config['model'],
|
||||||
|
$config['id'],
|
||||||
|
$config['items'],
|
||||||
|
$config['conditions'],
|
||||||
|
$this->sessionKey(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createRedisDriver(): CartDriver
|
||||||
|
{
|
||||||
|
$config = $this->config->get('cart.drivers.redis', []);
|
||||||
|
|
||||||
|
return new RedisDriver(
|
||||||
|
$this->container['redis'],
|
||||||
|
$this->sessionKey(),
|
||||||
|
$config['connection'] ?? 'default',
|
||||||
|
$config['ttl'] ?? 604800,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createNullDriver(): CartDriver
|
||||||
|
{
|
||||||
|
return new NullDriver($this->sessionKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createMultiDriver(): CartDriver
|
||||||
|
{
|
||||||
|
$names = $this->config->get('cart.drivers.multi', ['session', 'database']);
|
||||||
|
$drivers = array_map(fn(string $name) => $this->driver($name), $names);
|
||||||
|
|
||||||
|
return new MultiDriver($drivers);
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
-35
@@ -1,63 +1,53 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Wearepixel\Cart;
|
namespace Wearepixel\Cart;
|
||||||
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Wearepixel\Cart\Commands\DebugCommand;
|
||||||
|
use Wearepixel\Cart\Commands\InstallCommand;
|
||||||
|
use Wearepixel\Cart\Commands\MakeCouponCommand;
|
||||||
|
use Wearepixel\Cart\Commands\MakeDriverCommand;
|
||||||
|
use Wearepixel\Cart\Commands\MakeShippingCommand;
|
||||||
|
use Wearepixel\Cart\Commands\MakeTaxCommand;
|
||||||
|
|
||||||
class CartServiceProvider extends ServiceProvider
|
class CartServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
/**
|
public function boot(): void
|
||||||
* Indicates if loading of the provider is deferred.
|
|
||||||
*
|
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
protected $defer = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Boot the service provider.
|
|
||||||
*/
|
|
||||||
public function boot()
|
|
||||||
{
|
{
|
||||||
if ($this->app->runningInConsole()) {
|
if ($this->app->runningInConsole()) {
|
||||||
$this->publishes([
|
$this->publishes([
|
||||||
__DIR__ . '/../config/config.php' => config_path('cart.php'),
|
__DIR__ . '/../config/config.php' => config_path('cart.php'),
|
||||||
], 'config');
|
], 'cart-config');
|
||||||
|
|
||||||
|
$this->commands([
|
||||||
|
InstallCommand::class,
|
||||||
|
MakeCouponCommand::class,
|
||||||
|
MakeTaxCommand::class,
|
||||||
|
MakeShippingCommand::class,
|
||||||
|
MakeDriverCommand::class,
|
||||||
|
DebugCommand::class,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function register(): void
|
||||||
* Register the service provider.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function register()
|
|
||||||
{
|
{
|
||||||
$this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'cart');
|
$this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'cart');
|
||||||
|
|
||||||
|
$this->app->singleton('cart.manager', fn($app) => new CartManager($app));
|
||||||
|
|
||||||
$this->app->singleton('cart', function ($app) {
|
$this->app->singleton('cart', function ($app) {
|
||||||
$config = config('cart');
|
$config = config('cart');
|
||||||
$events = $app['events'];
|
$events = $app['events'];
|
||||||
|
$driver = $app['cart.manager']->driver();
|
||||||
|
|
||||||
$storage = $config['driver'] === 'database'
|
return new Cart($driver, $events, 'cart', $config);
|
||||||
? new $config['storage']['database']['model']
|
|
||||||
: $app['session'];
|
|
||||||
|
|
||||||
return new Cart(
|
|
||||||
$storage,
|
|
||||||
$events,
|
|
||||||
'cart',
|
|
||||||
session()->getId(),
|
|
||||||
$config
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function provides(): array
|
||||||
* Get the services provided by the provider.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function provides()
|
|
||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Wearepixel\Cart;
|
|
||||||
|
|
||||||
class CartSession
|
|
||||||
{
|
|
||||||
private $customStorage;
|
|
||||||
|
|
||||||
public $sessionKey;
|
|
||||||
public $driver;
|
|
||||||
|
|
||||||
private $session;
|
|
||||||
|
|
||||||
private $sessionId = null;
|
|
||||||
private $itemsKey = null;
|
|
||||||
private $conditionsKey = null;
|
|
||||||
|
|
||||||
public function __construct($session, $sessionKey, $config)
|
|
||||||
{
|
|
||||||
$this->sessionKey = $sessionKey;
|
|
||||||
$this->driver = $config['driver'];
|
|
||||||
|
|
||||||
if ($this->driver === 'session') {
|
|
||||||
$this->customStorage = false;
|
|
||||||
$this->session = $session;
|
|
||||||
$this->itemsKey = $this->sessionKey . '_cart_items';
|
|
||||||
$this->conditionsKey = $this->sessionKey . '_cart_conditions';
|
|
||||||
} elseif ($this->driver === 'database') {
|
|
||||||
$this->customStorage = true;
|
|
||||||
|
|
||||||
$this->sessionId = $config['storage']['database']['id'];
|
|
||||||
$this->itemsKey = $config['storage']['database']['items'];
|
|
||||||
$this->conditionsKey = $config['storage']['database']['conditions'];
|
|
||||||
|
|
||||||
$this->session = $session->firstOrNew([$this->sessionId => $this->sessionKey]);
|
|
||||||
|
|
||||||
$this->session[$this->itemsKey] = $this->session[$this->itemsKey] ?? [];
|
|
||||||
$this->session[$this->conditionsKey] = $this->session[$this->conditionsKey] ?? [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isInternalSession($session)
|
|
||||||
{
|
|
||||||
return in_array(is_object($session) ? get_class($session) : $session, [
|
|
||||||
'Illuminate\Session\SessionManager',
|
|
||||||
'Wearepixel\Cart\Tests\Helpers\SessionMock',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSessionKey()
|
|
||||||
{
|
|
||||||
return $this->sessionKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setSessionKey(string $sessionKey)
|
|
||||||
{
|
|
||||||
// set the session key and then update the session
|
|
||||||
$this->sessionKey = $sessionKey;
|
|
||||||
|
|
||||||
if ($this->driver == 'session') {
|
|
||||||
$session = $this->session->get($this->itemsKey);
|
|
||||||
$sessionKey = $this->session->get($this->conditionsKey);
|
|
||||||
|
|
||||||
$this->session->forget($this->itemsKey);
|
|
||||||
$this->session->forget($this->conditionsKey);
|
|
||||||
|
|
||||||
$this->itemsKey = $this->sessionKey . '_cart_items';
|
|
||||||
$this->conditionsKey = $this->sessionKey . '_cart_conditions';
|
|
||||||
|
|
||||||
$this->session->put($this->itemsKey, $session);
|
|
||||||
$this->session->put($this->conditionsKey, $sessionKey);
|
|
||||||
} elseif ($this->driver == 'database') {
|
|
||||||
$this->session[$this->sessionId] = $sessionKey;
|
|
||||||
$this->session->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function has($key)
|
|
||||||
{
|
|
||||||
return $this->session->has($key) ?? $this->session;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getItems()
|
|
||||||
{
|
|
||||||
return $this->driver == 'session' ? $this->session->get($this->itemsKey) : $this->session[$this->itemsKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getConditions()
|
|
||||||
{
|
|
||||||
return $this->driver == 'session' ? $this->session->get($this->conditionsKey) : $this->session[$this->conditionsKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function putItems($value)
|
|
||||||
{
|
|
||||||
if ($this->driver == 'session') {
|
|
||||||
return $this->session->put($this->itemsKey, $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert any conditions to arrays
|
|
||||||
$value = is_array($value) ? $value : $value->toArray();
|
|
||||||
|
|
||||||
foreach ($value as $key => $item) {
|
|
||||||
if ($item['conditions']) {
|
|
||||||
if (is_array($item['conditions'])) {
|
|
||||||
$value[$key]['conditions'] = collect($item['conditions'])->map(function ($condition) {
|
|
||||||
return $condition->toArray();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$value[$key]['conditions'] = $item['conditions']->toArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->session[$this->itemsKey] = $value;
|
|
||||||
$this->session->save();
|
|
||||||
|
|
||||||
return $this->session;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function putConditions($value)
|
|
||||||
{
|
|
||||||
if ($this->driver == 'session') {
|
|
||||||
return $this->session->put($this->conditionsKey, $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! empty($value)) {
|
|
||||||
$value = $value->mapWithKeys(function ($condition) {
|
|
||||||
return [$condition->getName() => $condition->toArray()];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->session[$this->conditionsKey] = $value;
|
|
||||||
$this->session->save();
|
|
||||||
|
|
||||||
return $this->session;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function clearItems()
|
|
||||||
{
|
|
||||||
if ($this->driver == 'session') {
|
|
||||||
$this->session->forget($this->itemsKey);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->session[$this->itemsKey] = [];
|
|
||||||
|
|
||||||
return $this->session->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function clear()
|
|
||||||
{
|
|
||||||
if ($this->driver == 'session') {
|
|
||||||
$this->session->forget($this->itemsKey);
|
|
||||||
$this->session->forget($this->conditionsKey);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$deleted = $this->session->delete();
|
|
||||||
|
|
||||||
$this->session = [
|
|
||||||
$this->itemsKey => [],
|
|
||||||
$this->conditionsKey => [],
|
|
||||||
];
|
|
||||||
|
|
||||||
return $deleted;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSessionModel()
|
|
||||||
{
|
|
||||||
return $this->session;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Wearepixel\Cart\Cart;
|
||||||
|
|
||||||
|
class DebugCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'cart:debug';
|
||||||
|
|
||||||
|
protected $description = 'Dump the current cart state. Only runs outside production.';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
if (app()->isProduction()) {
|
||||||
|
$this->error('cart:debug is not available in production.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cart = app('cart');
|
||||||
|
$data = static::debugData($cart);
|
||||||
|
|
||||||
|
$this->info('Cart Debug');
|
||||||
|
$this->line('');
|
||||||
|
$this->line(" Driver: <comment>{$data['driver']}</comment>");
|
||||||
|
$this->line(" Items: <comment>{$data['item_count']}</comment>");
|
||||||
|
$this->line(" Total Quantity: <comment>{$data['total_quantity']}</comment>");
|
||||||
|
$this->line(" Subtotal: <comment>{$data['subtotal']}</comment>");
|
||||||
|
$this->line(" Total: <comment>{$data['total']}</comment>");
|
||||||
|
|
||||||
|
if (! empty($data['conditions'])) {
|
||||||
|
$this->line(" Conditions: <comment>" . implode(', ', $data['conditions']) . '</comment>');
|
||||||
|
} else {
|
||||||
|
$this->line(' Conditions: <comment>none</comment>');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($data['items'])) {
|
||||||
|
$this->line('');
|
||||||
|
$this->table(['ID', 'Name', 'Price', 'Qty'], $data['items']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function debugData(Cart $cart): array
|
||||||
|
{
|
||||||
|
$items = $cart->getContent();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'driver' => $cart->getDriver()->getDriverName(),
|
||||||
|
'item_count' => $items->count(),
|
||||||
|
'total_quantity' => (int) $cart->getTotalQuantity(),
|
||||||
|
'subtotal' => (float) $cart->getSubTotal(false),
|
||||||
|
'total' => (float) $cart->getTotal(),
|
||||||
|
'conditions' => $cart->getConditions()->map(fn($c) => $c->getName())->values()->toArray(),
|
||||||
|
'items' => $items->map(fn($item) => [
|
||||||
|
$item->id,
|
||||||
|
$item->name,
|
||||||
|
$item->price,
|
||||||
|
$item->quantity,
|
||||||
|
])->values()->toArray(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class InstallCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'cart:install';
|
||||||
|
|
||||||
|
protected $description = 'Publish the cart config and scaffold the app/Cart directory structure.';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$this->call('vendor:publish', ['--tag' => 'cart-config', '--ansi' => true]);
|
||||||
|
|
||||||
|
static::scaffoldDirectories(app_path('Cart'));
|
||||||
|
|
||||||
|
$this->info('Cart installed successfully.');
|
||||||
|
$this->newLine();
|
||||||
|
$this->line(' Next steps:');
|
||||||
|
$this->line(' 1. Review <comment>config/cart.php</comment> and set your preferred storage driver.');
|
||||||
|
$this->line(' 2. Run <comment>php artisan cart:make:coupon MyCoupon</comment> to create your first coupon.');
|
||||||
|
$this->line(' 3. Run <comment>php artisan cart:make:tax MyTax</comment> to add a tax rule.');
|
||||||
|
$this->line(' 4. Run <comment>php artisan cart:make:shipping MyShipping</comment> to add a shipping rate.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function scaffoldDirectories(string $base): void
|
||||||
|
{
|
||||||
|
foreach (['Coupons', 'Tax', 'Shipping', 'Drivers'] as $subdir) {
|
||||||
|
$path = "{$base}/{$subdir}";
|
||||||
|
if (! is_dir($path)) {
|
||||||
|
mkdir($path, 0755, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class MakeCouponCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'cart:make:coupon {name : The class name of the coupon}';
|
||||||
|
|
||||||
|
protected $description = 'Create a new Coupon class in app/Cart/Coupons.';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$name = $this->argument('name');
|
||||||
|
|
||||||
|
if (! preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $name)) {
|
||||||
|
$this->error("Invalid class name: [{$name}]. Must be a valid PHP class name.");
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$directory = app_path('Cart/Coupons');
|
||||||
|
$path = "{$directory}/{$name}.php";
|
||||||
|
|
||||||
|
if (file_exists($path)) {
|
||||||
|
$this->error("Coupon already exists: app/Cart/Coupons/{$name}.php");
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_dir($directory)) {
|
||||||
|
mkdir($directory, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_put_contents($path, static::stub($name)) === false) {
|
||||||
|
$this->error("Failed to write file: {$path}");
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Coupon created: app/Cart/Coupons/{$name}.php");
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function stub(string $className): string
|
||||||
|
{
|
||||||
|
return <<<PHP
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Cart\Coupons;
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Coupons\Coupon;
|
||||||
|
|
||||||
|
class {$className} extends Coupon
|
||||||
|
{
|
||||||
|
protected string \$code = 'COUPON_CODE';
|
||||||
|
|
||||||
|
protected string \$value = '-10%';
|
||||||
|
|
||||||
|
protected string \$target = 'subtotal';
|
||||||
|
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PHP;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class MakeDriverCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'cart:make:driver {name : The class name of the custom cart driver}';
|
||||||
|
|
||||||
|
protected $description = 'Create a new CartDriver implementation in app/Cart/Drivers.';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$name = $this->argument('name');
|
||||||
|
|
||||||
|
if (! preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $name)) {
|
||||||
|
$this->error("Invalid class name: [{$name}]. Must be a valid PHP class name.");
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$directory = app_path('Cart/Drivers');
|
||||||
|
$path = "{$directory}/{$name}.php";
|
||||||
|
|
||||||
|
if (file_exists($path)) {
|
||||||
|
$this->error("Driver already exists: app/Cart/Drivers/{$name}.php");
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_dir($directory)) {
|
||||||
|
mkdir($directory, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_put_contents($path, static::stub($name)) === false) {
|
||||||
|
$this->error("Failed to write file: {$path}");
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Driver created: app/Cart/Drivers/{$name}.php");
|
||||||
|
$this->line(" Register it in config/cart.php under <comment>drivers</comment>.");
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function stub(string $className): string
|
||||||
|
{
|
||||||
|
return <<<PHP
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Cart\Drivers;
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Drivers\Contracts\CartDriver;
|
||||||
|
|
||||||
|
class {$className} implements CartDriver
|
||||||
|
{
|
||||||
|
private string \$sessionKey;
|
||||||
|
|
||||||
|
public function __construct(string \$sessionKey)
|
||||||
|
{
|
||||||
|
\$this->sessionKey = \$sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSessionKey(): string
|
||||||
|
{
|
||||||
|
return \$this->sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSessionKey(string \$sessionKey): void
|
||||||
|
{
|
||||||
|
\$this->sessionKey = \$sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDriverName(): string
|
||||||
|
{
|
||||||
|
return 'custom';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getItems(): array
|
||||||
|
{
|
||||||
|
// TODO: retrieve items from your storage backend
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function putItems(array \$items): void
|
||||||
|
{
|
||||||
|
// TODO: persist items to your storage backend
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConditions(): array
|
||||||
|
{
|
||||||
|
// TODO: retrieve conditions from your storage backend
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function putConditions(array \$conditions): void
|
||||||
|
{
|
||||||
|
// TODO: persist conditions to your storage backend
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearItems(): void
|
||||||
|
{
|
||||||
|
// TODO: remove only items from your storage backend
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flush(): void
|
||||||
|
{
|
||||||
|
// TODO: remove both items and conditions from your storage backend
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSessionModel(): mixed
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PHP;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class MakeShippingCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'cart:make:shipping {name : The class name of the shipping rate}';
|
||||||
|
|
||||||
|
protected $description = 'Create a new ShippingRate class in app/Cart/Shipping.';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$name = $this->argument('name');
|
||||||
|
|
||||||
|
if (! preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $name)) {
|
||||||
|
$this->error("Invalid class name: [{$name}]. Must be a valid PHP class name.");
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$directory = app_path('Cart/Shipping');
|
||||||
|
$path = "{$directory}/{$name}.php";
|
||||||
|
|
||||||
|
if (file_exists($path)) {
|
||||||
|
$this->error("Shipping rate already exists: app/Cart/Shipping/{$name}.php");
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_dir($directory)) {
|
||||||
|
mkdir($directory, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_put_contents($path, static::stub($name)) === false) {
|
||||||
|
$this->error("Failed to write file: {$path}");
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Shipping rate created: app/Cart/Shipping/{$name}.php");
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function stub(string $className): string
|
||||||
|
{
|
||||||
|
return <<<PHP
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Cart\Shipping;
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Shipping\ShippingRate;
|
||||||
|
|
||||||
|
class {$className} extends ShippingRate
|
||||||
|
{
|
||||||
|
protected string \$name = 'Standard Shipping';
|
||||||
|
|
||||||
|
protected string \$value = '+10';
|
||||||
|
|
||||||
|
protected string \$target = 'total';
|
||||||
|
|
||||||
|
public function isApplicable(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PHP;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class MakeTaxCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'cart:make:tax {name : The class name of the tax rule}';
|
||||||
|
|
||||||
|
protected $description = 'Create a new TaxRule class in app/Cart/Tax.';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$name = $this->argument('name');
|
||||||
|
|
||||||
|
if (! preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $name)) {
|
||||||
|
$this->error("Invalid class name: [{$name}]. Must be a valid PHP class name.");
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$directory = app_path('Cart/Tax');
|
||||||
|
$path = "{$directory}/{$name}.php";
|
||||||
|
|
||||||
|
if (file_exists($path)) {
|
||||||
|
$this->error("Tax rule already exists: app/Cart/Tax/{$name}.php");
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_dir($directory)) {
|
||||||
|
mkdir($directory, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_put_contents($path, static::stub($name)) === false) {
|
||||||
|
$this->error("Failed to write file: {$path}");
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Tax rule created: app/Cart/Tax/{$name}.php");
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function stub(string $className): string
|
||||||
|
{
|
||||||
|
return <<<PHP
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Cart\Tax;
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Tax\TaxRule;
|
||||||
|
|
||||||
|
class {$className} extends TaxRule
|
||||||
|
{
|
||||||
|
protected string \$name = 'Tax Name';
|
||||||
|
|
||||||
|
protected string \$value = '10%';
|
||||||
|
|
||||||
|
protected string \$target = 'subtotal';
|
||||||
|
|
||||||
|
public function isApplicable(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PHP;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Concerns;
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Cart;
|
||||||
|
|
||||||
|
trait HasCart
|
||||||
|
{
|
||||||
|
public array $cartItems = [];
|
||||||
|
|
||||||
|
public float $cartTotal = 0.0;
|
||||||
|
|
||||||
|
public float $cartSubTotal = 0.0;
|
||||||
|
|
||||||
|
public int $cartCount = 0;
|
||||||
|
|
||||||
|
protected function getCart(): Cart
|
||||||
|
{
|
||||||
|
return app('cart');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function refreshCart(): void
|
||||||
|
{
|
||||||
|
$cart = $this->getCart();
|
||||||
|
|
||||||
|
$this->cartItems = $cart->getContent()->toArray();
|
||||||
|
$this->cartSubTotal = (float) $cart->getSubTotal(false);
|
||||||
|
$this->cartTotal = (float) $cart->getTotal();
|
||||||
|
$this->cartCount = (int) $cart->getTotalQuantity();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cartAdd(
|
||||||
|
int|string $id,
|
||||||
|
string $name,
|
||||||
|
float $price,
|
||||||
|
int $quantity = 1,
|
||||||
|
array $attributes = [],
|
||||||
|
): void {
|
||||||
|
$this->getCart()->add($id, $name, $price, $quantity, $attributes);
|
||||||
|
$this->refreshCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cartRemove(int|string $id): void
|
||||||
|
{
|
||||||
|
$this->getCart()->remove($id);
|
||||||
|
$this->refreshCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cartUpdate(int|string $id, array $data): void
|
||||||
|
{
|
||||||
|
$this->getCart()->update($id, $data);
|
||||||
|
$this->refreshCart();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cartClear(): void
|
||||||
|
{
|
||||||
|
$this->getCart()->clear();
|
||||||
|
$this->refreshCart();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Concerns;
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Cart;
|
||||||
|
|
||||||
|
class ShareCartWithInertia
|
||||||
|
{
|
||||||
|
public static function data(?Cart $cart = null): array
|
||||||
|
{
|
||||||
|
$cart ??= app('cart');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'items' => $cart->getContent()->toArray(),
|
||||||
|
'subtotal' => (float) $cart->getSubTotal(false),
|
||||||
|
'total' => (float) $cart->getTotal(),
|
||||||
|
'count' => (int) $cart->getTotalQuantity(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Contracts;
|
||||||
|
|
||||||
|
use Wearepixel\Cart\CartCondition;
|
||||||
|
|
||||||
|
interface CartConditionable
|
||||||
|
{
|
||||||
|
public function toCondition(): CartCondition;
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Coupons;
|
||||||
|
|
||||||
|
use Wearepixel\Cart\CartCondition;
|
||||||
|
use Wearepixel\Cart\Contracts\CartConditionable;
|
||||||
|
use Wearepixel\Cart\Exceptions\InvalidCouponException;
|
||||||
|
|
||||||
|
abstract class Coupon implements CartConditionable
|
||||||
|
{
|
||||||
|
protected string $code;
|
||||||
|
|
||||||
|
protected string $value;
|
||||||
|
|
||||||
|
protected string $target = 'subtotal';
|
||||||
|
|
||||||
|
protected ?float $minimum = null;
|
||||||
|
|
||||||
|
protected ?float $maximum = null;
|
||||||
|
|
||||||
|
abstract public function isValid(): bool;
|
||||||
|
|
||||||
|
public function getCode(): string
|
||||||
|
{
|
||||||
|
return $this->code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toCondition(): CartCondition
|
||||||
|
{
|
||||||
|
if (! $this->isValid()) {
|
||||||
|
throw new InvalidCouponException("Coupon [{$this->code}] is not valid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'name' => $this->code,
|
||||||
|
'type' => 'coupon',
|
||||||
|
'value' => $this->value,
|
||||||
|
'target' => $this->target,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->minimum !== null) {
|
||||||
|
$data['minimum'] = $this->minimum;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->maximum !== null) {
|
||||||
|
$data['maximum'] = $this->maximum;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CartCondition($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Drivers\Contracts;
|
||||||
|
|
||||||
|
interface CartDriver
|
||||||
|
{
|
||||||
|
public function getSessionKey(): string;
|
||||||
|
|
||||||
|
public function setSessionKey(string $sessionKey): void;
|
||||||
|
|
||||||
|
public function getDriverName(): string;
|
||||||
|
|
||||||
|
public function getItems(): array;
|
||||||
|
|
||||||
|
public function putItems(array $items): void;
|
||||||
|
|
||||||
|
public function getConditions(): array;
|
||||||
|
|
||||||
|
public function putConditions(array $conditions): void;
|
||||||
|
|
||||||
|
public function clearItems(): void;
|
||||||
|
|
||||||
|
public function flush(): void;
|
||||||
|
|
||||||
|
public function getSessionModel(): mixed;
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Drivers;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Wearepixel\Cart\Drivers\Contracts\CartDriver;
|
||||||
|
|
||||||
|
class DatabaseDriver implements CartDriver
|
||||||
|
{
|
||||||
|
private Model $model;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private string $modelClass,
|
||||||
|
private string $idColumn,
|
||||||
|
private string $itemsColumn,
|
||||||
|
private string $conditionsColumn,
|
||||||
|
private string $sessionKey,
|
||||||
|
) {
|
||||||
|
$this->model = (new $modelClass)->firstOrNew([$idColumn => $sessionKey]);
|
||||||
|
$this->model[$itemsColumn] ??= [];
|
||||||
|
$this->model[$conditionsColumn] ??= [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSessionKey(): string
|
||||||
|
{
|
||||||
|
return $this->sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSessionKey(string $sessionKey): void
|
||||||
|
{
|
||||||
|
$this->sessionKey = $sessionKey;
|
||||||
|
$this->model[$this->idColumn] = $sessionKey;
|
||||||
|
$this->model->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDriverName(): string
|
||||||
|
{
|
||||||
|
return 'database';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getItems(): array
|
||||||
|
{
|
||||||
|
return $this->model[$this->itemsColumn] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function putItems(array $items): void
|
||||||
|
{
|
||||||
|
$this->model[$this->itemsColumn] = $items;
|
||||||
|
$this->model->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConditions(): array
|
||||||
|
{
|
||||||
|
return $this->model[$this->conditionsColumn] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function putConditions(array $conditions): void
|
||||||
|
{
|
||||||
|
$this->model[$this->conditionsColumn] = $conditions;
|
||||||
|
$this->model->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearItems(): void
|
||||||
|
{
|
||||||
|
$this->model[$this->itemsColumn] = [];
|
||||||
|
$this->model->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flush(): void
|
||||||
|
{
|
||||||
|
$this->model->delete();
|
||||||
|
$this->model[$this->itemsColumn] = [];
|
||||||
|
$this->model[$this->conditionsColumn] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSessionModel(): mixed
|
||||||
|
{
|
||||||
|
return $this->model;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Drivers;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use Wearepixel\Cart\Drivers\Contracts\CartDriver;
|
||||||
|
|
||||||
|
class MultiDriver implements CartDriver
|
||||||
|
{
|
||||||
|
/** @param CartDriver[] $drivers */
|
||||||
|
public function __construct(private array $drivers)
|
||||||
|
{
|
||||||
|
if (empty($drivers)) {
|
||||||
|
throw new InvalidArgumentException('MultiDriver requires at least one driver.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function primary(): CartDriver
|
||||||
|
{
|
||||||
|
return $this->drivers[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSessionKey(): string
|
||||||
|
{
|
||||||
|
return $this->primary()->getSessionKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSessionKey(string $sessionKey): void
|
||||||
|
{
|
||||||
|
foreach ($this->drivers as $driver) {
|
||||||
|
$driver->setSessionKey($sessionKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDriverName(): string
|
||||||
|
{
|
||||||
|
return 'multi';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getItems(): array
|
||||||
|
{
|
||||||
|
return $this->primary()->getItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function putItems(array $items): void
|
||||||
|
{
|
||||||
|
foreach ($this->drivers as $driver) {
|
||||||
|
$driver->putItems($items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConditions(): array
|
||||||
|
{
|
||||||
|
return $this->primary()->getConditions();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function putConditions(array $conditions): void
|
||||||
|
{
|
||||||
|
foreach ($this->drivers as $driver) {
|
||||||
|
$driver->putConditions($conditions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearItems(): void
|
||||||
|
{
|
||||||
|
foreach ($this->drivers as $driver) {
|
||||||
|
$driver->clearItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flush(): void
|
||||||
|
{
|
||||||
|
foreach ($this->drivers as $driver) {
|
||||||
|
$driver->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSessionModel(): mixed
|
||||||
|
{
|
||||||
|
return $this->primary()->getSessionModel();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Drivers;
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Drivers\Contracts\CartDriver;
|
||||||
|
|
||||||
|
class NullDriver implements CartDriver
|
||||||
|
{
|
||||||
|
private array $items = [];
|
||||||
|
|
||||||
|
private array $conditions = [];
|
||||||
|
|
||||||
|
public function __construct(private string $sessionKey) {}
|
||||||
|
|
||||||
|
public function getSessionKey(): string
|
||||||
|
{
|
||||||
|
return $this->sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSessionKey(string $sessionKey): void
|
||||||
|
{
|
||||||
|
$this->sessionKey = $sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDriverName(): string
|
||||||
|
{
|
||||||
|
return 'null';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getItems(): array
|
||||||
|
{
|
||||||
|
return $this->items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function putItems(array $items): void
|
||||||
|
{
|
||||||
|
$this->items = $items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConditions(): array
|
||||||
|
{
|
||||||
|
return $this->conditions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function putConditions(array $conditions): void
|
||||||
|
{
|
||||||
|
$this->conditions = $conditions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearItems(): void
|
||||||
|
{
|
||||||
|
$this->items = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flush(): void
|
||||||
|
{
|
||||||
|
$this->items = [];
|
||||||
|
$this->conditions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSessionModel(): mixed
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Drivers;
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Drivers\Contracts\CartDriver;
|
||||||
|
|
||||||
|
class RedisDriver implements CartDriver
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private mixed $redis,
|
||||||
|
private string $sessionKey,
|
||||||
|
private string $connection = 'default',
|
||||||
|
private int $ttl = 604800,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private function itemsKey(): string
|
||||||
|
{
|
||||||
|
return "cart:{$this->sessionKey}:items";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function conditionsKey(): string
|
||||||
|
{
|
||||||
|
return "cart:{$this->sessionKey}:conditions";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSessionKey(): string
|
||||||
|
{
|
||||||
|
return $this->sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSessionKey(string $sessionKey): void
|
||||||
|
{
|
||||||
|
$items = $this->getItems();
|
||||||
|
$conditions = $this->getConditions();
|
||||||
|
|
||||||
|
$this->flush();
|
||||||
|
|
||||||
|
$this->sessionKey = $sessionKey;
|
||||||
|
|
||||||
|
if (! empty($items)) {
|
||||||
|
$this->putItems($items);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($conditions)) {
|
||||||
|
$this->putConditions($conditions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDriverName(): string
|
||||||
|
{
|
||||||
|
return 'redis';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getItems(): array
|
||||||
|
{
|
||||||
|
$data = $this->redis->connection($this->connection)->get($this->itemsKey());
|
||||||
|
|
||||||
|
return $data ? (json_decode($data, true) ?? []) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function putItems(array $items): void
|
||||||
|
{
|
||||||
|
$this->redis->connection($this->connection)->setex(
|
||||||
|
$this->itemsKey(),
|
||||||
|
$this->ttl,
|
||||||
|
json_encode($items),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConditions(): array
|
||||||
|
{
|
||||||
|
$data = $this->redis->connection($this->connection)->get($this->conditionsKey());
|
||||||
|
|
||||||
|
return $data ? (json_decode($data, true) ?? []) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function putConditions(array $conditions): void
|
||||||
|
{
|
||||||
|
$this->redis->connection($this->connection)->setex(
|
||||||
|
$this->conditionsKey(),
|
||||||
|
$this->ttl,
|
||||||
|
json_encode($conditions),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearItems(): void
|
||||||
|
{
|
||||||
|
$this->redis->connection($this->connection)->del($this->itemsKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flush(): void
|
||||||
|
{
|
||||||
|
$this->redis->connection($this->connection)->del($this->itemsKey(), $this->conditionsKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSessionModel(): mixed
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Drivers;
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Drivers\Contracts\CartDriver;
|
||||||
|
|
||||||
|
class SessionDriver implements CartDriver
|
||||||
|
{
|
||||||
|
private string $itemsKey;
|
||||||
|
|
||||||
|
private string $conditionsKey;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private mixed $session = null,
|
||||||
|
private ?string $sessionKey = null,
|
||||||
|
) {
|
||||||
|
$this->session ??= app('session');
|
||||||
|
$this->sessionKey ??= $this->session->getId();
|
||||||
|
$this->itemsKey = $this->sessionKey . '_cart_items';
|
||||||
|
$this->conditionsKey = $this->sessionKey . '_cart_conditions';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSessionKey(): string
|
||||||
|
{
|
||||||
|
return $this->sessionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSessionKey(string $sessionKey): void
|
||||||
|
{
|
||||||
|
$items = $this->session->get($this->itemsKey);
|
||||||
|
$conditions = $this->session->get($this->conditionsKey);
|
||||||
|
|
||||||
|
$this->session->forget($this->itemsKey);
|
||||||
|
$this->session->forget($this->conditionsKey);
|
||||||
|
|
||||||
|
$this->sessionKey = $sessionKey;
|
||||||
|
$this->itemsKey = $sessionKey . '_cart_items';
|
||||||
|
$this->conditionsKey = $sessionKey . '_cart_conditions';
|
||||||
|
|
||||||
|
if ($items !== null) {
|
||||||
|
$this->session->put($this->itemsKey, $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($conditions !== null) {
|
||||||
|
$this->session->put($this->conditionsKey, $conditions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDriverName(): string
|
||||||
|
{
|
||||||
|
return 'session';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getItems(): array
|
||||||
|
{
|
||||||
|
return $this->session->get($this->itemsKey) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function putItems(array $items): void
|
||||||
|
{
|
||||||
|
$this->session->put($this->itemsKey, $items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConditions(): array
|
||||||
|
{
|
||||||
|
return $this->session->get($this->conditionsKey) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function putConditions(array $conditions): void
|
||||||
|
{
|
||||||
|
$this->session->put($this->conditionsKey, $conditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearItems(): void
|
||||||
|
{
|
||||||
|
$this->session->forget($this->itemsKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flush(): void
|
||||||
|
{
|
||||||
|
$this->session->forget($this->itemsKey);
|
||||||
|
$this->session->forget($this->conditionsKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSessionModel(): mixed
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Exceptions;
|
||||||
|
|
||||||
|
class InvalidCouponException extends \Exception {}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Exceptions;
|
||||||
|
|
||||||
|
class InvalidShippingRateException extends \Exception {}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Exceptions;
|
||||||
|
|
||||||
|
class InvalidTaxRuleException extends \Exception {}
|
||||||
@@ -3,10 +3,68 @@
|
|||||||
namespace Wearepixel\Cart\Facades;
|
namespace Wearepixel\Cart\Facades;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Facade;
|
use Illuminate\Support\Facades\Facade;
|
||||||
|
use Wearepixel\Cart\Cart;
|
||||||
|
use Wearepixel\Cart\CartCollection;
|
||||||
|
use Wearepixel\Cart\CartCondition;
|
||||||
|
use Wearepixel\Cart\CartConditionCollection;
|
||||||
|
use Wearepixel\Cart\Coupons\Coupon;
|
||||||
|
use Wearepixel\Cart\Drivers\Contracts\CartDriver;
|
||||||
|
use Wearepixel\Cart\ItemCollection;
|
||||||
|
use Wearepixel\Cart\Shipping\ShippingRate;
|
||||||
|
use Wearepixel\Cart\Tax\TaxRule;
|
||||||
|
use Wearepixel\Cart\Testing\CartFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @mixin Cart
|
||||||
|
*
|
||||||
|
* @method static CartDriver getDriver()
|
||||||
|
* @method static CartFactory fake()
|
||||||
|
* @method static void assertContains(int|string $id)
|
||||||
|
* @method static void assertCount(int $count)
|
||||||
|
* @method static void assertTotalQuantity(int|float $quantity)
|
||||||
|
* @method static void assertSubTotal(float $amount)
|
||||||
|
* @method static void assertTotal(float $amount)
|
||||||
|
* @method static void assertConditionApplied(string $conditionName)
|
||||||
|
* @method static void assertEmpty()
|
||||||
|
* @method static void assertNotEmpty()
|
||||||
|
* @method static string getSessionKey()
|
||||||
|
* @method static void setSessionKey(string $sessionKey)
|
||||||
|
* @method static string getInstanceName()
|
||||||
|
* @method static ItemCollection|null get(int|string $itemId)
|
||||||
|
* @method static bool has(int|string $itemId)
|
||||||
|
* @method static Cart add(string|int|array $id, string|null $name = null, float|null $price = null, int|float|null $quantity = null, array $attributes = [], CartCondition|array $conditions = [], string|null $associatedModel = null)
|
||||||
|
* @method static bool removeInvalidItems()
|
||||||
|
* @method static bool update(int|string $id, array $data)
|
||||||
|
* @method static void setConfig(array $config)
|
||||||
|
* @method static Cart addItemCondition(int|string $productId, CartCondition $itemCondition)
|
||||||
|
* @method static bool remove(int|string $id)
|
||||||
|
* @method static bool clear()
|
||||||
|
* @method static bool clearItems()
|
||||||
|
* @method static Cart condition(array|CartCondition $condition)
|
||||||
|
* @method static Cart coupon(Coupon $coupon)
|
||||||
|
* @method static Cart tax(TaxRule $taxRule)
|
||||||
|
* @method static Cart shipping(ShippingRate $shippingRate)
|
||||||
|
* @method static CartConditionCollection|array getConditions(bool $array = false, bool $active = false)
|
||||||
|
* @method static CartCondition|null getCondition(string $conditionName)
|
||||||
|
* @method static CartConditionCollection getConditionsByType(string $type)
|
||||||
|
* @method static void removeConditionsByType(string $type)
|
||||||
|
* @method static void removeCartCondition(string $conditionName)
|
||||||
|
* @method static bool removeItemCondition(int|string $itemId, string $conditionName)
|
||||||
|
* @method static bool clearItemConditions(int|string $itemId)
|
||||||
|
* @method static void clearCartConditions()
|
||||||
|
* @method static void clearAllConditions()
|
||||||
|
* @method static int|float|string getSubTotalWithoutConditions(bool $formatted = true)
|
||||||
|
* @method static int|float|string getSubTotal(bool $formatted = true)
|
||||||
|
* @method static int|float getTotal()
|
||||||
|
* @method static int|float getCalculatedValueForCondition(string $conditionName)
|
||||||
|
* @method static int|float getTotalQuantity()
|
||||||
|
* @method static CartCollection getContent()
|
||||||
|
* @method static bool isEmpty()
|
||||||
|
* @method static Cart associate(mixed $model)
|
||||||
|
*/
|
||||||
class CartFacade extends Facade
|
class CartFacade extends Facade
|
||||||
{
|
{
|
||||||
protected static function getFacadeAccessor()
|
protected static function getFacadeAccessor(): string
|
||||||
{
|
{
|
||||||
return 'cart';
|
return 'cart';
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-5
@@ -5,6 +5,14 @@
|
|||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Wearepixel\Cart\Helpers\Helpers;
|
use Wearepixel\Cart\Helpers\Helpers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property mixed $id
|
||||||
|
* @property string $name
|
||||||
|
* @property float|int $price
|
||||||
|
* @property int|float $quantity
|
||||||
|
* @property array<string, mixed> $attributes
|
||||||
|
* @property mixed $conditions
|
||||||
|
*/
|
||||||
class ItemCollection extends Collection
|
class ItemCollection extends Collection
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@@ -46,9 +54,9 @@ public function __get($name)
|
|||||||
/**
|
/**
|
||||||
* return the associated model of an item
|
* return the associated model of an item
|
||||||
*
|
*
|
||||||
* @return bool
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
protected function getAssociatedModel()
|
protected function getAssociatedModel(): mixed
|
||||||
{
|
{
|
||||||
if (! $this->has('associatedModel')) {
|
if (! $this->has('associatedModel')) {
|
||||||
return null;
|
return null;
|
||||||
@@ -97,7 +105,7 @@ public function getConditions()
|
|||||||
|
|
||||||
// check if we're already an array of CartConditions
|
// check if we're already an array of CartConditions
|
||||||
foreach ($conditions as $key => $condition) {
|
foreach ($conditions as $key => $condition) {
|
||||||
if ($condition instanceof CartCondition) {
|
if (is_object($condition) && $condition instanceof CartCondition) {
|
||||||
return $conditions;
|
return $conditions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,7 +113,7 @@ public function getConditions()
|
|||||||
$hasSubArray = false;
|
$hasSubArray = false;
|
||||||
|
|
||||||
foreach ($conditions as $key => $condition) {
|
foreach ($conditions as $key => $condition) {
|
||||||
if (is_array($condition) && ! $condition instanceof CartCondition) {
|
if (is_array($condition)) {
|
||||||
$hasSubArray = true;
|
$hasSubArray = true;
|
||||||
$conditionsArray[] = new CartCondition($condition);
|
$conditionsArray[] = new CartCondition($condition);
|
||||||
} elseif ($condition instanceof CartCondition) {
|
} elseif ($condition instanceof CartCondition) {
|
||||||
@@ -162,6 +170,31 @@ public function getPriceWithConditions($formatted = true)
|
|||||||
*/
|
*/
|
||||||
public function getPriceSumWithConditions($formatted = true)
|
public function getPriceSumWithConditions($formatted = true)
|
||||||
{
|
{
|
||||||
return Helpers::formatValue($this->getPriceWithConditions(false) * $this->quantity, $formatted, $this->config);
|
$conditions = $this->hasConditions() ? $this->getConditions() : [];
|
||||||
|
$conditions = is_array($conditions) ? $conditions : [$conditions];
|
||||||
|
|
||||||
|
$hasLimit = collect($conditions)->some(fn ($c) => $c->getAppliesToQuantity() !== null);
|
||||||
|
|
||||||
|
if (! $hasLimit) {
|
||||||
|
return Helpers::formatValue($this->getPriceWithConditions(false) * $this->quantity, $formatted, $this->config);
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = 0.0;
|
||||||
|
|
||||||
|
for ($i = 1; $i <= $this->quantity; $i++) {
|
||||||
|
$price = $this->price;
|
||||||
|
|
||||||
|
foreach ($conditions as $condition) {
|
||||||
|
$limit = $condition->getAppliesToQuantity();
|
||||||
|
|
||||||
|
if ($limit === null || $i <= $limit) {
|
||||||
|
$price = $condition->applyCondition($price);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$total += $price;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Helpers::formatValue($total, $formatted, $this->config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Shipping;
|
||||||
|
|
||||||
|
use Wearepixel\Cart\CartCondition;
|
||||||
|
use Wearepixel\Cart\Contracts\CartConditionable;
|
||||||
|
use Wearepixel\Cart\Exceptions\InvalidShippingRateException;
|
||||||
|
|
||||||
|
abstract class ShippingRate implements CartConditionable
|
||||||
|
{
|
||||||
|
protected string $name;
|
||||||
|
|
||||||
|
protected string $value;
|
||||||
|
|
||||||
|
protected string $target = 'total';
|
||||||
|
|
||||||
|
protected ?float $minimum = null;
|
||||||
|
|
||||||
|
protected ?float $maximum = null;
|
||||||
|
|
||||||
|
abstract public function isApplicable(): bool;
|
||||||
|
|
||||||
|
public function toCondition(): CartCondition
|
||||||
|
{
|
||||||
|
if (! $this->isApplicable()) {
|
||||||
|
throw new InvalidShippingRateException("Shipping rate [{$this->name}] is not applicable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'name' => $this->name,
|
||||||
|
'type' => 'shipping',
|
||||||
|
'value' => $this->value,
|
||||||
|
'target' => $this->target,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->minimum !== null) {
|
||||||
|
$data['minimum'] = $this->minimum;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->maximum !== null) {
|
||||||
|
$data['maximum'] = $this->maximum;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CartCondition($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Tax;
|
||||||
|
|
||||||
|
use Wearepixel\Cart\CartCondition;
|
||||||
|
use Wearepixel\Cart\Contracts\CartConditionable;
|
||||||
|
use Wearepixel\Cart\Exceptions\InvalidTaxRuleException;
|
||||||
|
|
||||||
|
abstract class TaxRule implements CartConditionable
|
||||||
|
{
|
||||||
|
protected string $name;
|
||||||
|
|
||||||
|
protected string $value;
|
||||||
|
|
||||||
|
protected string $target = 'subtotal';
|
||||||
|
|
||||||
|
protected ?float $minimum = null;
|
||||||
|
|
||||||
|
protected ?float $maximum = null;
|
||||||
|
|
||||||
|
abstract public function isApplicable(): bool;
|
||||||
|
|
||||||
|
public function toCondition(): CartCondition
|
||||||
|
{
|
||||||
|
if (! $this->isApplicable()) {
|
||||||
|
throw new InvalidTaxRuleException("Tax rule [{$this->name}] is not applicable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'name' => $this->name,
|
||||||
|
'type' => 'tax',
|
||||||
|
'value' => $this->value,
|
||||||
|
'target' => $this->target,
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->minimum !== null) {
|
||||||
|
$data['minimum'] = $this->minimum;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->maximum !== null) {
|
||||||
|
$data['maximum'] = $this->maximum;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CartCondition($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Testing;
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Cart;
|
||||||
|
use Wearepixel\Cart\CartCondition;
|
||||||
|
use Wearepixel\Cart\Coupons\Coupon;
|
||||||
|
|
||||||
|
class CartFactory
|
||||||
|
{
|
||||||
|
public function __construct(private Cart $cart) {}
|
||||||
|
|
||||||
|
public function withItems(int $count): static
|
||||||
|
{
|
||||||
|
$start = $this->cart->getContent()->count() + 1;
|
||||||
|
for ($i = $start; $i < $start + $count; $i++) {
|
||||||
|
$this->cart->add($i, "Test Item {$i}", 10.00, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withCondition(CartCondition $condition): static
|
||||||
|
{
|
||||||
|
$this->cart->condition($condition);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withCoupon(Coupon $coupon): static
|
||||||
|
{
|
||||||
|
$this->cart->coupon($coupon);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,9 @@
|
|||||||
use Illuminate\Translation\FileLoader;
|
use Illuminate\Translation\FileLoader;
|
||||||
use Illuminate\Translation\Translator;
|
use Illuminate\Translation\Translator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @method static \Illuminate\Validation\Validator make(array $data, array $rules, array $messages = [], array $attributes = [])
|
||||||
|
*/
|
||||||
abstract class Validator
|
abstract class Validator
|
||||||
{
|
{
|
||||||
protected static $factory;
|
protected static $factory;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use Wearepixel\Cart\Cart;
|
use Wearepixel\Cart\Cart;
|
||||||
use Wearepixel\Cart\CartCondition;
|
use Wearepixel\Cart\CartCondition;
|
||||||
use Wearepixel\Cart\CartConditionCollection;
|
use Wearepixel\Cart\CartConditionCollection;
|
||||||
|
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||||
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
@@ -10,10 +11,9 @@
|
|||||||
$events->shouldReceive('dispatch');
|
$events->shouldReceive('dispatch');
|
||||||
|
|
||||||
$this->cart = new Cart(
|
$this->cart = new Cart(
|
||||||
new SessionMock,
|
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||||
$events,
|
$events,
|
||||||
'cart',
|
'cart',
|
||||||
'SAMPLESESSIONKEY',
|
|
||||||
require (__DIR__ . '/../Helpers/ConfigConditionsMock.php')
|
require (__DIR__ . '/../Helpers/ConfigConditionsMock.php')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -950,8 +950,8 @@
|
|||||||
|
|
||||||
expect($this->cart->getCalculatedValueForCondition('Coupon Discount'))->toEqual(200.0, 'Coupon Discount should be 200.0');
|
expect($this->cart->getCalculatedValueForCondition('Coupon Discount'))->toEqual(200.0, 'Coupon Discount should be 200.0');
|
||||||
expect($this->cart->getCalculatedValueForCondition('Gift Card'))->toEqual(0, 'Gift Card should be 0');
|
expect($this->cart->getCalculatedValueForCondition('Gift Card'))->toEqual(0, 'Gift Card should be 0');
|
||||||
expect($couponDiscount->getCalculatedValue())->toEqual(200.0, 'Coupon Discount value should be 200.0');
|
expect($this->cart->getCondition('Coupon Discount')->getCalculatedValue(200.0))->toEqual(200.0, 'Coupon Discount value should be 200.0');
|
||||||
expect($giftCard->getCalculatedValue())->toEqual(0, 'Gift Card value should be 0');
|
expect($this->cart->getCondition('Gift Card')->getCalculatedValue(0))->toEqual(0, 'Gift Card value should be 0');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('calculate the subtotal correctly when one condition makes it $0, and the other adds $10', function () {
|
test('calculate the subtotal correctly when one condition makes it $0, and the other adds $10', function () {
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Cart;
|
||||||
|
use Wearepixel\Cart\CartCondition;
|
||||||
|
use Wearepixel\Cart\Coupons\Coupon;
|
||||||
|
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||||
|
use Wearepixel\Cart\Exceptions\InvalidCouponException;
|
||||||
|
use Wearepixel\Cart\Shipping\ShippingRate;
|
||||||
|
use Wearepixel\Cart\Tax\TaxRule;
|
||||||
|
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||||
|
|
||||||
|
function makeCart(): Cart
|
||||||
|
{
|
||||||
|
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||||
|
$events->shouldReceive('dispatch');
|
||||||
|
|
||||||
|
return new Cart(
|
||||||
|
new SessionDriver(new SessionMock, 'TESTKEY'),
|
||||||
|
$events,
|
||||||
|
'cart',
|
||||||
|
require __DIR__ . '/../Helpers/ConfigMock.php',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestCoupon extends Coupon
|
||||||
|
{
|
||||||
|
protected string $code = 'SAVE10';
|
||||||
|
protected string $value = '-10%';
|
||||||
|
public function isValid(): bool { return true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvalidTestCoupon extends Coupon
|
||||||
|
{
|
||||||
|
protected string $code = 'EXPIRED';
|
||||||
|
protected string $value = '-10%';
|
||||||
|
public function isValid(): bool { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestTax extends TaxRule
|
||||||
|
{
|
||||||
|
protected string $name = 'GST';
|
||||||
|
protected string $value = '10%';
|
||||||
|
public function isApplicable(): bool { return true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestShipping extends ShippingRate
|
||||||
|
{
|
||||||
|
protected string $name = 'Flat Rate';
|
||||||
|
protected string $value = '+10';
|
||||||
|
public function isApplicable(): bool { return true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->cart = makeCart();
|
||||||
|
$this->cart->add(1, 'Widget', 100.00, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(fn() => Mockery::close());
|
||||||
|
|
||||||
|
describe('Cart first-class objects', function () {
|
||||||
|
test('Cart::coupon() applies condition from a Coupon instance', function () {
|
||||||
|
$this->cart->coupon(new TestCoupon);
|
||||||
|
|
||||||
|
expect($this->cart->getCondition('SAVE10'))->toBeInstanceOf(CartCondition::class);
|
||||||
|
expect($this->cart->getSubTotal())->toEqual(90.00);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Cart::coupon() throws when coupon is invalid', function () {
|
||||||
|
expect(fn() => $this->cart->coupon(new InvalidTestCoupon))
|
||||||
|
->toThrow(InvalidCouponException::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Cart::tax() applies condition from a TaxRule instance', function () {
|
||||||
|
$this->cart->tax(new TestTax);
|
||||||
|
|
||||||
|
expect($this->cart->getCondition('GST'))->toBeInstanceOf(CartCondition::class);
|
||||||
|
expect($this->cart->getSubTotal())->toEqual(110.00);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Cart::shipping() applies condition from a ShippingRate instance', function () {
|
||||||
|
$this->cart->shipping(new TestShipping);
|
||||||
|
|
||||||
|
expect($this->cart->getCondition('Flat Rate'))->toBeInstanceOf(CartCondition::class);
|
||||||
|
expect($this->cart->getTotal())->toEqual(110.00);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('raw Cart::condition() still works alongside first-class objects', function () {
|
||||||
|
$this->cart->coupon(new TestCoupon);
|
||||||
|
$this->cart->condition(new CartCondition([
|
||||||
|
'name' => 'Manual Discount',
|
||||||
|
'type' => 'discount',
|
||||||
|
'value' => '-5',
|
||||||
|
'target' => 'subtotal',
|
||||||
|
]));
|
||||||
|
|
||||||
|
expect($this->cart->getConditions()->count())->toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Wearepixel\Cart\Cart;
|
use Wearepixel\Cart\Cart;
|
||||||
|
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||||
use Wearepixel\Cart\Tests\Helpers\MockProduct;
|
use Wearepixel\Cart\Tests\Helpers\MockProduct;
|
||||||
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||||
|
|
||||||
@@ -9,10 +10,9 @@
|
|||||||
$events->shouldReceive('dispatch');
|
$events->shouldReceive('dispatch');
|
||||||
|
|
||||||
$this->cart = new Cart(
|
$this->cart = new Cart(
|
||||||
new SessionMock,
|
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||||
$events,
|
$events,
|
||||||
'shopping',
|
'shopping',
|
||||||
'SAMPLESESSIONKEY',
|
|
||||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Wearepixel\Cart\Cart;
|
use Wearepixel\Cart\Cart;
|
||||||
|
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||||
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||||
|
|
||||||
beforeEach(function () {});
|
beforeEach(function () {});
|
||||||
@@ -14,10 +15,9 @@
|
|||||||
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Created', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Created', Mockery::type('array'), true);
|
||||||
|
|
||||||
$cart = new Cart(
|
$cart = new Cart(
|
||||||
new SessionMock,
|
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||||
$events,
|
$events,
|
||||||
'cart',
|
'cart',
|
||||||
'SAMPLESESSIONKEY',
|
|
||||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -31,10 +31,9 @@
|
|||||||
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Added', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Added', Mockery::type('array'), true);
|
||||||
|
|
||||||
$cart = new Cart(
|
$cart = new Cart(
|
||||||
new SessionMock,
|
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||||
$events,
|
$events,
|
||||||
'cart',
|
'cart',
|
||||||
'SAMPLESESSIONKEY',
|
|
||||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -50,10 +49,9 @@
|
|||||||
$events->shouldReceive('dispatch')->times(2)->with('LaravelCart.Added', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->times(2)->with('LaravelCart.Added', Mockery::type('array'), true);
|
||||||
|
|
||||||
$cart = new Cart(
|
$cart = new Cart(
|
||||||
new SessionMock,
|
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||||
$events,
|
$events,
|
||||||
'cart',
|
'cart',
|
||||||
'SAMPLESESSIONKEY',
|
|
||||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -94,10 +92,9 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
$cart = new Cart(
|
$cart = new Cart(
|
||||||
new SessionMock,
|
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||||
$events,
|
$events,
|
||||||
'cart',
|
'cart',
|
||||||
'SAMPLESESSIONKEY',
|
|
||||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -139,10 +136,9 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
$cart = new Cart(
|
$cart = new Cart(
|
||||||
new SessionMock,
|
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||||
$events,
|
$events,
|
||||||
'cart',
|
'cart',
|
||||||
'SAMPLESESSIONKEY',
|
|
||||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -186,10 +182,9 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
$cart = new Cart(
|
$cart = new Cart(
|
||||||
new SessionMock,
|
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||||
$events,
|
$events,
|
||||||
'cart',
|
'cart',
|
||||||
'SAMPLESESSIONKEY',
|
|
||||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Wearepixel\Cart\Cart;
|
use Wearepixel\Cart\Cart;
|
||||||
|
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||||
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
@@ -8,18 +9,16 @@
|
|||||||
$events->shouldReceive('dispatch');
|
$events->shouldReceive('dispatch');
|
||||||
|
|
||||||
$this->cart1 = new Cart(
|
$this->cart1 = new Cart(
|
||||||
new SessionMock,
|
new SessionDriver(new SessionMock, 'uniquesessionkey123'),
|
||||||
$events,
|
$events,
|
||||||
'shopping',
|
'shopping',
|
||||||
'uniquesessionkey123',
|
|
||||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->cart2 = new Cart(
|
$this->cart2 = new Cart(
|
||||||
new SessionMock,
|
new SessionDriver(new SessionMock, 'uniquesessionkey456'),
|
||||||
$events,
|
$events,
|
||||||
'wishlist',
|
'wishlist',
|
||||||
'uniquesessionkey456',
|
|
||||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Cart;
|
||||||
|
use Wearepixel\Cart\Commands\DebugCommand;
|
||||||
|
use Wearepixel\Cart\Drivers\NullDriver;
|
||||||
|
|
||||||
|
function makeDebugCart(): Cart
|
||||||
|
{
|
||||||
|
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||||
|
$events->shouldReceive('dispatch');
|
||||||
|
|
||||||
|
return new Cart(
|
||||||
|
new NullDriver('debug-session'),
|
||||||
|
$events,
|
||||||
|
'cart',
|
||||||
|
require __DIR__ . '/../../Helpers/ConfigMock.php',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(fn() => Mockery::close());
|
||||||
|
|
||||||
|
describe('DebugCommand::debugData()', function () {
|
||||||
|
test('returns driver name', function () {
|
||||||
|
$cart = makeDebugCart();
|
||||||
|
$data = DebugCommand::debugData($cart);
|
||||||
|
expect($data['driver'])->toBe('null');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns item count', function () {
|
||||||
|
$cart = makeDebugCart();
|
||||||
|
$cart->add(1, 'Widget', 10.00, 1);
|
||||||
|
$data = DebugCommand::debugData($cart);
|
||||||
|
expect($data['item_count'])->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns total quantity', function () {
|
||||||
|
$cart = makeDebugCart();
|
||||||
|
$cart->add(1, 'Widget', 10.00, 3);
|
||||||
|
$data = DebugCommand::debugData($cart);
|
||||||
|
expect($data['total_quantity'])->toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns subtotal', function () {
|
||||||
|
$cart = makeDebugCart();
|
||||||
|
$cart->add(1, 'Widget', 25.00, 2);
|
||||||
|
$data = DebugCommand::debugData($cart);
|
||||||
|
expect($data['subtotal'])->toBe(50.00);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns total', function () {
|
||||||
|
$cart = makeDebugCart();
|
||||||
|
$cart->add(1, 'Widget', 25.00, 2);
|
||||||
|
$data = DebugCommand::debugData($cart);
|
||||||
|
expect($data['total'])->toBe(50.00);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns empty items array when cart is empty', function () {
|
||||||
|
$cart = makeDebugCart();
|
||||||
|
$data = DebugCommand::debugData($cart);
|
||||||
|
expect($data['items'])->toBeArray()->toBeEmpty();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns condition names', function () {
|
||||||
|
$cart = makeDebugCart();
|
||||||
|
$cart->condition(new \Wearepixel\Cart\CartCondition([
|
||||||
|
'name' => 'GST', 'type' => 'tax', 'value' => '10%', 'target' => 'subtotal',
|
||||||
|
]));
|
||||||
|
$data = DebugCommand::debugData($cart);
|
||||||
|
expect($data['conditions'])->toContain('GST');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Commands\InstallCommand;
|
||||||
|
|
||||||
|
describe('InstallCommand', function () {
|
||||||
|
test('scaffoldDirectories() creates the expected subdirectories', function () {
|
||||||
|
$base = sys_get_temp_dir() . '/cart-install-' . uniqid();
|
||||||
|
|
||||||
|
InstallCommand::scaffoldDirectories($base);
|
||||||
|
|
||||||
|
expect(is_dir("{$base}/Coupons"))->toBeTrue();
|
||||||
|
expect(is_dir("{$base}/Tax"))->toBeTrue();
|
||||||
|
expect(is_dir("{$base}/Shipping"))->toBeTrue();
|
||||||
|
expect(is_dir("{$base}/Drivers"))->toBeTrue();
|
||||||
|
|
||||||
|
foreach (["{$base}/Coupons", "{$base}/Tax", "{$base}/Shipping", "{$base}/Drivers", $base] as $dir) {
|
||||||
|
@rmdir($dir);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('scaffoldDirectories() is idempotent - does not throw if dirs already exist', function () {
|
||||||
|
$base = sys_get_temp_dir() . '/cart-install-idem-' . uniqid();
|
||||||
|
|
||||||
|
InstallCommand::scaffoldDirectories($base);
|
||||||
|
InstallCommand::scaffoldDirectories($base);
|
||||||
|
|
||||||
|
expect(is_dir("{$base}/Coupons"))->toBeTrue();
|
||||||
|
|
||||||
|
foreach (["{$base}/Coupons", "{$base}/Tax", "{$base}/Shipping", "{$base}/Drivers", $base] as $dir) {
|
||||||
|
@rmdir($dir);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Commands\MakeCouponCommand;
|
||||||
|
|
||||||
|
describe('MakeCouponCommand', function () {
|
||||||
|
test('stub contains the given class name', function () {
|
||||||
|
$stub = MakeCouponCommand::stub('TenPercentCoupon');
|
||||||
|
expect($stub)->toContain('class TenPercentCoupon extends Coupon');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stub uses the App\\Cart\\Coupons namespace', function () {
|
||||||
|
$stub = MakeCouponCommand::stub('TenPercentCoupon');
|
||||||
|
expect($stub)->toContain('namespace App\\Cart\\Coupons');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stub imports the Coupon base class', function () {
|
||||||
|
$stub = MakeCouponCommand::stub('TenPercentCoupon');
|
||||||
|
expect($stub)->toContain('use Wearepixel\\Cart\\Coupons\\Coupon');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stub declares isValid() method', function () {
|
||||||
|
$stub = MakeCouponCommand::stub('TenPercentCoupon');
|
||||||
|
expect($stub)->toContain('public function isValid(): bool');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stub uses strict_types', function () {
|
||||||
|
$stub = MakeCouponCommand::stub('TenPercentCoupon');
|
||||||
|
expect($stub)->toContain('declare(strict_types=1)');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Commands\MakeDriverCommand;
|
||||||
|
|
||||||
|
describe('MakeDriverCommand', function () {
|
||||||
|
test('stub contains the given class name', function () {
|
||||||
|
$stub = MakeDriverCommand::stub('RedisDriver');
|
||||||
|
expect($stub)->toContain('class RedisDriver implements CartDriver');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stub uses the App\\Cart\\Drivers namespace', function () {
|
||||||
|
$stub = MakeDriverCommand::stub('RedisDriver');
|
||||||
|
expect($stub)->toContain('namespace App\\Cart\\Drivers');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stub imports the CartDriver interface', function () {
|
||||||
|
$stub = MakeDriverCommand::stub('RedisDriver');
|
||||||
|
expect($stub)->toContain('use Wearepixel\\Cart\\Drivers\\Contracts\\CartDriver');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stub declares all CartDriver interface methods', function () {
|
||||||
|
$stub = MakeDriverCommand::stub('RedisDriver');
|
||||||
|
expect($stub)->toContain('public function getSessionKey(): string');
|
||||||
|
expect($stub)->toContain('public function setSessionKey(string $sessionKey): void');
|
||||||
|
expect($stub)->toContain('public function getDriverName(): string');
|
||||||
|
expect($stub)->toContain('public function getItems(): array');
|
||||||
|
expect($stub)->toContain('public function putItems(array $items): void');
|
||||||
|
expect($stub)->toContain('public function getConditions(): array');
|
||||||
|
expect($stub)->toContain('public function putConditions(array $conditions): void');
|
||||||
|
expect($stub)->toContain('public function clearItems(): void');
|
||||||
|
expect($stub)->toContain('public function flush(): void');
|
||||||
|
expect($stub)->toContain('public function getSessionModel(): mixed');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Commands\MakeShippingCommand;
|
||||||
|
|
||||||
|
describe('MakeShippingCommand', function () {
|
||||||
|
test('stub contains the given class name', function () {
|
||||||
|
$stub = MakeShippingCommand::stub('FlatRateShipping');
|
||||||
|
expect($stub)->toContain('class FlatRateShipping extends ShippingRate');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stub uses the App\\Cart\\Shipping namespace', function () {
|
||||||
|
$stub = MakeShippingCommand::stub('FlatRateShipping');
|
||||||
|
expect($stub)->toContain('namespace App\\Cart\\Shipping');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stub imports the ShippingRate base class', function () {
|
||||||
|
$stub = MakeShippingCommand::stub('FlatRateShipping');
|
||||||
|
expect($stub)->toContain('use Wearepixel\\Cart\\Shipping\\ShippingRate');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stub declares isApplicable() method', function () {
|
||||||
|
$stub = MakeShippingCommand::stub('FlatRateShipping');
|
||||||
|
expect($stub)->toContain('public function isApplicable(): bool');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stub defaults target to total', function () {
|
||||||
|
$stub = MakeShippingCommand::stub('FlatRateShipping');
|
||||||
|
expect($stub)->toContain("'total'");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Commands\MakeTaxCommand;
|
||||||
|
|
||||||
|
describe('MakeTaxCommand', function () {
|
||||||
|
test('stub contains the given class name', function () {
|
||||||
|
$stub = MakeTaxCommand::stub('GstTaxRule');
|
||||||
|
expect($stub)->toContain('class GstTaxRule extends TaxRule');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stub uses the App\\Cart\\Tax namespace', function () {
|
||||||
|
$stub = MakeTaxCommand::stub('GstTaxRule');
|
||||||
|
expect($stub)->toContain('namespace App\\Cart\\Tax');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stub imports the TaxRule base class', function () {
|
||||||
|
$stub = MakeTaxCommand::stub('GstTaxRule');
|
||||||
|
expect($stub)->toContain('use Wearepixel\\Cart\\Tax\\TaxRule');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stub declares isApplicable() method', function () {
|
||||||
|
$stub = MakeTaxCommand::stub('GstTaxRule');
|
||||||
|
expect($stub)->toContain('public function isApplicable(): bool');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stub uses strict_types', function () {
|
||||||
|
$stub = MakeTaxCommand::stub('GstTaxRule');
|
||||||
|
expect($stub)->toContain('declare(strict_types=1)');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Cart;
|
||||||
|
use Wearepixel\Cart\Concerns\HasCart;
|
||||||
|
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||||
|
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||||
|
|
||||||
|
class FakeComponent
|
||||||
|
{
|
||||||
|
use HasCart;
|
||||||
|
|
||||||
|
public array $cartItems = [];
|
||||||
|
public float $cartTotal = 0.0;
|
||||||
|
public float $cartSubTotal = 0.0;
|
||||||
|
public int $cartCount = 0;
|
||||||
|
|
||||||
|
public function __construct(private Cart $cart) {}
|
||||||
|
|
||||||
|
protected function getCart(): Cart
|
||||||
|
{
|
||||||
|
return $this->cart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeComponent(): FakeComponent
|
||||||
|
{
|
||||||
|
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||||
|
$events->shouldReceive('dispatch');
|
||||||
|
|
||||||
|
$cart = new Cart(
|
||||||
|
new SessionDriver(new SessionMock, 'TESTKEY'),
|
||||||
|
$events,
|
||||||
|
'cart',
|
||||||
|
require __DIR__ . '/../../Helpers/ConfigMock.php',
|
||||||
|
);
|
||||||
|
|
||||||
|
return new FakeComponent($cart);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->component = makeComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(fn() => Mockery::close());
|
||||||
|
|
||||||
|
describe('HasCart trait', function () {
|
||||||
|
test('refreshCart() populates cartItems', function () {
|
||||||
|
$this->component->cartAdd(1, 'Widget', 50.00, 2);
|
||||||
|
|
||||||
|
expect($this->component->cartItems)->not->toBeEmpty();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cartAdd() adds an item and refreshes state', function () {
|
||||||
|
$this->component->cartAdd(1, 'Widget', 50.00, 1);
|
||||||
|
|
||||||
|
expect($this->component->cartCount)->toBe(1);
|
||||||
|
expect($this->component->cartSubTotal)->toBe(50.00);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cartRemove() removes an item and refreshes state', function () {
|
||||||
|
$this->component->cartAdd(1, 'Widget', 50.00, 1);
|
||||||
|
$this->component->cartRemove(1);
|
||||||
|
|
||||||
|
expect($this->component->cartCount)->toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cartUpdate() updates an item and refreshes state', function () {
|
||||||
|
$this->component->cartAdd(1, 'Widget', 50.00, 1);
|
||||||
|
$this->component->cartUpdate(1, ['quantity' => ['relative' => false, 'value' => 3]]);
|
||||||
|
|
||||||
|
expect($this->component->cartCount)->toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cartClear() empties the cart and refreshes state', function () {
|
||||||
|
$this->component->cartAdd(1, 'Widget', 50.00, 1);
|
||||||
|
$this->component->cartClear();
|
||||||
|
|
||||||
|
expect($this->component->cartCount)->toBe(0);
|
||||||
|
expect($this->component->cartItems)->toBeEmpty();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cartTotal reflects the cart total', function () {
|
||||||
|
$this->component->cartAdd(1, 'Widget', 100.00, 1);
|
||||||
|
|
||||||
|
expect($this->component->cartTotal)->toBe(100.00);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Cart;
|
||||||
|
use Wearepixel\Cart\Concerns\ShareCartWithInertia;
|
||||||
|
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||||
|
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||||
|
|
||||||
|
function makeCartForInertia(): Cart
|
||||||
|
{
|
||||||
|
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||||
|
$events->shouldReceive('dispatch');
|
||||||
|
|
||||||
|
return new Cart(
|
||||||
|
new SessionDriver(new SessionMock, 'TESTKEY'),
|
||||||
|
$events,
|
||||||
|
'cart',
|
||||||
|
require __DIR__ . '/../../Helpers/ConfigMock.php',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->cart = makeCartForInertia();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(fn() => Mockery::close());
|
||||||
|
|
||||||
|
describe('ShareCartWithInertia', function () {
|
||||||
|
test('data() returns items, total, subtotal, and count', function () {
|
||||||
|
$this->cart->add(1, 'Widget', 50.00, 2);
|
||||||
|
|
||||||
|
$data = ShareCartWithInertia::data($this->cart);
|
||||||
|
|
||||||
|
expect($data)->toHaveKeys(['items', 'total', 'subtotal', 'count']);
|
||||||
|
expect($data['count'])->toBe(2);
|
||||||
|
expect($data['subtotal'])->toBe(100.00);
|
||||||
|
expect($data['total'])->toBe(100.00);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('data() returns empty state when cart is empty', function () {
|
||||||
|
$data = ShareCartWithInertia::data($this->cart);
|
||||||
|
|
||||||
|
expect($data['items'])->toBeEmpty();
|
||||||
|
expect($data['count'])->toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('data() items are plain arrays suitable for JSON serialization', function () {
|
||||||
|
$this->cart->add(1, 'Widget', 50.00, 1);
|
||||||
|
|
||||||
|
$data = ShareCartWithInertia::data($this->cart);
|
||||||
|
|
||||||
|
expect($data['items'])->toBeArray();
|
||||||
|
expect(json_encode($data))->toBeString();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Wearepixel\Cart\CartCondition;
|
||||||
|
use Wearepixel\Cart\Coupons\Coupon;
|
||||||
|
use Wearepixel\Cart\Exceptions\InvalidCouponException;
|
||||||
|
|
||||||
|
class AlwaysValidCoupon extends Coupon
|
||||||
|
{
|
||||||
|
protected string $code = 'SAVE10';
|
||||||
|
protected string $value = '-10%';
|
||||||
|
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NeverValidCoupon extends Coupon
|
||||||
|
{
|
||||||
|
protected string $code = 'EXPIRED';
|
||||||
|
protected string $value = '-10%';
|
||||||
|
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Coupon', function () {
|
||||||
|
test('converts to a CartCondition', function () {
|
||||||
|
$condition = (new AlwaysValidCoupon)->toCondition();
|
||||||
|
|
||||||
|
expect($condition)->toBeInstanceOf(CartCondition::class);
|
||||||
|
expect($condition->getName())->toBe('SAVE10');
|
||||||
|
expect($condition->getValue())->toBe('-10%');
|
||||||
|
expect($condition->getType())->toBe('coupon');
|
||||||
|
expect($condition->getTarget())->toBe('subtotal');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('respects custom target', function () {
|
||||||
|
$coupon = new class extends Coupon {
|
||||||
|
protected string $code = 'TOTAL10';
|
||||||
|
protected string $value = '-10%';
|
||||||
|
protected string $target = 'total';
|
||||||
|
public function isValid(): bool { return true; }
|
||||||
|
};
|
||||||
|
|
||||||
|
expect($coupon->toCondition()->getTarget())->toBe('total');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('includes minimum in condition when set', function () {
|
||||||
|
$coupon = new class extends Coupon {
|
||||||
|
protected string $code = 'BIG10';
|
||||||
|
protected string $value = '-10%';
|
||||||
|
protected ?float $minimum = 100.0;
|
||||||
|
public function isValid(): bool { return true; }
|
||||||
|
};
|
||||||
|
|
||||||
|
expect($coupon->toCondition()->getMinimum())->toBe(100.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isValid() returning false causes toCondition() to throw', function () {
|
||||||
|
expect(fn() => (new NeverValidCoupon)->toCondition())
|
||||||
|
->toThrow(InvalidCouponException::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getCode returns the coupon code', function () {
|
||||||
|
expect((new AlwaysValidCoupon)->getCode())->toBe('SAVE10');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,19 +2,23 @@
|
|||||||
|
|
||||||
use Wearepixel\Cart\Cart;
|
use Wearepixel\Cart\Cart;
|
||||||
use Wearepixel\Cart\CartCondition;
|
use Wearepixel\Cart\CartCondition;
|
||||||
|
use Wearepixel\Cart\Drivers\DatabaseDriver;
|
||||||
use Wearepixel\Cart\Tests\Helpers\MockCartModel;
|
use Wearepixel\Cart\Tests\Helpers\MockCartModel;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||||
$events->shouldReceive('dispatch');
|
$events->shouldReceive('dispatch');
|
||||||
|
|
||||||
$storage = new MockCartModel;
|
|
||||||
|
|
||||||
$this->cart = new Cart(
|
$this->cart = new Cart(
|
||||||
$storage,
|
new DatabaseDriver(
|
||||||
|
MockCartModel::class,
|
||||||
|
'session_id',
|
||||||
|
'items',
|
||||||
|
'conditions',
|
||||||
|
'SAMPLESESSIONKEY',
|
||||||
|
),
|
||||||
$events,
|
$events,
|
||||||
'cart',
|
'cart',
|
||||||
'SAMPLESESSIONKEY',
|
|
||||||
require (__DIR__ . '/../Helpers/ConfigDatabaseMock.php')
|
require (__DIR__ . '/../Helpers/ConfigDatabaseMock.php')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -108,6 +112,38 @@
|
|||||||
expect($this->cart->getTotal())->toEqual(18.45, 'Cart should have total of 18.45');
|
expect($this->cart->getTotal())->toEqual(18.45, 'Cart should have total of 18.45');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('cart-level conditions survive database round-trip', function () {
|
||||||
|
$this->cart->condition(new CartCondition([
|
||||||
|
'name' => 'GST',
|
||||||
|
'type' => 'tax',
|
||||||
|
'value' => '10%',
|
||||||
|
'target' => 'subtotal',
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Create a fresh cart instance pointing at the same DB row
|
||||||
|
$events2 = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||||
|
$events2->shouldReceive('dispatch');
|
||||||
|
|
||||||
|
$cart2 = new \Wearepixel\Cart\Cart(
|
||||||
|
new \Wearepixel\Cart\Drivers\DatabaseDriver(
|
||||||
|
\Wearepixel\Cart\Tests\Helpers\MockCartModel::class,
|
||||||
|
'session_id',
|
||||||
|
'items',
|
||||||
|
'conditions',
|
||||||
|
'SAMPLESESSIONKEY',
|
||||||
|
),
|
||||||
|
$events2,
|
||||||
|
'cart',
|
||||||
|
require (__DIR__ . '/../Helpers/ConfigDatabaseMock.php')
|
||||||
|
);
|
||||||
|
|
||||||
|
$condition = $cart2->getCondition('GST');
|
||||||
|
|
||||||
|
expect($condition)->not->toBeNull();
|
||||||
|
expect($condition->getName())->toBe('GST');
|
||||||
|
expect($condition->getValue())->toBe('10%');
|
||||||
|
});
|
||||||
|
|
||||||
test('can clear a simple cart', function () {
|
test('can clear a simple cart', function () {
|
||||||
$this->cart->add(1, 'Sample Item', 100.99, 2, []);
|
$this->cart->add(1, 'Sample Item', 100.99, 2, []);
|
||||||
|
|
||||||
@@ -123,4 +159,23 @@
|
|||||||
expect($this->cart->getContent()->count())->toEqual(0, 'Cart content should be 0');
|
expect($this->cart->getContent()->count())->toEqual(0, 'Cart content should be 0');
|
||||||
expect(MockCartModel::all()->count())->toEqual(0, 'The carts database table should have 0 rows');
|
expect(MockCartModel::all()->count())->toEqual(0, 'The carts database table should have 0 rows');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('can clear cart conditions after clearing the cart', function () {
|
||||||
|
$condition = new CartCondition([
|
||||||
|
'name' => '10% Off',
|
||||||
|
'type' => 'coupon',
|
||||||
|
'target' => 'subtotal',
|
||||||
|
'value' => '-10%',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->add(1, 'Sample Item', 100.99, 2, []);
|
||||||
|
$this->cart->condition($condition);
|
||||||
|
|
||||||
|
$this->cart->clear();
|
||||||
|
|
||||||
|
// This was throwing "Call to a member function save() on array"
|
||||||
|
// because clear() replaced $this->session with a plain array
|
||||||
|
expect(fn () => $this->cart->clearCartConditions())->not->toThrow(Error::class);
|
||||||
|
expect($this->cart->getConditions())->toBeEmpty();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Drivers\DatabaseDriver;
|
||||||
|
use Wearepixel\Cart\Tests\Helpers\MockCartModel;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->driver = new DatabaseDriver(
|
||||||
|
MockCartModel::class,
|
||||||
|
'session_id',
|
||||||
|
'items',
|
||||||
|
'conditions',
|
||||||
|
'test-session',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DatabaseDriver', function () {
|
||||||
|
test('returns its session key', function () {
|
||||||
|
expect($this->driver->getSessionKey())->toBe('test-session');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns database as driver name', function () {
|
||||||
|
expect($this->driver->getDriverName())->toBe('database');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('starts with empty items', function () {
|
||||||
|
expect($this->driver->getItems())->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stores and retrieves items', function () {
|
||||||
|
$items = [['id' => 1, 'name' => 'Widget']];
|
||||||
|
$this->driver->putItems($items);
|
||||||
|
|
||||||
|
$fresh = new DatabaseDriver(MockCartModel::class, 'session_id', 'items', 'conditions', 'test-session');
|
||||||
|
expect($fresh->getItems())->toBe($items);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('starts with empty conditions', function () {
|
||||||
|
expect($this->driver->getConditions())->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stores and retrieves conditions', function () {
|
||||||
|
$conditions = [['name' => 'GST', 'type' => 'tax', 'value' => '10%']];
|
||||||
|
$this->driver->putConditions($conditions);
|
||||||
|
|
||||||
|
$fresh = new DatabaseDriver(MockCartModel::class, 'session_id', 'items', 'conditions', 'test-session');
|
||||||
|
expect($fresh->getConditions())->toBe($conditions);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('flush deletes the database record', function () {
|
||||||
|
$this->driver->putItems([['id' => 1]]);
|
||||||
|
$this->driver->flush();
|
||||||
|
|
||||||
|
expect(MockCartModel::count())->toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clearItems empties items but keeps conditions', function () {
|
||||||
|
$this->driver->putItems([['id' => 1]]);
|
||||||
|
$this->driver->putConditions([['name' => 'GST']]);
|
||||||
|
$this->driver->clearItems();
|
||||||
|
|
||||||
|
$fresh = new DatabaseDriver(MockCartModel::class, 'session_id', 'items', 'conditions', 'test-session');
|
||||||
|
expect($fresh->getItems())->toBe([]);
|
||||||
|
expect($fresh->getConditions())->toBe([['name' => 'GST']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getSessionModel returns the Eloquent model', function () {
|
||||||
|
expect($this->driver->getSessionModel())->toBeInstanceOf(MockCartModel::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setSessionKey updates the session key on the model', function () {
|
||||||
|
$this->driver->putItems([['id' => 1]]);
|
||||||
|
$this->driver->setSessionKey('new-session');
|
||||||
|
|
||||||
|
expect($this->driver->getSessionKey())->toBe('new-session');
|
||||||
|
expect($this->driver->getItems())->toBe([['id' => 1]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Drivers\MultiDriver;
|
||||||
|
use Wearepixel\Cart\Drivers\NullDriver;
|
||||||
|
|
||||||
|
describe('MultiDriver', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->primary = new NullDriver('test-session');
|
||||||
|
$this->secondary = new NullDriver('test-session');
|
||||||
|
$this->driver = new MultiDriver([$this->primary, $this->secondary]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns session key from primary driver', function () {
|
||||||
|
expect($this->driver->getSessionKey())->toBe('test-session');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns multi as driver name', function () {
|
||||||
|
expect($this->driver->getDriverName())->toBe('multi');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reads items from primary driver', function () {
|
||||||
|
$this->primary->putItems([['id' => 1]]);
|
||||||
|
$this->secondary->putItems([['id' => 2]]);
|
||||||
|
|
||||||
|
expect($this->driver->getItems())->toBe([['id' => 1]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('writes items to all drivers', function () {
|
||||||
|
$this->driver->putItems([['id' => 1]]);
|
||||||
|
|
||||||
|
expect($this->primary->getItems())->toBe([['id' => 1]]);
|
||||||
|
expect($this->secondary->getItems())->toBe([['id' => 1]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reads conditions from primary driver', function () {
|
||||||
|
$this->primary->putConditions([['name' => 'GST']]);
|
||||||
|
$this->secondary->putConditions([['name' => 'Other']]);
|
||||||
|
|
||||||
|
expect($this->driver->getConditions())->toBe([['name' => 'GST']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('writes conditions to all drivers', function () {
|
||||||
|
$this->driver->putConditions([['name' => 'GST']]);
|
||||||
|
|
||||||
|
expect($this->primary->getConditions())->toBe([['name' => 'GST']]);
|
||||||
|
expect($this->secondary->getConditions())->toBe([['name' => 'GST']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clearItems clears items on all drivers', function () {
|
||||||
|
$this->driver->putItems([['id' => 1]]);
|
||||||
|
$this->driver->clearItems();
|
||||||
|
|
||||||
|
expect($this->primary->getItems())->toBe([]);
|
||||||
|
expect($this->secondary->getItems())->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('flush flushes all drivers', function () {
|
||||||
|
$this->driver->putItems([['id' => 1]]);
|
||||||
|
$this->driver->putConditions([['name' => 'GST']]);
|
||||||
|
$this->driver->flush();
|
||||||
|
|
||||||
|
expect($this->primary->getItems())->toBe([]);
|
||||||
|
expect($this->primary->getConditions())->toBe([]);
|
||||||
|
expect($this->secondary->getItems())->toBe([]);
|
||||||
|
expect($this->secondary->getConditions())->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setSessionKey updates all drivers', function () {
|
||||||
|
$this->driver->setSessionKey('new-key');
|
||||||
|
|
||||||
|
expect($this->primary->getSessionKey())->toBe('new-key');
|
||||||
|
expect($this->secondary->getSessionKey())->toBe('new-key');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws when constructed with no drivers', function () {
|
||||||
|
expect(fn() => new MultiDriver([]))->toThrow(InvalidArgumentException::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getSessionModel delegates to primary driver', function () {
|
||||||
|
expect($this->driver->getSessionModel())->toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Drivers\NullDriver;
|
||||||
|
|
||||||
|
describe('NullDriver', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->driver = new NullDriver('test-session');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns its session key', function () {
|
||||||
|
expect($this->driver->getSessionKey())->toBe('test-session');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns \'null\' as driver name', function () {
|
||||||
|
expect($this->driver->getDriverName())->toBe('null');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('starts with empty items', function () {
|
||||||
|
expect($this->driver->getItems())->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stores and retrieves items', function () {
|
||||||
|
$this->driver->putItems([['id' => 1, 'name' => 'Widget']]);
|
||||||
|
expect($this->driver->getItems())->toBe([['id' => 1, 'name' => 'Widget']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('starts with empty conditions', function () {
|
||||||
|
expect($this->driver->getConditions())->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stores and retrieves conditions', function () {
|
||||||
|
$this->driver->putConditions([['name' => 'GST', 'type' => 'tax', 'value' => '10%']]);
|
||||||
|
expect($this->driver->getConditions())->toBe([['name' => 'GST', 'type' => 'tax', 'value' => '10%']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clearItems empties items but keeps conditions', function () {
|
||||||
|
$this->driver->putItems([['id' => 1]]);
|
||||||
|
$this->driver->putConditions([['name' => 'GST']]);
|
||||||
|
$this->driver->clearItems();
|
||||||
|
|
||||||
|
expect($this->driver->getItems())->toBe([]);
|
||||||
|
expect($this->driver->getConditions())->toBe([['name' => 'GST']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('flush empties items and conditions', function () {
|
||||||
|
$this->driver->putItems([['id' => 1]]);
|
||||||
|
$this->driver->putConditions([['name' => 'GST']]);
|
||||||
|
$this->driver->flush();
|
||||||
|
|
||||||
|
expect($this->driver->getItems())->toBe([]);
|
||||||
|
expect($this->driver->getConditions())->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setSessionKey updates the key', function () {
|
||||||
|
$this->driver->setSessionKey('new-key');
|
||||||
|
expect($this->driver->getSessionKey())->toBe('new-key');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getSessionModel returns null', function () {
|
||||||
|
expect($this->driver->getSessionModel())->toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||||
|
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||||
|
|
||||||
|
describe('SessionDriver', function () {
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->session = new SessionMock;
|
||||||
|
$this->driver = new SessionDriver($this->session, 'test-session');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns its session key', function () {
|
||||||
|
expect($this->driver->getSessionKey())->toBe('test-session');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns session as driver name', function () {
|
||||||
|
expect($this->driver->getDriverName())->toBe('session');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('starts with empty items', function () {
|
||||||
|
expect($this->driver->getItems())->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stores and retrieves items', function () {
|
||||||
|
$items = [['id' => 1, 'name' => 'Widget', 'price' => 10.0]];
|
||||||
|
$this->driver->putItems($items);
|
||||||
|
expect($this->driver->getItems())->toBe($items);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('starts with empty conditions', function () {
|
||||||
|
expect($this->driver->getConditions())->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stores and retrieves conditions', function () {
|
||||||
|
$conditions = [['name' => 'GST', 'type' => 'tax', 'value' => '10%']];
|
||||||
|
$this->driver->putConditions($conditions);
|
||||||
|
expect($this->driver->getConditions())->toBe($conditions);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clearItems removes items from session', function () {
|
||||||
|
$this->driver->putItems([['id' => 1]]);
|
||||||
|
$this->driver->putConditions([['name' => 'GST']]);
|
||||||
|
$this->driver->clearItems();
|
||||||
|
|
||||||
|
expect($this->driver->getItems())->toBe([]);
|
||||||
|
expect($this->driver->getConditions())->toBe([['name' => 'GST']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('flush removes items and conditions from session', function () {
|
||||||
|
$this->driver->putItems([['id' => 1]]);
|
||||||
|
$this->driver->putConditions([['name' => 'GST']]);
|
||||||
|
$this->driver->flush();
|
||||||
|
|
||||||
|
expect($this->driver->getItems())->toBe([]);
|
||||||
|
expect($this->driver->getConditions())->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setSessionKey migrates data to new key', function () {
|
||||||
|
$this->driver->putItems([['id' => 1]]);
|
||||||
|
$this->driver->putConditions([['name' => 'GST']]);
|
||||||
|
$this->driver->setSessionKey('new-key');
|
||||||
|
|
||||||
|
expect($this->driver->getSessionKey())->toBe('new-key');
|
||||||
|
expect($this->driver->getItems())->toBe([['id' => 1]]);
|
||||||
|
expect($this->driver->getConditions())->toBe([['name' => 'GST']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getSessionModel returns null', function () {
|
||||||
|
expect($this->driver->getSessionModel())->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepts explicit session and key, using them directly', function () {
|
||||||
|
$session = new SessionMock;
|
||||||
|
$driver = new SessionDriver($session, 'explicit-key');
|
||||||
|
|
||||||
|
$driver->putItems([['id' => 1]]);
|
||||||
|
|
||||||
|
expect($driver->getSessionKey())->toBe('explicit-key');
|
||||||
|
expect($driver->getItems())->toBe([['id' => 1]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setSessionKey on empty cart does not write null to session', function () {
|
||||||
|
$this->driver->setSessionKey('new-key');
|
||||||
|
|
||||||
|
// After key change, empty cart should still return empty arrays, not null
|
||||||
|
expect($this->driver->getItems())->toBe([]);
|
||||||
|
expect($this->driver->getConditions())->toBe([]);
|
||||||
|
// And the session should not have null values written
|
||||||
|
expect($this->session->get('new-key_cart_items'))->toBeNull();
|
||||||
|
expect($this->session->get('new-key_cart_conditions'))->toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use Wearepixel\Cart\Cart;
|
use Wearepixel\Cart\Cart;
|
||||||
use Wearepixel\Cart\CartCondition;
|
use Wearepixel\Cart\CartCondition;
|
||||||
|
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||||
use Wearepixel\Cart\Tests\Helpers\MockProduct;
|
use Wearepixel\Cart\Tests\Helpers\MockProduct;
|
||||||
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||||
|
|
||||||
@@ -10,10 +11,9 @@
|
|||||||
$events->shouldReceive('dispatch');
|
$events->shouldReceive('dispatch');
|
||||||
|
|
||||||
$this->cart = new Cart(
|
$this->cart = new Cart(
|
||||||
new SessionMock,
|
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||||
$events,
|
$events,
|
||||||
'shopping',
|
'shopping',
|
||||||
'SAMPLESESSIONKEY',
|
|
||||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Wearepixel\Cart\Cart;
|
use Wearepixel\Cart\Cart;
|
||||||
|
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||||
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
@@ -8,10 +9,9 @@
|
|||||||
$events->shouldReceive('dispatch');
|
$events->shouldReceive('dispatch');
|
||||||
|
|
||||||
$this->cart = new Cart(
|
$this->cart = new Cart(
|
||||||
new SessionMock,
|
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||||
$events,
|
$events,
|
||||||
'shopping',
|
'shopping',
|
||||||
'SAMPLESESSIONKEY',
|
|
||||||
require (__DIR__ . '/../Helpers/ConfigMockOtherFormat.php')
|
require (__DIR__ . '/../Helpers/ConfigMockOtherFormat.php')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Cart;
|
||||||
|
use Wearepixel\Cart\CartCondition;
|
||||||
|
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||||
|
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||||
|
$events->shouldReceive('dispatch');
|
||||||
|
|
||||||
|
$this->cart = new Cart(
|
||||||
|
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||||
|
$events,
|
||||||
|
'cart',
|
||||||
|
require (__DIR__ . '/../Helpers/ConfigConditionsMock.php')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
Mockery::close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('quantity-limited item conditions', function () {
|
||||||
|
test('condition without applies_to applies to all items', function () {
|
||||||
|
$discount = new CartCondition([
|
||||||
|
'name' => '5% Off',
|
||||||
|
'type' => 'discount',
|
||||||
|
'value' => '-5%',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Widget',
|
||||||
|
'price' => 50.00,
|
||||||
|
'quantity' => 2,
|
||||||
|
'attributes' => [],
|
||||||
|
'conditions' => $discount,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Without applies_to: 50*0.95 * 2 = 95
|
||||||
|
expect($this->cart->get(1)->getPriceSumWithConditions())->toEqual(95.00);
|
||||||
|
expect($this->cart->getSubTotal())->toEqual(95.00);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('condition with applies_to only discounts that many items', function () {
|
||||||
|
$discount = new CartCondition([
|
||||||
|
'name' => '5% Off First Item',
|
||||||
|
'type' => 'discount',
|
||||||
|
'value' => '-5%',
|
||||||
|
'applies_to' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Widget',
|
||||||
|
'price' => 50.00,
|
||||||
|
'quantity' => 2,
|
||||||
|
'attributes' => [],
|
||||||
|
'conditions' => $discount,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// applies_to: 1 → (50*0.95) + 50 = 97.5
|
||||||
|
expect($this->cart->get(1)->getPriceSumWithConditions())->toEqual(97.50);
|
||||||
|
expect($this->cart->getSubTotal())->toEqual(97.50);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('applies_to greater than or equal to quantity discounts all items', function () {
|
||||||
|
$discount = new CartCondition([
|
||||||
|
'name' => '5% Off',
|
||||||
|
'type' => 'discount',
|
||||||
|
'value' => '-5%',
|
||||||
|
'applies_to' => 5,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Widget',
|
||||||
|
'price' => 50.00,
|
||||||
|
'quantity' => 2,
|
||||||
|
'attributes' => [],
|
||||||
|
'conditions' => $discount,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// applies_to 5 >= qty 2, so all items discounted: 50*0.95 * 2 = 95
|
||||||
|
expect($this->cart->get(1)->getPriceSumWithConditions())->toEqual(95.00);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('applies_to of zero means condition does not apply to any item', function () {
|
||||||
|
$discount = new CartCondition([
|
||||||
|
'name' => '5% Off',
|
||||||
|
'type' => 'discount',
|
||||||
|
'value' => '-5%',
|
||||||
|
'applies_to' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Widget',
|
||||||
|
'price' => 50.00,
|
||||||
|
'quantity' => 2,
|
||||||
|
'attributes' => [],
|
||||||
|
'conditions' => $discount,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// applies_to 0: no items discounted → 50 * 2 = 100
|
||||||
|
expect($this->cart->get(1)->getPriceSumWithConditions())->toEqual(100.00);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multiple conditions where only one has applies_to', function () {
|
||||||
|
$limitedDiscount = new CartCondition([
|
||||||
|
'name' => '10% Off First Item',
|
||||||
|
'type' => 'discount',
|
||||||
|
'value' => '-10%',
|
||||||
|
'applies_to' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$globalDiscount = new CartCondition([
|
||||||
|
'name' => '5% Off All',
|
||||||
|
'type' => 'discount',
|
||||||
|
'value' => '-5%',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Widget',
|
||||||
|
'price' => 100.00,
|
||||||
|
'quantity' => 2,
|
||||||
|
'attributes' => [],
|
||||||
|
'conditions' => [$limitedDiscount, $globalDiscount],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Item 1: 100 * 0.90 * 0.95 = 85.50
|
||||||
|
// Item 2: 100 * 0.95 = 95.00 (only global applies)
|
||||||
|
// Total: 180.50
|
||||||
|
expect($this->cart->get(1)->getPriceSumWithConditions())->toEqual(180.50);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getAppliesToQuantity returns null when not set', function () {
|
||||||
|
$condition = new CartCondition([
|
||||||
|
'name' => 'Discount',
|
||||||
|
'type' => 'discount',
|
||||||
|
'value' => '-5%',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($condition->getAppliesToQuantity())->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getAppliesToQuantity returns the set value', function () {
|
||||||
|
$condition = new CartCondition([
|
||||||
|
'name' => 'Discount',
|
||||||
|
'type' => 'discount',
|
||||||
|
'value' => '-5%',
|
||||||
|
'applies_to' => 3,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($condition->getAppliesToQuantity())->toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Wearepixel\Cart\CartCondition;
|
||||||
|
use Wearepixel\Cart\Shipping\ShippingRate;
|
||||||
|
use Wearepixel\Cart\Exceptions\InvalidShippingRateException;
|
||||||
|
|
||||||
|
class FlatRateShipping extends ShippingRate
|
||||||
|
{
|
||||||
|
protected string $name = 'Standard Shipping';
|
||||||
|
protected string $value = '+10';
|
||||||
|
|
||||||
|
public function isApplicable(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FreeShipping extends ShippingRate
|
||||||
|
{
|
||||||
|
protected string $name = 'Free Shipping';
|
||||||
|
protected string $value = '+0';
|
||||||
|
|
||||||
|
public function isApplicable(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ShippingRate', function () {
|
||||||
|
test('converts to a CartCondition', function () {
|
||||||
|
$condition = (new FlatRateShipping)->toCondition();
|
||||||
|
|
||||||
|
expect($condition)->toBeInstanceOf(CartCondition::class);
|
||||||
|
expect($condition->getName())->toBe('Standard Shipping');
|
||||||
|
expect($condition->getValue())->toBe('+10');
|
||||||
|
expect($condition->getType())->toBe('shipping');
|
||||||
|
expect($condition->getTarget())->toBe('total');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isApplicable() returning false causes toCondition() to throw', function () {
|
||||||
|
expect(fn() => (new FreeShipping)->toCondition())
|
||||||
|
->toThrow(InvalidShippingRateException::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports subtotal as target', function () {
|
||||||
|
$rate = new class extends ShippingRate {
|
||||||
|
protected string $name = 'Express';
|
||||||
|
protected string $value = '+25';
|
||||||
|
protected string $target = 'subtotal';
|
||||||
|
public function isApplicable(): bool { return true; }
|
||||||
|
};
|
||||||
|
|
||||||
|
expect($rate->toCondition()->getTarget())->toBe('subtotal');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports maximum free shipping threshold', function () {
|
||||||
|
$rate = new class extends ShippingRate {
|
||||||
|
protected string $name = 'Conditional Shipping';
|
||||||
|
protected string $value = '+15';
|
||||||
|
protected ?float $maximum = 100.0;
|
||||||
|
public function isApplicable(): bool { return true; }
|
||||||
|
};
|
||||||
|
|
||||||
|
expect($rate->toCondition()->getMaximum())->toBe(100.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Wearepixel\Cart\CartCondition;
|
||||||
|
use Wearepixel\Cart\Tax\TaxRule;
|
||||||
|
use Wearepixel\Cart\Exceptions\InvalidTaxRuleException;
|
||||||
|
|
||||||
|
class AuGst extends TaxRule
|
||||||
|
{
|
||||||
|
protected string $name = 'GST';
|
||||||
|
protected string $value = '10%';
|
||||||
|
|
||||||
|
public function isApplicable(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotApplicableTax extends TaxRule
|
||||||
|
{
|
||||||
|
protected string $name = 'VAT';
|
||||||
|
protected string $value = '20%';
|
||||||
|
|
||||||
|
public function isApplicable(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('TaxRule', function () {
|
||||||
|
test('converts to a CartCondition', function () {
|
||||||
|
$condition = (new AuGst)->toCondition();
|
||||||
|
|
||||||
|
expect($condition)->toBeInstanceOf(CartCondition::class);
|
||||||
|
expect($condition->getName())->toBe('GST');
|
||||||
|
expect($condition->getValue())->toBe('10%');
|
||||||
|
expect($condition->getType())->toBe('tax');
|
||||||
|
expect($condition->getTarget())->toBe('subtotal');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isApplicable() returning false causes toCondition() to throw', function () {
|
||||||
|
expect(fn() => (new NotApplicableTax)->toCondition())
|
||||||
|
->toThrow(InvalidTaxRuleException::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports total as target', function () {
|
||||||
|
$tax = new class extends TaxRule {
|
||||||
|
protected string $name = 'Sales Tax';
|
||||||
|
protected string $value = '8%';
|
||||||
|
protected string $target = 'total';
|
||||||
|
public function isApplicable(): bool { return true; }
|
||||||
|
};
|
||||||
|
|
||||||
|
expect($tax->toCondition()->getTarget())->toBe('total');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports minimum threshold', function () {
|
||||||
|
$tax = new class extends TaxRule {
|
||||||
|
protected string $name = 'Luxury Tax';
|
||||||
|
protected string $value = '15%';
|
||||||
|
protected ?float $minimum = 500.0;
|
||||||
|
public function isApplicable(): bool { return true; }
|
||||||
|
};
|
||||||
|
|
||||||
|
expect($tax->toCondition()->getMinimum())->toBe(500.0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Cart;
|
||||||
|
use Wearepixel\Cart\CartCondition;
|
||||||
|
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||||
|
use Wearepixel\Cart\Testing\CartFactory;
|
||||||
|
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||||
|
|
||||||
|
function makeCartForFactory(): Cart
|
||||||
|
{
|
||||||
|
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||||
|
$events->shouldReceive('dispatch');
|
||||||
|
|
||||||
|
return new Cart(
|
||||||
|
new SessionDriver(new SessionMock, 'TESTKEY'),
|
||||||
|
$events,
|
||||||
|
'cart',
|
||||||
|
require __DIR__ . '/../../Helpers/ConfigMock.php',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->cart = makeCartForFactory();
|
||||||
|
$this->factory = new CartFactory($this->cart);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(fn() => Mockery::close());
|
||||||
|
|
||||||
|
describe('CartFactory', function () {
|
||||||
|
test('withItems adds N generic items to the cart', function () {
|
||||||
|
$this->factory->withItems(3);
|
||||||
|
|
||||||
|
expect($this->cart->getContent()->count())->toBe(3);
|
||||||
|
expect($this->cart->getTotalQuantity())->toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('withCondition adds a condition to the cart', function () {
|
||||||
|
$condition = new CartCondition(['name' => 'GST', 'type' => 'tax', 'value' => '10%', 'target' => 'subtotal']);
|
||||||
|
$this->factory->withCondition($condition);
|
||||||
|
|
||||||
|
expect($this->cart->getCondition('GST'))->not->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('methods are chainable', function () {
|
||||||
|
$result = $this->factory->withItems(2)->withCondition(
|
||||||
|
new CartCondition(['name' => 'Discount', 'type' => 'discount', 'value' => '-5%', 'target' => 'subtotal'])
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($result)->toBeInstanceOf(CartFactory::class);
|
||||||
|
expect($this->cart->getContent()->count())->toBe(2);
|
||||||
|
expect($this->cart->getCondition('Discount'))->not->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('withItems generates unique item IDs', function () {
|
||||||
|
$this->factory->withItems(3);
|
||||||
|
$ids = $this->cart->getContent()->keys()->toArray();
|
||||||
|
|
||||||
|
expect(array_unique($ids))->toHaveCount(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('consecutive withItems calls produce unique IDs', function () {
|
||||||
|
$this->factory->withItems(2)->withItems(2);
|
||||||
|
|
||||||
|
$ids = $this->cart->getContent()->keys()->toArray();
|
||||||
|
expect(array_unique($ids))->toHaveCount(4);
|
||||||
|
expect($this->cart->getContent()->count())->toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('withCoupon adds a coupon condition to the cart', function () {
|
||||||
|
$coupon = new class extends \Wearepixel\Cart\Coupons\Coupon {
|
||||||
|
protected string $code = 'SAVE10';
|
||||||
|
protected string $value = '-10%';
|
||||||
|
protected string $target = 'subtotal';
|
||||||
|
|
||||||
|
public function isValid(): bool { return true; }
|
||||||
|
};
|
||||||
|
|
||||||
|
$this->factory->withCoupon($coupon);
|
||||||
|
|
||||||
|
expect($this->cart->getCondition('SAVE10'))->not->toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Cart;
|
||||||
|
use Wearepixel\Cart\CartCondition;
|
||||||
|
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||||
|
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||||
|
|
||||||
|
function makeFakeableCart(): Cart
|
||||||
|
{
|
||||||
|
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||||
|
$events->shouldReceive('dispatch');
|
||||||
|
|
||||||
|
return new Cart(
|
||||||
|
new SessionDriver(new SessionMock, 'TESTKEY'),
|
||||||
|
$events,
|
||||||
|
'cart',
|
||||||
|
require __DIR__ . '/../../Helpers/ConfigMock.php',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$this->cart = makeFakeableCart();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(fn() => Mockery::close());
|
||||||
|
|
||||||
|
describe('Cart::fake()', function () {
|
||||||
|
test('returns the CartFactory instance', function () {
|
||||||
|
expect($this->cart->fake())->toBeInstanceOf(\Wearepixel\Cart\Testing\CartFactory::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('swaps driver to NullDriver', function () {
|
||||||
|
$this->cart->fake();
|
||||||
|
expect($this->cart->getDriver()->getDriverName())->toBe('null');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Cart operations work after fake()', function () {
|
||||||
|
$this->cart->fake();
|
||||||
|
$this->cart->add(1, 'Widget', 50.00, 2);
|
||||||
|
|
||||||
|
expect($this->cart->isEmpty())->toBeFalse();
|
||||||
|
expect($this->cart->getContent()->count())->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('assertContains passes when item is in cart', function () {
|
||||||
|
$this->cart->fake();
|
||||||
|
$this->cart->add(1, 'Widget', 50.00, 1);
|
||||||
|
|
||||||
|
expect(fn() => $this->cart->assertContains(1))->not->toThrow(\PHPUnit\Framework\AssertionFailedError::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('assertContains fails when item is not in cart', function () {
|
||||||
|
$this->cart->fake();
|
||||||
|
|
||||||
|
expect(fn() => $this->cart->assertContains(99))->toThrow(\PHPUnit\Framework\AssertionFailedError::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('assertCount passes with correct count', function () {
|
||||||
|
$this->cart->fake();
|
||||||
|
$this->cart->add(1, 'A', 10.00, 1);
|
||||||
|
$this->cart->add(2, 'B', 20.00, 1);
|
||||||
|
|
||||||
|
expect(fn() => $this->cart->assertCount(2))->not->toThrow(\PHPUnit\Framework\AssertionFailedError::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('assertTotalQuantity passes with correct quantity', function () {
|
||||||
|
$this->cart->fake();
|
||||||
|
$this->cart->add(1, 'A', 10.00, 3);
|
||||||
|
|
||||||
|
expect(fn() => $this->cart->assertTotalQuantity(3))->not->toThrow(\PHPUnit\Framework\AssertionFailedError::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('assertSubTotal passes with correct subtotal', function () {
|
||||||
|
$this->cart->fake();
|
||||||
|
$this->cart->add(1, 'Widget', 50.00, 2);
|
||||||
|
|
||||||
|
expect(fn() => $this->cart->assertSubTotal(100.00))->not->toThrow(\PHPUnit\Framework\AssertionFailedError::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('assertTotal passes with correct total', function () {
|
||||||
|
$this->cart->fake();
|
||||||
|
$this->cart->add(1, 'Widget', 50.00, 1);
|
||||||
|
|
||||||
|
expect(fn() => $this->cart->assertTotal(50.00))->not->toThrow(\PHPUnit\Framework\AssertionFailedError::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('assertConditionApplied passes when condition exists', function () {
|
||||||
|
$this->cart->fake();
|
||||||
|
$this->cart->condition(new CartCondition(['name' => 'GST', 'type' => 'tax', 'value' => '10%', 'target' => 'subtotal']));
|
||||||
|
|
||||||
|
expect(fn() => $this->cart->assertConditionApplied('GST'))->not->toThrow(\PHPUnit\Framework\AssertionFailedError::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('assertConditionApplied fails when condition does not exist', function () {
|
||||||
|
$this->cart->fake();
|
||||||
|
|
||||||
|
expect(fn() => $this->cart->assertConditionApplied('GST'))->toThrow(\PHPUnit\Framework\AssertionFailedError::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('assertEmpty passes when cart is empty', function () {
|
||||||
|
$this->cart->fake();
|
||||||
|
|
||||||
|
expect(fn() => $this->cart->assertEmpty())->not->toThrow(\PHPUnit\Framework\AssertionFailedError::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('assertNotEmpty passes when cart has items', function () {
|
||||||
|
$this->cart->fake();
|
||||||
|
$this->cart->add(1, 'Widget', 10.00, 1);
|
||||||
|
|
||||||
|
expect(fn() => $this->cart->assertNotEmpty())->not->toThrow(\PHPUnit\Framework\AssertionFailedError::class);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
// Suppress PHP 8.4 deprecation notices that originate from vendor code.
|
||||||
|
// These are typically from third-party packages (e.g. Mockery) that have not
|
||||||
|
// yet updated implicit-nullable parameter signatures and are not actionable here.
|
||||||
|
set_error_handler(static function (int $errno, string $errstr, string $errfile): bool {
|
||||||
|
if ($errno === E_DEPRECATED && str_contains($errfile, DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}, E_DEPRECATED);
|
||||||
Reference in New Issue
Block a user