Repository: saleor/saleor
Pull request: Add default tax calculation strategy
I want to merge this change because it fixes #11343 
We want tax calculation strategy to be always provided. So I added default strategy to `tax_calculation_strategy` field of `TaxConfiguration`. 
At the moment we leave the field nullable as we want to keep backward-compatibility. 
Resolver for `tax_calculation_strategy` will return `FLAT_RATE` by default if it's `None` and calculations also treat `None` as `FLAT_RATE` by default.

We agreed, that in the future we need to implement a way to choose default `tax_calculation_strategy` for `channel` during channel creation.

<!-- Please mention all relevant issue numbers. -->

# Impact

* [ ] New migrations
* [ ] New/Updated API fields or mutations
* [ ] Deprecated API fields or mutations
* [ ] Removed API types, fields, or mutations
* [ ] Documentation needs to be updated

# Pull Request Checklist

<!-- Please keep this section. It will make the maintainer's life easier. -->

* [ ] Privileged queries and mutations are guarded by proper permission checks
* [ ] Database queries are optimized and the number of queries is constant
* [ ] Database migration files are up to date
* [ ] The changes are tested
* [ ] GraphQL schema and type definitions are up to date
* [ ] Changes are mentioned in the changelog


Problem statement:
Bug: Saleor returns totalPrice 0 if taxCalculationStrategy is null
### What are you trying to achieve?

By default, the tax charge method is empty, so if I forgot to change it and do checkout totalPrice will be 0. 

### Steps to reproduce the problem

1. Create a new channel 
2. Assign Shipping methods, Warehouse, products 
3. You can run query tax configurations to verify that tax configuration strategy for your channel is null
```
          "node": {
            "channel": {
              "name": "creazy",
            },
            "taxCalculationStrategy": null
          }
```
4. Create new checkout 
```
mutation CreateCheckout($checkoutInput: CheckoutCreateInput!) {
  checkoutCreate(input: $checkoutInput) {
    errors{
      code
      message
    }
    checkout {
      ...Checkout
    }
  }
}

fragment Price on TaxedMoney {
  gross {
    amount
    currency
  }
  net {
    amount
    currency
  }
  tax {
    amount
    currency
  }
}

fragment ProductVariant on ProductVariant {
  name
  quantityAvailable
  pricing {
    onSale
    priceUndiscounted {
      ...Price
    }
    price {
      ...Price
    }
  }
  product {
    name
  }
}

fragment CheckoutLine on CheckoutLine {
  quantity
  totalPrice {
    ...Price
  }
  variant {
    ...ProductVariant
  }
}


fragment Checkout on Checkout {
  token
  id
  totalPrice {
    ...Price
  }
  lines {
    ...CheckoutLine
  }
  subtotalPrice {
    ...Price
  }
  shippingPrice {
    ...Price
  }
  lines {
    ...CheckoutLine
  }
  isShippingRequired
  discount {
    currency
    amount
  }
  discountName
  translatedDiscountName
  voucherCode
}
```
variables:
```
{
  "checkoutInput": {
    "channel": "creazy",
    "email": "john@doe.com",
    "lines": [
      {
        "quantity": 1,
        "variantId": "UHJvZHVjdFZhcmlhbnQ6MTk4"
      }
    ]  
	}
}
```

### What did you expect to happen?

When we don’t have a tax method selected Saleor should calculate the total without taxes

### Logs

```
{
  "data": {
    "checkoutCreate": {
      "errors": [],
      "checkout": {
        "totalPrice": {
          "gross": {
            "amount": 0,
            "currency": "USD"
          },
          "net": {
            "amount": 0,
            "currency": "USD"
          },
          "tax": {
            "amount": 0,
            "currency": "USD"
          }
        },
        "lines": [
          {
            "quantity": 1,
            "totalPrice": {
              "gross": {
                "amount": 0,
                "currency": "USD"
              },
              "net": {
                "amount": 0,
                "currency": "USD"
              },
              "tax": {
                "amount": 0,
                "currency": "USD"
              }
            },
            "variant": {
              "name": "55cm x 55cm",
              "quantityAvailable": 123,
              "pricing": {
                "onSale": true,
                "priceUndiscounted": {
                  "gross": {
                    "amount": 30,
                    "currency": "USD"
                  },
                  "net": {
                    "amount": 30,
                    "currency": "USD"
                  },
                  "tax": {
                    "amount": 0,
                    "currency": "USD"
                  }
                },
                "price": {
                  "gross": {
                    "amount": 0,
                    "currency": "USD"
                  },
                  "net": {
                    "amount": 0,
                    "currency": "USD"
                  },
                  "tax": {
                    "amount": 0,
                    "currency": "USD"
                  }
                }
              },
              "product": {
                "name": "Colored Parrot Cushion",
                "slug": "colored-parrot-cushion"
              }
            }
          }
        ],
        "subtotalPrice": {
          "gross": {
            "amount": 0,
            "currency": "USD"
          },
          "net": {
            "amount": 0,
            "currency": "USD"
          },
          "tax": {
            "amount": 0,
            "currency": "USD"
          }
        }
      }
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 11,
      "maximumAvailable": 50000
    }
  }
}
```

### Environment

Saleor version: core v3.9.0-a

Hints from issue discussion:

Commit message: add default tax calculation strategy

Patch to review:
diff --git a/saleor/checkout/calculations.py b/saleor/checkout/calculations.py
index fef7b1e66cc..42fa4a8133f 100644
--- a/saleor/checkout/calculations.py
+++ b/saleor/checkout/calculations.py
@@ -348,7 +348,7 @@ def _calculate_and_add_tax(
         # Get the taxes calculated with apps and apply to checkout.
         tax_data = manager.get_taxes_for_checkout(checkout_info, lines)
         _apply_tax_data(checkout, lines, tax_data)
-    elif tax_calculation_strategy == TaxCalculationStrategy.FLAT_RATES:
+    else:
         # Get taxes calculated with flat rates and apply to checkout.
         update_checkout_prices_with_flat_rates(
             checkout, checkout_info, lines, prices_entered_with_tax, address, discounts
diff --git a/saleor/checkout/tests/test_calculations.py b/saleor/checkout/tests/test_calculations.py
index 14a4a737d2b..c8e24c64877 100644
--- a/saleor/checkout/tests/test_calculations.py
+++ b/saleor/checkout/tests/test_calculations.py
@@ -221,6 +221,44 @@ def test_fetch_checkout_prices_if_expired_flat_rates(
     assert checkout.shipping_tax_rate == Decimal("0.2300")
 
 
+@patch(
+    "saleor.checkout.calculations.update_checkout_prices_with_flat_rates",
+    wraps=update_checkout_prices_with_flat_rates,
+)
+def test_fetch_checkout_prices_if_expired_flat_rates_and_no_tax_calc_strategy(
+    mocked_update_checkout_prices_with_flat_rates,
+    checkout_with_items_and_shipping,
+    fetch_kwargs,
+):
+    # given
+    checkout = checkout_with_items_and_shipping
+    tc = checkout.channel.tax_configuration
+    tc.country_exceptions.all().delete()
+    tc.prices_entered_with_tax = True
+    tc.tax_calculation_strategy = None
+    tc.save()
+
+    country_code = checkout.shipping_address.country.code
+    for line in checkout.lines.all():
+        line.variant.product.tax_class.country_rates.update_or_create(
+            country=country_code, rate=23
+        )
+
+    checkout.shipping_method.tax_class.country_rates.update_or_create(
+        country=country_code, rate=23
+    )
+
+    # when
+    fetch_checkout_prices_if_expired(**fetch_kwargs)
+    checkout.refresh_from_db()
+    line = checkout.lines.first()
+
+    # then
+    mocked_update_checkout_prices_with_flat_rates.assert_called_once()
+    assert line.tax_rate == Decimal("0.2300")
+    assert checkout.shipping_tax_rate == Decimal("0.2300")
+
+
 @freeze_time("2020-12-12 12:00:00")
 def test_fetch_checkout_prices_if_expired_webhooks_success(
     plugins_manager,
diff --git a/saleor/graphql/tax/tests/queries/test_tax_configurations.py b/saleor/graphql/tax/tests/queries/test_tax_configurations.py
index 6ae18108630..20047c72eb6 100644
--- a/saleor/graphql/tax/tests/queries/test_tax_configurations.py
+++ b/saleor/graphql/tax/tests/queries/test_tax_configurations.py
@@ -1,5 +1,6 @@
 import graphene
 
+from .....tax import TaxCalculationStrategy
 from .....tax.models import TaxConfiguration
 from ....tests.utils import assert_no_permission, get_graphql_content
 from ..fragments import TAX_CONFIGURATION_FRAGMENT
@@ -74,3 +75,21 @@ def test_tax_configurations_filter(channel_USD, staff_api_client):
     edges = content["data"]["taxConfigurations"]["edges"]
     assert len(edges) == 1
     assert edges[0]["node"]["id"] == id
+
+
+def test_tax_configurations_query_staff_user_empty_calculation_strategy(
+    channel_USD, staff_api_client
+):
+    # given
+    tc = TaxConfiguration.objects.first()
+    tc.tax_calculation_strategy = None
+    tc.save(update_fields=["tax_calculation_strategy"])
+
+    # when
+    response = staff_api_client.post_graphql(QUERY, {})
+
+    # then
+    content = get_graphql_content(response)
+
+    node = content["data"]["taxConfigurations"]["edges"][0]["node"]
+    assert node["taxCalculationStrategy"] == TaxCalculationStrategy.FLAT_RATES
diff --git a/saleor/graphql/tax/types.py b/saleor/graphql/tax/types.py
index a30d7d9c96c..a8e7e2f6b74 100644
--- a/saleor/graphql/tax/types.py
+++ b/saleor/graphql/tax/types.py
@@ -68,6 +68,10 @@ def resolve_countries(root: models.TaxConfiguration, info):
             root.pk
         )
 
+    @staticmethod
+    def resolve_tax_calculation_strategy(root: models.TaxConfiguration, _info):
+        return root.tax_calculation_strategy or TaxCalculationStrategy.FLAT_RATES
+
 
 class TaxConfigurationCountableConnection(CountableConnection):
     class Meta:
diff --git a/saleor/tax/migrations/0006_alter_taxconfiguration_tax_calculation_strategy.py b/saleor/tax/migrations/0006_alter_taxconfiguration_tax_calculation_strategy.py
new file mode 100644
index 00000000000..7fb67c9212c
--- /dev/null
+++ b/saleor/tax/migrations/0006_alter_taxconfiguration_tax_calculation_strategy.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.16 on 2022-12-01 15:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("tax", "0005_migrate_vatlayer"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="taxconfiguration",
+            name="tax_calculation_strategy",
+            field=models.CharField(
+                blank=True,
+                choices=[("FLAT_RATES", "Flat rates"), ("TAX_APP", "Tax app")],
+                default="FLAT_RATES",
+                max_length=20,
+                null=True,
+            ),
+        ),
+    ]
diff --git a/saleor/tax/models.py b/saleor/tax/models.py
index be8b0afd8b9..d695dc8339f 100644
--- a/saleor/tax/models.py
+++ b/saleor/tax/models.py
@@ -54,7 +54,11 @@ class TaxConfiguration(ModelWithMetadata):
     )
     charge_taxes = models.BooleanField(default=True)
     tax_calculation_strategy = models.CharField(
-        max_length=20, choices=TaxCalculationStrategy.CHOICES, blank=True, null=True
+        max_length=20,
+        choices=TaxCalculationStrategy.CHOICES,
+        blank=True,
+        null=True,
+        default=TaxCalculationStrategy.FLAT_RATES,
     )
     display_gross_prices = models.BooleanField(default=True)
     prices_entered_with_tax = models.BooleanField(default=True)


Code review comments:
diff_hunk: @@ -221,6 +221,44 @@ def test_fetch_checkout_prices_if_expired_flat_rates(
     assert checkout.shipping_tax_rate == Decimal("0.2300")
 
 
+@patch(
+    "saleor.checkout.calculations.update_checkout_prices_with_flat_rates",
+    wraps=update_checkout_prices_with_flat_rates,
+)
+def test_fetch_checkout_prices_if_expired_flat_rates_and_no_tax_calc_strategy(
+    mocked_update_checkout_prices_with_flat_rates,
+    checkout_with_items_and_shipping,
+    fetch_kwargs,
+):
+    # given
+    checkout = checkout_with_items_and_shipping
+    tc = checkout.channel.tax_configuration
+    tc.country_exceptions.all().delete()
+    tc.prices_entered_with_tax = True
+    tc.tax_calculation_strategy = None
+    tc.save()
path: saleor/checkout/tests/test_calculations.py
text: @user1:
```suggestion
    tc.save(update_fields=["prices_entered_with_tax", "tax_calculation_strategy"])
```