Skip to content

Custom Discount Breaks Shipping Rate Calculation in Quote

<?php
declare(strict_types=1);

namespace VendorModuleModelQuoteTotal;

use MagentoQuoteApiDataShippingAssignmentInterface;
use MagentoQuoteModelQuote;
use MagentoQuoteModelQuoteAddressTotal;
use MagentoQuoteModelQuoteAddressTotalAbstractTotal;
use MagentoQuoteModelQuoteItem as QuoteItem;
use MagentoQuoteModelQuoteAddress;

class CustomDiscount extends AbstractTotal
{
    private const CODE  = 'custom_discount';
    private const LABEL = 'Points Redeemed';

    
    public function __construct(
    ) {
        $this->setCode(self::CODE);
    }

    /**
     * Collect totals for the quote.
     *
     * @param Quote $quote
     * @param ShippingAssignmentInterface $shippingAssignment
     * @param Total $total
     * @return self
     */
    public function collect(
        Quote $quote,
        ShippingAssignmentInterface $shippingAssignment,
        Total $total
    ): self {
        parent::collect($quote, $shippingAssignment, $total);

        $address = $shippingAssignment->getShipping()->getAddress();

        $items = $shippingAssignment->getItems();
        $subtotal = $total->getSubtotal();
        if (!$items || $subtotal <= 0.0) {
            return $this;
        }

        $dynamicDiscount = 0.0;
        $myPoints = 0;
        $storeId = (int) $quote->getStoreId();

        foreach ($items as $item) {
            [$discount, $points] = $this->calculateDiscountAndPoints($item, $storeId);
            $dynamicDiscount += $discount;
            $myPoints += $points;
            $item->setDiscountAmount($item->getDiscountAmount() + abs($discount));
            $item->setBaseDiscountAmount($item->getBaseDiscountAmount() + abs($discount));
        }

        $cartDiscount = $total->getDiscountAmount() ?: 0.0;
        $totalDiscount = $dynamicDiscount + $cartDiscount;
        $totalDiscount = max(-$subtotal, $totalDiscount);
        if (abs($totalDiscount) < 0.0001) {
            return $this;
        }

        $this->applyTotals(
            $quote,
            $total,
            $shippingAssignment,
            $dynamicDiscount,
            $totalDiscount,
            $myPoints,
            $cartDiscount,
            $address
        );

        return $this;
    }

    /**
     * Fetch totals for the quote.
     *
     * @param Quote $quote
     * @param Total $total
     * @return array
     */
    public function fetch(Quote $quote, Total $total): array
    {
        $discountAmount = (float)$total->getDiscountAmount() ?: 0.0;
        $dynamicDiscount = 0.0;
        $myPoints = 0;
        $storeId = (int) $quote->getStoreId();

        foreach ($quote->getAllItems() as $item) {
            [$dynamicDiscount, $points] = $this->calculateDiscountAndPoints($item, $storeId);
            $myPoints += $points;
            $dynamicDiscount += $dynamicDiscount;
        }

        if (abs($dynamicDiscount) < 0.0001) {
            return [];
        }

        return [
            'code'  => self::CODE,
            'title' => $myPoints > 0 ? __($myPoints . ' ' . self::LABEL) : __('Discount'),
            'value' => $dynamicDiscount,
        ];
    }

    /**
     * Calculate the discount and points for a given item.
     *
     * @param QuoteItem $item
     * @param int $storeId
     * @return array
     */
    private function calculateDiscountAndPoints($item, int $storeId): array
    {
        // my custom logic to calculate discount and points
        return [$totalDiscount, $myPoints];
        
    }

    /**
     * Apply the calculated totals to the quote.
     *
     * @param Quote   $quote
     * @param Total   $total
     * 
     * @param float   $dynamicDiscount
     * @param float   $totalDiscount
     * @param int     $myPoints
     * @param float   $cartDiscount
     * @param Address $address
     * @return void
     */
    private function applyTotals(
        Quote   $quote,
        Total   $total,
        $shippingAssignment,
        float   $dynamicDiscount,
        float   $totalDiscount,
        int     $myPoints,
        float   $cartDiscount,
        Address $address
    ): void {
        $descriptions = [];

        if ($total->getDiscountDescription()) {
            $descriptions[] = (string) $total->getDiscountDescription();
        }

        if ($myPoints > 0) {
            $descriptions[] = $myPoints . ' ' . self::LABEL;
        }

        $combinedDesc = implode(', ', $descriptions);

        $total->setDiscountDescription($combinedDesc);
        $total->addTotalAmount(self::CODE, $dynamicDiscount);
        $total->addBaseTotalAmount(self::CODE, $dynamicDiscount);
        $total->setDiscountAmount($total->getDiscountAmount() + $dynamicDiscount);
        $total->setBaseDiscountAmount($total->getBaseDiscountAmount() + $dynamicDiscount);
        $total->setCustomDiscountAmount($dynamicDiscount);
        $total->setBaseCustomDiscountAmount($dynamicDiscount);
        // Adjust subtotal with discount

        $total->setSubtotalWithDiscount($total->getSubtotal() + $total->getDiscountAmount());
        $total->setBaseSubtotalWithDiscount($total->getBaseSubtotal() + $total->getBaseDiscountAmount());

        $quote->setDiscountAmount($total->getDiscountAmount());
        $quote->setBaseDiscountAmount($total->getBaseDiscountAmount());

        // $address->setDiscountDescription($combinedDesc);
        $address->setDiscountAmount($total->getDiscountAmount());
        $address->setBaseDiscountAmount($total->getBaseDiscountAmount());
        $address->setCustomDiscountAmount($total->getCustomDiscountAmount());
        $address->setBaseCustomDiscountAmount($total->getBaseCustomDiscountAmount());
        $address->setSubtotalWithDiscount($total->getSubtotalWithDiscount());
        $address->setBaseSubtotalWithDiscount($total->getBaseSubtotalWithDiscount());
    }
}
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Sales:etc/sales.xsd">
    <section name="quote">
        <group name="totals">
            <item name="custom_discount"
                instance="VendorModuleModelQuoteTotalCustomDiscount"
                sort_order="310"/>
        </group>
    </section>
</config>

With this code i have an issue, when a redeemed product is in the cart, the shipping price is always 0, even though the subtotal_with_discount is below the free shipping threshold (using table rates). All other data, like subtotal and base_* fields, are saved correctly.
If I try setting the shipping amount manually, it displays correctly up to the payment page but does not appear in the order receipt after the order is placed and .
My table rate condition is set to package_value_with_discount.

Can anyone help clarify and resolve this issue?