mirror of https://github.com/zulip/zulip.git
stripe: Move `make_end_of_cycle_updates_if_needed` to BillingSession.
Moves the 'make_end_of_cycle_updates_if_needed' function to the 'BillingSession' abstract class. This refactoring will help in minimizing duplicate code while supporting both realm and remote_server customers. Since the function is called from our main daily billing cron job as well, we have changed 'RealmBillingSession' to accept 'user=None' (our convention for automated system jobs).
This commit is contained in:
parent
5accf36115
commit
f8a0035215
|
@ -54,10 +54,10 @@ if settings.ZILENCER_ENABLED:
|
|||
|
||||
if settings.BILLING_ENABLED:
|
||||
from corporate.lib.stripe import (
|
||||
RealmBillingSession,
|
||||
downgrade_at_the_end_of_billing_cycle,
|
||||
downgrade_now_without_creating_additional_invoices,
|
||||
get_latest_seat_count,
|
||||
make_end_of_cycle_updates_if_needed,
|
||||
switch_realm_from_standard_to_plus_plan,
|
||||
void_all_open_invoices,
|
||||
)
|
||||
|
@ -185,6 +185,8 @@ def support(
|
|||
context["success_message"] = request.session["success_message"]
|
||||
del request.session["success_message"]
|
||||
|
||||
acting_user = request.user
|
||||
assert isinstance(acting_user, UserProfile)
|
||||
if settings.BILLING_ENABLED and request.method == "POST":
|
||||
# We check that request.POST only has two keys in it: The
|
||||
# realm_id and a field to change.
|
||||
|
@ -197,8 +199,6 @@ def support(
|
|||
assert realm_id is not None
|
||||
realm = Realm.objects.get(id=realm_id)
|
||||
|
||||
acting_user = request.user
|
||||
assert isinstance(acting_user, UserProfile)
|
||||
if plan_type is not None:
|
||||
current_plan_type = realm.plan_type
|
||||
do_change_realm_plan_type(realm, plan_type, acting_user=acting_user)
|
||||
|
@ -375,7 +375,8 @@ def support(
|
|||
current_plan=current_plan,
|
||||
)
|
||||
if current_plan is not None:
|
||||
new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed(
|
||||
billing_session = RealmBillingSession(user=None, realm=realm)
|
||||
new_plan, last_ledger_entry = billing_session.make_end_of_cycle_updates_if_needed(
|
||||
current_plan, timezone_now()
|
||||
)
|
||||
if last_ledger_entry is not None:
|
||||
|
|
|
@ -255,7 +255,11 @@ def next_invoice_date(plan: CustomerPlan) -> Optional[datetime]:
|
|||
def renewal_amount(plan: CustomerPlan, event_time: datetime) -> int: # nocoverage: TODO
|
||||
if plan.fixed_price is not None:
|
||||
return plan.fixed_price
|
||||
new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed(plan, event_time)
|
||||
realm = plan.customer.realm
|
||||
billing_session = RealmBillingSession(user=None, realm=realm)
|
||||
new_plan, last_ledger_entry = billing_session.make_end_of_cycle_updates_if_needed(
|
||||
plan, event_time
|
||||
)
|
||||
if last_ledger_entry is None:
|
||||
return 0
|
||||
if last_ledger_entry.licenses_at_next_renewal is None:
|
||||
|
@ -402,6 +406,7 @@ class AuditLogEventType(Enum):
|
|||
SPONSORSHIP_APPROVED = 5
|
||||
SPONSORSHIP_PENDING_STATUS_CHANGED = 6
|
||||
BILLING_METHOD_CHANGED = 7
|
||||
CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN = 8
|
||||
|
||||
|
||||
class BillingSessionAuditLogEventError(Exception):
|
||||
|
@ -464,6 +469,10 @@ class BillingSession(ABC):
|
|||
def do_change_plan_type(self, *, tier: Optional[int], is_sponsored: bool = False) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def process_downgrade(self, plan: CustomerPlan) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def approve_sponsorship(self) -> None:
|
||||
pass
|
||||
|
@ -821,207 +830,11 @@ class BillingSession(ABC):
|
|||
)
|
||||
return data
|
||||
|
||||
|
||||
class RealmBillingSession(BillingSession):
|
||||
def __init__(self, user: UserProfile, realm: Optional[Realm] = None) -> None:
|
||||
self.user = user
|
||||
if realm is not None:
|
||||
assert user.is_staff
|
||||
self.realm = realm
|
||||
self.support_session = True
|
||||
else:
|
||||
self.realm = user.realm
|
||||
self.support_session = False
|
||||
|
||||
@override
|
||||
@property
|
||||
def billing_session_url(self) -> str:
|
||||
return self.realm.uri
|
||||
|
||||
@override
|
||||
def get_customer(self) -> Optional[Customer]:
|
||||
return get_customer_by_realm(self.realm)
|
||||
|
||||
@override
|
||||
def current_count_for_billed_licenses(self) -> int:
|
||||
return get_latest_seat_count(self.realm)
|
||||
|
||||
@override
|
||||
def get_audit_log_event(self, event_type: AuditLogEventType) -> int:
|
||||
if event_type is AuditLogEventType.STRIPE_CUSTOMER_CREATED:
|
||||
return RealmAuditLog.STRIPE_CUSTOMER_CREATED
|
||||
elif event_type is AuditLogEventType.STRIPE_CARD_CHANGED:
|
||||
return RealmAuditLog.STRIPE_CARD_CHANGED
|
||||
elif event_type is AuditLogEventType.CUSTOMER_PLAN_CREATED:
|
||||
return RealmAuditLog.CUSTOMER_PLAN_CREATED
|
||||
elif event_type is AuditLogEventType.DISCOUNT_CHANGED:
|
||||
return RealmAuditLog.REALM_DISCOUNT_CHANGED
|
||||
elif event_type is AuditLogEventType.SPONSORSHIP_APPROVED:
|
||||
return RealmAuditLog.REALM_SPONSORSHIP_APPROVED
|
||||
elif event_type is AuditLogEventType.SPONSORSHIP_PENDING_STATUS_CHANGED:
|
||||
return RealmAuditLog.REALM_SPONSORSHIP_PENDING_STATUS_CHANGED
|
||||
elif event_type is AuditLogEventType.BILLING_METHOD_CHANGED:
|
||||
return RealmAuditLog.REALM_BILLING_METHOD_CHANGED
|
||||
else:
|
||||
raise BillingSessionAuditLogEventError(event_type)
|
||||
|
||||
@override
|
||||
def write_to_audit_log(
|
||||
self,
|
||||
event_type: AuditLogEventType,
|
||||
event_time: datetime,
|
||||
*,
|
||||
extra_data: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
audit_log_event = self.get_audit_log_event(event_type)
|
||||
if extra_data:
|
||||
RealmAuditLog.objects.create(
|
||||
realm=self.realm,
|
||||
acting_user=self.user,
|
||||
event_type=audit_log_event,
|
||||
event_time=event_time,
|
||||
extra_data=extra_data,
|
||||
)
|
||||
else:
|
||||
RealmAuditLog.objects.create(
|
||||
realm=self.realm,
|
||||
acting_user=self.user,
|
||||
event_type=audit_log_event,
|
||||
event_time=event_time,
|
||||
)
|
||||
|
||||
@override
|
||||
def get_data_for_stripe_customer(self) -> StripeCustomerData:
|
||||
# Support requests do not set any stripe billing information.
|
||||
assert self.support_session is False
|
||||
metadata: Dict[str, Any] = {}
|
||||
metadata["realm_id"] = self.realm.id
|
||||
metadata["realm_str"] = self.realm.string_id
|
||||
realm_stripe_customer_data = StripeCustomerData(
|
||||
description=f"{self.realm.string_id} ({self.realm.name})",
|
||||
email=self.user.delivery_email,
|
||||
metadata=metadata,
|
||||
)
|
||||
return realm_stripe_customer_data
|
||||
|
||||
@override
|
||||
def update_data_for_checkout_session_and_payment_intent(
|
||||
self, metadata: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
updated_metadata = dict(
|
||||
user_email=self.user.delivery_email,
|
||||
realm_id=self.realm.id,
|
||||
realm_str=self.realm.string_id,
|
||||
user_id=self.user.id,
|
||||
**metadata,
|
||||
)
|
||||
return updated_metadata
|
||||
|
||||
@override
|
||||
def get_data_for_stripe_payment_intent(
|
||||
self, price_per_license: int, licenses: int
|
||||
) -> StripePaymentIntentData:
|
||||
# Support requests do not set any stripe billing information.
|
||||
assert self.support_session is False
|
||||
amount = price_per_license * licenses
|
||||
description = f"Upgrade to Zulip Cloud Standard, ${price_per_license/100} x {licenses}"
|
||||
plan_name = "Zulip Cloud Standard"
|
||||
return StripePaymentIntentData(
|
||||
amount=amount,
|
||||
description=description,
|
||||
plan_name=plan_name,
|
||||
email=self.user.delivery_email,
|
||||
)
|
||||
|
||||
@override
|
||||
def update_or_create_customer(
|
||||
self, stripe_customer_id: Optional[str] = None, *, defaults: Optional[Dict[str, Any]] = None
|
||||
) -> Customer:
|
||||
if stripe_customer_id is not None:
|
||||
# Support requests do not set any stripe billing information.
|
||||
assert self.support_session is False
|
||||
customer, created = Customer.objects.update_or_create(
|
||||
realm=self.realm, defaults={"stripe_customer_id": stripe_customer_id}
|
||||
)
|
||||
from zerver.actions.users import do_make_user_billing_admin
|
||||
|
||||
do_make_user_billing_admin(self.user)
|
||||
return customer
|
||||
else:
|
||||
customer, created = Customer.objects.update_or_create(
|
||||
realm=self.realm, defaults=defaults
|
||||
)
|
||||
return customer
|
||||
|
||||
@override
|
||||
def do_change_plan_type(self, *, tier: Optional[int], is_sponsored: bool = False) -> None:
|
||||
from zerver.actions.realm_settings import do_change_realm_plan_type
|
||||
|
||||
# This function needs to translate between the different
|
||||
# formats of CustomerPlan.tier and Realm.plan_type.
|
||||
if is_sponsored:
|
||||
plan_type = Realm.PLAN_TYPE_STANDARD_FREE
|
||||
elif tier == CustomerPlan.STANDARD:
|
||||
plan_type = Realm.PLAN_TYPE_STANDARD
|
||||
elif tier == CustomerPlan.PLUS: # nocoverage # Plus plan doesn't use this code path yet.
|
||||
plan_type = Realm.PLAN_TYPE_PLUS
|
||||
else:
|
||||
raise AssertionError("Unexpected tier")
|
||||
do_change_realm_plan_type(self.realm, plan_type, acting_user=self.user)
|
||||
|
||||
@override
|
||||
def approve_sponsorship(self) -> None:
|
||||
# Sponsorship approval is only a support admin action.
|
||||
assert self.support_session
|
||||
|
||||
from zerver.actions.message_send import internal_send_private_message
|
||||
|
||||
self.do_change_plan_type(tier=None, is_sponsored=True)
|
||||
customer = self.get_customer()
|
||||
if customer is not None and customer.sponsorship_pending:
|
||||
customer.sponsorship_pending = False
|
||||
customer.save(update_fields=["sponsorship_pending"])
|
||||
self.write_to_audit_log(
|
||||
event_type=AuditLogEventType.SPONSORSHIP_APPROVED, event_time=timezone_now()
|
||||
)
|
||||
notification_bot = get_system_bot(settings.NOTIFICATION_BOT, self.realm.id)
|
||||
for user in self.realm.get_human_billing_admin_and_realm_owner_users():
|
||||
with override_language(user.default_language):
|
||||
# Using variable to make life easier for translators if these details change.
|
||||
message = _(
|
||||
"Your organization's request for sponsored hosting has been approved! "
|
||||
"You have been upgraded to {plan_name}, free of charge. {emoji}\n\n"
|
||||
"If you could {begin_link}list Zulip as a sponsor on your website{end_link}, "
|
||||
"we would really appreciate it!"
|
||||
).format(
|
||||
plan_name="Zulip Cloud Standard",
|
||||
emoji=":tada:",
|
||||
begin_link="[",
|
||||
end_link="](/help/linking-to-zulip-website)",
|
||||
)
|
||||
internal_send_private_message(notification_bot, user, message)
|
||||
|
||||
|
||||
def stripe_customer_has_credit_card_as_default_payment_method(
|
||||
stripe_customer: stripe.Customer,
|
||||
) -> bool:
|
||||
if not stripe_customer.invoice_settings.default_payment_method:
|
||||
return False
|
||||
return stripe_customer.invoice_settings.default_payment_method.type == "card"
|
||||
|
||||
|
||||
def customer_has_credit_card_as_default_payment_method(customer: Customer) -> bool:
|
||||
if not customer.stripe_customer_id:
|
||||
return False
|
||||
stripe_customer = stripe_get_customer(customer.stripe_customer_id)
|
||||
return stripe_customer_has_credit_card_as_default_payment_method(stripe_customer)
|
||||
|
||||
|
||||
# event_time should roughly be timezone_now(). Not designed to handle
|
||||
# event_times in the past or future
|
||||
@transaction.atomic
|
||||
def make_end_of_cycle_updates_if_needed(
|
||||
plan: CustomerPlan, event_time: datetime
|
||||
self, plan: CustomerPlan, event_time: datetime
|
||||
) -> Tuple[Optional[CustomerPlan], Optional[LicenseLedger]]:
|
||||
last_ledger_entry = LicenseLedger.objects.filter(plan=plan).order_by("-id").first()
|
||||
last_ledger_renewal = (
|
||||
|
@ -1097,13 +910,9 @@ def make_end_of_cycle_updates_if_needed(
|
|||
licenses_at_next_renewal=licenses_at_next_renewal,
|
||||
)
|
||||
|
||||
realm = new_plan.customer.realm
|
||||
assert realm is not None
|
||||
|
||||
RealmAuditLog.objects.create(
|
||||
realm=realm,
|
||||
self.write_to_audit_log(
|
||||
event_type=AuditLogEventType.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN,
|
||||
event_time=event_time,
|
||||
event_type=RealmAuditLog.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN,
|
||||
extra_data={
|
||||
"monthly_plan_id": plan.id,
|
||||
"annual_plan_id": new_plan.id,
|
||||
|
@ -1155,11 +964,224 @@ def make_end_of_cycle_updates_if_needed(
|
|||
return plus_plan, plus_plan_ledger_entry
|
||||
|
||||
if plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE:
|
||||
process_downgrade(plan)
|
||||
self.process_downgrade(plan)
|
||||
return None, None
|
||||
return None, last_ledger_entry
|
||||
|
||||
|
||||
class RealmBillingSession(BillingSession):
|
||||
def __init__(self, user: Optional[UserProfile] = None, realm: Optional[Realm] = None) -> None:
|
||||
self.user = user
|
||||
assert user is not None or realm is not None
|
||||
if user is not None and realm is not None:
|
||||
assert user.is_staff
|
||||
self.realm = realm
|
||||
self.support_session = True
|
||||
elif user is not None:
|
||||
self.realm = user.realm
|
||||
self.support_session = False
|
||||
else:
|
||||
assert realm is not None # for mypy
|
||||
self.realm = realm
|
||||
self.support_session = False
|
||||
|
||||
@override
|
||||
@property
|
||||
def billing_session_url(self) -> str:
|
||||
return self.realm.uri
|
||||
|
||||
@override
|
||||
def get_customer(self) -> Optional[Customer]:
|
||||
return get_customer_by_realm(self.realm)
|
||||
|
||||
@override
|
||||
def current_count_for_billed_licenses(self) -> int:
|
||||
return get_latest_seat_count(self.realm)
|
||||
|
||||
@override
|
||||
def get_audit_log_event(self, event_type: AuditLogEventType) -> int:
|
||||
if event_type is AuditLogEventType.STRIPE_CUSTOMER_CREATED:
|
||||
return RealmAuditLog.STRIPE_CUSTOMER_CREATED
|
||||
elif event_type is AuditLogEventType.STRIPE_CARD_CHANGED:
|
||||
return RealmAuditLog.STRIPE_CARD_CHANGED
|
||||
elif event_type is AuditLogEventType.CUSTOMER_PLAN_CREATED:
|
||||
return RealmAuditLog.CUSTOMER_PLAN_CREATED
|
||||
elif event_type is AuditLogEventType.DISCOUNT_CHANGED:
|
||||
return RealmAuditLog.REALM_DISCOUNT_CHANGED
|
||||
elif event_type is AuditLogEventType.SPONSORSHIP_APPROVED:
|
||||
return RealmAuditLog.REALM_SPONSORSHIP_APPROVED
|
||||
elif event_type is AuditLogEventType.SPONSORSHIP_PENDING_STATUS_CHANGED:
|
||||
return RealmAuditLog.REALM_SPONSORSHIP_PENDING_STATUS_CHANGED
|
||||
elif event_type is AuditLogEventType.BILLING_METHOD_CHANGED:
|
||||
return RealmAuditLog.REALM_BILLING_METHOD_CHANGED
|
||||
elif event_type is AuditLogEventType.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN:
|
||||
return RealmAuditLog.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN
|
||||
else:
|
||||
raise BillingSessionAuditLogEventError(event_type)
|
||||
|
||||
@override
|
||||
def write_to_audit_log(
|
||||
self,
|
||||
event_type: AuditLogEventType,
|
||||
event_time: datetime,
|
||||
*,
|
||||
extra_data: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
audit_log_event = self.get_audit_log_event(event_type)
|
||||
audit_log_data = {
|
||||
"realm": self.realm,
|
||||
"event_type": audit_log_event,
|
||||
"event_time": event_time,
|
||||
}
|
||||
|
||||
if extra_data:
|
||||
audit_log_data["extra_data"] = extra_data
|
||||
|
||||
if self.user is not None:
|
||||
audit_log_data["acting_user"] = self.user
|
||||
|
||||
RealmAuditLog.objects.create(**audit_log_data)
|
||||
|
||||
@override
|
||||
def get_data_for_stripe_customer(self) -> StripeCustomerData:
|
||||
# Support requests do not set any stripe billing information.
|
||||
assert self.support_session is False
|
||||
assert self.user is not None
|
||||
metadata: Dict[str, Any] = {}
|
||||
metadata["realm_id"] = self.realm.id
|
||||
metadata["realm_str"] = self.realm.string_id
|
||||
realm_stripe_customer_data = StripeCustomerData(
|
||||
description=f"{self.realm.string_id} ({self.realm.name})",
|
||||
email=self.user.delivery_email,
|
||||
metadata=metadata,
|
||||
)
|
||||
return realm_stripe_customer_data
|
||||
|
||||
@override
|
||||
def update_data_for_checkout_session_and_payment_intent(
|
||||
self, metadata: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
assert self.user is not None
|
||||
updated_metadata = dict(
|
||||
user_email=self.user.delivery_email,
|
||||
realm_id=self.realm.id,
|
||||
realm_str=self.realm.string_id,
|
||||
user_id=self.user.id,
|
||||
**metadata,
|
||||
)
|
||||
return updated_metadata
|
||||
|
||||
@override
|
||||
def get_data_for_stripe_payment_intent(
|
||||
self, price_per_license: int, licenses: int
|
||||
) -> StripePaymentIntentData:
|
||||
# Support requests do not set any stripe billing information.
|
||||
assert self.support_session is False
|
||||
assert self.user is not None
|
||||
amount = price_per_license * licenses
|
||||
description = f"Upgrade to Zulip Cloud Standard, ${price_per_license/100} x {licenses}"
|
||||
plan_name = "Zulip Cloud Standard"
|
||||
return StripePaymentIntentData(
|
||||
amount=amount,
|
||||
description=description,
|
||||
plan_name=plan_name,
|
||||
email=self.user.delivery_email,
|
||||
)
|
||||
|
||||
@override
|
||||
def update_or_create_customer(
|
||||
self, stripe_customer_id: Optional[str] = None, *, defaults: Optional[Dict[str, Any]] = None
|
||||
) -> Customer:
|
||||
if stripe_customer_id is not None:
|
||||
# Support requests do not set any stripe billing information.
|
||||
assert self.support_session is False
|
||||
customer, created = Customer.objects.update_or_create(
|
||||
realm=self.realm, defaults={"stripe_customer_id": stripe_customer_id}
|
||||
)
|
||||
from zerver.actions.users import do_make_user_billing_admin
|
||||
|
||||
assert self.user is not None
|
||||
do_make_user_billing_admin(self.user)
|
||||
return customer
|
||||
else:
|
||||
customer, created = Customer.objects.update_or_create(
|
||||
realm=self.realm, defaults=defaults
|
||||
)
|
||||
return customer
|
||||
|
||||
@override
|
||||
def do_change_plan_type(self, *, tier: Optional[int], is_sponsored: bool = False) -> None:
|
||||
from zerver.actions.realm_settings import do_change_realm_plan_type
|
||||
|
||||
# This function needs to translate between the different
|
||||
# formats of CustomerPlan.tier and Realm.plan_type.
|
||||
if is_sponsored:
|
||||
plan_type = Realm.PLAN_TYPE_STANDARD_FREE
|
||||
elif tier == CustomerPlan.STANDARD:
|
||||
plan_type = Realm.PLAN_TYPE_STANDARD
|
||||
elif tier == CustomerPlan.PLUS: # nocoverage # Plus plan doesn't use this code path yet.
|
||||
plan_type = Realm.PLAN_TYPE_PLUS
|
||||
else:
|
||||
raise AssertionError("Unexpected tier")
|
||||
do_change_realm_plan_type(self.realm, plan_type, acting_user=self.user)
|
||||
|
||||
@override
|
||||
def process_downgrade(self, plan: CustomerPlan) -> None:
|
||||
from zerver.actions.realm_settings import do_change_realm_plan_type
|
||||
|
||||
assert plan.customer.realm is not None
|
||||
do_change_realm_plan_type(plan.customer.realm, Realm.PLAN_TYPE_LIMITED, acting_user=None)
|
||||
plan.status = CustomerPlan.ENDED
|
||||
plan.save(update_fields=["status"])
|
||||
|
||||
@override
|
||||
def approve_sponsorship(self) -> None:
|
||||
# Sponsorship approval is only a support admin action.
|
||||
assert self.support_session
|
||||
|
||||
from zerver.actions.message_send import internal_send_private_message
|
||||
|
||||
self.do_change_plan_type(tier=None, is_sponsored=True)
|
||||
customer = self.get_customer()
|
||||
if customer is not None and customer.sponsorship_pending:
|
||||
customer.sponsorship_pending = False
|
||||
customer.save(update_fields=["sponsorship_pending"])
|
||||
self.write_to_audit_log(
|
||||
event_type=AuditLogEventType.SPONSORSHIP_APPROVED, event_time=timezone_now()
|
||||
)
|
||||
notification_bot = get_system_bot(settings.NOTIFICATION_BOT, self.realm.id)
|
||||
for user in self.realm.get_human_billing_admin_and_realm_owner_users():
|
||||
with override_language(user.default_language):
|
||||
# Using variable to make life easier for translators if these details change.
|
||||
message = _(
|
||||
"Your organization's request for sponsored hosting has been approved! "
|
||||
"You have been upgraded to {plan_name}, free of charge. {emoji}\n\n"
|
||||
"If you could {begin_link}list Zulip as a sponsor on your website{end_link}, "
|
||||
"we would really appreciate it!"
|
||||
).format(
|
||||
plan_name="Zulip Cloud Standard",
|
||||
emoji=":tada:",
|
||||
begin_link="[",
|
||||
end_link="](/help/linking-to-zulip-website)",
|
||||
)
|
||||
internal_send_private_message(notification_bot, user, message)
|
||||
|
||||
|
||||
def stripe_customer_has_credit_card_as_default_payment_method(
|
||||
stripe_customer: stripe.Customer,
|
||||
) -> bool:
|
||||
if not stripe_customer.invoice_settings.default_payment_method:
|
||||
return False
|
||||
return stripe_customer.invoice_settings.default_payment_method.type == "card"
|
||||
|
||||
|
||||
def customer_has_credit_card_as_default_payment_method(customer: Customer) -> bool:
|
||||
if not customer.stripe_customer_id:
|
||||
return False
|
||||
stripe_customer = stripe_get_customer(customer.stripe_customer_id)
|
||||
return stripe_customer_has_credit_card_as_default_payment_method(stripe_customer)
|
||||
|
||||
|
||||
def calculate_discounted_price_per_license(
|
||||
original_price_per_license: int, discount: Decimal
|
||||
) -> int:
|
||||
|
@ -1303,7 +1325,10 @@ def update_license_ledger_for_manual_plan(
|
|||
def update_license_ledger_for_automanaged_plan(
|
||||
realm: Realm, plan: CustomerPlan, event_time: datetime
|
||||
) -> None:
|
||||
new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed(plan, event_time)
|
||||
billing_session = RealmBillingSession(user=None, realm=realm)
|
||||
new_plan, last_ledger_entry = billing_session.make_end_of_cycle_updates_if_needed(
|
||||
plan, event_time
|
||||
)
|
||||
if last_ledger_entry is None:
|
||||
return
|
||||
if new_plan is not None:
|
||||
|
@ -1345,7 +1370,9 @@ def invoice_plan(plan: CustomerPlan, event_time: datetime) -> None:
|
|||
f"Realm {plan.customer.realm.string_id} has a paid plan without a Stripe customer."
|
||||
)
|
||||
|
||||
make_end_of_cycle_updates_if_needed(plan, event_time)
|
||||
realm = plan.customer.realm
|
||||
billing_session = RealmBillingSession(user=None, realm=realm)
|
||||
billing_session.make_end_of_cycle_updates_if_needed(plan, event_time)
|
||||
|
||||
if plan.invoicing_status == CustomerPlan.INITIAL_INVOICE_TO_BE_SENT:
|
||||
invoiced_through_id = -1
|
||||
|
@ -1466,15 +1493,6 @@ def do_change_plan_status(plan: CustomerPlan, status: int) -> None:
|
|||
)
|
||||
|
||||
|
||||
def process_downgrade(plan: CustomerPlan) -> None:
|
||||
from zerver.actions.realm_settings import do_change_realm_plan_type
|
||||
|
||||
assert plan.customer.realm is not None
|
||||
do_change_realm_plan_type(plan.customer.realm, Realm.PLAN_TYPE_LIMITED, acting_user=None)
|
||||
plan.status = CustomerPlan.ENDED
|
||||
plan.save(update_fields=["status"])
|
||||
|
||||
|
||||
# During realm deactivation we instantly downgrade the plan to Limited.
|
||||
# Extra users added in the final month are not charged. Also used
|
||||
# for the cancellation of Free Trial.
|
||||
|
@ -1483,7 +1501,8 @@ def downgrade_now_without_creating_additional_invoices(realm: Realm) -> None:
|
|||
if plan is None:
|
||||
return
|
||||
|
||||
process_downgrade(plan)
|
||||
billing_session = RealmBillingSession(user=None, realm=realm)
|
||||
billing_session.process_downgrade(plan)
|
||||
plan.invoiced_through = LicenseLedger.objects.filter(plan=plan).order_by("id").last()
|
||||
plan.next_invoice_date = next_invoice_date(plan)
|
||||
plan.save(update_fields=["invoiced_through", "next_invoice_date"])
|
||||
|
|
|
@ -62,7 +62,6 @@ from corporate.lib.stripe import (
|
|||
is_free_trial_offer_enabled,
|
||||
is_realm_on_free_trial,
|
||||
is_sponsored_realm,
|
||||
make_end_of_cycle_updates_if_needed,
|
||||
next_month,
|
||||
sign_string,
|
||||
stripe_customer_has_credit_card_as_default_payment_method,
|
||||
|
@ -4503,11 +4502,17 @@ class LicenseLedgerTest(StripeTestCase):
|
|||
self.assertEqual(LicenseLedger.objects.count(), 1)
|
||||
plan = CustomerPlan.objects.get()
|
||||
# Plan hasn't renewed yet
|
||||
make_end_of_cycle_updates_if_needed(plan, self.next_year - timedelta(days=1))
|
||||
realm = plan.customer.realm
|
||||
billing_session = RealmBillingSession(user=None, realm=realm)
|
||||
billing_session.make_end_of_cycle_updates_if_needed(
|
||||
plan, self.next_year - timedelta(days=1)
|
||||
)
|
||||
self.assertEqual(LicenseLedger.objects.count(), 1)
|
||||
# Plan needs to renew
|
||||
# TODO: do_deactivate_user for a user, so that licenses_at_next_renewal != licenses
|
||||
new_plan, ledger_entry = make_end_of_cycle_updates_if_needed(plan, self.next_year)
|
||||
new_plan, ledger_entry = billing_session.make_end_of_cycle_updates_if_needed(
|
||||
plan, self.next_year
|
||||
)
|
||||
self.assertIsNone(new_plan)
|
||||
self.assertEqual(LicenseLedger.objects.count(), 2)
|
||||
ledger_params = {
|
||||
|
@ -4520,7 +4525,9 @@ class LicenseLedgerTest(StripeTestCase):
|
|||
for key, value in ledger_params.items():
|
||||
self.assertEqual(getattr(ledger_entry, key), value)
|
||||
# Plan needs to renew, but we already added the plan_renewal ledger entry
|
||||
make_end_of_cycle_updates_if_needed(plan, self.next_year + timedelta(days=1))
|
||||
billing_session.make_end_of_cycle_updates_if_needed(
|
||||
plan, self.next_year + timedelta(days=1)
|
||||
)
|
||||
self.assertEqual(LicenseLedger.objects.count(), 2)
|
||||
|
||||
def test_update_license_ledger_if_needed(self) -> None:
|
||||
|
@ -4632,7 +4639,8 @@ class LicenseLedgerTest(StripeTestCase):
|
|||
self.assertEqual(plan.licenses(), self.seat_count + 10)
|
||||
self.assertEqual(plan.licenses_at_next_renewal(), self.seat_count + 10)
|
||||
|
||||
make_end_of_cycle_updates_if_needed(plan, self.next_year)
|
||||
billing_session = RealmBillingSession(user=None, realm=realm)
|
||||
billing_session.make_end_of_cycle_updates_if_needed(plan, self.next_year)
|
||||
self.assertEqual(plan.licenses(), self.seat_count + 10)
|
||||
|
||||
ledger_entries = list(
|
||||
|
|
|
@ -10,13 +10,13 @@ from django.utils.timezone import now as timezone_now
|
|||
from django.utils.translation import gettext as _
|
||||
|
||||
from corporate.lib.stripe import (
|
||||
RealmBillingSession,
|
||||
cents_to_dollar_string,
|
||||
do_change_plan_status,
|
||||
downgrade_at_the_end_of_billing_cycle,
|
||||
downgrade_now_without_creating_additional_invoices,
|
||||
format_money,
|
||||
get_latest_seat_count,
|
||||
make_end_of_cycle_updates_if_needed,
|
||||
renewal_amount,
|
||||
start_of_next_billing_cycle,
|
||||
stripe_get_customer,
|
||||
|
@ -159,7 +159,9 @@ def billing_home(
|
|||
plan = get_current_plan_by_customer(customer)
|
||||
if plan is not None:
|
||||
now = timezone_now()
|
||||
new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed(plan, now)
|
||||
realm = plan.customer.realm
|
||||
billing_session = RealmBillingSession(user=None, realm=realm)
|
||||
new_plan, last_ledger_entry = billing_session.make_end_of_cycle_updates_if_needed(plan, now)
|
||||
if last_ledger_entry is not None:
|
||||
if new_plan is not None: # nocoverage
|
||||
plan = new_plan
|
||||
|
@ -254,7 +256,11 @@ def update_plan(
|
|||
plan = get_current_plan_by_realm(user.realm)
|
||||
assert plan is not None # for mypy
|
||||
|
||||
new_plan, last_ledger_entry = make_end_of_cycle_updates_if_needed(plan, timezone_now())
|
||||
realm = plan.customer.realm
|
||||
billing_session = RealmBillingSession(user=None, realm=realm)
|
||||
new_plan, last_ledger_entry = billing_session.make_end_of_cycle_updates_if_needed(
|
||||
plan, timezone_now()
|
||||
)
|
||||
if new_plan is not None:
|
||||
raise JsonableError(
|
||||
_("Unable to update the plan. The plan has been expired and replaced with a new plan.")
|
||||
|
|
Loading…
Reference in New Issue