diff --git a/ai_docs/masterplan.md b/ai_docs/masterplan.md index 302fdea..718ad73 100644 --- a/ai_docs/masterplan.md +++ b/ai_docs/masterplan.md @@ -29,6 +29,7 @@ This document outlines the high-level development phases for ProjectVote. Each s ### 4.2 New Application Notification ### 4.3 Final Decision Notification ### 4.4 Configurable Rejection Emails +### 4.5 Application Confirmation Notification ## 5. Integration & End-to-End Testing ### 5.1 Backend-Frontend Integration diff --git a/ai_docs/requirements.md b/ai_docs/requirements.md index d2bcf9d..71ebd1d 100644 --- a/ai_docs/requirements.md +++ b/ai_docs/requirements.md @@ -9,6 +9,7 @@ This document outlines the core functional requirements for the web application * A user-friendly form for entering project details (title, description, costs, department). * Collection of applicant's contact information (name, email). * Optional file upload for supporting documents (PDF, XLSX, DOCX, etc.). + * Upon successful submission, the applicant receives a confirmation email containing all the details of their submission. ## 2. Token-Based Board Voting diff --git a/ai_docs/task_4_5.md b/ai_docs/task_4_5.md new file mode 100644 index 0000000..d3f5fa2 --- /dev/null +++ b/ai_docs/task_4_5.md @@ -0,0 +1,34 @@ +# Task 4.5: Application Confirmation Notification + +## Goal +Implement an automated confirmation email to the applicant upon successful submission of a new funding application, ensuring they have a record of their submission. + +## Plan +1. Create a new, dedicated email template for the applicant confirmation. +2. Integrate the email sending logic into the application submission endpoint. +3. Ensure all submitted data is included in the email for the applicant's reference. +4. Verify the functionality through testing and local email inspection. + +## Tasks + +### Phase 1: Email Template Creation +* [x] Create a new HTML email template named `application_confirmation.html` in `src/projectvote/backend/templates/email/`. +* [ ] The template should be designed to clearly display all key application details: + * Applicant's Full Name + * Applicant's Email + * Department + * Project Title + * Project Description + * Estimated Costs +* [x] The template should also acknowledge if a file was attached (e.g., "Your attachment '[filename]' was successfully uploaded."). +* [x] The template should contain a link to the archive of the web app. + +### Phase 2: Backend Integration +* [x] Modify the `submit_application` function in `src/projectvote/backend/main.py`. +* [x] After successfully saving the application and any attachments, call the `send_email` function. +* [x] The recipient for this email should be the `applicant_email` from the submitted data. +* [x] Pass all necessary application data (including details of the attachment, if any) to the `application_confirmation.html` template. + +### Phase 3: Verification +* [x] Add a unit test to verify that the email sending function is called with the correct parameters (recipient, subject, template) upon a successful application submission. +* [x] Use MailHog during local development to visually inspect the email and confirm its content, formatting, and accuracy. diff --git a/src/projectvote/backend/main.py b/src/projectvote/backend/main.py index 268e48f..308c802 100644 --- a/src/projectvote/backend/main.py +++ b/src/projectvote/backend/main.py @@ -154,6 +154,29 @@ class ApplicationOut(BaseModel): # --- Email Sending Functions --- +async def send_confirmation_email(application: Application, settings: Settings) -> None: + """Send a confirmation email to the applicant.""" + await send_email( + recipients=[application.applicant_email], # type: ignore[list-item] + subject=f"Bestätigung Ihres Antrags: {application.project_title}", + template_body={ + "first_name": application.first_name, + "last_name": application.last_name, + "applicant_email": application.applicant_email, + "department": application.department, + "project_title": application.project_title, + "project_description": application.project_description, + "costs": application.costs, + "attachment_filename": application.attachments[0].filename + if application.attachments + else None, + "frontend_url": settings.frontend_url, + }, + template_name="application_confirmation.html", + settings=settings, + ) + + async def send_voting_links( application: Application, db: AsyncSession, @@ -373,6 +396,9 @@ async def submit_application( # Refresh the application with attachments relationship loaded await db.refresh(new_application, attribute_names=["attachments"]) + # Send confirmation email to the applicant + await send_confirmation_email(new_application, settings) + # Generate vote records and send links await send_voting_links(new_application, db, board_members, settings) await db.commit() # Commit all changes (application, attachment, and vote records) diff --git a/src/projectvote/backend/templates/email/application_confirmation.html b/src/projectvote/backend/templates/email/application_confirmation.html new file mode 100644 index 0000000..021818d --- /dev/null +++ b/src/projectvote/backend/templates/email/application_confirmation.html @@ -0,0 +1,27 @@ + + +
+Hallo {{ first_name }} {{ last_name }},
+Vielen Dank für Ihren Förderantrag. Wir haben ihn erfolgreich erhalten.
+Diese E-Mail dient als Bestätigung Ihrer Einreichung. Nachfolgend finden Sie eine Zusammenfassung Ihrer übermittelten Details:
+ +Anhang: Ihre Datei '{{ attachment_filename }}' wurde erfolgreich hochgeladen.
+ {% endif %} + +Wir werden Ihren Antrag prüfen und uns nach Abschluss des Abstimmungsprozesses bei Ihnen melden.
+Sie können den Status aller Anträge im Antragsarchiv einsehen.
+Mit freundlichen Grüßen,
+LvD Förderverein
+ + diff --git a/tests/test_main.py b/tests/test_main.py index 9923ce5..924f1d7 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -120,17 +120,51 @@ async def test_create_application( actual_status = actual_status.value assert actual_status == VoteStatus.PENDING.value - # Verify that email sending was triggered - assert send_email_mock.call_count == len(TEST_BOARD_MEMBERS) - - # Verify that the email body contains the correct data - first_call_args = send_email_mock.call_args_list[0] - template_body = first_call_args.kwargs["template_body"] - assert template_body["first_name"] == application_data["first_name"] - assert template_body["last_name"] == application_data["last_name"] - assert template_body["project_title"] == application_data["project_title"] - assert template_body["costs"] == application_data["costs"] - assert "vote_url" in template_body + # Verify that email sending was triggered for board members and the applicant + assert send_email_mock.call_count == len(TEST_BOARD_MEMBERS) + 1 + + # Separate the calls for applicant and board members + applicant_email_call = None + board_member_email_calls = [] + for call in send_email_mock.call_args_list: + if call.kwargs["recipients"] == [application_data["applicant_email"]]: + applicant_email_call = call + else: + board_member_email_calls.append(call) + + # Verify the confirmation email to the applicant + assert applicant_email_call is not None + assert ( + applicant_email_call.kwargs["subject"] + == f"Bestätigung Ihres Antrags: {application_data['project_title']}" + ) + assert ( + applicant_email_call.kwargs["template_name"] == "application_confirmation.html" + ) + applicant_template_body = applicant_email_call.kwargs["template_body"] + assert applicant_template_body["first_name"] == application_data["first_name"] + assert applicant_template_body["last_name"] == application_data["last_name"] + assert ( + applicant_template_body["applicant_email"] + == application_data["applicant_email"] + ) + assert applicant_template_body["department"] == application_data["department"] + assert applicant_template_body["project_title"] == application_data["project_title"] + assert ( + applicant_template_body["project_description"] + == application_data["project_description"] + ) + assert applicant_template_body["costs"] == application_data["costs"] + + # Verify the notification emails to board members + assert len(board_member_email_calls) == len(TEST_BOARD_MEMBERS) + first_board_call_args = board_member_email_calls[0] + board_template_body = first_board_call_args.kwargs["template_body"] + assert board_template_body["first_name"] == application_data["first_name"] + assert board_template_body["last_name"] == application_data["last_name"] + assert board_template_body["project_title"] == application_data["project_title"] + assert board_template_body["costs"] == application_data["costs"] + assert "vote_url" in board_template_body @pytest.mark.parametrize(