From 205323b69144f2cbab1b1ebd2136f97c09b4d038 Mon Sep 17 00:00:00 2001 From: Anna Sirota Date: Thu, 6 Jun 2024 16:06:08 +0200 Subject: [PATCH 1/8] Tests: add tblib to run with --parallel --- poetry.lock | 14 +++++++++++++- pyproject.toml | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 673e8b7..d303426 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1785,6 +1785,18 @@ lxml = "*" reportlab = "*" tinycss2 = ">=0.6.0" +[[package]] +name = "tblib" +version = "3.0.0" +description = "Traceback serialization library." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tblib-3.0.0-py3-none-any.whl", hash = "sha256:80a6c77e59b55e83911e1e607c649836a69c103963c5f28a46cbeef44acf8129"}, + {file = "tblib-3.0.0.tar.gz", hash = "sha256:93622790a0a29e04f0346458face1e144dc4d32f493714c6c3dff82a4adb77e6"}, +] + [[package]] name = "tinycss2" version = "1.2.1" @@ -2060,4 +2072,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "cf5844e74a99155ee23a29cc3f711e0b8577264b72ff07acd858ce16a407e05d" +content-hash = "00e67af2695baac5d34a3c9a67ac24286a6a218b30196cccc14c3a35461ffc05" diff --git a/pyproject.toml b/pyproject.toml index fba6c94..bc2788c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ flake8-implicit-str-concat = "^0.1.0" psycopg2 = "*" mypy = "^0.991" factory-boy = "^3.0" +tblib = "3.0.0" [tool.black] line-length = 100 -- 2.30.2 From 1a458a45108e8f7276b88677a670507f21aa7a69 Mon Sep 17 00:00:00 2001 From: Anna Sirota Date: Thu, 6 Jun 2024 16:08:04 +0200 Subject: [PATCH 2/8] Separate gateways fixture, to re-use it in Studio --- looper/fixtures/devfund.json | 28 ---------------------------- looper/fixtures/gateways.json | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 28 deletions(-) create mode 100644 looper/fixtures/gateways.json diff --git a/looper/fixtures/devfund.json b/looper/fixtures/devfund.json index 9f22f3c..165beab 100644 --- a/looper/fixtures/devfund.json +++ b/looper/fixtures/devfund.json @@ -1,32 +1,4 @@ [ - { - "model": "looper.gateway", - "pk": 1, - "fields": { - "name": "braintree", - "is_default": false, - "frontend_name": "Credit Card or PayPal" - } - }, - { - "model": "looper.gateway", - "pk": 2, - "fields": { - "name": "bank", - "is_default": false, - "frontend_name": "Bank Transfer", - "form_description": "

This option requires you to manually perform a bank transfer before the membership can be activated. Automatic payment is not possible with bank transfer. Bank details will be given to you when you choose this option.

" - } - }, - { - "model": "looper.gateway", - "pk": 3, - "fields": { - "name": "stripe", - "is_default": true, - "frontend_name": "Checkout with Stripe" - } - }, { "model": "looper.product", "pk": 1, diff --git a/looper/fixtures/gateways.json b/looper/fixtures/gateways.json new file mode 100644 index 0000000..b8a910e --- /dev/null +++ b/looper/fixtures/gateways.json @@ -0,0 +1,30 @@ +[ + { + "model": "looper.gateway", + "pk": 1, + "fields": { + "name": "braintree", + "is_default": false, + "frontend_name": "Credit Card or PayPal" + } + }, + { + "model": "looper.gateway", + "pk": 2, + "fields": { + "name": "bank", + "is_default": false, + "frontend_name": "Bank Transfer", + "form_description": "

This option requires you to manually perform a bank transfer before the membership can be activated. Automatic payment is not possible with bank transfer. Bank details will be given to you when you choose this option.

" + } + }, + { + "model": "looper.gateway", + "pk": 3, + "fields": { + "name": "stripe", + "is_default": true, + "frontend_name": "Checkout with Stripe" + } + } +] -- 2.30.2 From b0a34fa6f085042b9e0c717df31953be13b780f8 Mon Sep 17 00:00:00 2001 From: Anna Sirota Date: Thu, 6 Jun 2024 16:08:28 +0200 Subject: [PATCH 3/8] Migrations: fix migration that doesn't work in Blender Studio --- .../0078_move_customer_fields_to_address.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/looper/migrations/0078_move_customer_fields_to_address.py b/looper/migrations/0078_move_customer_fields_to_address.py index c4eb03e..fbb09ff 100644 --- a/looper/migrations/0078_move_customer_fields_to_address.py +++ b/looper/migrations/0078_move_customer_fields_to_address.py @@ -4,6 +4,9 @@ from django.db import migrations, models class Migration(migrations.Migration): + from django.contrib.auth import get_user_model + User = get_user_model() + user_table = User.objects.model._meta.db_table dependencies = [ ('looper', '0077_revenueperplan'), @@ -25,10 +28,20 @@ class Migration(migrations.Migration): name='vat_number', field=models.CharField(blank=True, default='', max_length=255, verbose_name='VAT identification number'), ), + migrations.RunSQL('SET CONSTRAINTS ALL IMMEDIATE;'), + # Create customer records for each account that doesn't have a customer + migrations.RunSQL( + "insert into looper_customer (user_id, full_name, company, billing_email, tax_exempt, vat_number)" + "select u.id, '', '', '', false, '' " + f"from {user_table} as u left join looper_customer as cu on u.id = cu.user_id " + "where cu.user_id is null", + ), # Create address records for accounts without any migrations.RunSQL( - "insert into looper_address (category, user_id, full_name, company, country, tax_exempt, vat_number)" - "select 'billing', cu.user_id, cu.full_name, cu.company, '', false, '' " + "insert into looper_address (category, user_id, full_name, company, country, tax_exempt, vat_number, " + "street_address, extended_address, locality, postal_code, region)" + "select 'billing', cu.user_id, cu.full_name, cu.company, '', false, '', " + "'', '', '', '', '' " "from looper_customer as cu left join looper_address as addr using(user_id) " "where addr.user_id is null and cu.user_id is not null", ), @@ -44,6 +57,7 @@ class Migration(migrations.Migration): "full_name = addr.full_name, company = addr.company " "from looper_address as addr where cu.user_id = addr.user_id", ), + migrations.RunSQL('SET CONSTRAINTS ALL DEFERRED;'), migrations.AlterField( model_name='address', name='email', -- 2.30.2 From cc6803ce3280646932c4683b313fe9fba02cd4c2 Mon Sep 17 00:00:00 2001 From: Anna Sirota Date: Fri, 7 Jun 2024 18:13:48 +0200 Subject: [PATCH 4/8] Fix get_tax --- looper/taxes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/looper/taxes.py b/looper/taxes.py index 9748b85..5b27187 100644 --- a/looper/taxes.py +++ b/looper/taxes.py @@ -181,8 +181,10 @@ class Taxable: is_business = False if user and user.is_authenticated: customer = user.customer - country_code = customer.billing_address.country or country_code - is_business = bool(customer.vat_number) + billing_address = customer.billing_address + country_code = billing_address.country or country_code + vat_number = billing_address.vat_number + is_business = bool(vat_number) tax_type, tax_rate = ProductType(product_type).get_tax( buyer_country_code=country_code, is_business=is_business ) -- 2.30.2 From 93cbd5ac561951acc97524e9894aa617348c2495 Mon Sep 17 00:00:00 2001 From: Anna Sirota Date: Mon, 10 Jun 2024 11:11:07 +0200 Subject: [PATCH 5/8] Remove changes belonging to Studio from the migration --- .../migrations/0078_move_customer_fields_to_address.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/looper/migrations/0078_move_customer_fields_to_address.py b/looper/migrations/0078_move_customer_fields_to_address.py index fbb09ff..93e54e8 100644 --- a/looper/migrations/0078_move_customer_fields_to_address.py +++ b/looper/migrations/0078_move_customer_fields_to_address.py @@ -4,9 +4,6 @@ from django.db import migrations, models class Migration(migrations.Migration): - from django.contrib.auth import get_user_model - User = get_user_model() - user_table = User.objects.model._meta.db_table dependencies = [ ('looper', '0077_revenueperplan'), @@ -29,13 +26,6 @@ class Migration(migrations.Migration): field=models.CharField(blank=True, default='', max_length=255, verbose_name='VAT identification number'), ), migrations.RunSQL('SET CONSTRAINTS ALL IMMEDIATE;'), - # Create customer records for each account that doesn't have a customer - migrations.RunSQL( - "insert into looper_customer (user_id, full_name, company, billing_email, tax_exempt, vat_number)" - "select u.id, '', '', '', false, '' " - f"from {user_table} as u left join looper_customer as cu on u.id = cu.user_id " - "where cu.user_id is null", - ), # Create address records for accounts without any migrations.RunSQL( "insert into looper_address (category, user_id, full_name, company, country, tax_exempt, vat_number, " -- 2.30.2 From f6c53505e5e66d4da0bea8b73941c64b9d65c077 Mon Sep 17 00:00:00 2001 From: Anna Sirota Date: Mon, 10 Jun 2024 17:15:37 +0200 Subject: [PATCH 6/8] Way to override cancel_url of PaymentMethodChangeView and CheckoutExistingOrderView --- looper/views/checkout_stripe.py | 5 ++++- looper/views/settings.py | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/looper/views/checkout_stripe.py b/looper/views/checkout_stripe.py index ad449be..f394a5c 100644 --- a/looper/views/checkout_stripe.py +++ b/looper/views/checkout_stripe.py @@ -147,6 +147,9 @@ class CheckoutExistingOrderView(LoginRequiredMixin, ExpectReadableIPAddressMixin # should be passed from the app, this url name is defined in example_app cancel_url = 'settings_home' + def get_cancel_url(self): + return reverse(self.cancel_url) + def get_object(self, queryset=None) -> models.Order: order_id: int = self.kwargs['order_id'] order_q = models.Order.objects.filter(customer=self.request.user.customer) @@ -171,7 +174,7 @@ class CheckoutExistingOrderView(LoginRequiredMixin, ExpectReadableIPAddressMixin # we have to do it to avoid uri-encoding of curly braces, # otherwise stripe doesn't do the template substitution success_url = success_url.replace('CHECKOUT_SESSION_ID', '{CHECKOUT_SESSION_ID}', 1) - cancel_url = self.request.build_absolute_uri(reverse(self.cancel_url)) + cancel_url = self.request.build_absolute_uri(self.get_cancel_url()) session = stripe_utils.create_stripe_checkout_session_for_order( order, success_url, diff --git a/looper/views/settings.py b/looper/views/settings.py index 99628a4..2c21aa9 100644 --- a/looper/views/settings.py +++ b/looper/views/settings.py @@ -102,7 +102,10 @@ class PaymentMethodChangeView(LoginRequiredMixin, View): log = log.getChild('PaymentMethodChangeView') subscription: models.Subscription success_url = '/' - cancel_url = '/' + cancel_url = 'settings_home' + + def get_cancel_url(self): + return reverse(self.cancel_url) def post(self, request, *args, **kwargs): self.subscription = get_object_or_404( @@ -120,7 +123,7 @@ class PaymentMethodChangeView(LoginRequiredMixin, View): # we have to do it to avoid uri-encoding of curly braces, # otherwise stripe doesn't do the template substitution success_url = success_url.replace('CHECKOUT_SESSION_ID', '{CHECKOUT_SESSION_ID}', 1) - cancel_url = self.request.build_absolute_uri(reverse(self.cancel_url)) + cancel_url = self.request.build_absolute_uri(self.get_cancel_url()) session = stripe_utils.setup_stripe_payment_method( self.subscription.currency.lower(), request.user.customer, -- 2.30.2 From e46910eea4c22fa9fc43be0a539c3a8103e7d896 Mon Sep 17 00:00:00 2001 From: Anna Sirota Date: Tue, 11 Jun 2024 16:26:49 +0200 Subject: [PATCH 7/8] Tests passing --- looper/tests/__init__.py | 4 ++-- looper/tests/factories.py | 6 ++++-- looper/tests/test_checkout_braintree.py | 2 +- looper/tests/test_checkout_stripe.py | 2 +- looper/tests/test_clock_stripe.py | 2 +- looper/tests/test_gateways.py | 2 +- looper/tests/test_moneyfield.py | 2 +- looper/tests/test_payment_method_change_stripe.py | 2 +- looper/tests/test_plan.py | 4 ++-- looper/tests/test_stripe_utils.py | 6 +++--- 10 files changed, 17 insertions(+), 15 deletions(-) diff --git a/looper/tests/__init__.py b/looper/tests/__init__.py index d098584..028c348 100644 --- a/looper/tests/__init__.py +++ b/looper/tests/__init__.py @@ -27,7 +27,7 @@ class AbstractBaseTestCase(TestCase): class AbstractLooperTestCase(AbstractBaseTestCase): log = log.getChild('AbstractLooperTestCase') - fixtures = ['devfund', 'testuser', 'systemuser'] + fixtures = ['gateways', 'devfund', 'testuser', 'systemuser'] valid_payload = { 'gateway': 'stripe', 'email': 'erik@example.com', @@ -79,7 +79,7 @@ class AbstractLooperTestCase(AbstractBaseTestCase): self.pay_meth = self.user.customer.payment_method_default def create_accountless_customer(self): - self.accountless_customer = Customer.objects.create() + self.accountless_customer = Customer.objects.create(user=None) self.accountless_customer.billing_address.save() gw = self._get_test_gateway() if gw: diff --git a/looper/tests/factories.py b/looper/tests/factories.py index e4f883a..d57f5bb 100644 --- a/looper/tests/factories.py +++ b/looper/tests/factories.py @@ -10,12 +10,11 @@ import looper.taxes User = get_user_model() +@factory.django.mute_signals(signals.pre_save, signals.post_save) class UserFactory(DjangoModelFactory): class Meta: model = User - id = factory.Sequence(lambda n: 1000 + n) - first_name = factory.Faker('first_name') last_name = factory.Faker('last_name') username = factory.LazyAttribute(lambda o: f'{o.first_name}_{o.last_name}') @@ -30,6 +29,8 @@ class CustomerFactory(DjangoModelFactory): class Meta: model = looper.models.Customer + user = factory.SubFactory(UserFactory) + class PaymentMethodFactory(DjangoModelFactory): class Meta: @@ -58,6 +59,7 @@ class SubscriptionFactory(DjangoModelFactory): class Meta: model = looper.models.Subscription + customer = factory.SubFactory(CustomerFactory) plan = factory.SubFactory(PlanFactory) payment_method = factory.SubFactory(PaymentMethodFactory) diff --git a/looper/tests/test_checkout_braintree.py b/looper/tests/test_checkout_braintree.py index 69f5cc4..65f069e 100644 --- a/looper/tests/test_checkout_braintree.py +++ b/looper/tests/test_checkout_braintree.py @@ -22,7 +22,7 @@ CHECKOUT = 'looper-braintree:checkout' # Prevent communication with Google's reCAPTCHA API. @override_settings(GOOGLE_RECAPTCHA_SECRET_KEY='') class AbstractCheckoutTestCase(AbstractLooperTestCase): - fixtures = ['devfund', 'testuser', 'systemuser'] + fixtures = ['gateways', 'devfund', 'testuser', 'systemuser'] checkout_url = reverse_lazy(CHECKOUT, kwargs={'plan_id': 2, 'plan_variation_id': 5}) def setUp(self): diff --git a/looper/tests/test_checkout_stripe.py b/looper/tests/test_checkout_stripe.py index 3ecd253..569db5f 100644 --- a/looper/tests/test_checkout_stripe.py +++ b/looper/tests/test_checkout_stripe.py @@ -15,7 +15,7 @@ User = get_user_model() class _BaseTestCase(AbstractLooperTestCase): - fixtures = ['devfund', 'testuser', 'systemuser'] + fixtures = ['gateways', 'devfund', 'testuser', 'systemuser'] checkout_url = reverse_lazy('looper:checkout', kwargs={'plan_id': 2, 'plan_variation_id': 5}) gateway_name = 'stripe' diff --git a/looper/tests/test_clock_stripe.py b/looper/tests/test_clock_stripe.py index d9315f3..865ea65 100644 --- a/looper/tests/test_clock_stripe.py +++ b/looper/tests/test_clock_stripe.py @@ -19,7 +19,7 @@ from looper.tests.factories import ( @mock.patch('looper.gateways.stripe.webhook.WebhookSignature.verify_header', return_value=True) class ClockForStripe(ResponsesMixin, TestCase): - fixtures = ['devfund', 'systemuser'] + fixtures = ['gateways', 'devfund', 'systemuser'] responses_files = ['clock_stripe.yaml'] # @_recorder.record(file_path=f'{ResponsesMixin.responses_file_path}/clock_stripe.yaml') diff --git a/looper/tests/test_gateways.py b/looper/tests/test_gateways.py index f2666a3..7b5d981 100644 --- a/looper/tests/test_gateways.py +++ b/looper/tests/test_gateways.py @@ -116,7 +116,7 @@ class BraintreeErrorTest(AbstractBaseTestCase): class BankGatewayTest(AbstractBaseTestCase): - fixtures = ['testuser', 'devfund'] + fixtures = ['testuser', 'gateways', 'devfund'] def setUp(self): super().setUp() diff --git a/looper/tests/test_moneyfield.py b/looper/tests/test_moneyfield.py index 088c5c0..3489471 100644 --- a/looper/tests/test_moneyfield.py +++ b/looper/tests/test_moneyfield.py @@ -10,7 +10,7 @@ from . import AbstractLooperTestCase, AbstractBaseTestCase class MoneyFieldTest(AbstractBaseTestCase): - fixtures = ['testuser', 'devfund', 'systemuser'] + fixtures = ['testuser', 'gateways', 'devfund', 'systemuser'] def test_create_instance(self): pv = models.PlanVariation(plan_id=1, currency='EUR', price=Money('EUR', 155)) diff --git a/looper/tests/test_payment_method_change_stripe.py b/looper/tests/test_payment_method_change_stripe.py index dbdea4a..dc811c2 100644 --- a/looper/tests/test_payment_method_change_stripe.py +++ b/looper/tests/test_payment_method_change_stripe.py @@ -15,7 +15,7 @@ User = get_user_model() class _BaseTestCase(AbstractLooperTestCase): - fixtures = ['devfund', 'testuser', 'systemuser'] + fixtures = ['gateways', 'devfund', 'testuser', 'systemuser'] gateway_name = 'stripe' subscription = None diff --git a/looper/tests/test_plan.py b/looper/tests/test_plan.py index 50d1d1a..c78627a 100644 --- a/looper/tests/test_plan.py +++ b/looper/tests/test_plan.py @@ -6,7 +6,7 @@ from ..money import Money class PlanVariationModelTestCase(django.test.TestCase): - fixtures = ['devfund'] + fixtures = ['gateways', 'devfund'] def setUp(self): product = Product.objects.create(name='Ежедневный Пророк') @@ -29,7 +29,7 @@ class PlanVariationModelTestCase(django.test.TestCase): class PlanVariationTest(django.test.TestCase): - fixtures = ['devfund'] + fixtures = ['gateways', 'devfund'] def test_variation_for_currency(self): plan = Plan.objects.get(pk=1) diff --git a/looper/tests/test_stripe_utils.py b/looper/tests/test_stripe_utils.py index 1e740e5..6a10c92 100644 --- a/looper/tests/test_stripe_utils.py +++ b/looper/tests/test_stripe_utils.py @@ -35,7 +35,7 @@ event_payload_cc = { @mock.patch('looper.gateways.stripe.webhook.WebhookSignature.verify_header', return_value=True) class UpsertOrderFromPaymentIntentAndProduct(ResponsesMixin, TestCase): - fixtures = ['devfund', 'systemuser'] + fixtures = ['gateways', 'devfund', 'systemuser'] responses_files = [ 'payment_intent__card.yaml', 'payment_intent__ideal.yaml', @@ -452,7 +452,7 @@ class UpsertOrderFromPaymentIntentAndProduct(ResponsesMixin, TestCase): class TestCheckoutPlanVariationAnonymous(ResponsesMixin, TestCase): - fixtures = ['devfund', 'systemuser'] + fixtures = ['gateways', 'devfund', 'systemuser'] responses_files = [ 'checkout_plan_variation_anonymous.yaml', 'checkout_plan_variation_webhook.yaml', @@ -535,7 +535,7 @@ class TestCheckoutPlanVariationAnonymous(ResponsesMixin, TestCase): class TestCheckoutPlanVariationLoggedIn(ResponsesMixin, TestCase): - fixtures = ['devfund', 'systemuser'] + fixtures = ['gateways', 'devfund', 'systemuser'] responses_files = [ 'checkout_plan_variation_logged_in.yaml', ] -- 2.30.2 From 981391c447b5dbb4cad579401ab0194c6927cbbf Mon Sep 17 00:00:00 2001 From: Anna Sirota Date: Fri, 14 Jun 2024 16:40:11 +0200 Subject: [PATCH 8/8] Undo unintended change to factories --- looper/tests/factories.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/looper/tests/factories.py b/looper/tests/factories.py index d57f5bb..fe6ef60 100644 --- a/looper/tests/factories.py +++ b/looper/tests/factories.py @@ -15,6 +15,8 @@ class UserFactory(DjangoModelFactory): class Meta: model = User + id = factory.Sequence(lambda n: 1000 + n) + first_name = factory.Faker('first_name') last_name = factory.Faker('last_name') username = factory.LazyAttribute(lambda o: f'{o.first_name}_{o.last_name}') -- 2.30.2