Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
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
- request payslip after 5 months
"""


Expand All @@ -32,3 +34,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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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(_("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"""

context = get_email_template_context(application)
subject = get_benefit_notice_email_notification_subject()
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)
44 changes: 44 additions & 0 deletions backend/benefit/applications/tests/test_applications_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,6 +33,7 @@
AhjoStatus,
ApplicationAlterationState,
ApplicationBatchStatus,
ApplicationOrigin,
ApplicationStatus,
ApplicationStep,
AttachmentType,
Expand All @@ -52,6 +54,7 @@
from calculator.enums import InstalmentStatus
from calculator.models import Calculation, Instalment
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
Expand All @@ -64,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})
Expand Down Expand Up @@ -2553,6 +2557,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()
Expand Down
3 changes: 3 additions & 0 deletions backend/benefit/locale/en/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"
3 changes: 3 additions & 0 deletions backend/benefit/locale/fi/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
3 changes: 3 additions & 0 deletions backend/benefit/locale/sv/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
25 changes: 25 additions & 0 deletions frontend/benefit/common/src/emails/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
25 changes: 25 additions & 0 deletions frontend/benefit/common/src/emails/i18n/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
25 changes: 25 additions & 0 deletions frontend/benefit/common/src/emails/i18n/sv.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<mjml>
<mj-head>
<mj-include path="partials/base.css" type="css" />
<mj-include path="partials/attributes.mjml" />
<mj-title>${payslipRequired.headerText}</mj-title>
</mj-head>
<mj-body>
<mj-section background-color="#D8D8D8" padding-bottom="10px" padding-top="10px">
<mj-column>
<mj-text font-size="15px">${payslipRequired.headerText}</mj-text>
</mj-column>
</mj-section>

<mj-include path="partials/header-service.mjml" />

<mj-section background-color="#fff" padding-bottom="0px">
<mj-column>
<mj-text padding-bottom="20px" padding-top="10px">${general.greeting}</mj-text>
<mj-text>
<p>${payslipRequired.bodyText}</p>
<p>${payslipRequired.requiredText}</p>
</mj-text>
</mj-column>
</mj-section>

<mj-section padding-top="10px">
<mj-column>
<mj-text>${payslipRequired.helper.text} <a href="${payslipRequired.helper.link.url}">${payslipRequired.helper.link.title}</a></mj-text>
</mj-column>
</mj-section>

<mj-section padding-bottom="0px">
<mj-column>
<mj-text line-height="100%">${general.bestRegards1}</mj-text>
<mj-text line-height="100%">${general.bestRegards2}</mj-text>
</mj-column>
</mj-section>

<mj-section padding-bottom="0px">
<mj-column>
<mj-text font-weight="600" font-size="18px">${application.applicationDetails}</mj-text>
<mj-table font-size="15px">
<tr style="border-top: 1px solid #ecedee; border-bottom: 1px solid #ecedee; text-align: left">
<td style="padding: 15px 0">${application.applicationNumber}</td>
<td style="padding: 15px 0; text-align: right">{{ application.application_number }}</td>
</tr>
<tr style="border-top: 1px solid #ecedee; border-bottom: 1px solid #ecedee; text-align: left">
<td style="padding: 15px 0">${application.beneficiary}</td>
<td style="padding: 15px 0; text-align: right">{{ application.company_name }}</td>
</tr>
<tr style="border-bottom: 1px solid #ecedee">
<td style="padding: 15px 0">${application.benefitPeriod}</td>
<td style="padding: 15px 0; text-align: right">
<span style="background: #fef4d4; padding: 7px 14px">{{ application.start_date }}-{{ application.end_date }}</span>
</td>
</tr>
</mj-table>
</mj-column>
</mj-section>

<mj-section>
<mj-column>
<mj-text>
${payslipRequired.delivery.request}
</mj-text>
<mj-text>
${payslipRequired.delivery.alternative}
</mj-text>
<mj-text>
${payslipRequired.delivery.secureTitle1}&nbsp;<a href="${payslipRequired.delivery.secureLink}">${payslipRequired.delivery.secureText}</a>&nbsp;
${payslipRequired.delivery.secureTitle2}&nbsp;${payslipRequired.delivery.emailAddress}&nbsp;${payslipRequired.delivery.secureTitle3}
</mj-text>
</mj-column>
</mj-section>

<mj-include path="partials/automaticReplyDisclaimer.mjml" />

<mj-include path="partials/footer.mjml" />
</mj-body>
</mjml>
Loading