1) For customer account: /V1/carts/mine/coupons/:couponCode
I just explain how sale rules work for customer logged
a) Set coupon:
vendor/magento/module-quote/etc/webapi.xml
<route url="/V1/carts/mine/coupons/:couponCode" method="PUT">
<service class="Magento\Quote\Api\CouponManagementInterface" method="set"/>
<resources>
<resource ref="Magento_Cart::manage" />
</resources>
</route>
So, the service class interface class Magento\Quote\Api\CouponManagementInterface::set()
will be used and Magento\Quote\Model\CouponManagement::set
will be called.
$quote->setCouponCode($couponCode);
$this->quoteRepository->save($quote->collectTotals());
When quote collects totals: $quote->collectTotals()
\Magento\SalesRule\Model\Quote\Discount::collect()
will be called. It will calculate the coupon logic here.
b) Remove coupon:
vendor/magento/module-quote/etc/webapi.xml
<route url="/V1/carts/mine/coupons" method="DELETE">
<service class="Magento\Quote\Api\CouponManagementInterface" method="remove"/>
<resources>
<resource ref="self" />
</resources>
<data>
<parameter name="cartId" force="true">%cart_id%</parameter>
</data>
</route>
Magento\Quote\Model\CouponManagement::remove()
will be called.
c) Total Segment: V1/carts/mine/payment-information
vendor/magento/module-checkout/etc/webapi.xml
<route url="/V1/carts/mine/payment-information" method="GET">
<service class="Magento\Checkout\Api\PaymentInformationManagementInterface" method="getPaymentInformation"/>
<resources>
<resource ref="self" />
</resources>
<data>
<parameter name="cartId" force="true">%cart_id%</parameter>
</data>
</route>
vendor/magento/module-checkout/Model/PaymentInformationManagement.php
/**
* {@inheritDoc}
*/
public function getPaymentInformation($cartId)
{
...
$paymentDetails->setTotals($this->cartTotalsRepository->get($cartId));
...
}
\Magento\Quote\Model\Cart\CartTotalRepository::get()
if ($quote->isVirtual()) {
$addressTotalsData = $quote->getBillingAddress()->getData();
$addressTotals = $quote->getBillingAddress()->getTotals();
} else {
$addressTotalsData = $quote->getShippingAddress()->getData();
$addressTotals = $quote->getShippingAddress()->getTotals();
}
......
$calculatedTotals = $this->totalsConverter->process($addressTotals);
$quoteTotals->setTotalSegments($calculatedTotals);
This will get the totals data and set the total segments.
\Magento\Quote\Model\Quote\TotalsReader::fetch()
foreach ($this->collectorList->getCollectors($quote->getStoreId()) as $reader) {
$data = $reader->fetch($quote, $total);
2) For guest account, we can see the apis:
vendor/magento/module-quote/etc/webapi.xml
<!-- Managing Guest Cart Coupons -->
<route url="/V1/guest-carts/:cartId/coupons" method="GET">
<service class="Magento\Quote\Api\GuestCouponManagementInterface" method="get"/>
<resources>
<resource ref="anonymous" />
</resources>
</route>
<route url="/V1/guest-carts/:cartId/coupons/:couponCode" method="PUT">
<service class="Magento\Quote\Api\GuestCouponManagementInterface" method="set"/>
<resources>
<resource ref="anonymous" />
</resources>
</route>
<route url="/V1/guest-carts/:cartId/coupons" method="DELETE">
<service class="Magento\Quote\Api\GuestCouponManagementInterface" method="remove"/>
<resources>
<resource ref="anonymous" />
</resources>
</route>
You can use an action like this:
require(
[
'Magento_Checkout/js/model/cart/cache',
'Magento_Checkout/js/model/cart/totals-processor/default',
'Magento_Checkout/js/model/quote'
],
function (cartCache, totalsProcessor, quote) {
cartCache.clear('cartVersion');
totalsProcessor.estimateTotals(quote.shippingAddress());
}
);
It will update the cart totals from the cart page using default shipping address from the quote. If there was no address it is another issue.
I tested it in the browser console this way:
require(
[
'Magento_Checkout/js/model/cart/cache',
'Magento_Checkout/js/model/cart/totals-processor/default',
'Magento_Checkout/js/model/quote'
],
function (cartCache, totalsProcessor, quote) {
var form = jQuery('#discount-coupon-form');
jQuery.ajax(
{
type: "POST",
url: form.attr('action'),
data: form.serialize(),
success: function (response) {
console.log(response);
cartCache.clear('cartVersion');
totalsProcessor.estimateTotals(quote.shippingAddress());
}
}
);
}
)();
and all works fine on Magento 2.3.1 for not logged in customer.
PS: I know that my answer is not a complete, but maybe this information will be useful for you.
Update:
Here is simplified example:
Add our component which submit coupon code using AJAX and update cart totals:
app/code/Vendor/Module/view/frontend/requirejs-config.js
var config = {
map: {
'*': {
submitCoupon: 'Vendor_Module/js/submitCoupon'
}
}
};
Create js file with action (it is simplified, you need to write own logic):
app/code/Vendor/Module/view/frontend/web/js/submitCoupon.js
define([
'jquery',
'Magento_Checkout/js/model/cart/cache',
'Magento_Checkout/js/model/cart/totals-processor/default',
'Magento_Checkout/js/model/quote'
], function ($, cartCache, totalsProcessor, quote) {
'use strict';
console.log('Submit coupon loaded');
var form = $('#discount-coupon-form');
$('#submit_coupon').on('click', function () {
$.ajax(
{
type: "POST",
url: form.attr('action'),
data: form.serialize(),
success: function (response) {
console.log(response);
cartCache.clear('cartVersion');
totalsProcessor.estimateTotals(quote.shippingAddress());
}
}
);
});
});
Add simple block with template into layout for the cart page (simplified, you need to write own):
app/code/Vendor/Module/view/frontend/layout/checkout_cart_index.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="content">
<block class="Magento\Framework\View\Element\Template" name="submit_coupon" before="-" template="Vendor_Module::coupon.phtml"/>
</referenceContainer>
</body>
</page>
Create template (simplified too, write your own with desired design):
app/code/Vendor/Module/view/frontend/templates/coupon.phtml
<div id="submit_coupon">
<?= __('Submit Coupon'); ?>
</div>
<script type="text/x-magento-init">
{
"#submit_coupon": {
"submitCoupon": {}
}
}
</script>
As a result after clean cache and redeploy static content you will see the <div>
having a text Submit Coupon
. When you have press it the coupon code entered in the default input will be sent to a server and after it will be processed the cart totals will be updated.
PPS: As I said, it is just a very simplified scheme which serves to understand "how it will work". For your purposes you need to write something more complicated and well designed.
Best Answer
Managed to get this resolved. The issue was not with Magento Core, but with the MageWorx Magento 2 Gift Cards Extension. The extension came with a custom discount model to use in
$invoice->collectTotals()
which reset the the discount amount (and description) to0
.Found it by putting debug code in
Magento\Sales\Model\Order\Invoice->collectTotals()
to see which models were manipulating the data.