Stripe checkout #104411
@ -52,3 +52,7 @@ MAILGUN_WEBHOOK_SECRET=
|
|||||||
GOOGLE_RECAPTCHA_SITE_KEY=
|
GOOGLE_RECAPTCHA_SITE_KEY=
|
||||||
GOOGLE_RECAPTCHA_SECRET_KEY=
|
GOOGLE_RECAPTCHA_SECRET_KEY=
|
||||||
GOOGLE_ANALYTICS_TRACKING_ID=
|
GOOGLE_ANALYTICS_TRACKING_ID=
|
||||||
|
|
||||||
|
STRIPE_API_PUBLISHABLE_KEY=
|
||||||
|
STRIPE_API_SECRET_KEY=
|
||||||
|
STRIPE_ENDPOINT_SECRET=
|
||||||
|
@ -1,53 +1,10 @@
|
|||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.db.models import signals
|
|
||||||
|
|
||||||
from factory.django import DjangoModelFactory
|
from factory.django import DjangoModelFactory
|
||||||
import factory
|
import factory
|
||||||
|
|
||||||
import looper.models
|
from looper.tests.factories import SubscriptionFactory
|
||||||
|
|
||||||
from common.tests.factories.users import UserFactory
|
|
||||||
from subscriptions.models import Team
|
from subscriptions.models import Team
|
||||||
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentMethodFactory(DjangoModelFactory):
|
|
||||||
class Meta:
|
|
||||||
model = looper.models.PaymentMethod
|
|
||||||
|
|
||||||
gateway_id = factory.LazyAttribute(lambda _: looper.models.Gateway.objects.first().pk)
|
|
||||||
|
|
||||||
user = factory.SubFactory('common.tests.factories.users.UserFactory')
|
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionFactory(DjangoModelFactory):
|
|
||||||
class Meta:
|
|
||||||
model = looper.models.Subscription
|
|
||||||
|
|
||||||
plan_id = factory.LazyAttribute(lambda _: looper.models.Plan.objects.first().pk)
|
|
||||||
payment_method = factory.SubFactory(PaymentMethodFactory)
|
|
||||||
|
|
||||||
user = factory.SubFactory('common.tests.factories.users.UserFactory')
|
|
||||||
|
|
||||||
|
|
||||||
class OrderFactory(DjangoModelFactory):
|
|
||||||
class Meta:
|
|
||||||
model = looper.models.Order
|
|
||||||
|
|
||||||
user = factory.SubFactory('common.tests.factories.users.UserFactory')
|
|
||||||
subscription = factory.SubFactory(SubscriptionFactory)
|
|
||||||
payment_method = factory.SubFactory(PaymentMethodFactory)
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionFactory(DjangoModelFactory):
|
|
||||||
class Meta:
|
|
||||||
model = looper.models.Transaction
|
|
||||||
|
|
||||||
user = factory.SubFactory('common.tests.factories.users.UserFactory')
|
|
||||||
order = factory.SubFactory(OrderFactory)
|
|
||||||
payment_method = factory.SubFactory(PaymentMethodFactory)
|
|
||||||
|
|
||||||
|
|
||||||
class TeamFactory(DjangoModelFactory):
|
class TeamFactory(DjangoModelFactory):
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -55,38 +12,3 @@ class TeamFactory(DjangoModelFactory):
|
|||||||
|
|
||||||
name = factory.Faker('text', max_nb_chars=15)
|
name = factory.Faker('text', max_nb_chars=15)
|
||||||
subscription = factory.SubFactory(SubscriptionFactory)
|
subscription = factory.SubFactory(SubscriptionFactory)
|
||||||
|
|
||||||
|
|
||||||
@factory.django.mute_signals(signals.pre_save, signals.post_save)
|
|
||||||
class CustomerFactory(DjangoModelFactory):
|
|
||||||
class Meta:
|
|
||||||
model = looper.models.Customer
|
|
||||||
|
|
||||||
billing_email = factory.LazyAttribute(lambda o: '%s.billing@example.com' % o.user.username)
|
|
||||||
user = factory.SubFactory('common.tests.factories.users.UserFactory')
|
|
||||||
|
|
||||||
|
|
||||||
@factory.django.mute_signals(signals.pre_save, signals.post_save)
|
|
||||||
class AddressFactory(DjangoModelFactory):
|
|
||||||
class Meta:
|
|
||||||
model = looper.models.Address
|
|
||||||
|
|
||||||
user = factory.SubFactory('common.tests.factories.users.UserFactory')
|
|
||||||
|
|
||||||
|
|
||||||
# TODO(anna): this should probably move to looper
|
|
||||||
@factory.django.mute_signals(signals.pre_save, signals.post_save)
|
|
||||||
def create_customer_with_billing_address(**data):
|
|
||||||
"""Use factories to create a User with a Customer and Address records."""
|
|
||||||
customer_field_names = {f.name for f in looper.models.Customer._meta.get_fields()}
|
|
||||||
address_field_names = {f.name for f in looper.models.Address._meta.get_fields()}
|
|
||||||
user_field_names = {f.name for f in User._meta.get_fields()}
|
|
||||||
|
|
||||||
address_kwargs = {k: v for k, v in data.items() if k in address_field_names}
|
|
||||||
customer_kwargs = {k: v for k, v in data.items() if k in customer_field_names}
|
|
||||||
user_kwargs = {k: v for k, v in data.items() if k in user_field_names}
|
|
||||||
|
|
||||||
user = UserFactory(**user_kwargs)
|
|
||||||
AddressFactory(user=user, **address_kwargs)
|
|
||||||
CustomerFactory(user=user, **customer_kwargs)
|
|
||||||
return user
|
|
||||||
|
61
poetry.lock
generated
61
poetry.lock
generated
@ -504,19 +504,19 @@ test = ["djangorestframework", "graphene-django", "pytest", "pytest-cov", "pytes
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-debug-toolbar"
|
name = "django-debug-toolbar"
|
||||||
version = "2.2.1"
|
version = "4.3.0"
|
||||||
description = "A configurable set of panels that display various debug information about the current request/response."
|
description = "A configurable set of panels that display various debug information about the current request/response."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "django-debug-toolbar-2.2.1.tar.gz", hash = "sha256:7aadab5240796ffe8e93cc7dfbe2f87a204054746ff7ff93cd6d4a0c3747c853"},
|
{file = "django_debug_toolbar-4.3.0-py3-none-any.whl", hash = "sha256:e09b7dcb8417b743234dfc57c95a7c1d1d87a88844abd13b4c5387f807b31bf6"},
|
||||||
{file = "django_debug_toolbar-2.2.1-py3-none-any.whl", hash = "sha256:7feaee934608f5cdd95432154be832fe30fda6c1249018191e2c27bc0b6a965e"},
|
{file = "django_debug_toolbar-4.3.0.tar.gz", hash = "sha256:0b0dddee5ea29b9cb678593bc0d7a6d76b21d7799cb68e091a2148341a80f3c4"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
Django = ">=1.11"
|
django = ">=3.2.4"
|
||||||
sqlparse = ">=0.2.0"
|
sqlparse = ">=0.2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-loginas"
|
name = "django-loginas"
|
||||||
@ -532,19 +532,22 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-nested-admin"
|
name = "django-nested-admin"
|
||||||
version = "3.4.0"
|
version = "4.0.2"
|
||||||
description = "Django admin classes that allow for nested inlines"
|
description = "Django admin classes that allow for nested inlines"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = ">=3.6"
|
||||||
files = [
|
files = [
|
||||||
{file = "django-nested-admin-3.4.0.tar.gz", hash = "sha256:fbcf20d75a73dcbcc6285793ff936eff8df4deba5b169e0c1ab765394c562805"},
|
{file = "django-nested-admin-4.0.2.tar.gz", hash = "sha256:79a5ce80b81c41a0f48f778fb556a3721029df48580bfce60a5962ffa2ef2d47"},
|
||||||
{file = "django_nested_admin-3.4.0-py2.py3-none-any.whl", hash = "sha256:c6852c5ac632f4e698b6beda455006fd464c852459e5e858a6db832cdb23d9e1"},
|
{file = "django_nested_admin-4.0.2-py3-none-any.whl", hash = "sha256:74d8fc0d0f17862ffcece66d39b8814cb649878b3b6ba8438076145a688ea473"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
python-monkey-business = ">=1.0.0"
|
python-monkey-business = ">=1.0.0"
|
||||||
six = "*"
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["Pillow", "black", "dj-database-url", "django-selenosis", "flake8", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "selenium"]
|
||||||
|
test = ["Pillow", "dj-database-url", "django-selenosis", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "selenium"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-pipeline"
|
name = "django-pipeline"
|
||||||
@ -1297,7 +1300,7 @@ six = ">=1.11.0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "looper"
|
name = "looper"
|
||||||
version = "2.1.2"
|
version = "3.2.9"
|
||||||
description = ""
|
description = ""
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@ -1313,19 +1316,21 @@ braintree = "4.17.1"
|
|||||||
colorhash = "^1.0.3"
|
colorhash = "^1.0.3"
|
||||||
django = "^2.2.0 || 3.0 || 3.0.* || 3.2.*"
|
django = "^2.2.0 || 3.0 || 3.0.* || 3.2.*"
|
||||||
django-countries = "^7.2.1"
|
django-countries = "^7.2.1"
|
||||||
|
django-nested-admin = "^4.0.2"
|
||||||
django-pipeline = "^2.0.6"
|
django-pipeline = "^2.0.6"
|
||||||
geoip2 = "^3.0"
|
geoip2 = "^3.0"
|
||||||
python-dateutil = "^2.7"
|
python-dateutil = "^2.7"
|
||||||
python-stdnum = "^1.16"
|
python-stdnum = "^1.16"
|
||||||
requests = "^2.22"
|
requests = "^2.22"
|
||||||
|
stripe = "7.1.0"
|
||||||
xhtml2pdf = "^0.2"
|
xhtml2pdf = "^0.2"
|
||||||
zeep = "4.0.0"
|
zeep = "4.0.0"
|
||||||
|
|
||||||
[package.source]
|
[package.source]
|
||||||
type = "git"
|
type = "git"
|
||||||
url = "https://projects.blender.org/infrastructure/looper.git"
|
url = "https://projects.blender.org/infrastructure/looper.git"
|
||||||
reference = "c5f54b309d0"
|
reference = "56c6d4b"
|
||||||
resolved_reference = "c5f54b309d001912ef29ea5482864f97be8a2773"
|
resolved_reference = "56c6d4b612a4b0cae032a6b5f9dad0ac4c3608ac"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lxml"
|
name = "lxml"
|
||||||
@ -2674,6 +2679,22 @@ files = [
|
|||||||
{file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"},
|
{file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stripe"
|
||||||
|
version = "7.1.0"
|
||||||
|
description = "Python bindings for the Stripe API"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "stripe-7.1.0-py2.py3-none-any.whl", hash = "sha256:efd1e54825752c41bb311497cb5b6ae745464a57ca63bbe2847984a2409bcb0a"},
|
||||||
|
{file = "stripe-7.1.0.tar.gz", hash = "sha256:9cc2632230d5742eeb779af2b41c1510e724f498a296dfb40507de98d563f9a2"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
requests = {version = ">=2.20", markers = "python_version >= \"3.0\""}
|
||||||
|
typing-extensions = {version = ">=4.5.0", markers = "python_version >= \"3.7\""}
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tblib"
|
name = "tblib"
|
||||||
version = "3.0.0"
|
version = "3.0.0"
|
||||||
@ -2770,14 +2791,14 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.4.0"
|
version = "4.12.1"
|
||||||
description = "Backported and Experimental Type Hints for Python 3.7+"
|
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
|
{file = "typing_extensions-4.12.1-py3-none-any.whl", hash = "sha256:6024b58b69089e5a89c347397254e35f1bf02a907728ec7fee9bf0fe837d203a"},
|
||||||
{file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
|
{file = "typing_extensions-4.12.1.tar.gz", hash = "sha256:915f5e35ff76f56588223f15fdd5938f9a1cf9195c0de25130c627e4d597f6d1"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2926,4 +2947,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.8"
|
python-versions = "^3.8"
|
||||||
content-hash = "11610e2657ee40c139b3aa89ccc597309b77d5b1fa8dcc9358cbe06fb3ac5796"
|
content-hash = "8ca93ee62a2b6e396676492b70085f90ca192e5a84d742f032e884c3ef691d41"
|
||||||
|
@ -14,7 +14,7 @@ libsasscompiler = "^0.1.5"
|
|||||||
jsmin = "3.0.0"
|
jsmin = "3.0.0"
|
||||||
sorl-thumbnail = "^12.10.0"
|
sorl-thumbnail = "^12.10.0"
|
||||||
mistune = "2.0.0a4"
|
mistune = "2.0.0a4"
|
||||||
looper = {git = "https://projects.blender.org/infrastructure/looper.git", rev = "c5f54b309d0"}
|
looper = {git = "https://projects.blender.org/infrastructure/looper.git", rev = "56c6d4b"}
|
||||||
Pillow = "^8.0"
|
Pillow = "^8.0"
|
||||||
django-storages = {extras = ["google"], version = "1.11.1"}
|
django-storages = {extras = ["google"], version = "1.11.1"}
|
||||||
pymongo = "^3.10.1"
|
pymongo = "^3.10.1"
|
||||||
@ -32,7 +32,7 @@ requests-oauthlib = "^1.3.0"
|
|||||||
django-activity-stream = "^0.9.0"
|
django-activity-stream = "^0.9.0"
|
||||||
django-background-tasks-updated = {git = "https://projects.blender.org/infrastructure/django-background-tasks.git", rev ="2cbe547"}
|
django-background-tasks-updated = {git = "https://projects.blender.org/infrastructure/django-background-tasks.git", rev ="2cbe547"}
|
||||||
django-anymail = {extras = ["mailgun"], version = "8.2"}
|
django-anymail = {extras = ["mailgun"], version = "8.2"}
|
||||||
django-nested-admin = "^3.3.3"
|
django-nested-admin = "^4.0.2"
|
||||||
html5lib = "1.1"
|
html5lib = "1.1"
|
||||||
braintree = "4.17.1"
|
braintree = "4.17.1"
|
||||||
python-stdnum = "^1.16"
|
python-stdnum = "^1.16"
|
||||||
@ -57,7 +57,7 @@ django-stubs = "^1.5"
|
|||||||
pre-commit = "2.16.0"
|
pre-commit = "2.16.0"
|
||||||
ipython = "^7.17"
|
ipython = "^7.17"
|
||||||
factory-boy = "^3.0"
|
factory-boy = "^3.0"
|
||||||
django-debug-toolbar = "^2.2"
|
django-debug-toolbar = "^4.2.0"
|
||||||
flake8 = "^3.8.3"
|
flake8 = "^3.8.3"
|
||||||
flake8-docstrings = "^1.5.0"
|
flake8-docstrings = "^1.5.0"
|
||||||
freezegun = "^1.0.0"
|
freezegun = "^1.0.0"
|
||||||
|
@ -506,6 +506,12 @@ GATEWAYS = {
|
|||||||
'supported_collection_methods': {'automatic', 'manual'},
|
'supported_collection_methods': {'automatic', 'manual'},
|
||||||
},
|
},
|
||||||
'bank': {'supported_collection_methods': {'manual'}},
|
'bank': {'supported_collection_methods': {'manual'}},
|
||||||
|
'stripe': {
|
||||||
|
'api_publishable_key': _get('STRIPE_API_PUBLISHABLE_KEY'),
|
||||||
|
'api_secret_key': _get('STRIPE_API_SECRET_KEY'),
|
||||||
|
'endpoint_secret': _get('STRIPE_ENDPOINT_SECRET'),
|
||||||
|
'supported_collection_methods': {'automatic'},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Optional Sentry configuration
|
# Optional Sentry configuration
|
||||||
@ -678,5 +684,15 @@ S3DIRECT_DESTINATIONS = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# A list of payment method types used with stripe for setting a recurring payment:
|
||||||
|
# https://stripe.com/docs/api/checkout/sessions/create#create_checkout_session-payment_method_types
|
||||||
|
STRIPE_OFF_SESSION_PAYMENT_METHOD_TYPES = [
|
||||||
|
'card',
|
||||||
|
'link',
|
||||||
|
'paypal',
|
||||||
|
]
|
||||||
|
|
||||||
|
STRIPE_CHECKOUT_SUBMIT_TYPE = 'donate'
|
||||||
|
|
||||||
# Maximum number of attempts for failing background tasks
|
# Maximum number of attempts for failing background tasks
|
||||||
MAX_ATTEMPTS = 3
|
MAX_ATTEMPTS = 3
|
||||||
|
@ -5,13 +5,11 @@ import logging
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.forms.fields import Field
|
from django.forms.fields import Field
|
||||||
from django.forms.models import model_to_dict
|
|
||||||
|
|
||||||
from localflavor.administrative_areas import ADMINISTRATIVE_AREAS
|
from localflavor.administrative_areas import ADMINISTRATIVE_AREAS
|
||||||
from localflavor.generic.validators import validate_country_postcode
|
from localflavor.generic.validators import validate_country_postcode
|
||||||
from stdnum.eu import vat
|
from stdnum.eu import vat
|
||||||
import localflavor.exceptions
|
import localflavor.exceptions
|
||||||
|
|
||||||
import looper.form_fields
|
import looper.form_fields
|
||||||
import looper.forms
|
import looper.forms
|
||||||
import looper.models
|
import looper.models
|
||||||
@ -48,14 +46,6 @@ REQUIRED_FIELDS = {
|
|||||||
|
|
||||||
|
|
||||||
class BillingAddressForm(forms.ModelForm):
|
class BillingAddressForm(forms.ModelForm):
|
||||||
"""Unify Customer and Address in a single form."""
|
|
||||||
|
|
||||||
# Customer.billing_email is exposed as email in the Form
|
|
||||||
# because Looper scripts and forms already use "email" everywhere.
|
|
||||||
__customer_fields = {'billing_email': 'email', 'vat_number': 'vat_number'}
|
|
||||||
# Colliding "full_name" and "company" values are taken from and saved to the Address.
|
|
||||||
# FIXME(anna): do we need to use company and full_name on the Customer or only Address?
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = looper.models.Address
|
model = looper.models.Address
|
||||||
fields = looper.models.Address.PUBLIC_FIELDS
|
fields = looper.models.Address.PUBLIC_FIELDS
|
||||||
@ -81,20 +71,6 @@ class BillingAddressForm(forms.ModelForm):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Load additional model data from Customer and set form placeholders."""
|
"""Load additional model data from Customer and set form placeholders."""
|
||||||
instance: looper.models.Address = kwargs.get('instance')
|
|
||||||
if instance:
|
|
||||||
assert isinstance(instance, looper.models.Address), 'Must be an instance of Address'
|
|
||||||
customer = instance.user.customer
|
|
||||||
initial = kwargs.get('initial') or {}
|
|
||||||
customer_data = model_to_dict(customer, self.__customer_fields.keys(), {})
|
|
||||||
# Remap the fields, e.g. turning "billing_email" into "email"
|
|
||||||
customer_form_data = {v: customer_data[k] for k, v in self.__customer_fields.items()}
|
|
||||||
# Add Customer data into initial,
|
|
||||||
# making sure that it still overrides the instance data, as it's supposed to
|
|
||||||
kwargs['initial'] = {
|
|
||||||
**customer_form_data,
|
|
||||||
**initial,
|
|
||||||
}
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Set placeholder values on all form fields
|
# Set placeholder values on all form fields
|
||||||
@ -160,43 +136,33 @@ class BillingAddressForm(forms.ModelForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def save(self, commit=True):
|
def save(self, commit=True):
|
||||||
"""Save Customer data as well."""
|
"""Save cleared region field."""
|
||||||
# Validation against region choices is already done, because choices are set on __init__,
|
# Validation against region choices is already done, because choices are set on __init__,
|
||||||
# however Django won't set the updated blank region value if was omitted from the form.
|
# however Django won't set the updated blank region value if was omitted from the form.
|
||||||
if self.cleaned_data['region'] == '':
|
if self.cleaned_data['region'] == '':
|
||||||
self.instance.region = ''
|
self.instance.region = ''
|
||||||
instance = super().save(commit=commit)
|
instance = super().save(commit=commit)
|
||||||
|
|
||||||
customer = instance.user.customer
|
|
||||||
for model_field, form_field in self.__customer_fields.items():
|
|
||||||
setattr(customer, model_field, self.cleaned_data[form_field])
|
|
||||||
if commit:
|
|
||||||
customer.save(update_fields=self.__customer_fields)
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
class BillingAddressReadonlyForm(forms.ModelForm):
|
class PaymentForm(BillingAddressForm):
|
||||||
"""Display the billing details in a payment form but neither validate nor update them.
|
"""Handle PlanVariation ID and payment method details in the second step of the checkout.
|
||||||
|
|
||||||
Used in PaymentMethodChangeView and PayExistingOrderView.
|
Billing details are displayed as read-only and cannot be edited,
|
||||||
|
but are still used by the payment flow.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
gateway = looper.form_fields.GatewayChoiceField()
|
||||||
model = looper.models.Address
|
price = forms.CharField(widget=forms.HiddenInput(), required=True)
|
||||||
fields = looper.models.Address.PUBLIC_FIELDS
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
# These are used when a payment fails, so that the next attempt to pay can reuse
|
||||||
"""Disable all the billing details fields.
|
# the already-created subscription and order.
|
||||||
|
subscription_pk = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||||
|
order_pk = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||||
|
|
||||||
The billing details are only for display and for use by the payment flow.
|
|
||||||
"""
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
for field_name, field in self.fields.items():
|
|
||||||
if field_name not in BILLING_DETAILS_PLACEHOLDERS:
|
|
||||||
continue
|
|
||||||
field.disabled = True
|
|
||||||
|
|
||||||
email = forms.EmailField(required=False, disabled=True)
|
class AutomaticPaymentForm(PaymentForm):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SelectPlanVariationForm(forms.Form):
|
class SelectPlanVariationForm(forms.Form):
|
||||||
@ -223,53 +189,13 @@ class SelectPlanVariationForm(forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PaymentForm(BillingAddressForm):
|
class PayExistingOrderForm(forms.Form): # TODO
|
||||||
"""Handle PlanVariation ID and payment method details in the second step of the checkout.
|
|
||||||
|
|
||||||
Billing details are displayed as read-only and cannot be edited,
|
|
||||||
but are still used by the payment flow.
|
|
||||||
"""
|
|
||||||
|
|
||||||
payment_method_nonce = forms.CharField(initial='set-in-javascript', widget=forms.HiddenInput())
|
|
||||||
gateway = looper.form_fields.GatewayChoiceField()
|
|
||||||
device_data = forms.CharField(
|
|
||||||
initial='set-in-javascript', widget=forms.HiddenInput(), required=False
|
|
||||||
)
|
|
||||||
|
|
||||||
price = forms.CharField(widget=forms.HiddenInput(), required=True)
|
|
||||||
|
|
||||||
# These are used when a payment fails, so that the next attempt to pay can reuse
|
|
||||||
# the already-created subscription and order.
|
|
||||||
subscription_pk = forms.CharField(widget=forms.HiddenInput(), required=False)
|
|
||||||
order_pk = forms.CharField(widget=forms.HiddenInput(), required=False)
|
|
||||||
|
|
||||||
|
|
||||||
class AutomaticPaymentForm(PaymentForm):
|
|
||||||
"""Same as the PaymentForm, but only allows payment gateways that support transactions."""
|
|
||||||
|
|
||||||
gateway = looper.form_fields.GatewayChoiceField(
|
|
||||||
queryset=looper.models.Gateway.objects.filter(
|
|
||||||
name__in=looper.gateways.Registry.gateway_names_supports_transactions()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PayExistingOrderForm(BillingAddressReadonlyForm):
|
|
||||||
"""Same as AutomaticPaymentForm, but doesn't validate or update billing details."""
|
"""Same as AutomaticPaymentForm, but doesn't validate or update billing details."""
|
||||||
|
|
||||||
payment_method_nonce = forms.CharField(initial='set-in-javascript', widget=forms.HiddenInput())
|
|
||||||
gateway = looper.form_fields.GatewayChoiceField(
|
|
||||||
queryset=looper.models.Gateway.objects.filter(
|
|
||||||
name__in=looper.gateways.Registry.gateway_names_supports_transactions()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
device_data = forms.CharField(
|
|
||||||
initial='set-in-javascript', widget=forms.HiddenInput(), required=False
|
|
||||||
)
|
|
||||||
price = forms.CharField(widget=forms.HiddenInput(), required=True)
|
price = forms.CharField(widget=forms.HiddenInput(), required=True)
|
||||||
|
|
||||||
|
|
||||||
class ChangePaymentMethodForm(BillingAddressReadonlyForm, looper.forms.ChangePaymentMethodForm):
|
class ChangePaymentMethodForm(forms.Form): # TODO
|
||||||
"""Add full billing address to the change payment form."""
|
"""Add full billing address to the change payment form."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
@ -1,29 +1,10 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.management import call_command
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
form_description = {
|
|
||||||
'bank': 'When choosing to pay by bank, you will be required to manually perform payments and the subscription will be activated only when we receive the funds.',
|
|
||||||
'braintree': 'Automatic charges',
|
|
||||||
}
|
|
||||||
frontend_names = {
|
|
||||||
'bank': 'Bank Transfer',
|
|
||||||
'braintree': 'Credit Card or PayPal',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def add_gateways(apps, schema_editor):
|
def add_gateways(apps, schema_editor):
|
||||||
Gateway = apps.get_model('looper', 'Gateway')
|
call_command('loaddata', 'gateways.json', app_label='looper')
|
||||||
|
|
||||||
for (name, _) in Gateway._meta.get_field('name').choices:
|
|
||||||
if name == 'mock':
|
|
||||||
continue
|
|
||||||
Gateway.objects.get_or_create(
|
|
||||||
name=name,
|
|
||||||
frontend_name=frontend_names.get(name),
|
|
||||||
is_default=name.lower() == 'braintree',
|
|
||||||
form_description=form_description.get(name),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def remove_gateways(apps, schema_editor):
|
def remove_gateways(apps, schema_editor):
|
||||||
|
@ -19,7 +19,7 @@ def has_active_subscription(user: User) -> bool:
|
|||||||
active_subscriptions: 'QuerySet[Subscription]' = Subscription.objects.active()
|
active_subscriptions: 'QuerySet[Subscription]' = Subscription.objects.active()
|
||||||
|
|
||||||
return active_subscriptions.filter(
|
return active_subscriptions.filter(
|
||||||
Q(user_id=user.id) | Q(team__team_users__user_id=user.id)
|
Q(customer__user_id=user.id) | Q(team__team_users__user_id=user.id)
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
@ -33,7 +33,9 @@ def has_non_legacy_subscription(user: User) -> bool:
|
|||||||
|
|
||||||
subscriptions: 'QuerySet[Subscription]' = Subscription.objects.filter(is_legacy=False)
|
subscriptions: 'QuerySet[Subscription]' = Subscription.objects.filter(is_legacy=False)
|
||||||
|
|
||||||
return subscriptions.filter(Q(user_id=user.id) | Q(team__team_users__user_id=user.id)).exists()
|
return subscriptions.filter(
|
||||||
|
Q(customer__user_id=user.id) | Q(team__team_users__user_id=user.id)
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
def has_subscription(user: User) -> bool:
|
def has_subscription(user: User) -> bool:
|
||||||
@ -42,7 +44,7 @@ def has_subscription(user: User) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
return Subscription.objects.filter(
|
return Subscription.objects.filter(
|
||||||
Q(user_id=user.id) | Q(team__team_users__user_id=user.id)
|
Q(customer__user_id=user.id) | Q(team__team_users__user_id=user.id)
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
@ -51,7 +53,8 @@ def should_redirect_to_billing(user: User) -> bool:
|
|||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if user.subscription_set.exclude(status__in=Subscription._CANCELLED_STATUSES).count() == 0:
|
customer = user.customer
|
||||||
|
if customer.subscription_set.exclude(status__in=Subscription._CANCELLED_STATUSES).count() == 0:
|
||||||
# Only cancelled subscriptions, no need to redirect to billing
|
# Only cancelled subscriptions, no need to redirect to billing
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -60,7 +63,7 @@ def should_redirect_to_billing(user: User) -> bool:
|
|||||||
# so this seems to be the only currently available way to tell
|
# so this seems to be the only currently available way to tell
|
||||||
# when to stop showing the checkout to the customer.
|
# when to stop showing the checkout to the customer.
|
||||||
subscription.latest_order() and subscription.payment_method
|
subscription.latest_order() and subscription.payment_method
|
||||||
for subscription in user.subscription_set.all()
|
for subscription in customer.subscription_set.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -76,4 +79,4 @@ def has_not_yet_cancelled_subscription(user: User) -> bool:
|
|||||||
status__in=Subscription._CANCELLED_STATUSES
|
status__in=Subscription._CANCELLED_STATUSES
|
||||||
)
|
)
|
||||||
|
|
||||||
return not_yet_cancelled_subscriptions.filter(Q(user_id=user.id)).exists()
|
return not_yet_cancelled_subscriptions.filter(Q(customer__user_id=user.id)).exists()
|
||||||
|
@ -3,12 +3,13 @@ from typing import Set
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import transaction
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
import alphabetic_timestamp as ats
|
import alphabetic_timestamp as ats
|
||||||
import django.db.models.signals as django_signals
|
import django.db.models.signals as django_signals
|
||||||
|
|
||||||
from looper.models import Customer, Order
|
from looper.models import Order
|
||||||
import looper.admin_log
|
import looper.admin_log
|
||||||
import looper.signals
|
import looper.signals
|
||||||
|
|
||||||
@ -30,17 +31,40 @@ def timebased_order_number():
|
|||||||
|
|
||||||
|
|
||||||
@receiver(django_signals.post_save, sender=User)
|
@receiver(django_signals.post_save, sender=User)
|
||||||
def create_customer(sender, instance: User, created, **kwargs):
|
def create_customer(sender, instance: User, created, raw, **kwargs):
|
||||||
"""Create Customer on User creation."""
|
"""Create Customer on User creation."""
|
||||||
|
from looper.models import Customer
|
||||||
|
|
||||||
|
if raw:
|
||||||
|
return
|
||||||
|
|
||||||
if not created:
|
if not created:
|
||||||
return
|
return
|
||||||
logger.debug("Creating Customer for user %i" % instance.id)
|
|
||||||
# Assume billing name and email are the same, they should be able to change them later
|
my_log = logger.getChild('create_customer')
|
||||||
Customer.objects.create(
|
try:
|
||||||
user_id=instance.pk,
|
customer = instance.customer
|
||||||
billing_email=instance.email,
|
except Customer.DoesNotExist:
|
||||||
full_name=instance.full_name,
|
pass
|
||||||
|
else:
|
||||||
|
my_log.debug(
|
||||||
|
'Newly created User %d already has a Customer %d, not creating new one',
|
||||||
|
instance.pk,
|
||||||
|
customer.pk,
|
||||||
)
|
)
|
||||||
|
billing_address = customer.billing_address
|
||||||
|
my_log.info('Creating new billing address due to creation of user %s', instance.pk)
|
||||||
|
if not billing_address.pk:
|
||||||
|
billing_address.email = instance.email
|
||||||
|
billing_address.save()
|
||||||
|
return
|
||||||
|
|
||||||
|
my_log.info('Creating new Customer due to creation of user %s', instance.pk)
|
||||||
|
with transaction.atomic():
|
||||||
|
customer = Customer.objects.create(user=instance)
|
||||||
|
billing_address = customer.billing_address
|
||||||
|
billing_address.email = instance.email
|
||||||
|
billing_address.save()
|
||||||
|
|
||||||
|
|
||||||
@receiver(django_signals.pre_save, sender=Order)
|
@receiver(django_signals.pre_save, sender=Order)
|
||||||
|
@ -45,7 +45,7 @@ def send_mail_bank_transfer_required(subscription_id: int):
|
|||||||
"""Send out an email notifying about the required bank transfer payment."""
|
"""Send out an email notifying about the required bank transfer payment."""
|
||||||
subscription = looper.models.Subscription.objects.get(pk=subscription_id)
|
subscription = looper.models.Subscription.objects.get(pk=subscription_id)
|
||||||
user = subscription.user
|
user = subscription.user
|
||||||
email = user.customer.billing_email or user.email
|
email = user.customer.billing_address.email or user.email
|
||||||
assert (
|
assert (
|
||||||
email
|
email
|
||||||
), f'Cannot send notification about bank payment for subscription {subscription.pk}: no email'
|
), f'Cannot send notification about bank payment for subscription {subscription.pk}: no email'
|
||||||
@ -90,7 +90,7 @@ def send_mail_subscription_status_changed(subscription_id: int):
|
|||||||
"""Send out an email notifying about the activated subscription."""
|
"""Send out an email notifying about the activated subscription."""
|
||||||
subscription = looper.models.Subscription.objects.get(pk=subscription_id)
|
subscription = looper.models.Subscription.objects.get(pk=subscription_id)
|
||||||
user = subscription.user
|
user = subscription.user
|
||||||
email = user.customer.billing_email or user.email
|
email = user.customer.billing_address.email or user.email
|
||||||
assert email, f'Cannot send notification about subscription {subscription.pk} status: no email'
|
assert email, f'Cannot send notification about subscription {subscription.pk} status: no email'
|
||||||
if is_noreply(email):
|
if is_noreply(email):
|
||||||
raise
|
raise
|
||||||
@ -135,7 +135,7 @@ def send_mail_automatic_payment_performed(order_id: int, transaction_id: int):
|
|||||||
transaction = looper.models.Transaction.objects.get(pk=transaction_id)
|
transaction = looper.models.Transaction.objects.get(pk=transaction_id)
|
||||||
user = order.user
|
user = order.user
|
||||||
customer = user.customer
|
customer = user.customer
|
||||||
email = customer.billing_email or user.email
|
email = customer.billing_address.email or user.email
|
||||||
logger.debug('Sending %r notification to %s', order.status, email)
|
logger.debug('Sending %r notification to %s', order.status, email)
|
||||||
|
|
||||||
# An Unsubscribe record will prevent this message from being delivered by Mailgun.
|
# An Unsubscribe record will prevent this message from being delivered by Mailgun.
|
||||||
@ -282,7 +282,7 @@ def send_mail_no_payment_method(order_id: int):
|
|||||||
|
|
||||||
user = order.user
|
user = order.user
|
||||||
customer = user.customer
|
customer = user.customer
|
||||||
email = customer.billing_email or user.email
|
email = customer.billing_address.email or user.email
|
||||||
logger.debug('Sending %r notification to %s', order.status, email)
|
logger.debug('Sending %r notification to %s', order.status, email)
|
||||||
|
|
||||||
# An Unsubscribe record will prevent this message from being delivered by Mailgun.
|
# An Unsubscribe record will prevent this message from being delivered by Mailgun.
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
{# Owned subscriptions #}
|
{# Owned subscriptions #}
|
||||||
{% for subscription in user.subscription_set.all %}
|
{% for subscription in user.customer.subscription_set.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fw-bold">{% if subscription.team %}Team {% endif %}Subscription #{{ subscription.pk }}</td>
|
<td class="fw-bold">{% if subscription.team %}Team {% endif %}Subscription #{{ subscription.pk }}</td>
|
||||||
<td>{% include "subscriptions/components/pretty_status.html" %}</td>
|
<td>{% include "subscriptions/components/pretty_status.html" %}</td>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
{% endblock header_logo %}
|
{% endblock header_logo %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<p>Dear {% firstof user.customer.full_name user.customer.billing_address.full_name user.full_name user.email %},</p>
|
<p>Dear {% firstof user.customer.billing_address.full_name user.customer.billing_address.full_name user.full_name user.email %},</p>
|
||||||
{% block content %}{% endblock content %}
|
{% block content %}{% endblock content %}
|
||||||
<p>Manage subscription in your billing settings: <a href="{{ billing_url }}">{{ billing_url }}</a>.</p>
|
<p>Manage subscription in your billing settings: <a href="{{ billing_url }}">{{ billing_url }}</a>.</p>
|
||||||
<p>
|
<p>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
Dear {% firstof user.customer.full_name user.customer.billing_address.full_name user.full_name user.email %},
|
Dear {% firstof user.customer.billing_address.full_name user.customer.billing_address.full_name user.full_name user.email %},
|
||||||
{% block content %}{% endblock content %}
|
{% block content %}{% endblock content %}
|
||||||
Manage subscription in your billing settings: {{ billing_url }}.
|
Manage subscription in your billing settings: {{ billing_url }}.
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{% extends "subscriptions/emails/base.html" %}
|
{% extends "subscriptions/emails/base.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<p>
|
<p>
|
||||||
{{ user.customer.full_name|default:user.email }} has a {% include "subscriptions/components/info.html" %} that just passed its next payment date.
|
{{ user.customer.billing_address.full_name|default:user.email }} has a {% include "subscriptions/components/info.html" %} that just passed its next payment date.
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
{{ user.customer.full_name|default:user.email }} has a {% include "subscriptions/components/info.txt" %} that just passed its next payment date.
|
{{ user.customer.billing_address.full_name|default:user.email }} has a {% include "subscriptions/components/info.txt" %} that just passed its next payment date.
|
||||||
|
|
||||||
See {{ admin_url }} in the Blender Studio admin.
|
See {{ admin_url }} in the Blender Studio admin.
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
{% endblock header_logo %}
|
{% endblock header_logo %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<p>Dear {% firstof user.customer.full_name user.customer.billing_address.full_name user.full_name user.email %},</p>
|
<p>Dear {% firstof user.customer.billing_address.full_name user.customer.billing_address.full_name user.full_name user.email %},</p>
|
||||||
<p>
|
<p>
|
||||||
As you may have heard, Blender Studio's subscription system recently got a new shiny update,
|
As you may have heard, Blender Studio's subscription system recently got a new shiny update,
|
||||||
more on that in <a href="https://studio.blender.org/blog/subscription-system-update-2021/">the blog post</a>.
|
more on that in <a href="https://studio.blender.org/blog/subscription-system-update-2021/">the blog post</a>.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% load subscriptions %}Dear {% firstof user.customer.full_name user.customer.billing_address.full_name user.full_name user.email %},
|
{% load subscriptions %}Dear {% firstof user.customer.billing_address.full_name user.customer.billing_address.full_name user.full_name user.email %},
|
||||||
|
|
||||||
As you may have heard, Blender Studio's subscription system recently got a new shiny update,
|
As you may have heard, Blender Studio's subscription system recently got a new shiny update,
|
||||||
more on that in the blog post https://studio.blender.org/blog/subscription-system-update-2021/ .
|
more on that in the blog post https://studio.blender.org/blog/subscription-system-update-2021/ .
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
{% endblock header_logo %}
|
{% endblock header_logo %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<p>Dear {% firstof user.customer.full_name user.customer.billing_address.full_name user.full_name user.email %},</p>
|
<p>Dear {% firstof user.customer.billing_address.full_name user.customer.billing_address.full_name user.full_name user.email %},</p>
|
||||||
<p>Your Blender Studio subscription #{{subscription.pk}} has expired a while back. We miss you -- and you are missing some exciting content on Blender Studio as well.</p>
|
<p>Your Blender Studio subscription #{{subscription.pk}} has expired a while back. We miss you -- and you are missing some exciting content on Blender Studio as well.</p>
|
||||||
{% if latest_posts or latest_trainings %}
|
{% if latest_posts or latest_trainings %}
|
||||||
<p>Just recently, we've published:</p>
|
<p>Just recently, we've published:</p>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
Dear {% firstof user.customer.full_name user.customer.billing_address.full_name user.full_name user.email %},
|
Dear {% firstof user.customer.billing_address.full_name user.customer.billing_address.full_name user.full_name user.email %},
|
||||||
|
|
||||||
Your Blender Studio subscription #{{subscription.pk}} has expired a while back. We miss you -- and you are missing some exciting content on Blender Studio as well.{% if latest_posts or latest_trainings %} Just recently, we've published:
|
Your Blender Studio subscription #{{subscription.pk}} has expired a while back. We miss you -- and you are missing some exciting content on Blender Studio as well.{% if latest_posts or latest_trainings %} Just recently, we've published:
|
||||||
{% for post in latest_posts|slice:":2" %}
|
{% for post in latest_posts|slice:":2" %}
|
||||||
|
@ -5,7 +5,6 @@ from django import template
|
|||||||
|
|
||||||
from looper.models import PlanVariation, Subscription
|
from looper.models import PlanVariation, Subscription
|
||||||
import looper.money
|
import looper.money
|
||||||
import looper.taxes
|
|
||||||
|
|
||||||
import subscriptions.queries
|
import subscriptions.queries
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ from django.urls import reverse
|
|||||||
import factory
|
import factory
|
||||||
import responses
|
import responses
|
||||||
|
|
||||||
from common.tests.factories.subscriptions import create_customer_with_billing_address
|
from looper.tests.factories import create_customer_with_billing_address
|
||||||
import users.tests.util as util
|
import users.tests.util as util
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
@ -33,7 +33,7 @@ class BaseSubscriptionTestCase(TestCase):
|
|||||||
# Create the admin user used for logging
|
# Create the admin user used for logging
|
||||||
self.admin_user = util.create_admin_log_user()
|
self.admin_user = util.create_admin_log_user()
|
||||||
|
|
||||||
self.user = create_customer_with_billing_address(
|
self.customer = create_customer_with_billing_address(
|
||||||
full_name='Алексей Н.',
|
full_name='Алексей Н.',
|
||||||
company='Testcompany B.V.',
|
company='Testcompany B.V.',
|
||||||
street_address='Billing street 1',
|
street_address='Billing street 1',
|
||||||
@ -43,9 +43,9 @@ class BaseSubscriptionTestCase(TestCase):
|
|||||||
region='North Holland',
|
region='North Holland',
|
||||||
country='NL',
|
country='NL',
|
||||||
vat_number='NL-KVK-41202535',
|
vat_number='NL-KVK-41202535',
|
||||||
billing_email='billing@example.com',
|
email='billing@example.com',
|
||||||
)
|
)
|
||||||
self.customer = self.user.customer
|
self.user = self.customer.user
|
||||||
self.billing_address = self.customer.billing_address
|
self.billing_address = self.customer.billing_address
|
||||||
|
|
||||||
def _mock_vies_response(self, is_valid=True, is_broken=False):
|
def _mock_vies_response(self, is_valid=True, is_broken=False):
|
||||||
@ -319,7 +319,7 @@ class BaseSubscriptionTestCase(TestCase):
|
|||||||
self.assertContains(response, '<h2 class="h3">Bank details:</h2>', html=True)
|
self.assertContains(response, '<h2 class="h3">Bank details:</h2>', html=True)
|
||||||
self.assertContains(response, 'on hold')
|
self.assertContains(response, 'on hold')
|
||||||
self.assertContains(response, 'NL07 INGB 0008 4489 82')
|
self.assertContains(response, 'NL07 INGB 0008 4489 82')
|
||||||
subscription = response.wsgi_request.user.subscription_set.first()
|
subscription = response.wsgi_request.user.customer.subscription_set.first()
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
response, f'Blender Studio order-{subscription.latest_order().display_number}'
|
response, f'Blender Studio order-{subscription.latest_order().display_number}'
|
||||||
)
|
)
|
||||||
@ -341,11 +341,11 @@ class BaseSubscriptionTestCase(TestCase):
|
|||||||
self.assertEqual(len(mail.outbox), 0)
|
self.assertEqual(len(mail.outbox), 0)
|
||||||
|
|
||||||
def _assert_bank_transfer_email_is_sent(self, subscription):
|
def _assert_bank_transfer_email_is_sent(self, subscription):
|
||||||
user = subscription.user
|
customer = subscription.customer
|
||||||
self.assertEqual(len(mail.outbox), 1)
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
_write_mail(mail)
|
_write_mail(mail)
|
||||||
email = mail.outbox[0]
|
email = mail.outbox[0]
|
||||||
self.assertEqual(email.to, [user.customer.billing_email])
|
self.assertEqual(email.to, [customer.billing_address.email])
|
||||||
# TODO(anna): set the correct reply_to
|
# TODO(anna): set the correct reply_to
|
||||||
self.assertEqual(email.reply_to, [])
|
self.assertEqual(email.reply_to, [])
|
||||||
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
||||||
@ -384,11 +384,11 @@ class BaseSubscriptionTestCase(TestCase):
|
|||||||
self.assertIn('Recurring total: €\xa026.45', email_body.replace(' ', ' '))
|
self.assertIn('Recurring total: €\xa026.45', email_body.replace(' ', ' '))
|
||||||
|
|
||||||
def _assert_subscription_activated_email_is_sent(self, subscription):
|
def _assert_subscription_activated_email_is_sent(self, subscription):
|
||||||
user = subscription.user
|
customer = subscription.customer
|
||||||
self.assertEqual(len(mail.outbox), 1)
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
_write_mail(mail)
|
_write_mail(mail)
|
||||||
email = mail.outbox[0]
|
email = mail.outbox[0]
|
||||||
self.assertEqual(email.to, [user.customer.billing_email])
|
self.assertEqual(email.to, [customer.billing_address.email])
|
||||||
# TODO(anna): set the correct reply_to
|
# TODO(anna): set the correct reply_to
|
||||||
self.assertEqual(email.reply_to, [])
|
self.assertEqual(email.reply_to, [])
|
||||||
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
||||||
@ -397,17 +397,17 @@ class BaseSubscriptionTestCase(TestCase):
|
|||||||
self.assertEqual(email.alternatives[0][1], 'text/html')
|
self.assertEqual(email.alternatives[0][1], 'text/html')
|
||||||
for email_body in (email.body, email.alternatives[0][0]):
|
for email_body in (email.body, email.alternatives[0][0]):
|
||||||
self.assertIn('activated', email_body)
|
self.assertIn('activated', email_body)
|
||||||
self.assertIn(f'Dear {user.customer.full_name},', email_body)
|
self.assertIn(f'Dear {customer.billing_address.full_name},', email_body)
|
||||||
self.assertIn(reverse('user-settings-billing'), email_body)
|
self.assertIn(reverse('user-settings-billing'), email_body)
|
||||||
self.assertIn('Automatic renewal subscription', email_body)
|
self.assertIn('Automatic renewal subscription', email_body)
|
||||||
self.assertIn('Blender Studio Team', email_body)
|
self.assertIn('Blender Studio Team', email_body)
|
||||||
|
|
||||||
def _assert_team_subscription_activated_email_is_sent(self, subscription):
|
def _assert_team_subscription_activated_email_is_sent(self, subscription):
|
||||||
user = subscription.user
|
customer = subscription.customer
|
||||||
self.assertEqual(len(mail.outbox), 1)
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
_write_mail(mail)
|
_write_mail(mail)
|
||||||
email = mail.outbox[0]
|
email = mail.outbox[0]
|
||||||
self.assertEqual(email.to, [user.customer.billing_email])
|
self.assertEqual(email.to, [customer.billing_address.email])
|
||||||
# TODO(anna): set the correct reply_to
|
# TODO(anna): set the correct reply_to
|
||||||
self.assertEqual(email.reply_to, [])
|
self.assertEqual(email.reply_to, [])
|
||||||
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
||||||
@ -416,17 +416,17 @@ class BaseSubscriptionTestCase(TestCase):
|
|||||||
self.assertEqual(email.alternatives[0][1], 'text/html')
|
self.assertEqual(email.alternatives[0][1], 'text/html')
|
||||||
for email_body in (email.body, email.alternatives[0][0]):
|
for email_body in (email.body, email.alternatives[0][0]):
|
||||||
self.assertIn('activated', email_body)
|
self.assertIn('activated', email_body)
|
||||||
self.assertIn(f'Dear {user.customer.full_name},', email_body)
|
self.assertIn(f'Dear {customer.billing_address.full_name},', email_body)
|
||||||
self.assertIn(reverse('user-settings-billing'), email_body)
|
self.assertIn(reverse('user-settings-billing'), email_body)
|
||||||
self.assertIn('Automatic renewal, 15 seats subscription', email_body)
|
self.assertIn('Automatic renewal, 15 seats subscription', email_body)
|
||||||
self.assertIn('Blender Studio Team', email_body)
|
self.assertIn('Blender Studio Team', email_body)
|
||||||
|
|
||||||
def _assert_subscription_deactivated_email_is_sent(self, subscription):
|
def _assert_subscription_deactivated_email_is_sent(self, subscription):
|
||||||
user = subscription.user
|
customer = subscription.customer
|
||||||
self.assertEqual(len(mail.outbox), 1)
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
_write_mail(mail)
|
_write_mail(mail)
|
||||||
email = mail.outbox[0]
|
email = mail.outbox[0]
|
||||||
self.assertEqual(email.to, [user.customer.billing_email])
|
self.assertEqual(email.to, [customer.billing_address.email])
|
||||||
# TODO(anna): set the correct reply_to
|
# TODO(anna): set the correct reply_to
|
||||||
self.assertEqual(email.reply_to, [])
|
self.assertEqual(email.reply_to, [])
|
||||||
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
||||||
@ -444,7 +444,7 @@ class BaseSubscriptionTestCase(TestCase):
|
|||||||
self.assertEqual(len(mail.outbox), 1)
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
_write_mail(mail)
|
_write_mail(mail)
|
||||||
email = mail.outbox[0]
|
email = mail.outbox[0]
|
||||||
self.assertEqual(email.to, [user.customer.billing_email])
|
self.assertEqual(email.to, [user.customer.billing_address.email])
|
||||||
# TODO(anna): set the correct reply_to
|
# TODO(anna): set the correct reply_to
|
||||||
self.assertEqual(email.reply_to, [])
|
self.assertEqual(email.reply_to, [])
|
||||||
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
||||||
@ -454,7 +454,7 @@ class BaseSubscriptionTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(email.alternatives[0][1], 'text/html')
|
self.assertEqual(email.alternatives[0][1], 'text/html')
|
||||||
for email_body in (email.body, email.alternatives[0][0]):
|
for email_body in (email.body, email.alternatives[0][0]):
|
||||||
self.assertIn(f'Dear {user.customer.full_name},', email_body)
|
self.assertIn(f'Dear {user.customer.billing_address.full_name},', email_body)
|
||||||
self.assertIn('Automatic payment', email_body)
|
self.assertIn('Automatic payment', email_body)
|
||||||
self.assertIn('failed', email_body)
|
self.assertIn('failed', email_body)
|
||||||
self.assertIn('try again', email_body)
|
self.assertIn('try again', email_body)
|
||||||
@ -474,7 +474,7 @@ class BaseSubscriptionTestCase(TestCase):
|
|||||||
self.assertEqual(len(mail.outbox), 1)
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
_write_mail(mail)
|
_write_mail(mail)
|
||||||
email = mail.outbox[0]
|
email = mail.outbox[0]
|
||||||
self.assertEqual(email.to, [user.customer.billing_email])
|
self.assertEqual(email.to, [user.customer.billing_address.email])
|
||||||
# TODO(anna): set the correct reply_to
|
# TODO(anna): set the correct reply_to
|
||||||
self.assertEqual(email.reply_to, [])
|
self.assertEqual(email.reply_to, [])
|
||||||
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
||||||
@ -482,7 +482,7 @@ class BaseSubscriptionTestCase(TestCase):
|
|||||||
self.assertEqual(email.subject, 'Blender Studio Subscription: payment failed')
|
self.assertEqual(email.subject, 'Blender Studio Subscription: payment failed')
|
||||||
self.assertEqual(email.alternatives[0][1], 'text/html')
|
self.assertEqual(email.alternatives[0][1], 'text/html')
|
||||||
for email_body in (email.body, email.alternatives[0][0]):
|
for email_body in (email.body, email.alternatives[0][0]):
|
||||||
self.assertIn(f'Dear {user.customer.full_name},', email_body)
|
self.assertIn(f'Dear {user.customer.billing_address.full_name},', email_body)
|
||||||
self.assertIn('Automatic payment', email_body)
|
self.assertIn('Automatic payment', email_body)
|
||||||
self.assertIn('failed', email_body)
|
self.assertIn('failed', email_body)
|
||||||
self.assertIn('3 times', email_body)
|
self.assertIn('3 times', email_body)
|
||||||
@ -501,7 +501,7 @@ class BaseSubscriptionTestCase(TestCase):
|
|||||||
self.assertEqual(len(mail.outbox), 1)
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
_write_mail(mail)
|
_write_mail(mail)
|
||||||
email = mail.outbox[0]
|
email = mail.outbox[0]
|
||||||
self.assertEqual(email.to, [user.customer.billing_email])
|
self.assertEqual(email.to, [user.customer.billing_address.email])
|
||||||
# TODO(anna): set the correct reply_to
|
# TODO(anna): set the correct reply_to
|
||||||
self.assertEqual(email.reply_to, [])
|
self.assertEqual(email.reply_to, [])
|
||||||
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
# TODO(anna): set the correct from_email DEFAULT_FROM_EMAIL
|
||||||
@ -509,7 +509,7 @@ class BaseSubscriptionTestCase(TestCase):
|
|||||||
self.assertEqual(email.subject, 'Blender Studio Subscription: payment received')
|
self.assertEqual(email.subject, 'Blender Studio Subscription: payment received')
|
||||||
self.assertEqual(email.alternatives[0][1], 'text/html')
|
self.assertEqual(email.alternatives[0][1], 'text/html')
|
||||||
for email_body in (email.body, email.alternatives[0][0]):
|
for email_body in (email.body, email.alternatives[0][0]):
|
||||||
self.assertIn(f'Dear {user.customer.full_name},', email_body)
|
self.assertIn(f'Dear {user.customer.billing_address.full_name},', email_body)
|
||||||
self.assertIn('Automatic monthly payment', email_body)
|
self.assertIn('Automatic monthly payment', email_body)
|
||||||
self.assertIn('successful', email_body)
|
self.assertIn('successful', email_body)
|
||||||
self.assertIn('$\xa011.10', email_body)
|
self.assertIn('$\xa011.10', email_body)
|
||||||
@ -533,7 +533,7 @@ class BaseSubscriptionTestCase(TestCase):
|
|||||||
self.assertEqual(email.subject, 'Blender Studio managed subscription needs attention')
|
self.assertEqual(email.subject, 'Blender Studio managed subscription needs attention')
|
||||||
self.assertEqual(email.alternatives[0][1], 'text/html')
|
self.assertEqual(email.alternatives[0][1], 'text/html')
|
||||||
for email_body in (email.body, email.alternatives[0][0]):
|
for email_body in (email.body, email.alternatives[0][0]):
|
||||||
self.assertIn(f'{user.customer.full_name} has', email_body)
|
self.assertIn(f'{user.customer.billing_address.full_name} has', email_body)
|
||||||
self.assertIn('its next payment date', email_body)
|
self.assertIn('its next payment date', email_body)
|
||||||
self.assertIn('$\xa011.10', email_body)
|
self.assertIn('$\xa011.10', email_body)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
@ -551,7 +551,7 @@ class BaseSubscriptionTestCase(TestCase):
|
|||||||
self.assertEqual(email.subject, 'We miss you at Blender Studio')
|
self.assertEqual(email.subject, 'We miss you at Blender Studio')
|
||||||
self.assertEqual(email.alternatives[0][1], 'text/html')
|
self.assertEqual(email.alternatives[0][1], 'text/html')
|
||||||
for email_body in (email.body, email.alternatives[0][0]):
|
for email_body in (email.body, email.alternatives[0][0]):
|
||||||
self.assertIn(f'Dear {user.customer.full_name}', email_body)
|
self.assertIn(f'Dear {user.customer.billing_address.full_name}', email_body)
|
||||||
self.assertIn(f'#{subscription.pk}', email_body)
|
self.assertIn(f'#{subscription.pk}', email_body)
|
||||||
self.assertIn('has expired', email_body)
|
self.assertIn('has expired', email_body)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
|
@ -11,11 +11,8 @@ from looper import admin_log
|
|||||||
from looper.clock import Clock
|
from looper.clock import Clock
|
||||||
from looper.models import Gateway, Subscription
|
from looper.models import Gateway, Subscription
|
||||||
from looper.money import Money
|
from looper.money import Money
|
||||||
|
from looper.tests.factories import SubscriptionFactory, create_customer_with_billing_address
|
||||||
|
|
||||||
from common.tests.factories.subscriptions import (
|
|
||||||
SubscriptionFactory,
|
|
||||||
create_customer_with_billing_address,
|
|
||||||
)
|
|
||||||
from subscriptions.tests.base import BaseSubscriptionTestCase
|
from subscriptions.tests.base import BaseSubscriptionTestCase
|
||||||
import subscriptions.tasks
|
import subscriptions.tasks
|
||||||
import users.tasks
|
import users.tasks
|
||||||
@ -31,7 +28,7 @@ class TestClock(BaseSubscriptionTestCase):
|
|||||||
# print('fake now:', mock_now.return_value)
|
# print('fake now:', mock_now.return_value)
|
||||||
subscription = SubscriptionFactory(
|
subscription = SubscriptionFactory(
|
||||||
user=user,
|
user=user,
|
||||||
payment_method__user_id=user.pk,
|
payment_method__customer_id=user.customer.pk,
|
||||||
payment_method__recognisable_name='Test payment method',
|
payment_method__recognisable_name='Test payment method',
|
||||||
payment_method__gateway=Gateway.objects.get(name='braintree'),
|
payment_method__gateway=Gateway.objects.get(name='braintree'),
|
||||||
currency='USD',
|
currency='USD',
|
||||||
|
@ -21,7 +21,6 @@ class TestBillingAddressForm(BaseSubscriptionTestCase):
|
|||||||
def test_instance_loads_both_address_and_customer_data(self):
|
def test_instance_loads_both_address_and_customer_data(self):
|
||||||
form = BillingAddressForm(instance=self.billing_address)
|
form = BillingAddressForm(instance=self.billing_address)
|
||||||
|
|
||||||
# N.B.: email is loaded from Customer.billing_email
|
|
||||||
self.assertEqual(form['email'].value(), 'billing@example.com')
|
self.assertEqual(form['email'].value(), 'billing@example.com')
|
||||||
self.assertEqual(form['company'].value(), 'Testcompany B.V.')
|
self.assertEqual(form['company'].value(), 'Testcompany B.V.')
|
||||||
self.assertEqual(form['country'].value(), 'NL')
|
self.assertEqual(form['country'].value(), 'NL')
|
||||||
@ -223,7 +222,6 @@ class TestPaymentForm(BaseSubscriptionTestCase):
|
|||||||
def test_instance_loads_both_address_and_customer_data(self):
|
def test_instance_loads_both_address_and_customer_data(self):
|
||||||
form = PaymentForm(instance=self.billing_address)
|
form = PaymentForm(instance=self.billing_address)
|
||||||
|
|
||||||
# N.B.: email is loaded from Customer.billing_email
|
|
||||||
self.assertEqual(form['email'].value(), 'billing@example.com')
|
self.assertEqual(form['email'].value(), 'billing@example.com')
|
||||||
self.assertEqual(form['company'].value(), 'Testcompany B.V.')
|
self.assertEqual(form['company'].value(), 'Testcompany B.V.')
|
||||||
self.assertEqual(form['country'].value(), 'NL')
|
self.assertEqual(form['country'].value(), 'NL')
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from looper.tests.factories import SubscriptionFactory
|
||||||
|
|
||||||
from common.tests.factories.users import UserFactory
|
from common.tests.factories.users import UserFactory
|
||||||
from common.tests.factories.subscriptions import SubscriptionFactory, TeamFactory
|
from common.tests.factories.subscriptions import TeamFactory
|
||||||
|
|
||||||
import looper.models
|
import looper.models
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
'settings/billing-address/',
|
'settings/billing-address/',
|
||||||
settings.BillingAddressView.as_view(),
|
looper_settings.BillingAddressView.as_view(),
|
||||||
name='billing-address',
|
name='billing-address',
|
||||||
),
|
),
|
||||||
path('settings/receipts/', looper_settings.settings_receipts, name='receipts'),
|
path('settings/receipts/', looper_settings.settings_receipts, name='receipts'),
|
||||||
|
@ -10,7 +10,7 @@ from django.shortcuts import redirect, get_object_or_404
|
|||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
|
|
||||||
from looper.middleware import COUNTRY_CODE_SESSION_KEY
|
from looper.middleware import COUNTRY_CODE_SESSION_KEY
|
||||||
from looper.views.checkout import AbstractPaymentView, CheckoutView
|
from looper.views.checkout_braintree import AbstractPaymentView, CheckoutView
|
||||||
import looper.gateways
|
import looper.gateways
|
||||||
import looper.middleware
|
import looper.middleware
|
||||||
import looper.models
|
import looper.models
|
||||||
@ -45,7 +45,7 @@ class _JoinMixin:
|
|||||||
|
|
||||||
def _get_existing_subscription(self):
|
def _get_existing_subscription(self):
|
||||||
# Exclude cancelled subscriptions because they cannot transition to active
|
# Exclude cancelled subscriptions because they cannot transition to active
|
||||||
existing_subscriptions = self.request.user.subscription_set.exclude(
|
existing_subscriptions = self.request.user.customer.subscription_set.exclude(
|
||||||
status__in=looper.models.Subscription._CANCELLED_STATUSES
|
status__in=looper.models.Subscription._CANCELLED_STATUSES
|
||||||
)
|
)
|
||||||
return existing_subscriptions.first()
|
return existing_subscriptions.first()
|
||||||
@ -213,7 +213,6 @@ class ConfirmAndPayView(_JoinMixin, LoginRequiredMixin, FormView):
|
|||||||
ctx = {
|
ctx = {
|
||||||
**super().get_context_data(**kwargs),
|
**super().get_context_data(**kwargs),
|
||||||
'current_plan_variation': self.plan_variation,
|
'current_plan_variation': self.plan_variation,
|
||||||
'client_token': self.get_client_token(currency) if self.customer else None,
|
|
||||||
'subscription': self.subscription,
|
'subscription': self.subscription,
|
||||||
}
|
}
|
||||||
return ctx
|
return ctx
|
||||||
|
@ -35,7 +35,9 @@ class SingleSubscriptionMixin(LoginRequiredMixin):
|
|||||||
|
|
||||||
def get_subscription(self) -> Subscription:
|
def get_subscription(self) -> Subscription:
|
||||||
"""Retrieve Subscription object."""
|
"""Retrieve Subscription object."""
|
||||||
return get_object_or_404(self.request.user.subscription_set, pk=self.subscription_id)
|
return get_object_or_404(
|
||||||
|
self.request.user.customer.subscription_set, pk=self.subscription_id
|
||||||
|
)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs) -> dict:
|
def get_context_data(self, **kwargs) -> dict:
|
||||||
"""Add Subscription to the template context."""
|
"""Add Subscription to the template context."""
|
||||||
@ -51,7 +53,7 @@ class SingleSubscriptionMixin(LoginRequiredMixin):
|
|||||||
The AnonymousUser instance doesn't have a 'subscriptions' property,
|
The AnonymousUser instance doesn't have a 'subscriptions' property,
|
||||||
but login checking only happens in the super().dispatch() call.
|
but login checking only happens in the super().dispatch() call.
|
||||||
"""
|
"""
|
||||||
if not hasattr(request.user, 'subscription_set'):
|
if not hasattr(request.user.customer, 'subscription_set'):
|
||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
response = self.pre_dispatch(request, *args, **kwargs)
|
response = self.pre_dispatch(request, *args, **kwargs)
|
||||||
if response:
|
if response:
|
||||||
|
@ -5,7 +5,7 @@ import logging
|
|||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
|
|
||||||
from looper.views.checkout import AbstractPaymentView
|
from looper.views.checkout_braintree import AbstractPaymentView
|
||||||
import looper.gateways
|
import looper.gateways
|
||||||
import looper.middleware
|
import looper.middleware
|
||||||
import looper.models
|
import looper.models
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
"""Views handling subscription management."""
|
"""Views handling subscription management."""
|
||||||
from typing import Optional
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.http import HttpResponseForbidden
|
from django.http import HttpResponseForbidden
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
@ -10,10 +8,11 @@ from django.urls import reverse_lazy, reverse
|
|||||||
from django.views.generic import UpdateView, FormView
|
from django.views.generic import UpdateView, FormView
|
||||||
|
|
||||||
import looper.models
|
import looper.models
|
||||||
|
import looper.views.checkout_braintree
|
||||||
import looper.views.settings
|
import looper.views.settings
|
||||||
|
import looper.views.settings_braintree
|
||||||
|
|
||||||
from subscriptions.forms import (
|
from subscriptions.forms import (
|
||||||
BillingAddressForm,
|
|
||||||
CancelSubscriptionForm,
|
CancelSubscriptionForm,
|
||||||
ChangePaymentMethodForm,
|
ChangePaymentMethodForm,
|
||||||
PayExistingOrderForm,
|
PayExistingOrderForm,
|
||||||
@ -26,25 +25,6 @@ logger = logging.getLogger(__name__)
|
|||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
class BillingAddressView(LoginRequiredMixin, UpdateView):
|
|
||||||
"""Combine looper's Customer and Address into a billing address."""
|
|
||||||
|
|
||||||
template_name = 'settings/billing_address.html'
|
|
||||||
model = looper.models.Address
|
|
||||||
form_class = BillingAddressForm
|
|
||||||
success_url = reverse_lazy('subscriptions:billing-address')
|
|
||||||
|
|
||||||
def _get_customer_object(self) -> Optional[looper.models.Customer]:
|
|
||||||
if self.request.user.is_anonymous:
|
|
||||||
return None
|
|
||||||
return self.request.user.customer
|
|
||||||
|
|
||||||
def get_object(self, queryset=None) -> Optional[looper.models.Address]:
|
|
||||||
"""Get billing address."""
|
|
||||||
customer = self._get_customer_object()
|
|
||||||
return customer.billing_address if customer else None
|
|
||||||
|
|
||||||
|
|
||||||
class CancelSubscriptionView(SingleSubscriptionMixin, FormView):
|
class CancelSubscriptionView(SingleSubscriptionMixin, FormView):
|
||||||
"""Confirm and cancel a subscription."""
|
"""Confirm and cancel a subscription."""
|
||||||
|
|
||||||
@ -67,7 +47,7 @@ class CancelSubscriptionView(SingleSubscriptionMixin, FormView):
|
|||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
class PaymentMethodChangeView(looper.views.settings.PaymentMethodChangeView):
|
class PaymentMethodChangeView(looper.views.settings_braintree.PaymentMethodChangeView):
|
||||||
"""Use the Braintree drop-in UI to switch a subscription's payment method."""
|
"""Use the Braintree drop-in UI to switch a subscription's payment method."""
|
||||||
|
|
||||||
template_name = 'subscriptions/payment_method_change.html'
|
template_name = 'subscriptions/payment_method_change.html'
|
||||||
@ -98,7 +78,7 @@ class PaymentMethodChangeView(looper.views.settings.PaymentMethodChangeView):
|
|||||||
return super().form_invalid(form)
|
return super().form_invalid(form)
|
||||||
|
|
||||||
|
|
||||||
class PayExistingOrderView(looper.views.checkout.CheckoutExistingOrderView):
|
class PayExistingOrderView(looper.views.checkout_braintree.CheckoutExistingOrderView):
|
||||||
"""Override looper's view with our forms."""
|
"""Override looper's view with our forms."""
|
||||||
|
|
||||||
# Redirect to LOGIN_URL instead of raising an exception
|
# Redirect to LOGIN_URL instead of raising an exception
|
||||||
@ -111,7 +91,7 @@ class PayExistingOrderView(looper.views.checkout.CheckoutExistingOrderView):
|
|||||||
"""Prefill the payment amount and missing form data, if any."""
|
"""Prefill the payment amount and missing form data, if any."""
|
||||||
initial = {
|
initial = {
|
||||||
'price': self.order.price.decimals_string,
|
'price': self.order.price.decimals_string,
|
||||||
'email': self.customer.billing_email,
|
'email': self.customer.billing_address.email,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Only set initial values if they aren't already saved to the billing address.
|
# Only set initial values if they aren't already saved to the billing address.
|
||||||
@ -137,7 +117,7 @@ class PayExistingOrderView(looper.views.checkout.CheckoutExistingOrderView):
|
|||||||
if request.user.is_authenticated and self.order.user_id != request.user.id:
|
if request.user.is_authenticated and self.order.user_id != request.user.id:
|
||||||
return HttpResponseForbidden()
|
return HttpResponseForbidden()
|
||||||
self.plan = self.order.subscription.plan
|
self.plan = self.order.subscription.plan
|
||||||
return super(looper.views.checkout.CheckoutExistingOrderView, self).dispatch(
|
return super(looper.views.checkout_braintree.CheckoutExistingOrderView, self).dispatch(
|
||||||
request, *args, **kwargs
|
request, *args, **kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic.base import TemplateView
|
||||||
|
|
||||||
from looper.views.checkout import AbstractPaymentView
|
from looper.views.checkout_braintree import AbstractPaymentView
|
||||||
import looper.models
|
import looper.models
|
||||||
|
|
||||||
import subscriptions.models
|
import subscriptions.models
|
||||||
|
@ -12,7 +12,7 @@ from looper.tests.test_preferred_currency import EURO_IPV4, USA_IPV4, SINGAPORE_
|
|||||||
from looper.money import Money
|
from looper.money import Money
|
||||||
import looper.models
|
import looper.models
|
||||||
|
|
||||||
from common.tests.factories.subscriptions import create_customer_with_billing_address
|
from looper.tests.factories import create_customer_with_billing_address
|
||||||
from common.tests.factories.users import UserFactory
|
from common.tests.factories.users import UserFactory
|
||||||
from subscriptions.tests.base import BaseSubscriptionTestCase
|
from subscriptions.tests.base import BaseSubscriptionTestCase
|
||||||
import subscriptions.tasks
|
import subscriptions.tasks
|
||||||
@ -445,7 +445,7 @@ class TestPOSTConfirmAndPayView(BaseSubscriptionTestCase):
|
|||||||
|
|
||||||
self._assert_transactionless_done_page_displayed(response)
|
self._assert_transactionless_done_page_displayed(response)
|
||||||
|
|
||||||
subscription = user.subscription_set.first()
|
subscription = user.customer.subscription_set.first()
|
||||||
self.assertEqual(subscription.status, 'on-hold')
|
self.assertEqual(subscription.status, 'on-hold')
|
||||||
self.assertEqual(subscription.price, Money('EUR', 1490))
|
self.assertEqual(subscription.price, Money('EUR', 1490))
|
||||||
self.assertEqual(subscription.tax, Money('EUR', 259))
|
self.assertEqual(subscription.tax, Money('EUR', 259))
|
||||||
@ -524,7 +524,7 @@ class TestPOSTConfirmAndPayView(BaseSubscriptionTestCase):
|
|||||||
|
|
||||||
self._assert_transactionless_done_page_displayed(response)
|
self._assert_transactionless_done_page_displayed(response)
|
||||||
|
|
||||||
subscription = user.subscription_set.first()
|
subscription = user.customer.subscription_set.first()
|
||||||
self.assertEqual(subscription.status, 'on-hold')
|
self.assertEqual(subscription.status, 'on-hold')
|
||||||
self.assertEqual(subscription.price, Money('EUR', 3200))
|
self.assertEqual(subscription.price, Money('EUR', 3200))
|
||||||
self.assertEqual(subscription.tax, Money('EUR', 0))
|
self.assertEqual(subscription.tax, Money('EUR', 0))
|
||||||
@ -607,7 +607,7 @@ class TestPOSTConfirmAndPayView(BaseSubscriptionTestCase):
|
|||||||
|
|
||||||
self._assert_done_page_displayed(response)
|
self._assert_done_page_displayed(response)
|
||||||
|
|
||||||
subscription = user.subscription_set.first()
|
subscription = user.customer.subscription_set.first()
|
||||||
order = subscription.latest_order()
|
order = subscription.latest_order()
|
||||||
self.assertEqual(subscription.status, 'active')
|
self.assertEqual(subscription.status, 'active')
|
||||||
self.assertEqual(subscription.price, Money('EUR', 990))
|
self.assertEqual(subscription.price, Money('EUR', 990))
|
||||||
@ -655,7 +655,7 @@ class TestPOSTConfirmAndPayView(BaseSubscriptionTestCase):
|
|||||||
|
|
||||||
self._assert_done_page_displayed(response)
|
self._assert_done_page_displayed(response)
|
||||||
|
|
||||||
subscription = user.subscription_set.first()
|
subscription = user.customer.subscription_set.first()
|
||||||
order = subscription.latest_order()
|
order = subscription.latest_order()
|
||||||
self.assertEqual(subscription.status, 'active')
|
self.assertEqual(subscription.status, 'active')
|
||||||
self.assertEqual(subscription.price, Money('EUR', 9000))
|
self.assertEqual(subscription.price, Money('EUR', 9000))
|
||||||
@ -693,7 +693,7 @@ class TestPOSTConfirmAndPayView(BaseSubscriptionTestCase):
|
|||||||
|
|
||||||
self._assert_done_page_displayed(response)
|
self._assert_done_page_displayed(response)
|
||||||
|
|
||||||
subscription = user.subscription_set.first()
|
subscription = user.customer.subscription_set.first()
|
||||||
self.assertEqual(subscription.status, 'active')
|
self.assertEqual(subscription.status, 'active')
|
||||||
self.assertEqual(subscription.price, Money('EUR', 1490))
|
self.assertEqual(subscription.price, Money('EUR', 1490))
|
||||||
self.assertEqual(subscription.tax, Money('EUR', 0))
|
self.assertEqual(subscription.tax, Money('EUR', 0))
|
||||||
|
@ -10,11 +10,9 @@ from freezegun import freeze_time
|
|||||||
from looper.tests.factories import PaymentMethodFactory, OrderFactory
|
from looper.tests.factories import PaymentMethodFactory, OrderFactory
|
||||||
import looper.taxes
|
import looper.taxes
|
||||||
|
|
||||||
from common.tests.factories.subscriptions import (
|
from common.tests.factories.subscriptions import TeamFactory
|
||||||
TeamFactory,
|
|
||||||
create_customer_with_billing_address,
|
|
||||||
)
|
|
||||||
from common.tests.factories.users import UserFactory
|
from common.tests.factories.users import UserFactory
|
||||||
|
from looper.tests.factories import create_customer_with_billing_address
|
||||||
|
|
||||||
expected_text_tmpl = '''Invoice
|
expected_text_tmpl = '''Invoice
|
||||||
Blender Studio B.V.
|
Blender Studio B.V.
|
||||||
|
@ -7,7 +7,7 @@ from freezegun import freeze_time
|
|||||||
|
|
||||||
from looper.tests.test_preferred_currency import EURO_IPV4, USA_IPV4 # , SINGAPORE_IPV4
|
from looper.tests.test_preferred_currency import EURO_IPV4, USA_IPV4 # , SINGAPORE_IPV4
|
||||||
|
|
||||||
from common.tests.factories.subscriptions import create_customer_with_billing_address
|
from looper.tests.factories import create_customer_with_billing_address
|
||||||
from subscriptions.tests.base import BaseSubscriptionTestCase
|
from subscriptions.tests.base import BaseSubscriptionTestCase
|
||||||
|
|
||||||
# **N.B.**: test cases below require settings.GEOIP2_DB to point to an existing GeoLite2 database.
|
# **N.B.**: test cases below require settings.GEOIP2_DB to point to an existing GeoLite2 database.
|
||||||
|
@ -4,9 +4,9 @@ from django.urls import reverse
|
|||||||
|
|
||||||
from looper.models import PaymentMethod, PaymentMethodAuthentication, Gateway
|
from looper.models import PaymentMethod, PaymentMethodAuthentication, Gateway
|
||||||
from looper.money import Money
|
from looper.money import Money
|
||||||
|
from looper.tests.factories import SubscriptionFactory
|
||||||
|
|
||||||
from common.tests.factories.users import UserFactory
|
from common.tests.factories.users import UserFactory
|
||||||
from common.tests.factories.subscriptions import SubscriptionFactory
|
|
||||||
from subscriptions.tests.base import BaseSubscriptionTestCase
|
from subscriptions.tests.base import BaseSubscriptionTestCase
|
||||||
import subscriptions.tasks
|
import subscriptions.tasks
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ class TestSubscriptionSettingsBillingAddress(BaseSubscriptionTestCase):
|
|||||||
user = UserFactory()
|
user = UserFactory()
|
||||||
self.client.force_login(user)
|
self.client.force_login(user)
|
||||||
|
|
||||||
url = reverse('subscriptions:billing-address')
|
url = reverse('user-settings-billing')
|
||||||
response = self.client.post(url, full_billing_address_data)
|
response = self.client.post(url, full_billing_address_data)
|
||||||
|
|
||||||
# Check that the redirect on success happened
|
# Check that the redirect on success happened
|
||||||
@ -51,15 +51,14 @@ class TestSubscriptionSettingsBillingAddress(BaseSubscriptionTestCase):
|
|||||||
self.assertEqual(address.company, 'Test LLC')
|
self.assertEqual(address.company, 'Test LLC')
|
||||||
|
|
||||||
# Check that customer fields were updated as well
|
# Check that customer fields were updated as well
|
||||||
self.assertEqual(customer.vat_number, 'NL818152011B01')
|
self.assertEqual(customer.billing_address.vat_number, 'NL818152011B01')
|
||||||
# N.B.: email is saved as Customer.billing_email
|
self.assertEqual(customer.billing_address.email, 'my.billing.email@example.com')
|
||||||
self.assertEqual(customer.billing_email, 'my.billing.email@example.com')
|
|
||||||
|
|
||||||
def test_invalid_missing_required_fields(self):
|
def test_invalid_missing_required_fields(self):
|
||||||
user = UserFactory()
|
user = UserFactory()
|
||||||
self.client.force_login(user)
|
self.client.force_login(user)
|
||||||
|
|
||||||
response = self.client.post(reverse('subscriptions:billing-address'), {})
|
response = self.client.post(reverse('user-settings-billing'), {})
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, 'errorlist')
|
self.assertContains(response, 'errorlist')
|
||||||
@ -72,7 +71,7 @@ class TestSubscriptionSettingsBillingAddress(BaseSubscriptionTestCase):
|
|||||||
data = {
|
data = {
|
||||||
'email': 'new@example.com',
|
'email': 'new@example.com',
|
||||||
}
|
}
|
||||||
response = self.client.post(reverse('subscriptions:billing-address'), data)
|
response = self.client.post(reverse('user-settings-billing'), data)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, 'errorlist')
|
self.assertContains(response, 'errorlist')
|
||||||
@ -85,7 +84,7 @@ class TestSubscriptionSettingsBillingAddress(BaseSubscriptionTestCase):
|
|||||||
data = {
|
data = {
|
||||||
'full_name': 'New Full Name',
|
'full_name': 'New Full Name',
|
||||||
}
|
}
|
||||||
response = self.client.post(reverse('subscriptions:billing-address'), data)
|
response = self.client.post(reverse('user-settings-billing'), data)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, 'errorlist')
|
self.assertContains(response, 'errorlist')
|
||||||
@ -103,8 +102,8 @@ class TestSubscriptionSettingsChangePaymentMethod(BaseSubscriptionTestCase):
|
|||||||
def test_can_change_payment_method_from_bank_to_credit_card_with_sca(self):
|
def test_can_change_payment_method_from_bank_to_credit_card_with_sca(self):
|
||||||
bank = Gateway.objects.get(name='bank')
|
bank = Gateway.objects.get(name='bank')
|
||||||
subscription = SubscriptionFactory(
|
subscription = SubscriptionFactory(
|
||||||
user=self.user,
|
customer=self.user.customer,
|
||||||
payment_method__user_id=self.user.pk,
|
payment_method__customer_id=self.user.customer.pk,
|
||||||
payment_method__gateway=bank,
|
payment_method__gateway=bank,
|
||||||
)
|
)
|
||||||
self.assertEqual(PaymentMethod.objects.count(), 1)
|
self.assertEqual(PaymentMethod.objects.count(), 1)
|
||||||
@ -139,8 +138,8 @@ class TestSubscriptionSettingsChangePaymentMethod(BaseSubscriptionTestCase):
|
|||||||
def test_can_change_payment_method_from_credit_card_to_bank(self):
|
def test_can_change_payment_method_from_credit_card_to_bank(self):
|
||||||
braintree = Gateway.objects.get(name='braintree')
|
braintree = Gateway.objects.get(name='braintree')
|
||||||
subscription = SubscriptionFactory(
|
subscription = SubscriptionFactory(
|
||||||
user=self.user,
|
customer=self.user.customer,
|
||||||
payment_method__user_id=self.user.pk,
|
payment_method__customer_id=self.user.customer.pk,
|
||||||
payment_method__gateway=braintree,
|
payment_method__gateway=braintree,
|
||||||
)
|
)
|
||||||
self.assertEqual(PaymentMethod.objects.count(), 1)
|
self.assertEqual(PaymentMethod.objects.count(), 1)
|
||||||
@ -173,8 +172,8 @@ class TestSubscriptionCancel(BaseSubscriptionTestCase):
|
|||||||
|
|
||||||
def test_can_cancel_when_on_hold(self):
|
def test_can_cancel_when_on_hold(self):
|
||||||
subscription = SubscriptionFactory(
|
subscription = SubscriptionFactory(
|
||||||
user=self.user,
|
customer=self.user.customer,
|
||||||
payment_method__user_id=self.user.pk,
|
payment_method__customer_id=self.user.customer.pk,
|
||||||
payment_method__gateway=Gateway.objects.get(name='bank'),
|
payment_method__gateway=Gateway.objects.get(name='bank'),
|
||||||
status='on-hold',
|
status='on-hold',
|
||||||
)
|
)
|
||||||
@ -198,8 +197,8 @@ class TestSubscriptionCancel(BaseSubscriptionTestCase):
|
|||||||
|
|
||||||
def test_can_cancel_when_active(self):
|
def test_can_cancel_when_active(self):
|
||||||
subscription = SubscriptionFactory(
|
subscription = SubscriptionFactory(
|
||||||
user=self.user,
|
customer=self.user.customer,
|
||||||
payment_method__user_id=self.user.pk,
|
payment_method__customer_id=self.user.customer.pk,
|
||||||
payment_method__gateway=Gateway.objects.get(name='bank'),
|
payment_method__gateway=Gateway.objects.get(name='bank'),
|
||||||
status='active',
|
status='active',
|
||||||
)
|
)
|
||||||
@ -218,8 +217,8 @@ class TestSubscriptionCancel(BaseSubscriptionTestCase):
|
|||||||
|
|
||||||
def test_email_sent_when_pending_cancellation_changes_to_cancelled(self):
|
def test_email_sent_when_pending_cancellation_changes_to_cancelled(self):
|
||||||
subscription = SubscriptionFactory(
|
subscription = SubscriptionFactory(
|
||||||
user=self.user,
|
customer=self.user.customer,
|
||||||
payment_method__user_id=self.user.pk,
|
payment_method__customer_id=self.user.customer.pk,
|
||||||
payment_method__gateway=Gateway.objects.get(name='bank'),
|
payment_method__gateway=Gateway.objects.get(name='bank'),
|
||||||
status='pending-cancellation',
|
status='pending-cancellation',
|
||||||
)
|
)
|
||||||
@ -244,8 +243,8 @@ class TestPayExistingOrder(BaseSubscriptionTestCase):
|
|||||||
|
|
||||||
def test_redirect_to_login_when_anonymous(self):
|
def test_redirect_to_login_when_anonymous(self):
|
||||||
subscription = SubscriptionFactory(
|
subscription = SubscriptionFactory(
|
||||||
user=self.user,
|
customer=self.user.customer,
|
||||||
payment_method__user_id=self.user.pk,
|
payment_method__customer_id=self.user.customer.pk,
|
||||||
payment_method__gateway=Gateway.objects.get(name='bank'),
|
payment_method__gateway=Gateway.objects.get(name='bank'),
|
||||||
status='on-hold',
|
status='on-hold',
|
||||||
)
|
)
|
||||||
@ -264,8 +263,8 @@ class TestPayExistingOrder(BaseSubscriptionTestCase):
|
|||||||
|
|
||||||
def test_cannot_pay_someone_elses_order(self):
|
def test_cannot_pay_someone_elses_order(self):
|
||||||
subscription = SubscriptionFactory(
|
subscription = SubscriptionFactory(
|
||||||
user=self.user,
|
customer=self.user.customer,
|
||||||
payment_method__user_id=self.user.pk,
|
payment_method__customer_id=self.user.customer.pk,
|
||||||
payment_method__gateway=Gateway.objects.get(name='bank'),
|
payment_method__gateway=Gateway.objects.get(name='bank'),
|
||||||
status='on-hold',
|
status='on-hold',
|
||||||
)
|
)
|
||||||
@ -284,8 +283,8 @@ class TestPayExistingOrder(BaseSubscriptionTestCase):
|
|||||||
|
|
||||||
def test_invalid_missing_required_form_data(self):
|
def test_invalid_missing_required_form_data(self):
|
||||||
subscription = SubscriptionFactory(
|
subscription = SubscriptionFactory(
|
||||||
user=self.user,
|
customer=self.user.customer,
|
||||||
payment_method__user_id=self.user.pk,
|
payment_method__customer_id=self.user.customer.pk,
|
||||||
payment_method__gateway=Gateway.objects.get(name='bank'),
|
payment_method__gateway=Gateway.objects.get(name='bank'),
|
||||||
status='on-hold',
|
status='on-hold',
|
||||||
)
|
)
|
||||||
@ -315,8 +314,8 @@ class TestPayExistingOrder(BaseSubscriptionTestCase):
|
|||||||
)
|
)
|
||||||
def test_can_pay_for_manual_subscription_with_an_order(self):
|
def test_can_pay_for_manual_subscription_with_an_order(self):
|
||||||
subscription = SubscriptionFactory(
|
subscription = SubscriptionFactory(
|
||||||
user=self.user,
|
customer=self.user.customer,
|
||||||
payment_method__user_id=self.user.pk,
|
payment_method__customer_id=self.user.customer.pk,
|
||||||
payment_method__gateway=Gateway.objects.get(name='bank'),
|
payment_method__gateway=Gateway.objects.get(name='bank'),
|
||||||
currency='USD',
|
currency='USD',
|
||||||
price=Money('USD', 1110),
|
price=Money('USD', 1110),
|
||||||
|
@ -6,6 +6,9 @@ from django.utils.html import format_html
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.authtoken.admin import TokenAdmin
|
from rest_framework.authtoken.admin import TokenAdmin
|
||||||
|
|
||||||
|
import nested_admin
|
||||||
|
|
||||||
|
from looper.admin import REL_CUSTOMER_SEARCH_FIELDS
|
||||||
import looper.admin
|
import looper.admin
|
||||||
import looper.models
|
import looper.models
|
||||||
|
|
||||||
@ -26,7 +29,7 @@ user_section_progress_link.short_description = 'Training sections progress'
|
|||||||
|
|
||||||
def user_subscriptions_link(obj, title='View subscriptions of this user'):
|
def user_subscriptions_link(obj, title='View subscriptions of this user'):
|
||||||
admin_view = looper.admin._get_admin_url_name(looper.models.Subscription, 'changelist')
|
admin_view = looper.admin._get_admin_url_name(looper.models.Subscription, 'changelist')
|
||||||
link = reverse(admin_view) + f'?user_id={obj.pk}'
|
link = reverse(admin_view) + f'?customer_id={obj.customer.pk}'
|
||||||
return format_html('<a href="{}">{}</a>', link, title)
|
return format_html('<a href="{}">{}</a>', link, title)
|
||||||
|
|
||||||
|
|
||||||
@ -80,7 +83,7 @@ class NumberOfBraintreeCustomerIDsFilter(admin.SimpleListFilter):
|
|||||||
|
|
||||||
|
|
||||||
@admin.register(get_user_model())
|
@admin.register(get_user_model())
|
||||||
class UserAdmin(auth_admin.UserAdmin):
|
class UserAdmin(auth_admin.UserAdmin, nested_admin.NestedModelAdmin):
|
||||||
change_form_template = 'loginas/change_form.html'
|
change_form_template = 'loginas/change_form.html'
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request):
|
||||||
@ -91,9 +94,9 @@ class UserAdmin(auth_admin.UserAdmin):
|
|||||||
"""Count user subscriptions for subscription debugging purposes."""
|
"""Count user subscriptions for subscription debugging purposes."""
|
||||||
queryset = super().get_queryset(*args, **kwargs)
|
queryset = super().get_queryset(*args, **kwargs)
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
subscriptions_count=Count('subscription', distinct=True),
|
subscriptions_count=Count('customer__subscription', distinct=True),
|
||||||
braintreecustomerids_count=Count(
|
braintreecustomerids_count=Count(
|
||||||
'gatewaycustomerid', Q(gateway__name='braintree'), distinct=True
|
'customer__gatewaycustomerid', Q(customer__gateway__name='braintree'), distinct=True
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return queryset
|
return queryset
|
||||||
@ -147,13 +150,12 @@ class UserAdmin(auth_admin.UserAdmin):
|
|||||||
user_subscriptions_link,
|
user_subscriptions_link,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
)
|
)
|
||||||
inlines = [
|
inlines = (
|
||||||
looper.admin.AddressInline,
|
looper.admin.LinkCustomerTokenInline,
|
||||||
looper.admin.CustomerInline,
|
looper.admin.CustomerInline,
|
||||||
looper.admin.GatewayCustomerIdInline,
|
)
|
||||||
]
|
|
||||||
ordering = ['-date_joined']
|
ordering = ['-date_joined']
|
||||||
search_fields = ['email', 'full_name', 'username']
|
search_fields = ['email', 'full_name', 'username', *REL_CUSTOMER_SEARCH_FIELDS]
|
||||||
|
|
||||||
def deletion_requested(self, obj):
|
def deletion_requested(self, obj):
|
||||||
"""Display yes/no icon status of deletion request."""
|
"""Display yes/no icon status of deletion request."""
|
||||||
|
@ -107,7 +107,7 @@ def handle_tracking_event_unsubscribe(event_type: str, message_id: str, event: D
|
|||||||
def unsubscribe_from_newsletters(pk: int):
|
def unsubscribe_from_newsletters(pk: int):
|
||||||
"""Remove emails of user with given pk from newsletter lists."""
|
"""Remove emails of user with given pk from newsletter lists."""
|
||||||
user = User.objects.get(pk=pk)
|
user = User.objects.get(pk=pk)
|
||||||
emails = (user.email, user.customer.billing_email)
|
emails = (user.email, user.customer.billing_address.email)
|
||||||
for email in emails:
|
for email in emails:
|
||||||
if not email:
|
if not email:
|
||||||
continue
|
continue
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
{% block settings %}
|
{% block settings %}
|
||||||
<p class="text-muted">Settings</p>
|
<p class="text-muted">Settings</p>
|
||||||
<h1 class="mb-3">Subscription{% if user.subscription_set.count > 1 %}s{% endif %}</h1>
|
<h1 class="mb-3">Subscription{% if user.customer.subscription_set.count > 1 %}s{% endif %}</h1>
|
||||||
<p class="mb-2 text-muted">If you have any problems with billing, contact the team directly on <a href="mailto:{{ ADMIN_MAIL }}">{{ ADMIN_MAIL }}</a>.</p>
|
<p class="mb-2 text-muted">If you have any problems with billing, contact the team directly on <a href="mailto:{{ ADMIN_MAIL }}">{{ ADMIN_MAIL }}</a>.</p>
|
||||||
<div>
|
<div>
|
||||||
{% if user|has_group:"demo" %}
|
{% if user|has_group:"demo" %}
|
||||||
|
@ -5,14 +5,15 @@ from django.contrib.auth import get_user_model
|
|||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from comments.queries import set_comment_like
|
from looper.tests.factories import (
|
||||||
from common.tests.factories.comments import CommentFactory
|
|
||||||
from common.tests.factories.subscriptions import (
|
|
||||||
TeamFactory,
|
|
||||||
PaymentMethodFactory,
|
PaymentMethodFactory,
|
||||||
TransactionFactory,
|
TransactionFactory,
|
||||||
create_customer_with_billing_address,
|
create_customer_with_billing_address,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from comments.queries import set_comment_like
|
||||||
|
from common.tests.factories.comments import CommentFactory
|
||||||
|
from common.tests.factories.subscriptions import TeamFactory
|
||||||
from common.tests.factories.users import UserFactory
|
from common.tests.factories.users import UserFactory
|
||||||
import users.tasks as tasks
|
import users.tasks as tasks
|
||||||
import users.tests.util as util
|
import users.tests.util as util
|
||||||
|
Loading…
Reference in New Issue
Block a user