Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ttempleton committed Sep 25, 2018
0 parents commit c191d32
Show file tree
Hide file tree
Showing 13 changed files with 619 additions and 0 deletions.
40 changes: 40 additions & 0 deletions LICENSE.md
@@ -0,0 +1,40 @@
Copyright © Spicy Web

Permission is hereby granted to any person obtaining a copy of this software
(the “Software”) to use, copy, modify, merge, publish and/or distribute copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

1. **Don’t plagiarize.** The above copyright notice and this license shall be
included in all copies or substantial portions of the Software.

2. **Don’t use the same license on more than one project.** Each licensed copy
of the Software shall be actively installed in no more than one production
environment at a time.

3. **Don’t mess with the licensing features.** Software features related to
licensing shall not be altered or circumvented in any way, including (but
not limited to) license validation, payment prompts, feature restrictions,
and update eligibility.

4. **Pay up.** Payment shall be made immediately upon receipt of any notice,
prompt, reminder, or other message indicating that a payment is owed.

5. **Follow the law.** All use of the Software shall not violate any applicable
law or regulation, nor infringe the rights of any other person or entity.

Failure to comply with the foregoing conditions will automatically and
immediately result in termination of the permission granted hereby. This
license does not include any right to receive updates to the Software or
technical support. Licensees bear all risk related to the quality and
performance of the Software and any modifications made or obtained to it,
including liability for actual and consequential harm, such as loss or
corruption of data, and any necessary service, repair, or correction.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
LIABILITY, INCLUDING SPECIAL, INCIDENTAL AND CONSEQUENTIAL DAMAGES, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
42 changes: 42 additions & 0 deletions README.md
@@ -0,0 +1,42 @@
<img src="docs/icon.png" width="60">

# ReOrder

#### Easy reordering of previous Craft Commerce user orders.

ReOrder makes it easy to allow users to quickly replicate an old order's line items in their existing cart, including the items' quantities, product options and notes.

## Requirements

- Craft CMS 3.0.0 or newer
- Craft Commerce 2.0.0-beta.5 or newer

## Installation

ReOrder can be installed through the Craft [Plugin Store](https://plugins.craftcms.com/). It can also be set up using Composer:

```
composer require spicyweb/craft-reorder
```

Then browse to **Settings &rarr; Plugins** in the Craft control panel and choose to install ReOrder.

## Usage

ReOrder can be configured to either keep or discard the existing cart items when reordering an old order, and to allow or disallow reordering an order if not all of the associated purchasables are still available, whether they have been deleted, disabled or are just out of stock -- in which case, if reordering is allowed, ReOrder will just replicate the available items.

These options can be configured globally in the Craft control panel and can be overridden on a case-by-case basis in your template files.

#### Example: retain cart but disallow partial reorders

```twig
<form method="POST">
<input type="hidden" name="action" value="reorder/reorder">
{{ csrfInput() }}
{{ redirectInput('shop/checkout') }}
<input type="hidden" name="order" value="{{ order.number }}">
<input type="hidden" name="retainCart" value="1">
<input type="hidden" name="allowPartial" value="0">
<button type="submit">ReOrder!</button>
</form>
```
35 changes: 35 additions & 0 deletions composer.json
@@ -0,0 +1,35 @@
{
"name": "spicyweb/craft-reorder",
"description": "Easy reordering of previous Craft Commerce user orders",
"version": "1.0.0",
"type": "craft-plugin",
"keywords": [
"craftcms",
"craft-plugin",
"commerce"
],
"license": "proprietary",
"homepage": "https://spicyweb.com.au/",
"support": {
"email": "craft@spicyweb.com.au",
"issues": "https://github.com/spicywebau/craft-reorder/issues"
},
"require": {
"craftcms/cms": "^3.0.0",
"craftcms/commerce": "^2.0.0-beta.5"
},
"autoload": {
"psr-4": {
"spicyweb\\reorder\\": "src/"
}
},
"extra": {
"handle": "reorder",
"name": "ReOrder",
"class": "spicyweb\\reorder\\Plugin",
"developer": "Spicy Web",
"developerUrl": "https://spicyweb.com.au/",
"changelogUrl": "https://github.com/spicywebau/craft-reorder/blob/master/CHANGELOG.md",
"downloadUrl": "https://github.com/spicywebau/craft-reorder/archive/master.zip"
}
}
Binary file added docs/icon.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
67 changes: 67 additions & 0 deletions src/Plugin.php
@@ -0,0 +1,67 @@
<?php
namespace spicyweb\reorder;

use yii\base\Event;

use Craft;
use craft\base\Plugin as BasePlugin;
use craft\web\twig\variables\CraftVariable;

use spicyweb\reorder\models\Settings;

/**
* Class Plugin
*
* @package spicyweb\reorder
* @author Spicy Web <craft@spicyweb.com.au>
* @since 1.0.0
*/
class Plugin extends BasePlugin
{
/**
* @var Plugin
*/
public static $plugin;

/**
* @inheritdoc
*/
public $hasCpSettings = true;

/**
* @inheritdoc
*/
public function init()
{
parent::init();

self::$plugin = $this;

$this->setComponents([
'methods' => Service::class,
]);

Event::on(CraftVariable::class, CraftVariable::EVENT_INIT, function(Event $event)
{
$event->sender->set('reorder', Variable::class);
});
}

/**
* @inheritdoc
*/
protected function createSettingsModel(): Settings
{
return new Settings();
}

/**
* @inheritdoc
*/
protected function settingsHtml(): string
{
return Craft::$app->getView()->renderTemplate('reorder/settings', [
'settings' => $this->getSettings(),
]);
}
}
183 changes: 183 additions & 0 deletions src/Service.php
@@ -0,0 +1,183 @@
<?php
namespace spicyweb\reorder;

use yii\base\Component;

use Craft;
use craft\base\Element;
use craft\commerce\Plugin as Commerce;
use craft\commerce\elements\Order;
use craft\commerce\elements\Variant;
use craft\commerce\models\LineItem;

use spicyweb\reorder\enums\LineItemStatus;

/**
* Class Service
*
* @package spicyweb\reorder
* @author Spicy Web <craft@spicyweb.com.au>
* @since 1.0.0
*/
class Service extends Component
{
/**
* Copies line items from an order to the user's cart.
*
* @param Order $order The order from which the line items will be copied.
* @param bool $allowPartial Whether to allow partially available items, e.g. if there's insufficient stock.
* @return bool Whether the line items were successfully copied.
*/
public function copyLineItems(Order $order, bool $allowPartial = false): bool
{
$commerce = Commerce::getInstance();
$cart = $commerce->getCarts()->getCart();

foreach ($order->lineItems as $item)
{
$itemStatus = $this->_getLineItemStatus($item, $cart->id);
$itemAvailable = $itemStatus === LineItemStatus::Available;
$itemInsufficientStock = $itemStatus === LineItemStatus::InsufficientStock;
$itemAboveMaxQty = $itemStatus === LineItemStatus::AboveMaxQty;
$itemPartiallyAvailable = $allowPartial && ($itemInsufficientStock || $itemAboveMaxQty);

if ($itemAvailable || $itemPartiallyAvailable)
{
$purchasable = $item->getPurchasable();
$qty = $itemAboveMaxQty ? $purchasable->maxQty :
($itemInsufficientStock ? $purchasable->stock : $item->qty);

$lineItem = $commerce->getLineItems()->resolveLineItem(
$cart->id,
$purchasable->id,
$item->options,
$qty,
$item->note
);

// If the item had insufficient stock but was already in the cart, its quantity will now exceed the
// available stock and will need to be reset to the available stock.
if ($itemInsufficientStock && $lineItem->qty > $purchasable->stock)
{
$lineItem->qty = $purchasable->stock;
}

// If the item quantity exceeded the maximum for that purchasable but was already in the cart, its
// quantity will now exceed the maximum and will need to be reset to the maximum.
if ($itemAboveMaxQty && $lineItem->qty > $purchasable->maxQty)
{
$lineItem->qty = $purchasable->maxQty;
}

$cart->addLineItem($lineItem);
}
}

return $cart->validate() && Craft::$app->getElements()->saveElement($cart, false);
}

/**
* Checks an order's line items and returns the unavailable items along with why they're unavailable.
*
* @param Order $order The order to check.
* @param int $cartId A cart ID, to check for the quantity of items already in the user's cart.
* return array The line items that are unavailable and why.
*/
public function getUnavailableLineItems(Order $order, int $cartId = null): array
{
$unavailableLineItems = [];

foreach ($order->lineItems as $item)
{
$itemStatus = $this->_getLineItemStatus($item, $cartId);

if ($itemStatus !== LineItemStatus::Available)
{
$itemData = [
'lineItem' => $item,
'status' => $itemStatus,
];

array_push($unavailableLineItems, $itemData);
}
}

return $unavailableLineItems;
}

/**
* Checks whether a line item's purchasable is available.
*
* @param LineItem $lineItem The line item to check.
* @param int $cartId A cart ID, to check for the quantity of items already in the user's cart.
* @return string The line item status.
*/
private function _getLineItemStatus(LineItem $lineItem, int $cartId = null): string
{
$commerce = Commerce::getInstance();
$purchasable = $lineItem->getPurchasable();

if ($purchasable === null)
{
return LineItemStatus::Deleted;
}

if ($purchasable instanceof Element && $purchasable->getStatus() !== Element::STATUS_ENABLED)
{
return LineItemStatus::Disabled;
}

if ($purchasable instanceof Variant)
{
$qty = $lineItem->qty;

// Make sure any item quantity checks take into account what's already in the user's cart.
if ($cartId !== null)
{
$cartItem = $commerce->getLineItems()->resolveLineItem(
$cartId,
$purchasable->id,
$lineItem->options,
$qty,
$lineItem->note
);

// Only count the cart item quantity if the item has an ID (and is therefore actually in the cart).
if ($cartItem->id !== null)
{
$qty += $cartItem->qty;
}
}

$minQty = $purchasable->minQty;
$maxQty = $purchasable->maxQty;

if ($minQty !== null && $qty < $minQty)
{
return LineItemStatus::BelowMinQty;
}

if ($maxQty !== null && $qty > $maxQty)
{
return LineItemStatus::AboveMaxQty;
}

if (!$purchasable->hasUnlimitedStock)
{
$stock = (int)$purchasable->stock;

if ($stock === 0)
{
return LineItemStatus::OutOfStock;
}

if ($qty > $stock)
{
return LineItemStatus::InsufficientStock;
}
}
}

return LineItemStatus::Available;
}
}

0 comments on commit c191d32

Please sign in to comment.