diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT-V3.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT-V3.yml index ddafcd4d47..5ef974b047 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT-V3.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT-V3.yml @@ -2,8 +2,9 @@ name: Bug Report – Commerce 3 description: Report an issue or unexpected behavior pertaining to Commerce 3 title: '[3.x]: ' labels: - - 🐞 bug - commerce3 + - Craft Commerce # Linear + - bug # Linear body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT-V4.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT-V4.yml index c1e737e08e..89180561bf 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT-V4.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT-V4.yml @@ -2,8 +2,9 @@ name: Bug Report – Commerce 4 description: Report an issue or unexpected behavior pertaining to Commerce 4 title: '[4.x]: ' labels: - - 🐞 bug - commerce4 + - Craft Commerce # Linear + - bug # Linear body: - type: markdown attributes: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b8eb695d7..6690daf3f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Release Notes for Craft Commerce +## 4.5.3 - 2024-04-02 + +- The “Postcode Condition Formula” condition rule now allows multi-line input. ([#3147](https://github.com/craftcms/commerce/issues/3147) +- Fixed a bug where order adjustments could be duplicated. ([#3438](https://github.com/craftcms/commerce/issues/3438)) +- Fixed a bug where successful refunds could show a failure notice. + +## 4.5.2 - 2024-03-06 + +- Fixed a bug where order status sources weren’t showing count badges on the Orders index page. ([#3397](https://github.com/craftcms/commerce/issues/3397)) +- Fixed a bug where discounts weren’t listing more than 128 related purchasables or categories. ([#3379](https://github.com/craftcms/commerce/issues/3379)) + ## 4.5.1.1 - 2024-03-01 - Fixed a bug where the “Share cart” order index action wasn’t working. diff --git a/src/Plugin.php b/src/Plugin.php index c38bcc2e4b..ef6a91363c 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -207,7 +207,7 @@ public static function editions(): array /** * @inheritDoc */ - public string $schemaVersion = '4.5.0'; + public string $schemaVersion = '4.5.1'; /** * @inheritdoc diff --git a/src/controllers/CartController.php b/src/controllers/CartController.php index 4e0eb14555..0407df92d7 100644 --- a/src/controllers/CartController.php +++ b/src/controllers/CartController.php @@ -24,6 +24,7 @@ use Throwable; use yii\base\Exception; use yii\base\InvalidConfigException; +use yii\mutex\Mutex; use yii\web\BadRequestHttpException; use yii\web\HttpException; use yii\web\NotFoundHttpException; @@ -52,6 +53,16 @@ class CartController extends BaseFrontEndController */ protected ?User $_currentUser = null; + /** + * @var Mutex|null + */ + private ?Mutex $_mutex = null; + + /** + * @var string|null + */ + private ?string $_mutexLockName = null; + /** * @throws InvalidConfigException */ @@ -92,10 +103,36 @@ public function actionUpdateCart(): ?Response { $this->requirePostRequest(); $isSiteRequest = $this->request->getIsSiteRequest(); + $isConsoleRequest = $this->request->getIsConsoleRequest(); $currentUser = Craft::$app->getUser()->getIdentity(); /** @var Plugin $plugin */ $plugin = Plugin::getInstance(); + $useMutex = (!$isConsoleRequest && Craft::$app->getRequest()->getBodyParam('number')) || (!$isConsoleRequest && $plugin->getCarts()->getHasSessionCartNumber()); + + if ($useMutex) { + $lockOrderNumber = null; + if ($bodyNumber = Craft::$app->getRequest()->getBodyParam('number')) { + $lockOrderNumber = $bodyNumber; + } elseif (!$isConsoleRequest) { + $request = Craft::$app->getRequest(); + $requestCookies = $request->getCookies(); + $cookieNumber = $requestCookies->getValue($plugin->getCarts()->cartCookie['name']); + + if ($cookieNumber) { + $lockOrderNumber = $cookieNumber; + } + } + + if ($lockOrderNumber) { + $this->_mutexLockName = "order:$lockOrderNumber"; + $this->_mutex = Craft::$app->getMutex(); + if (!$this->_mutex->acquire($this->_mutexLockName, 5)) { + throw new Exception('Unable to acquire a lock for saving of Order: ' . $lockOrderNumber); + } + } + } + // Get the cart from the request or from the session. // When we are about to update the cart, we consider it a real cart at this point, and want to actually create it in the DB. $this->_cart = $this->_getCart(true); @@ -489,6 +526,10 @@ private function _returnCart(): ?Response $error = Craft::t('commerce', 'Unable to update cart.'); $message = $this->request->getValidatedBodyParam('failMessage') ?? $error; + if ($this->_mutex && $this->_mutexLockName) { + $this->_mutex->release($this->_mutexLockName); + } + return $this->asModelFailure( $this->_cart, $message, @@ -509,6 +550,10 @@ private function _returnCart(): ?Response $this->_cartVariable => $this->_cart, ]); + if ($this->_mutex && $this->_mutexLockName) { + $this->_mutex->release($this->_mutexLockName); + } + return $this->asModelSuccess( $this->_cart, $message, diff --git a/src/controllers/OrdersController.php b/src/controllers/OrdersController.php index ee0a70b8ba..567543cf30 100644 --- a/src/controllers/OrdersController.php +++ b/src/controllers/OrdersController.php @@ -1039,7 +1039,7 @@ public function actionTransactionRefund(): Response $message = $child->message ? ' (' . $child->message . ')' : ''; - if ($child->status == TransactionRecord::STATUS_SUCCESS) { + if ($child->status == TransactionRecord::STATUS_SUCCESS || $child->status == TransactionRecord::STATUS_PROCESSING) { $child->order->updateOrderPaidInformation(); $this->setSuccessFlash(Craft::t('commerce', 'Transaction refunded successfully: {message}', [ 'message' => $message, @@ -1210,7 +1210,7 @@ private function _registerJavascript(array $variables): void $shippingCategories = Plugin::getInstance()->getShippingCategories()->getAllShippingCategoriesAsList(); Craft::$app->getView()->registerJs('window.orderEdit.shippingCategories = ' . Json::encode(ArrayHelper::toArray($shippingCategories)) . ';', View::POS_BEGIN); - + $currentUser = Craft::$app->getUser()->getIdentity(); $permissions = [ 'commerce-manageOrders' => $currentUser->can('commerce-manageOrders'), diff --git a/src/controllers/PaymentsController.php b/src/controllers/PaymentsController.php index e767fddc8d..be64544f8e 100644 --- a/src/controllers/PaymentsController.php +++ b/src/controllers/PaymentsController.php @@ -81,6 +81,32 @@ public function actionPay(): ?Response $number = $this->request->getParam('number'); + $useMutex = $number || (!$isCpRequest && $plugin->getCarts()->getHasSessionCartNumber()); + + if ($useMutex) { + $lockOrderNumber = null; + if ($number) { + $lockOrderNumber = $number; + } elseif (!$isCpRequest) { + $request = Craft::$app->getRequest(); + $requestCookies = $request->getCookies(); + $cookieNumber = $requestCookies->getValue($plugin->getCarts()->cartCookie['name']); + + if ($cookieNumber) { + $lockOrderNumber = $cookieNumber; + } + } + + if ($lockOrderNumber) { + $lockName = "order:$lockOrderNumber"; + $mutex = Craft::$app->getMutex(); + if (!$mutex->acquire($lockName, 5)) { + throw new Exception('Unable to acquire a lock for saving of Order: ' . $lockOrderNumber); + } + } + } + + if ($number !== null) { $order = $plugin->getOrders()->getOrderByNumber($number); @@ -375,6 +401,10 @@ public function actionPay(): ?Response $error = Craft::t('commerce', 'Something changed with the order before payment, please review your order and submit payment again.'); + if ($useMutex && isset($mutex, $lockName)) { + $mutex->release($lockName); + } + return $this->asModelFailure( $paymentForm, $error, @@ -390,6 +420,10 @@ public function actionPay(): ?Response } } + if ($useMutex && isset($mutex, $lockName)) { + $mutex->release($lockName); + } + $redirect = ''; $redirectData = []; $transaction = null; diff --git a/src/elements/Order.php b/src/elements/Order.php index 52e9ce6f9f..78250477d3 100644 --- a/src/elements/Order.php +++ b/src/elements/Order.php @@ -3399,6 +3399,12 @@ private function _saveAdjustments(): void $previousAdjustment->delete(); } } + + // Make sure all other adjustments have been cleaned up. + Db::delete( + Table::ORDERADJUSTMENTS, + ['and', ['orderId' => $this->id], ['not', ['id' => $newAdjustmentIds]]] + ); } diff --git a/src/elements/conditions/addresses/PostalCodeFormulaConditionRule.php b/src/elements/conditions/addresses/PostalCodeFormulaConditionRule.php index fee5391758..c320d3b6c2 100644 --- a/src/elements/conditions/addresses/PostalCodeFormulaConditionRule.php +++ b/src/elements/conditions/addresses/PostalCodeFormulaConditionRule.php @@ -22,11 +22,17 @@ */ class PostalCodeFormulaConditionRule extends BaseTextConditionRule implements ElementConditionRuleInterface { + /** + * @inheritdoc + */ public function getLabel(): string { return Craft::t('commerce', 'Postal Code Formula'); } + /** + * @inheritdoc + */ public function getExclusiveQueryParams(): array { return []; @@ -70,14 +76,14 @@ public function operators(): array public function inputHtml(): string { return Html::hiddenLabel($this->getLabel(), 'value') . - Cp::textHtml([ - 'type' => $this->inputType(), - 'id' => 'value', - 'name' => 'value', - 'code' => 'value', - 'value' => $this->value, - 'autocomplete' => false, - 'class' => 'fullwidth code', - ]); + Cp::textareaHtml([ + 'type' => $this->inputType(), + 'id' => 'value', + 'name' => 'value', + 'code' => 'value', + 'value' => $this->value, + 'autocomplete' => false, + 'class' => 'fullwidth code', + ]); } } diff --git a/src/elements/traits/OrderElementTrait.php b/src/elements/traits/OrderElementTrait.php index 7538661a54..c966de7a20 100644 --- a/src/elements/traits/OrderElementTrait.php +++ b/src/elements/traits/OrderElementTrait.php @@ -301,7 +301,6 @@ protected static function defineSources(string $context = null): array 'label' => Craft::t('commerce', 'All Orders'), 'criteria' => ['isCompleted' => true], 'defaultSort' => ['dateOrdered', 'desc'], - 'badgeCount' => 0, 'data' => [ 'date-attr' => 'dateOrdered', ], @@ -320,7 +319,6 @@ protected static function defineSources(string $context = null): array 'label' => Craft::t('site', $orderStatus->name), 'criteria' => $criteriaStatus, 'defaultSort' => ['dateOrdered', 'desc'], - 'badgeCount' => 0, 'data' => [ 'handle' => $orderStatus->handle, 'date-attr' => 'dateOrdered', diff --git a/src/migrations/Install.php b/src/migrations/Install.php index b1bf11f8f8..579a4af0f3 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -169,7 +169,9 @@ public function createTables(): void 'hasFreeShippingForMatchingItems' => $this->boolean()->notNull()->defaultValue(false), 'hasFreeShippingForOrder' => $this->boolean()->notNull()->defaultValue(false), 'allPurchasables' => $this->boolean()->notNull()->defaultValue(false), + 'purchasableIds' => $this->text(), 'allCategories' => $this->boolean()->notNull()->defaultValue(false), + 'categoryIds' => $this->text(), 'appliedTo' => $this->enum('appliedTo', ['matchingLineItems', 'allLineItems'])->notNull()->defaultValue('matchingLineItems'), 'categoryRelationshipType' => $this->enum('categoryRelationshipType', ['element', 'sourceElement', 'targetElement'])->notNull()->defaultValue('element'), 'orderConditionFormula' => $this->text(), diff --git a/src/migrations/m240306_091057_move_element_ids_on_discount_to_columns.php b/src/migrations/m240306_091057_move_element_ids_on_discount_to_columns.php new file mode 100644 index 0000000000..37f7622df3 --- /dev/null +++ b/src/migrations/m240306_091057_move_element_ids_on_discount_to_columns.php @@ -0,0 +1,63 @@ +addColumn($discountsTable, 'purchasableIds', $this->text()->after('allPurchasables')); + $this->addColumn($discountsTable, 'categoryIds', $this->text()->after('allCategories')); + + $purchasableIdsByDiscountId = (new Query()) + ->select(['discountId', 'purchasableId']) + ->from([$discountPurchasablesTables]) + ->collect(); + + $purchasableIdsByDiscountId = $purchasableIdsByDiscountId->groupBy('discountId')->map(function($row) { + return array_column($row->toArray(), 'purchasableId'); + }); + + $categoryIdsByDiscountId = (new Query()) + ->select(['discountId', 'categoryId']) + ->from([$discountCategoriesTable]) + ->collect(); + + $categoryIdsByDiscountId = $categoryIdsByDiscountId->groupBy('discountId')->map(function($row) { + return array_column($row->toArray(), 'categoryId'); + }); + + foreach ($purchasableIdsByDiscountId as $discountId => $purchasableIds) { + $this->update($discountsTable, ['purchasableIds' => Json::encode($purchasableIds)], ['id' => $discountId]); + } + + foreach ($categoryIdsByDiscountId as $discountId => $categoryIds) { + $this->update($discountsTable, ['categoryIds' => Json::encode($categoryIds)], ['id' => $discountId]); + } + + return true; + } + + /** + * @inheritdoc + */ + public function safeDown(): bool + { + echo "m240306_091057_move_element_ids_on_discount_to_columns cannot be reverted.\n"; + return false; + } +} diff --git a/src/records/Discount.php b/src/records/Discount.php index 3dcfaa9a05..bbf06f9605 100644 --- a/src/records/Discount.php +++ b/src/records/Discount.php @@ -18,7 +18,9 @@ * Discount record. * * @property bool $allCategories + * @property ?array $categoryIds * @property bool $allPurchasables + * @property ?array $purchasableIds * @property float $baseDiscount * @property float $purchaseTotal * @property string $baseDiscountType diff --git a/src/services/Discounts.php b/src/services/Discounts.php index 56a1e2575d..b65e19e02f 100644 --- a/src/services/Discounts.php +++ b/src/services/Discounts.php @@ -34,7 +34,7 @@ use craft\helpers\ArrayHelper; use craft\helpers\DateTimeHelper; use craft\helpers\Db; -use craft\helpers\StringHelper; +use craft\helpers\Json; use DateTime; use Throwable; use Twig\Error\LoaderError; @@ -741,6 +741,8 @@ public function saveDiscount(Discount $model, bool $runValidation = true): bool $record->totalDiscountUseLimit = $model->totalDiscountUseLimit; $record->ignoreSales = $model->ignoreSales; $record->appliedTo = $model->appliedTo; + $record->purchasableIds = $model->getPurchasableIds(); + $record->categoryIds = $model->getCategoryIds(); // If the discount is new, set the sort order to be at the top of the list. // We will ensure the sort orders are sequential when we save the discount. @@ -752,9 +754,11 @@ public function saveDiscount(Discount $model, bool $runValidation = true): bool $record->categoryRelationshipType = $model->categoryRelationshipType; if ($record->allCategories = $model->allCategories) { $model->setCategoryIds([]); + $record->categoryIds = null; } if ($record->allPurchasables = $model->allPurchasables) { $model->setPurchasableIds([]); + $record->purchasableIds = null; } $db = Craft::$app->getDb(); @@ -1234,9 +1238,10 @@ private function _populateDiscounts(array $discounts): array { foreach ($discounts as &$discount) { // @TODO remove this when we can widen the accepted params on the setters - $discount['purchasableIds'] = !empty($discount['purchasableIds']) ? StringHelper::split($discount['purchasableIds']) : []; + + $discount['purchasableIds'] = !empty($discount['purchasableIds']) ? Json::decodeIfJson($discount['purchasableIds'], true) : []; // IDs can be either category ID or entry ID due to the entryfication - $discount['categoryIds'] = !empty($discount['categoryIds']) ? StringHelper::split($discount['categoryIds']) : []; + $discount['categoryIds'] = !empty($discount['categoryIds']) ? Json::decodeIfJson($discount['categoryIds'], true) : []; $discount['orderCondition'] = $discount['orderCondition'] ?? ''; $discount['customerCondition'] = $discount['customerCondition'] ?? ''; $discount['billingAddressCondition'] = $discount['billingAddressCondition'] ?? ''; @@ -1294,6 +1299,8 @@ private function _createDiscountQuery(): Query '[[discounts.customerCondition]]', '[[discounts.shippingAddressCondition]]', '[[discounts.billingAddressCondition]]', + '[[discounts.purchasableIds]]', + '[[discounts.categoryIds]]', ]) ->from(['discounts' => Table::DISCOUNTS]) ->orderBy(['sortOrder' => SORT_ASC]) @@ -1301,18 +1308,6 @@ private function _createDiscountQuery(): Query ->leftJoin(Table::DISCOUNT_CATEGORIES . ' dpt', '[[dpt.discountId]]=[[discounts.id]]') ->groupBy(['discounts.id']); - if (Craft::$app->getDb()->getIsPgsql()) { - $query->addSelect([ - 'purchasableIds' => new Expression("STRING_AGG([[dp.purchasableId]]::text, ',')"), - 'categoryIds' => new Expression("STRING_AGG([[dpt.categoryId]]::text, ',')"), - ]); - } else { - $query->addSelect([ - 'purchasableIds' => new Expression('GROUP_CONCAT([[dp.purchasableId]])'), - 'categoryIds' => new Expression('GROUP_CONCAT([[dpt.categoryId]])'), - ]); - } - return $query; } } diff --git a/src/translations/en/commerce.php b/src/translations/en/commerce.php index 6e6b95da0c..9a5f6e4491 100644 --- a/src/translations/en/commerce.php +++ b/src/translations/en/commerce.php @@ -509,7 +509,6 @@ 'Invalid formula syntax' => 'Invalid formula syntax', 'Invalid gateway: {value}' => 'Invalid gateway: {value}', 'Invalid order condition syntax.' => 'Invalid order condition syntax.', - 'Invalid page number.' => 'Invalid page number.', 'Invalid payment or order. Please review.' => 'Invalid payment or order. Please review.', 'Invalid payment source ID: {value}' => 'Invalid payment source ID: {value}', 'Invoice amount' => 'Invoice amount', @@ -951,7 +950,6 @@ 'Switch' => 'Switch', 'System Settings' => 'System Settings', 'System' => 'System', - 'Tax & Shipping (Lite)' => 'Tax & Shipping (Lite)', 'Tax & Shipping' => 'Tax & Shipping', 'Tax (inc)' => 'Tax (inc)', 'Tax Categories' => 'Tax Categories', diff --git a/src/web/assets/commercecp/dist/commercecp.js b/src/web/assets/commercecp/dist/commercecp.js index 5ef033ae22..a15f50c7d5 100644 --- a/src/web/assets/commercecp/dist/commercecp.js +++ b/src/web/assets/commercecp/dist/commercecp.js @@ -1,2 +1,2 @@ -!function(){var t={528:function(){function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t(e)}!function(e){"undefined"===t(Craft.Commerce)&&(Craft.Commerce={}),Craft.Commerce.initUnlimitedStockCheckbox=function(t){t.find("input.unlimited-stock:first").change(Craft.Commerce.handleUnlimitedStockCheckboxChange)},Craft.Commerce.handleUnlimitedStockCheckboxChange=function(t){var n=e(t.currentTarget),a=n.parent().prevAll(".textwrapper:first").children(".text:first");n.prop("checked")?a.prop("disabled",!0).addClass("disabled").val(""):a.prop("disabled",!1).removeClass("disabled").focus()}}(jQuery)},322:function(){function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t(e)}"undefined"===t(Craft.Commerce)&&(Craft.Commerce={}),Craft.Commerce.OrderEdit=Garnish.Base.extend({orderId:null,paymentForm:null,paymentAmount:null,paymentCurrency:null,$makePayment:null,init:function(t){this.setSettings(t),this.orderId=this.settings.orderId,this.paymentForm=this.settings.paymentForm,this.paymentAmount=this.settings.paymentAmount,this.paymentCurrency=this.settings.paymentCurrency,this.$makePayment=$("#make-payment"),this.addListener(this.$makePayment,"click","makePayment"),Object.keys(this.paymentForm.errors).length>0&&this.openPaymentModal()},openPaymentModal:function(){this.paymentModal?this.paymentModal.show():this.paymentModal=new Craft.Commerce.PaymentModal({orderId:this.orderId,paymentForm:this.paymentForm,paymentAmount:this.paymentAmount,paymentCurrency:this.paymentCurrency})},makePayment:function(t){t.preventDefault(),this.openPaymentModal()},_getCountries:function(){return window.countries}},{defaults:{orderId:null,paymentForm:null,paymentAmount:null,paymentCurrency:null,$makePayment:null}})},588:function(){function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t(e)}"undefined"===t(Craft.Commerce)&&(Craft.Commerce={}),Craft.Commerce.OrderIndex=Craft.BaseElementIndex.extend({startDate:null,endDate:null,init:function(t,e,n){if(this.on("selectSource",$.proxy(this,"updateSelectedSource")),this.base(t,e,n),Craft.ui.createDateRangePicker({onChange:function(t,e){this.startDate=t,this.endDate=e,this.updateElements()}.bind(this)}).appendTo(this.$toolbar),window.orderEdit&&window.orderEdit.currentUserPermissions["commerce-editOrders"]){var a=$("",{class:"btn submit icon add",href:Craft.getUrl("commerce/orders/create"),text:Craft.t("commerce","New Order")});this.addButton(a)}},updateSelectedSource:function(){var t="all"!==(this.$source?this.$source:"all")?this.$source.data("handle"):null;if("index"===this.settings.context&&"undefined"!=typeof history){var e="commerce/orders";t&&(e+="/"+t),history.replaceState({},"",Craft.getUrl(e))}},getDefaultSourceKey:function(){var t=window.defaultStatusHandle;if(t)for(var e=0;e="+this.startDate.getTime()/1e3),this.endDate&&t.criteria[e].push("<"+(this.endDate.getTime()/1e3+86400))}return t},updateSourcesBadgeCounts:function(){$.ajax({url:Craft.getActionUrl("commerce/orders/get-index-sources-badge-counts"),type:"GET",dataType:"json",success:$.proxy((function(t){if(t.counts){var e=this.$sidebar;$.each(t.counts,(function(t,n){var a=e.find('nav a[data-key="orderStatus:'+n.handle+'"]');a&&a.find(".badge").text(n.orderCount)}))}if(t.total){var n=this.$sidebar.find('nav a[data-key="*"]');n&&n.find(".badge").text(t.total)}}),this)})},setIndexAvailable:function(){this.updateSourcesBadgeCounts(),this.base()}}),Craft.registerElementIndexClass("craft\\commerce\\elements\\Order",Craft.Commerce.OrderIndex)},255:function(){function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t(e)}"undefined"===t(Craft.Commerce)&&(Craft.Commerce={}),Craft.Commerce.PaymentModal=Garnish.Modal.extend({$container:null,$body:null,init:function(t){var e=this;this.$container=$("
",{id:"paymentmodal",class:"modal fitted loading"}).appendTo(Garnish.$bod),this.base(this.$container,$.extend({resizable:!1},t));var n={orderId:t.orderId,paymentForm:t.paymentForm,paymentAmount:t.paymentAmount,paymentCurrency:t.paymentCurrency};Craft.sendActionRequest("POST","commerce/orders/get-payment-modal",{data:n}).then((function(t){e.$container.removeClass("loading");var n=e;e.$container.append(t.data.modalHtml),Craft.appendHeadHtml(t.data.headHtml),Craft.appendFootHtml(t.data.footHtml);var a=$(".buttons",e.$container),s=$('
'+Craft.t("commerce","Cancel")+"
").prependTo(a);e.addListener(s,"click","cancelPayment"),$("select#payment-form-select").change($.proxy((function(t){var e=$(t.currentTarget).val();$(".gateway-form").addClass("hidden"),$("#gateway-"+e+"-form").removeClass("hidden"),setTimeout((function(){Craft.initUiElements(this.$container),n.updateSizeAndPosition()}),200)}),e)).trigger("change"),setTimeout((function(){Craft.initUiElements(this.$container),n.updateSizeAndPosition()}),200)})).catch((function(t){var n=t.response;e.$container.removeClass("loading");var a=Craft.t("commerce","An unknown error occurred.");n.data.message&&(a=n.data.message),e.$container.append('
'+a+"
")}))},cancelPayment:function(){this.hide()}},{})},166:function(){function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},t(e)}"undefined"===t(Craft.Commerce)&&(Craft.Commerce={}),Craft.Commerce.ProductSalesModal=Garnish.Modal.extend({id:null,$newSale:null,$cancelBtn:null,$select:null,$saveBtn:null,$spinner:null,$purchasableCheckboxes:[],init:function(t,e){this.id=Math.floor(1e9*Math.random()),this.setSettings(e,this.defaults),this.$form=$('