Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,3 +572,5 @@ def show_toolbar(request):
# Rate limits for users requesting to join an organization
ORG_JOIN_REQUEST_LIMIT = env.int("ORG_JOIN_REQUEST_LIMIT", default=3)
ORG_JOIN_REQUEST_WINDOW = env.int("ORG_JOIN_REQUEST_WINDOW", default=3600)

STRIPE_PROVIDER = env("STRIPE_PROVIDER", default="modern")
5 changes: 5 additions & 0 deletions config/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@
# https://docs.djangoproject.com/en/dev/ref/settings/#email-port
EMAIL_PORT = 1025

# DJANGO VITE
# ------------------------------------------------------------------------------
# Skip manifest lookup in tests — no built assets available
DJANGO_VITE = {"default": {"dev_mode": True}}

# Your stuff...
# ------------------------------------------------------------------------------
# Should not contact stripe during tests
Expand Down
2 changes: 1 addition & 1 deletion requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ sqlparse==0.5.4
# django-request-token
stack-data==0.3.0
# via ipython
stripe==8.11.0
stripe==15.0.0
# via -r requirements/base.in
tinycss2==1.5.1
# via
Expand Down
4 changes: 2 additions & 2 deletions requirements/local.txt
Original file line number Diff line number Diff line change
Expand Up @@ -591,8 +591,8 @@ stack-data==0.3.0
# via
# -r requirements/base.txt
# ipython
stripe==8.11.0
# via -r requirements/base.txt
stripe==15.0.0
# via -r /app/requirements/base.txt
termcolor==1.1.0
# via pytest-sugar
text-unidecode==1.3
Expand Down
4 changes: 2 additions & 2 deletions requirements/production.txt
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,8 @@ stack-data==0.3.0
# via
# -r requirements/base.txt
# ipython
stripe==8.11.0
# via -r requirements/base.txt
stripe==15.0.0
# via -r /app/requirements/base.txt
tinycss2==1.5.1
# via
# -r requirements/base.txt
Expand Down
32 changes: 15 additions & 17 deletions squarelet/core/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ class TestFormatStripeError:

def test_card_error_expired_card(self):
"""CardError with expired_card code should show detailed user message"""
error = stripe.error.CardError(
error = stripe.CardError(
message="Your card has expired.",
param="exp_month",
code="expired_card",
Expand All @@ -224,7 +224,7 @@ def test_card_error_expired_card(self):

def test_card_error_card_declined(self):
"""CardError with card_declined code should show detailed user message"""
error = stripe.error.CardError(
error = stripe.CardError(
message="Your card was declined.",
param="card",
code="card_declined",
Expand All @@ -235,7 +235,7 @@ def test_card_error_card_declined(self):

def test_card_error_insufficient_funds(self):
"""CardError with insufficient_funds code should show detailed user message"""
error = stripe.error.CardError(
error = stripe.CardError(
message="Your card has insufficient funds.",
param="card",
code="insufficient_funds",
Expand All @@ -246,7 +246,7 @@ def test_card_error_insufficient_funds(self):

def test_card_error_incorrect_cvc(self):
"""CardError with incorrect_cvc code should show detailed user message"""
error = stripe.error.CardError(
error = stripe.CardError(
message="Your card's security code is incorrect.",
param="cvc",
code="incorrect_cvc",
Expand All @@ -257,7 +257,7 @@ def test_card_error_incorrect_cvc(self):

def test_card_error_processing_error(self):
"""CardError with processing_error code should show detailed user message"""
error = stripe.error.CardError(
error = stripe.CardError(
message="An error occurred while processing your card.",
param="card",
code="processing_error",
Expand All @@ -270,7 +270,7 @@ def test_card_error_processing_error(self):

def test_card_error_generic(self):
"""Generic CardError should use Stripe's user message"""
error = stripe.error.CardError(
error = stripe.CardError(
message="Your card was declined for an unknown reason.",
param="card",
code="generic_decline",
Expand All @@ -282,69 +282,67 @@ def test_card_error_generic(self):

def test_api_connection_error(self):
"""APIConnectionError should show generic message"""
error = stripe.error.APIConnectionError("Network connection failed")
error = stripe.APIConnectionError("Network connection failed")
user_message = format_stripe_error(error)

assert "contact" in user_message.lower() or "support" in user_message.lower()
assert "try again" in user_message.lower()

def test_rate_limit_error(self):
"""RateLimitError should show generic message"""
error = stripe.error.RateLimitError("Too many requests")
error = stripe.RateLimitError("Too many requests")
user_message = format_stripe_error(error)

assert "contact" in user_message.lower() or "support" in user_message.lower()
assert "try again" in user_message.lower()

def test_api_error(self):
"""APIError should show generic message"""
error = stripe.error.APIError("Internal server error")
error = stripe.APIError("Internal server error")
user_message = format_stripe_error(error)

assert "contact" in user_message.lower() or "support" in user_message.lower()
assert "try again" in user_message.lower()

def test_invalid_request_error(self):
"""InvalidRequestError should show generic message"""
error = stripe.error.InvalidRequestError(
message="Invalid request", param="amount"
)
error = stripe.InvalidRequestError(message="Invalid request", param="amount")
user_message = format_stripe_error(error)

assert "contact" in user_message.lower() or "support" in user_message.lower()

def test_authentication_error(self):
"""AuthenticationError should show generic message"""
error = stripe.error.AuthenticationError("Invalid API key")
error = stripe.AuthenticationError("Invalid API key")
user_message = format_stripe_error(error)

assert "contact" in user_message.lower() or "support" in user_message.lower()

def test_idempotency_error(self):
"""IdempotencyError should show generic message"""
error = stripe.error.IdempotencyError("Idempotency key reused")
error = stripe.IdempotencyError("Idempotency key reused")
user_message = format_stripe_error(error)

assert "contact" in user_message.lower() or "support" in user_message.lower()
assert "try again" in user_message.lower()

def test_permission_error(self):
"""PermissionError should show generic message"""
error = stripe.error.PermissionError("Insufficient permissions")
error = stripe.PermissionError("Insufficient permissions")
user_message = format_stripe_error(error)

assert "contact" in user_message.lower() or "support" in user_message.lower()

def test_generic_stripe_error(self):
"""Generic StripeError should show generic message"""
error = stripe.error.StripeError("Unknown error")
error = stripe.StripeError("Unknown error")
user_message = format_stripe_error(error)

assert "contact" in user_message.lower() or "support" in user_message.lower()

def test_technical_error_includes_mailto_link(self):
"""Technical errors should include a mailto link with error details"""
error = stripe.error.APIError("Internal server error")
error = stripe.APIError("Internal server error")
user_message = format_stripe_error(error)

# Check that the message contains an HTML mailto link
Expand Down
15 changes: 1 addition & 14 deletions squarelet/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import logging
import os.path
import sys
import uuid
from datetime import timedelta
from hashlib import md5
from urllib.parse import quote
Expand Down Expand Up @@ -85,18 +84,6 @@ def retry_on_error(errors, func, *args, **kwargs):
return retry_on_error(errors, func, times=times, *args, **kwargs)


def stripe_retry_on_error(func, *args, **kwargs):
"""Retry stripe API calls on connection errors"""
if kwargs.get("idempotency_key") is True:
kwargs["idempotency_key"] = uuid.uuid4().hex
return retry_on_error(
(stripe.error.APIConnectionError, stripe.error.RateLimitError),
func,
*args,
**kwargs,
)


def mailchimp_subscribe(emails, list_=settings.MAILCHIMP_LIST_DEFAULT):
"""Adds the email to the mailing list throught the MailChimp API.
https://mailchimp.com/developer/marketing/api/lists/"""
Expand Down Expand Up @@ -255,7 +242,7 @@ def format_stripe_error(error):
"""
# CardErrors - show detailed user messages
# These are user-facing errors where Stripe provides helpful messages
if isinstance(error, stripe.error.CardError):
if isinstance(error, stripe.CardError):
# Stripe's user_message is already user-friendly for card errors
user_message = str(error)
logger.error("Stripe CardError: %s", error, exc_info=sys.exc_info())
Expand Down
2 changes: 1 addition & 1 deletion squarelet/organizations/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ def mark_as_paid(self, request, queryset):
invoice.status = "paid"
invoice.save()
success_count += 1
except stripe.error.StripeError as exc:
except stripe.StripeError as exc:
logger.error(
"Failed to mark invoice %s as paid in Stripe: %s",
invoice.invoice_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def update_and_add_plans(apps, schema_editor):
product={"name": plan.name, "unit_label": "Seats"},
**kwargs,
)
except stripe.error.InvalidRequestError:
except stripe.InvalidRequestError:
# If the plan already exists, just skip
pass

Expand Down Expand Up @@ -268,7 +268,7 @@ def update_and_add_plans(apps, schema_editor):
product={"name": plan.name, "unit_label": "Seats"},
**kwargs,
)
except stripe.error.InvalidRequestError:
except stripe.InvalidRequestError:
# If the plan already exists, just skip
pass

Expand Down Expand Up @@ -332,7 +332,7 @@ def reverse_changes(apps, schema_editor):
product = stripe.Product.retrieve(id=stripe_plan.product)
stripe_plan.delete()
product.delete()
except stripe.error.InvalidRequestError:
except stripe.InvalidRequestError:
# If the plan or product do not exist, just skip
pass

Expand All @@ -358,7 +358,7 @@ def reverse_changes(apps, schema_editor):
product = stripe.Product.retrieve(id=stripe_plan.product)
stripe_plan.delete()
product.delete()
except stripe.error.InvalidRequestError:
except stripe.InvalidRequestError:
# If the plan or product do not exist, just skip
pass

Expand Down
10 changes: 7 additions & 3 deletions squarelet/organizations/models/invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,12 @@ def get_hosted_invoice_url(self):
stripe_invoice = (
get_payment_provider().get_invoice_service().retrieve(self.invoice_id)
)
return stripe_invoice.get("hosted_invoice_url")
except stripe.error.StripeError:
return (
stripe_invoice.hosted_invoice_url
if "hosted_invoice_url" in stripe_invoice
else None
)
except stripe.StripeError:
return None

@classmethod
Expand Down Expand Up @@ -139,6 +143,6 @@ def mark_uncollectible_in_stripe(self):
Mark this invoice as uncollectible in Stripe.

Raises:
stripe.error.StripeError: If the Stripe API call fails
stripe.StripeError: If the Stripe API call fails
"""
get_payment_provider().get_invoice_service().mark_uncollectible(self.invoice_id)
7 changes: 4 additions & 3 deletions squarelet/organizations/models/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,8 +591,9 @@ def set_subscription(self, token, plan, max_users, user, payment_method=None):
def subscription_cancelled(self, subscription=None):
"""The subscription was cancelled due to payment failure

Takes an arg for the specific subscription to cancel.
If None, cancels the org's current subscription.
Args:
subscription: The specific subscription to cancel. If None,
cancels self.subscription (the org's current subscription).
"""
subscription = subscription or self.subscription

Expand Down Expand Up @@ -621,7 +622,7 @@ def subscription_cancelled(self, subscription=None):
subscription.subscription_id,
self.uuid,
)
except stripe.error.StripeError as exc:
except stripe.StripeError as exc:
logger.error(
"Failed to cancel Stripe subscription %s for organization %s: %s",
subscription.subscription_id,
Expand Down
8 changes: 4 additions & 4 deletions squarelet/organizations/models/payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def stripe_customer(self):
stripe_customer.id, name=self.organization.user_full_name
)
return stripe_customer
except stripe.error.InvalidRequestError as exc:
except stripe.InvalidRequestError as exc:
logger.error(
"[STRIPE CUSTOMER] Invalid Request Error "
"while fetching Customer %s "
Expand Down Expand Up @@ -254,7 +254,7 @@ def start(self, payment_method="card"):
"created" if created else "updated",
stripe_invoice.id,
)
except stripe.error.StripeError as exc:
except stripe.StripeError as exc:
# Log error but don't fail subscription creation
# Webhook will create the invoice as fallback
logger.error(
Expand Down Expand Up @@ -607,7 +607,7 @@ def make_stripe_plan(self):
product={"name": self.name, "unit_label": "Seats"},
**kwargs,
)
except stripe.error.InvalidRequestError: # pragma: no cover
except stripe.InvalidRequestError: # pragma: no cover
# if the plan already exists, just skip
pass

Expand All @@ -620,7 +620,7 @@ def delete_stripe_plan(self):
product = plan_service.retrieve_product(plan.product)
plan_service.delete(plan)
plan_service.delete_product(product)
except stripe.error.InvalidRequestError:
except stripe.InvalidRequestError:
# if the plan or product do not exist, just skip
pass

Expand Down
9 changes: 9 additions & 0 deletions squarelet/organizations/payments/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ def cancel_at_period_end(self, stripe_subscription):
def delete(self, stripe_subscription):
"""Immediately cancel and delete a subscription."""

@abstractmethod
def get_current_period_end(self, stripe_subscription):
"""Return the current period end timestamp for a subscription.

The field location changed in API version 2025-03-31.basil:
prior versions expose it at the subscription root; basil and later
expose it on each subscription item.
"""


class ChargeService(ABC):
"""Manages Stripe Charge objects."""
Expand Down
Loading
Loading