mirror of
https://github.com/wearepixel/laravel-cart.git
synced 2026-11-14 22:01:48 +00:00
Compare commits
113
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ccee0e6ba | ||
|
|
cab17cdc72 | ||
|
|
8f99b148d6 | ||
|
|
6bd569cca7 | ||
|
|
63d310bf7e | ||
|
|
c9163a7d78 | ||
|
|
45ef72894b | ||
|
|
c6ec19121c | ||
|
|
c8baf7f0a8 | ||
|
|
0ea24eb419 | ||
|
|
917d530006 | ||
|
|
d56ce61dec | ||
|
|
11d734e848 | ||
|
|
07d155159d | ||
|
|
45362d3c62 | ||
|
|
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 |
@@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[docker-compose.yml]
|
||||
indent_size = 4
|
||||
@@ -0,0 +1,11 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
+26
-5
@@ -1,17 +1,38 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
target-branch: "master"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
open-pull-requests-limit: 10
|
||||
groups:
|
||||
actions:
|
||||
applies-to: version-updates
|
||||
patterns: ["*"]
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
labels:
|
||||
- "deps"
|
||||
- "dependencies"
|
||||
|
||||
- package-ecosystem: "composer"
|
||||
directory: "/"
|
||||
target-branch: "master"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "tuesday"
|
||||
open-pull-requests-limit: 10
|
||||
groups:
|
||||
php-minor-patch:
|
||||
applies-to: version-updates
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
php-security:
|
||||
applies-to: security-updates
|
||||
patterns: ["*"]
|
||||
commit-message:
|
||||
prefix: "chore"
|
||||
include: "scope"
|
||||
labels:
|
||||
- "deps"
|
||||
- "dependencies"
|
||||
|
||||
@@ -1,32 +1,59 @@
|
||||
name: dependabot-auto-merge
|
||||
on: pull_request_target
|
||||
name: Dependabot Auto-merge
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Tests"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
auto-merge:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.actor.login == 'dependabot[bot]'
|
||||
|
||||
steps:
|
||||
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v2.2.0
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- name: Auto-merge Dependabot PRs for semver-minor updates
|
||||
if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}}
|
||||
run: gh pr merge --auto --merge "$PR_URL"
|
||||
- name: Find PR and check update type
|
||||
id: check
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
run: |
|
||||
PR_DATA=$(gh pr list --repo "${{ github.repository }}" --head "$HEAD_BRANCH" --json number,title,url,author --jq '.[0]')
|
||||
|
||||
- name: Auto-merge Dependabot PRs for semver-patch updates
|
||||
if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
|
||||
run: gh pr merge --auto --merge "$PR_URL"
|
||||
PR_URL=$(echo "$PR_DATA" | jq -r '.url')
|
||||
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
||||
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
||||
|
||||
echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ "$PR_AUTHOR" != "app/dependabot" ]; then
|
||||
echo "is_safe=false" >> $GITHUB_OUTPUT
|
||||
echo "Not a Dependabot PR, skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse major version numbers from title: "bump X from A[.B.C] to D[.E.F]"
|
||||
# Works for both "from 4.0.0 to 4.1.0" and "from 4 to 6" (GitHub Actions)
|
||||
FROM_MAJOR=$(echo "$PR_TITLE" | grep -oP 'from \K[0-9]+')
|
||||
TO_MAJOR=$(echo "$PR_TITLE" | grep -oP 'to \K[0-9]+')
|
||||
|
||||
echo "PR: $PR_TITLE (major: $FROM_MAJOR -> $TO_MAJOR)"
|
||||
|
||||
if [ "$FROM_MAJOR" = "$TO_MAJOR" ]; then
|
||||
echo "is_safe=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "is_safe=false" >> $GITHUB_OUTPUT
|
||||
echo "Major version bump, skipping auto-merge."
|
||||
fi
|
||||
|
||||
- name: Merge minor and patch updates
|
||||
if: steps.check.outputs.is_safe == 'true'
|
||||
run: gh pr merge --merge "${{ steps.check.outputs.pr_url }}"
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
name: Dependabot Slack Alert
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
name: Notify Slack of major update
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
steps:
|
||||
- name: Check if major version bump
|
||||
id: check
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
run: |
|
||||
from_major=$(echo "$PR_TITLE" | grep -oP 'from \K[0-9]+' | head -1)
|
||||
to_major=$(echo "$PR_TITLE" | grep -oP 'to \K[0-9]+' | head -1)
|
||||
|
||||
if [ -n "$from_major" ] && [ -n "$to_major" ] && [ "$from_major" != "$to_major" ]; then
|
||||
echo "is_major=true" >> $GITHUB_OUTPUT
|
||||
echo "Major bump detected: $from_major -> $to_major"
|
||||
else
|
||||
echo "is_major=false" >> $GITHUB_OUTPUT
|
||||
echo "Minor/patch bump, skipping"
|
||||
fi
|
||||
|
||||
- name: Send Slack alert
|
||||
if: steps.check.outputs.is_major == 'true'
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
if [ -z "$SLACK_WEBHOOK_URL" ]; then
|
||||
echo "SLACK_WEBHOOK_URL not configured for this repo, skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
payload=$(jq -n \
|
||||
--arg repo "$REPO" \
|
||||
--arg title "$PR_TITLE" \
|
||||
--arg url "$PR_URL" \
|
||||
--arg pr "#$PR_NUMBER" \
|
||||
'{
|
||||
blocks: [
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: ":dependabot: *Major dependency update requires review*"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
fields: [
|
||||
{type: "mrkdwn", text: ("*Repository*\n`" + $repo + "`")},
|
||||
{type: "mrkdwn", text: ("*Pull Request*\n<" + $url + "|" + $pr + ">") }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: $title
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "context",
|
||||
elements: [
|
||||
{
|
||||
type: "mrkdwn",
|
||||
text: "Major version bumps are not auto-merged as they may contain breaking changes."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "actions",
|
||||
elements: [
|
||||
{
|
||||
type: "button",
|
||||
text: {type: "plain_text", text: "Review PR"},
|
||||
url: $url,
|
||||
style: "primary"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}')
|
||||
|
||||
curl -s -o /dev/null -w "%{http_code}" -X POST "$SLACK_WEBHOOK_URL" \
|
||||
-H "Content-type: application/json" \
|
||||
--data "$payload" | grep -q "^200$" \
|
||||
&& echo "Slack notification sent" \
|
||||
|| echo "Slack notification failed"
|
||||
@@ -9,19 +9,19 @@ on:
|
||||
jobs:
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
php: ['8.1', '8.2', '8.3']
|
||||
php: ["8.3", "8.4", "8.5"]
|
||||
stability: [prefer-stable]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
|
||||
with:
|
||||
php-version: ${{ matrix.php }}
|
||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd, redis, memcached
|
||||
@@ -29,12 +29,15 @@ jobs:
|
||||
coverage: none
|
||||
|
||||
- name: Install dependencies
|
||||
uses: nick-fields/retry@v3
|
||||
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
|
||||
with:
|
||||
timeout_minutes: 5
|
||||
max_attempts: 5
|
||||
command: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress ${{ matrix.flags }}
|
||||
|
||||
|
||||
- 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'
|
||||
|
||||
+3
-1
@@ -3,4 +3,6 @@
|
||||
.idea
|
||||
.phpunit.result.cache
|
||||
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)
|
||||
|
||||
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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,675 +1,36 @@
|
||||
<a name="top"></a>
|
||||
[](https://joelmale.com)
|
||||
[](https://packagist.org/packages/wearepixel/laravel-cart)
|
||||
[](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)
|
||||
[](#-license)
|
||||
|
||||
A Cart Implementation for Laravel.
|
||||
A Cart Implementation for Laravel. To learn all about it, head over to the [extensive documentation](https://wearepixel.com.au/docs/laravel-cart/v3/introduction).
|
||||
|
||||
Supported Laravel Versions: 9, 10, 11
|
||||
Supported Laravel Versions: 11, 12, and 13.
|
||||
|
||||
## Quick Demo
|
||||
For Laravel 10.0 and below, please use version [1.0](https://github.com/wearepixel/laravel-cart/releases/tag/1.0.12)
|
||||
|
||||
```php
|
||||
// Add an item to the cart
|
||||
\Cart::add(1, 'Product 1', 19.99, 2, ['size' => 'large']);
|
||||
> **Upgrading from v2?** See the [Upgrade Guide](https://wearepixel.com.au/docs/laravel-cart/v3/upgrade-guide) for breaking changes and recommended v3 patterns.
|
||||
|
||||
// Get the cart contents
|
||||
$cartContents = \Cart::getContent();
|
||||
|
||||
// Update item quantity by its id
|
||||
\Cart::update(1, ['quantity' => 3]);
|
||||
|
||||
// Remove an item from the cart by its id
|
||||
\Cart::remove(1);
|
||||
|
||||
// Get cart total
|
||||
\$total = Cart::getTotal();
|
||||
|
||||
// Clear the entire cart
|
||||
\Cart::clear();
|
||||
```
|
||||
|
||||
## Installing
|
||||
## Installation
|
||||
|
||||
Install the package through [Composer](http://getcomposer.org/).
|
||||
|
||||
`composer require "wearepixel/laravel-cart"`
|
||||
|
||||
## Configuration
|
||||
|
||||
You can publish the configuration file to customize various options.
|
||||
|
||||
```php
|
||||
php artisan vendor:publish --provider="Wearepixel\Cart\CartServiceProvider" --tag="config"
|
||||
```bash
|
||||
composer require wearepixel/laravel-cart
|
||||
```
|
||||
|
||||
### Formatting Numbers
|
||||
## Documentation
|
||||
|
||||
The package by default does not use round to format numbers, and instead returns the number using floatval().
|
||||
Full documentation, including configuration, usage, conditions, storage drivers, testing, and the v2 upgrade guide, is available at [wearepixel.com.au/docs/laravel-cart](https://wearepixel.com.au/docs/laravel-cart/v3/introduction).
|
||||
|
||||
If you'd prefer this number to be rounded, you can customize the formatting in the configuration file.
|
||||
## 🫡 Credits
|
||||
|
||||
Defaults to false.
|
||||
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.
|
||||
|
||||
```php
|
||||
'format_numbers' => env('LARAVEL_CART_FORMAT_VALUES', false),
|
||||
'round_mode' => env('LARAVEL_CART_ROUND_MODE', 'down'),
|
||||
```
|
||||
## 📓 License
|
||||
|
||||
### Decimals
|
||||
|
||||
You can customize the number of decimals in the configuration file.
|
||||
|
||||
Defaults to 2.
|
||||
|
||||
```php
|
||||
'decimals' => env('LARAVEL_CART_DECIMALS', 2),
|
||||
```
|
||||
|
||||
### Round Mode
|
||||
|
||||
The package uses the `round` function to round the prices. You can customize the rounding mode in the configuration file.
|
||||
|
||||
Defaults to `down`.
|
||||
|
||||
```php
|
||||
'round_mode' => env('LARAVEL_CART_ROUND_MODE', 'down'),
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
- [Basic Usage](#basic-usage)
|
||||
- [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
|
||||
|
||||
The cart has a default sessionKey that holds the cart data and stores it in the session.
|
||||
|
||||
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.
|
||||
|
||||
```php
|
||||
// Binds the cart to a unique id (user id, session id, etc.)
|
||||
\Cart::session(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()**
|
||||
|
||||
There are a few ways to add items to the cart.
|
||||
|
||||
```php
|
||||
// Add a simple product to the cart
|
||||
Cart::add(
|
||||
455, # product id
|
||||
'Sample Item', # product name
|
||||
100.99, # product price
|
||||
2, # quantity
|
||||
[] # optional attributes
|
||||
);
|
||||
|
||||
// array format
|
||||
Cart::add([
|
||||
456, # product id
|
||||
'Leather Shoes', # product name
|
||||
187, # product price
|
||||
1, # quantity
|
||||
[] # optional attributes
|
||||
]);
|
||||
|
||||
|
||||
// add an item with attributes
|
||||
Cart::add([
|
||||
457, // product id
|
||||
'T-Shirt', // product name
|
||||
29.99, // product price
|
||||
1, // quantity
|
||||
[
|
||||
'size' => 'L',
|
||||
'color' => 'Blue'
|
||||
] // attributes
|
||||
]);
|
||||
|
||||
// add an item with conditions
|
||||
Cart::add([
|
||||
458, // product id
|
||||
'Headphones', // product name
|
||||
199.99, // product price
|
||||
1, // quantity
|
||||
[], // attributes
|
||||
[
|
||||
[
|
||||
'name' => '10% Off',
|
||||
'type' => 'discount',
|
||||
'value' => '-10%'
|
||||
]
|
||||
] // conditions
|
||||
]);
|
||||
|
||||
// add multiple items at one time
|
||||
Cart::add(
|
||||
[
|
||||
456, # product id
|
||||
'Leather Shoes', # product name
|
||||
187, # product price
|
||||
1, # quantity
|
||||
[] # optional attributes
|
||||
],
|
||||
[
|
||||
431, # product id
|
||||
'Leather Jacket', # product name
|
||||
254.50, # product price
|
||||
1, # quantity
|
||||
[] # optional attributes
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
### Updating an item on a cart: **Cart::update()**
|
||||
|
||||
```php
|
||||
Cart::update(
|
||||
456, # product id
|
||||
[
|
||||
'name' => 'New Item Name', // new item name
|
||||
'price' => 98.67, // new item price as a float or string
|
||||
]
|
||||
);
|
||||
|
||||
// updating a product's quantity
|
||||
Cart::update(
|
||||
456, # product id
|
||||
[
|
||||
'quantity' => 2, // by default adding the quantity (so if from 4 to 6)
|
||||
]
|
||||
);
|
||||
|
||||
// reducing it...
|
||||
Cart::update(
|
||||
456,
|
||||
[
|
||||
'quantity' => -1, // so if from 4 to 3
|
||||
]
|
||||
);
|
||||
|
||||
// you can replace the quantity by setting relative to false
|
||||
Cart::update(
|
||||
456, # product id
|
||||
[
|
||||
'quantity' => [
|
||||
'relative' => false,
|
||||
'value' => 5 // if the quantity was 2, it will now be 5
|
||||
],
|
||||
]
|
||||
);
|
||||
```
|
||||
|
||||
### Removing an item on a cart: **Cart::remove()**
|
||||
|
||||
```php
|
||||
// Remove an item from the cart by its id
|
||||
Cart::remove(456);
|
||||
```
|
||||
|
||||
### Getting an item on a cart: **Cart::get()**
|
||||
|
||||
```php
|
||||
// Get an item from the cart by its id
|
||||
Cart::get(456);
|
||||
|
||||
// You can also get the total price of the item
|
||||
$summedPrice = Cart::get($itemId)->getPriceSum();
|
||||
```
|
||||
|
||||
### Getting the cart content: **Cart::getContent()**
|
||||
|
||||
```php
|
||||
// Returns a collection of the cart's contents
|
||||
$cartData = Cart::getContent();
|
||||
|
||||
// Gets the total number of items (not quantity) in the cart
|
||||
$cartCollection->count();
|
||||
|
||||
// Transform the collection to an array or a JSON
|
||||
$cartCollection->toArray();
|
||||
$cartCollection->toJson();
|
||||
```
|
||||
|
||||
Get cart total quantity: **Cart::getTotalQuantity()**
|
||||
|
||||
```php
|
||||
$cartTotalQuantity = Cart::getTotalQuantity();
|
||||
```
|
||||
|
||||
### Cart subtotal: **Cart::getSubTotal()**
|
||||
|
||||
```php
|
||||
$subTotal = Cart::getSubTotal();
|
||||
```
|
||||
|
||||
### Cart subtotal without conditions: **Cart::getSubTotalWithoutConditions()**
|
||||
|
||||
```php
|
||||
$subTotalWithoutConditions = Cart::getSubTotalWithoutConditions();
|
||||
```
|
||||
|
||||
Cart Total: **Cart::getTotal()**
|
||||
|
||||
```php
|
||||
$total = Cart::getTotal();
|
||||
```
|
||||
|
||||
### Check if cart is empty: **Cart::isEmpty()**
|
||||
|
||||
```php
|
||||
Cart::isEmpty();
|
||||
```
|
||||
|
||||
### Clearing the Cart: **Cart::clear()**
|
||||
|
||||
```php
|
||||
Cart::clear();
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
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.
|
||||
|
||||
### Conditions on the cart
|
||||
|
||||
#### Adding a condition to the cart: **Cart::condition()**
|
||||
|
||||
```php
|
||||
// Add a single condition to the cart
|
||||
$condition = new \Wearepixel\Cart\CartCondition([
|
||||
'name' => 'Tax: 10%',
|
||||
'type' => 'tax',
|
||||
'target' => 'subtotal', // this condition will be applied to cart's subtotal when getSubTotal() is called.
|
||||
'value' => '10%',
|
||||
'attributes' => [ // add extra attributes here
|
||||
'description' => 'Compulsory tax',
|
||||
]
|
||||
]);
|
||||
|
||||
Cart::condition($condition);
|
||||
|
||||
// Add multiple conditions
|
||||
$tax = new \Wearepixel\Cart\CartCondition([
|
||||
'name' => 'Tax: 10%',
|
||||
'type' => 'tax',
|
||||
'target' => 'subtotal', // this condition will be applied to cart's subtotal when getSubTotal() is called.
|
||||
'value' => '10%',
|
||||
'order' => 2
|
||||
]);
|
||||
|
||||
$shipping = new \Wearepixel\Cart\CartCondition([
|
||||
'name' => 'Shipping: $15',
|
||||
'type' => 'shipping',
|
||||
'target' => 'subtotal', // this condition will be applied to cart's subtotal when getSubTotal() is called.
|
||||
'value' => '+15',
|
||||
'order' => 1
|
||||
]);
|
||||
|
||||
Cart::condition($tax);
|
||||
Cart::condition($shipping);
|
||||
|
||||
// or as an array
|
||||
Cart::condition([$tax, $shipping]);
|
||||
|
||||
// add condition to only apply on totals, not in subtotal
|
||||
$shipping = new \Wearepixel\Cart\CartCondition([
|
||||
'name' => 'Express Shipping $15',
|
||||
'type' => 'shipping',
|
||||
'target' => 'total',
|
||||
'value' => '+15',
|
||||
'order' => 1
|
||||
]);
|
||||
|
||||
Cart::condition($shipping);
|
||||
```
|
||||
|
||||
#### Getting conditions on the cart: **Cart::getConditions()**
|
||||
|
||||
```php
|
||||
// To get all applied conditions on a cart, use below:
|
||||
$cartConditions = Cart::getConditions();
|
||||
|
||||
foreach($cartConditions as $condition)
|
||||
{
|
||||
$condition->getTarget(); // the target of which the condition was applied
|
||||
$condition->getName(); // the name of the condition
|
||||
$condition->getType(); // the type
|
||||
$condition->getValue(); // the value 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
|
||||
}
|
||||
```
|
||||
|
||||
#### 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');
|
||||
|
||||
$condition->getTarget(); // the target of which the condition was applied
|
||||
$condition->getName(); // the name of the condition
|
||||
$condition->getType(); // the type
|
||||
$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
|
||||
```
|
||||
|
||||
#### 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:
|
||||
|
||||
1. Using the `getCalculatedValue` method on the condition instance
|
||||
2. Using the `getCalculatedValueForCondition` method on the cart instance and passing the condition name
|
||||
|
||||
#### Using the `getCalculatedValue` method on the condition instance
|
||||
|
||||
```php
|
||||
$subTotal = Cart::getSubTotal();
|
||||
$condition = Cart::getCondition('10% GST');
|
||||
$conditionCalculatedValue = $condition->getCalculatedValue($subTotal);
|
||||
```
|
||||
|
||||
#### 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.
|
||||
|
||||
```php
|
||||
Cart::add([
|
||||
'id' => 1,
|
||||
'name' => 'Apple iPhone 15',
|
||||
'price' => 200,
|
||||
'quantity' => 1,
|
||||
'attributes' => [],
|
||||
]);
|
||||
|
||||
$couponDiscount = new CartCondition([
|
||||
'name' => 'Coupon Discount',
|
||||
'type' => 'discount',
|
||||
'target' => 'subtotal',
|
||||
'value' => '-200',
|
||||
'order' => 1,
|
||||
]);
|
||||
|
||||
$giftCard = new CartCondition([
|
||||
'name' => 'Gift Card',
|
||||
'type' => 'discount',
|
||||
'target' => 'subtotal',
|
||||
'value' => '-200',
|
||||
'order' => 2,
|
||||
]);
|
||||
|
||||
Cart::getCalculatedValueForCondition('Coupon Discount'); // returns 200
|
||||
Cart::getCalculatedValueForCondition('Gift Card'); // returns 0 as the coupon discount is applied first and brings the subtotal to 0
|
||||
```
|
||||
|
||||
### 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)
|
||||
```
|
||||
|
||||
### Condition on items
|
||||
|
||||
Item conditions are useful if you have discounts to be applied specifically on an item and not on the whole cart value.
|
||||
|
||||
```php
|
||||
|
||||
// lets create first our condition instance
|
||||
$saleCondition = new \Wearepixel\Cart\CartCondition([
|
||||
'name' => '50% Off',
|
||||
'type' => 'tax',
|
||||
'value' => '-50%',
|
||||
]);
|
||||
|
||||
// Create the product data with the condition
|
||||
$product = [
|
||||
'id' => 456,
|
||||
'name' => 'Sample Item 1',
|
||||
'price' => 100,
|
||||
'quantity' => 1,
|
||||
'attributes' => [],
|
||||
'conditions' => $saleCondition
|
||||
];
|
||||
|
||||
// Now add the product to the cart
|
||||
Cart::add($product);
|
||||
|
||||
// You can of course also do multiple conditions on an item
|
||||
$saleCondition = new \Wearepixel\Cart\CartCondition([
|
||||
'name' => 'SALE 5%',
|
||||
'type' => 'sale',
|
||||
'value' => '-5%',
|
||||
]);
|
||||
|
||||
$discountCode = new CartCondition([
|
||||
'name' => 'Discount Code',
|
||||
'type' => 'promo',
|
||||
'value' => '-25',
|
||||
]);
|
||||
|
||||
$item = [
|
||||
'id' => 456,
|
||||
'name' => 'Sample Item 1',
|
||||
'price' => 100,
|
||||
'quantity' => 1,
|
||||
'attributes' => [],
|
||||
'conditions' => [$saleCondition, $discountCode]
|
||||
];
|
||||
|
||||
Cart::add($item);
|
||||
```
|
||||
|
||||
> 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.
|
||||
|
||||
```php
|
||||
// the subtotal will be calculated based on the conditions added that has target => "subtotal"
|
||||
// and also conditions that are added on per item
|
||||
$cartSubTotal = Cart::getSubTotal();
|
||||
```
|
||||
|
||||
#### 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.
|
||||
|
||||
```php
|
||||
$condition = new CartCondition([
|
||||
'name' => 'COUPON 101',
|
||||
'type' => 'coupon',
|
||||
'value' => '-5%',
|
||||
]);
|
||||
|
||||
Cart::addItemCondition(456, $condition);
|
||||
```
|
||||
|
||||
### Clearing Cart Conditions: **Cart::clearCartConditions()**
|
||||
|
||||
This clears all cart level conditions, and does not affect item level conditions.
|
||||
|
||||
```php
|
||||
Cart::clearCartConditions()
|
||||
```
|
||||
|
||||
If you wish to clear all conditions from all items and the cart, use **Cart::clearAllConditions()**
|
||||
|
||||
```php
|
||||
Cart::clearAllConditions()
|
||||
```
|
||||
|
||||
### Remove a specific cart condtion: **Cart::removeCartCondition($conditionName)**
|
||||
|
||||
```php
|
||||
Cart::removeCartCondition('Summer Sale 5%')
|
||||
```
|
||||
|
||||
### Remove a specific item condition: **Cart::removeItemCondition($itemId, $conditionName)**
|
||||
|
||||
```php
|
||||
Cart::removeItemCondition(456, 'SALE 5%')
|
||||
```
|
||||
|
||||
### Clear all item conditions: **Cart::clearItemConditions($itemId)**
|
||||
|
||||
```php
|
||||
Cart::clearItemConditions(456)
|
||||
```
|
||||
|
||||
### Get conditions by type: **Cart::getConditionsByType($type)**
|
||||
|
||||
This returns all conditions that has been added to the cart by the type specified.
|
||||
|
||||
```php
|
||||
$tax = Cart::getConditionsByType('tax');
|
||||
```
|
||||
|
||||
### Remove conditions by type: **Cart::removeConditionsByType($type)**
|
||||
|
||||
```php
|
||||
Cart::removeConditionsByType('tax');
|
||||
```
|
||||
|
||||
## Cart Items
|
||||
|
||||
The method **Cart::getContent()** returns a collection of items.
|
||||
|
||||
Apart from the above methods, you can also get the price of an item with or without **item level conditions** applied.
|
||||
|
||||
These methods do not apply cart level conditions.
|
||||
|
||||
```php
|
||||
// With no conditions, just the price * quantity
|
||||
$item->getPriceSum();
|
||||
|
||||
// With conditions applied get the price of a single quantity
|
||||
$item->getPriceWithConditions();
|
||||
|
||||
// Get the sum with conditions applied
|
||||
$item->getPriceSumWithConditions();
|
||||
|
||||
// Without conditions applied
|
||||
$item->getPriceSumWithConditions();
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
Huge credits to the original maintainer of the package, located at [darryldecode/laravelshoppingcart](https://github.com/darryldecode/laravelshoppingcart)
|
||||
|
||||
We have forked this package to update it to Laravel 11 and to add some features and support for newer PHP versions.
|
||||
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",
|
||||
"version": "1.0.8",
|
||||
"version": "3.0.0",
|
||||
"description": "A cart implementation for Laravel",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
@@ -15,26 +15,34 @@
|
||||
},
|
||||
{
|
||||
"name": "Joel Male",
|
||||
"email": "joel@wearepixel.com.au"
|
||||
"email": "joel@joelmale.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"illuminate/support": "^9.0|^10.0|^11.0",
|
||||
"illuminate/validation": "^9.0|^10.0|^11.0",
|
||||
"illuminate/translation": "^9.0|^10.0|^11.0"
|
||||
"php": ">=8.3",
|
||||
"illuminate/support": "^11.0|^12.0|^13.0",
|
||||
"illuminate/auth": "^11.0|^12.0|^13.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": {
|
||||
"mockery/mockery": "^1.4.2",
|
||||
"laravel/pint": "^1.17",
|
||||
"pestphp/pest": "2.35.1",
|
||||
"larapack/dd": "1.1"
|
||||
"mockery/mockery": "^2.0",
|
||||
"laravel/pint": "^1.21",
|
||||
"pestphp/pest": "^4.7",
|
||||
"larapack/dd": "dev-master",
|
||||
"phpstan/phpstan": "2.2.x-dev"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Wearepixel\\Cart\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
|
||||
Generated
+4062
-1566
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
|
||||
+6
-7
@@ -1,17 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.4/phpunit.xsd"
|
||||
>
|
||||
<source>
|
||||
<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">
|
||||
<source ignoreIndirectDeprecations="true">
|
||||
<include>
|
||||
<directory suffix=".php">./src</directory>
|
||||
</include>
|
||||
</source>
|
||||
<testsuites>
|
||||
<testsuites>
|
||||
<testsuite name="Laravel Cart Test Suite">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<php>
|
||||
<env name="DB_CONNECTION" value="testing"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"preset": "laravel",
|
||||
"rules": {
|
||||
"concat_space": {
|
||||
"spacing": "one"
|
||||
},
|
||||
"class_attributes_separation": false,
|
||||
"ordered_imports": {
|
||||
"sort_algorithm": "length"
|
||||
"preset": "laravel",
|
||||
"rules": {
|
||||
"concat_space": {
|
||||
"spacing": "one"
|
||||
},
|
||||
"class_attributes_separation": false,
|
||||
"ordered_imports": {
|
||||
"sort_algorithm": "length"
|
||||
},
|
||||
"not_operator_with_space": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+337
-456
File diff suppressed because it is too large
Load Diff
+10
-6
@@ -78,6 +78,14 @@ public function getMaximum(): int|float|null
|
||||
return $this->args['maximum'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
@@ -105,13 +113,11 @@ public function setValue($value): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the order to apply this condition. If no argument order is applied we return 0 as
|
||||
* indicator that no assignment has been made
|
||||
* Set the order to apply this condition.
|
||||
*
|
||||
* @param int $order
|
||||
* @return int
|
||||
*/
|
||||
public function setOrder($order = 1)
|
||||
public function setOrder($order = 1): void
|
||||
{
|
||||
$this->args['order'] = $order;
|
||||
}
|
||||
@@ -139,8 +145,6 @@ public function applyCondition($totalOrSubTotalOrPrice)
|
||||
|
||||
/**
|
||||
* get the calculated value of this condition supplied by the subtotal|price
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCalculatedValue($totalOrSubTotalOrPrice = null): int|float
|
||||
{
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Wearepixel\Cart;
|
||||
|
||||
use Illuminate\Support\Manager;
|
||||
use Wearepixel\Cart\Drivers\MultiDriver;
|
||||
use Wearepixel\Cart\Drivers\NullDriver;
|
||||
use Wearepixel\Cart\Drivers\RedisDriver;
|
||||
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||
use Wearepixel\Cart\Drivers\DatabaseDriver;
|
||||
use Wearepixel\Cart\Drivers\Contracts\CartDriver;
|
||||
|
||||
class CartManager extends Manager
|
||||
{
|
||||
public function getDefaultDriver(): string
|
||||
{
|
||||
return $this->config->get('cart.driver', 'session');
|
||||
}
|
||||
|
||||
private function sessionKey(): string
|
||||
{
|
||||
return $this->container['session']->getId();
|
||||
}
|
||||
|
||||
public function createSessionDriver(): CartDriver
|
||||
{
|
||||
return new SessionDriver(
|
||||
$this->container['session'],
|
||||
$this->sessionKey(),
|
||||
);
|
||||
}
|
||||
|
||||
public function createDatabaseDriver(): CartDriver
|
||||
{
|
||||
$config = $this->config->get('cart.storage.database');
|
||||
|
||||
return new DatabaseDriver(
|
||||
$config['model'],
|
||||
$config['id'],
|
||||
$config['items'],
|
||||
$config['conditions'],
|
||||
$this->sessionKey(),
|
||||
);
|
||||
}
|
||||
|
||||
public function createRedisDriver(): CartDriver
|
||||
{
|
||||
$config = $this->config->get('cart.drivers.redis', []);
|
||||
|
||||
return new RedisDriver(
|
||||
$this->container['redis'],
|
||||
$this->sessionKey(),
|
||||
$config['connection'] ?? 'default',
|
||||
$config['ttl'] ?? 604800,
|
||||
);
|
||||
}
|
||||
|
||||
public function createNullDriver(): CartDriver
|
||||
{
|
||||
return new NullDriver($this->sessionKey());
|
||||
}
|
||||
|
||||
public function createMultiDriver(): CartDriver
|
||||
{
|
||||
$names = $this->config->get('cart.drivers.multi', ['session', 'database']);
|
||||
$drivers = array_map(fn(string $name) => $this->driver($name), $names);
|
||||
|
||||
return new MultiDriver($drivers);
|
||||
}
|
||||
}
|
||||
+27
-42
@@ -1,68 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Wearepixel\Cart;
|
||||
|
||||
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
|
||||
{
|
||||
/**
|
||||
* Indicates if loading of the provider is deferred.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $defer = false;
|
||||
|
||||
/**
|
||||
* Boot the service provider.
|
||||
*/
|
||||
public function boot()
|
||||
public function boot(): void
|
||||
{
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->publishes([
|
||||
__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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
public function register(): void
|
||||
{
|
||||
$this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'cart');
|
||||
|
||||
$this->app->singleton('cart.manager', fn($app) => new CartManager($app));
|
||||
|
||||
$this->app->singleton('cart', function ($app) {
|
||||
$storageClass = config('cart.storage');
|
||||
$eventsClass = config('cart.events');
|
||||
$config = config('cart');
|
||||
$events = $app['events'];
|
||||
$driver = $app['cart.manager']->driver();
|
||||
|
||||
$storage = $storageClass ? new $storageClass : $app['session'];
|
||||
$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')
|
||||
);
|
||||
return new Cart($driver, $events, 'cart', $config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function provides()
|
||||
public function provides(): array
|
||||
{
|
||||
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 with <comment>Cart::extend()</comment> in a service provider's boot() method, then set 'driver' to its name in config/cart.php.");
|
||||
|
||||
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;
|
||||
|
||||
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
|
||||
{
|
||||
protected static function getFacadeAccessor()
|
||||
protected static function getFacadeAccessor(): string
|
||||
{
|
||||
return 'cart';
|
||||
}
|
||||
|
||||
+70
-7
@@ -5,6 +5,14 @@
|
||||
use Illuminate\Support\Collection;
|
||||
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
|
||||
{
|
||||
/**
|
||||
@@ -46,9 +54,9 @@ public function __get($name)
|
||||
/**
|
||||
* return the associated model of an item
|
||||
*
|
||||
* @return bool
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getAssociatedModel()
|
||||
protected function getAssociatedModel(): mixed
|
||||
{
|
||||
if (! $this->has('associatedModel')) {
|
||||
return null;
|
||||
@@ -91,7 +99,37 @@ public function getConditions()
|
||||
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;
|
||||
|
||||
if ($this->hasConditions()) {
|
||||
if (is_array($this->conditions)) {
|
||||
foreach ($this->conditions as $condition) {
|
||||
if (is_array($this->getConditions())) {
|
||||
foreach ($this->getConditions() as $condition) {
|
||||
($processed > 0) ? $toBeCalculated = $newPrice : $toBeCalculated = $originalPrice;
|
||||
$newPrice = $condition->applyCondition($toBeCalculated);
|
||||
$processed++;
|
||||
}
|
||||
} else {
|
||||
$newPrice = $this['conditions']->applyCondition($originalPrice);
|
||||
$newPrice = $this->getConditions()->applyCondition($originalPrice);
|
||||
}
|
||||
|
||||
return Helpers::formatValue($newPrice, $formatted, $this->config);
|
||||
@@ -132,6 +170,31 @@ public function getPriceWithConditions($formatted = true)
|
||||
*/
|
||||
public function getPriceSumWithConditions($formatted = true)
|
||||
{
|
||||
return Helpers::formatValue($this->getPriceWithConditions(false) * $this->quantity, $formatted, $this->config);
|
||||
$conditions = $this->hasConditions() ? $this->getConditions() : [];
|
||||
$conditions = is_array($conditions) ? $conditions : [$conditions];
|
||||
|
||||
$hasLimit = collect($conditions)->some(fn ($c) => $c->getAppliesToQuantity() !== null);
|
||||
|
||||
if (! $hasLimit) {
|
||||
return Helpers::formatValue($this->getPriceWithConditions(false) * $this->quantity, $formatted, $this->config);
|
||||
}
|
||||
|
||||
$total = 0.0;
|
||||
|
||||
for ($i = 1; $i <= $this->quantity; $i++) {
|
||||
$price = $this->price;
|
||||
|
||||
foreach ($conditions as $condition) {
|
||||
$limit = $condition->getAppliesToQuantity();
|
||||
|
||||
if ($limit === null || $i <= $limit) {
|
||||
$price = $condition->applyCondition($price);
|
||||
}
|
||||
}
|
||||
|
||||
$total += $price;
|
||||
}
|
||||
|
||||
return Helpers::formatValue($total, $formatted, $this->config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Wearepixel\Cart\Shipping;
|
||||
|
||||
use Wearepixel\Cart\CartCondition;
|
||||
use Wearepixel\Cart\Contracts\CartConditionable;
|
||||
use Wearepixel\Cart\Exceptions\InvalidShippingRateException;
|
||||
|
||||
abstract class ShippingRate implements CartConditionable
|
||||
{
|
||||
protected string $name;
|
||||
|
||||
protected string $value;
|
||||
|
||||
protected string $target = 'total';
|
||||
|
||||
protected ?float $minimum = null;
|
||||
|
||||
protected ?float $maximum = null;
|
||||
|
||||
abstract public function isApplicable(): bool;
|
||||
|
||||
public function toCondition(): CartCondition
|
||||
{
|
||||
if (! $this->isApplicable()) {
|
||||
throw new InvalidShippingRateException("Shipping rate [{$this->name}] is not applicable.");
|
||||
}
|
||||
|
||||
$data = [
|
||||
'name' => $this->name,
|
||||
'type' => 'shipping',
|
||||
'value' => $this->value,
|
||||
'target' => $this->target,
|
||||
];
|
||||
|
||||
if ($this->minimum !== null) {
|
||||
$data['minimum'] = $this->minimum;
|
||||
}
|
||||
|
||||
if ($this->maximum !== null) {
|
||||
$data['maximum'] = $this->maximum;
|
||||
}
|
||||
|
||||
return new CartCondition($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Wearepixel\Cart\Tax;
|
||||
|
||||
use Wearepixel\Cart\CartCondition;
|
||||
use Wearepixel\Cart\Contracts\CartConditionable;
|
||||
use Wearepixel\Cart\Exceptions\InvalidTaxRuleException;
|
||||
|
||||
abstract class TaxRule implements CartConditionable
|
||||
{
|
||||
protected string $name;
|
||||
|
||||
protected string $value;
|
||||
|
||||
protected string $target = 'subtotal';
|
||||
|
||||
protected ?float $minimum = null;
|
||||
|
||||
protected ?float $maximum = null;
|
||||
|
||||
abstract public function isApplicable(): bool;
|
||||
|
||||
public function toCondition(): CartCondition
|
||||
{
|
||||
if (! $this->isApplicable()) {
|
||||
throw new InvalidTaxRuleException("Tax rule [{$this->name}] is not applicable.");
|
||||
}
|
||||
|
||||
$data = [
|
||||
'name' => $this->name,
|
||||
'type' => 'tax',
|
||||
'value' => $this->value,
|
||||
'target' => $this->target,
|
||||
];
|
||||
|
||||
if ($this->minimum !== null) {
|
||||
$data['minimum'] = $this->minimum;
|
||||
}
|
||||
|
||||
if ($this->maximum !== null) {
|
||||
$data['maximum'] = $this->maximum;
|
||||
}
|
||||
|
||||
return new CartCondition($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Wearepixel\Cart\Testing;
|
||||
|
||||
use Wearepixel\Cart\Cart;
|
||||
use Wearepixel\Cart\CartCondition;
|
||||
use Wearepixel\Cart\Coupons\Coupon;
|
||||
|
||||
class CartFactory
|
||||
{
|
||||
public function __construct(private Cart $cart) {}
|
||||
|
||||
public function withItems(int $count): static
|
||||
{
|
||||
$start = $this->cart->getContent()->count() + 1;
|
||||
for ($i = $start; $i < $start + $count; $i++) {
|
||||
$this->cart->add($i, "Test Item {$i}", 10.00, 1);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withCondition(CartCondition $condition): static
|
||||
{
|
||||
$this->cart->condition($condition);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withCoupon(Coupon $coupon): static
|
||||
{
|
||||
$this->cart->coupon($coupon);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,9 @@
|
||||
use Illuminate\Translation\FileLoader;
|
||||
use Illuminate\Translation\Translator;
|
||||
|
||||
/**
|
||||
* @method static \Illuminate\Validation\Validator make(array $data, array $rules, array $messages = [], array $attributes = [])
|
||||
*/
|
||||
abstract class Validator
|
||||
{
|
||||
protected static $factory;
|
||||
|
||||
@@ -4,6 +4,5 @@
|
||||
'format_numbers' => true,
|
||||
'decimals' => 2,
|
||||
'dec_point' => '.',
|
||||
'thousands_sep' => ',',
|
||||
'use_cents' => false,
|
||||
'driver' => 'session',
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
'decimals' => 2,
|
||||
'round_mode' => 'up',
|
||||
'driver' => 'session',
|
||||
];
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
'format_numbers' => false,
|
||||
'decimals' => 3,
|
||||
'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;
|
||||
}
|
||||
|
||||
public function forget($key)
|
||||
{
|
||||
unset($this->session[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-26
@@ -1,29 +1,5 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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.
|
||||
|
|
||||
*/
|
||||
use Tests\TestCase;
|
||||
|
||||
// pest()->extend(Tests\TestCase::class)->in('Feature');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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);
|
||||
});
|
||||
uses(TestCase::class)->in(__DIR__);
|
||||
|
||||
+44
-2
@@ -1,10 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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;
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use Wearepixel\Cart\Cart;
|
||||
use Wearepixel\Cart\CartCondition;
|
||||
use Wearepixel\Cart\CartConditionCollection;
|
||||
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||
|
||||
beforeEach(function () {
|
||||
@@ -10,10 +11,9 @@
|
||||
$events->shouldReceive('dispatch');
|
||||
|
||||
$this->cart = new Cart(
|
||||
new SessionMock,
|
||||
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||
$events,
|
||||
'cart',
|
||||
'SAMPLESESSIONKEY',
|
||||
require (__DIR__ . '/../Helpers/ConfigConditionsMock.php')
|
||||
);
|
||||
});
|
||||
@@ -119,7 +119,7 @@
|
||||
|
||||
// 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()[0]->getName())->toEqual('Mother\'s Day Gift Pack');
|
||||
expect($this->cart->getConditions()->first()->getName())->toEqual('Mother\'s Day Gift Pack');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -950,8 +950,8 @@
|
||||
|
||||
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($couponDiscount->getCalculatedValue())->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('Coupon Discount')->getCalculatedValue(200.0))->toEqual(200.0, 'Coupon Discount value should be 200.0');
|
||||
expect($this->cart->getCondition('Gift Card')->getCalculatedValue(0))->toEqual(0, 'Gift Card value should be 0');
|
||||
});
|
||||
|
||||
test('calculate the subtotal correctly when one condition makes it $0, and the other adds $10', function () {
|
||||
@@ -1065,7 +1065,7 @@
|
||||
|
||||
expect($this->cart->getTotal())->toEqual(90, 'Cart should have total of 90');
|
||||
|
||||
$this->cart->clear();
|
||||
$this->cart->clearItems();
|
||||
|
||||
$this->cart->add([
|
||||
'id' => 1,
|
||||
@@ -1099,7 +1099,7 @@
|
||||
|
||||
expect($this->cart->getTotal())->toEqual(90, 'Cart should have total of 90');
|
||||
|
||||
$this->cart->clear();
|
||||
$this->cart->clearItems();
|
||||
|
||||
$this->cart->add([
|
||||
'id' => 1,
|
||||
@@ -1201,7 +1201,7 @@
|
||||
|
||||
expect($this->cart->getConditions(active: true))->toHaveCount(0, 'Cart should have 0 conditions');
|
||||
|
||||
$this->cart->clear();
|
||||
$this->cart->clearItems();
|
||||
|
||||
$this->cart->add([
|
||||
'id' => 1,
|
||||
@@ -1310,7 +1310,7 @@
|
||||
|
||||
// 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'][0]->getName())->toEqual('Mother\'s Day Gift Pack');
|
||||
expect($this->cart->get(1)['conditions'][1]->getName())->toEqual('Mother\'s Day Gift Pack');
|
||||
});
|
||||
|
||||
test('can add item with multiple negative item conditions', function () {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Wearepixel\Cart\Cart;
|
||||
use Wearepixel\Cart\CartCondition;
|
||||
use Wearepixel\Cart\Coupons\Coupon;
|
||||
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||
use Wearepixel\Cart\Exceptions\InvalidCouponException;
|
||||
use Wearepixel\Cart\Shipping\ShippingRate;
|
||||
use Wearepixel\Cart\Tax\TaxRule;
|
||||
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||
|
||||
function makeCart(): Cart
|
||||
{
|
||||
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||
$events->shouldReceive('dispatch');
|
||||
|
||||
return new Cart(
|
||||
new SessionDriver(new SessionMock, 'TESTKEY'),
|
||||
$events,
|
||||
'cart',
|
||||
require __DIR__ . '/../Helpers/ConfigMock.php',
|
||||
);
|
||||
}
|
||||
|
||||
class TestCoupon extends Coupon
|
||||
{
|
||||
protected string $code = 'SAVE10';
|
||||
protected string $value = '-10%';
|
||||
public function isValid(): bool { return true; }
|
||||
}
|
||||
|
||||
class InvalidTestCoupon extends Coupon
|
||||
{
|
||||
protected string $code = 'EXPIRED';
|
||||
protected string $value = '-10%';
|
||||
public function isValid(): bool { return false; }
|
||||
}
|
||||
|
||||
class TestTax extends TaxRule
|
||||
{
|
||||
protected string $name = 'GST';
|
||||
protected string $value = '10%';
|
||||
public function isApplicable(): bool { return true; }
|
||||
}
|
||||
|
||||
class TestShipping extends ShippingRate
|
||||
{
|
||||
protected string $name = 'Flat Rate';
|
||||
protected string $value = '+10';
|
||||
public function isApplicable(): bool { return true; }
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
$this->cart = makeCart();
|
||||
$this->cart->add(1, 'Widget', 100.00, 1);
|
||||
});
|
||||
|
||||
afterEach(fn() => Mockery::close());
|
||||
|
||||
describe('Cart first-class objects', function () {
|
||||
test('Cart::coupon() applies condition from a Coupon instance', function () {
|
||||
$this->cart->coupon(new TestCoupon);
|
||||
|
||||
expect($this->cart->getCondition('SAVE10'))->toBeInstanceOf(CartCondition::class);
|
||||
expect($this->cart->getSubTotal())->toEqual(90.00);
|
||||
});
|
||||
|
||||
test('Cart::coupon() throws when coupon is invalid', function () {
|
||||
expect(fn() => $this->cart->coupon(new InvalidTestCoupon))
|
||||
->toThrow(InvalidCouponException::class);
|
||||
});
|
||||
|
||||
test('Cart::tax() applies condition from a TaxRule instance', function () {
|
||||
$this->cart->tax(new TestTax);
|
||||
|
||||
expect($this->cart->getCondition('GST'))->toBeInstanceOf(CartCondition::class);
|
||||
expect($this->cart->getSubTotal())->toEqual(110.00);
|
||||
});
|
||||
|
||||
test('Cart::shipping() applies condition from a ShippingRate instance', function () {
|
||||
$this->cart->shipping(new TestShipping);
|
||||
|
||||
expect($this->cart->getCondition('Flat Rate'))->toBeInstanceOf(CartCondition::class);
|
||||
expect($this->cart->getTotal())->toEqual(110.00);
|
||||
});
|
||||
|
||||
test('raw Cart::condition() still works alongside first-class objects', function () {
|
||||
$this->cart->coupon(new TestCoupon);
|
||||
$this->cart->condition(new CartCondition([
|
||||
'name' => 'Manual Discount',
|
||||
'type' => 'discount',
|
||||
'value' => '-5',
|
||||
'target' => 'subtotal',
|
||||
]));
|
||||
|
||||
expect($this->cart->getConditions()->count())->toBe(2);
|
||||
});
|
||||
});
|
||||
+14
-2
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Wearepixel\Cart\Cart;
|
||||
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||
use Wearepixel\Cart\Tests\Helpers\MockProduct;
|
||||
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||
|
||||
@@ -9,10 +10,9 @@
|
||||
$events->shouldReceive('dispatch');
|
||||
|
||||
$this->cart = new Cart(
|
||||
new SessionMock,
|
||||
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||
$events,
|
||||
'shopping',
|
||||
'SAMPLESESSIONKEY',
|
||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||
);
|
||||
});
|
||||
@@ -689,3 +689,15 @@
|
||||
expect($this->cart->getTotalQuantity())->toBeInt('Return type should be INT');
|
||||
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
|
||||
|
||||
use Wearepixel\Cart\Cart;
|
||||
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||
|
||||
beforeEach(function () {});
|
||||
@@ -9,15 +10,14 @@
|
||||
Mockery::close();
|
||||
});
|
||||
|
||||
test('event cart created', function () {
|
||||
test('event cart Created', function () {
|
||||
$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(
|
||||
new SessionMock,
|
||||
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||
$events,
|
||||
'cart',
|
||||
'SAMPLESESSIONKEY',
|
||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||
);
|
||||
|
||||
@@ -25,16 +25,15 @@
|
||||
});
|
||||
|
||||
test('event cart adding', function () {
|
||||
$events = Mockery::mock('Illuminate\Events\Dispatcher');
|
||||
$events->shouldReceive('dispatch')->once()->with('cart.created', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->once()->with('cart.adding', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->once()->with('cart.added', Mockery::type('array'), true);
|
||||
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Created', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Adding', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Added', Mockery::type('array'), true);
|
||||
|
||||
$cart = new Cart(
|
||||
new SessionMock,
|
||||
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||
$events,
|
||||
'cart',
|
||||
'SAMPLESESSIONKEY',
|
||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||
);
|
||||
|
||||
@@ -44,16 +43,15 @@
|
||||
});
|
||||
|
||||
test('event cart adding multiple times', function () {
|
||||
$events = Mockery::mock('Illuminate\Events\Dispatcher');
|
||||
$events->shouldReceive('dispatch')->once()->with('cart.created', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(2)->with('cart.adding', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(2)->with('cart.added', Mockery::type('array'), true);
|
||||
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Created', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(2)->with('LaravelCart.Adding', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(2)->with('LaravelCart.Added', Mockery::type('array'), true);
|
||||
|
||||
$cart = new Cart(
|
||||
new SessionMock,
|
||||
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||
$events,
|
||||
'cart',
|
||||
'SAMPLESESSIONKEY',
|
||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||
);
|
||||
|
||||
@@ -64,10 +62,10 @@
|
||||
});
|
||||
|
||||
test('event cart adding multiple times scenario two', function () {
|
||||
$events = Mockery::mock('Illuminate\Events\Dispatcher');
|
||||
$events->shouldReceive('dispatch')->once()->with('cart.created', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(3)->with('cart.adding', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(3)->with('cart.added', Mockery::type('array'), true);
|
||||
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Created', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(3)->with('LaravelCart.Adding', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(3)->with('LaravelCart.Added', Mockery::type('array'), true);
|
||||
|
||||
$items = [
|
||||
[
|
||||
@@ -94,10 +92,9 @@
|
||||
];
|
||||
|
||||
$cart = new Cart(
|
||||
new SessionMock,
|
||||
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||
$events,
|
||||
'cart',
|
||||
'SAMPLESESSIONKEY',
|
||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||
);
|
||||
|
||||
@@ -107,12 +104,12 @@
|
||||
});
|
||||
|
||||
test('event cart remove item', function () {
|
||||
$events = Mockery::mock('Illuminate\Events\Dispatcher');
|
||||
$events->shouldReceive('dispatch')->once()->with('cart.created', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(3)->with('cart.adding', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(3)->with('cart.added', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(1)->with('cart.removing', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(1)->with('cart.removed', Mockery::type('array'), true);
|
||||
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Created', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(3)->with('LaravelCart.Adding', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(3)->with('LaravelCart.Added', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(1)->with('LaravelCart.Removing', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(1)->with('LaravelCart.Removed', Mockery::type('array'), true);
|
||||
|
||||
$items = [
|
||||
[
|
||||
@@ -139,10 +136,9 @@
|
||||
];
|
||||
|
||||
$cart = new Cart(
|
||||
new SessionMock,
|
||||
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||
$events,
|
||||
'cart',
|
||||
'SAMPLESESSIONKEY',
|
||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||
);
|
||||
|
||||
@@ -154,12 +150,12 @@
|
||||
});
|
||||
|
||||
test('event cart clear', function () {
|
||||
$events = Mockery::mock('Illuminate\Events\Dispatcher');
|
||||
$events->shouldReceive('dispatch')->once()->with('cart.created', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(3)->with('cart.adding', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(3)->with('cart.added', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->once()->with('cart.clearing', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->once()->with('cart.cleared', Mockery::type('array'), true);
|
||||
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
|
||||
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Created', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(3)->with('LaravelCart.Adding', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->times(3)->with('LaravelCart.Added', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Clearing', Mockery::type('array'), true);
|
||||
$events->shouldReceive('dispatch')->once()->with('LaravelCart.Cleared', Mockery::type('array'), true);
|
||||
|
||||
$items = [
|
||||
[
|
||||
@@ -186,10 +182,9 @@
|
||||
];
|
||||
|
||||
$cart = new Cart(
|
||||
new SessionMock,
|
||||
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||
$events,
|
||||
'cart',
|
||||
'SAMPLESESSIONKEY',
|
||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Wearepixel\Cart\Cart;
|
||||
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||
|
||||
beforeEach(function () {
|
||||
@@ -8,18 +9,16 @@
|
||||
$events->shouldReceive('dispatch');
|
||||
|
||||
$this->cart1 = new Cart(
|
||||
new SessionMock,
|
||||
new SessionDriver(new SessionMock, 'uniquesessionkey123'),
|
||||
$events,
|
||||
'shopping',
|
||||
'uniquesessionkey123',
|
||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||
);
|
||||
|
||||
$this->cart2 = new Cart(
|
||||
new SessionMock,
|
||||
new SessionDriver(new SessionMock, 'uniquesessionkey456'),
|
||||
$events,
|
||||
'wishlist',
|
||||
'uniquesessionkey456',
|
||||
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\CartCondition;
|
||||
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||
use Wearepixel\Cart\Tests\Helpers\MockProduct;
|
||||
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||
|
||||
@@ -10,10 +11,9 @@
|
||||
$events->shouldReceive('dispatch');
|
||||
|
||||
$this->cart = new Cart(
|
||||
new SessionMock,
|
||||
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||
$events,
|
||||
'shopping',
|
||||
'SAMPLESESSIONKEY',
|
||||
require (__DIR__ . '/../Helpers/ConfigMock.php')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Wearepixel\Cart\Cart;
|
||||
use Wearepixel\Cart\Drivers\SessionDriver;
|
||||
use Wearepixel\Cart\Tests\Helpers\SessionMock;
|
||||
|
||||
beforeEach(function () {
|
||||
@@ -8,10 +9,9 @@
|
||||
$events->shouldReceive('dispatch');
|
||||
|
||||
$this->cart = new Cart(
|
||||
new SessionMock,
|
||||
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
|
||||
$events,
|
||||
'shopping',
|
||||
'SAMPLESESSIONKEY',
|
||||
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