Stripe in Blender Studio #93018
@ -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": "<p>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.</p>"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "looper.gateway",
|
|
||||||
"pk": 3,
|
|
||||||
"fields": {
|
|
||||||
"name": "stripe",
|
|
||||||
"is_default": true,
|
|
||||||
"frontend_name": "Checkout with Stripe"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"model": "looper.product",
|
"model": "looper.product",
|
||||||
"pk": 1,
|
"pk": 1,
|
||||||
|
30
looper/fixtures/gateways.json
Normal file
30
looper/fixtures/gateways.json
Normal file
@ -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": "<p>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.</p>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "looper.gateway",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"name": "stripe",
|
||||||
|
"is_default": true,
|
||||||
|
"frontend_name": "Checkout with Stripe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -25,10 +25,13 @@ class Migration(migrations.Migration):
|
|||||||
name='vat_number',
|
name='vat_number',
|
||||||
field=models.CharField(blank=True, default='', max_length=255, verbose_name='VAT identification 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
|
# Create address records for accounts without any
|
||||||
migrations.RunSQL(
|
migrations.RunSQL(
|
||||||
"insert into looper_address (category, user_id, full_name, company, country, tax_exempt, vat_number)"
|
"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, '' "
|
"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) "
|
"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",
|
"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 "
|
"full_name = addr.full_name, company = addr.company "
|
||||||
"from looper_address as addr where cu.user_id = addr.user_id",
|
"from looper_address as addr where cu.user_id = addr.user_id",
|
||||||
),
|
),
|
||||||
|
migrations.RunSQL('SET CONSTRAINTS ALL DEFERRED;'),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='address',
|
model_name='address',
|
||||||
name='email',
|
name='email',
|
||||||
|
@ -181,8 +181,10 @@ class Taxable:
|
|||||||
is_business = False
|
is_business = False
|
||||||
if user and user.is_authenticated:
|
if user and user.is_authenticated:
|
||||||
customer = user.customer
|
customer = user.customer
|
||||||
country_code = customer.billing_address.country or country_code
|
billing_address = customer.billing_address
|
||||||
is_business = bool(customer.vat_number)
|
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(
|
tax_type, tax_rate = ProductType(product_type).get_tax(
|
||||||
buyer_country_code=country_code, is_business=is_business
|
buyer_country_code=country_code, is_business=is_business
|
||||||
)
|
)
|
||||||
|
@ -27,7 +27,7 @@ class AbstractBaseTestCase(TestCase):
|
|||||||
|
|
||||||
class AbstractLooperTestCase(AbstractBaseTestCase):
|
class AbstractLooperTestCase(AbstractBaseTestCase):
|
||||||
log = log.getChild('AbstractLooperTestCase')
|
log = log.getChild('AbstractLooperTestCase')
|
||||||
fixtures = ['devfund', 'testuser', 'systemuser']
|
fixtures = ['gateways', 'devfund', 'testuser', 'systemuser']
|
||||||
valid_payload = {
|
valid_payload = {
|
||||||
'gateway': 'stripe',
|
'gateway': 'stripe',
|
||||||
'email': 'erik@example.com',
|
'email': 'erik@example.com',
|
||||||
@ -79,7 +79,7 @@ class AbstractLooperTestCase(AbstractBaseTestCase):
|
|||||||
self.pay_meth = self.user.customer.payment_method_default
|
self.pay_meth = self.user.customer.payment_method_default
|
||||||
|
|
||||||
def create_accountless_customer(self):
|
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()
|
self.accountless_customer.billing_address.save()
|
||||||
gw = self._get_test_gateway()
|
gw = self._get_test_gateway()
|
||||||
if gw:
|
if gw:
|
||||||
|
@ -10,6 +10,7 @@ import looper.taxes
|
|||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
@factory.django.mute_signals(signals.pre_save, signals.post_save)
|
||||||
class UserFactory(DjangoModelFactory):
|
class UserFactory(DjangoModelFactory):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
@ -30,6 +31,8 @@ class CustomerFactory(DjangoModelFactory):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = looper.models.Customer
|
model = looper.models.Customer
|
||||||
|
|
||||||
|
user = factory.SubFactory(UserFactory)
|
||||||
|
|
||||||
|
|
||||||
class PaymentMethodFactory(DjangoModelFactory):
|
class PaymentMethodFactory(DjangoModelFactory):
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -58,6 +61,7 @@ class SubscriptionFactory(DjangoModelFactory):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = looper.models.Subscription
|
model = looper.models.Subscription
|
||||||
|
|
||||||
|
customer = factory.SubFactory(CustomerFactory)
|
||||||
plan = factory.SubFactory(PlanFactory)
|
plan = factory.SubFactory(PlanFactory)
|
||||||
payment_method = factory.SubFactory(PaymentMethodFactory)
|
payment_method = factory.SubFactory(PaymentMethodFactory)
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ CHECKOUT = 'looper-braintree:checkout'
|
|||||||
# Prevent communication with Google's reCAPTCHA API.
|
# Prevent communication with Google's reCAPTCHA API.
|
||||||
@override_settings(GOOGLE_RECAPTCHA_SECRET_KEY='')
|
@override_settings(GOOGLE_RECAPTCHA_SECRET_KEY='')
|
||||||
class AbstractCheckoutTestCase(AbstractLooperTestCase):
|
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})
|
checkout_url = reverse_lazy(CHECKOUT, kwargs={'plan_id': 2, 'plan_variation_id': 5})
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -15,7 +15,7 @@ User = get_user_model()
|
|||||||
|
|
||||||
|
|
||||||
class _BaseTestCase(AbstractLooperTestCase):
|
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})
|
checkout_url = reverse_lazy('looper:checkout', kwargs={'plan_id': 2, 'plan_variation_id': 5})
|
||||||
gateway_name = 'stripe'
|
gateway_name = 'stripe'
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ from looper.tests.factories import (
|
|||||||
|
|
||||||
@mock.patch('looper.gateways.stripe.webhook.WebhookSignature.verify_header', return_value=True)
|
@mock.patch('looper.gateways.stripe.webhook.WebhookSignature.verify_header', return_value=True)
|
||||||
class ClockForStripe(ResponsesMixin, TestCase):
|
class ClockForStripe(ResponsesMixin, TestCase):
|
||||||
fixtures = ['devfund', 'systemuser']
|
fixtures = ['gateways', 'devfund', 'systemuser']
|
||||||
responses_files = ['clock_stripe.yaml']
|
responses_files = ['clock_stripe.yaml']
|
||||||
|
|
||||||
# @_recorder.record(file_path=f'{ResponsesMixin.responses_file_path}/clock_stripe.yaml')
|
# @_recorder.record(file_path=f'{ResponsesMixin.responses_file_path}/clock_stripe.yaml')
|
||||||
|
@ -116,7 +116,7 @@ class BraintreeErrorTest(AbstractBaseTestCase):
|
|||||||
|
|
||||||
|
|
||||||
class BankGatewayTest(AbstractBaseTestCase):
|
class BankGatewayTest(AbstractBaseTestCase):
|
||||||
fixtures = ['testuser', 'devfund']
|
fixtures = ['testuser', 'gateways', 'devfund']
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
@ -10,7 +10,7 @@ from . import AbstractLooperTestCase, AbstractBaseTestCase
|
|||||||
|
|
||||||
|
|
||||||
class MoneyFieldTest(AbstractBaseTestCase):
|
class MoneyFieldTest(AbstractBaseTestCase):
|
||||||
fixtures = ['testuser', 'devfund', 'systemuser']
|
fixtures = ['testuser', 'gateways', 'devfund', 'systemuser']
|
||||||
|
|
||||||
def test_create_instance(self):
|
def test_create_instance(self):
|
||||||
pv = models.PlanVariation(plan_id=1, currency='EUR', price=Money('EUR', 155))
|
pv = models.PlanVariation(plan_id=1, currency='EUR', price=Money('EUR', 155))
|
||||||
|
@ -15,7 +15,7 @@ User = get_user_model()
|
|||||||
|
|
||||||
|
|
||||||
class _BaseTestCase(AbstractLooperTestCase):
|
class _BaseTestCase(AbstractLooperTestCase):
|
||||||
fixtures = ['devfund', 'testuser', 'systemuser']
|
fixtures = ['gateways', 'devfund', 'testuser', 'systemuser']
|
||||||
gateway_name = 'stripe'
|
gateway_name = 'stripe'
|
||||||
subscription = None
|
subscription = None
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ from ..money import Money
|
|||||||
|
|
||||||
|
|
||||||
class PlanVariationModelTestCase(django.test.TestCase):
|
class PlanVariationModelTestCase(django.test.TestCase):
|
||||||
fixtures = ['devfund']
|
fixtures = ['gateways', 'devfund']
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
product = Product.objects.create(name='Ежедневный Пророк')
|
product = Product.objects.create(name='Ежедневный Пророк')
|
||||||
@ -29,7 +29,7 @@ class PlanVariationModelTestCase(django.test.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class PlanVariationTest(django.test.TestCase):
|
class PlanVariationTest(django.test.TestCase):
|
||||||
fixtures = ['devfund']
|
fixtures = ['gateways', 'devfund']
|
||||||
|
|
||||||
def test_variation_for_currency(self):
|
def test_variation_for_currency(self):
|
||||||
plan = Plan.objects.get(pk=1)
|
plan = Plan.objects.get(pk=1)
|
||||||
|
@ -35,7 +35,7 @@ event_payload_cc = {
|
|||||||
|
|
||||||
@mock.patch('looper.gateways.stripe.webhook.WebhookSignature.verify_header', return_value=True)
|
@mock.patch('looper.gateways.stripe.webhook.WebhookSignature.verify_header', return_value=True)
|
||||||
class UpsertOrderFromPaymentIntentAndProduct(ResponsesMixin, TestCase):
|
class UpsertOrderFromPaymentIntentAndProduct(ResponsesMixin, TestCase):
|
||||||
fixtures = ['devfund', 'systemuser']
|
fixtures = ['gateways', 'devfund', 'systemuser']
|
||||||
responses_files = [
|
responses_files = [
|
||||||
'payment_intent__card.yaml',
|
'payment_intent__card.yaml',
|
||||||
'payment_intent__ideal.yaml',
|
'payment_intent__ideal.yaml',
|
||||||
@ -452,7 +452,7 @@ class UpsertOrderFromPaymentIntentAndProduct(ResponsesMixin, TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class TestCheckoutPlanVariationAnonymous(ResponsesMixin, TestCase):
|
class TestCheckoutPlanVariationAnonymous(ResponsesMixin, TestCase):
|
||||||
fixtures = ['devfund', 'systemuser']
|
fixtures = ['gateways', 'devfund', 'systemuser']
|
||||||
responses_files = [
|
responses_files = [
|
||||||
'checkout_plan_variation_anonymous.yaml',
|
'checkout_plan_variation_anonymous.yaml',
|
||||||
'checkout_plan_variation_webhook.yaml',
|
'checkout_plan_variation_webhook.yaml',
|
||||||
@ -535,7 +535,7 @@ class TestCheckoutPlanVariationAnonymous(ResponsesMixin, TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class TestCheckoutPlanVariationLoggedIn(ResponsesMixin, TestCase):
|
class TestCheckoutPlanVariationLoggedIn(ResponsesMixin, TestCase):
|
||||||
fixtures = ['devfund', 'systemuser']
|
fixtures = ['gateways', 'devfund', 'systemuser']
|
||||||
responses_files = [
|
responses_files = [
|
||||||
'checkout_plan_variation_logged_in.yaml',
|
'checkout_plan_variation_logged_in.yaml',
|
||||||
]
|
]
|
||||||
|
@ -147,6 +147,9 @@ class CheckoutExistingOrderView(LoginRequiredMixin, ExpectReadableIPAddressMixin
|
|||||||
# should be passed from the app, this url name is defined in example_app
|
# should be passed from the app, this url name is defined in example_app
|
||||||
cancel_url = 'settings_home'
|
cancel_url = 'settings_home'
|
||||||
|
|
||||||
|
def get_cancel_url(self):
|
||||||
|
return reverse(self.cancel_url)
|
||||||
|
|
||||||
def get_object(self, queryset=None) -> models.Order:
|
def get_object(self, queryset=None) -> models.Order:
|
||||||
order_id: int = self.kwargs['order_id']
|
order_id: int = self.kwargs['order_id']
|
||||||
order_q = models.Order.objects.filter(customer=self.request.user.customer)
|
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,
|
# we have to do it to avoid uri-encoding of curly braces,
|
||||||
# otherwise stripe doesn't do the template substitution
|
# otherwise stripe doesn't do the template substitution
|
||||||
success_url = success_url.replace('CHECKOUT_SESSION_ID', '{CHECKOUT_SESSION_ID}', 1)
|
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(
|
session = stripe_utils.create_stripe_checkout_session_for_order(
|
||||||
order,
|
order,
|
||||||
success_url,
|
success_url,
|
||||||
|
@ -102,7 +102,10 @@ class PaymentMethodChangeView(LoginRequiredMixin, View):
|
|||||||
log = log.getChild('PaymentMethodChangeView')
|
log = log.getChild('PaymentMethodChangeView')
|
||||||
subscription: models.Subscription
|
subscription: models.Subscription
|
||||||
success_url = '/'
|
success_url = '/'
|
||||||
cancel_url = '/'
|
cancel_url = 'settings_home'
|
||||||
|
|
||||||
|
def get_cancel_url(self):
|
||||||
|
return reverse(self.cancel_url)
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
self.subscription = get_object_or_404(
|
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,
|
# we have to do it to avoid uri-encoding of curly braces,
|
||||||
# otherwise stripe doesn't do the template substitution
|
# otherwise stripe doesn't do the template substitution
|
||||||
success_url = success_url.replace('CHECKOUT_SESSION_ID', '{CHECKOUT_SESSION_ID}', 1)
|
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(
|
session = stripe_utils.setup_stripe_payment_method(
|
||||||
self.subscription.currency.lower(),
|
self.subscription.currency.lower(),
|
||||||
request.user.customer,
|
request.user.customer,
|
||||||
|
14
poetry.lock
generated
14
poetry.lock
generated
@ -1785,6 +1785,18 @@ lxml = "*"
|
|||||||
reportlab = "*"
|
reportlab = "*"
|
||||||
tinycss2 = ">=0.6.0"
|
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]]
|
[[package]]
|
||||||
name = "tinycss2"
|
name = "tinycss2"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@ -2060,4 +2072,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 = "cf5844e74a99155ee23a29cc3f711e0b8577264b72ff07acd858ce16a407e05d"
|
content-hash = "00e67af2695baac5d34a3c9a67ac24286a6a218b30196cccc14c3a35461ffc05"
|
||||||
|
@ -40,6 +40,7 @@ flake8-implicit-str-concat = "^0.1.0"
|
|||||||
psycopg2 = "*"
|
psycopg2 = "*"
|
||||||
mypy = "^0.991"
|
mypy = "^0.991"
|
||||||
factory-boy = "^3.0"
|
factory-boy = "^3.0"
|
||||||
|
tblib = "3.0.0"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
|
Loading…
Reference in New Issue
Block a user