From 603a54ab4e2d9ecff503a037660ec96618182b25 Mon Sep 17 00:00:00 2001 From: ext-hjasplund Date: Tue, 17 Feb 2026 08:35:07 +0200 Subject: [PATCH 1/4] feat(benefit): automatic checkpoint notification Notify the applicant to send payslips to continue the benefit. Refs: HL-1654 --- .../jobs/daily/daily_application_jobs.py | 10 ++- .../management/commands/request_payslip.py | 83 +++++++++++++++++++ 2 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 backend/benefit/applications/management/commands/request_payslip.py diff --git a/backend/benefit/applications/jobs/daily/daily_application_jobs.py b/backend/benefit/applications/jobs/daily/daily_application_jobs.py index 947543d134..3f2e086f4d 100755 --- a/backend/benefit/applications/jobs/daily/daily_application_jobs.py +++ b/backend/benefit/applications/jobs/daily/daily_application_jobs.py @@ -4,10 +4,11 @@ from applications.enums import ApplicationStatus """ -Daily job to delete cancelled applications. - -Run as a cronjob every day to delete applications that have -been in the cancelled state for more than 30 days. +Run as a cronjob every day to +- check if drafts should be deleted +- get decision maker from Ahjo +- get signer +- check if applicants should be notified about ending benefits """ @@ -32,3 +33,4 @@ def execute(self): call_command("get_decision_maker") call_command("get_signer") call_command("check_and_notify_ending_benefits", notify=30) + call_command("request_payslip", notify=150) diff --git a/backend/benefit/applications/management/commands/request_payslip.py b/backend/benefit/applications/management/commands/request_payslip.py new file mode 100644 index 0000000000..ee64dfaa40 --- /dev/null +++ b/backend/benefit/applications/management/commands/request_payslip.py @@ -0,0 +1,83 @@ +from datetime import date, timedelta + +from django.core.management.base import BaseCommand +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +from applications.enums import ApplicationOrigin, ApplicationStatus +from applications.models import Application +from messages.automatic_messages import ( + get_email_template_context, + render_email_template, + send_email_to_applicant, +) + +BENEFIT_CHECKPOINT_IS_UPCOMING_MESSAGE = _( + "Your application's {application_number} checkpoint is upcoming, at" + " {benefit_checkpoint_date}." +) + + +class Command(BaseCommand): + help = ( + "Query applications that are close to the checkpoint date and" + " notification to the applicant" + ) + + def add_arguments(self, parser): + parser.add_argument( + "--notify", + type=int, + default=150, + help=( + "The number of days before which to notify about sending payslip" + " of the applicant" + ), + ) + + def handle(self, *args, **options): + number_of_notified_applications = notify_applications(options["notify"]) + self.stdout.write( + f"Notified users of {number_of_notified_applications} applications about" + " benefit checkpoint" + ) + + +def notify_applications(days_to_notify: int) -> int: + """Query applications that are close to the benefit checkpoint date + and not have any alterations. Send a notification to the applicant. + Returns the number of notified applications.""" # noqa: E501 + + target_date = timezone.now() - timedelta(days=days_to_notify) + applications_to_notify = Application.objects.filter( + application_origin=ApplicationOrigin.APPLICANT, + status=ApplicationStatus.ACCEPTED, + start_date=target_date, + alteration_set__isnull=True, + ) + + for application in applications_to_notify: + _send_notification_mail(application, days_to_notify) + + return applications_to_notify.count() + + +def get_benefit_notice_email_notification_subject(): + return str(_("You need to send the employee's payslip")) + + +def _send_notification_mail(application: Application, days_to_notify: int) -> int: + """Send a notification mail to the applicant about the upcoming checkpoint""" # noqa: E501 + + context = get_email_template_context(application) + notification_date = (application.start_date + timedelta(days=days_to_notify)) + context["benefit_company_name"] = application.company_name + context["benefit_start_date"] = application.start_date.strftime("%d.%m.%Y") + context["benefit_checkpoint_date"] = notification_date.strftime("%d.%m.%Y") + context["benefit_end_date"] = application.end_date.strftime("%d.%m.%Y") + + subject = get_benefit_notice_email_notification_subject() + message = render_email_template(context, "benefit-request-payslip", "txt") + html_message = render_email_template(context, "benefit-request-payslip", "html") + + return send_email_to_applicant(application, subject, message, html_message) From b655801f5504d45de27b20ab39bdf85b8d3ceb65 Mon Sep 17 00:00:00 2001 From: ext-hjasplund Date: Tue, 17 Feb 2026 11:49:41 +0200 Subject: [PATCH 2/4] feat(benefit): automatic checkpoint notification Template and i18n for employee payslip request. Refs: HL-1654 --- .../management/commands/request_payslip.py | 14 +--- .../benefit/common/src/emails/i18n/en.json | 25 ++++++ .../benefit/common/src/emails/i18n/fi.json | 25 ++++++ .../benefit/common/src/emails/i18n/sv.json | 25 ++++++ .../templates/payslip-required.template.mjml | 80 +++++++++++++++++++ 5 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 frontend/benefit/common/src/emails/templates/payslip-required.template.mjml diff --git a/backend/benefit/applications/management/commands/request_payslip.py b/backend/benefit/applications/management/commands/request_payslip.py index ee64dfaa40..430af536c5 100644 --- a/backend/benefit/applications/management/commands/request_payslip.py +++ b/backend/benefit/applications/management/commands/request_payslip.py @@ -63,21 +63,15 @@ def notify_applications(days_to_notify: int) -> int: def get_benefit_notice_email_notification_subject(): - return str(_("You need to send the employee's payslip")) + return str(_("Payment of the second installment of the Helsinki benefit requires measures")) def _send_notification_mail(application: Application, days_to_notify: int) -> int: - """Send a notification mail to the applicant about the upcoming checkpoint""" # noqa: E501 + """Send a notification mail to the applicant about the upcoming checkpoint""" context = get_email_template_context(application) - notification_date = (application.start_date + timedelta(days=days_to_notify)) - context["benefit_company_name"] = application.company_name - context["benefit_start_date"] = application.start_date.strftime("%d.%m.%Y") - context["benefit_checkpoint_date"] = notification_date.strftime("%d.%m.%Y") - context["benefit_end_date"] = application.end_date.strftime("%d.%m.%Y") - subject = get_benefit_notice_email_notification_subject() - message = render_email_template(context, "benefit-request-payslip", "txt") - html_message = render_email_template(context, "benefit-request-payslip", "html") + message = render_email_template(context, "payslip-required", "txt") + html_message = render_email_template(context, "payslip-required", "html") return send_email_to_applicant(application, subject, message, html_message) diff --git a/frontend/benefit/common/src/emails/i18n/en.json b/frontend/benefit/common/src/emails/i18n/en.json index 573e4bae64..0f6f784e1f 100644 --- a/frontend/benefit/common/src/emails/i18n/en.json +++ b/frontend/benefit/common/src/emails/i18n/en.json @@ -47,10 +47,35 @@ "text": "If you have any questions, please send us a message via the Helsinki benefit e-service." } }, + "payslipRequired": { + "headerText": "Payment of the second installment of the Helsinki benefit requires measures", + "heading": "Payment of the second installment of the Helsinki benefit requires measures", + "bodyText": "We have granted you the Helsinki benefit for hiring an employee. The Helsinki benefit is paid in two installments. As a recipient of the subsidy, you are obliged to notify us of the salary paid to the employee. To receive the second installment of the Helsinki benefit, the employer must submit:", + "requiredText": "A payslip or a bank transfer receipt of the paid salary.", + "helper": { + "text": "You can easily submit the information using", + "link": { + "title": "the Helsinki benefit online service", + "url": "https://helsinkilisa.hel.fi/en/login" + } + }, + "delivery": { + "request": "Log in and submit the required attachment", + "alternative": "", + "secureTitle1": "If you have not used the online service, you can submit the attachment using the secure connection via", + "secureTitle2": "(Link takes you to an external service). Mark", + "secureTitle3": "as the recipient.", + "secureText": "https://securemail.hel.fi/", + "secureLink": "https://securemail.hel.fi/", + "emailAddress": "helsinkilisa@hel.fi" + } + }, "application": { "applicationDetails": "Application details", "applicationNumber": "Application number", "createdAt": "Application submission date", + "beneficiary": "Beneficiary", + "benefitPeriod": "Grant period", "status": { "title": "Application status", "unfinished": "Unfinished", diff --git a/frontend/benefit/common/src/emails/i18n/fi.json b/frontend/benefit/common/src/emails/i18n/fi.json index 0b64192314..ddc5c9e19c 100644 --- a/frontend/benefit/common/src/emails/i18n/fi.json +++ b/frontend/benefit/common/src/emails/i18n/fi.json @@ -47,10 +47,35 @@ "text": "Mikäli sinulla herää kysyttävää, lähetä meille viesti Helsinki-lisän asiointipalvelun kautta." } }, + "payslipRequired": { + "headerText": "Myönnetyn Helsinki-lisän toisen erän maksu edellyttää toimenpiteitä", + "heading": "Myönnetyn Helsinki-lisän toisen erän maksu edellyttää toimenpiteitä", + "bodyText": "Myönsimme teille Helsinki-lisää työntekijän palkkaamiseen. Helsinki-lisä maksetaan kahdessa erässä. Tuen saajana olet velvollinen ilmoittamaan meille työsuhteeseen maksetun palkan. Saadakseen toisen Helsinki-lisän maksuerän työnantajan on toimitettava:", + "requiredText": "Palkkakuitti tai pankinsuoritustosite maksetusta palkasta.", + "helper": { + "text": "Ilmoittaminen hoituu helposti", + "link": { + "title": "asiointipalvelussa", + "url": "https://helsinkilisa.hel.fi/fi/login" + } + }, + "delivery": { + "request": "Kirjaudu ja/tai toimita tarvittava liite", + "alternative": "Jos et ole käyttänyt asiointipalvelua, niin voit toimittaa liitteen turvasähköpostilla", + "secureTitle1": "Lähetä liite", + "secureTitle2": "suojatun yhteyden kautta osoitteeseen", + "secureTitle3": "", + "secureText": "suojattu sähköposti (linkki ulkoiseen palveluun)", + "secureLink": "https://suojattusahkoposti.hel.fi/", + "emailAddress": "helsinkilisa@hel.fi" + } + }, "application": { "applicationDetails": "Hakemuksen tiedot", "applicationNumber": "Hakemusnumero", "createdAt": "Hakemuksen lähetyspäivä", + "beneficiary": "Tuen saaja", + "benefitPeriod": "Tukiaika", "status": { "title": "Hakemuksen tila", "unfinished": "Keskeneräinen", diff --git a/frontend/benefit/common/src/emails/i18n/sv.json b/frontend/benefit/common/src/emails/i18n/sv.json index 6789914ab0..5c8624644d 100644 --- a/frontend/benefit/common/src/emails/i18n/sv.json +++ b/frontend/benefit/common/src/emails/i18n/sv.json @@ -47,10 +47,35 @@ "text": "Om du har frågor, skicka oss ett meddelande genom e-tjänst för sysselsättning Helsingforstillägg." } }, + "payslipRequired": { + "headerText": "Utbetalning av den andra raten av det beviljade Helsingforstillägget kräver åtgärder", + "heading": "Utbetalning av den andra raten av det beviljade Helsingforstillägget kräver åtgärder", + "bodyText": "Vi har beviljat dig Helsingforstillägg för anställning av en arbetstagare. Helsingforstillägget betalas ut i två rater. Som mottagare av stödet är du skyldig att meddela oss den lön som betalats ut för anställningsförhållandet. För att få den andra raten av Helsingforstillägget måste arbetsgivaren skicka in:", + "requiredText": "En lönespecifikation eller ett kvitto på banköverföring för den utbetalda lönen.", + "helper": { + "text": "Du kan enkelt sköta ärendet i", + "link": { + "title": "onlinetjänsten", + "url": "https://helsinkilisa.hel.fi/sv/login", + } + }, + "delivery": { + "request": "Logga in och/eller skicka in den nödvändiga bilagan", + "alternative": "Om du inte har använt onlinetjänsten kan du skicka in bilagan via säker e-post", + "secureTitle1": "Skicka bilagan vi", + "secureTitle2": "(Länk leder till en extern tjänst) via en säker anslutning till", + "secureTitle3": "", + "secureText": "säker e-post", + "secureLink": "https://suojattusahkoposti.hel.fi/", + "emailAddress": "helsinkilisa@hel.fi" + } + }, "application": { "applicationDetails": "Uppgifter om ansökningen", "applicationNumber": "Ansökningsnummer", "createdAt": "Datum då ansökningen skickades", + "beneficiary": "Förmånstagare", + "benefitPeriod": "Stödperiod", "status": { "title": "Ansökningsstatus", "unfinished": "Oavslutat", diff --git a/frontend/benefit/common/src/emails/templates/payslip-required.template.mjml b/frontend/benefit/common/src/emails/templates/payslip-required.template.mjml new file mode 100644 index 0000000000..264fb9d6d3 --- /dev/null +++ b/frontend/benefit/common/src/emails/templates/payslip-required.template.mjml @@ -0,0 +1,80 @@ + + + + + ${payslipRequired.headerText} + + + + + ${payslipRequired.headerText} + + + + + + + + ${general.greeting} + +

${payslipRequired.bodyText}

+

${payslipRequired.requiredText}

+
+
+
+ + + + ${payslipRequired.helper.text} ${payslipRequired.helper.link.title} + + + + + + ${general.bestRegards1} + ${general.bestRegards2} + + + + + + ${application.applicationDetails} + + + ${application.applicationNumber} + {{ application.application_number }} + + + ${application.beneficiary} + {{ application.company_name }} + + + ${application.benefitPeriod} + + {{ application.start_date }}-{{ application.end_date }} + + + + + + + + + + ${payslipRequired.delivery.request} + + + ${payslipRequired.delivery.alternative} + + + ${payslipRequired.delivery.secureTitle1} ${payslipRequired.delivery.secureText}  + ${payslipRequired.delivery.secureTitle2} ${payslipRequired.delivery.emailAddress} ${payslipRequired.delivery.secureTitle3} + + + + + + + +
+
From cb817f524d8c7cddeb44cac14f98d0821af08ef6 Mon Sep 17 00:00:00 2001 From: ext-hjasplund Date: Tue, 17 Feb 2026 11:57:15 +0200 Subject: [PATCH 3/4] feat(benefit): fixed .po files Updated .po files Refs: HL-1654 --- .../benefit/applications/jobs/daily/daily_application_jobs.py | 1 + backend/benefit/locale/en/LC_MESSAGES/django.po | 3 +++ backend/benefit/locale/fi/LC_MESSAGES/django.po | 3 +++ backend/benefit/locale/sv/LC_MESSAGES/django.po | 3 +++ 4 files changed, 10 insertions(+) diff --git a/backend/benefit/applications/jobs/daily/daily_application_jobs.py b/backend/benefit/applications/jobs/daily/daily_application_jobs.py index 3f2e086f4d..d1ea0fcc7e 100755 --- a/backend/benefit/applications/jobs/daily/daily_application_jobs.py +++ b/backend/benefit/applications/jobs/daily/daily_application_jobs.py @@ -9,6 +9,7 @@ - get decision maker from Ahjo - get signer - check if applicants should be notified about ending benefits +- request payslip after 5 months """ diff --git a/backend/benefit/locale/en/LC_MESSAGES/django.po b/backend/benefit/locale/en/LC_MESSAGES/django.po index 9ecf1cc1c7..785c643931 100644 --- a/backend/benefit/locale/en/LC_MESSAGES/django.po +++ b/backend/benefit/locale/en/LC_MESSAGES/django.po @@ -1564,3 +1564,6 @@ msgstr "" msgid "terms of service approvals" msgstr "" + +msgid "Payment of the second installment of the Helsinki benefit requires measures" +msgstr "Payment of the second installment of the Helsinki benefit requires measures" diff --git a/backend/benefit/locale/fi/LC_MESSAGES/django.po b/backend/benefit/locale/fi/LC_MESSAGES/django.po index cc7549e7ff..77fa60a320 100644 --- a/backend/benefit/locale/fi/LC_MESSAGES/django.po +++ b/backend/benefit/locale/fi/LC_MESSAGES/django.po @@ -1619,6 +1619,9 @@ msgstr "palveluehtojen hyväksyntä" msgid "terms of service approvals" msgstr "palveluehtojen hyväksynnät" +msgid "Payment of the second installment of the Helsinki benefit requires measures" +msgstr "Myönnetyn Helsinki-lisän toisen erän maksu edellyttää toimenpiteitä" + #~ msgid "e-invoice address" #~ msgstr "Verkkolaskuosoite" diff --git a/backend/benefit/locale/sv/LC_MESSAGES/django.po b/backend/benefit/locale/sv/LC_MESSAGES/django.po index 6bee7f213a..e8ae88f085 100644 --- a/backend/benefit/locale/sv/LC_MESSAGES/django.po +++ b/backend/benefit/locale/sv/LC_MESSAGES/django.po @@ -1644,6 +1644,9 @@ msgstr "godkännande av villkoren för tjänsten" msgid "terms of service approvals" msgstr "godkännanden av villkoren för tjänsten" +msgid "Payment of the second installment of the Helsinki benefit requires measures" +msgstr "Utbetalning av den andra raten av det beviljade Helsingforstillägget kräver åtgärder " + #, fuzzy #~| msgid "Delivery address" #~ msgid "e-invoice address" From 97de0e5e3d1be0c6d1d06f383cfeeed7fc8fd435 Mon Sep 17 00:00:00 2001 From: ext-hjasplund Date: Tue, 24 Feb 2026 08:01:34 +0200 Subject: [PATCH 4/4] feat(benefit): created tests for sending emails Created tests for the request payslip command Refs: HL-1654 --- .../tests/test_applications_api.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/backend/benefit/applications/tests/test_applications_api.py b/backend/benefit/applications/tests/test_applications_api.py index 3a45c84e1b..737358fd74 100755 --- a/backend/benefit/applications/tests/test_applications_api.py +++ b/backend/benefit/applications/tests/test_applications_api.py @@ -13,6 +13,7 @@ import pytest from dateutil.relativedelta import relativedelta from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.management import call_command from django.test import override_settings from freezegun import freeze_time from PIL import Image @@ -31,6 +32,7 @@ AhjoStatus, ApplicationAlterationState, ApplicationBatchStatus, + ApplicationOrigin, ApplicationStatus, ApplicationStep, AttachmentType, @@ -48,8 +50,10 @@ ReceivedApplicationFactory, ) from applications.tests.test_alteration_api import _create_application_alteration +from calculator.enums import InstalmentStatus from calculator.models import Calculation from calculator.tests.conftest import fill_empty_calculation_fields +from calculator.tests.factories import InstalmentFactory from common.tests.conftest import get_client_user from common.utils import duration_in_months from companies.tests.factories import CompanyFactory @@ -63,6 +67,7 @@ from shared.service_bus.enums import YtjOrganizationCode from terms.models import TermsOfServiceApproval +from applications.management.commands.request_payslip import notify_applications def get_detail_url(application): return reverse("v1:applicant-application-detail", kwargs={"pk": application.id}) @@ -2533,6 +2538,46 @@ def test_require_additional_information(handler_api_client, application, mailout get_additional_information_email_notification_subject() in mailoutbox[0].subject ) +@mock.patch( + "applications.management.commands.request_payslip.send_email_to_applicant", + return_value=1, +) +@mock.patch( + "applications.management.commands.request_payslip.get_email_template_context", + return_value={}, +) +@mock.patch( + "applications.management.commands.request_payslip.render_email_template", + side_effect=["txt-body", "html-body"], +) +@pytest.mark.freeze_time("2026-02-15") +def test_request_payslip_sends_email_for_matching_application( + render_email_template_mock, + get_email_template_context_mock, + send_email_to_applicant_mock, +): + days_to_notify = 150 + target_date = (date.today() - relativedelta(days=days_to_notify)).date() + app = DecidedApplicationFactory( + application_origin=ApplicationOrigin.APPLICANT, + status=ApplicationStatus.ACCEPTED, + start_date=target_date, + ) + count = notify_applications(days_to_notify) + + assert count == 1 + assert render_email_template_mock.call_count == 2 + assert get_email_template_context_mock.call_count == 2 + send_email_to_applicant_mock.assert_called_once() + + args, _kwargs = send_email_to_applicant_mock.call_args + assert args[0].id == app.id + assert ( + args[1] + == "Payment of the second installment of the Helsinki benefit requires measures" + ) + assert args[2] == "txt-body" + assert args[3] == "html-body" def _create_random_applications(): f = faker.Faker()