Skip to content
intermediate Phase 45 · Checkout Fundamentals

Totals and Taxes

TotalsCollector, tax calculation, discount calculation, and grand total computation

1h
0 problems
Topic Progress 0%

Totals Collector System

TotalsCollector Architecture

Magento uses a collector pattern to calculate each total:

// Magento\Quote\Model\Quote\TotalsCollector
namespace Magento\Quote\Model\Quote;

class TotalsCollector
{
    /**
     * Collect all totals
     */
    public function collect(
        \Magento\Quote\Model\Quote $quote
    ): \Magento\Quote\Model\Quote {
        $quote->getShippingAddress()->setCollectShippingRates(true);

        foreach ($this->getCollectors() as $collector) {
            $collector->collect($quote);
        }

        return $quote;
    }
}

Available Collectors

// Configured intotals_collector.xml
Magento\Sales\Model\Quote\Address\Total\Collector

// Built-in collectors:
// - subtotal: Product prices × qty
// - discount: Cart price rule discounts
// - shipping: Shipping cost
// - tax: Tax calculations
// - grand_total: Sum of all totals

Custom Total Collector

namespace Vendor\Tax\Model\Quote\Address\Total;

class CustomFee implements \Magento\Quote\Api\Data\TotalInterface
{
    public function collect(
        \Magento\Quote\Model\Quote $quote,
        \Magento\Quote\Model\Quote\Address\Total\Collector $totalCollector
    ): self {
        $customFee = $this->calculateCustomFee($quote);

        $total->setCustomFee($customFee);
        $total->setBaseCustomFee($customFee);

        $quote->getShippingAddress()->addTotal([
            'code' => 'custom_fee',
            'title' => 'Custom Fee',
            'value' => $customFee,
        ]);

        return $this;
    }

    public function fetch(
        \Magento\Quote\Model\Quote $quote,
        \Magento\Quote\Model\Quote\Address\Total $total
    ): self {
        $total->addTotal(['code' => 'custom_fee', 'value' => $total->getCustomFee()]);
        return $this;
    }
}

Tax Calculation

Tax Calculation Process

// Magento\Tax\Model\Calculation
namespace Magento\Tax\Model;

class Calculation
{
    public function __construct(
        private \Magento\Tax\Model\Calculation\Rate $rate,
        private \Magento\Tax\Model\Calculation\Rule $rule
    ) {}

    /**
     * Get tax rate for address and customer tax class
     */
    public function getRate(
        \Magento\Tax\Model\Calculation\Request $request
    ): float {
        $taxRate = $this->rate->loadByCustomerTaxClassAndPostcode(
            $request->getCustomerTaxClassId(),
            $request->getProductTaxClassId(),
            $request->getCountryId(),
            $request->getRegionId(),
            $request->getPostcode()
        );

        return $taxRate->getRate();
    }
}

Tax Configuration

<!-- app/code/Vendor/Tax/etc/config.xml -->
<config>
    <default>
        <tax>
            <catalog/prices>1</catalog/prices>
            <sales/display/price>1</sales/display/price>
            <sales/display/shipping>1</sales/display/shipping>
            <sales/display/subtotal>1</sales/display/subtotal>
        </tax>
    </default>
</config>

Tax Classes

// Product Tax Class
$product->setTaxClassId(2); // Taxable Goods

// Customer Tax Class
$customer->setTaxClassId(3); // Retail Customer

// Tax rules combine product + customer classes with rates

Discount Calculation

Discount Processing

// Magento\Quote\Model\Quote\Address\Total\Discount
namespace Magento\Quote\Model\Quote\Address\Total;

class Discount
{
    public function collect(
        \Magento\Quote\Model\Quote $quote,
        \Magento\Quote\Model\Quote\Address\Total\Collector $total
    ): self {
        $discountAmount = 0;

        foreach ($quote->getItems() as $item) {
            $discountAmount += $item->getDiscountAmount();
        }

        $shippingDiscount = $quote->getShippingAddress()->getDiscountAmount();
        $discountAmount += $shippingDiscount;

        $total->setDiscountAmount($discountAmount);
        $total->setBaseDiscountAmount($discountAmount);

        $quote->getShippingAddress()->addTotal([
            'code' => 'discount',
            'title' => 'Discount',
            'value' => -$discountAmount,
        ]);

        return $this;
    }
}

Applying Cart Price Rules

// Cart price rules are applied during totals collection
$rule = $this->ruleFactory->create()->load($ruleId);

// Check if rule applies to quote
if ($rule->validate($quote)) {
    // Calculate discount
    $discount = $rule->calcDiscount($quote->getSubtotal());

    // Apply to quote items
    foreach ($quote->getItems() as $item) {
        $item->setDiscountAmount($discount);
        $item->setBaseDiscountAmount($discount);
    }
}

Grand Total Computation

Grand Total Calculation Flow

// Totals are collected in this order:
// 1. Subtotal (items × qty × price)
// 2. Discounts (cart price rules)
// 3. Shipping
// 4. Tax
// 5. Grand Total (sum of all)

// Example totals structure:
$totals = [
    'subtotal' => [
        'code' => 'subtotal',
        'title' => 'Subtotal',
        'value' => 100.00,
    ],
    'discount' => [
        'code' => 'discount',
        'title' => 'Discount (10%)',
        'value' => -10.00,
    ],
    'shipping' => [
        'code' => 'shipping',
        'title' => 'Shipping',
        'value' => 5.99,
    ],
    'tax' => [
        'code' => 'tax',
        'title' => 'Tax (8%)',
        'value' => 7.68,
    ],
    'grand_total' => [
        'code' => 'grand_total',
        'title' => 'Grand Total',
        'value' => 103.67,
    ],
];

Reading Totals

$quote = $this->cartRepository->get($cartId);

// Get specific totals
$subtotal = $quote->getShippingAddress()->getSubtotal();
$discount = $quote->getShippingAddress()->getDiscountAmount();
$tax = $quote->getShippingAddress()->getTaxAmount();
$grandTotal = $quote->getShippingAddress()->getGrandTotal();

// Get all totals
$allTotals = $quote->getShippingAddress()->getTotals();

foreach ($allTotals as $total) {
    echo $total->getCode() . ': ' . $total->getValue();
}

Modifying Totals

// Add custom total
$quote->getShippingAddress()->addTotal([
    'code' => 'custom_fee',
    'title' => 'Handling Fee',
    'value' => 2.50,
    'area' => 'shipping',
]);

// Recalculate grand total
$quote->getShippingAddress()->collectTotals();

Quiz

1. What is the order of totals collection?

Question 1 options

2. How are taxes calculated?

Question 2 options

3. What does the totals collector pattern provide?

Question 3 options

Flashcards

Question

What is TotalsCollector?

Answer

System that iterates through total collectors to calculate subtotal, discounts, shipping, tax, and grand total

Question

Tax calculation inputs?

Answer

Product tax class + customer tax class + destination (country/region/postcode)

Question

Discount sources?

Answer

Cart price rules applied during totals collection to items and shipping

Question

Grand total formula?

Answer

Subtotal - Discounts + Shipping + Tax = Grand Total

Revision Notes

Key Takeaways

  • 1. TotalsCollector iterates through pluggable collector classes
  • 2. Each collector handles one total type: subtotal, discount, shipping, tax, grand total
  • 3. Tax calculation uses product class, customer class, and destination
  • 4. Discounts come from cart price rules applied during collection
  • 5. Custom totals can be added by implementing TotalInterface

Interview Tips

  • Explain the collector pattern and why it's used for totals
  • Describe how tax rates are determined from product/customer classes and geography
  • Discuss adding custom fees by implementing a new total collector

Cheat Sheet

Totals Flow:
  Subtotal → Discounts → Shipping → Tax → Grand Total

Collector Pattern:
  Each total type has its own collector class
  implement TotalInterface: collect() + fetch()

Tax Calculation:
  Product Tax Class + Customer Tax Class + Location = Tax Rate

Custom Totals:
  Implement TotalInterface, register in totals_collector.xml