Skip to content

Commit

Permalink
➕ Declined booking event handling (#776)
Browse files Browse the repository at this point in the history
  • Loading branch information
devmount authored Nov 28, 2024
1 parent 5d7a0bf commit 81e9274
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 19 deletions.
51 changes: 41 additions & 10 deletions backend/src/appointment/controller/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from icalendar import Calendar, Event, vCalAddress, vText
from datetime import datetime, timedelta, timezone, UTC
from zoneinfo import ZoneInfo
from enum import Enum

from sqlalchemy.orm import Session

Expand All @@ -34,8 +35,12 @@
from ..controller.mailer import Attachment
from ..exceptions.validation import RemoteCalendarConnectionError
from ..l10n import l10n
from ..tasks.emails import send_invite_email, send_pending_email
from ..tasks.emails import send_invite_email, send_pending_email, send_rejection_email

class RemoteEventState(Enum):
CANCELLED = 'CANCELLED'
TENTATIVE = 'TENTATIVE'
CONFIRMED = 'CONFIRMED'

class BaseConnector:
redis_instance: Redis | RedisCluster | None
Expand Down Expand Up @@ -502,13 +507,13 @@ def create_vevent(
appointment: schemas.Appointment,
slot: schemas.Slot,
organizer: schemas.Subscriber,
on_hold = False,
event_status = RemoteEventState.CONFIRMED.value,
):
"""create an event in ical format for .ics file creation"""
cal = Calendar()
cal.add('prodid', '-//Thunderbird Appointment//tba.dk//')
cal.add('version', '2.0')
cal.add('method', 'REQUEST')
cal.add('method', 'CANCEL' if event_status == RemoteEventState.CANCELLED.value else 'REQUEST')
org = vCalAddress('MAILTO:' + organizer.preferred_email)
org.params['cn'] = vText(organizer.preferred_email)
org.params['role'] = vText('CHAIR')
Expand All @@ -521,7 +526,7 @@ def create_vevent(
slot.start.replace(tzinfo=timezone.utc) + timedelta(minutes=slot.duration),
)
event.add('dtstamp', datetime.now(UTC))
event.add('status', 'TENTATIVE' if on_hold else 'CONFIRMED')
event.add('status', event_status)
event['description'] = appointment.details
event['organizer'] = org

Expand All @@ -544,7 +549,7 @@ def send_invitation_vevent(
attendee: schemas.AttendeeBase,
):
"""send a booking confirmation email to attendee with .ics file attached"""
invite = Attachment(
ics_file = Attachment(
mime=('text', 'calendar'),
filename='AppointmentInvite.ics',
data=self.create_vevent(appointment, slot, organizer),
Expand All @@ -560,7 +565,7 @@ def send_invitation_vevent(
date=date,
duration=slot.duration,
to=attendee.email,
attachment=invite
attachment=ics_file
)

def send_hold_vevent(
Expand All @@ -571,11 +576,11 @@ def send_hold_vevent(
organizer: schemas.Subscriber,
attendee: schemas.AttendeeBase,
):
"""send a booking confirmation email to attendee with .ics file attached"""
invite = Attachment(
"""send a hold booking email to attendee with .ics file attached"""
ics_file = Attachment(
mime=('text', 'calendar'),
filename='AppointmentInvite.ics',
data=self.create_vevent(appointment, slot, organizer, True),
data=self.create_vevent(appointment, slot, organizer, RemoteEventState.TENTATIVE.value),
)
if attendee.timezone is None:
attendee.timezone = 'UTC'
Expand All @@ -586,7 +591,33 @@ def send_hold_vevent(
organizer.name,
date=date,
to=attendee.email,
attachment=invite
attachment=ics_file
)

def send_cancel_vevent(
self,
background_tasks: BackgroundTasks,
appointment: models.Appointment,
slot: schemas.Slot,
organizer: schemas.Subscriber,
attendee: schemas.AttendeeBase,
):
"""send a hold booking email to attendee with .ics file attached"""
ics_file = Attachment(
mime=('text', 'calendar'),
filename='AppointmentInvite.ics',
data=self.create_vevent(appointment, slot, organizer, RemoteEventState.CANCELLED.value),
)
if attendee.timezone is None:
attendee.timezone = 'UTC'
date = slot.start.replace(tzinfo=timezone.utc).astimezone(ZoneInfo(attendee.timezone))
# Send mail
background_tasks.add_task(
send_rejection_email,
organizer.name,
date=date,
to=attendee.email,
attachment=ics_file
)


Expand Down
11 changes: 9 additions & 2 deletions backend/src/appointment/controller/mailer.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def __init__(
html: str = '',
plain: str = '',
attachments: list[Attachment] = [],
method: str = 'REQUEST'
):
self.sender = sender
self.to = to
Expand All @@ -64,6 +65,7 @@ def __init__(
self.body_html = html
self.body_plain = plain
self.attachments = attachments
self.method = method

def html(self):
"""provide email body as html per default"""
Expand Down Expand Up @@ -98,11 +100,15 @@ def build(self):
if a.mime_main == 'text' and a.mime_sub == 'calendar':
message.add_attachment(
a.data,
maintype=a.mime_main, subtype=a.mime_sub,
maintype=a.mime_main,
subtype=a.mime_sub,
filename=a.filename
)
# Fix the header of the attachment
message.get_payload()[-1].replace_header('Content-Type', f'{a.mime_main}/{a.mime_sub}; charset="UTF-8"; method=REQUEST')
message.get_payload()[-1].replace_header(
'Content-Type',
f'{a.mime_main}/{a.mime_sub}; charset="UTF-8"; method={self.method}'
)
else:
# Attach it to the html payload
message.get_payload()[1].add_related(
Expand Down Expand Up @@ -298,6 +304,7 @@ def __init__(self, owner_name, date, *args, **kwargs):
self.date = date
default_kwargs = {'subject': l10n('reject-mail-subject')}
super(RejectionMail, self).__init__(*args, **default_kwargs, **kwargs)
self.method = 'CANCEL'

def text(self):
return l10n('reject-mail-plain', {'owner_name': self.owner_name, 'date': self.date})
Expand Down
6 changes: 1 addition & 5 deletions backend/src/appointment/routes/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,12 +506,8 @@ def handle_schedule_availability_decision(
# TODO: Check booking expiration date
# check if request was denied
if confirmed is False:
# human readable date in subscribers timezone
# TODO: Handle locale date representation
date = slot.start.replace(tzinfo=timezone.utc).astimezone(ZoneInfo(subscriber.timezone)).strftime('%c')
date = f'{date}, {slot.duration} minutes'
# send rejection information to bookee
background_tasks.add_task(send_rejection_email, owner_name=subscriber.name, date=date, to=slot.attendee.email)
Tools().send_cancel_vevent(background_tasks, appointment, slot, subscriber, slot.attendee)
repo.slot.delete(db, slot.id)

if slot.appointment_id:
Expand Down
4 changes: 2 additions & 2 deletions backend/src/appointment/tasks/emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ def send_pending_email(owner_name, date, to, attachment):
sentry_sdk.capture_exception(e)


def send_rejection_email(owner_name, date, to):
def send_rejection_email(owner_name, date, to, attachment):
try:
mail = RejectionMail(owner_name=owner_name, date=date, to=to)
mail = RejectionMail(owner_name=owner_name, date=date, to=to, attachments=[attachment])
mail.send()
except Exception as e:
if os.getenv('APP_ENV') == APP_ENV_DEV:
Expand Down

0 comments on commit 81e9274

Please sign in to comment.