Skip to content

Commit 3b46530

Browse files
committed
add endpoint to send "trial will end" email
1 parent 8c62640 commit 3b46530

File tree

4 files changed

+100
-1
lines changed

4 files changed

+100
-1
lines changed

backend/btrixcloud/emailsender.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import smtplib
66
import ssl
77
from uuid import UUID
8-
from typing import Optional, Union
8+
from typing import Optional, Union, Literal
99

1010
from email.message import EmailMessage
1111
from email.mime.text import MIMEText
@@ -220,3 +220,28 @@ async def send_subscription_will_be_canceled(
220220
support_email=self.support_email,
221221
survey_url=self.survey_url,
222222
)
223+
224+
async def send_subscription_trial_ending_soon(
225+
self,
226+
trial_end_date: datetime,
227+
user_name: str,
228+
receiver_email: str,
229+
behavior_on_trial_end: Literal["cancel", "continue"],
230+
org: Organization,
231+
headers=None,
232+
):
233+
"""Send email indicating subscription trial is ending soon"""
234+
235+
origin = get_origin(headers)
236+
org_url = f"{origin}/orgs/{org.slug}/"
237+
238+
await self._send_encrypted(
239+
receiver_email,
240+
"trialEndingSoon",
241+
user_name=user_name,
242+
org_name=org.name,
243+
org_url=org_url,
244+
trial_end_date=trial_end_date.isoformat(),
245+
behavior_on_trial_end=behavior_on_trial_end,
246+
support_email=self.support_email,
247+
)

backend/btrixcloud/models.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1904,6 +1904,14 @@ class SubscriptionCancel(BaseModel):
19041904
subId: str
19051905

19061906

1907+
# ============================================================================
1908+
class SubscriptionTrialEndReminder(BaseModel):
1909+
"""Email reminder that subscription will end soon"""
1910+
1911+
subId: str
1912+
behavior_on_trial_end: Literal["cancel", "continue"]
1913+
1914+
19071915
# ============================================================================
19081916
class SubscriptionCancelOut(SubscriptionCancel, SubscriptionEventOut):
19091917
"""Output model for subscription cancellation event"""
@@ -1940,6 +1948,8 @@ class Subscription(BaseModel):
19401948
planId: str
19411949

19421950
futureCancelDate: Optional[datetime] = None
1951+
"When in a trial, future cancel date is the trial end date; when not in a trial, future cancel date is the date the subscription will be canceled, if set"
1952+
19431953
readOnlyOnCancel: bool = False
19441954

19451955

@@ -1950,6 +1960,12 @@ class SubscriptionCanceledResponse(BaseModel):
19501960
deleted: bool
19511961
canceled: bool
19521962

1963+
# ============================================================================
1964+
class SubscriptionReminderResponse(BaseModel):
1965+
"""Response model for subscription reminder"""
1966+
1967+
sent: bool
1968+
19531969

19541970
# ============================================================================
19551971
# User Org Info With Subs

backend/btrixcloud/orgs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,11 @@ async def cancel_subscription_data(
553553
)
554554
return Organization.from_dict(org_data) if org_data else None
555555

556+
async def find_org_by_subscription_id(self, sub_id: str) -> Optional[Organization]:
557+
"""Find org by subscription id"""
558+
org_data = await self.orgs.find_one({"subscription.subId": sub_id})
559+
return Organization.from_dict(org_data) if org_data else None
560+
556561
async def is_subscription_activated(self, sub_id: str) -> bool:
557562
"""return true if subscription for this org was 'activated', eg. at least
558563
one user has signed up and changed the slug

backend/btrixcloud/subs.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
SubscriptionPortalUrlRequest,
2828
SubscriptionPortalUrlResponse,
2929
SubscriptionCanceledResponse,
30+
SubscriptionReminderResponse,
3031
Organization,
3132
InviteToOrgRequest,
3233
InviteAddedResponse,
@@ -40,6 +41,7 @@
4041
)
4142
from .pagination import DEFAULT_PAGE_SIZE, paginated_format
4243
from .utils import dt_now
44+
from backend.btrixcloud.models import SubscriptionTrialEndReminder
4345

4446

4547
# if set, will enable this api
@@ -182,6 +184,46 @@ async def cancel_subscription(self, cancel: SubscriptionCancel) -> dict[str, boo
182184
await self.add_sub_event("cancel", cancel, org.id)
183185
return {"canceled": True, "deleted": deleted}
184186

187+
async def send_trial_end_reminder(
188+
self,
189+
reminder: SubscriptionTrialEndReminder,
190+
):
191+
org = await self.org_ops.find_org_by_subscription_id(reminder.subId)
192+
193+
if not org:
194+
print(f"Organization not found for subscription ID {reminder.subId}")
195+
raise HTTPException(
196+
status_code=404, detail="org_for_subscription_not_found"
197+
)
198+
199+
if not org.subscription:
200+
print(f"Subscription not found for organization ID {org.id} with sub id {reminder.subId}")
201+
raise HTTPException(
202+
status_code=404, detail="subscription_not_found"
203+
)
204+
205+
if not org.subscription.futureCancelDate:
206+
print(f"Future cancel date not found for subscription ID {reminder.subId}")
207+
raise HTTPException(
208+
status_code=404, detail="future_cancel_date_not_found"
209+
)
210+
211+
users = await self.org_ops.get_users_for_org(org, UserRole.OWNER)
212+
await asyncio.gather(
213+
*[
214+
self.user_manager.email.send_subscription_trial_ending_soon(
215+
trial_end_date=org.subscription.futureCancelDate,
216+
user_name=user.name,
217+
receiver_email=user.email,
218+
org=org,
219+
behavior_on_trial_end=reminder.behavior_on_trial_end
220+
)
221+
for user in users
222+
]
223+
)
224+
225+
return SubscriptionReminderResponse(sent=True)
226+
185227
async def add_sub_event(
186228
self,
187229
type_: str,
@@ -395,6 +437,17 @@ async def cancel_subscription(
395437
):
396438
return await ops.cancel_subscription(cancel)
397439

440+
@app.post(
441+
"/subscriptions/trial-end-reminder",
442+
tags=["subscriptions"],
443+
dependencies=[Depends(user_or_shared_secret_dep)],
444+
response_model=SubscriptionReminderResponse,
445+
)
446+
async def send_trial_end_reminder(
447+
reminder: SubscriptionTrialEndReminder,
448+
):
449+
return await ops.send_trial_end_reminder(reminder)
450+
398451
assert org_ops.router
399452

400453
@app.get(

0 commit comments

Comments
 (0)