Skip to content

Commit 735f333

Browse files
feat: Automatically send voucher when grant is confirmed
When a user accepts a grant by setting the status to "confirmed", the system now automatically creates a voucher for the free ticket and sends an email to the grantee, similar to how it works for speakers. Changes: - Add `create_and_send_grant_voucher` Celery task in grants/tasks.py - Creates voucher via Pretix integration - Handles case where user already has a speaker voucher (skips) - Upgrades co-speaker voucher to grant voucher if applicable - Sends voucher code email to the grantee - Trigger the task when grant status changes to "confirmed" in the send_grant_reply GraphQL mutation - Add tests for the new functionality Closes #4572 Co-authored-by: Marco Acierno <[email protected]>
1 parent 4883f6c commit 735f333

File tree

4 files changed

+188
-1
lines changed

4 files changed

+188
-1
lines changed

backend/api/grants/mutations.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
create_change_admin_log_entry,
1616
)
1717
from grants.models import Grant as GrantModel
18-
from grants.tasks import get_name, notify_new_grant_reply_slack
18+
from grants.tasks import (
19+
create_and_send_grant_voucher,
20+
get_name,
21+
notify_new_grant_reply_slack,
22+
)
1923
from notifications.models import EmailTemplate, EmailTemplateIdentifier
2024
from participants.models import Participant
2125
from privacy_policy.record import record_privacy_policy_acceptance
@@ -352,4 +356,7 @@ def send_grant_reply(
352356
admin_url = request.build_absolute_uri(grant.get_admin_url())
353357
notify_new_grant_reply_slack.delay(grant_id=grant.id, admin_url=admin_url)
354358

359+
if grant.status == GrantModel.Status.confirmed:
360+
create_and_send_grant_voucher.delay(grant_id=grant.id)
361+
355362
return Grant.from_model(grant)

backend/api/grants/tests/test_send_grant_reply.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,29 @@ def test_call_notify_new_grant_reply(graphql_client, user, mocker):
122122

123123
assert response["data"]["sendGrantReply"]["__typename"] == "Grant"
124124
mock_publisher.delay.assert_called_once_with(grant_id=grant.id, admin_url=ANY)
125+
126+
127+
def test_create_voucher_when_grant_is_confirmed(graphql_client, user, mocker):
128+
graphql_client.force_login(user)
129+
grant = GrantFactory(user_id=user.id, status=Grant.Status.waiting_for_confirmation)
130+
mock_voucher_task = mocker.patch(
131+
"api.grants.mutations.create_and_send_grant_voucher"
132+
)
133+
134+
response = _send_grant_reply(graphql_client, grant, status="confirmed")
135+
136+
assert response["data"]["sendGrantReply"]["__typename"] == "Grant"
137+
mock_voucher_task.delay.assert_called_once_with(grant_id=grant.id)
138+
139+
140+
def test_voucher_not_created_when_grant_is_refused(graphql_client, user, mocker):
141+
graphql_client.force_login(user)
142+
grant = GrantFactory(user_id=user.id, status=Grant.Status.waiting_for_confirmation)
143+
mock_voucher_task = mocker.patch(
144+
"api.grants.mutations.create_and_send_grant_voucher"
145+
)
146+
147+
response = _send_grant_reply(graphql_client, grant, status="refused")
148+
149+
assert response["data"]["sendGrantReply"]["__typename"] == "Grant"
150+
mock_voucher_task.delay.assert_not_called()

backend/grants/tasks.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
from django.conf import settings
66
from django.utils import timezone
77

8+
from conferences.models.conference_voucher import ConferenceVoucher
9+
from conferences.tasks import send_conference_voucher_email
10+
from conferences.vouchers import create_conference_voucher
811
from grants.models import Grant
912
from integrations import slack
1013
from notifications.models import EmailTemplate, EmailTemplateIdentifier
@@ -182,3 +185,47 @@ def _new_send_grant_email(
182185

183186
grant.applicant_reply_sent_at = timezone.now()
184187
grant.save()
188+
189+
190+
@app.task
191+
def create_and_send_grant_voucher(grant_id: int):
192+
"""
193+
Creates a voucher for a confirmed grant and sends an email to the grantee.
194+
This is triggered when a grant is confirmed by the user.
195+
"""
196+
grant = Grant.objects.get(id=grant_id)
197+
conference = grant.conference
198+
user = grant.user
199+
200+
# Check if user already has a voucher for this conference
201+
existing_voucher = (
202+
ConferenceVoucher.objects.for_conference(conference).for_user(user).first()
203+
)
204+
205+
if existing_voucher:
206+
# If user has a co-speaker voucher, upgrade it to a grant voucher
207+
if existing_voucher.voucher_type == ConferenceVoucher.VoucherType.CO_SPEAKER:
208+
logger.info(
209+
"User %s already has a co-speaker voucher for conference %s, "
210+
"upgrading to grant voucher",
211+
user.id,
212+
conference.id,
213+
)
214+
existing_voucher.voucher_type = ConferenceVoucher.VoucherType.GRANT
215+
existing_voucher.save(update_fields=["voucher_type"])
216+
send_conference_voucher_email.delay(conference_voucher_id=existing_voucher.id)
217+
else:
218+
logger.info(
219+
"User %s already has a voucher for conference %s, not creating a new one",
220+
user.id,
221+
conference.id,
222+
)
223+
return
224+
225+
conference_voucher = create_conference_voucher(
226+
conference=conference,
227+
user=user,
228+
voucher_type=ConferenceVoucher.VoucherType.GRANT,
229+
)
230+
231+
send_conference_voucher_email.delay(conference_voucher_id=conference_voucher.id)

backend/grants/tests/test_tasks.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from conferences.tests.factories import ConferenceFactory, DeadlineFactory
77
from grants.tasks import (
8+
create_and_send_grant_voucher,
89
send_grant_reply_approved_email,
910
send_grant_reply_rejected_email,
1011
send_grant_reply_waiting_list_email,
@@ -466,3 +467,109 @@ def test_send_grant_waiting_list_email_missing_deadline():
466467

467468
with pytest.raises(ValueError, match="missing grants_waiting_list_update deadline"):
468469
send_grant_reply_waiting_list_email(grant_id=grant.id)
470+
471+
472+
def test_create_and_send_grant_voucher(mocker, sent_emails):
473+
from conferences.models.conference_voucher import ConferenceVoucher
474+
from notifications.models import EmailTemplateIdentifier
475+
from notifications.tests.factories import EmailTemplateFactory
476+
477+
mock_create_voucher = mocker.patch(
478+
"grants.tasks.create_conference_voucher",
479+
)
480+
mock_conference_voucher = mocker.MagicMock()
481+
mock_conference_voucher.id = 123
482+
mock_create_voucher.return_value = mock_conference_voucher
483+
484+
mock_send_email = mocker.patch(
485+
"grants.tasks.send_conference_voucher_email",
486+
)
487+
488+
user = UserFactory(
489+
full_name="Marco Acierno",
490+
491+
)
492+
grant = GrantFactory(user=user)
493+
494+
EmailTemplateFactory(
495+
conference=grant.conference,
496+
identifier=EmailTemplateIdentifier.voucher_code,
497+
)
498+
499+
create_and_send_grant_voucher(grant_id=grant.id)
500+
501+
mock_create_voucher.assert_called_once_with(
502+
conference=grant.conference,
503+
user=user,
504+
voucher_type=ConferenceVoucher.VoucherType.GRANT,
505+
)
506+
mock_send_email.delay.assert_called_once_with(conference_voucher_id=123)
507+
508+
509+
def test_create_and_send_grant_voucher_user_already_has_voucher(mocker):
510+
from conferences.models.conference_voucher import ConferenceVoucher
511+
from conferences.tests.factories import ConferenceVoucherFactory
512+
513+
mock_create_voucher = mocker.patch(
514+
"grants.tasks.create_conference_voucher",
515+
)
516+
mock_send_email = mocker.patch(
517+
"grants.tasks.send_conference_voucher_email",
518+
)
519+
520+
user = UserFactory(
521+
full_name="Marco Acierno",
522+
523+
)
524+
grant = GrantFactory(user=user)
525+
526+
# Create an existing voucher for this user and conference
527+
ConferenceVoucherFactory(
528+
conference=grant.conference,
529+
user=user,
530+
voucher_type=ConferenceVoucher.VoucherType.SPEAKER,
531+
)
532+
533+
create_and_send_grant_voucher(grant_id=grant.id)
534+
535+
# Should not create a new voucher
536+
mock_create_voucher.assert_not_called()
537+
mock_send_email.delay.assert_not_called()
538+
539+
540+
def test_create_and_send_grant_voucher_upgrades_co_speaker_voucher(mocker):
541+
from conferences.models.conference_voucher import ConferenceVoucher
542+
from conferences.tests.factories import ConferenceVoucherFactory
543+
544+
mock_create_voucher = mocker.patch(
545+
"grants.tasks.create_conference_voucher",
546+
)
547+
mock_send_email = mocker.patch(
548+
"grants.tasks.send_conference_voucher_email",
549+
)
550+
551+
user = UserFactory(
552+
full_name="Marco Acierno",
553+
554+
)
555+
grant = GrantFactory(user=user)
556+
557+
# Create an existing co-speaker voucher for this user and conference
558+
existing_voucher = ConferenceVoucherFactory(
559+
conference=grant.conference,
560+
user=user,
561+
voucher_type=ConferenceVoucher.VoucherType.CO_SPEAKER,
562+
)
563+
564+
create_and_send_grant_voucher(grant_id=grant.id)
565+
566+
# Should not create a new voucher but should upgrade the existing one
567+
mock_create_voucher.assert_not_called()
568+
569+
existing_voucher.refresh_from_db()
570+
assert existing_voucher.voucher_type == ConferenceVoucher.VoucherType.GRANT
571+
572+
# Should send email with the upgraded voucher
573+
mock_send_email.delay.assert_called_once_with(
574+
conference_voucher_id=existing_voucher.id
575+
)

0 commit comments

Comments
 (0)