Repository: saleor/saleor
Pull request: Checkout plugins rework
I want to merge this change because...

- Fix #4965
- want to remove methods from models
- want to address way of using extensions (via importing manager vs helper functions)
- type annotations matter

### Screenshots

<!-- If your changes affect the UI, providing "before" and "after" screenshots will
greatly reduce the amount of work needed to review your work. -->

### Pull Request Checklist

<!-- Please keep this section. It will make maintainer's life easier. -->

1. [x] Privileged views and APIs are guarded by proper permission checks.
1. [x] All visible strings are translated with proper context.
1. [x] All data-formatting is locale-aware (dates, numbers, and so on).
1. [x] Database queries are optimized and the number of queries is constant.
1. [x] Database migration files are up to date.
1. [x] The changes are tested.
1. [x] GraphQL schema and type definitions are up to date.
1. [x] Changes are mentioned in the changelog.


Problem statement:
Shipping calculation, totals and plugins
### What I'm trying to achieve
Shipping price calculation plugin

### Steps to reproduce the problem
1. I’ve introduced plugin with overwritten calculate_checkout_shipping method
2. Now I can call the manager to get shipping prices, cool!
3. Now checkout total will be invalid: `manager.calculate_checkout_total` takes total from Checkout.get_gotal()
4. Checkout.get_gotal() takes shipping from Checkout.get_shipping_price(), which ignores plugin manager existence

### What I expected to happen
Using a plugin to modify shipping price also modify checkout total


@korycins Am I missing something?

Hints from issue discussion:
The manager calculates only the default value (inside his body) which will be used in the case when you don't have any plugin with the method that the manager is gonna call.

Your case:
`manager.calculate_checkout_total` - calculates `default_value` base on `checkout.get_total(discounts)`. The manager wil pass it as a default_value but it is plugin responsibility to calculate `checkout_total`. You can do it by calling `calculate_subtotal` and `calculate_shipping` (see VatlayerPlugin - `saleor.extensions.plugins.vatlayer.plugin.VatlayerPlugin#calculate_checkout_total`) or you can create ther your own logic (see AvataxPlugin - `saleor.extensions.plugins.avatax.plugin.AvataxPlugin#calculate_checkout_total`).

So I assume that you can do it exacly what you want.
One thing that we need to change (probably) is "cooperation" between plugins. Plugins like Avalara or Vatlayer should use recieved `default_value` instead of calculating it directly from the checkout.

After discussion on chat:
- manager.get_checkout_total should calculate using manager.get_checkout_subtotal() and manager.get_checout_shipping()
- logic for checkout.get_total should not be duplicated 
- checkout.get_total should be removed from the model, and moved to the utils package

Commit message: Rebase issues

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6ad9e7601cb..719ad23704a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@ All notable, unreleased changes to this project will be documented in this file.
 
 ## [Unreleased]
 
+- Relocate Checkout and CheckoutLine methods into separate module and update checkout related plugins to use them - #4980 by @krzysztofwolski
 - Fix problem with free shipping voucher - #4942 by @IKarbowiak
 - Add sub-categories to random data - #4949 by @IKarbowiak
 - Deprecate `localized` field in Money type - #4952 by @IKarbowiak
diff --git a/saleor/checkout/base_calculations.py b/saleor/checkout/base_calculations.py
new file mode 100644
index 00000000000..bc7ec96481c
--- /dev/null
+++ b/saleor/checkout/base_calculations.py
@@ -0,0 +1,52 @@
+"""Contain functions which are base for calculating checkout properties.
+
+It's recommended to use functions from calculations.py module to take in account plugin
+manager.
+"""
+
+from typing import TYPE_CHECKING, List
+
+from prices import TaxedMoney
+
+from ..core.taxes import quantize_price, zero_taxed_money
+
+if TYPE_CHECKING:
+    # flake8: noqa
+    from .models import Checkout, CheckoutLine
+    from ..discount.types import DiscountsListType
+
+
+def base_checkout_shipping_price(checkout: "Checkout") -> TaxedMoney:
+    """Return checkout shipping price."""
+    if not checkout.shipping_method or not checkout.is_shipping_required():
+        return zero_taxed_money(checkout.currency)
+
+    shipping_price = checkout.shipping_method.get_total()
+    return quantize_price(
+        TaxedMoney(net=shipping_price, gross=shipping_price), shipping_price.currency
+    )
+
+
+def base_checkout_subtotal(line_totals: List[TaxedMoney], currency: str) -> TaxedMoney:
+    """Return the total cost of all checkout lines."""
+    return sum(line_totals, zero_taxed_money(currency))
+
+
+def base_checkout_total(
+    subtotal: TaxedMoney,
+    shipping_price: TaxedMoney,
+    discount: TaxedMoney,
+    currency: str,
+) -> TaxedMoney:
+    """Return the total cost of the checkout."""
+    total = subtotal + shipping_price - discount
+    return max(total, zero_taxed_money(currency))
+
+
+def base_checkout_line_total(
+    line: "CheckoutLine", discounts: "DiscountsListType" = None
+) -> TaxedMoney:
+    """Return the total price of this line."""
+    amount = line.quantity * line.variant.get_price(discounts)
+    price = quantize_price(amount, amount.currency)
+    return TaxedMoney(net=price, gross=price)
diff --git a/saleor/checkout/calculations.py b/saleor/checkout/calculations.py
new file mode 100644
index 00000000000..2a2fda64cee
--- /dev/null
+++ b/saleor/checkout/calculations.py
@@ -0,0 +1,51 @@
+from typing import TYPE_CHECKING
+
+from ..extensions.manager import get_extensions_manager
+
+if TYPE_CHECKING:
+    from prices import TaxedMoney
+    from .models import Checkout, CheckoutLine
+    from ..discount.types import DiscountsListType
+
+
+def checkout_shipping_price(
+    checkout: "Checkout", discounts: "DiscountsListType" = None
+) -> "TaxedMoney":
+    """Return checkout shipping price.
+
+    It takes in account all extensions.
+    """
+    return get_extensions_manager().calculate_checkout_shipping(checkout, discounts)
+
+
+def checkout_subtotal(
+    checkout: "Checkout", discounts: "DiscountsListType" = None
+) -> "TaxedMoney":
+    """Return the total cost of all the checkout lines, taxes included.
+
+    It takes in account all extensions.
+    """
+    return get_extensions_manager().calculate_checkout_subtotal(checkout, discounts)
+
+
+def checkout_total(
+    checkout: "Checkout", discounts: "DiscountsListType" = None
+) -> "TaxedMoney":
+    """Return the total cost of the checkout.
+
+    Total is a cost of all lines and shipping fees, minus checkout discounts,
+    taxes included.
+
+    It takes in account all extensions.
+    """
+    return get_extensions_manager().calculate_checkout_total(checkout, discounts)
+
+
+def checkout_line_total(
+    line: "CheckoutLine", discounts: "DiscountsListType" = None
+) -> "TaxedMoney":
+    """Return the total price of provided line, taxes included.
+
+    It takes in account all extensions.
+    """
+    return get_extensions_manager().calculate_checkout_line_total(line, discounts)
diff --git a/saleor/checkout/models.py b/saleor/checkout/models.py
index c325d0ffa14..4711dad1318 100644
--- a/saleor/checkout/models.py
+++ b/saleor/checkout/models.py
@@ -1,6 +1,6 @@
 """Checkout-related ORM models."""
-from decimal import Decimal
 from operator import attrgetter
+from typing import TYPE_CHECKING, Optional
 from uuid import uuid4
 
 from django.conf import settings
@@ -18,7 +18,11 @@
 from ..giftcard.models import GiftCard
 from ..shipping.models import ShippingMethod
 
-CENTS = Decimal("0.01")
+if TYPE_CHECKING:
+    # flake8: noqa
+    from ..product.models import ProductVariant
+    from django_measurement import Weight
+    from ..payment.models import Payment
 
 
 class CheckoutQueryset(models.QuerySet):
@@ -99,31 +103,14 @@ def __iter__(self):
     def __len__(self):
         return self.lines.count()
 
-    def get_customer_email(self):
+    def get_customer_email(self) -> str:
         return self.user.email if self.user else self.email
 
-    def is_shipping_required(self):
+    def is_shipping_required(self) -> bool:
         """Return `True` if any of the lines requires shipping."""
         return any(line.is_shipping_required() for line in self)
 
-    def get_shipping_price(self):
-        return (
-            self.shipping_method.get_total()
-            if self.shipping_method and self.is_shipping_required()
-            else zero_money(self.currency)
-        )
-
-    def get_subtotal(self, discounts=None):
-        """Return the total cost of the checkout prior to shipping."""
-        subtotals = (line.get_total(discounts) for line in self)
-        return sum(subtotals, zero_money(currency=self.currency))
-
-    def get_total(self, discounts=None):
-        """Return the total cost of the checkout."""
-        total = self.get_subtotal(discounts) + self.get_shipping_price() - self.discount
-        return max(total, zero_money(total.currency))
-
-    def get_total_gift_cards_balance(self):
+    def get_total_gift_cards_balance(self) -> Money:
         """Return the total balance of the gift cards assigned to the checkout."""
         balance = self.gift_cards.aggregate(models.Sum("current_balance_amount"))[
             "current_balance_amount__sum"
@@ -132,19 +119,19 @@ def get_total_gift_cards_balance(self):
             return zero_money(currency=self.currency)
         return Money(balance, self.currency)
 
-    def get_total_weight(self):
+    def get_total_weight(self) -> "Weight":
         # Cannot use `sum` as it parses an empty Weight to an int
         weights = zero_weight()
         for line in self:
             weights += line.variant.get_weight() * line.quantity
         return weights
 
-    def get_line(self, variant):
+    def get_line(self, variant: "ProductVariant") -> Optional["CheckoutLine"]:
         """Return a line matching the given variant and data if any."""
         matching_lines = (line for line in self if line.variant.pk == variant.pk)
         return next(matching_lines, None)
 
-    def get_last_active_payment(self):
+    def get_last_active_payment(self) -> Optional["Payment"]:
         payments = [payment for payment in self.payments.all() if payment.is_active]
         return max(payments, default=None, key=attrgetter("pk"))
 
@@ -192,11 +179,6 @@ def __getstate__(self):
     def __setstate__(self, data):
         self.variant, self.quantity = data
 
-    def get_total(self, discounts=None):
-        """Return the total price of this line."""
-        amount = self.quantity * self.variant.get_price(discounts)
-        return amount.quantize(CENTS)
-
-    def is_shipping_required(self):
+    def is_shipping_required(self) -> bool:
         """Return `True` if the related product variant requires shipping."""
         return self.variant.is_shipping_required()
diff --git a/saleor/checkout/utils.py b/saleor/checkout/utils.py
index d5f4900c48f..e139b7db2d7 100644
--- a/saleor/checkout/utils.py
+++ b/saleor/checkout/utils.py
@@ -1,7 +1,7 @@
 """Checkout-related utility functions."""
 from datetime import date, timedelta
 from functools import wraps
-from typing import Optional, Tuple
+from typing import TYPE_CHECKING, List, Optional, Tuple
 from uuid import UUID
 
 from django.contrib import messages
@@ -11,11 +11,12 @@
 from django.utils import timezone
 from django.utils.encoding import smart_text
 from django.utils.translation import get_language, pgettext, pgettext_lazy
-from prices import Money, MoneyRange, TaxedMoneyRange
+from prices import Money, MoneyRange, TaxedMoney, TaxedMoneyRange
 
 from ..account.forms import get_address_form
 from ..account.models import Address, User
 from ..account.utils import store_user_address
+from ..checkout import calculations
 from ..checkout.error_codes import CheckoutErrorCode
 from ..core.exceptions import InsufficientStock
 from ..core.taxes import quantize_price, zero_taxed_money
@@ -53,6 +54,9 @@
 )
 from .models import Checkout, CheckoutLine
 
+if TYPE_CHECKING:
+    from ..discount.types import DiscountsListType
+
 COOKIE_NAME = "checkout"
 
 
@@ -96,7 +100,9 @@ def remove_unavailable_variants(checkout):
             add_variant_to_checkout(checkout, line.variant, quantity, replace=True)
 
 
-def get_prices_of_discounted_specific_product(lines, voucher, discounts=None):
+def get_prices_of_discounted_specific_product(
+    lines: List[CheckoutLine], voucher: Voucher, discounts: "DiscountsListType" = None
+) -> List[Money]:
     """Get prices of variants belonging to the discounted specific products.
 
     Specific products are products, collections and categories.
@@ -125,9 +131,8 @@ def get_prices_of_discounted_specific_product(lines, voucher, discounts=None):
         # it means that all products are discounted
         discounted_lines.extend(list(lines))
 
-    manager = get_extensions_manager()
     for line in discounted_lines:
-        line_total = manager.calculate_checkout_line_total(line, discounts or []).gross
+        line_total = calculations.checkout_line_total(line, discounts or []).gross
         line_unit_price = quantize_price(
             (line_total / line.quantity), line_total.currency
         )
@@ -656,16 +661,22 @@ def change_shipping_address_in_checkout(checkout, address):
         checkout.save(update_fields=["shipping_address"])
 
 
-def get_checkout_context(checkout, discounts, currency=None, shipping_range=None):
+def get_checkout_context(
+    checkout: Checkout,
+    discounts: "DiscountsListType",
+    currency: Optional[str] = None,
+    shipping_range=None,
+) -> dict:
     """Retrieve the data shared between views in checkout process."""
-    manager = get_extensions_manager()
-    checkout_total = (
-        manager.calculate_checkout_total(checkout=checkout, discounts=discounts)
-        - checkout.get_total_gift_cards_balance()
-    )
+    cards_balance = checkout.get_total_gift_cards_balance()
+
+    # FIXME: We are having here parts of checkout total calculation logic
+    checkout_total = calculations.checkout_total(checkout=checkout, discounts=discounts)
+    checkout_total.gross -= cards_balance
+    checkout_total.net -= cards_balance
     checkout_total = max(checkout_total, zero_taxed_money(checkout_total.currency))
-    checkout_subtotal = manager.calculate_checkout_subtotal(checkout, discounts)
-    shipping_price = manager.calculate_checkout_shipping(checkout, discounts)
+    checkout_subtotal = calculations.checkout_subtotal(checkout, discounts)
+    shipping_price = calculations.checkout_shipping_price(checkout, discounts)
 
     shipping_required = checkout.is_shipping_required()
     total_with_shipping = TaxedMoneyRange(
@@ -673,11 +684,13 @@ def get_checkout_context(checkout, discounts, currency=None, shipping_range=None
     )
     if shipping_required and shipping_range:
         total_with_shipping = shipping_range + checkout_subtotal
+
+    manager = get_extensions_manager()
     context = {
         "checkout": checkout,
         "checkout_are_taxes_handled": manager.taxes_are_enabled(),
         "checkout_lines": [
-            (line, manager.calculate_checkout_line_total(line, discounts))
+            (line, calculations.checkout_line_total(line, discounts))
             for line in checkout
         ],
         "checkout_shipping_price": shipping_price,
@@ -697,7 +710,9 @@ def get_checkout_context(checkout, discounts, currency=None, shipping_range=None
     return context
 
 
-def _get_shipping_voucher_discount_for_checkout(voucher, checkout, discounts=None):
+def _get_shipping_voucher_discount_for_checkout(
+    voucher, checkout, discounts: "DiscountsListType" = None
+):
     """Calculate discount value for a voucher of shipping type."""
     if not checkout.is_shipping_required():
         msg = pgettext(
@@ -719,12 +734,13 @@ def _get_shipping_voucher_discount_for_checkout(voucher, checkout, discounts=Non
         )
         raise NotApplicable(msg)
 
-    manager = get_extensions_manager()
-    shipping_price = manager.calculate_checkout_shipping(checkout, discounts).gross
+    shipping_price = calculations.checkout_shipping_price(checkout, discounts).gross
     return voucher.get_discount_amount_for(shipping_price)
 
 
-def _get_products_voucher_discount(checkout, voucher, discounts=None):
+def _get_products_voucher_discount(
+    checkout, voucher, discounts: "DiscountsListType" = None
+):
     """Calculate products discount value for a voucher, depending on its type."""
     prices = None
     if voucher.type == VoucherType.SPECIFIC_PRODUCT:
@@ -737,15 +753,16 @@ def _get_products_voucher_discount(checkout, voucher, discounts=None):
     return get_products_voucher_discount(voucher, prices)
 
 
-def get_voucher_discount_for_checkout(voucher, checkout, discounts=None) -> Money:
+def get_voucher_discount_for_checkout(
+    voucher: Voucher, checkout: Checkout, discounts: "DiscountsListType" = None
+) -> Money:
     """Calculate discount value depending on voucher and discount types.
 
     Raise NotApplicable if voucher of given type cannot be applied.
     """
     validate_voucher_for_checkout(voucher, checkout, discounts)
     if voucher.type == VoucherType.ENTIRE_ORDER:
-        manager = get_extensions_manager()
-        subtotal = manager.calculate_checkout_subtotal(checkout, discounts).gross
+        subtotal = calculations.checkout_subtotal(checkout, discounts).gross
         return voucher.get_discount_amount_for(subtotal)
     if voucher.type == VoucherType.SHIPPING:
         return _get_shipping_voucher_discount_for_checkout(voucher, checkout, discounts)
@@ -754,7 +771,9 @@ def get_voucher_discount_for_checkout(voucher, checkout, discounts=None) -> Mone
     raise NotImplementedError("Unknown discount type")
 
 
-def get_voucher_for_checkout(checkout, vouchers=None, with_lock=False):
+def get_voucher_for_checkout(
+    checkout: Checkout, vouchers=None, with_lock: bool = False
+) -> Optional[Voucher]:
     """Return voucher with voucher code saved in checkout if active or None."""
     if checkout.voucher_code is not None:
         if vouchers is None:
@@ -769,7 +788,7 @@ def get_voucher_for_checkout(checkout, vouchers=None, with_lock=False):
     return None
 
 
-def recalculate_checkout_discount(checkout, discounts):
+def recalculate_checkout_discount(checkout: Checkout, discounts: "DiscountsListType"):
     """Recalculate `checkout.discount` based on the voucher.
 
     Will clear both voucher and discount if the discount is no longer
@@ -782,8 +801,7 @@ def recalculate_checkout_discount(checkout, discounts):
         except NotApplicable:
             remove_voucher_from_checkout(checkout)
         else:
-            manager = get_extensions_manager()
-            subtotal = manager.calculate_checkout_subtotal(checkout, discounts).gross
+            subtotal = calculations.checkout_subtotal(checkout, discounts).gross
             checkout.discount = (
                 min(discount, subtotal)
                 if voucher.type != VoucherType.SHIPPING
@@ -807,7 +825,9 @@ def recalculate_checkout_discount(checkout, discounts):
         remove_voucher_from_checkout(checkout)
 
 
-def add_promo_code_to_checkout(checkout: Checkout, promo_code: str, discounts=None):
+def add_promo_code_to_checkout(
+    checkout: Checkout, promo_code: str, discounts: "DiscountsListType" = None
+):
     """Add gift card or voucher data to checkout.
 
     Raise InvalidPromoCode if promo code does not match to any voucher or gift card.
@@ -820,7 +840,9 @@ def add_promo_code_to_checkout(checkout: Checkout, promo_code: str, discounts=No
         raise InvalidPromoCode()
 
 
-def add_voucher_code_to_checkout(checkout: Checkout, voucher_code: str, discounts=None):
+def add_voucher_code_to_checkout(
+    checkout: Checkout, voucher_code: str, discounts: "DiscountsListType" = None
+):
     """Add voucher data to checkout by code.
 
     Raise InvalidPromoCode() if voucher of given type cannot be applied.
@@ -842,7 +864,9 @@ def add_voucher_code_to_checkout(checkout: Checkout, voucher_code: str, discount
         )
 
 
-def add_voucher_to_checkout(checkout: Checkout, voucher: Voucher, discounts=None):
+def add_voucher_to_checkout(
+    checkout: Checkout, voucher: Voucher, discounts: "DiscountsListType" = None
+):
     """Add voucher data to checkout.
 
     Raise NotApplicable if voucher of given type cannot be applied.
@@ -897,7 +921,9 @@ def remove_voucher_from_checkout(checkout: Checkout):
 
 
 def get_valid_shipping_methods_for_checkout(
-    checkout: Checkout, discounts, country_code=None
+    checkout: Checkout,
+    discounts: "DiscountsListType",
+    country_code: Optional[str] = None,
 ):
     manager = get_extensions_manager()
     return ShippingMethod.objects.applicable_shipping_methods_for_instance(
@@ -907,7 +933,7 @@ def get_valid_shipping_methods_for_checkout(
     )
 
 
-def is_valid_shipping_method(checkout, discounts):
+def is_valid_shipping_method(checkout: Checkout, discounts: "DiscountsListType"):
     """Check if shipping method is valid and remove (if not)."""
     if not checkout.shipping_method:
         return False
@@ -919,7 +945,9 @@ def is_valid_shipping_method(checkout, discounts):
     return True
 
 
-def get_shipping_price_estimate(checkout: Checkout, discounts, country_code):
+def get_shipping_price_estimate(
+    checkout: Checkout, discounts: "DiscountsListType", country_code: str
+) -> Optional[TaxedMoneyRange]:
     """Return the estimated price range for shipping for given order."""
 
     shipping_methods = get_valid_shipping_methods_for_checkout(
@@ -929,6 +957,7 @@ def get_shipping_price_estimate(checkout: Checkout, discounts, country_code):
     if shipping_methods is None:
         return None
 
+    # TODO: extension manager should be able to have impact on shipping price estimates
     min_price_amount, max_price_amount = shipping_methods.aggregate(
         price_amount_min=Min("price_amount"), price_amount_max=Max("price_amount")
     ).values()
@@ -944,12 +973,12 @@ def get_shipping_price_estimate(checkout: Checkout, discounts, country_code):
     return manager.apply_taxes_to_shipping_price_range(prices, country_code)
 
 
-def clear_shipping_method(checkout):
+def clear_shipping_method(checkout: Checkout):
     checkout.shipping_method = None
     checkout.save(update_fields=["shipping_method"])
 
 
-def _get_voucher_data_for_order(checkout):
+def _get_voucher_data_for_order(checkout: Checkout) -> dict:
     """Fetch, process and return voucher/discount data from checkout.
 
     Careful! It should be called inside a transaction.
@@ -979,7 +1008,9 @@ def _get_voucher_data_for_order(checkout):
     }
 
 
-def _process_shipping_data_for_order(checkout, shipping_price):
+def _process_shipping_data_for_order(
+    checkout: Checkout, shipping_price: TaxedMoney
+) -> dict:
     """Fetch, process and return shipping data from checkout."""
     if not checkout.is_shipping_required():
         return {}
@@ -1000,7 +1031,7 @@ def _process_shipping_data_for_order(checkout, shipping_price):
     }
 
 
-def _process_user_data_for_order(checkout):
+def _process_user_data_for_order(checkout: Checkout):
     """Fetch, process and return shipping data from checkout."""
     billing_address = checkout.billing_address
 
@@ -1082,11 +1113,12 @@ def prepare_order_data(*, checkout: Checkout, tracking_code: str, discounts) ->
     order_data = {}
 
     manager = get_extensions_manager()
-    total = (
-        manager.calculate_checkout_total(checkout=checkout, discounts=discounts)
-        - checkout.get_total_gift_cards_balance()
-    )
-    total = max(total, zero_taxed_money(total.currency))
+    taxed_total = calculations.checkout_total(checkout=checkout, discounts=discounts)
+    cards_total = checkout.get_total_gift_cards_balance()
+    taxed_total.gross -= cards_total
+    taxed_total.net -= cards_total
+
+    taxed_total = max(taxed_total, zero_taxed_money(checkout.currency))
 
     shipping_total = manager.calculate_checkout_shipping(checkout, discounts)
     order_data.update(_process_shipping_data_for_order(checkout, shipping_total))
@@ -1095,7 +1127,7 @@ def prepare_order_data(*, checkout: Checkout, tracking_code: str, discounts) ->
         {
             "language_code": get_language(),
             "tracking_client_id": tracking_code,
-            "total": total,
+            "total": taxed_total,
         }
     )
 
@@ -1111,6 +1143,7 @@ def prepare_order_data(*, checkout: Checkout, tracking_code: str, discounts) ->
     order_data.update(_get_voucher_data_for_order(checkout))
 
     # assign gift cards to the order
+
     order_data["total_price_left"] = (
         manager.calculate_checkout_subtotal(checkout, discounts)
         + shipping_total
@@ -1175,16 +1208,15 @@ def create_order(*, checkout: Checkout, order_data: dict, user: User) -> Order:
     return order
 
 
-def is_fully_paid(checkout: Checkout, discounts):
+def is_fully_paid(checkout: Checkout, discounts: "DiscountsListType"):
     """Check if provided payment methods cover the checkout's total amount.
 
     Note that these payments may not be captured or charged at all.
     """
     payments = [payment for payment in checkout.payments.all() if payment.is_active]
     total_paid = sum([p.total for p in payments])
-    manager = get_extensions_manager()
     checkout_total = (
-        manager.calculate_checkout_total(checkout=checkout, discounts=discounts)
+        calculations.checkout_total(checkout=checkout, discounts=discounts)
         - checkout.get_total_gift_cards_balance()
     )
     checkout_total = max(
@@ -1193,7 +1225,7 @@ def is_fully_paid(checkout: Checkout, discounts):
     return total_paid >= checkout_total.amount
 
 
-def clean_checkout(checkout: Checkout, discounts):
+def clean_checkout(checkout: Checkout, discounts: "DiscountsListType"):
     """Check if checkout can be completed."""
     if checkout.is_shipping_required():
         if not checkout.shipping_method:
diff --git a/saleor/core/taxes.py b/saleor/core/taxes.py
index fec800c590f..2e2ec6577ac 100644
--- a/saleor/core/taxes.py
+++ b/saleor/core/taxes.py
@@ -12,36 +12,33 @@ class TaxError(Exception):
     """Default tax error."""
 
 
-def zero_money(currency=None):
+def zero_money(currency: str = settings.DEFAULT_CURRENCY) -> Money:
     """Return a money object set to zero.
 
     This is a function used as a model's default.
     """
-
-    if currency is None:
-        currency = settings.DEFAULT_CURRENCY
     return Money(0, currency)
 
 
-def zero_taxed_money(currency=settings.DEFAULT_CURRENCY):
+def zero_taxed_money(currency: str = settings.DEFAULT_CURRENCY) -> TaxedMoney:
     zero = zero_money(currency)
     return TaxedMoney(net=zero, gross=zero)
 
 
-def include_taxes_in_prices():
+def include_taxes_in_prices() -> bool:
     return Site.objects.get_current().settings.include_taxes_in_prices
 
 
-def display_gross_prices():
+def display_gross_prices() -> bool:
     return Site.objects.get_current().settings.display_gross_prices
 
 
-def charge_taxes_on_shipping():
+def charge_taxes_on_shipping() -> bool:
     return Site.objects.get_current().settings.charge_taxes_on_shipping
 
 
 def get_display_price(
-    base: Union[TaxedMoney, TaxedMoneyRange], display_gross=None
+    base: Union[TaxedMoney, TaxedMoneyRange], display_gross: bool = False
 ) -> Money:
     """Return the price amount that should be displayed based on settings."""
     if not display_gross:
@@ -58,8 +55,8 @@ def get_display_price(
 
 
 def quantize_price(
-    price: Union["TaxedMoney", "Money", "Decimal", "TaxedMoneyRange"], currency
-):
+    price: Union["TaxedMoney", "Money", "Decimal", "TaxedMoneyRange"], currency: str
+) -> Union["TaxedMoney", "Money", "Decimal", "TaxedMoneyRange"]:
     precision = get_currency_precision(currency)
     number_places = Decimal(10) ** -precision
     return price.quantize(number_places)
diff --git a/saleor/discount/types.py b/saleor/discount/types.py
new file mode 100644
index 00000000000..a372c167d2c
--- /dev/null
+++ b/saleor/discount/types.py
@@ -0,0 +1,5 @@
+from typing import Iterable, Optional
+
+from . import DiscountInfo
+
+DiscountsListType = Optional[Iterable[DiscountInfo]]
diff --git a/saleor/discount/utils.py b/saleor/discount/utils.py
index 9e37d97526f..5d980efb44b 100644
--- a/saleor/discount/utils.py
+++ b/saleor/discount/utils.py
@@ -1,30 +1,39 @@
 import datetime
 from collections import defaultdict
-from typing import Iterable
+from typing import TYPE_CHECKING, Dict, Iterable, List, Set
 
 from django.db.models import F
 from django.utils import timezone
 from django.utils.translation import pgettext
+from prices import Money
 
+from ..checkout import calculations
 from ..core.taxes import zero_money
-from ..extensions.manager import get_extensions_manager
 from . import DiscountInfo
 from .models import NotApplicable, Sale, VoucherCustomer
 
+if TYPE_CHECKING:
+    # flake8: noqa
+    from .types import DiscountsListType
+    from .models import Voucher
+    from ..checkout.models import Checkout
+    from ..product.models import Product, Collection
+    from ..order.models import Order
 
-def increase_voucher_usage(voucher):
+
+def increase_voucher_usage(voucher: "Voucher"):
     """Increase voucher uses by 1."""
     voucher.used = F("used") + 1
     voucher.save(update_fields=["used"])
 
 
-def decrease_voucher_usage(voucher):
+def decrease_voucher_usage(voucher: "Voucher"):
     """Decrease voucher uses by 1."""
     voucher.used = F("used") - 1
     voucher.save(update_fields=["used"])
 
 
-def add_voucher_usage_by_customer(voucher, customer_email):
+def add_voucher_usage_by_customer(voucher: "Voucher", customer_email: str):
     voucher_customer = VoucherCustomer.objects.filter(
         voucher=voucher, customer_email=customer_email
     )
@@ -38,7 +47,7 @@ def add_voucher_usage_by_customer(voucher, customer_email):
     VoucherCustomer.objects.create(voucher=voucher, customer_email=customer_email)
 
 
-def remove_voucher_usage_by_customer(voucher, customer_email):
+def remove_voucher_usage_by_customer(voucher: "Voucher", customer_email: str):
     voucher_customer = VoucherCustomer.objects.filter(
         voucher=voucher, customer_email=customer_email
     )
@@ -46,7 +55,9 @@ def remove_voucher_usage_by_customer(voucher, customer_email):
         voucher_customer.delete()
 
 
-def get_product_discount_on_sale(product, product_collections, discount: DiscountInfo):
+def get_product_discount_on_sale(
+    product: "Product", product_collections: Set["Collection"], discount: DiscountInfo
+):
     """Return discount value if product is on sale or raise NotApplicable."""
     is_product_on_sale = (
         product.id in discount.product_ids
@@ -60,7 +71,7 @@ def get_product_discount_on_sale(product, product_collections, discount: Discoun
     )
 
 
-def get_product_discounts(product, discounts: Iterable[DiscountInfo]):
+def get_product_discounts(product: "Product", discounts: "DiscountsListType"):
     """Return discount values for all discounts applicable to a product."""
     product_collections = set(product.collections.all().values_list("pk", flat=True))
     for discount in discounts:
@@ -70,7 +81,9 @@ def get_product_discounts(product, discounts: Iterable[DiscountInfo]):
             pass
 
 
-def calculate_discounted_price(product, price, discounts: Iterable[DiscountInfo]):
+def calculate_discounted_price(
+    product: "Product", price: Money, discounts: "DiscountsListType"
+) -> Money:
     """Return minimum product's price of all prices with discounts applied."""
     if discounts:
         discounts = list(get_product_discounts(product, discounts))
@@ -79,28 +92,32 @@ def calculate_discounted_price(product, price, discounts: Iterable[DiscountInfo]
     return price
 
 
-def validate_voucher_for_checkout(voucher, checkout, discounts):
-    manager = get_extensions_manager()
-    subtotal = manager.calculate_checkout_subtotal(checkout, discounts)
+def validate_voucher_for_checkout(
+    voucher: "Voucher", checkout: "Checkout", discounts: "DiscountsListType"
+):
+    subtotal = calculations.checkout_subtotal(checkout, discounts)
+
     customer_email = checkout.get_customer_email()
     validate_voucher(voucher, subtotal.gross, checkout.quantity, customer_email)
 
 
-def validate_voucher_in_order(order):
+def validate_voucher_in_order(order: "Order"):
     subtotal = order.get_subtotal()
     quantity = order.get_total_quantity()
     customer_email = order.get_customer_email()
     validate_voucher(order.voucher, subtotal.gross, quantity, customer_email)
 
 
-def validate_voucher(voucher, total_price, quantity, customer_email):
+def validate_voucher(
+    voucher: "Voucher", total_price: Money, quantity: int, customer_email: str
+):
     voucher.validate_min_spent(total_price)
     voucher.validate_min_checkout_items_quantity(quantity)
     if voucher.apply_once_per_customer:
         voucher.validate_once_per_customer(customer_email)
 
 
-def get_products_voucher_discount(voucher, prices):
+def get_products_voucher_discount(voucher: "Voucher", prices: Iterable[Money]) -> Money:
     """Calculate discount value for a voucher of product or category type."""
     if voucher.apply_once_per_order:
         return voucher.get_discount_amount_for(min(prices))
@@ -109,7 +126,7 @@ def get_products_voucher_discount(voucher, prices):
     return total_amount
 
 
-def _fetch_categories(sale_pks):
+def _fetch_categories(sale_pks: Iterable[str]) -> Dict[str, Set[str]]:
     from ..product.models import Category
 
     categories = Sale.categories.through.objects.filter(
@@ -128,7 +145,7 @@ def _fetch_categories(sale_pks):
     return subcategory_map
 
 
-def _fetch_collections(sale_pks):
+def _fetch_collections(sale_pks: Iterable[str]) -> dict:
     collections = Sale.collections.through.objects.filter(
         sale_id__in=sale_pks
     ).values_list("sale_id", "collection_id")
@@ -138,7 +155,7 @@ def _fetch_collections(sale_pks):
     return collection_map
 
 
-def _fetch_products(sale_pks):
+def _fetch_products(sale_pks: Iterable[str]) -> dict:
     products = Sale.products.through.objects.filter(sale_id__in=sale_pks).values_list(
         "sale_id", "product_id"
     )
@@ -148,7 +165,7 @@ def _fetch_products(sale_pks):
     return product_map
 
 
-def fetch_discounts(date: datetime.date):
+def fetch_discounts(date: datetime.date) -> List[DiscountInfo]:
     sales = list(Sale.objects.active(date))
     pks = {s.pk for s in sales}
     collections = _fetch_collections(pks)
@@ -166,5 +183,5 @@ def fetch_discounts(date: datetime.date):
     ]
 
 
-def fetch_active_discounts():
+def fetch_active_discounts() -> List[DiscountInfo]:
     return fetch_discounts(timezone.now())
diff --git a/saleor/extensions/base_plugin.py b/saleor/extensions/base_plugin.py
index d9b136e83d4..3dd380bfe8f 100644
--- a/saleor/extensions/base_plugin.py
+++ b/saleor/extensions/base_plugin.py
@@ -10,12 +10,14 @@
 from .models import PluginConfiguration
 
 if TYPE_CHECKING:
+    # flake8: noqa
     from ..core.taxes import TaxType
     from ..checkout.models import Checkout, CheckoutLine
-    from ..product.models import Product
+    from ..discount import DiscountInfo
+    from ..product.models import Product, ProductType
     from ..account.models import Address, User
     from ..order.models import OrderLine, Order
-    from ..payment.interface import GatewayResponse, PaymentData
+    from ..payment.interface import GatewayResponse, PaymentData, CustomerSource
 
 
 class BasePlugin:
diff --git a/saleor/extensions/manager.py b/saleor/extensions/manager.py
index 07d2f3b7918..ebaa7d6a7a4 100644
--- a/saleor/extensions/manager.py
+++ b/saleor/extensions/manager.py
@@ -6,17 +6,25 @@
 from django_countries.fields import Country
 from prices import Money, MoneyRange, TaxedMoney, TaxedMoneyRange
 
+from ..checkout import base_calculations
 from ..core.payments import PaymentInterface
 from ..core.taxes import TaxType, quantize_price
 from .models import PluginConfiguration
 
 if TYPE_CHECKING:
+    # flake8: noqa
     from .base_plugin import BasePlugin
     from ..checkout.models import Checkout, CheckoutLine
-    from ..product.models import Product
+    from ..discount.types import DiscountsListType
+    from ..product.models import Product, ProductType
     from ..account.models import Address, User
     from ..order.models import OrderLine, Order
-    from ..payment.interface import PaymentData, TokenConfig
+    from ..payment.interface import (
+        PaymentData,
+        TokenConfig,
+        GatewayResponse,
+        CustomerSource,
+    )
 
 
 class ExtensionsManager(PaymentInterface):
@@ -73,33 +81,36 @@ def change_user_address(
         )
 
     def calculate_checkout_total(
-        self, checkout: "Checkout", discounts: List["DiscountInfo"]
+        self, checkout: "Checkout", discounts: "DiscountsListType"
     ) -> TaxedMoney:
-        total = checkout.get_total(discounts)
-        default_value = quantize_price(
-            TaxedMoney(net=total, gross=total), total.currency
+
+        default_value = base_calculations.base_checkout_total(
+            subtotal=self.calculate_checkout_subtotal(checkout, discounts),
+            shipping_price=self.calculate_checkout_shipping(checkout, discounts),
+            discount=checkout.discount,
+            currency=checkout.currency,
         )
         return self.__run_method_on_plugins(
             "calculate_checkout_total", default_value, checkout, discounts
         )
 
     def calculate_checkout_subtotal(
-        self, checkout: "Checkout", discounts: List["DiscountInfo"]
+        self, checkout: "Checkout", discounts: "DiscountsListType"
     ) -> TaxedMoney:
-        subtotal = checkout.get_subtotal(discounts)
-        default_value = quantize_price(
-            TaxedMoney(net=subtotal, gross=subtotal), subtotal.currency
+        line_totals = [
+            self.calculate_checkout_line_total(line, discounts) for line in checkout
+        ]
+        default_value = base_calculations.base_checkout_subtotal(
+            line_totals, checkout.currency
         )
         return self.__run_method_on_plugins(
             "calculate_checkout_subtotal", default_value, checkout, discounts
         )
 
     def calculate_checkout_shipping(
-        self, checkout: "Checkout", discounts: List["DiscountInfo"]
+        self, checkout: "Checkout", discounts: "DiscountsListType"
     ) -> TaxedMoney:
-        total = checkout.get_shipping_price()
-        total = TaxedMoney(net=total, gross=total)
-        default_value = quantize_price(total, total.currency)
+        default_value = base_calculations.base_checkout_shipping_price(checkout)
         return self.__run_method_on_plugins(
             "calculate_checkout_shipping", default_value, checkout, discounts
         )
@@ -115,11 +126,10 @@ def calculate_order_shipping(self, order: "Order") -> TaxedMoney:
         )
 
     def calculate_checkout_line_total(
-        self, checkout_line: "CheckoutLine", discounts: List["DiscountInfo"]
+        self, checkout_line: "CheckoutLine", discounts: "DiscountsListType"
     ):
-        total = checkout_line.get_total(discounts)
-        default_value = quantize_price(
-            TaxedMoney(net=total, gross=total), total.currency
+        default_value = base_calculations.base_checkout_line_total(
+            checkout_line, discounts
         )
         return self.__run_method_on_plugins(
             "calculate_checkout_line_total", default_value, checkout_line, discounts
@@ -175,7 +185,7 @@ def apply_taxes_to_shipping_price_range(self, prices: MoneyRange, country: Count
         )
 
     def preprocess_order_creation(
-        self, checkout: "Checkout", discounts: List["DiscountInfo"]
+        self, checkout: "Checkout", discounts: "DiscountsListType"
     ):
         default_value = None
         return self.__run_method_on_plugins(
@@ -316,7 +326,7 @@ def __run_payment_method(
         method_name: str,
         payment_information: "PaymentData",
         **kwargs,
-    ) -> Optional["GatewayResposne"]:
+    ) -> Optional["GatewayResponse"]:
         default_value = None
         gtw = self.get_plugin(gateway)
         if gtw is not None:
diff --git a/saleor/extensions/plugins/avatax/__init__.py b/saleor/extensions/plugins/avatax/__init__.py
index 8e4bf68099d..b03ae7f85cd 100644
--- a/saleor/extensions/plugins/avatax/__init__.py
+++ b/saleor/extensions/plugins/avatax/__init__.py
@@ -12,9 +12,13 @@
 from django.utils.translation import pgettext_lazy
 from requests.auth import HTTPBasicAuth
 
+from ....checkout import base_calculations
+
 if TYPE_CHECKING:
+    # flake8: noqa
     from ....checkout.models import Checkout
     from ....order.models import Order
+    from ....product.models import Product, ProductVariant
 
 logger = logging.getLogger(__name__)
 
@@ -107,7 +111,7 @@ def _validate_adddress_details(
 
 def _validate_order(order: "Order") -> bool:
     """Validate the order object if it is ready to generate a request to avatax."""
-    if not order.lines.count():
+    if not len(order.lines.all()):
         return False
     shipping_address = order.shipping_address
     is_shipping_required = order.is_shipping_required()
@@ -119,7 +123,7 @@ def _validate_order(order: "Order") -> bool:
 
 def _validate_checkout(checkout: "Checkout") -> bool:
     """Validate the checkout object if it is ready to generate a request to avatax."""
-    if not checkout.lines.count():
+    if not len(checkout.lines.all()):
         return False
 
     shipping_address = checkout.shipping_address
@@ -228,7 +232,9 @@ def get_checkout_lines_data(
         append_line_to_data(
             data=data,
             quantity=line.quantity,
-            amount=str(line.get_total(discounts).amount),
+            amount=str(
+                base_calculations.base_checkout_line_total(line, discounts).gross.amount
+            ),
             tax_code=tax_code,
             item_code=line.variant.sku,
             description=description,
@@ -337,8 +343,7 @@ def generate_request_data_from_checkout(
     address = checkout.shipping_address or checkout.billing_address
     lines = get_checkout_lines_data(checkout, discounts)
 
-    # FIXME after we introduce multicurrency this should be taken from Checkout obj
-    currency = checkout.get_subtotal().currency
+    currency = checkout.currency
     data = generate_request_data(
         transaction_type=transaction_type,
         lines=lines,
diff --git a/saleor/extensions/plugins/avatax/plugin.py b/saleor/extensions/plugins/avatax/plugin.py
index 09d4b5535c1..8c5f8ec9fb6 100644
--- a/saleor/extensions/plugins/avatax/plugin.py
+++ b/saleor/extensions/plugins/avatax/plugin.py
@@ -33,6 +33,8 @@
     from ....checkout.models import Checkout, CheckoutLine
     from ....order.models import Order, OrderLine
     from ...models import PluginConfiguration
+    from ....discount.types import DiscountsListType
+
 
 logger = logging.getLogger(__name__)
 
@@ -139,7 +141,7 @@ def _skip_plugin(self, previous_value: Union[TaxedMoney, TaxedMoneyRange]) -> bo
     def calculate_checkout_total(
         self,
         checkout: "Checkout",
-        discounts: List["DiscountInfo"],
+        discounts: "DiscountsListType",
         previous_value: TaxedMoney,
     ) -> TaxedMoney:
         self._initialize_plugin_configuration()
@@ -147,12 +149,13 @@ def calculate_checkout_total(
         if self._skip_plugin(previous_value):
             return previous_value
 
-        checkout_total = checkout.get_total(discounts=discounts)
+        checkout_total = previous_value
+
         if not _validate_checkout(checkout):
-            return TaxedMoney(net=checkout_total, gross=checkout_total)
+            return checkout_total
         response = get_checkout_tax_data(checkout, discounts, self.config)
         if not response or "error" in response:
-            return TaxedMoney(net=checkout_total, gross=checkout_total)
+            return checkout_total
 
         currency = response.get("currencyCode")
         tax = Decimal(response.get("totalTax", 0.0))
@@ -182,7 +185,7 @@ def _calculate_checkout_subtotal(
     def calculate_checkout_subtotal(
         self,
         checkout: "Checkout",
-        discounts: List["DiscountInfo"],
+        discounts: "DiscountsListType",
         previous_value: TaxedMoney,
     ) -> TaxedMoney:
         self._initialize_plugin_configuration()
@@ -190,22 +193,22 @@ def calculate_checkout_subtotal(
         if self._skip_plugin(previous_value):
             return previous_value
 
-        sub_total = checkout.get_subtotal(discounts)
+        base_subtotal = previous_value
         if not _validate_checkout(checkout):
-            return TaxedMoney(net=sub_total, gross=sub_total)
+            return base_subtotal
 
         response = get_checkout_tax_data(checkout, discounts, self.config)
         if not response or "error" in response:
-            return TaxedMoney(net=sub_total, gross=sub_total)
+            return base_subtotal
 
         currency = response.get("currencyCode")
         return self._calculate_checkout_subtotal(currency, response.get("lines", []))
 
     def _calculate_checkout_shipping(
-        self, currency: str, lines: List[Dict], shipping_price: Money
+        self, currency: str, lines: List[Dict], shipping_price: TaxedMoney
     ) -> TaxedMoney:
         shipping_tax = Decimal(0.0)
-        shipping_net = shipping_price.amount
+        shipping_net = shipping_price.net.amount
         for line in lines:
             if line["itemCode"] == "Shipping":
                 shipping_net = Decimal(line["lineAmount"])
@@ -219,7 +222,7 @@ def _calculate_checkout_shipping(
     def calculate_checkout_shipping(
         self,
         checkout: "Checkout",
-        discounts: List["DiscountInfo"],
+        discounts: "DiscountsListType",
         previous_value: TaxedMoney,
     ) -> TaxedMoney:
         self._initialize_plugin_configuration()
@@ -227,21 +230,21 @@ def calculate_checkout_shipping(
         if self._skip_plugin(previous_value):
             return previous_value
 
-        shipping_price = checkout.get_shipping_price()
+        base_shipping_price = previous_value
         if not _validate_checkout(checkout):
-            return TaxedMoney(net=shipping_price, gross=shipping_price)
+            return base_shipping_price
 
         response = get_checkout_tax_data(checkout, discounts, self.config)
         if not response or "error" in response:
-            return TaxedMoney(net=shipping_price, gross=shipping_price)
+            return base_shipping_price
 
         currency = response.get("currencyCode")
         return self._calculate_checkout_shipping(
-            currency, response.get("lines", []), shipping_price
+            currency, response.get("lines", []), base_shipping_price
         )
 
     def preprocess_order_creation(
-        self, checkout: "Checkout", discounts: List["DiscountInfo"], previous_value: Any
+        self, checkout: "Checkout", discounts: "DiscountsListType", previous_value: Any
     ):
         """Ensure all the data is correct and we can proceed with creation of order.
 
@@ -293,7 +296,7 @@ def order_created(self, order: "Order", previous_value: Any) -> Any:
     def calculate_checkout_line_total(
         self,
         checkout_line: "CheckoutLine",
-        discounts: List["DiscountInfo"],
+        discounts: "DiscountsListType",
         previous_value: TaxedMoney,
     ) -> TaxedMoney:
         self._initialize_plugin_configuration()
@@ -302,9 +305,11 @@ def calculate_checkout_line_total(
             return previous_value
 
         checkout = checkout_line.checkout
-        total = checkout_line.get_total(discounts)
+        base_total = previous_value
+
         if not _validate_checkout(checkout):
-            return TaxedMoney(net=total, gross=total)
+            return base_total
+
         taxes_data = get_checkout_tax_data(checkout, discounts, self.config)
         currency = taxes_data.get("currencyCode")
         for line in taxes_data.get("lines", []):
@@ -315,8 +320,7 @@ def calculate_checkout_line_total(
                 line_net = Money(amount=line_net, currency=currency)
                 return TaxedMoney(net=line_net, gross=line_gross)
 
-        total = checkout_line.get_total(discounts)
-        return TaxedMoney(net=total, gross=total)
+        return base_total
 
     def _calculate_order_line_unit(self, order_line):
         order = order_line.order
diff --git a/saleor/extensions/plugins/vatlayer/plugin.py b/saleor/extensions/plugins/vatlayer/plugin.py
index 427d635d570..c9122889f5e 100644
--- a/saleor/extensions/plugins/vatlayer/plugin.py
+++ b/saleor/extensions/plugins/vatlayer/plugin.py
@@ -81,30 +81,6 @@ def calculate_checkout_total(
             - checkout.discount
         )
 
-    def calculate_checkout_subtotal(
-        self,
-        checkout: "Checkout",
-        discounts: List["DiscountInfo"],
-        previous_value: TaxedMoney,
-    ) -> TaxedMoney:
-        """Calculate subtotal gross for checkout."""
-        self._initialize_plugin_configuration()
-
-        if self._skip_plugin(previous_value):
-            return previous_value
-
-        address = checkout.shipping_address or checkout.billing_address
-        lines = checkout.lines.prefetch_related("variant__product__product_type")
-        zero_total = Money(0, currency=previous_value.currency)
-
-        lines_total = TaxedMoney(net=zero_total, gross=zero_total)
-        for line in lines:
-            price = line.variant.get_price(discounts)
-            lines_total += line.quantity * self.__apply_taxes_to_product(
-                line.variant.product, price, address.country if address else None
-            )
-        return lines_total
-
     def _get_taxes_for_country(self, country: Country):
         """Try to fetch cached taxes on the plugin level.
 
@@ -172,10 +148,11 @@ def calculate_checkout_line_total(
             checkout_line.checkout.shipping_address
             or checkout_line.checkout.billing_address
         )
-        price = checkout_line.variant.get_price(discounts) * checkout_line.quantity
+        price = checkout_line.variant.get_price(discounts)
         country = address.country if address else None
-        return self.__apply_taxes_to_product(
-            checkout_line.variant.product, price, country
+        return (
+            self.__apply_taxes_to_product(checkout_line.variant.product, price, country)
+            * checkout_line.quantity
         )
 
     def calculate_order_line_unit(
diff --git a/saleor/graphql/product/types/products.py b/saleor/graphql/product/types/products.py
index 3ceaf2cefbf..2fb2b44fd36 100644
--- a/saleor/graphql/product/types/products.py
+++ b/saleor/graphql/product/types/products.py
@@ -344,11 +344,7 @@ def resolve_cost_price(root: models.ProductVariant, *_args):
 
     @staticmethod
     def resolve_price(root: models.ProductVariant, *_args):
-        return (
-            root.price_override
-            if root.price_override is not None
-            else root.product.price
-        )
+        return root.base_price
 
     @staticmethod
     @gql_optimizer.resolver_hints(
diff --git a/saleor/payment/gateway.py b/saleor/payment/gateway.py
index 998dcdfccf2..449cfd0d703 100644
--- a/saleor/payment/gateway.py
+++ b/saleor/payment/gateway.py
@@ -1,6 +1,6 @@
 import logging
 from decimal import Decimal
-from typing import List
+from typing import TYPE_CHECKING, Callable, List
 
 from django import forms
 
@@ -18,12 +18,17 @@
     validate_gateway_response,
 )
 
+if TYPE_CHECKING:
+    # flake8: noqa
+    from ..payment.interface import CustomerSource
+
+
 logger = logging.getLogger(__name__)
 ERROR_MSG = "Oops! Something went wrong."
 GENERIC_TRANSACTION_ERROR = "Transaction was unsuccessful"
 
 
-def raise_payment_error(fn):
+def raise_payment_error(fn: Callable) -> Callable:
     def wrapped(*args, **kwargs):
         result = fn(*args, **kwargs)
         if not result.is_success:
@@ -33,7 +38,7 @@ def wrapped(*args, **kwargs):
     return wrapped
 
 
-def payment_postprocess(fn):
+def payment_postprocess(fn: Callable) -> Callable:
     def wrapped(*args, **kwargs):
         txn = fn(*args, **kwargs)
         gateway_postprocess(txn, txn.payment)
@@ -42,7 +47,7 @@ def wrapped(*args, **kwargs):
     return wrapped
 
 
-def require_active_payment(fn):
+def require_active_payment(fn: Callable) -> Callable:
     def wrapped(payment: Payment, *args, **kwargs):
         if not payment.is_active:
             raise PaymentError("This payment is no longer active.")
@@ -129,7 +134,7 @@ def refund(payment: Payment, amount: Decimal = None) -> Transaction:
     plugin_manager = get_extensions_manager()
     if amount is None:
         amount = payment.captured_amount
-    _validate_refund_amound(payment, amount)
+    _validate_refund_amount(payment, amount)
     if not payment.can_refund():
         raise PaymentError("This payment cannot be refunded.")
     token = _get_past_transaction_token(payment, TransactionKind.CAPTURE)
@@ -218,7 +223,7 @@ def _fetch_gateway_response(fn, *args, **kwargs):
         response = fn(*args, **kwargs)
         validate_gateway_response(response)
     except GatewayError:
-        logger.exception("Gateway reponse validation failed!")
+        logger.exception("Gateway response validation failed!")
         response = None
         error = ERROR_MSG
     except Exception:
@@ -235,7 +240,7 @@ def _get_past_transaction_token(payment: Payment, kind: TransactionKind):
     return txn.token
 
 
-def _validate_refund_amound(payment: Payment, amount: Decimal):
+def _validate_refund_amount(payment: Payment, amount: Decimal):
     if amount <= 0:
         raise PaymentError("Amount should be a positive number.")
     if amount > payment.captured_amount:
diff --git a/saleor/payment/interface.py b/saleor/payment/interface.py
index a22c871e7c0..c9ebbcb464a 100644
--- a/saleor/payment/interface.py
+++ b/saleor/payment/interface.py
@@ -1,6 +1,9 @@
 from dataclasses import dataclass
 from decimal import Decimal
-from typing import Any, Dict, Optional
+from typing import TYPE_CHECKING, Any, Dict, Optional
+
+if TYPE_CHECKING:
+    from . import TransactionKind
 
 
 @dataclass
@@ -24,10 +27,10 @@ class GatewayResponse:
 
     is_success: bool
     action_required: bool
-    kind: str
+    kind: "TransactionKind"
     amount: Decimal
     currency: str
-    transaction_id: str
+    transaction_id: Optional[str]
     error: Optional[str]
     customer_id: Optional[str] = None
     card_info: Optional[CreditCardInfo] = None
@@ -61,7 +64,7 @@ class PaymentData:
     currency: str
     billing: Optional[AddressData]
     shipping: Optional[AddressData]
-    order_id: int
+    order_id: Optional[int]
     customer_ip_address: str
     customer_email: str
     token: Optional[str] = None
@@ -100,4 +103,4 @@ class CustomerSource:
 
     id: str
     gateway: str
-    credit_card_info: CreditCardInfo = None
+    credit_card_info: Optional[CreditCardInfo] = None
diff --git a/saleor/seo/schema/email.py b/saleor/seo/schema/email.py
index dce97e44a88..d54d569c9cc 100644
--- a/saleor/seo/schema/email.py
+++ b/saleor/seo/schema/email.py
@@ -1,17 +1,21 @@
 import json
+from typing import TYPE_CHECKING
 
 from django.contrib.sites.models import Site
 
 from ...core.utils import build_absolute_uri
 from ...core.utils.json_serializer import HTMLSafeJSON
 
+if TYPE_CHECKING:
+    from ...order.models import OrderLine, Order
+
 
 def get_organization():
     site = Site.objects.get_current()
     return {"@type": "Organization", "name": site.name}
 
 
-def get_product_data(line, organization):
+def get_product_data(line: "OrderLine", organization: dict) -> dict:
     gross_product_price = line.get_total().gross
     line_name = str(line)
     if line.translated_product_name:
@@ -40,7 +44,7 @@ def get_product_data(line, organization):
     return product_data
 
 
-def get_order_confirmation_markup(order):
+def get_order_confirmation_markup(order: "Order") -> str:
     """Generate schema.org markup for order confirmation e-mail message."""
     organization = get_organization()
     order_url = build_absolute_uri(order.get_absolute_url())
diff --git a/tests/api/benchmark/test_checkout.py b/tests/api/benchmark/test_checkout.py
index 93f2e698c75..c03ac87e919 100644
--- a/tests/api/benchmark/test_checkout.py
+++ b/tests/api/benchmark/test_checkout.py
@@ -1,7 +1,7 @@
 import pytest
 from graphene import Node
-from prices import TaxedMoney
 
+from saleor.checkout import calculations
 from saleor.checkout.utils import add_variant_to_checkout
 from saleor.payment import ChargeStatus, TransactionKind
 from saleor.payment.models import Payment
@@ -39,8 +39,7 @@ def checkout_with_billing_address(checkout_with_shipping_method, address):
 def checkout_with_charged_payment(checkout_with_billing_address):
     checkout = checkout_with_billing_address
 
-    total = checkout.get_total()
-    taxed_total = TaxedMoney(total, total)
+    taxed_total = calculations.checkout_total(checkout)
     payment = Payment.objects.create(
         gateway="Dummy", is_active=True, total=taxed_total.gross.amount, currency="USD"
     )
diff --git a/tests/api/test_checkout.py b/tests/api/test_checkout.py
index f0c2177fe0c..84da3c06544 100644
--- a/tests/api/test_checkout.py
+++ b/tests/api/test_checkout.py
@@ -7,11 +7,13 @@
 from django.core.exceptions import ValidationError
 from prices import Money, TaxedMoney
 
+from saleor.checkout import calculations
 from saleor.checkout.error_codes import CheckoutErrorCode
 from saleor.checkout.models import Checkout
 from saleor.checkout.utils import clean_checkout, is_fully_paid
 from saleor.core.payments import PaymentInterface
 from saleor.core.taxes import zero_money
+from saleor.extensions.manager import ExtensionsManager
 from saleor.graphql.checkout.mutations import (
     clean_shipping_method,
     update_checkout_shipping_method_if_invalid,
@@ -22,7 +24,6 @@
 from saleor.shipping import ShippingMethodType
 from saleor.shipping.models import ShippingMethod
 from tests.api.utils import get_graphql_content
-from saleor.extensions.manager import ExtensionsManager
 
 
 @pytest.fixture
@@ -1168,8 +1169,7 @@ def test_checkout_complete(
     checkout_line_variant = checkout_line.variant
 
     gift_current_balance = checkout.get_total_gift_cards_balance()
-    total = checkout.get_total()
-    total = TaxedMoney(total, total)
+    total = calculations.checkout_total(checkout)
     payment = payment_dummy
     payment.is_active = True
     payment.order = None
@@ -1273,8 +1273,7 @@ def test_checkout_complete_does_not_delete_checkout_after_unsuccessful_payment(
     checkout.billing_address = address
     checkout.save()
 
-    total = checkout.get_total()
-    taxed_total = TaxedMoney(total, total)
+    taxed_total = calculations.checkout_total(checkout)
     payment = payment_dummy
     payment.is_active = True
     payment.order = None
@@ -1350,8 +1349,7 @@ def test_checkout_complete_insufficient_stock(
     checkout.shipping_method = shipping_method
     checkout.billing_address = address
     checkout.save()
-    total = checkout.get_total()
-    total = TaxedMoney(total, total)
+    total = calculations.checkout_total(checkout)
     payment = payment_dummy
     payment.is_active = True
     payment.order = None
@@ -1441,14 +1439,10 @@ def test_checkout_prices(user_api_client, checkout_with_item):
     data = content["data"]["checkout"]
     assert data["token"] == str(checkout_with_item.token)
     assert len(data["lines"]) == checkout_with_item.lines.count()
-    total = checkout_with_item.get_total()
-    assert data["totalPrice"]["gross"]["amount"] == (
-        TaxedMoney(total, total).gross.amount
-    )
-    subtotal = checkout_with_item.get_subtotal()
-    assert data["subtotalPrice"]["gross"]["amount"] == (
-        TaxedMoney(subtotal, subtotal).gross.amount
-    )
+    total = calculations.checkout_total(checkout_with_item)
+    assert data["totalPrice"]["gross"]["amount"] == (total.gross.amount)
+    subtotal = calculations.checkout_subtotal(checkout_with_item)
+    assert data["subtotalPrice"]["gross"]["amount"] == (subtotal.gross.amount)
 
 
 MUTATION_UPDATE_SHIPPING_METHOD = """
@@ -1583,8 +1577,7 @@ def test_clean_checkout(checkout_with_item, payment_dummy, address, shipping_met
     checkout.shipping_method = shipping_method
     checkout.billing_address = address
     checkout.save()
-    total = checkout.get_total()
-    total = TaxedMoney(total, total)
+    total = calculations.checkout_total(checkout)
     payment = payment_dummy
     payment.is_active = True
     payment.order = None
@@ -1665,8 +1658,7 @@ def test_clean_checkout_no_payment(checkout_with_item, shipping_method, address)
 
 def test_is_fully_paid(checkout_with_item, payment_dummy):
     checkout = checkout_with_item
-    total = checkout.get_total()
-    total = TaxedMoney(total, total)
+    total = calculations.checkout_total(checkout)
     payment = payment_dummy
     payment.is_active = True
     payment.order = None
@@ -1680,8 +1672,7 @@ def test_is_fully_paid(checkout_with_item, payment_dummy):
 
 def test_is_fully_paid_many_payments(checkout_with_item, payment_dummy):
     checkout = checkout_with_item
-    total = checkout.get_total()
-    total = TaxedMoney(total, total)
+    total = calculations.checkout_total(checkout)
     payment = payment_dummy
     payment.is_active = True
     payment.order = None
@@ -1703,8 +1694,7 @@ def test_is_fully_paid_many_payments(checkout_with_item, payment_dummy):
 
 def test_is_fully_paid_partially_paid(checkout_with_item, payment_dummy):
     checkout = checkout_with_item
-    total = checkout.get_total()
-    total = TaxedMoney(total, total)
+    total = calculations.checkout_total(checkout)
     payment = payment_dummy
     payment.is_active = True
     payment.order = None
diff --git a/tests/api/test_checkout_digital.py b/tests/api/test_checkout_digital.py
index d5f2101b202..b13e2aba7f2 100644
--- a/tests/api/test_checkout_digital.py
+++ b/tests/api/test_checkout_digital.py
@@ -1,9 +1,9 @@
 """Test the API's checkout process over full digital orders."""
 import pytest
 from graphene import Node
-from prices import TaxedMoney
 
 from saleor.account.models import Address
+from saleor.checkout import calculations
 from saleor.checkout.error_codes import CheckoutErrorCode
 from saleor.checkout.models import Checkout
 from saleor.checkout.utils import add_variant_to_checkout
@@ -176,8 +176,7 @@ def test_checkout_complete(
     checkout.save(update_fields=["billing_address"])
 
     # Create a dummy payment to charge
-    total = checkout.get_total()
-    total = TaxedMoney(total, total)
+    total = calculations.checkout_total(checkout)
     payment = payment_dummy
     payment.is_active = True
     payment.order = None
diff --git a/tests/api/test_checkout_promo_codes.py b/tests/api/test_checkout_promo_codes.py
index 5895fbe91d5..e7f9efe363d 100644
--- a/tests/api/test_checkout_promo_codes.py
+++ b/tests/api/test_checkout_promo_codes.py
@@ -2,8 +2,9 @@
 from decimal import Decimal
 
 import graphene
-from prices import Money, TaxedMoney
+from prices import Money
 
+from saleor.checkout import calculations
 from saleor.checkout.utils import add_voucher_to_checkout
 from saleor.discount import DiscountInfo, VoucherType
 from tests.api.test_checkout import (
@@ -90,8 +91,8 @@ def test_checkout_add_voucher_not_applicable_voucher(
 def test_checkout_lines_delete_with_not_applicable_voucher(
     user_api_client, checkout_with_item, voucher
 ):
-    subtotal = checkout_with_item.get_subtotal()
-    voucher.min_spent = TaxedMoney(subtotal, subtotal).gross
+    subtotal = calculations.checkout_subtotal(checkout_with_item)
+    voucher.min_spent = subtotal.gross
     voucher.save(update_fields=["min_spent_amount", "currency"])
 
     add_voucher_to_checkout(checkout_with_item, voucher)
@@ -194,17 +195,13 @@ def test_checkout_totals_use_discounts(api_client, checkout_with_item, sale):
             collection_ids=set(),
         )
     ]
-    total = checkout.get_total(discounts=discounts)
-    taxed_total = TaxedMoney(total, total)
+    taxed_total = calculations.checkout_total(checkout, discounts=discounts)
     assert data["totalPrice"]["gross"]["amount"] == taxed_total.gross.amount
     assert data["subtotalPrice"]["gross"]["amount"] == taxed_total.gross.amount
 
     line = checkout.lines.first()
-    line_total = line.get_total(discounts=discounts)
-    assert (
-        data["lines"][0]["totalPrice"]["gross"]["amount"]
-        == TaxedMoney(line_total, line_total).gross.amount
-    )
+    line_total = calculations.checkout_line_total(line, discounts=discounts)
+    assert data["lines"][0]["totalPrice"]["gross"]["amount"] == line_total.gross.amount
 
 
 QUERY_GET_CHECKOUT_GIFT_CARD_CODES = """
@@ -308,9 +305,9 @@ def test_checkout_add_voucher_code(api_client, checkout_with_item, voucher):
 def test_checkout_add_voucher_code_checkout_with_sale(
     api_client, checkout_with_item, voucher_percentage, discount_info
 ):
-    assert checkout_with_item.get_subtotal() > checkout_with_item.get_subtotal(
-        discounts=[discount_info]
-    )
+    assert calculations.checkout_subtotal(
+        checkout_with_item
+    ) > calculations.checkout_subtotal(checkout_with_item, discounts=[discount_info])
     checkout_id = graphene.Node.to_global_id("Checkout", checkout_with_item.pk)
     variables = {"checkoutId": checkout_id, "promoCode": voucher_percentage.code}
     data = _mutate_checkout_add_promo_code(api_client, variables)
@@ -327,7 +324,9 @@ def test_checkout_add_specific_product_voucher_code_checkout_with_sale(
     voucher = voucher_specific_product_type
     checkout = checkout_with_item
     expected_discount = Decimal(1.5)
-    assert checkout.get_subtotal() > checkout.get_subtotal(discounts=[discount_info])
+    assert calculations.checkout_subtotal(checkout) > calculations.checkout_subtotal(
+        checkout, discounts=[discount_info]
+    )
     checkout_id = graphene.Node.to_global_id("Checkout", checkout.pk)
     variables = {"checkoutId": checkout_id, "promoCode": voucher.code}
     data = _mutate_checkout_add_promo_code(api_client, variables)
@@ -349,7 +348,9 @@ def test_checkout_add_products_voucher_code_checkout_with_sale(
     voucher.save()
     voucher.products.add(product)
 
-    assert checkout.get_subtotal() > checkout.get_subtotal(discounts=[discount_info])
+    assert calculations.checkout_subtotal(checkout) > calculations.checkout_subtotal(
+        checkout, discounts=[discount_info]
+    )
     checkout_id = graphene.Node.to_global_id("Checkout", checkout.pk)
     variables = {"checkoutId": checkout_id, "promoCode": voucher.code}
     data = _mutate_checkout_add_promo_code(api_client, variables)
@@ -371,7 +372,9 @@ def test_checkout_add_collection_voucher_code_checkout_with_sale(
     voucher.save()
     voucher.collections.add(collection)
 
-    assert checkout.get_subtotal() > checkout.get_subtotal(discounts=[discount_info])
+    assert calculations.checkout_subtotal(checkout) > calculations.checkout_subtotal(
+        checkout, discounts=[discount_info]
+    )
     checkout_id = graphene.Node.to_global_id("Checkout", checkout.pk)
     variables = {"checkoutId": checkout_id, "promoCode": voucher.code}
     data = _mutate_checkout_add_promo_code(api_client, variables)
@@ -392,7 +395,9 @@ def test_checkout_add_category_code_checkout_with_sale(
     voucher.save()
     voucher.categories.add(category)
 
-    assert checkout.get_subtotal() > checkout.get_subtotal(discounts=[discount_info])
+    assert calculations.checkout_subtotal(checkout) > calculations.checkout_subtotal(
+        checkout, discounts=[discount_info]
+    )
     checkout_id = graphene.Node.to_global_id("Checkout", checkout.pk)
     variables = {"checkoutId": checkout_id, "promoCode": voucher.code}
     data = _mutate_checkout_add_promo_code(api_client, variables)
@@ -449,8 +454,7 @@ def test_checkout_add_many_gift_card_code(
 
 
 def test_checkout_get_total_with_gift_card(api_client, checkout_with_item, gift_card):
-    total = checkout_with_item.get_total()
-    taxed_total = TaxedMoney(total, total)
+    taxed_total = calculations.checkout_total(checkout_with_item)
     total_with_gift_card = taxed_total.gross.amount - gift_card.current_balance_amount
 
     checkout_id = graphene.Node.to_global_id("Checkout", checkout_with_item.pk)
@@ -466,11 +470,9 @@ def test_checkout_get_total_with_gift_card(api_client, checkout_with_item, gift_
 def test_checkout_get_total_with_many_gift_card(
     api_client, checkout_with_gift_card, gift_card_created_by_staff
 ):
-    total = (
-        checkout_with_gift_card.get_total()
-        - checkout_with_gift_card.get_total_gift_cards_balance()
-    )
-    taxed_total = TaxedMoney(total, total)
+    taxed_total = calculations.checkout_total(checkout_with_gift_card)
+    taxed_total.gross -= checkout_with_gift_card.get_total_gift_cards_balance()
+    taxed_total.net -= checkout_with_gift_card.get_total_gift_cards_balance()
     total_with_gift_card = (
         taxed_total.gross.amount - gift_card_created_by_staff.current_balance_amount
     )
diff --git a/tests/api/test_payment.py b/tests/api/test_payment.py
index 552b33ce052..8cd40d6ff47 100644
--- a/tests/api/test_payment.py
+++ b/tests/api/test_payment.py
@@ -2,8 +2,8 @@
 
 import graphene
 import pytest
-from prices import TaxedMoney
 
+from saleor.checkout import calculations
 from saleor.core.utils import get_country_name_by_code
 from saleor.graphql.payment.enums import OrderAction, PaymentChargeStatusEnum
 from saleor.payment.interface import CreditCardInfo, CustomerSource, TokenConfig
@@ -94,8 +94,7 @@ def test_checkout_add_payment(
 ):
     checkout = checkout_with_item
     checkout_id = graphene.Node.to_global_id("Checkout", checkout.pk)
-    total = checkout.get_total()
-    total = TaxedMoney(net=total, gross=total)
+    total = calculations.checkout_total(checkout)
     variables = {
         "checkoutId": checkout_id,
         "input": {
@@ -125,8 +124,7 @@ def test_checkout_add_payment_default_amount(
 ):
     checkout = checkout_with_item
     checkout_id = graphene.Node.to_global_id("Checkout", checkout.pk)
-    total = checkout.get_total()
-    total = TaxedMoney(net=total, gross=total)
+    total = calculations.checkout_total(checkout)
 
     variables = {
         "checkoutId": checkout_id,
@@ -162,7 +160,9 @@ def test_checkout_add_payment_bad_amount(
         "input": {
             "gateway": "DUMMY",
             "token": "sample-token",
-            "amount": str(checkout.get_total().amount + Decimal(1)),
+            "amount": str(
+                calculations.checkout_total(checkout).gross.amount + Decimal(1)
+            ),
             "billingAddress": graphql_address_data,
         },
     }
@@ -177,8 +177,7 @@ def test_use_checkout_billing_address_as_payment_billing(
 ):
     checkout = checkout_with_item
     checkout_id = graphene.Node.to_global_id("Checkout", checkout.pk)
-    total = checkout.get_total()
-    total = TaxedMoney(net=total, gross=total)
+    total = calculations.checkout_total(checkout)
     variables = {
         "checkoutId": checkout_id,
         "input": {
diff --git a/tests/extensions/sample_plugins.py b/tests/extensions/sample_plugins.py
index 63dbf961698..6a664adb93d 100644
--- a/tests/extensions/sample_plugins.py
+++ b/tests/extensions/sample_plugins.py
@@ -1,5 +1,5 @@
 from decimal import Decimal
-from typing import Union
+from typing import TYPE_CHECKING, Union
 
 from django_countries.fields import Country
 from prices import Money, TaxedMoney
@@ -8,6 +8,11 @@
 from saleor.extensions import ConfigurationTypeField
 from saleor.extensions.base_plugin import BasePlugin
 from saleor.extensions.models import PluginConfiguration
+from saleor.product.models import Product, ProductType
+
+if TYPE_CHECKING:
+    # flake8: noqa
+    from saleor.product.models import Product, ProductType
 
 
 class PluginSample(BasePlugin):
@@ -50,23 +55,23 @@ def _get_default_configuration(cls):
         }
 
     def calculate_checkout_total(self, checkout, discounts, previous_value):
-        total = Money("1.0", currency=checkout.get_total().currency)
+        total = Money("1.0", currency=checkout.currency)
         return TaxedMoney(total, total)
 
     def calculate_checkout_subtotal(self, checkout, discounts, previous_value):
-        subtotal = Money("1.0", currency=checkout.get_total().currency)
+        subtotal = Money("1.0", currency=checkout.currency)
         return TaxedMoney(subtotal, subtotal)
 
     def calculate_checkout_shipping(self, checkout, discounts, previous_value):
-        price = Money("1.0", currency=checkout.get_total().currency)
+        price = Money("1.0", currency=checkout.currency)
         return TaxedMoney(price, price)
 
     def calculate_order_shipping(self, order, previous_value):
-        price = Money("1.0", currency=order.total.currency)
+        price = Money("1.0", currency=order.currency)
         return TaxedMoney(price, price)
 
     def calculate_checkout_line_total(self, checkout_line, discounts, previous_value):
-        price = Money("1.0", currency=checkout_line.get_total().currency)
+        price = Money("1.0", currency=checkout_line.checkout.currency)
         return TaxedMoney(price, price)
 
     def calculate_order_line_unit(self, order_line, previous_value):
diff --git a/tests/extensions/test_manager.py b/tests/extensions/test_manager.py
index 0200259c798..0068493b203 100644
--- a/tests/extensions/test_manager.py
+++ b/tests/extensions/test_manager.py
@@ -30,7 +30,7 @@ def test_get_extensions_manager():
 def test_manager_calculates_checkout_total(
     checkout_with_item, discount_info, plugins, total_amount
 ):
-    currency = checkout_with_item.get_total().currency
+    currency = checkout_with_item.currency
     expected_total = Money(total_amount, currency)
     manager = ExtensionsManager(plugins=plugins)
     taxed_total = manager.calculate_checkout_total(checkout_with_item, [discount_info])
@@ -44,7 +44,7 @@ def test_manager_calculates_checkout_total(
 def test_manager_calculates_checkout_subtotal(
     checkout_with_item, discount_info, plugins, subtotal_amount
 ):
-    currency = checkout_with_item.get_total().currency
+    currency = checkout_with_item.currency
     expected_subtotal = Money(subtotal_amount, currency)
     taxed_subtotal = ExtensionsManager(plugins=plugins).calculate_checkout_subtotal(
         checkout_with_item, [discount_info]
@@ -59,7 +59,7 @@ def test_manager_calculates_checkout_subtotal(
 def test_manager_calculates_checkout_shipping(
     checkout_with_item, discount_info, plugins, shipping_amount
 ):
-    currency = checkout_with_item.get_total().currency
+    currency = checkout_with_item.currency
     expected_shipping_price = Money(shipping_amount, currency)
     taxed_shipping_price = ExtensionsManager(
         plugins=plugins
@@ -95,7 +95,7 @@ def test_manager_calculates_checkout_line_total(
     checkout_with_item, discount_info, plugins, amount
 ):
     line = checkout_with_item.lines.all()[0]
-    currency = line.get_total().currency
+    currency = checkout_with_item.currency
     expected_total = Money(amount, currency)
     taxed_total = ExtensionsManager(plugins=plugins).calculate_checkout_line_total(
         line, [discount_info]
diff --git a/tests/test_cart.py b/tests/test_cart.py
index 14910cb4b16..54fde5dcd2e 100644
--- a/tests/test_cart.py
+++ b/tests/test_cart.py
@@ -9,7 +9,7 @@
 from measurement.measures import Weight
 from prices import Money, TaxedMoney
 
-from saleor.checkout import forms, utils
+from saleor.checkout import calculations, forms, utils
 from saleor.checkout.context_processors import checkout_counter
 from saleor.checkout.models import Checkout
 from saleor.checkout.utils import (
@@ -253,8 +253,8 @@ def test_adding_same_variant(checkout, product):
     add_variant_to_checkout(checkout, variant, 2)
     assert len(checkout) == 1
     assert checkout.quantity == 3
-    subtotal = Money("30.00", "USD")
-    assert checkout.get_subtotal() == subtotal
+    subtotal = TaxedMoney(Money("30.00", "USD"), Money("30.00", "USD"))
+    assert calculations.checkout_subtotal(checkout) == subtotal
 
 
 def test_replacing_same_variant(checkout, product):
@@ -531,8 +531,8 @@ def test_view_checkout(client, request_checkout_with_item):
     checkout_line = request_checkout_with_item.lines.first()
     assert response.status_code == 200
     assert not response_checkout_line["get_total"].tax.amount
-    total = checkout_line.get_total()
-    assert response_checkout_line["get_total"] == TaxedMoney(total, total)
+    total = calculations.checkout_line_total(checkout_line)
+    assert response_checkout_line["get_total"] == total
 
 
 def test_view_update_checkout_quantity(client, request_checkout_with_item, monkeypatch):
@@ -603,8 +603,8 @@ def test_checkout_summary_page(settings, client, request_checkout_with_item):
     assert response.status_code == 200
     content = response.context
     assert content["quantity"] == request_checkout_with_item.quantity
-    checkout_total = request_checkout_with_item.get_subtotal()
-    assert content["total"] == TaxedMoney(checkout_total, checkout_total)
+    checkout_total = calculations.checkout_subtotal(request_checkout_with_item)
+    assert content["total"] == checkout_total
     assert len(content["lines"]) == 1
     checkout_line = content["lines"][0]
     variant = request_checkout_with_item.lines.get().variant
diff --git a/tests/test_checkout.py b/tests/test_checkout.py
index b6888d2d3a1..616c96dc118 100644
--- a/tests/test_checkout.py
+++ b/tests/test_checkout.py
@@ -12,8 +12,9 @@
 from saleor.account import CustomerEvents
 from saleor.account.models import Address, CustomerEvent, User
 from saleor.account.utils import store_user_address
-from saleor.checkout import AddressType, views
+from saleor.checkout import AddressType, calculations, views
 from saleor.checkout.forms import CheckoutVoucherForm, CountryForm
+from saleor.checkout.models import Checkout
 from saleor.checkout.utils import (
     add_variant_to_checkout,
     add_voucher_to_checkout,
@@ -709,13 +710,11 @@ def test_create_order_with_gift_card(
     checkout.shipping_method = shipping_method
     checkout.save()
 
-    subtotal = checkout.get_subtotal()
-    shipping_price = checkout.get_shipping_price()
+    subtotal = calculations.checkout_subtotal(checkout)
+    shipping_price = calculations.checkout_shipping_price(checkout)
     total_gross_without_gift_cards = (
-        TaxedMoney(subtotal, subtotal)
-        + TaxedMoney(shipping_price, shipping_price)
-        - checkout.discount
-    ).gross
+        subtotal.gross + shipping_price.gross - checkout.discount
+    )
     gift_cards_balance = checkout.get_total_gift_cards_balance()
 
     order = create_order(
@@ -741,8 +740,7 @@ def test_create_order_with_gift_card_partial_use(
     checkout.shipping_method = shipping_method
     checkout.save()
 
-    checkout_total = checkout.get_total()
-    price_without_gift_card = TaxedMoney(net=checkout_total, gross=checkout_total)
+    price_without_gift_card = calculations.checkout_total(checkout)
     gift_card_balance_before_order = gift_card_used.current_balance_amount
 
     checkout.gift_cards.add(gift_card_used)
@@ -781,8 +779,7 @@ def test_create_order_with_many_gift_cards(
     checkout.shipping_method = shipping_method
     checkout.save()
 
-    checkout_total = checkout.get_total()
-    price_without_gift_card = TaxedMoney(net=checkout_total, gross=checkout_total)
+    price_without_gift_card = calculations.checkout_total(checkout)
     gift_cards_balance_before_order = (
         gift_card_created_by_staff.current_balance.amount
         + gift_card.current_balance.amount
@@ -845,6 +842,7 @@ def test_get_discount_for_checkout_value_voucher(
     discount_value,
     discount_value_type,
     expected_value,
+    monkeypatch,
 ):
     voucher = Voucher(
         code="unique",
@@ -856,8 +854,15 @@ def test_get_discount_for_checkout_value_voucher(
         ),
         min_checkout_items_quantity=min_checkout_items_quantity,
     )
-    checkout = Mock(
-        get_subtotal=Mock(return_value=Money(total, "USD")), quantity=total_quantity
+    checkout = Mock(spec=Checkout, quantity=total_quantity)
+    subtotal = TaxedMoney(Money(total, "USD"), Money(total, "USD"))
+    monkeypatch.setattr(
+        "saleor.checkout.utils.calculations.checkout_subtotal",
+        lambda checkout, discounts: subtotal,
+    )
+    monkeypatch.setattr(
+        "saleor.discount.utils.calculations.checkout_subtotal",
+        lambda checkout, discounts: subtotal,
     )
     discount = get_voucher_discount_for_checkout(voucher, checkout)
     assert discount == Money(expected_value, "USD")
@@ -895,6 +900,7 @@ def test_get_discount_for_checkout_entire_order_voucher_not_applicable(
     discount_type,
     min_spent_amount,
     min_checkout_items_quantity,
+    monkeypatch,
 ):
     voucher = Voucher(
         code="unique",
@@ -906,8 +912,15 @@ def test_get_discount_for_checkout_entire_order_voucher_not_applicable(
         ),
         min_checkout_items_quantity=min_checkout_items_quantity,
     )
-    checkout = Mock(
-        get_subtotal=Mock(return_value=Money(total, "USD")), quantity=total_quantity
+    checkout = Mock(spec=Checkout, quantity=total_quantity)
+    subtotal = TaxedMoney(Money(total, "USD"), Money(total, "USD"))
+    monkeypatch.setattr(
+        "saleor.checkout.utils.calculations.checkout_subtotal",
+        lambda checkout, discounts: subtotal,
+    )
+    monkeypatch.setattr(
+        "saleor.discount.utils.calculations.checkout_subtotal",
+        lambda checkout, discounts: subtotal,
     )
     with pytest.raises(NotApplicable):
         get_voucher_discount_for_checkout(voucher, checkout)
@@ -969,6 +982,23 @@ def test_get_discount_for_checkout_specific_products_voucher_not_applicable(
         "saleor.checkout.utils.get_prices_of_discounted_specific_product",
         lambda checkout, discounts, product: [],
     )
+    monkeypatch.setattr(
+        "saleor.checkout.calculations.checkout_shipping_price",
+        lambda _: TaxedMoney(Money(0, "USD"), Money(0, "USD")),
+    )
+    monkeypatch.setattr(
+        "saleor.discount.utils.calculations.checkout_subtotal",
+        lambda checkout, discounts: TaxedMoney(
+            Money(total, "USD"), Money(total, "USD")
+        ),
+    )
+    monkeypatch.setattr(
+        "saleor.checkout.utils.calculations.checkout_subtotal",
+        lambda checkout, discounts: TaxedMoney(
+            Money(total, "USD"), Money(total, "USD")
+        ),
+    )
+
     voucher = Voucher(
         code="unique",
         type=VoucherType.SPECIFIC_PRODUCT,
@@ -979,9 +1009,7 @@ def test_get_discount_for_checkout_specific_products_voucher_not_applicable(
         ),
         min_checkout_items_quantity=min_checkout_items_quantity,
     )
-    checkout = Mock(
-        get_subtotal=Mock(return_value=Money(total, "USD")), quantity=total_quantity
-    )
+    checkout = Mock(quantity=total_quantity, spec=Checkout)
     with pytest.raises(NotApplicable):
         get_voucher_discount_for_checkout(voucher, checkout, discounts)
 
@@ -1003,11 +1031,20 @@ def test_get_discount_for_checkout_shipping_voucher(
     discount_type,
     countries,
     expected_value,
+    monkeypatch,
 ):
-    subtotal = Money(100, "USD")
+    subtotal = TaxedMoney(Money(100, "USD"), Money(100, "USD"))
+    monkeypatch.setattr(
+        "saleor.checkout.utils.calculations.checkout_subtotal",
+        lambda checkout, discounts: subtotal,
+    )
+    monkeypatch.setattr(
+        "saleor.discount.utils.calculations.checkout_subtotal",
+        lambda checkout, discounts: subtotal,
+    )
     shipping_total = Money(shipping_cost, "USD")
     checkout = Mock(
-        get_subtotal=Mock(return_value=subtotal),
+        spec=Checkout,
         is_shipping_required=Mock(return_value=True),
         shipping_method=Mock(get_total=Mock(return_value=shipping_total)),
         get_shipping_price=Mock(return_value=shipping_total),
@@ -1024,14 +1061,25 @@ def test_get_discount_for_checkout_shipping_voucher(
     assert discount == Money(expected_value, "USD")
 
 
-def test_get_discount_for_checkout_shipping_voucher_all_countries():
-    subtotal = Money(100, "USD")
-    shipping_total = Money(10, "USD")
+def test_get_discount_for_checkout_shipping_voucher_all_countries(monkeypatch):
+    subtotal = TaxedMoney(Money(100, "USD"), Money(100, "USD"))
+    monkeypatch.setattr(
+        "saleor.checkout.utils.calculations.checkout_subtotal",
+        lambda checkout, discounts: subtotal,
+    )
+    monkeypatch.setattr(
+        "saleor.discount.utils.calculations.checkout_subtotal",
+        lambda checkout, discounts: subtotal,
+    )
+    shipping_total = TaxedMoney(Money(10, "USD"), Money(10, "USD"))
+    monkeypatch.setattr(
+        "saleor.checkout.utils.calculations.checkout_shipping_price",
+        lambda checkout, discounts: shipping_total,
+    )
     checkout = Mock(
-        get_subtotal=Mock(return_value=subtotal),
+        spec=Checkout,
         is_shipping_required=Mock(return_value=True),
         shipping_method=Mock(get_total=Mock(return_value=shipping_total)),
-        get_shipping_price=Mock(return_value=shipping_total),
         shipping_address=Mock(country=Country("PL")),
     )
     voucher = Voucher(
@@ -1051,7 +1099,8 @@ def test_get_discount_for_checkout_shipping_voucher_limited_countries(monkeypatc
     subtotal = TaxedMoney(net=Money(100, "USD"), gross=Money(100, "USD"))
     shipping_total = TaxedMoney(net=Money(10, "USD"), gross=Money(10, "USD"))
     monkeypatch.setattr(
-        ExtensionsManager, "calculate_checkout_subtotal", Mock(return_value=subtotal)
+        "saleor.discount.utils.calculations.checkout_subtotal",
+        lambda checkout, discounts: subtotal,
     )
     checkout = Mock(
         get_subtotal=Mock(return_value=subtotal),
@@ -1084,7 +1133,7 @@ def test_get_discount_for_checkout_shipping_voucher_limited_countries(monkeypatc
             ["US"],
             None,
             None,
-            Money(10, "USD"),
+            TaxedMoney(Money(10, "USD"), Money(10, "USD")),
             10,
             "This offer is not valid in your country.",
         ),
@@ -1096,7 +1145,7 @@ def test_get_discount_for_checkout_shipping_voucher_limited_countries(monkeypatc
             [],
             None,
             None,
-            Money(10, "USD"),
+            TaxedMoney(Money(10, "USD"), Money(10, "USD")),
             10,
             "Please select a shipping method first.",
         ),
@@ -1108,7 +1157,7 @@ def test_get_discount_for_checkout_shipping_voucher_limited_countries(monkeypatc
             [],
             None,
             None,
-            Money(10, "USD"),
+            TaxedMoney(Money(10, "USD"), Money(10, "USD")),
             10,
             "Your order does not require shipping.",
         ),
@@ -1120,7 +1169,7 @@ def test_get_discount_for_checkout_shipping_voucher_limited_countries(monkeypatc
             [],
             5,
             None,
-            Money(2, "USD"),
+            TaxedMoney(Money(2, "USD"), Money(2, "USD")),
             10,
             "This offer is only valid for orders over $5.00.",
         ),
@@ -1132,7 +1181,7 @@ def test_get_discount_for_checkout_shipping_voucher_limited_countries(monkeypatc
             [],
             5,
             10,
-            Money(5, "USD"),
+            TaxedMoney(Money(5, "USD"), Money(5, "USD")),
             9,
             "This offer is only valid for orders with a minimum of 10 quantity.",
         ),
@@ -1144,7 +1193,7 @@ def test_get_discount_for_checkout_shipping_voucher_limited_countries(monkeypatc
             [],
             5,
             10,
-            Money(2, "USD"),
+            TaxedMoney(Money(2, "USD"), Money(2, "USD")),
             9,
             "This offer is only valid for orders over $5.00.",
         ),
@@ -1161,14 +1210,23 @@ def test_get_discount_for_checkout_shipping_voucher_not_applicable(
     subtotal,
     total_quantity,
     error_msg,
+    monkeypatch,
 ):
+    monkeypatch.setattr(
+        "saleor.checkout.utils.calculations.checkout_subtotal",
+        lambda checkout, discounts: subtotal,
+    )
+    monkeypatch.setattr(
+        "saleor.discount.utils.calculations.checkout_subtotal",
+        lambda checkout, discounts: subtotal,
+    )
     checkout = Mock(
-        get_subtotal=Mock(return_value=subtotal),
         is_shipping_required=Mock(return_value=is_shipping_required),
         shipping_method=shipping_method,
-        get_shipping_price=Mock(return_value=Money(10, "USD")),
         quantity=total_quantity,
+        spec=Checkout,
     )
+
     voucher = Voucher(
         code="unique",
         type=VoucherType.SHIPPING,
@@ -1308,7 +1366,9 @@ def test_recalculate_checkout_discount_with_sale(
     checkout = checkout_with_voucher_percentage
     recalculate_checkout_discount(checkout, [discount_info])
     assert checkout.discount == Money("1.50", "USD")
-    assert checkout.get_total(discounts=[discount_info]) == Money("13.50", "USD")
+    assert calculations.checkout_total(
+        checkout, discounts=[discount_info]
+    ).gross == Money("13.50", "USD")
 
 
 def test_recalculate_checkout_discount_voucher_not_applicable(
@@ -1345,14 +1405,18 @@ def test_recalculate_checkout_discount_free_shipping_subtotal_less_than_shipping
 ):
     checkout = checkout_with_voucher_percentage_and_shipping
 
-    shipping_method.price = checkout.get_subtotal() + Money("10.00", "USD")
+    shipping_method.price = calculations.checkout_subtotal(checkout).gross + Money(
+        "10.00", "USD"
+    )
     shipping_method.save()
 
     recalculate_checkout_discount(checkout, None)
 
     assert checkout.discount == shipping_method.price
     assert checkout.discount_name == "Free shipping"
-    assert checkout.get_total() == checkout.get_subtotal()
+    assert calculations.checkout_total(checkout) == calculations.checkout_subtotal(
+        checkout
+    )
 
 
 def test_recalculate_checkout_discount_free_shipping_subtotal_bigger_than_shipping(
@@ -1362,14 +1426,18 @@ def test_recalculate_checkout_discount_free_shipping_subtotal_bigger_than_shippi
 ):
     checkout = checkout_with_voucher_percentage_and_shipping
 
-    shipping_method.price = checkout.get_subtotal() - Money("1.00", "USD")
+    shipping_method.price = calculations.checkout_subtotal(checkout).gross - Money(
+        "1.00", "USD"
+    )
     shipping_method.save()
 
     recalculate_checkout_discount(checkout, None)
 
     assert checkout.discount == shipping_method.price
     assert checkout.discount_name == "Free shipping"
-    assert checkout.get_total() == checkout.get_subtotal()
+    assert calculations.checkout_total(checkout) == calculations.checkout_subtotal(
+        checkout
+    )
 
 
 def test_recalculate_checkout_discount_free_shipping_for_checkout_without_shipping(


Code review comments:
diff_hunk: @@ -107,7 +111,7 @@ def _validate_adddress_details(
 
 def _validate_order(order: "Order") -> bool:
     """Validate the order object if it is ready to generate a request to avatax."""
-    if not order.lines.count():
+    if not len(order.lines.all()):
path: saleor/extensions/plugins/avatax/__init__.py
text: @user1:
Microptimization: isn't `exists()` the optimal way to do this?