Skip to content

Commit

Permalink
feat(ks-backend): add create_without_ssn endpoint & tests for it
Browse files Browse the repository at this point in the history
This new endpoint can be used to create youth applications without
a permanent Finnish personal identity code

refs YJDH-697
  • Loading branch information
karisal-anders committed May 6, 2024
1 parent b7b4f79 commit 097b099
Show file tree
Hide file tree
Showing 12 changed files with 1,170 additions and 82 deletions.
92 changes: 90 additions & 2 deletions backend/kesaseteli/applications/api/v1/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import json
from datetime import datetime
from datetime import date, datetime
from typing import Optional, Union

import filetype
Expand All @@ -12,7 +12,11 @@
from rest_framework import serializers

from applications.api.v1.validators import validate_additional_info_user_reasons
from applications.enums import AttachmentType, EmployerApplicationStatus
from applications.enums import (
AttachmentType,
EmployerApplicationStatus,
get_supported_languages,
)
from applications.models import (
Attachment,
EmployerApplication,
Expand Down Expand Up @@ -488,6 +492,7 @@ class Meta:
]
read_only_fields = [
"id",
"creator",
"created_at",
"modified_at",
"receipt_confirmed_at",
Expand All @@ -497,6 +502,8 @@ class Meta:
"additional_info_user_reasons",
"additional_info_description",
"additional_info_provided_at",
"non_vtj_birthdate",
"non_vtj_home_municipality",
] + vtj_data_fields
fields = read_only_fields + [
"first_name",
Expand All @@ -521,6 +528,11 @@ class Meta:
encrypted_handler_vtj_json = serializers.SerializerMethodField(
"get_encrypted_handler_vtj_json"
)
creator = serializers.PrimaryKeyRelatedField(
required=False,
allow_null=True,
queryset=HandlerPermission.get_handler_users_queryset(),
)
handler = serializers.PrimaryKeyRelatedField(
required=False,
allow_null=True,
Expand Down Expand Up @@ -560,6 +572,82 @@ class Meta:
]


class NonVtjYouthApplicationSerializer(serializers.ModelSerializer):
"""
Serializer for creating youth applications without VTJ data.
NOTE:
Use ONLY when applicant has no permanent Finnish personal identity code.
"""

class Meta:
model = YouthApplication
fields = [
"first_name",
"last_name",
"non_vtj_birthdate",
"non_vtj_home_municipality",
"school",
"is_unlisted_school",
"email",
"phone_number",
"postcode",
"language",
"receipt_confirmed_at",
"status",
"creator",
"handler",
"additional_info_provided_at",
"additional_info_user_reasons",
"additional_info_description",
]

def validate_additional_info_description(self, value):
if value is None or str(value).strip() == "":
raise serializers.ValidationError(
{"additional_info_description": _("Must be set")}
)
return value

def validate_language(self, value):
if value not in get_supported_languages():
raise serializers.ValidationError({"language": _("Invalid language")})
return value

def validate_non_vtj_birthdate(self, value):
if not isinstance(value, date):
try:
date.fromisoformat(value)
except (TypeError, ValueError):
raise serializers.ValidationError(
{"non_vtj_birthdate": _("Invalid date")}
)
return value

def validate(self, data):
data = super().validate(data)
self.validate_additional_info_description(
data.get("additional_info_description", None)
)
self.validate_language(data.get("language", None))
self.validate_non_vtj_birthdate(data.get("non_vtj_birthdate", None))
return data

def to_internal_value(self, data):
# Convert optional fields' input values from None to ""
if data.get("additional_info_description", None) is None:
data["additional_info_description"] = ""
if data.get("non_vtj_home_municipality", None) is None:
data["non_vtj_home_municipality"] = ""
return super().to_internal_value(data)

creator = serializers.PrimaryKeyRelatedField(
required=not HandlerPermission.allow_empty_handler(),
allow_null=HandlerPermission.allow_empty_handler(),
queryset=HandlerPermission.get_handler_users_queryset(),
)


class YouthApplicationAdditionalInfoSerializer(serializers.ModelSerializer):
def validate_additional_info_user_reasons(self, value):
validate_additional_info_user_reasons(value, allow_empty_list=False)
Expand Down
73 changes: 68 additions & 5 deletions backend/kesaseteli/applications/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.db.utils import ProgrammingError
from django.http import FileResponse, HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import redirect
from django.utils import translation
from django.utils import timezone, translation
from django.utils.decorators import method_decorator
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _
Expand All @@ -34,13 +34,15 @@
AttachmentSerializer,
EmployerApplicationSerializer,
EmployerSummerVoucherSerializer,
NonVtjYouthApplicationSerializer,
SchoolSerializer,
YouthApplicationAdditionalInfoSerializer,
YouthApplicationHandlingSerializer,
YouthApplicationSerializer,
YouthApplicationStatusSerializer,
)
from applications.enums import (
AdditionalInfoUserReason,
EmployerApplicationStatus,
YouthApplicationRejectedReason,
YouthApplicationStatus,
Expand Down Expand Up @@ -145,7 +147,10 @@ def retrieve(self, request, *args, **kwargs):
youth_application: YouthApplication = self.get_object().lock_for_update()
# Update unhandled youth applications' encrypted_handler_vtj_json so
# handlers can accept/reject using it
if not youth_application.is_handled:
if (
youth_application.has_social_security_number
and not youth_application.is_handled
):
youth_application.encrypted_handler_vtj_json = (
youth_application.fetch_vtj_json(
end_user=VTJClient.get_end_user(request)
Expand Down Expand Up @@ -293,7 +298,7 @@ def fetch_employee_data(self, request, *args, **kwargs) -> HttpResponse:
"employee_name": youth_application.name,
"employee_ssn": youth_application.social_security_number,
"employee_phone_number": youth_application.phone_number,
"employee_home_city": youth_application.vtj_home_municipality,
"employee_home_city": youth_application.home_municipality,
"employee_postcode": youth_application.postcode,
"employee_school": youth_application.school,
},
Expand All @@ -306,7 +311,10 @@ def fetch_employee_data(self, request, *args, **kwargs) -> HttpResponse:
def accept(self, request, *args, **kwargs) -> HttpResponse:
youth_application: YouthApplication = self.get_object().lock_for_update()

if settings.NEXT_PUBLIC_DISABLE_VTJ:
if (
settings.NEXT_PUBLIC_DISABLE_VTJ
or not youth_application.has_social_security_number
):
encrypted_handler_vtj_json = None
else:
try:
Expand Down Expand Up @@ -351,7 +359,10 @@ def accept(self, request, *args, **kwargs) -> HttpResponse:
def reject(self, request, *args, **kwargs) -> HttpResponse:
youth_application: YouthApplication = self.get_object().lock_for_update()

if settings.NEXT_PUBLIC_DISABLE_VTJ:
if (
settings.NEXT_PUBLIC_DISABLE_VTJ
or not youth_application.has_social_security_number
):
encrypted_handler_vtj_json = None
else:
try:
Expand Down Expand Up @@ -573,6 +584,58 @@ def create(self, request, *args, **kwargs): # noqa: C901
)
raise

@transaction.atomic
@enforce_handler_view_adfs_login
@action(methods=["post"], detail=False, url_path="create-without-ssn")
def create_without_ssn(self, request, *args, **kwargs) -> HttpResponse:
"""
Create a YouthApplication without a social security number.
"""
try:
data = request.data.copy()
data["is_unlisted_school"] = True
data["receipt_confirmed_at"] = timezone.now()
data["additional_info_provided_at"] = timezone.now()
data["additional_info_user_reasons"] = [AdditionalInfoUserReason.OTHER]
data["status"] = YouthApplicationStatus.ADDITIONAL_INFORMATION_PROVIDED
data["creator"] = request.user.id
data["handler"] = None

serializer = NonVtjYouthApplicationSerializer(
data=data, context=self.get_serializer_context()
)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
app: YouthApplication = serializer.instance

was_email_sent = app.send_processing_email_to_handler(request)
if not was_email_sent:
transaction.set_rollback(True)
with translation.override(app.language):
return HttpResponse(
_("Failed to send manual processing email to handler"),
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)

LOGGER.info(
f"Created youth application {app.id} without social security number: "
"Sending application to be processed by a handler"
)

headers = self.get_success_headers(serializer.data)
return Response(
data={"id": app.id},
status=status.HTTP_201_CREATED,
headers=headers,
)
except ValidationError as e:
LOGGER.error(
"Youth application without social security number "
"submission rejected because of validation error. "
f"Validation error codes: {str(e.get_codes())}"
)
raise


class EmployerApplicationViewSet(AuditLoggingModelViewSet):
queryset = EmployerApplication.objects.all()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.23 on 2024-04-16 11:50

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('applications', '0032_alter_employersummervoucher_target_group'),
]

operations = [
migrations.AddField(
model_name='youthapplication',
name='non_vtj_birthdate',
field=models.DateField(blank=True, default=None, help_text='Birthdate of person who has no permanent Finnish personal identity code, and thus no data obtainable through the VTJ integration', null=True, verbose_name='non-vtj birthdate'),
),
migrations.AddField(
model_name='youthapplication',
name='non_vtj_home_municipality',
field=models.CharField(blank=True, default='', help_text='Home municipality of person who has no permanent Finnish personal identity code, and thus no data obtainable through the VTJ integration', max_length=64, verbose_name='non-vtj home municipality'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 3.2.23 on 2024-04-16 13:46

import common.utils
from django.db import migrations
import encrypted_fields.fields


class Migration(migrations.Migration):

dependencies = [
('applications', '0033_add_non_vtj_birthdate_and_home_municipality'),
]

operations = [
migrations.AlterField(
model_name='youthapplication',
name='social_security_number',
field=encrypted_fields.fields.SearchField(blank=True, db_index=True, encrypted_field_name='encrypted_social_security_number', hash_key='ee235e39ebc238035a6264c063dd829d4b6d2270604b57ee1f463e676ec44669', max_length=66, null=True, validators=[common.utils.validate_optional_finnish_social_security_number]),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 3.2.23 on 2024-04-19 11:09

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('applications', '0034_make_social_security_number_optional'),
]

operations = [
migrations.AddField(
model_name='youthapplication',
name='creator',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_youth_applications', to=settings.AUTH_USER_MODEL, verbose_name='creator'),
),
migrations.AlterField(
model_name='youthapplication',
name='handler',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='handled_youth_applications', to=settings.AUTH_USER_MODEL, verbose_name='handler'),
),
]
Loading

0 comments on commit 097b099

Please sign in to comment.