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" + } + } +] diff --git a/looper/migrations/0078_move_customer_fields_to_address.py b/looper/migrations/0078_move_customer_fields_to_address.py index c4eb03e..93e54e8 100644 --- a/looper/migrations/0078_move_customer_fields_to_address.py +++ b/looper/migrations/0078_move_customer_fields_to_address.py @@ -25,10 +25,13 @@ 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 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 +47,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', 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 ) 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..fe6ef60 100644 --- a/looper/tests/factories.py +++ b/looper/tests/factories.py @@ -10,6 +10,7 @@ import looper.taxes User = get_user_model() +@factory.django.mute_signals(signals.pre_save, signals.post_save) class UserFactory(DjangoModelFactory): class Meta: model = User @@ -30,6 +31,8 @@ class CustomerFactory(DjangoModelFactory): class Meta: model = looper.models.Customer + user = factory.SubFactory(UserFactory) + class PaymentMethodFactory(DjangoModelFactory): class Meta: @@ -58,6 +61,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', ] 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, 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 926a6f0..520d06a 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