Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 1 deletion backend/routers/payment.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from config.plan_catalog import configured_ladder_prices, validate_ladder_price

Check warning on line 1 in backend/routers/payment.py

View workflow job for this annotation

GitHub Actions / Hygiene

Large changed file

backend/routers/payment.py is 1934 lines; consider splitting files over 800 lines.
from database.fsr_read_cache import fresh_reader, fresh_reads
import os
from datetime import datetime, timezone
Expand Down Expand Up @@ -447,7 +447,7 @@
current_price_id = stripe_sub['items']['data'][0]['price']['id']

# Check for pending subscription schedules
customer_id = stripe_sub.get('customer')

Check warning on line 450 in backend/routers/payment.py

View workflow job for this annotation

GitHub Actions / Hygiene

Long function

get_available_plans_endpoint is 164 lines; consider extracting focused helpers over 150 lines.
if customer_id:
try:
# Get all subscription schedules for this customer
Expand Down Expand Up @@ -655,7 +655,7 @@
# subscriber promised something this checkout cannot give, so refuse rather than charge at once.
if request.trial_days and existing_customer_id:
raise HTTPException(status_code=400, detail="trial_unavailable")
trial_period_days = request.trial_days or None
trial_period_days = None if resolved_checkout_promo_id else (request.trial_days or None)
try:
session = stripe_utils.create_subscription_checkout_session(
uid,
Expand Down Expand Up @@ -809,7 +809,7 @@
from utils.processing_backlog_worker import upgrade

if allowance_increased(current_subscription, new_subscription):
upgrade(

Check warning on line 812 in backend/routers/payment.py

View workflow job for this annotation

GitHub Actions / Hygiene

Long function

upgrade_subscription_endpoint is 215 lines; consider extracting focused helpers over 150 lines.
uid,
f'plan-change:{new_subscription.stripe_subscription_id}:{request.price_id}:{new_subscription.current_period_start}',
intent_at=time.time(),
Expand Down Expand Up @@ -1124,7 +1124,7 @@
f"Ignoring checkout session for subscription with unknown price_id: {price_id}"
)
except Exception as e:
logger.error(f"Error retrieving subscription for notification: {sanitize(str(e))}")

Check warning on line 1127 in backend/routers/payment.py

View workflow job for this annotation

GitHub Actions / Hygiene

Long function

_handle_stripe_event is 446 lines; consider extracting focused helpers over 150 lines.

if event['type'] in [
'customer.subscription.updated',
Expand Down
61 changes: 61 additions & 0 deletions backend/tests/unit/test_payment_checkout_trial.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,64 @@ def test_no_trial_requested_keeps_the_old_checkout():
assert result['session_id'] == 'cs_test'
assert captured['trial_period_days'] is None
assert captured['customer_id'] == 'cus_existing'


def test_promo_code_suppresses_trial_period_days_on_checkout_endpoint():
"""A promotion code on a trial checkout suppresses trial_period_days so the
initial invoice is not $0 and Stripe's minimum_amount check passes (#23300)."""
captured = {}

def create_session(
uid, price_id, idempotency_key, customer_id=None, promotion_code_id=None, trial_period_days=None
):
captured['promotion_code_id'] = promotion_code_id
captured['trial_period_days'] = trial_period_days
return SimpleNamespace(url='https://checkout.stripe.com/c/pay/cs_test', id='cs_test')

promo_obj = SimpleNamespace(id='promo_unlimited_year')
with (
patch.object(payment, 'is_purchasable_price_id', return_value=True),
patch.object(payment.subscription_utils, 'can_user_make_payment', return_value=(True, '')),
patch.object(payment.subscription_utils, 'new_buyer_single_plan_violation', return_value=None, create=True),
patch.object(payment, '_try_reactivate_subscription', return_value=None),
patch.object(payment.users_db, 'get_stripe_customer_id', return_value=None),
patch.object(payment.stripe.PromotionCode, 'list', return_value=SimpleNamespace(data=[promo_obj])),
patch.object(payment.stripe_utils, 'create_subscription_checkout_session', side_effect=create_session),
):
result = payment.create_checkout_session_endpoint(
CreateCheckoutRequest(price_id='price_1', trial_days=7, promotion_code='OMI-4FHY-W6SR'),
uid='uid_1',
)

assert result['session_id'] == 'cs_test'
assert captured['promotion_code_id'] == 'promo_unlimited_year'
assert captured['trial_period_days'] is None


def test_stripe_helper_omits_trial_period_days_when_promotion_code_present():
"""create_subscription_checkout_session omits subscription_data.trial_period_days
when promotion_code_id is provided so minimum_amount codes are not rejected (#23300)."""
import importlib
from unittest.mock import MagicMock
import utils.stripe as stripe_utils

if isinstance(getattr(stripe_utils, 'create_subscription_checkout_session', None), MagicMock):
stripe_utils = importlib.reload(stripe_utils)

captured = {}

def fake_session_create(**params):
captured.update(params)
return SimpleNamespace(url='https://checkout.stripe.com/c/pay/cs_test', id='cs_test')

with patch('utils.stripe.stripe.checkout.Session.create', side_effect=fake_session_create):
stripe_utils.create_subscription_checkout_session(
uid='uid_1',
price_id='price_unlimited_yearly',
idempotency_key='idem_1',
promotion_code_id='promo_unlimited_year',
trial_period_days=7,
)

assert captured['discounts'] == [{'promotion_code': 'promo_unlimited_year'}]
assert 'trial_period_days' not in captured['subscription_data']
5 changes: 4 additions & 1 deletion backend/utils/stripe.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ def create_subscription_checkout_session(
}

if trial_period_days:
session_params['subscription_data']['trial_period_days'] = trial_period_days
# A free trial makes the initial Checkout invoice $0, so Stripe rejects
# promotion codes with a minimum_amount restriction (issue #23300).
if not promotion_code_id:
session_params['subscription_data']['trial_period_days'] = trial_period_days

if promotion_code_id:
session_params['discounts'] = [{'promotion_code': promotion_code_id}]
Expand Down
Loading