Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.1] Custom line items (non-purchasable dependent) #3541

Draft
wants to merge 35 commits into
base: 5.1
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
85c9a64
WIP custom line items
nfourtythree Mar 5, 2024
e25e18b
Merge branch '5.0' into feature/5.x-custom-line-items
nfourtythree Mar 5, 2024
b7810c4
Update merge line items keys
nfourtythree Mar 5, 2024
74390d2
WIp more custom line item editing
nfourtythree Mar 5, 2024
096bc5a
adding a new custom line item on order edit
nfourtythree Mar 5, 2024
d05b27a
Merge branch '5.0' into feature/5.x-custom-line-items
nfourtythree Mar 6, 2024
9fe1473
Merge branch '5.1' into feature/5.x-custom-line-items
nfourtythree May 1, 2024
abb7d46
Merge branch '5.1' into feature/5.x-custom-line-items
nfourtythree May 1, 2024
683e0e3
Merge branch '5.1' into feature/5.x-custom-line-items
nfourtythree May 2, 2024
782f87d
Merge branch '5.x' into feature/5.x-custom-line-items
nfourtythree May 3, 2024
af7a5ba
Merge branch '5.1' into feature/5.x-custom-line-items
nfourtythree May 22, 2024
7142e7b
Switch line item type to enum
nfourtythree May 23, 2024
7caf1d4
Create `isPromotable`, `hasFreeShipping` properties on lineitems
nfourtythree May 24, 2024
b1c9504
Changelog update
nfourtythree May 24, 2024
140797a
Adding `hasFreeShipping` and `isPromotable` props to line items
nfourtythree May 29, 2024
85462ff
Use non-deprecated method
nfourtythree May 29, 2024
f746dbf
Merge branch '5.1' into feature/5.x-custom-line-items
nfourtythree May 30, 2024
a00c390
WIP is taxable is shippable
nfourtythree May 30, 2024
609b8e8
Add taxable and shippable to line items
nfourtythree May 30, 2024
10954fe
Rework creating and refreshing line items
nfourtythree Jun 3, 2024
77e43c5
fix cs
nfourtythree Jun 3, 2024
04b7d9b
No need for todo will be removed at next breaking change
nfourtythree Jun 3, 2024
4732a21
Update `LineItems::create()` signature, update deprecated uses of `cr…
nfourtythree Jun 4, 2024
e0c6768
Safe attributes
nfourtythree Jun 5, 2024
31c0cdd
Make sure default `qty` in `create()`
nfourtythree Jun 5, 2024
18d9e99
Merge branch '5.1' into feature/5.x-custom-line-items
nfourtythree Jun 6, 2024
7e52ce9
Fix missing columns on install
nfourtythree Jun 10, 2024
c9e3a8f
WIP front end add custom line item
nfourtythree Jun 10, 2024
4572f4e
Ability to add custom line items to the cart from the front end
nfourtythree Jun 11, 2024
4be292f
fix cs
nfourtythree Jun 11, 2024
0ec2afc
Merge branch '5.1' into feature/5.x-custom-line-items
nfourtythree Jun 11, 2024
43a2d7d
Merge branch '5.1' into feature/5.x-custom-line-items
nfourtythree Jun 12, 2024
9ac1d52
Fixed a bug in `TopProducts` stat
nfourtythree Jun 12, 2024
bebbb0c
Merge branch '5.1' into feature/5.x-custom-line-items
nfourtythree Jun 19, 2024
0bd060f
Build files
nfourtythree Jun 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
# Release Notes for Craft Commerce 5.1 (WIP)

## Unreleased 5.1
## 5.1.0

### Store Management
- It’s now possible to create custom line items.
- Added the `commerceCustomLineItem()` Twig function.

### Administration
- Added a new “Manage subscription plans” permission.
- Added a new “Manage donation settings” permission.
- Added a new “Manage store general setting” permission.
- Added a new “Manage payment currencies” permission.

### Development

### Extensibility
- Added `craft\commerce\enums\LineItemType`.
- Added `craft\commerce\helpers\LineItem::generateCustomLineItemHash()`.
- Added `craft\commerce\models\LineItem::$type`.
- Added `craft\commerce\models\LineItem::populate()`.
- Added `craft\commerce\models\LineItem::refresh()`.
- Added `craft\commerce\models\LineItem::getHasFreeShipping()`.
- Added `craft\commerce\models\LineItem::setHasFreeShipping()`.
- Added `craft\commerce\models\LineItem::getIsPromotable()`.
- Added `craft\commerce\models\LineItem::setIsPromotable()`.
- Added `craft\commerce\models\LineItem::getIsShippable()`.
- Added `craft\commerce\models\LineItem::setIsShippable()`.
- Added `craft\commerce\models\LineItem::getIsTaxable()`.
- Added `craft\commerce\models\LineItem::setIsTaxable()`.
- Added `craft\commerce\models\Order::EVENT_AFTER_LINE_ITEMS_REFRESHED`.
- Added `craft\commerce\models\Order::EVENT_BEFORE_LINE_ITEMS_REFRESHED`.
- Added `craft\commerce\services\LineItems::create()`.
- Added `craft\commerce\services\LineItems::resolveCustomLineItem()`.
- Deprecated `craft\commerce\models\LineItem::populateFromPurchasable()`. Use `populate()` instead.
- Deprecated `craft\commerce\models\LineItem::refreshFromPurchasable()`. Use `refresh()` instead.
- Deprecated `craft\commerce\services\LineItems::createLineItem()`. Use `create()` instead.

### System
- Craft Commerce now requires Craft CMS 5.2 or later.
- Craft Commerce now requires Craft CMS 5.2 or later.
2 changes: 1 addition & 1 deletion src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ public static function editions(): array
/**
* @inheritDoc
*/
public string $schemaVersion = '5.0.76';
public string $schemaVersion = '5.0.77';

/**
* @inheritdoc
Expand Down
3 changes: 1 addition & 2 deletions src/adjusters/Discount.php
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,7 @@ public function adjust(Order $order): array

// Remove non-promotable line items
$lineItemsByPrice = ArrayHelper::where($lineItemsByPrice, function(LineItem $lineItem) {
$purchasable = $lineItem->getPurchasable();
return $purchasable && $purchasable->getIsPromotable();
return $lineItem->getIsPromotable();
}, true, true);

// Loop over each order level adjustment and add an adjustment to each line item until it runs out.
Expand Down
10 changes: 5 additions & 5 deletions src/adjusters/Shipping.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ public function adjust(Order $order): array
$nonShippableItems = [];

foreach ($lineItems as $item) {
$purchasable = $item->getPurchasable();
if ($purchasable && !Plugin::getInstance()->getPurchasables()->isPurchasableShippable($purchasable, $order)) {
if (!$item->getIsShippable()) {
$nonShippableItems[$item->id] = $item->id;
}
}
Expand Down Expand Up @@ -128,9 +127,10 @@ public function adjust(Order $order): array
}
}

$freeShippingFlagOnProduct = $item->purchasable->hasFreeShipping();
$shippable = Plugin::getInstance()->getPurchasables()->isPurchasableShippable($item->getPurchasable(), $order);
if (!$freeShippingFlagOnProduct && !$hasFreeShippingFromDiscount && $shippable) {
$lineItemHasFreeShipping = $item->getHasFreeShipping();
$shippable = $item->getIsShippable();

if (!$lineItemHasFreeShipping && !$hasFreeShippingFromDiscount && $shippable) {
$adjustment = $this->_createAdjustment($shippingMethod, $rule);

$percentageRate = $rule->getPercentageRate($item->shippingCategoryId);
Expand Down
32 changes: 19 additions & 13 deletions src/base/ShippingMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use craft\commerce\elements\conditions\orders\ShippingMethodOrderCondition;
use craft\commerce\elements\Order;
use craft\commerce\errors\NotImplementedException;
use craft\commerce\Plugin;
use craft\helpers\Json;
use DateTime;
use Illuminate\Support\Collection;
Expand Down Expand Up @@ -230,10 +229,11 @@ public function getPriceForOrder(Order $order): float
$nonShippableItems = [];

foreach ($lineItems as $item) {
$purchasable = $item->getPurchasable();
if ($purchasable && !Plugin::getInstance()->getPurchasables()->isPurchasableShippable($purchasable, $order)) {
$nonShippableItems[$item->id] = $item->id;
if ($item->getIsShippable()) {
continue;
}

$nonShippableItems[$item->id] = $item->id;
}

// Are all line items non shippable items? No shipping cost.
Expand All @@ -244,17 +244,23 @@ public function getPriceForOrder(Order $order): float
$amount = $shippingRule->getBaseRate();

foreach ($order->getLineItems() as $item) {
if ($item->getPurchasable() && !$item->purchasable->hasFreeShipping() && Plugin::getInstance()->getPurchasables()->isPurchasableShippable($item->getPurchasable(), $order)) {
$percentageRate = $shippingRule->getPercentageRate($item->shippingCategoryId);
$perItemRate = $shippingRule->getPerItemRate($item->shippingCategoryId);
$weightRate = $shippingRule->getWeightRate($item->shippingCategoryId);

$percentageAmount = $item->getSubtotal() * $percentageRate;
$perItemAmount = $item->qty * $perItemRate;
$weightAmount = ($item->weight * $item->qty) * $weightRate;
if ($item->getHasFreeShipping()) {
continue;
}

$amount += ($percentageAmount + $perItemAmount + $weightAmount);
if (!$item->getIsShippable()) {
continue;
}

$percentageRate = $shippingRule->getPercentageRate($item->shippingCategoryId);
$perItemRate = $shippingRule->getPerItemRate($item->shippingCategoryId);
$weightRate = $shippingRule->getWeightRate($item->shippingCategoryId);

$percentageAmount = $item->getSubtotal() * $percentageRate;
$perItemAmount = $item->qty * $perItemRate;
$weightAmount = ($item->weight * $item->qty) * $weightRate;

$amount += ($percentageAmount + $perItemAmount + $weightAmount);
}

$amount = max($amount, $shippingRule->getMinRate());
Expand Down
48 changes: 46 additions & 2 deletions src/controllers/CartController.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use craft\elements\User;
use craft\errors\ElementNotFoundException;
use craft\errors\MissingComponentException;
use craft\helpers\Json;
use craft\helpers\UrlHelper;
use Illuminate\Support\Collection;
use Throwable;
Expand Down Expand Up @@ -160,7 +161,7 @@ public function actionUpdateCart(): ?Response
if ($qty > 0) {
// We only want a new line item if they cleared the cart
if ($clearLineItems) {
$lineItem = Plugin::getInstance()->getLineItems()->createLineItem($this->_cart, $purchasableId, $options);
$lineItem = Plugin::getInstance()->getLineItems()->create($this->_cart, compact('purchasableId', 'options'));
} else {
$lineItem = Plugin::getInstance()->getLineItems()->resolveLineItem($this->_cart, $purchasableId, $options);
}
Expand Down Expand Up @@ -212,7 +213,10 @@ public function actionUpdateCart(): ?Response

// We only want a new line item if they cleared the cart
if ($clearLineItems) {
$lineItem = Plugin::getInstance()->getLineItems()->createLineItem($this->_cart, $purchasable['id'], $purchasable['options']);
$lineItem = Plugin::getInstance()->getLineItems()->create($this->_cart, [
'purchasableId' => $purchasable['id'],
'options' => $purchasable['options'],
]);
} else {
$lineItem = Plugin::getInstance()->getLineItems()->resolveLineItem($this->_cart, $purchasable['id'], $purchasable['options']);
}
Expand All @@ -230,6 +234,46 @@ public function actionUpdateCart(): ?Response
}
}

if ($customLineItems = $this->request->getParam('customLineItems')) {
foreach ($customLineItems as $key => $customLineItem) {
$customLineItemData = $this->request->getValidatedBodyParam("customLineItems.$key.lineItem");
if (!$customLineItemData) {
continue;
}

$customLineItemData = Json::decodeIfJson($customLineItemData);
if (!is_array($customLineItemData) || !isset($customLineItemData['description'], $customLineItemData['price'], $customLineItemData['sku'])) {
continue;
}

$qty = (int)$this->request->getParam("customLineItems.$key.qty", 1);
if ($qty === 0) {
continue;
}

$note = $this->request->getParam("customLineItems.$key.note", '');
$options = $this->request->getParam("customLineItems.$key.options", []);

// Resolve custom line item
$customLineItem = Plugin::getInstance()->getLineItems()->resolveCustomLineItem($this->_cart, $customLineItemData['sku'], $options);

$customLineItem->description = $customLineItemData['description'];
$customLineItem->price = $customLineItemData['price'];
$customLineItem->sku = $customLineItemData['sku'];
$customLineItem->taxCategoryId = $customLineItemData['taxCategoryId'];
$customLineItem->shippingCategoryId = $customLineItemData['shippingCategoryId'];
$customLineItem->setHasFreeShipping($customLineItemData['hasFreeShipping']);
$customLineItem->setIsPromotable($customLineItemData['isPromotable']);
$customLineItem->setIsShippable($customLineItemData['isShippable']);
$customLineItem->setIsTaxable($customLineItemData['isTaxable']);
$customLineItem->qty = $qty;
$customLineItem->note = $note;
$customLineItem->setOptions($options);

$this->_cart->addLineItem($customLineItem);
}
}

// Update multiple line items in the cart
if ($lineItems = $this->request->getParam('lineItems')) {
foreach ($lineItems as $key => $lineItem) {
Expand Down
82 changes: 78 additions & 4 deletions src/controllers/OrdersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use craft\commerce\db\Table;
use craft\commerce\elements\Order;
use craft\commerce\enums\InventoryTransactionType;
use craft\commerce\enums\LineItemType;
use craft\commerce\errors\CurrencyException;
use craft\commerce\errors\OrderStatusException;
use craft\commerce\errors\RefundException;
Expand Down Expand Up @@ -59,6 +60,7 @@
use craft\helpers\StringHelper;
use craft\helpers\UrlHelper;
use craft\models\Site;
use craft\web\assets\money\MoneyAsset;
use craft\web\Controller;
use craft\web\View;
use DateTime;
Expand Down Expand Up @@ -598,6 +600,10 @@ private function _orderToArray(Order $order): array
$lineItems = $order->getLineItems();
$purchasableCpEditUrlByPurchasableId = [];
foreach ($lineItems as $lineItem) {
if ($lineItem->type === LineItemType::Custom) {
continue;
}

/** @var Purchasable|PurchasableElement|null $purchasable */
$purchasable = $lineItem->getPurchasable();
if (!$purchasable || isset($purchasableCpEditUrlByPurchasableId[$purchasable->id])) {
Expand Down Expand Up @@ -1336,6 +1342,8 @@ private function _registerJavascript(array $variables): void
/** @var Order $order */
$order = $variables['order'];
Craft::$app->getView()->registerAssetBundle(CommerceOrderAsset::class);
// Include the input mask asset for use in pricing fields
Craft::$app->getView()->registerAssetBundle(MoneyAsset::class);

Craft::$app->getView()->registerJs('window.orderEdit = {};', View::POS_BEGIN);

Expand All @@ -1352,12 +1360,22 @@ private function _registerJavascript(array $variables): void
$lineItemStatuses = Plugin::getInstance()->getLineItemStatuses()->getAllLineItemStatuses($order->storeId)->all();
Craft::$app->getView()->registerJs('window.orderEdit.lineItemStatuses = ' . Json::encode($lineItemStatuses) . ';', View::POS_BEGIN);

$lineItemTypes = LineItemType::types();

Craft::$app->getView()->registerJs('window.orderEdit.lineItemTypes = ' . Json::encode($lineItemTypes) . ';', View::POS_BEGIN);

$taxCategories = Plugin::getInstance()->getTaxCategories()->getAllTaxCategoriesAsList();
Craft::$app->getView()->registerJs('window.orderEdit.taxCategories = ' . Json::encode(ArrayHelper::toArray($taxCategories)) . ';', View::POS_BEGIN);

$defaultTaxCategoryId = Plugin::getInstance()->getTaxCategories()->getDefaultTaxCategory()->id;
Craft::$app->getView()->registerJs('window.orderEdit.defaultTaxCategoryId = ' . Json::encode($defaultTaxCategoryId) . ';', View::POS_BEGIN);

$shippingCategories = Plugin::getInstance()->getShippingCategories()->getAllShippingCategoriesAsList($order->storeId);
Craft::$app->getView()->registerJs('window.orderEdit.shippingCategories = ' . Json::encode(ArrayHelper::toArray($shippingCategories)) . ';', View::POS_BEGIN);

$defaultShippingCategoryId = Plugin::getInstance()->getShippingCategories()->getDefaultShippingCategory($order->storeId)->id;
Craft::$app->getView()->registerJs('window.orderEdit.defaultShippingCategoryId = ' . Json::encode($defaultShippingCategoryId) . ';', View::POS_BEGIN);

$currentUser = Craft::$app->getUser()->getIdentity();
$permissions = [
'commerce-manageOrders' => $currentUser->can('commerce-manageOrders'),
Expand Down Expand Up @@ -1541,26 +1559,48 @@ private function _updateOrder(Order $order, $orderRequestData, bool $tryAutoSet

foreach ($orderRequestData['order']['lineItems'] as $lineItemData) {
// Normalize data
$type = $lineItemData['type'] ?? LineItemType::Purchasable;
if (is_string($type)) {
$type = LineItemType::from($type);
} elseif (is_array($type) && isset($type['value'])) {
$type = LineItemType::from($type['value']);
}

$description = $lineItemData['description'] ?? null;
$sku = $lineItemData['sku'] ?? null;
$lineItemId = $lineItemData['id'] ?? null;
$note = $lineItemData['note'] ?? '';
$privateNote = $lineItemData['privateNote'] ?? '';
$purchasableId = $lineItemData['purchasableId'];
$lineItemStatusId = $lineItemData['lineItemStatusId'];
$options = $lineItemData['options'] ?? [];
$qty = $lineItemData['qty'] ?? 1;
$shippingCategoryId = $lineItemData['shippingCategoryId'] ?? null;
$taxCategoryId = $lineItemData['taxCategoryId'] ?? null;
$hasFreeShipping = $lineItemData['hasFreeShipping'] ?? null;
$isPromotable = $lineItemData['isPromotable'] ?? null;
$isShippable = $lineItemData['isShippable'] ?? null;
$isTaxable = $lineItemData['isTaxable'] ?? null;
$uid = $lineItemData['uid'] ?? StringHelper::UUID();

if ($lineItemId) {
$lineItem = Plugin::getInstance()->getLineItems()->getLineItemById($lineItemId);
} else {
try {
$lineItem = Plugin::getInstance()->getLineItems()->createLineItem($order, $purchasableId, $options, $qty, $note, $uid);
$params = compact('options', 'qty', 'note', 'uid');
if ($type === LineItemType::Purchasable) {
$params['purchasableId'] = $purchasableId;
}

$lineItem = Plugin::getInstance()->getLineItems()->create($order, $params, $type);
} catch (\Exception $exception) {
$order->addError('lineItems', $exception->getMessage());
continue;
}
}

$lineItem->type = $type;

$lineItem->purchasableId = $purchasableId;
$lineItem->qty = $qty;
$lineItem->note = $note;
Expand All @@ -1571,14 +1611,48 @@ private function _updateOrder(Order $order, $orderRequestData, bool $tryAutoSet

$lineItem->setOrder($order);

if ($lineItem->type === LineItemType::Custom) {
if ($description) {
$lineItem->setDescription($description);
}

if ($sku) {
$lineItem->setSku($sku);
}

if ($shippingCategoryId) {
$lineItem->shippingCategoryId = $shippingCategoryId;
}

if ($taxCategoryId) {
$lineItem->taxCategoryId = $taxCategoryId;
}

if ($hasFreeShipping !== null) {
$lineItem->setHasFreeShipping($hasFreeShipping);
}

if ($isPromotable !== null) {
$lineItem->setIsPromotable($isPromotable);
}

if ($isShippable !== null) {
$lineItem->setIsShippable($isShippable);
}

if ($isTaxable !== null) {
$lineItem->setIsTaxable($isTaxable);
}
}

// Deleted a purchasable while we had a purchasable ID in memory on the order edit page, unset it.
if ($purchasableId && !Plugin::getInstance()->getPurchasables()->getPurchasableById($purchasableId, $orderRequestData['order']['orderSiteId'], $orderRequestData['order']['customerId'] ?? false)) {
if ($lineItem->type === LineItemType::Purchasable && $purchasableId && !Plugin::getInstance()->getPurchasables()->getPurchasableById($purchasableId, $orderRequestData['order']['orderSiteId'], $orderRequestData['order']['customerId'] ?? false)) {
$lineItem->purchasableId = null;
}

if ($order->getRecalculationMode() == Order::RECALCULATION_MODE_NONE) {
if ($order->getRecalculationMode() == Order::RECALCULATION_MODE_NONE || $lineItem->type === LineItemType::Custom) {
$promotionalPrice = $lineItemData['promotionalPrice'] ? Localization::normalizeNumber($lineItemData['promotionalPrice']) : null;
$price = $lineItemData['price'] ? Localization::normalizeNumber($lineItemData['price']) : null;
$price = $lineItemData['price'] ? Localization::normalizeNumber($lineItemData['price']) : 0;

$lineItem->setPromotionalPrice($promotionalPrice);
$lineItem->setPrice($price);
Expand Down
Loading
Loading