mirror of
https://github.com/wearepixel/laravel-cart.git
synced 2026-11-14 22:01:48 +00:00
Compare 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 | ||
|
|
e0d566cebd | ||
|
|
863ef52e8d | ||
|
|
6638f812c8 | ||
|
|
8f442f51e0 | ||
|
|
6dfd0ce761 | ||
|
|
a3161888aa | ||
|
|
4c02310bf7 | ||
|
|
e810eb4ef4 | ||
|
|
4cf078277d | ||
|
|
7adf5ecd1f | ||
|
|
158cf38f14 | ||
|
|
0f07b9ae8a | ||
|
|
ec99aa0563 | ||
|
|
8d6e1bcc26 | ||
|
|
b92dce9ca4 | ||
|
|
2196f58f01 | ||
|
|
ed4d112665 | ||
|
|
f60bf92157 | ||
|
|
a231ed8c43 | ||
|
|
599051631c | ||
|
|
b3ef4c38fc | ||
|
|
a45121ca63 | ||
|
|
5505f121c7 | ||
|
|
472d505041 | ||
|
|
c6c4240c5f | ||
|
|
54d7c8a54f | ||
|
|
a8d743eb9b | ||
|
|
b7381573e8 | ||
|
|
d4d4df7d58 | ||
|
|
6ea2cb3b8b | ||
|
|
7a2c41e02d | ||
|
|
9a59857825 | ||
|
|
43238b6f2e | ||
|
|
c0f0fcd2f0 | ||
|
|
185c4572ae | ||
|
|
c111cda3aa | ||
|
|
ab9eb6c873 | ||
|
|
7ee26add8d | ||
|
|
05318b7b13 | ||
|
|
4d2efcdeae | ||
|
|
a269c188e4 | ||
|
|
2ccba20119 | ||
|
|
313a4f7aa2 | ||
|
|
4a312a37e2 | ||
|
|
274a8504af | ||
|
|
4f22672ca5 | ||
|
|
61475a7060 | ||
|
|
6604cf984d | ||
|
|
7d955d722c | ||
|
|
54d94d8d12 |
+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@v2.2.0
|
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
|
||||||
with:
|
with:
|
||||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
name: PHP Formatting
|
|
||||||
|
|
||||||
on: pull_request
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
php-formatting:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: "Run Pint"
|
|
||||||
uses: aglipanci/laravel-pint-action@latest
|
|
||||||
with:
|
|
||||||
verboseMode: true
|
|
||||||
configPath: "pint.json"
|
|
||||||
pintVersion: 1.17.2
|
|
||||||
|
|
||||||
- name: Commit changes
|
|
||||||
uses: stefanzweifel/git-auto-commit-action@v5
|
|
||||||
with:
|
|
||||||
commit_message: "Apply Laravel Pint"
|
|
||||||
commit_options: '--no-verify --signoff'
|
|
||||||
file_pattern: "*.php"
|
|
||||||
commit_user_name: "Pixel Tech"
|
|
||||||
commit_user_email: "tech@wearepixel.com.au"
|
|
||||||
commit_author: "Pixel Tech<tech@wearepixel.com.au>"
|
|
||||||
@@ -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.1','8.2', '8.3']
|
php: ["8.3", "8.4", "8.5"]
|
||||||
stability: [prefer-stable]
|
stability: [prefer-stable]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
|
|
||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@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@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
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) Pixel Digital Pty Ltd <hello@wearepixel.com.au>
|
Copyright (c) Joel Male <joel@joelmale.com>
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,49 +1,88 @@
|
|||||||
|
<a name="top"></a>
|
||||||
|
[](https://joelmale.com)
|
||||||
[](https://packagist.org/packages/wearepixel/laravel-cart)
|
[](https://packagist.org/packages/wearepixel/laravel-cart)
|
||||||
[](https://github.com/wearepixel/laravel-cart/actions?query=workflow%3ATests+branch%3Amaster)
|
[](https://github.com/wearepixel/laravel-cart/actions?query=workflow%3ATests+branch%3Amaster)
|
||||||
[](https://packagist.org/packages/wearepixel/laravel-cart)
|
[](https://packagist.org/packages/wearepixel/laravel-cart)
|
||||||
|
[](#)
|
||||||
[](https://packagist.org/packages/wearepixel/laravel-cart)
|
[](https://packagist.org/packages/wearepixel/laravel-cart)
|
||||||
|
[](#-license)
|
||||||
|
|
||||||
A Cart Implementation for Laravel.
|
A Cart Implementation for Laravel.
|
||||||
|
|
||||||
Supported Laravel Versions: 9, 10, 11
|
Supported Laravel Versions: 10, 11, 12, and 13.
|
||||||
|
|
||||||
## Quick Demo
|
For Laravel 9.0 and below, please use version [1.0](https://github.com/wearepixel/laravel-cart/releases/tag/1.0.12)
|
||||||
|
|
||||||
```php
|
> **Upgrading from v2?** See the [Upgrade Guide](UPGRADE.md) for breaking changes and recommended v3 patterns.
|
||||||
// Add an item to the cart
|
|
||||||
\Cart::add(1, 'Product 1', 19.99, 2, ['size' => 'large']);
|
|
||||||
|
|
||||||
// Get the cart contents
|
## Table of Contents
|
||||||
$cartContents = \Cart::getContent();
|
|
||||||
|
|
||||||
// Update item quantity by its id
|
- [Getting Started](#-getting-started)
|
||||||
\Cart::update(1, ['quantity' => 3]);
|
- [Demo](#-deni)
|
||||||
|
- [Documentation](#-documentation)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [Basic Usage](#basic-usage)
|
||||||
|
- [Conditions](#conditions)
|
||||||
|
- [Cart Items](#cart-items)
|
||||||
|
- [Storage Drivers](#storage-drivers)
|
||||||
|
- [Artisan Generators](#artisan-generators)
|
||||||
|
- [First-class Objects](#first-class-objects)
|
||||||
|
- [Testing](#testing)
|
||||||
|
- [Livewire / Inertia](#livewire--inertia)
|
||||||
|
- [Events](#events)
|
||||||
|
- [Credits](#-credits)
|
||||||
|
- [License](#-license)
|
||||||
|
|
||||||
// Remove an item from the cart by its id
|
## 🚀 Getting Started
|
||||||
\Cart::remove(1);
|
|
||||||
|
|
||||||
// Get cart total
|
### 🔥 Installing
|
||||||
\$total = Cart::getTotal();
|
|
||||||
|
|
||||||
// Clear the entire cart
|
|
||||||
\Cart::clear();
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installing
|
|
||||||
|
|
||||||
Install the package through [Composer](http://getcomposer.org/).
|
Install the package through [Composer](http://getcomposer.org/).
|
||||||
|
|
||||||
`composer require "wearepixel/laravel-cart"`
|
`composer require wearepixel/laravel-cart`
|
||||||
|
|
||||||
## Configuration
|
## 🧑🍳 Demo
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Add an item to the cart
|
||||||
|
\Cart::add(
|
||||||
|
1, // any unique id
|
||||||
|
'Product 1', // product name
|
||||||
|
19.99, // product price
|
||||||
|
2, // quantity
|
||||||
|
['size' => 'large'] // an array of extra attributes
|
||||||
|
);
|
||||||
|
|
||||||
|
// get the entire cart
|
||||||
|
$cartContents = \Cart::getContent();
|
||||||
|
|
||||||
|
// update an item already in the cart
|
||||||
|
\Cart::update(
|
||||||
|
1, // the same unique id that was used to add the item
|
||||||
|
['quantity' => 3] // the quantity to update
|
||||||
|
);
|
||||||
|
|
||||||
|
// remove the item by its id
|
||||||
|
\Cart::remove(1);
|
||||||
|
|
||||||
|
// get the total of the cart
|
||||||
|
$total = Cart::getTotal();
|
||||||
|
|
||||||
|
// clear it all when you've finished (like when you've stored the order)
|
||||||
|
\Cart::clear();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Documentation
|
||||||
|
|
||||||
|
### ⚙️ 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
|
||||||
|
|
||||||
The package by default does not use round to format numbers, and instead returns the number using floatval().
|
The package by default does not use round to format numbers, and instead returns the number using floatval().
|
||||||
|
|
||||||
@@ -56,7 +95,7 @@ ### Formatting Numbers
|
|||||||
'round_mode' => env('LARAVEL_CART_ROUND_MODE', 'down'),
|
'round_mode' => env('LARAVEL_CART_ROUND_MODE', 'down'),
|
||||||
```
|
```
|
||||||
|
|
||||||
### Decimals
|
#### Decimals
|
||||||
|
|
||||||
You can customize the number of decimals in the configuration file.
|
You can customize the number of decimals in the configuration file.
|
||||||
|
|
||||||
@@ -66,7 +105,7 @@ ### Decimals
|
|||||||
'decimals' => env('LARAVEL_CART_DECIMALS', 2),
|
'decimals' => env('LARAVEL_CART_DECIMALS', 2),
|
||||||
```
|
```
|
||||||
|
|
||||||
### Round Mode
|
#### Round Mode
|
||||||
|
|
||||||
The package uses the `round` function to round the prices. You can customize the rounding mode in the configuration file.
|
The package uses the `round` function to round the prices. You can customize the rounding mode in the configuration file.
|
||||||
|
|
||||||
@@ -76,40 +115,22 @@ ### Round Mode
|
|||||||
'round_mode' => env('LARAVEL_CART_ROUND_MODE', 'down'),
|
'round_mode' => env('LARAVEL_CART_ROUND_MODE', 'down'),
|
||||||
```
|
```
|
||||||
|
|
||||||
## HOW TO USE
|
### Basic Usage
|
||||||
|
|
||||||
- [Usage](#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.
|
||||||
- [Conditions](#conditions)
|
|
||||||
- [Items](#items)
|
|
||||||
- [Associating Models](#associating-models)
|
|
||||||
- [Instances](#instances)
|
|
||||||
- [Exceptions](#exceptions)
|
|
||||||
- [Events](#events)
|
|
||||||
- [Format Response](#format)
|
|
||||||
- [Examples](#examples)
|
|
||||||
- [Using Different Storage](#storage)
|
|
||||||
- [License](#license)
|
|
||||||
|
|
||||||
## Usage
|
This also serves as a cart unique identifier which you can use to bind a cart to a specific user if you want to.
|
||||||
|
|
||||||
The cart has a default sessionKey that holds the cart data and stores it in the session.
|
Make sure to call `\Cart::setSessionKey($sessionKey)` before calling any other cart methods.
|
||||||
|
|
||||||
This
|
|
||||||
also serves as a cart unique identifier which you can use to bind a cart to a specific user if you want to.
|
|
||||||
|
|
||||||
Make sure to call `\Cart::session($sessionKey)` before calling any other cart methods.
|
|
||||||
|
|
||||||
Usually this is not required.
|
Usually this is not required.
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// Binds the cart to a unique id (user id, session id, etc.)
|
// Binds the cart to a unique id (user id, session id, etc.)
|
||||||
\Cart::session(User::first()->id);
|
\Cart::setSessionKey(User::first()->id);
|
||||||
|
|
||||||
// If you don't want to bind the cart on a project level, you can add it to each call
|
|
||||||
\Cart::session(User::first()->id)->add(1, 'Product 1', 19.99, 2, ['size' => 'large']);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Adding to the cart: **Cart::add()**
|
#### Adding to the cart: **Cart::add()**
|
||||||
|
|
||||||
There are a few ways to add items to the cart.
|
There are a few ways to add items to the cart.
|
||||||
|
|
||||||
@@ -180,7 +201,7 @@ ### Adding to the cart: **Cart::add()**
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Updating an item on a cart: **Cart::update()**
|
#### Updating an item on a cart: **Cart::update()**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
Cart::update(
|
Cart::update(
|
||||||
@@ -219,14 +240,14 @@ ### Updating an item on a cart: **Cart::update()**
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Removing an item on a cart: **Cart::remove()**
|
#### Removing an item on a cart: **Cart::remove()**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// Remove an item from the cart by its id
|
// Remove an item from the cart by its id
|
||||||
Cart::remove(456);
|
Cart::remove(456);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Getting an item on a cart: **Cart::get()**
|
#### Getting an item on a cart: **Cart::get()**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// Get an item from the cart by its id
|
// Get an item from the cart by its id
|
||||||
@@ -236,7 +257,7 @@ ### Getting an item on a cart: **Cart::get()**
|
|||||||
$summedPrice = Cart::get($itemId)->getPriceSum();
|
$summedPrice = Cart::get($itemId)->getPriceSum();
|
||||||
```
|
```
|
||||||
|
|
||||||
### Getting the cart content: **Cart::getContent()**
|
#### Getting the cart content: **Cart::getContent()**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// Returns a collection of the cart's contents
|
// Returns a collection of the cart's contents
|
||||||
@@ -256,13 +277,13 @@ ### Getting the cart content: **Cart::getContent()**
|
|||||||
$cartTotalQuantity = Cart::getTotalQuantity();
|
$cartTotalQuantity = Cart::getTotalQuantity();
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cart subtotal: **Cart::getSubTotal()**
|
#### Cart subtotal: **Cart::getSubTotal()**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$subTotal = Cart::getSubTotal();
|
$subTotal = Cart::getSubTotal();
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cart subtotal without conditions: **Cart::getSubTotalWithoutConditions()**
|
#### Cart subtotal without conditions: **Cart::getSubTotalWithoutConditions()**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$subTotalWithoutConditions = Cart::getSubTotalWithoutConditions();
|
$subTotalWithoutConditions = Cart::getSubTotalWithoutConditions();
|
||||||
@@ -274,31 +295,43 @@ ### Cart subtotal without conditions: **Cart::getSubTotalWithoutConditions()**
|
|||||||
$total = Cart::getTotal();
|
$total = Cart::getTotal();
|
||||||
```
|
```
|
||||||
|
|
||||||
### Check if cart is empty: **Cart::isEmpty()**
|
#### Check if cart is empty: **Cart::isEmpty()**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
Cart::isEmpty();
|
Cart::isEmpty();
|
||||||
```
|
```
|
||||||
|
|
||||||
### Clearing the Cart: **Cart::clear()**
|
#### Clearing the Cart: **Cart::clear()**
|
||||||
|
|
||||||
|
This clears all items and conditions from the cart.
|
||||||
|
|
||||||
```php
|
```php
|
||||||
Cart::clear();
|
Cart::clear();
|
||||||
```
|
```
|
||||||
|
|
||||||
## Conditions
|
#### Clearing the cart items only: **Cart::clearItems()**
|
||||||
|
|
||||||
Conditions are very useful in terms for adding things like discounts, taxes, shipping costs, etc.
|
This clears all items, but keeps the conditions (useful for when you want to keep the conditions but remove the items)
|
||||||
|
|
||||||
Conditions can be added on the entire cart or on individual items.
|
```php
|
||||||
|
Cart::clearItems();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conditions
|
||||||
|
|
||||||
|
Conditions are very useful for adding things like discounts, taxes, shipping, etc.
|
||||||
|
|
||||||
|
Conditions can be added on the entire cart or on individual items, and can even be applied only at certain cart values.
|
||||||
|
|
||||||
Conditions on a cart level should always have a `target` of `subtotal` or `total`. This tells the cart which value to apply the condition to.
|
Conditions on a cart level should always have a `target` of `subtotal` or `total`. This tells the cart which value to apply the condition to.
|
||||||
|
|
||||||
|
You can provide an optional `minimum` value which should be the dollar value in which the target (subtotal or total) needs to be for the condition to be active and impact the cart.
|
||||||
|
|
||||||
You can also provide an `order` to cart conditions which tells the cart in what order to apply the conditions. Item level conditions do not support the `order` parameter.
|
You can also provide an `order` to cart conditions which tells the cart in what order to apply the conditions. Item level conditions do not support the `order` parameter.
|
||||||
|
|
||||||
### Conditions on the cart
|
#### Conditions on the cart
|
||||||
|
|
||||||
#### Adding a condition to the cart: **Cart::condition()**
|
##### Adding a condition to the cart: **Cart::condition()**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// Add a single condition to the cart
|
// Add a single condition to the cart
|
||||||
@@ -349,7 +382,7 @@ #### Adding a condition to the cart: **Cart::condition()**
|
|||||||
Cart::condition($shipping);
|
Cart::condition($shipping);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Getting conditions on the cart: **Cart::getConditions()**
|
##### Getting conditions on the cart: **Cart::getConditions()**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// To get all applied conditions on a cart, use below:
|
// To get all applied conditions on a cart, use below:
|
||||||
@@ -362,34 +395,97 @@ #### Getting conditions on the cart: **Cart::getConditions()**
|
|||||||
$condition->getType(); // the type
|
$condition->getType(); // the type
|
||||||
$condition->getValue(); // the value of the condition
|
$condition->getValue(); // the value of the condition
|
||||||
$condition->getOrder(); // the order of the condition
|
$condition->getOrder(); // the order of the condition
|
||||||
|
$condition->getMinimum(); // the minimum dollar amount of the target, needed to activate the condition
|
||||||
|
$condition->getMaximum(); // the maximum dollar amount of the target, needed to keep the condition active
|
||||||
$condition->getAttributes(); // the attributes of the condition, returns an empty [] if no attributes added
|
$condition->getAttributes(); // the attributes of the condition, returns an empty [] if no attributes added
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Getting conditions on the cart as an array: **Cart::getConditions(array: true)**
|
||||||
|
|
||||||
|
You can get all cart conditions in array format by passing "array: true". This is useful if you want to store the carts conditions on a Livewire component since by default we have collections inside collections for conditions which Livewire does not support.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$cartConditions = Cart::getConditions(true);
|
||||||
|
$cartConditions = Cart::getConditions(active: true);
|
||||||
|
|
||||||
|
foreach ($cartConditions as $condition) {
|
||||||
|
$condition['name']; // the name of the condition
|
||||||
|
$condition['type']; // the type
|
||||||
|
$condition['value']; // the value of the condition
|
||||||
|
$condition['order']; // the order of the condition
|
||||||
|
$condition['minimum']; // the minimum dollar amount of the target, needed to activate the condition
|
||||||
|
$condition['maximum']; // the maximum dollar amount of the target, needed to keep the condition active
|
||||||
|
$condition['attributes']; // the attributes of the condition, returns an empty [] if no attributes added
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Getting conditions by name: **Cart::getCondition($conditionName)**
|
||||||
|
|
||||||
|
```php
|
||||||
|
$condition = Cart::getCondition('GST');
|
||||||
|
|
||||||
// You can also get a condition that has been applied on the cart by using its name:
|
|
||||||
$condition = Cart::getCondition('Tax: 10%');
|
|
||||||
$condition->getTarget(); // the target of which the condition was applied
|
$condition->getTarget(); // the target of which the condition was applied
|
||||||
$condition->getName(); // the name of the condition
|
$condition->getName(); // the name of the condition
|
||||||
$condition->getType(); // the type
|
$condition->getType(); // the type
|
||||||
$condition->getValue(); // the value of the condition
|
$condition->getValue(); // the value of the condition
|
||||||
|
$condition->getMinimum(); // the minimum dollar amount of the target, needed to activate the condition
|
||||||
|
$condition->getMaximum(); // the maximum dollar amount of the target, needed to keep the condition active
|
||||||
$condition->getAttributes(); // the attributes of the condition, returns an empty [] if no attributes added
|
$condition->getAttributes(); // the attributes of the condition, returns an empty [] if no attributes added
|
||||||
```
|
```
|
||||||
|
|
||||||
### Calculating condition value
|
##### Getting active conditions on the cart: **Cart::getConditions(active: true)**
|
||||||
|
|
||||||
|
You can get only active conditions by passsing `active: true` to the `getConditions()` method.
|
||||||
|
|
||||||
|
This will return conditions that are actively being applied to the cart (i.e if they meet their minimum or maximum value)
|
||||||
|
|
||||||
|
```php
|
||||||
|
$tenPercentOff = new CartCondition([
|
||||||
|
'name' => '10% Off',
|
||||||
|
'type' => 'discount',
|
||||||
|
'target' => 'subtotal',
|
||||||
|
'value' => '-10%',
|
||||||
|
'minimum' => 120,
|
||||||
|
'order' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Cart::getConditions(active: true);
|
||||||
|
|
||||||
|
// will return "10% Off" if the subtotal of the cart is $200.
|
||||||
|
// will return no conditions if the subtotal is $100.
|
||||||
|
|
||||||
|
$shipping = new CartCondition([
|
||||||
|
'name' => 'Shipping',
|
||||||
|
'type' => 'discount',
|
||||||
|
'target' => 'subtotal',
|
||||||
|
'value' => '10',
|
||||||
|
'maximum' => 200,
|
||||||
|
'order' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Cart::getConditions(active: true);
|
||||||
|
|
||||||
|
// will return "Shipping" if the subtotal of the cart is less than or equal 200
|
||||||
|
// will return no conditions if the subtotal is $210
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Calculating condition value
|
||||||
|
|
||||||
There are 2 ways to calculate the value of a condition:
|
There are 2 ways to calculate the value of a condition:
|
||||||
|
|
||||||
1. Using the `getCalculatedValue` method on the condition instance
|
1. Using the `getCalculatedValue` method on the condition instance
|
||||||
2. Using the `getCalculatedValueForCondition` method on the cart instance and passing the condition name
|
2. Using the `getCalculatedValueForCondition` method on the cart instance and passing the condition name
|
||||||
|
|
||||||
#### Using the `getCalculatedValue` method on the condition instance
|
##### Using the `getCalculatedValue` method on the condition instance
|
||||||
|
|
||||||
```php
|
```php
|
||||||
$subTotal = Cart::getSubTotal();
|
$subTotal = Cart::getSubTotal();
|
||||||
$condition = Cart::getCondition('VAT 12.5%');
|
$condition = Cart::getCondition('10% GST');
|
||||||
$conditionCalculatedValue = $condition->getCalculatedValue($subTotal);
|
$conditionCalculatedValue = $condition->getCalculatedValue($subTotal);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Using the `getCalculatedValueForCondition` method on the cart instance
|
##### Using the `getCalculatedValueForCondition` method on the cart instance
|
||||||
|
|
||||||
This method automatically calculates the value of a condition by it's name based on the order of the conditions.
|
This method automatically calculates the value of a condition by it's name based on the order of the conditions.
|
||||||
|
|
||||||
@@ -422,7 +518,45 @@ #### Using the `getCalculatedValueForCondition` method on the cart instance
|
|||||||
Cart::getCalculatedValueForCondition('Gift Card'); // returns 0 as the coupon discount is applied first and brings the subtotal to 0
|
Cart::getCalculatedValueForCondition('Gift Card'); // returns 0 as the coupon discount is applied first and brings the subtotal to 0
|
||||||
```
|
```
|
||||||
|
|
||||||
### Condition on items
|
##### Adding conditions that activate once a minimum value is met
|
||||||
|
|
||||||
|
You can add a `minimum` amount required for a condition to activate.
|
||||||
|
|
||||||
|
This is useful for applying discounts only after certain cart values, i.e: 10% off for any purchases over $120.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$tenPercentOff = new CartCondition([
|
||||||
|
'name' => '10% Off',
|
||||||
|
'type' => 'discount',
|
||||||
|
'target' => 'subtotal',
|
||||||
|
'value' => '-10%',
|
||||||
|
'minimum' => 120,
|
||||||
|
'order' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Cart::condition($tenPercentOff)
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Adding conditions that only activate up to a maximum value
|
||||||
|
|
||||||
|
You can add a `maximum` amount required for a condition to activate.
|
||||||
|
|
||||||
|
This is useful for applying discounts up until amounts, i.e shipping for anything below $200, and free shipping above.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$shipping = new CartCondition([
|
||||||
|
'name' => 'Shipping',
|
||||||
|
'type' => 'discount',
|
||||||
|
'target' => 'subtotal',
|
||||||
|
'value' => '12',
|
||||||
|
'maximum' => 200,
|
||||||
|
'order' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Cart::condition($shipping)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Conditions on items
|
||||||
|
|
||||||
Item conditions are useful if you have discounts to be applied specifically on an item and not on the whole cart value.
|
Item conditions are useful if you have discounts to be applied specifically on an item and not on the whole cart value.
|
||||||
|
|
||||||
@@ -473,6 +607,42 @@ ### Condition 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.
|
||||||
@@ -483,7 +653,7 @@ ### Condition on items
|
|||||||
$cartSubTotal = Cart::getSubTotal();
|
$cartSubTotal = Cart::getSubTotal();
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Add condition to existing Item on the cart: **Cart::addItemCondition($productId, $itemCondition)**
|
##### Add a condition to an existing item on the cart: **Cart::addItemCondition($productId, $itemCondition)**
|
||||||
|
|
||||||
Adding Condition to an existing Item on the cart is simple as well.
|
Adding Condition to an existing Item on the cart is simple as well.
|
||||||
|
|
||||||
@@ -497,7 +667,7 @@ #### Add condition to existing Item on the cart: **Cart::addItemCondition($produ
|
|||||||
Cart::addItemCondition(456, $condition);
|
Cart::addItemCondition(456, $condition);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Clearing Cart Conditions: **Cart::clearCartConditions()**
|
#### Clearing Cart Conditions: **Cart::clearCartConditions()**
|
||||||
|
|
||||||
This clears all cart level conditions, and does not affect item level conditions.
|
This clears all cart level conditions, and does not affect item level conditions.
|
||||||
|
|
||||||
@@ -511,25 +681,25 @@ ### Clearing Cart Conditions: **Cart::clearCartConditions()**
|
|||||||
Cart::clearAllConditions()
|
Cart::clearAllConditions()
|
||||||
```
|
```
|
||||||
|
|
||||||
### Remove a specific cart condtion: **Cart::removeCartCondition($conditionName)**
|
##### Remove a specific cart condtion: **Cart::removeCartCondition($conditionName)**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
Cart::removeCartCondition('Summer Sale 5%')
|
Cart::removeCartCondition('Summer Sale 5%')
|
||||||
```
|
```
|
||||||
|
|
||||||
### Remove a specific item condition: **Cart::removeItemCondition($itemId, $conditionName)**
|
##### Remove a specific item condition: **Cart::removeItemCondition($itemId, $conditionName)**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
Cart::removeItemCondition(456, 'SALE 5%')
|
Cart::removeItemCondition(456, 'SALE 5%')
|
||||||
```
|
```
|
||||||
|
|
||||||
### Clear all item conditions: **Cart::clearItemConditions($itemId)**
|
##### Clear all item conditions: **Cart::clearItemConditions($itemId)**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
Cart::clearItemConditions(456)
|
Cart::clearItemConditions(456)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Get conditions by type: **Cart::getConditionsByType($type)**
|
#### Get conditions by type: **Cart::getConditionsByType($type)**
|
||||||
|
|
||||||
This returns all conditions that has been added to the cart by the type specified.
|
This returns all conditions that has been added to the cart by the type specified.
|
||||||
|
|
||||||
@@ -537,13 +707,13 @@ ### Get conditions by type: **Cart::getConditionsByType($type)**
|
|||||||
$tax = Cart::getConditionsByType('tax');
|
$tax = Cart::getConditionsByType('tax');
|
||||||
```
|
```
|
||||||
|
|
||||||
### Remove conditions by type: **Cart::removeConditionsByType($type)**
|
##### Remove conditions by type: **Cart::removeConditionsByType($type)**
|
||||||
|
|
||||||
```php
|
```php
|
||||||
Cart::removeConditionsByType('tax');
|
Cart::removeConditionsByType('tax');
|
||||||
```
|
```
|
||||||
|
|
||||||
## Cart Items
|
### Cart Items
|
||||||
|
|
||||||
The method **Cart::getContent()** returns a collection of items.
|
The method **Cart::getContent()** returns a collection of items.
|
||||||
|
|
||||||
@@ -565,8 +735,380 @@ ## Cart Items
|
|||||||
$item->getPriceSumWithConditions();
|
$item->getPriceSumWithConditions();
|
||||||
```
|
```
|
||||||
|
|
||||||
## Credits
|
### Storage Drivers
|
||||||
|
|
||||||
Huge credits to the original maintainer of the package, located at [darryldecode/laravelshoppingcart](https://github.com/darryldecode/laravelshoppingcart)
|
The cart supports multiple storage backends via a driver system.
|
||||||
|
|
||||||
We have forked this package to update it to Laravel 11 and to add some features and support for newer PHP versions.
|
#### Session (default)
|
||||||
|
|
||||||
|
Cart data is stored in the Laravel session. No configuration needed.
|
||||||
|
|
||||||
|
#### Database
|
||||||
|
|
||||||
|
Store the cart in a database table for persistence across sessions.
|
||||||
|
|
||||||
|
**1. Create a migration:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
$table->string('session_id');
|
||||||
|
$table->text('items');
|
||||||
|
$table->text('conditions');
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Add casts to your model:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'items' => 'array',
|
||||||
|
'conditions' => 'array',
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Update `config/cart.php`:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
'driver' => 'database',
|
||||||
|
|
||||||
|
'drivers' => [
|
||||||
|
'database' => [
|
||||||
|
'model' => \App\Models\Cart::class,
|
||||||
|
'id' => 'session_id',
|
||||||
|
'items' => 'items',
|
||||||
|
'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
|
||||||
|
|
||||||
|
The package provides a few events that you can listen to in order to manipulate the cart or take actions based on cart events.
|
||||||
|
|
||||||
|
#### LaravelCart.Created
|
||||||
|
|
||||||
|
This fires every time a cart is instantiated (i.e every time \Cart::add() is called)
|
||||||
|
|
||||||
|
```
|
||||||
|
Event::listen('LaravelCart.Added', function () {
|
||||||
|
// cart was created
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LaravelCart.Adding
|
||||||
|
|
||||||
|
This fires every time an item is being added to the cart
|
||||||
|
|
||||||
|
```
|
||||||
|
Event::listen('LaravelCart.Adding', function ($item) {
|
||||||
|
// item is being added
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LaravelCart.Added
|
||||||
|
|
||||||
|
This fires every time an item is successfully added to the cart
|
||||||
|
|
||||||
|
```
|
||||||
|
Event::listen('LaravelCart.Added', function ($item) {
|
||||||
|
// item was added
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LaravelCart.Updating
|
||||||
|
|
||||||
|
This fires every time an item is being updated
|
||||||
|
|
||||||
|
```
|
||||||
|
Event::listen('LaravelCart.Updating', function ($item) {
|
||||||
|
// item is being updated
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LaravelCart.Updated
|
||||||
|
|
||||||
|
This fires every time an item is successfully updated
|
||||||
|
|
||||||
|
```
|
||||||
|
Event::listen('LaravelCart.Updated', function ($item) {
|
||||||
|
// item was updated
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LaravelCart.Removing
|
||||||
|
|
||||||
|
This fires every time an item is being removed
|
||||||
|
|
||||||
|
```
|
||||||
|
Event::listen('LaravelCart.Removing', function ($item) {
|
||||||
|
// item is being removed
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LaravelCart.Removed
|
||||||
|
|
||||||
|
This fires every time an item is successfully removed
|
||||||
|
|
||||||
|
```
|
||||||
|
Event::listen('LaravelCart.Removed', function ($item) {
|
||||||
|
// item was removed
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LaravelCart.Clearing
|
||||||
|
|
||||||
|
This fires every time the cart is being cleared
|
||||||
|
|
||||||
|
```
|
||||||
|
Event::listen('LaravelCart.Clearing', function () {
|
||||||
|
// cart is being cleared
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LaravelCart.Cleared
|
||||||
|
|
||||||
|
This fires every time the cart is successfully cleared
|
||||||
|
|
||||||
|
```
|
||||||
|
Event::listen('LaravelCart.Cleared', function () {
|
||||||
|
// cart was cleared
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🫡 Credits
|
||||||
|
|
||||||
|
This package was orignally created by [darryldecode](https://github.com/darryldecode) but has since seen almost no updates. I have decided to take the old package and transform it into a new package with new features and updates.
|
||||||
|
|
||||||
|
## 📓 License
|
||||||
|
|
||||||
|
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
|
||||||
|
|||||||
+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.
|
||||||
+18
-10
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wearepixel/laravel-cart",
|
"name": "wearepixel/laravel-cart",
|
||||||
"version": "1.0.4",
|
"version": "3.0.0",
|
||||||
"description": "A cart implementation for Laravel",
|
"description": "A cart implementation for Laravel",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"laravel",
|
"laravel",
|
||||||
@@ -15,26 +15,34 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Joel Male",
|
"name": "Joel Male",
|
||||||
"email": "joel@wearepixel.com.au"
|
"email": "joel@joelmale.com"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.1",
|
"php": ">=8.3",
|
||||||
"illuminate/support": "^9.0|^10.0|^11.0",
|
"illuminate/support": "^11.0|^12.0|^13.0",
|
||||||
"illuminate/validation": "^9.0|^10.0|^11.0",
|
"illuminate/auth": "^11.0|^12.0|^13.0",
|
||||||
"illuminate/translation": "^9.0|^10.0|^11.0"
|
"illuminate/container": "^11.0|^12.0|^13.0",
|
||||||
|
"illuminate/database": "^11.0|^12.0|^13.0",
|
||||||
|
"illuminate/validation": "^11.0|^12.0|^13.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"mockery/mockery": "^1.4.2",
|
"mockery/mockery": "^2.0",
|
||||||
"laravel/pint": "dev-main",
|
"laravel/pint": "^1.21",
|
||||||
"pestphp/pest": "2.35.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": {
|
||||||
"Wearepixel\\Cart\\": "src/"
|
"Wearepixel\\Cart\\": "src/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"minimum-stability": "dev",
|
"minimum-stability": "dev",
|
||||||
"extra": {
|
"extra": {
|
||||||
"laravel": {
|
"laravel": {
|
||||||
|
|||||||
Generated
+4120
-1625
File diff suppressed because it is too large
Load Diff
+21
-4
@@ -14,15 +14,32 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* ---------------------------------------------------------------
|
* ---------------------------------------------------------------
|
||||||
* Persistence
|
* Storage
|
||||||
* ---------------------------------------------------------------
|
* ---------------------------------------------------------------
|
||||||
|
* Supported: "session", "database", "redis", "null", "multi"
|
||||||
*/
|
*/
|
||||||
'storage' => null,
|
'driver' => 'session',
|
||||||
|
|
||||||
|
'storage' => [
|
||||||
|
'database' => [
|
||||||
|
'model' => '',
|
||||||
|
'id' => '',
|
||||||
|
'items' => '',
|
||||||
|
'conditions' => '',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ---------------------------------------------------------------
|
* ---------------------------------------------------------------
|
||||||
* Events
|
* Driver-specific options
|
||||||
* ---------------------------------------------------------------
|
* ---------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
'events' => null,
|
'drivers' => [
|
||||||
|
'redis' => [
|
||||||
|
'connection' => 'default',
|
||||||
|
'ttl' => 604800, // 7 days in seconds
|
||||||
|
],
|
||||||
|
|
||||||
|
'multi' => ['session', 'database'],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
@@ -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
|
||||||
+5
-6
@@ -1,10 +1,6 @@
|
|||||||
<?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>
|
||||||
@@ -14,4 +10,7 @@
|
|||||||
<directory>tests</directory>
|
<directory>tests</directory>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
</testsuites>
|
</testsuites>
|
||||||
|
<php>
|
||||||
|
<env name="DB_CONNECTION" value="testing"/>
|
||||||
|
</php>
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|||||||
+341
-461
File diff suppressed because it is too large
Load Diff
+32
-10
@@ -8,15 +8,12 @@
|
|||||||
|
|
||||||
class CartCondition
|
class CartCondition
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
private $args;
|
private $args;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the parsed raw value of the condition
|
* the parsed raw value of the condition
|
||||||
*/
|
*/
|
||||||
public $parsedRawValue = 0;
|
private $parsedRawValue = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $args (name, type, target, value)
|
* @param array $args (name, type, target, value)
|
||||||
@@ -65,6 +62,30 @@ public function getType()
|
|||||||
return $this->args['type'];
|
return $this->args['type'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the minimum value of this condition
|
||||||
|
*/
|
||||||
|
public function getMinimum(): int|float|null
|
||||||
|
{
|
||||||
|
return $this->args['minimum'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the maximum value of this condition
|
||||||
|
*/
|
||||||
|
public function getMaximum(): int|float|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
|
||||||
*
|
*
|
||||||
@@ -92,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;
|
||||||
}
|
}
|
||||||
@@ -126,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
|
||||||
{
|
{
|
||||||
@@ -253,4 +270,9 @@ protected function validate($args)
|
|||||||
throw new InvalidConditionException($validator->messages()->first());
|
throw new InvalidConditionException($validator->messages()->first());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return $this->args;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
-42
@@ -1,68 +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) {
|
||||||
$storageClass = config('cart.storage');
|
$config = config('cart');
|
||||||
$eventsClass = config('cart.events');
|
$events = $app['events'];
|
||||||
|
$driver = $app['cart.manager']->driver();
|
||||||
|
|
||||||
$storage = $storageClass ? new $storageClass : $app['session'];
|
return new Cart($driver, $events, 'cart', $config);
|
||||||
$events = $eventsClass ? new $eventsClass : $app['events'];
|
|
||||||
$instanceName = 'cart';
|
|
||||||
|
|
||||||
// default session or cart identifier. This will be overridden when calling Cart::session($sessionKey)->add() etc..
|
|
||||||
// like when adding a cart for a specific user name. Session Key can be string or maybe a unique identifier to bind a cart
|
|
||||||
// to a specific user, this can also be a user ID
|
|
||||||
$session_key = '4yTlTDKu3oJOfzD';
|
|
||||||
|
|
||||||
return new Cart(
|
|
||||||
$storage,
|
|
||||||
$events,
|
|
||||||
$instanceName,
|
|
||||||
$session_key,
|
|
||||||
config('cart')
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function provides(): array
|
||||||
* Get the services provided by the provider.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function provides()
|
|
||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class Helpers
|
|||||||
/**
|
/**
|
||||||
* Normalize prices
|
* Normalize prices
|
||||||
*/
|
*/
|
||||||
public static function normalizePrice($price): float
|
public static function normalizePrice($price): ?float
|
||||||
{
|
{
|
||||||
return (is_string($price)) ? floatval($price) : $price;
|
return (is_string($price)) ? floatval($price) : $price;
|
||||||
}
|
}
|
||||||
|
|||||||
+69
-6
@@ -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;
|
||||||
@@ -91,7 +99,37 @@ public function getConditions()
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this['conditions'];
|
$conditionsArray = [];
|
||||||
|
|
||||||
|
$conditions = $this['conditions'];
|
||||||
|
|
||||||
|
// check if we're already an array of CartConditions
|
||||||
|
foreach ($conditions as $key => $condition) {
|
||||||
|
if (is_object($condition) && $condition instanceof CartCondition) {
|
||||||
|
return $conditions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasSubArray = false;
|
||||||
|
|
||||||
|
foreach ($conditions as $key => $condition) {
|
||||||
|
if (is_array($condition)) {
|
||||||
|
$hasSubArray = true;
|
||||||
|
$conditionsArray[] = new CartCondition($condition);
|
||||||
|
} elseif ($condition instanceof CartCondition) {
|
||||||
|
$conditionsArray[] = $condition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $hasSubArray) {
|
||||||
|
if ($conditions instanceof CartCondition) {
|
||||||
|
return $conditions;
|
||||||
|
}
|
||||||
|
|
||||||
|
$conditionsArray[] = new CartCondition($conditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $conditionsArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -108,14 +146,14 @@ public function getPriceWithConditions($formatted = true)
|
|||||||
$processed = 0;
|
$processed = 0;
|
||||||
|
|
||||||
if ($this->hasConditions()) {
|
if ($this->hasConditions()) {
|
||||||
if (is_array($this->conditions)) {
|
if (is_array($this->getConditions())) {
|
||||||
foreach ($this->conditions as $condition) {
|
foreach ($this->getConditions() as $condition) {
|
||||||
($processed > 0) ? $toBeCalculated = $newPrice : $toBeCalculated = $originalPrice;
|
($processed > 0) ? $toBeCalculated = $newPrice : $toBeCalculated = $originalPrice;
|
||||||
$newPrice = $condition->applyCondition($toBeCalculated);
|
$newPrice = $condition->applyCondition($toBeCalculated);
|
||||||
$processed++;
|
$processed++;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$newPrice = $this['conditions']->applyCondition($originalPrice);
|
$newPrice = $this->getConditions()->applyCondition($originalPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Helpers::formatValue($newPrice, $formatted, $this->config);
|
return Helpers::formatValue($newPrice, $formatted, $this->config);
|
||||||
@@ -132,6 +170,31 @@ public function getPriceWithConditions($formatted = true)
|
|||||||
*/
|
*/
|
||||||
public function getPriceSumWithConditions($formatted = true)
|
public function getPriceSumWithConditions($formatted = true)
|
||||||
{
|
{
|
||||||
|
$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);
|
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;
|
||||||
|
|||||||
@@ -4,6 +4,5 @@
|
|||||||
'format_numbers' => true,
|
'format_numbers' => true,
|
||||||
'decimals' => 2,
|
'decimals' => 2,
|
||||||
'dec_point' => '.',
|
'dec_point' => '.',
|
||||||
'thousands_sep' => ',',
|
'driver' => 'session',
|
||||||
'use_cents' => false,
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'format_numbers' => true,
|
||||||
|
'decimals' => 2,
|
||||||
|
'round_mode' => 'up',
|
||||||
|
'driver' => 'database',
|
||||||
|
'storage' => [
|
||||||
|
'session',
|
||||||
|
'database' => [
|
||||||
|
'model' => \Wearepixel\Cart\Tests\Helpers\MockCartModel::class,
|
||||||
|
'id' => 'session_id',
|
||||||
|
'items' => 'items',
|
||||||
|
'conditions' => 'conditions',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -4,4 +4,5 @@
|
|||||||
'format_numbers' => true,
|
'format_numbers' => true,
|
||||||
'decimals' => 2,
|
'decimals' => 2,
|
||||||
'round_mode' => 'up',
|
'round_mode' => 'up',
|
||||||
|
'driver' => 'session',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -4,5 +4,5 @@
|
|||||||
'format_numbers' => false,
|
'format_numbers' => false,
|
||||||
'decimals' => 3,
|
'decimals' => 3,
|
||||||
'dec_point' => ',',
|
'dec_point' => ',',
|
||||||
'thousands_sep' => '.',
|
'driver' => 'session',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wearepixel\Cart\Tests\Helpers;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class MockCartModel extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'session_id',
|
||||||
|
'items',
|
||||||
|
'conditions',
|
||||||
|
];
|
||||||
|
|
||||||
|
public $timestamps = false;
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'items' => 'array',
|
||||||
|
'conditions' => 'array',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $table = 'carts';
|
||||||
|
}
|
||||||
@@ -20,4 +20,9 @@ public function put($key, $value)
|
|||||||
{
|
{
|
||||||
$this->session[$key] = $value;
|
$this->session[$key] = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function forget($key)
|
||||||
|
{
|
||||||
|
unset($this->session[$key]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-26
@@ -1,29 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/*
|
use Tests\TestCase;
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Test Case
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| The closure you provide to your test functions is always bound to a specific PHPUnit test
|
|
||||||
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
|
|
||||||
| need to change it using the "pest()" function to bind a different classes or traits.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
// pest()->extend(Tests\TestCase::class)->in('Feature');
|
uses(TestCase::class)->in(__DIR__);
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Expectations
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| When you're writing tests, you often need to check that values meet certain conditions. The
|
|
||||||
| "expect()" function gives you access to a set of "expectations" methods that you can use
|
|
||||||
| to assert different things. Of course, you may extend the Expectation API at any time.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
expect()->extend('toBeOne', function () {
|
|
||||||
return $this->toBe(1);
|
|
||||||
});
|
|
||||||
|
|||||||
+44
-2
@@ -1,10 +1,52 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Tests;
|
namespace Tests;
|
||||||
|
|
||||||
|
use Illuminate\Events\Dispatcher;
|
||||||
|
use Illuminate\Container\Container;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Capsule\Manager as DB;
|
||||||
use PHPUnit\Framework\TestCase as BaseTestCase;
|
use PHPUnit\Framework\TestCase as BaseTestCase;
|
||||||
|
|
||||||
abstract class TestCase extends BaseTestCase
|
class TestCase extends BaseTestCase
|
||||||
{
|
{
|
||||||
//
|
protected DB $db;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
// Create a Laravel container
|
||||||
|
$container = Container::getInstance();
|
||||||
|
$dispatcher = new Dispatcher;
|
||||||
|
|
||||||
|
// Setup Capsule (Eloquent ORM)
|
||||||
|
$this->db = new DB($container);
|
||||||
|
$this->db->addConnection([
|
||||||
|
'driver' => 'sqlite',
|
||||||
|
'database' => ':memory:',
|
||||||
|
'prefix' => '',
|
||||||
|
]);
|
||||||
|
$this->db->setAsGlobal();
|
||||||
|
$this->db->bootEloquent();
|
||||||
|
$this->db->getConnection()->setEventDispatcher($dispatcher);
|
||||||
|
|
||||||
|
// Run migrations (optional, for testing models)
|
||||||
|
$this->migrateDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function migrateDatabase()
|
||||||
|
{
|
||||||
|
$schema = $this->db->schema();
|
||||||
|
|
||||||
|
$schema->create('carts', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('session_id');
|
||||||
|
$table->text('items');
|
||||||
|
$table->text('conditions');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+434
-183
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
use Wearepixel\Cart\Cart;
|
use Wearepixel\Cart\Cart;
|
||||||
use Wearepixel\Cart\CartCondition;
|
use Wearepixel\Cart\CartCondition;
|
||||||
|
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 () {
|
||||||
@@ -9,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')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -21,7 +22,168 @@
|
|||||||
Mockery::close();
|
Mockery::close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('conditions', function () {
|
||||||
|
test('can be completely cleared', function () {
|
||||||
|
$siteWideDiscount = new CartCondition([
|
||||||
|
'name' => 'Site Wide Discount',
|
||||||
|
'type' => 'discount',
|
||||||
|
'target' => 'total',
|
||||||
|
'value' => '-5%',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$giftCard = new CartCondition([
|
||||||
|
'name' => 'Gift Card',
|
||||||
|
'type' => 'gift_card',
|
||||||
|
'target' => 'total',
|
||||||
|
'value' => '-25',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$itemCondition = new CartCondition([
|
||||||
|
'name' => 'Item Discount',
|
||||||
|
'type' => 'discount',
|
||||||
|
'value' => '-5%',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$item = [
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Backpack',
|
||||||
|
'price' => 168.72,
|
||||||
|
'quantity' => 1,
|
||||||
|
'attributes' => [],
|
||||||
|
'conditions' => [$itemCondition],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->cart->add($item);
|
||||||
|
|
||||||
|
$this->cart->condition([$siteWideDiscount, $giftCard]);
|
||||||
|
|
||||||
|
expect($this->cart->getConditions()->count())->toEqual(2, 'Cart should have two conditions');
|
||||||
|
expect($this->cart->get(1)['conditions'])->toHaveCount(1, 'Cart should have one condition');
|
||||||
|
|
||||||
|
$this->cart->clearAllConditions();
|
||||||
|
|
||||||
|
expect($this->cart->getConditions()->count())->toEqual(0, 'Cart should have no conditions now');
|
||||||
|
expect($this->cart->get(1)['conditions'])->toHaveCount(0, 'Cart items should have no conditions now');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can be converted to array', function () {
|
||||||
|
$condition = new CartCondition([
|
||||||
|
'name' => 'Site Wide Discount',
|
||||||
|
'type' => 'discount',
|
||||||
|
'target' => 'total',
|
||||||
|
'value' => '-5%',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->condition($condition);
|
||||||
|
|
||||||
|
$conditions = $this->cart->getConditions(true);
|
||||||
|
|
||||||
|
expect($conditions)->toHaveCount(1, 'Cart should have one condition');
|
||||||
|
expect($conditions['Site Wide Discount'])->toEqual([
|
||||||
|
'name' => 'Site Wide Discount',
|
||||||
|
'type' => 'discount',
|
||||||
|
'target' => 'total',
|
||||||
|
'value' => '-5%',
|
||||||
|
'order' => 1,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can be removed by name', function () {
|
||||||
|
$conditionOne = new CartCondition([
|
||||||
|
'name' => '5% Site Wide Sale',
|
||||||
|
'type' => 'sale',
|
||||||
|
'value' => '-5%',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$conditionTwo = new CartCondition([
|
||||||
|
'name' => 'Mother\'s Day Gift Pack',
|
||||||
|
'type' => 'promo',
|
||||||
|
'value' => '-25',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Body Scrub Kit',
|
||||||
|
'price' => 100,
|
||||||
|
'quantity' => 1,
|
||||||
|
'attributes' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->condition([$conditionOne, $conditionTwo]);
|
||||||
|
|
||||||
|
// let's very first the item has 2 conditions in it
|
||||||
|
expect($this->cart->getConditions())->toHaveCount(2, 'The item should have two conditions');
|
||||||
|
|
||||||
|
// now let's remove a condition on that item using the condition name
|
||||||
|
$this->cart->removeCartCondition('5% Site Wide Sale');
|
||||||
|
|
||||||
|
// now we should have only 1 condition left on that item
|
||||||
|
expect($this->cart->getConditions())->toHaveCount(1, 'Item should have one condition left');
|
||||||
|
expect($this->cart->getConditions()->first()->getName())->toEqual('Mother\'s Day Gift Pack');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('cart level conditions', function () {
|
describe('cart level conditions', function () {
|
||||||
|
test('can get conditions', function () {
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 456,
|
||||||
|
'name' => 'Leather Shoes',
|
||||||
|
'price' => 129.99,
|
||||||
|
'quantity' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$condition = new CartCondition([
|
||||||
|
'name' => '$5 Discount',
|
||||||
|
'type' => 'tax',
|
||||||
|
'target' => 'subtotal',
|
||||||
|
'value' => '-5',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->condition($condition);
|
||||||
|
|
||||||
|
$conditions = $this->cart->getConditions();
|
||||||
|
|
||||||
|
expect($conditions)->toHaveCount(1, 'Cart should have one condition');
|
||||||
|
expect($conditions)->toBeInstanceOf(CartConditionCollection::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can get conditions by name', function () {
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 456,
|
||||||
|
'name' => 'Leather Shoes',
|
||||||
|
'price' => 129.99,
|
||||||
|
'quantity' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$conditionOne = new CartCondition([
|
||||||
|
'name' => '$5 Discount',
|
||||||
|
'type' => 'tax',
|
||||||
|
'target' => 'subtotal',
|
||||||
|
'value' => '-5',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$conditionTwo = new CartCondition([
|
||||||
|
'name' => '5% Off',
|
||||||
|
'type' => 'tax',
|
||||||
|
'target' => 'subtotal',
|
||||||
|
'value' => '-5%',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->condition($conditionOne);
|
||||||
|
$this->cart->condition($conditionTwo);
|
||||||
|
|
||||||
|
$conditions = $this->cart->getConditions();
|
||||||
|
|
||||||
|
expect($conditions)->toHaveCount(2, 'Cart should have two conditions');
|
||||||
|
expect($conditions)->toBeInstanceOf(CartConditionCollection::class);
|
||||||
|
|
||||||
|
$condition = $this->cart->getCondition('5% Off');
|
||||||
|
|
||||||
|
expect($condition)->toBeInstanceOf(CartCondition::class);
|
||||||
|
expect($condition->getName())->toEqual('5% Off');
|
||||||
|
expect($condition->getValue())->toEqual('-5%');
|
||||||
|
});
|
||||||
|
|
||||||
test('can discount the subtotal by dollar amount', function () {
|
test('can discount the subtotal by dollar amount', function () {
|
||||||
$this->cart->add([
|
$this->cart->add([
|
||||||
'id' => 456,
|
'id' => 456,
|
||||||
@@ -788,42 +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('can be cleared', function () {
|
|
||||||
$siteWideDiscount = new CartCondition([
|
|
||||||
'name' => 'Site Wide Discount',
|
|
||||||
'type' => 'discount',
|
|
||||||
'target' => 'total',
|
|
||||||
'value' => '-5%',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$giftCard = new CartCondition([
|
|
||||||
'name' => 'Gift Card',
|
|
||||||
'type' => 'gift_card',
|
|
||||||
'target' => 'total',
|
|
||||||
'value' => '-25',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$item = [
|
|
||||||
'id' => 1,
|
|
||||||
'name' => 'Backpack',
|
|
||||||
'price' => 168.72,
|
|
||||||
'quantity' => 1,
|
|
||||||
'attributes' => [],
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->cart->add($item);
|
|
||||||
|
|
||||||
$this->cart->condition([$siteWideDiscount, $giftCard]);
|
|
||||||
|
|
||||||
expect($this->cart->getConditions()->count())->toEqual(2, 'Cart should have two conditions');
|
|
||||||
|
|
||||||
$this->cart->clearCartConditions();
|
|
||||||
|
|
||||||
expect($this->cart->getConditions()->count())->toEqual(0, 'Cart should have no conditions now');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 () {
|
||||||
@@ -915,10 +1043,218 @@
|
|||||||
expect($this->cart->getSubTotal())->toEqual(10.00, 'Cart should have sub total of 10.00');
|
expect($this->cart->getSubTotal())->toEqual(10.00, 'Cart should have sub total of 10.00');
|
||||||
expect($this->cart->getTotal())->toEqual(10.00, 'Cart should have a total of 10.00');
|
expect($this->cart->getTotal())->toEqual(10.00, 'Cart should have a total of 10.00');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('can activate a dollar discount when a minimum amount is met', function () {
|
||||||
|
$condition = new CartCondition([
|
||||||
|
'name' => 'Get $10 off when you spend $100',
|
||||||
|
'type' => 'target',
|
||||||
|
'target' => 'total',
|
||||||
|
'value' => '-10',
|
||||||
|
'minimum' => '100',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Apple iPhone 15',
|
||||||
|
'price' => 90,
|
||||||
|
'quantity' => 1,
|
||||||
|
'attributes' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->condition($condition);
|
||||||
|
|
||||||
|
expect($this->cart->getTotal())->toEqual(90, 'Cart should have total of 90');
|
||||||
|
|
||||||
|
$this->cart->clearItems();
|
||||||
|
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Apple iPhone 15',
|
||||||
|
'price' => 90,
|
||||||
|
'quantity' => 2,
|
||||||
|
'attributes' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($this->cart->getTotal())->toEqual(170, 'Cart should have total of 170');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can activate a percentage discount when a minimum amount is met', function () {
|
||||||
|
$condition = new CartCondition([
|
||||||
|
'name' => 'Get 10% off when you spend $100',
|
||||||
|
'type' => 'target',
|
||||||
|
'target' => 'total',
|
||||||
|
'value' => '-10%',
|
||||||
|
'minimum' => '100',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Apple iPhone 15',
|
||||||
|
'price' => 90,
|
||||||
|
'quantity' => 1,
|
||||||
|
'attributes' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->condition($condition);
|
||||||
|
|
||||||
|
expect($this->cart->getTotal())->toEqual(90, 'Cart should have total of 90');
|
||||||
|
|
||||||
|
$this->cart->clearItems();
|
||||||
|
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Apple iPhone 15',
|
||||||
|
'price' => 90,
|
||||||
|
'quantity' => 2,
|
||||||
|
'attributes' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($this->cart->getTotal())->toEqual(162, 'Cart should have total of 162');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can remove a dollar value condition when a maximum is met', function () {
|
||||||
|
$condition = new CartCondition([
|
||||||
|
'name' => 'Shipping',
|
||||||
|
'type' => 'target',
|
||||||
|
'target' => 'total',
|
||||||
|
'value' => '18',
|
||||||
|
'maximum' => '160',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Apple iPhone 15',
|
||||||
|
'price' => 90,
|
||||||
|
'quantity' => 1,
|
||||||
|
'attributes' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->condition($condition);
|
||||||
|
|
||||||
|
expect($this->cart->getTotal())->toEqual(108, 'Cart should have total of 108');
|
||||||
|
|
||||||
|
$this->cart->clear();
|
||||||
|
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Apple iPhone 15',
|
||||||
|
'price' => 90,
|
||||||
|
'quantity' => 2,
|
||||||
|
'attributes' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($this->cart->getTotal())->toEqual(180, 'Cart should have total of 180');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can remove a percentage condition when a maximum is met', function () {
|
||||||
|
$condition = new CartCondition([
|
||||||
|
'name' => 'Low Cart Tax',
|
||||||
|
'type' => 'target',
|
||||||
|
'target' => 'total',
|
||||||
|
'value' => '10%',
|
||||||
|
'maximum' => '140',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Apple iPhone 15',
|
||||||
|
'price' => 90,
|
||||||
|
'quantity' => 1,
|
||||||
|
'attributes' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->condition($condition);
|
||||||
|
|
||||||
|
expect($this->cart->getTotal())->toEqual(99, 'Cart should have total of 99');
|
||||||
|
|
||||||
|
$this->cart->clear();
|
||||||
|
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Apple iPhone 15',
|
||||||
|
'price' => 90,
|
||||||
|
'quantity' => 2,
|
||||||
|
'attributes' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($this->cart->getTotal())->toEqual(180, 'Cart should have total of 180');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('return cart conditions only when they are active', function () {
|
||||||
|
$condition = new CartCondition([
|
||||||
|
'name' => 'Get $10 off when you spend $100',
|
||||||
|
'type' => 'target',
|
||||||
|
'target' => 'total',
|
||||||
|
'value' => '-10',
|
||||||
|
'minimum' => '100',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Apple iPhone 15',
|
||||||
|
'price' => 90,
|
||||||
|
'quantity' => 1,
|
||||||
|
'attributes' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->cart->condition($condition);
|
||||||
|
|
||||||
|
expect($this->cart->getConditions(active: true))->toHaveCount(0, 'Cart should have 0 conditions');
|
||||||
|
|
||||||
|
$this->cart->clearItems();
|
||||||
|
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Apple iPhone 15',
|
||||||
|
'price' => 90,
|
||||||
|
'quantity' => 2,
|
||||||
|
'attributes' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($this->cart->getConditions(active: true))->toHaveCount(1, 'Cart should have 1 condition');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can be removed by condition name', function () {
|
||||||
|
$shipping = new CartCondition([
|
||||||
|
'name' => 'Shipping',
|
||||||
|
'type' => 'shipping',
|
||||||
|
'target' => 'total',
|
||||||
|
'value' => '12',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tax = new CartCondition([
|
||||||
|
'name' => 'Tax',
|
||||||
|
'type' => 'tax',
|
||||||
|
'target' => 'total',
|
||||||
|
'value' => '2.50',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$item = [
|
||||||
|
'id' => 456,
|
||||||
|
'name' => 'Sample Item 1',
|
||||||
|
'price' => 100,
|
||||||
|
'quantity' => 1,
|
||||||
|
'attributes' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->cart->add($item);
|
||||||
|
|
||||||
|
$this->cart->condition([$shipping, $tax]);
|
||||||
|
|
||||||
|
// let's prove first we have now two conditions in the cart
|
||||||
|
expect($this->cart->getConditions()->count())->toEqual(2, 'Cart should have two conditions');
|
||||||
|
|
||||||
|
// now let's remove a specific condition by condition name
|
||||||
|
$this->cart->removeCartCondition('Tax');
|
||||||
|
|
||||||
|
// cart should have now only 1 condition
|
||||||
|
expect($this->cart->getConditions()->count())->toEqual(1, 'Cart should have one condition');
|
||||||
|
expect($this->cart->getConditions()->first()->getName())->toEqual('Shipping');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('item level conditions', function () {
|
describe('item level conditions', function () {
|
||||||
test('can add an item with a condition', function () {
|
test('can be added to an item', function () {
|
||||||
$saleCondition = new CartCondition([
|
$saleCondition = new CartCondition([
|
||||||
'name' => '5% Discount',
|
'name' => '5% Discount',
|
||||||
'type' => 'sale',
|
'type' => 'sale',
|
||||||
@@ -942,6 +1278,41 @@
|
|||||||
expect($this->cart->getTotal())->toEqual(18, 'Cart should have total of 18');
|
expect($this->cart->getTotal())->toEqual(18, 'Cart should have total of 18');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('can be removed by name', function () {
|
||||||
|
$itemCondition1 = new CartCondition([
|
||||||
|
'name' => '5% Site Wide Sale',
|
||||||
|
'type' => 'sale',
|
||||||
|
'value' => '-5%',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$itemCondition2 = new CartCondition([
|
||||||
|
'name' => 'Mother\'s Day Gift Pack',
|
||||||
|
'type' => 'promo',
|
||||||
|
'value' => '-25',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$item = [
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Body Scrub Kit',
|
||||||
|
'price' => 100,
|
||||||
|
'quantity' => 1,
|
||||||
|
'attributes' => [],
|
||||||
|
'conditions' => [$itemCondition1, $itemCondition2],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->cart->add($item);
|
||||||
|
|
||||||
|
// let's very first the item has 2 conditions in it
|
||||||
|
expect($this->cart->get(1)['conditions'])->toHaveCount(2, 'The item should have two conditions');
|
||||||
|
|
||||||
|
// now let's remove a condition on that item using the condition name
|
||||||
|
$this->cart->removeItemCondition(1, '5% Site Wide Sale');
|
||||||
|
|
||||||
|
// now we should have only 1 condition left on that item
|
||||||
|
expect($this->cart->get(1)['conditions'])->toHaveCount(1, 'Item should have one condition left');
|
||||||
|
expect($this->cart->get(1)['conditions'][1]->getName())->toEqual('Mother\'s Day Gift Pack');
|
||||||
|
});
|
||||||
|
|
||||||
test('can add item with multiple negative item conditions', function () {
|
test('can add item with multiple negative item conditions', function () {
|
||||||
$saleCondition = new CartCondition([
|
$saleCondition = new CartCondition([
|
||||||
'name' => '5% Sale',
|
'name' => '5% Sale',
|
||||||
@@ -1082,110 +1453,36 @@
|
|||||||
expect($this->cart->get(1)->getPriceSumWithConditions())->toEqual(0.00, "The item's price should be prevented from going below zero.");
|
expect($this->cart->get(1)->getPriceSumWithConditions())->toEqual(0.00, "The item's price should be prevented from going below zero.");
|
||||||
});
|
});
|
||||||
|
|
||||||
// test('get cart condition by condition name', function () {
|
test('can be removed by condition name', function () {
|
||||||
// $itemCondition1 = new CartCondition([
|
$heavyItemSurcharge = new CartCondition([
|
||||||
// 'name' => 'SALE 5%',
|
'name' => 'Heavy Item Surcharge',
|
||||||
// 'type' => 'sale',
|
'type' => 'surcharge',
|
||||||
// 'target' => 'total',
|
'value' => '10',
|
||||||
// 'value' => '-5%',
|
]);
|
||||||
// ]);
|
|
||||||
// $itemCondition2 = new CartCondition([
|
|
||||||
// 'name' => 'Item Gift Pack 25.00',
|
|
||||||
// 'type' => 'promo',
|
|
||||||
// 'target' => 'total',
|
|
||||||
// 'value' => '-25',
|
|
||||||
// ]);
|
|
||||||
|
|
||||||
// $item = [
|
$tax = new CartCondition([
|
||||||
// 'id' => 456,
|
'name' => 'Tax',
|
||||||
// 'name' => 'Sample Item 1',
|
'type' => 'tax',
|
||||||
// 'price' => 100,
|
'value' => '10%',
|
||||||
// 'quantity' => 1,
|
]);
|
||||||
// 'attributes' => [],
|
|
||||||
// ];
|
|
||||||
|
|
||||||
// $this->cart->add($item);
|
$item = [
|
||||||
|
'id' => 12,
|
||||||
|
'name' => 'Beer (1 box)',
|
||||||
|
'price' => 410.60,
|
||||||
|
'quantity' => 1,
|
||||||
|
'attributes' => [],
|
||||||
|
'conditions' => [$heavyItemSurcharge, $tax],
|
||||||
|
];
|
||||||
|
|
||||||
// $this->cart->condition([$itemCondition1, $itemCondition2]);
|
$this->cart->add($item);
|
||||||
|
|
||||||
// // get a condition applied on cart by condition name
|
expect($this->cart->get(12)['conditions'])->toHaveCount(2, 'Item should have two conditions');
|
||||||
// $condition = $this->cart->getCondition($itemCondition1->getName());
|
|
||||||
|
|
||||||
// expect('SALE 5%')->toEqual($condition->getName());
|
$this->cart->removeItemCondition(12, 'Tax');
|
||||||
// expect('total')->toEqual($condition->getTarget());
|
|
||||||
// expect('sale')->toEqual($condition->getType());
|
|
||||||
// expect('-5%')->toEqual($condition->getValue());
|
|
||||||
// });
|
|
||||||
|
|
||||||
// test('remove cart condition by condition name', function () {
|
expect($this->cart->get(12)['conditions'])->toHaveCount(1, 'Item should have one condition');
|
||||||
// $itemCondition1 = new CartCondition([
|
});
|
||||||
// 'name' => 'SALE 5%',
|
|
||||||
// 'type' => 'sale',
|
|
||||||
// 'target' => 'total',
|
|
||||||
// 'value' => '-5%',
|
|
||||||
// ]);
|
|
||||||
// $itemCondition2 = new CartCondition([
|
|
||||||
// 'name' => 'Item Gift Pack 25.00',
|
|
||||||
// 'type' => 'promo',
|
|
||||||
// 'target' => 'total',
|
|
||||||
// 'value' => '-25',
|
|
||||||
// ]);
|
|
||||||
|
|
||||||
// $item = [
|
|
||||||
// 'id' => 456,
|
|
||||||
// 'name' => 'Sample Item 1',
|
|
||||||
// 'price' => 100,
|
|
||||||
// 'quantity' => 1,
|
|
||||||
// 'attributes' => [],
|
|
||||||
// ];
|
|
||||||
|
|
||||||
// $this->cart->add($item);
|
|
||||||
|
|
||||||
// $this->cart->condition([$itemCondition1, $itemCondition2]);
|
|
||||||
|
|
||||||
// // let's prove first we have now two conditions in the cart
|
|
||||||
// expect($this->cart->getConditions()->count())->toEqual(2, 'Cart should have two conditions');
|
|
||||||
|
|
||||||
// // now let's remove a specific condition by condition name
|
|
||||||
// $this->cart->removeCartCondition('SALE 5%');
|
|
||||||
|
|
||||||
// // cart should have now only 1 condition
|
|
||||||
// expect($this->cart->getConditions()->count())->toEqual(1, 'Cart should have one condition');
|
|
||||||
// expect($this->cart->getConditions()->first()->getName())->toEqual('Item Gift Pack 25.00');
|
|
||||||
// });
|
|
||||||
|
|
||||||
// test('remove item condition by condition name', function () {
|
|
||||||
// $itemCondition1 = new CartCondition([
|
|
||||||
// 'name' => 'SALE 5%',
|
|
||||||
// 'type' => 'sale',
|
|
||||||
// 'value' => '-5%',
|
|
||||||
// ]);
|
|
||||||
// $itemCondition2 = new CartCondition([
|
|
||||||
// 'name' => 'Item Gift Pack 25.00',
|
|
||||||
// 'type' => 'promo',
|
|
||||||
// 'value' => '-25',
|
|
||||||
// ]);
|
|
||||||
|
|
||||||
// $item = [
|
|
||||||
// 'id' => 456,
|
|
||||||
// 'name' => 'Sample Item 1',
|
|
||||||
// 'price' => 100,
|
|
||||||
// 'quantity' => 1,
|
|
||||||
// 'attributes' => [],
|
|
||||||
// 'conditions' => [$itemCondition1, $itemCondition2],
|
|
||||||
// ];
|
|
||||||
|
|
||||||
// $this->cart->add($item);
|
|
||||||
|
|
||||||
// // let's very first the item has 2 conditions in it
|
|
||||||
// expect($this->cart->get(456)['conditions'])->toHaveCount(2, 'Item should have two conditions');
|
|
||||||
|
|
||||||
// // now let's remove a condition on that item using the condition name
|
|
||||||
// $this->cart->removeItemCondition(456, 'SALE 5%');
|
|
||||||
|
|
||||||
// // now we should have only 1 condition left on that item
|
|
||||||
// expect($this->cart->get(456)['conditions'])->toHaveCount(1, 'Item should have one condition left');
|
|
||||||
// });
|
|
||||||
|
|
||||||
// test('remove item condition by condition name scenario two', function () {
|
// test('remove item condition by condition name scenario two', function () {
|
||||||
// // NOTE: in this scenario, we will add the conditions not in array format
|
// // NOTE: in this scenario, we will add the conditions not in array format
|
||||||
@@ -1556,50 +1853,4 @@
|
|||||||
// expect($this->cart->getConditions()->first()->getName())->toEqual('Item Gift Pack 20');
|
// expect($this->cart->getConditions()->first()->getName())->toEqual('Item Gift Pack 20');
|
||||||
// expect($this->cart->getConditions()->last()->getName())->toEqual('TAX');
|
// expect($this->cart->getConditions()->last()->getName())->toEqual('TAX');
|
||||||
// });
|
// });
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('conditions', function () {
|
|
||||||
test('can be completely cleared', function () {
|
|
||||||
$siteWideDiscount = new CartCondition([
|
|
||||||
'name' => 'Site Wide Discount',
|
|
||||||
'type' => 'discount',
|
|
||||||
'target' => 'total',
|
|
||||||
'value' => '-5%',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$giftCard = new CartCondition([
|
|
||||||
'name' => 'Gift Card',
|
|
||||||
'type' => 'gift_card',
|
|
||||||
'target' => 'total',
|
|
||||||
'value' => '-25',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$itemCondition = new CartCondition([
|
|
||||||
'name' => 'Item Discount',
|
|
||||||
'type' => 'discount',
|
|
||||||
'value' => '-5%',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$item = [
|
|
||||||
'id' => 1,
|
|
||||||
'name' => 'Backpack',
|
|
||||||
'price' => 168.72,
|
|
||||||
'quantity' => 1,
|
|
||||||
'attributes' => [],
|
|
||||||
'conditions' => [$itemCondition],
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->cart->add($item);
|
|
||||||
|
|
||||||
$this->cart->condition([$siteWideDiscount, $giftCard]);
|
|
||||||
|
|
||||||
expect($this->cart->getConditions()->count())->toEqual(2, 'Cart should have two conditions');
|
|
||||||
expect($this->cart->get(1)['conditions'])->toHaveCount(1, 'Cart should have one condition');
|
|
||||||
|
|
||||||
$this->cart->clearAllConditions();
|
|
||||||
|
|
||||||
expect($this->cart->getConditions()->count())->toEqual(0, 'Cart should have no conditions now');
|
|
||||||
expect($this->cart->get(1)['conditions'])->toHaveCount(0, 'Cart items should have no conditions now');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
+45
-17
@@ -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')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -49,6 +49,19 @@
|
|||||||
expect($this->cart->getTotal())->toEqual(0, 'Total should be 0');
|
expect($this->cart->getTotal())->toEqual(0, 'Total should be 0');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('can add an item an array with no price', function () {
|
||||||
|
$this->cart->add([
|
||||||
|
'id' => 455,
|
||||||
|
'name' => 'Sample Item',
|
||||||
|
'quantity' => 2,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($this->cart->isEmpty())->toBeFalse('Cart should not be empty');
|
||||||
|
expect($this->cart->getContent()->count())->toEqual(1, 'Cart content should be 1');
|
||||||
|
expect($this->cart->getContent()->first()['id'])->toEqual(455, 'Item added has ID of 455 so first content ID should be 455');
|
||||||
|
expect($this->cart->getTotal())->toEqual(0, 'Total should be 0');
|
||||||
|
});
|
||||||
|
|
||||||
test('can add an item without attributes', function () {
|
test('can add an item without attributes', function () {
|
||||||
$item = [
|
$item = [
|
||||||
'id' => 1,
|
'id' => 1,
|
||||||
@@ -283,6 +296,24 @@
|
|||||||
|
|
||||||
expect($item['quantity'])->toEqual(9.9, 'Item quantity should be 9.9');
|
expect($item['quantity'])->toEqual(9.9, 'Item quantity should be 9.9');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should throw an exception when no quantity is provided', function () {
|
||||||
|
$this->expectException('Wearepixel\Cart\Exceptions\InvalidItemException');
|
||||||
|
$this->expectExceptionMessage('The quantity must be at least 0.1.');
|
||||||
|
$this->cart->add(455, 'Sample Item', 100.99, 0, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw an exception when no id is provided', function () {
|
||||||
|
$this->expectException('Wearepixel\Cart\Exceptions\InvalidItemException');
|
||||||
|
$this->expectExceptionMessage('The id field is required.');
|
||||||
|
$this->cart->add('', 'Sample Item', 100.99, 2, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw an exception when no name is provided', function () {
|
||||||
|
$this->expectException('Wearepixel\Cart\Exceptions\InvalidItemException');
|
||||||
|
$this->expectExceptionMessage('The name field is required.');
|
||||||
|
$this->cart->add(523, '', 100.99, 2, []);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('cart items attributes', function () {
|
test('cart items attributes', function () {
|
||||||
@@ -542,21 +573,6 @@
|
|||||||
expect($item['quantity'])->toEqual(3, 'Item quantity of with item ID of 456 should now be reduced to 2');
|
expect($item['quantity'])->toEqual(3, 'Item quantity of with item ID of 456 should now be reduced to 2');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw exception when provided invalid values scenario one', function () {
|
|
||||||
$this->expectException('Wearepixel\Cart\Exceptions\InvalidItemException');
|
|
||||||
$this->cart->add(455, 'Sample Item', 100.99, 0, []);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw exception when provided invalid values scenario two', function () {
|
|
||||||
$this->expectException('Wearepixel\Cart\Exceptions\InvalidItemException');
|
|
||||||
$this->cart->add('', 'Sample Item', 100.99, 2, []);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw exception when provided invalid values scenario three', function () {
|
|
||||||
$this->expectException('Wearepixel\Cart\Exceptions\InvalidItemException');
|
|
||||||
$this->cart->add(523, '', 100.99, 2, []);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('clearing cart', function () {
|
test('clearing cart', function () {
|
||||||
$items = [
|
$items = [
|
||||||
[
|
[
|
||||||
@@ -673,3 +689,15 @@
|
|||||||
expect($this->cart->getTotalQuantity())->toBeInt('Return type should be INT');
|
expect($this->cart->getTotalQuantity())->toBeInt('Return type should be INT');
|
||||||
expect($this->cart->getTotalQuantity())->toEqual(12, 'Cart\'s quantity should be 4.');
|
expect($this->cart->getTotalQuantity())->toEqual(12, 'Cart\'s quantity should be 4.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('can add to cart and then update the session key', function () {
|
||||||
|
$this->cart->add(1, 'Sample Item', 100.99, 2, []);
|
||||||
|
|
||||||
|
expect($this->cart->getContent()->count())->toEqual(1, 'Cart content should be 1');
|
||||||
|
expect($this->cart->getContent()->first()['id'])->toEqual(1, 'Item added has ID of 1 so first content ID should be 1');
|
||||||
|
|
||||||
|
$this->cart->setSessionKey('new_session_key');
|
||||||
|
|
||||||
|
expect($this->cart->getSessionKey())->toEqual('new_session_key', 'Session key should be updated to new_session_key');
|
||||||
|
expect($this->cart->getContent()->count())->toEqual(1, 'Cart content should still be 1');
|
||||||
|
});
|
||||||
|
|||||||
@@ -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 () {});
|
||||||
@@ -9,15 +10,14 @@
|
|||||||
Mockery::close();
|
Mockery::close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('event cart created', function () {
|
test('event cart Created', function () {
|
||||||
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||||
$events->shouldReceive('dispatch')->once()->with('cart.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')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -25,16 +25,15 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('event cart adding', function () {
|
test('event cart adding', function () {
|
||||||
$events = Mockery::mock('Illuminate\Events\Dispatcher');
|
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||||
$events->shouldReceive('dispatch')->once()->with('cart.created', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Created', Mockery::type('array'), true);
|
||||||
$events->shouldReceive('dispatch')->once()->with('cart.adding', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Adding', Mockery::type('array'), true);
|
||||||
$events->shouldReceive('dispatch')->once()->with('cart.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')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -44,16 +43,15 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('event cart adding multiple times', function () {
|
test('event cart adding multiple times', function () {
|
||||||
$events = Mockery::mock('Illuminate\Events\Dispatcher');
|
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||||
$events->shouldReceive('dispatch')->once()->with('cart.created', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Created', Mockery::type('array'), true);
|
||||||
$events->shouldReceive('dispatch')->times(2)->with('cart.adding', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->times(2)->with('LaravelCart.Adding', Mockery::type('array'), true);
|
||||||
$events->shouldReceive('dispatch')->times(2)->with('cart.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')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -64,10 +62,10 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('event cart adding multiple times scenario two', function () {
|
test('event cart adding multiple times scenario two', function () {
|
||||||
$events = Mockery::mock('Illuminate\Events\Dispatcher');
|
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||||
$events->shouldReceive('dispatch')->once()->with('cart.created', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Created', Mockery::type('array'), true);
|
||||||
$events->shouldReceive('dispatch')->times(3)->with('cart.adding', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->times(3)->with('LaravelCart.Adding', Mockery::type('array'), true);
|
||||||
$events->shouldReceive('dispatch')->times(3)->with('cart.added', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->times(3)->with('LaravelCart.Added', Mockery::type('array'), true);
|
||||||
|
|
||||||
$items = [
|
$items = [
|
||||||
[
|
[
|
||||||
@@ -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')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -107,12 +104,12 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('event cart remove item', function () {
|
test('event cart remove item', function () {
|
||||||
$events = Mockery::mock('Illuminate\Events\Dispatcher');
|
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||||
$events->shouldReceive('dispatch')->once()->with('cart.created', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Created', Mockery::type('array'), true);
|
||||||
$events->shouldReceive('dispatch')->times(3)->with('cart.adding', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->times(3)->with('LaravelCart.Adding', Mockery::type('array'), true);
|
||||||
$events->shouldReceive('dispatch')->times(3)->with('cart.added', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->times(3)->with('LaravelCart.Added', Mockery::type('array'), true);
|
||||||
$events->shouldReceive('dispatch')->times(1)->with('cart.removing', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->times(1)->with('LaravelCart.Removing', Mockery::type('array'), true);
|
||||||
$events->shouldReceive('dispatch')->times(1)->with('cart.removed', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->times(1)->with('LaravelCart.Removed', Mockery::type('array'), true);
|
||||||
|
|
||||||
$items = [
|
$items = [
|
||||||
[
|
[
|
||||||
@@ -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')
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -154,12 +150,12 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('event cart clear', function () {
|
test('event cart clear', function () {
|
||||||
$events = Mockery::mock('Illuminate\Events\Dispatcher');
|
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||||
$events->shouldReceive('dispatch')->once()->with('cart.created', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Created', Mockery::type('array'), true);
|
||||||
$events->shouldReceive('dispatch')->times(3)->with('cart.adding', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->times(3)->with('LaravelCart.Adding', Mockery::type('array'), true);
|
||||||
$events->shouldReceive('dispatch')->times(3)->with('cart.added', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->times(3)->with('LaravelCart.Added', Mockery::type('array'), true);
|
||||||
$events->shouldReceive('dispatch')->once()->with('cart.clearing', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Clearing', Mockery::type('array'), true);
|
||||||
$events->shouldReceive('dispatch')->once()->with('cart.cleared', Mockery::type('array'), true);
|
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Cleared', Mockery::type('array'), true);
|
||||||
|
|
||||||
$items = [
|
$items = [
|
||||||
[
|
[
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Wearepixel\Cart\Cart;
|
||||||
|
use Wearepixel\Cart\CartCondition;
|
||||||
|
use Wearepixel\Cart\Drivers\DatabaseDriver;
|
||||||
|
use Wearepixel\Cart\Tests\Helpers\MockCartModel;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||||
|
$events->shouldReceive('dispatch');
|
||||||
|
|
||||||
|
$this->cart = new Cart(
|
||||||
|
new DatabaseDriver(
|
||||||
|
MockCartModel::class,
|
||||||
|
'session_id',
|
||||||
|
'items',
|
||||||
|
'conditions',
|
||||||
|
'SAMPLESESSIONKEY',
|
||||||
|
),
|
||||||
|
$events,
|
||||||
|
'cart',
|
||||||
|
require (__DIR__ . '/../Helpers/ConfigDatabaseMock.php')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
Mockery::close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('database cart', function () {
|
||||||
|
test('can add an item', function () {
|
||||||
|
$this->cart->add(1, 'Sample Item', 100.99, 2, []);
|
||||||
|
|
||||||
|
expect($this->cart->isEmpty())->toBeFalse('Cart should not be empty');
|
||||||
|
expect($this->cart->getContent()->count())->toEqual(1, 'Cart content should be 1');
|
||||||
|
expect($this->cart->getContent()->first()['id'])->toEqual(1, 'Item added has ID of 1 so first content ID should be 1');
|
||||||
|
expect($this->cart->getContent()->first()['price'])->toEqual(100.99, 'Item added has price of 100.99 so first content price should be 100.99');
|
||||||
|
|
||||||
|
expect(MockCartModel::all()->count())->toEqual(1, 'The carts database table should have 1 row');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can add and remove an item', function () {
|
||||||
|
$this->cart->add(1, 'Sample Item', 100.99, 2, []);
|
||||||
|
|
||||||
|
expect($this->cart->isEmpty())->toBeFalse('Cart should not be empty');
|
||||||
|
expect($this->cart->getContent()->count())->toEqual(1, 'Cart content should be 1');
|
||||||
|
expect($this->cart->getContent()->first()['id'])->toEqual(1, 'Item added has ID of 1 so first content ID should be 1');
|
||||||
|
expect($this->cart->getContent()->first()['price'])->toEqual(100.99, 'Item added has price of 100.99 so first content price should be 100.99');
|
||||||
|
|
||||||
|
expect(MockCartModel::all()->count())->toEqual(1, 'The carts database table should have 1 row');
|
||||||
|
|
||||||
|
$this->cart->remove(1);
|
||||||
|
|
||||||
|
$cartModel = MockCartModel::first();
|
||||||
|
|
||||||
|
expect($this->cart->isEmpty())->toBeTrue('Cart should be empty');
|
||||||
|
expect($this->cart->getContent()->count())->toEqual(0, 'Cart content should be 0');
|
||||||
|
expect($cartModel->items)->toEqual([], 'The carts database table should have 0 row');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can add an item with an item level condition', function () {
|
||||||
|
$saleCondition = new CartCondition([
|
||||||
|
'name' => '5% Discount',
|
||||||
|
'type' => 'sale',
|
||||||
|
'value' => '-5%',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$item = [
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Chicken Curry',
|
||||||
|
'price' => 18.95,
|
||||||
|
'quantity' => 1,
|
||||||
|
'attributes' => [],
|
||||||
|
'conditions' => $saleCondition,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->cart->add($item);
|
||||||
|
|
||||||
|
expect($this->cart->get(1)->getPriceSum())->toEqual(18.95);
|
||||||
|
expect($this->cart->get(1)->getPriceSumWithConditions())->toEqual(18);
|
||||||
|
expect($this->cart->getSubTotal())->toEqual(18, 'Cart should have subtotal of 18');
|
||||||
|
expect($this->cart->getTotal())->toEqual(18, 'Cart should have total of 18');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can add an item with multiple item level conditions', function () {
|
||||||
|
$saleCondition = new CartCondition([
|
||||||
|
'name' => '5% Discount',
|
||||||
|
'type' => 'sale',
|
||||||
|
'value' => '-5%',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$gstCondition = new CartCondition([
|
||||||
|
'name' => 'GST',
|
||||||
|
'type' => 'gst',
|
||||||
|
'value' => '2.5%',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$item = [
|
||||||
|
'id' => 1,
|
||||||
|
'name' => 'Chicken Curry',
|
||||||
|
'price' => 18.95,
|
||||||
|
'quantity' => 1,
|
||||||
|
'attributes' => [],
|
||||||
|
'conditions' => [$saleCondition, $gstCondition],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->cart->add($item);
|
||||||
|
|
||||||
|
expect($this->cart->get(1)->getPriceSum())->toEqual(18.95);
|
||||||
|
expect($this->cart->get(1)->getPriceSumWithConditions())->toEqual(18.45);
|
||||||
|
expect($this->cart->getSubTotal())->toEqual(18.45, 'Cart should have subtotal 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 () {
|
||||||
|
$this->cart->add(1, 'Sample Item', 100.99, 2, []);
|
||||||
|
|
||||||
|
expect($this->cart->isEmpty())->toBeFalse('Cart should not be empty');
|
||||||
|
expect($this->cart->getContent()->count())->toEqual(1, 'Cart content should be 1');
|
||||||
|
expect($this->cart->getContent()->first()['id'])->toEqual(1, 'Item added has ID of 1 so first content ID should be 1');
|
||||||
|
expect($this->cart->getContent()->first()['price'])->toEqual(100.99, 'Item added has price of 100.99 so first content price should be 100.99');
|
||||||
|
expect(MockCartModel::all()->count())->toEqual(1, 'The carts database table should have 1 row');
|
||||||
|
|
||||||
|
$this->cart->clear();
|
||||||
|
|
||||||
|
expect($this->cart->isEmpty())->toBeTrue('Cart should be empty');
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
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