Skip to content

Commit ab85e2e

Browse files
committed
Send voucher to the speaker when accepting their invitation
1 parent c39406c commit ab85e2e

File tree

7 files changed

+153
-17
lines changed

7 files changed

+153
-17
lines changed

backend/api/schedule/mutations/update_schedule_invitation.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from api.submissions.permissions import IsSubmissionSpeakerOrStaff
1616

1717
from schedule.tasks import (
18+
create_and_send_voucher_to_speaker,
1819
notify_new_schedule_invitation_answer_slack,
1920
send_schedule_invitation_plain_message,
2021
)
@@ -59,11 +60,9 @@ def update_schedule_invitation(
5960

6061
new_status = input.option.to_schedule_item_status()
6162
new_notes = input.notes
63+
status_changed = schedule_item.status != new_status
6264

63-
if (
64-
schedule_item.status == new_status
65-
and schedule_item.speaker_invitation_notes == new_notes
66-
):
65+
if not status_changed and schedule_item.speaker_invitation_notes == new_notes:
6766
# If nothing changed, do nothing
6867
return ScheduleInvitation.from_django_model(schedule_item)
6968

@@ -72,6 +71,9 @@ def update_schedule_invitation(
7271
schedule_item.speaker_invitation_notes = new_notes
7372
schedule_item.save()
7473

74+
if status_changed and new_status == ScheduleItem.STATUS.confirmed:
75+
create_and_send_voucher_to_speaker.delay(schedule_item.id)
76+
7577
request = info.context.request
7678
invitation_admin_url = request.build_absolute_uri(
7779
schedule_item.get_invitation_admin_url()

backend/api/schedule/tests/test_update_schedule_invitation.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ def test_update_invitation_answer(
2929
mock_plain = mocker.patch(
3030
"api.schedule.mutations.update_schedule_invitation.send_schedule_invitation_plain_message"
3131
)
32+
mock_send_voucher = mocker.patch(
33+
"api.schedule.mutations.update_schedule_invitation.create_and_send_voucher_to_speaker"
34+
)
3235

3336
graphql_client.force_login(user)
3437
submission = SubmissionFactory(
@@ -80,6 +83,9 @@ def test_update_invitation_answer(
8083
message="notes",
8184
)
8285

86+
if expected_status == ScheduleItem.STATUS.confirmed:
87+
mock_send_voucher.delay.assert_called_once_with(schedule_item.id)
88+
8389

8490
def test_saving_the_same_answer_does_not_trigger_event(
8591
graphql_client,
@@ -92,6 +98,9 @@ def test_saving_the_same_answer_does_not_trigger_event(
9298
mock_plain = mocker.patch(
9399
"api.schedule.mutations.update_schedule_invitation.send_schedule_invitation_plain_message"
94100
)
101+
mock_send_voucher = mocker.patch(
102+
"api.schedule.mutations.update_schedule_invitation.create_and_send_voucher_to_speaker"
103+
)
95104

96105
graphql_client.force_login(user)
97106
submission = SubmissionFactory(
@@ -141,6 +150,7 @@ def test_saving_the_same_answer_does_not_trigger_event(
141150

142151
mock_event.delay.assert_not_called()
143152
mock_plain.delay.assert_not_called()
153+
mock_send_voucher.delay.assert_not_called()
144154

145155

146156
def test_changing_notes_triggers_a_new_event(
@@ -350,6 +360,9 @@ def test_staff_can_update_invitation_answer(
350360
mock_plain = mocker.patch(
351361
"api.schedule.mutations.update_schedule_invitation.send_schedule_invitation_plain_message"
352362
)
363+
mock_send_voucher = mocker.patch(
364+
"api.schedule.mutations.update_schedule_invitation.create_and_send_voucher_to_speaker"
365+
)
353366

354367
graphql_client.force_login(admin_user)
355368
submission = SubmissionFactory()
@@ -398,3 +411,4 @@ def test_staff_can_update_invitation_answer(
398411
schedule_item_id=schedule_item.id,
399412
message="notes",
400413
)
414+
mock_send_voucher.delay.assert_called_once_with(schedule_item.id)

backend/conferences/querysets.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ def for_conference_code(self, conference):
1414

1515

1616
class ConferenceVoucherQuerySet(ConferenceQuerySetMixin, models.QuerySet):
17-
pass
17+
def for_user(self, user):
18+
return self.filter(user=user)

backend/conferences/vouchers.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from conferences.models.conference_voucher import ConferenceVoucher
2+
from conferences.models.conference import Conference
3+
from users.models import User
4+
from pretix import create_voucher
5+
6+
7+
def create_conference_voucher(
8+
*,
9+
conference: Conference,
10+
user: User,
11+
voucher_type: ConferenceVoucher.VoucherType,
12+
) -> ConferenceVoucher:
13+
conference_voucher = ConferenceVoucher(
14+
conference=conference,
15+
user=user,
16+
voucher_type=voucher_type,
17+
voucher_code=ConferenceVoucher.generate_code(),
18+
)
19+
20+
price_mode, value = conference_voucher.get_voucher_configuration()
21+
22+
pretix_voucher = create_voucher(
23+
conference=conference_voucher.conference,
24+
code=conference_voucher.voucher_code,
25+
comment=f"Voucher for user_id={conference_voucher.user_id}",
26+
tag=conference_voucher.voucher_type,
27+
quota_id=conference_voucher.conference.pretix_conference_voucher_quota_id,
28+
price_mode=price_mode,
29+
value=value,
30+
)
31+
32+
pretix_voucher_id = pretix_voucher["id"]
33+
conference_voucher.pretix_voucher_id = pretix_voucher_id
34+
conference_voucher.save()
35+
return conference_voucher

backend/schedule/tasks.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
from django.db.models import Q
2+
from conferences.tasks import send_conference_voucher_email
3+
from conferences.vouchers import create_conference_voucher
4+
from conferences.models.conference_voucher import ConferenceVoucher
25
from pycon.celery_utils import OnlyOneAtTimeTask
36
from google_api.exceptions import NoGoogleCloudQuotaLeftError
47
from googleapiclient.errors import HttpError
@@ -373,3 +376,29 @@ def process_schedule_items_videos_to_upload():
373376
)
374377
sent_for_video_upload_state.failed_reason = str(e)
375378
sent_for_video_upload_state.save(update_fields=["status", "failed_reason"])
379+
380+
381+
@app.task
382+
def create_and_send_voucher_to_speaker(schedule_item_id: int):
383+
schedule_item = ScheduleItem.objects.get(id=schedule_item_id)
384+
385+
if not schedule_item.submission_id:
386+
return
387+
388+
conference_voucher = (
389+
ConferenceVoucher.objects.for_conference(schedule_item.conference)
390+
.for_user(schedule_item.submission.speaker)
391+
.first()
392+
)
393+
394+
if conference_voucher:
395+
# Speaker already has a voucher
396+
return
397+
398+
conference_voucher = create_conference_voucher(
399+
conference=schedule_item.conference,
400+
user=schedule_item.submission.speaker,
401+
voucher_type=ConferenceVoucher.VoucherType.SPEAKER,
402+
)
403+
404+
send_conference_voucher_email.delay(conference_voucher_id=conference_voucher.id)

backend/schedule/tests/test_tasks.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from conferences.models.conference_voucher import ConferenceVoucher
12
from notifications.tests.factories import EmailTemplateFactory
23
from google_api.exceptions import NoGoogleCloudQuotaLeftError
34
from googleapiclient.errors import HttpError
@@ -6,13 +7,14 @@
67
from django.core.files.storage import storages
78
from django.core.files.uploadedfile import InMemoryUploadedFile
89
from unittest import mock
9-
from conferences.tests.factories import ConferenceFactory
10+
from conferences.tests.factories import ConferenceFactory, ConferenceVoucherFactory
1011
from i18n.strings import LazyI18nString
1112
from datetime import datetime, timezone
1213
from unittest.mock import ANY, patch
1314
from django.test import override_settings
1415

1516
from schedule.tasks import (
17+
create_and_send_voucher_to_speaker,
1618
notify_new_schedule_invitation_answer_slack,
1719
process_schedule_items_videos_to_upload,
1820
send_schedule_invitation_email,
@@ -736,3 +738,56 @@ def test_upload_schedule_item_video_with_failing_thumbnail_upload_fails(mocker):
736738
upload_schedule_item_video(
737739
sent_for_video_upload_state_id=sent_for_upload.id,
738740
)
741+
742+
743+
def test_create_and_send_voucher_to_speaker(mocker):
744+
mock_create = mocker.patch(
745+
"conferences.vouchers.create_voucher", return_value={"id": 123}
746+
)
747+
mock_send_email = mocker.patch("schedule.tasks.send_conference_voucher_email")
748+
749+
schedule_item = ScheduleItemFactory(type=ScheduleItem.TYPES.talk)
750+
create_and_send_voucher_to_speaker(schedule_item.id)
751+
752+
assert (
753+
ConferenceVoucher.objects.for_conference(schedule_item.conference)
754+
.filter(
755+
user=schedule_item.submission.speaker,
756+
voucher_type=ConferenceVoucher.VoucherType.SPEAKER,
757+
)
758+
.exists()
759+
)
760+
761+
mock_create.assert_called_once()
762+
mock_send_email.delay.assert_called_once()
763+
764+
765+
def test_create_and_send_voucher_to_speaker_does_nothing_if_voucher_exists(mocker):
766+
mock_create = mocker.patch("conferences.vouchers.create_voucher")
767+
mock_send_email = mocker.patch("schedule.tasks.send_conference_voucher_email")
768+
769+
schedule_item = ScheduleItemFactory(type=ScheduleItem.TYPES.talk)
770+
771+
ConferenceVoucherFactory(
772+
conference=schedule_item.conference,
773+
user=schedule_item.submission.speaker,
774+
)
775+
776+
create_and_send_voucher_to_speaker(schedule_item.id)
777+
778+
mock_create.assert_not_called()
779+
mock_send_email.delay.assert_not_called()
780+
781+
782+
def test_create_and_send_voucher_to_speaker_does_nothing_if_schedule_item_does_not_have_submission(
783+
mocker,
784+
):
785+
mock_create = mocker.patch("conferences.vouchers.create_voucher")
786+
mock_send_email = mocker.patch("schedule.tasks.send_conference_voucher_email")
787+
788+
schedule_item = ScheduleItemFactory(type=ScheduleItem.TYPES.talk, submission=None)
789+
790+
create_and_send_voucher_to_speaker(schedule_item.id)
791+
792+
mock_create.assert_not_called()
793+
mock_send_email.delay.assert_not_called()

backend/uv.lock

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)