diff --git a/.env.benefit-backend.example b/.env.benefit-backend.example index 2ded8da6a6..8f3ab8077c 100644 --- a/.env.benefit-backend.example +++ b/.env.benefit-backend.example @@ -118,3 +118,4 @@ AHJO_TEST_USER_AD_USERNAME = ENABLE_CLAMAV=1 CLAMAV_URL=http://localhost:8080/api/v1 AHJO_REQUEST_TIMEOUT=60 +ENABLE_AHJO_AUTOMATION=0 diff --git a/backend/benefit/applications/api/v1/ahjo_integration_views.py b/backend/benefit/applications/api/v1/ahjo_integration_views.py index 862f8adcd6..0378f956b4 100644 --- a/backend/benefit/applications/api/v1/ahjo_integration_views.py +++ b/backend/benefit/applications/api/v1/ahjo_integration_views.py @@ -1,4 +1,5 @@ import logging +from datetime import datetime, timezone from django.http import FileResponse from django.shortcuts import get_object_or_404 @@ -58,6 +59,8 @@ def get(self, request, *args, **kwargs): additional_information=f"attachment {attachment.attachment_file} \ of type {attachment.attachment_type} was sent to AHJO!", ) + attachment.downloaded_by_ahjo = datetime.now(timezone.utc) + attachment.save() return self._prepare_file_response(attachment) @staticmethod @@ -151,9 +154,13 @@ def handle_success_callback( ahjo_status = AhjoStatusEnum.DECISION_PROPOSAL_ACCEPTED info = "Decision proposal was sent to Ahjo" elif request_type == AhjoRequestType.UPDATE_APPLICATION: - self._handle_update_records_success(application, callback_data) + self._handle_update_or_add_records_success(application, callback_data) ahjo_status = AhjoStatusEnum.UPDATE_REQUEST_RECEIVED info = f"Updated application records were sent to Ahjo with request id: {callback_data['requestId']}" + elif request_type == AhjoRequestType.ADD_RECORDS: + self._handle_update_or_add_records_success(application, callback_data) + ahjo_status = AhjoStatusEnum.NEW_RECORDS_RECEIVED + info = f"A attachments were sent as records to Ahjo with request id: {callback_data['requestId']}" else: raise AhjoCallbackError( f"Unknown request type {request_type} in the Ahjo callback" @@ -183,7 +190,7 @@ def handle_failure_callback( status=status.HTTP_400_BAD_REQUEST, ) - def _handle_update_records_success( + def _handle_update_or_add_records_success( self, application: Application, callback_data: dict ): cb_records = callback_data.get("records", []) @@ -213,7 +220,9 @@ def _save_version_series_id( if the calculated sha256 hashes match.""" attachment_map = { attachment.ahjo_hash_value: attachment - for attachment in application.attachments.all() + for attachment in application.attachments.filter( + ahjo_hash_value__isnull=False + ) } for cb_record in cb_records: attachment = attachment_map.get(cb_record.get("hashValue")) diff --git a/backend/benefit/applications/api/v1/application_views.py b/backend/benefit/applications/api/v1/application_views.py index d0de9556ad..63660999f6 100755 --- a/backend/benefit/applications/api/v1/application_views.py +++ b/backend/benefit/applications/api/v1/application_views.py @@ -257,6 +257,7 @@ def post_attachment(self, request, *args, **kwargs): ) serializer.is_valid(raise_exception=True) serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) def _get_simplified_queryset(self, request, context) -> QuerySet: diff --git a/backend/benefit/applications/enums.py b/backend/benefit/applications/enums.py index 1ebd052a0e..57e4fe42d3 100644 --- a/backend/benefit/applications/enums.py +++ b/backend/benefit/applications/enums.py @@ -175,6 +175,8 @@ class AhjoStatus(models.TextChoices): ) DELETE_REQUEST_SENT = "delete_request_sent", _("Delete request sent") DELETE_REQUEST_RECEIVED = "delete_request_received", _("Delete request received") + NEW_RECORDS_REQUEST_SENT = "new_record_request_sent", _("New record request sent") + NEW_RECORDS_RECEIVED = "new_record_received", _("New record received by Ahjo") class ApplicationActions(models.TextChoices): @@ -192,6 +194,7 @@ class AhjoRequestType(models.TextChoices): OPEN_CASE = "open_case", _("Open case in Ahjo") DELETE_APPLICATION = "delete_application", _("Delete application in Ahjo") UPDATE_APPLICATION = "update_application", _("Update application in Ahjo") + ADD_RECORDS = "add_records", _("Send new records to Ahjo") SEND_DECISION_PROPOSAL = "send_decision", _("Send decision to Ahjo") @@ -222,6 +225,23 @@ class ApplicationReviewStep(models.TextChoices): STEP_3 = "step_3", _("Step 3 - review the decision") +class AhjoRecordType(models.TextChoices): + APPLICATION = "hakemus", _("Application") + ATTACHMENT = "hakemuksen liite", _("Application attachment") + DECISION_PROPOSAL = "viranhaltijan päätös", _("Decision proposal") + SECRET_ATTACHMENT = "viranhaltijan päätöksen liite", _("Secret decision attachment") + + +class AhjoRecordTitle(models.TextChoices): + APPLICATION = "Hakemus", _("Application title") + ATTACHMENT = "Hakemuksen liite", _("Application attachmen title") + DECISION_PROPOSAL = ( + "Avustuksen myöntäminen, Työllisyyspalvelut, työllisyydenhoidon Helsinki-lisä vuonna 2024", + _("Decision proposal title"), + ) + SECRET_ATTACHMENT = "Päätöksen liite", _("Secret decision attachment title") + + # Call gettext on some of the enums so that "makemessages" command can find them when used dynamically in templates _("granted") _("granted_aged") diff --git a/backend/benefit/applications/jobs/quarter_hourly/__init__.py b/backend/benefit/applications/jobs/quarter_hourly/__init__.py new file mode 100755 index 0000000000..e69de29bb2 diff --git a/backend/benefit/applications/jobs/quarter_hourly/quarter_hourly_ahjo_job.py b/backend/benefit/applications/jobs/quarter_hourly/quarter_hourly_ahjo_job.py new file mode 100644 index 0000000000..95702303fc --- /dev/null +++ b/backend/benefit/applications/jobs/quarter_hourly/quarter_hourly_ahjo_job.py @@ -0,0 +1,11 @@ +from django.conf import settings +from django.core.management import call_command +from django_extensions.management.jobs import QuarterHourlyJob + + +class Job(QuarterHourlyJob): + help = "Quarter hourly Ahjo integration jobs are executed here." + + def execute(self): + if settings.ENABLE_AHJO_AUTOMATION: + call_command("send_new_records") diff --git a/backend/benefit/applications/management/commands/send_new_records.py b/backend/benefit/applications/management/commands/send_new_records.py new file mode 100644 index 0000000000..b4c77719e1 --- /dev/null +++ b/backend/benefit/applications/management/commands/send_new_records.py @@ -0,0 +1,39 @@ +import logging +import time + +from django.core.exceptions import ImproperlyConfigured +from django.core.management.base import BaseCommand + +from applications.services.ahjo_authentication import AhjoToken +from applications.services.ahjo_integration import ( + get_token, + send_new_attachment_records_to_ahjo, +) + +LOGGER = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = "Send new records to Ahjo" + + def handle(self, *args, **options): + try: + ahjo_auth_token = get_token() + except ImproperlyConfigured as e: + LOGGER.error(f"Failed to get auth token from Ahjo: {e}") + return + + self.run_requests(ahjo_auth_token) + + def run_requests(self, ahjo_auth_token: AhjoToken): + start_time = time.time() + + self.stdout.write("Sending new records to Ahjo") + + responses = send_new_attachment_records_to_ahjo(ahjo_auth_token) + self.stdout.write(f"Sent records for {len(responses)} applications to Ahjo") + end_time = time.time() + elapsed_time = end_time - start_time + self.stdout.write( + f"Done! Sending new records for {len(responses)} applications {elapsed_time} seconds to run." + ) diff --git a/backend/benefit/applications/migrations/0065_downloaded_by_ahjo_timestamp.py b/backend/benefit/applications/migrations/0065_downloaded_by_ahjo_timestamp.py new file mode 100644 index 0000000000..75a0c2f856 --- /dev/null +++ b/backend/benefit/applications/migrations/0065_downloaded_by_ahjo_timestamp.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.23 on 2024-04-12 10:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0064_decision_proposal_drafts'), + ] + + operations = [ + migrations.AddField( + model_name='attachment', + name='downloaded_by_ahjo', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='historicalattachment', + name='downloaded_by_ahjo', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/backend/benefit/applications/migrations/0066_alter_ahjostatus_status.py b/backend/benefit/applications/migrations/0066_alter_ahjostatus_status.py new file mode 100644 index 0000000000..a26c86e49d --- /dev/null +++ b/backend/benefit/applications/migrations/0066_alter_ahjostatus_status.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2024-04-17 13:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0065_downloaded_by_ahjo_timestamp'), + ] + + operations = [ + migrations.AlterField( + model_name='ahjostatus', + name='status', + field=models.CharField(choices=[('submitted_but_not_sent_to_ahjo', 'Submitted but not sent to AHJO'), ('request_to_open_case_sent', 'Request to open the case sent to AHJO'), ('case_opened', 'Case opened in AHJO'), ('update_request_sent', 'Update request sent'), ('update_request_received', 'Update request received'), ('decision_proposal_sent', 'Decision proposal sent'), ('decision_proposal_accepted', 'Decision proposal accepted'), ('decision_proposal_rejected', 'Decision proposal rejected'), ('delete_request_sent', 'Delete request sent'), ('delete_request_received', 'Delete request received'), ('new_record_request_sent', 'New record request sent'), ('new_record_received', 'New record received by Ahjo')], default='submitted_but_not_sent_to_ahjo', max_length=64, verbose_name='status'), + ), + ] diff --git a/backend/benefit/applications/models.py b/backend/benefit/applications/models.py index 7313a52c37..280769f668 100755 --- a/backend/benefit/applications/models.py +++ b/backend/benefit/applications/models.py @@ -3,7 +3,7 @@ from dateutil.relativedelta import relativedelta from django.conf import settings from django.db import connection, models -from django.db.models import JSONField, OuterRef, Subquery +from django.db.models import JSONField, OuterRef, Prefetch, Subquery from django.db.models.constraints import UniqueConstraint from django.utils.translation import gettext_lazy as _ from encrypted_fields.fields import EncryptedCharField, SearchField @@ -121,6 +121,29 @@ def get_queryset(self): ) return qs + def with_downloaded_attachments(self): + """ + Returns applications with only those attachments that have + null in 'downloaded_by_ahjo' and where the applications have a non-null ahjo_case_id, + which means that a case has been opened for them in AHJO. + """ + + qs = self.get_queryset().filter(ahjo_case_id__isnull=False) + attachments_queryset = Attachment.objects.filter( + downloaded_by_ahjo__isnull=True, + attachment_type__in=[ + AttachmentType.EMPLOYMENT_CONTRACT, + AttachmentType.PAY_SUBSIDY_DECISION, + AttachmentType.COMMISSION_CONTRACT, + AttachmentType.EDUCATION_CONTRACT, + AttachmentType.HELSINKI_BENEFIT_VOUCHER, + AttachmentType.EMPLOYEE_CONSENT, + AttachmentType.OTHER_ATTACHMENT, + ], + ) + attachments_prefetch = Prefetch("attachments", queryset=attachments_queryset) + return qs.prefetch_related(attachments_prefetch) + class Application(UUIDModel, TimeStampedModel, DurationMixin): """ @@ -902,6 +925,8 @@ class Attachment(UUIDModel, TimeStampedModel): ahjo_hash_value = models.CharField(max_length=64, null=True, blank=True) + downloaded_by_ahjo = models.DateTimeField(null=True, blank=True) + history = HistoricalRecords( table_name="bf_applications_attachment_history", cascade_delete_history=True ) diff --git a/backend/benefit/applications/services/ahjo_integration.py b/backend/benefit/applications/services/ahjo_integration.py index 615c5d1754..d55eabbdf2 100644 --- a/backend/benefit/applications/services/ahjo_integration.py +++ b/backend/benefit/applications/services/ahjo_integration.py @@ -26,6 +26,7 @@ from applications.models import AhjoDecisionText, AhjoStatus, Application, Attachment from applications.services.ahjo_authentication import AhjoConnector, AhjoToken from applications.services.ahjo_payload import ( + prepare_attachment_records_payload, prepare_decision_proposal_payload, prepare_open_case_payload, prepare_update_application_payload, @@ -386,7 +387,7 @@ def export_application_batch(batch) -> bytes: def generate_application_attachment( application: Application, type: AttachmentType ) -> Attachment: - """Generate a xml decision of the given application and return it as an Attachment.""" + """Generate and save an Attachment of the requested type for the given application""" if type == AttachmentType.PDF_SUMMARY: attachment_data = generate_application_summary_file(application) attachment_filename = ( @@ -413,12 +414,20 @@ def generate_application_attachment( raise ValueError(f"Invalid attachment type {type}") attachment_file = ContentFile(attachment_data, attachment_filename) - attachment = Attachment.objects.create( - application=application, - attachment_file=attachment_file, - content_type=content_type, - attachment_type=type, - ) + if type == AttachmentType.PDF_SUMMARY: + # As there should only exist one pdf summary, update or create the attachment if it is one + attachment, _ = Attachment.objects.update_or_create( + application=application, + attachment_type=type, + defaults={"attachment_file": attachment_file, "content_type": content_type}, + ) + else: + attachment = Attachment.objects.create( + application=application, + attachment_file=attachment_file, + content_type=content_type, + attachment_type=type, + ) return attachment @@ -448,7 +457,7 @@ def prepare_headers( def get_application_for_ahjo(id: uuid.UUID) -> Optional[Application]: - """Get the first accepted application with attachment, calculation and company.""" + """Get the application with calculation, company and employee.""" application = Application.objects.select_related( "calculation", "company", "employee" ).get(pk=id) @@ -511,25 +520,28 @@ def send_request_to_ahjo( timeout = settings.AHJO_REQUEST_TIMEOUT headers["Content-Type"] = "application/json" + data = json.dumps(data, default=str) + url_base = f"{settings.AHJO_REST_API_URL}/cases" if request_type == AhjoRequestType.OPEN_CASE: method = "POST" api_url = url_base - data = json.dumps(data) elif request_type == AhjoRequestType.DELETE_APPLICATION: method = "DELETE" api_url = prepare_delete_url(url_base, application) + data = None elif request_type == AhjoRequestType.UPDATE_APPLICATION: method = "PUT" - data = json.dumps(data) api_url = f"{url_base}/{application.ahjo_case_id}/records" - elif request_type == AhjoRequestType.SEND_DECISION_PROPOSAL: + elif request_type in [ + AhjoRequestType.SEND_DECISION_PROPOSAL, + AhjoRequestType.ADD_RECORDS, + ]: method = "POST" api_url = f"{url_base}/{application.ahjo_case_id}/records" - data = json.dumps(data) try: response = requests.request( @@ -606,7 +618,12 @@ def delete_application_in_ahjo(application_id: uuid.UUID): LOGGER.error(f"Improperly configured: {e}") -def update_application_in_ahjo(application: Application, ahjo_auth_token: str): +def update_application_summary_record_in_ahjo( + application: Application, ahjo_auth_token: str +): + """Update the application summary pdf in Ahjo. + Should be done just before the decision proposal is sent. + """ try: headers = prepare_headers( ahjo_auth_token, application, AhjoRequestType.UPDATE_APPLICATION @@ -615,7 +632,7 @@ def update_application_in_ahjo(application: Application, ahjo_auth_token: str): pdf_summary = generate_application_attachment( application, AttachmentType.PDF_SUMMARY ) - data = prepare_update_application_payload(application, pdf_summary) + data = prepare_update_application_payload(pdf_summary, application) result = send_request_to_ahjo( AhjoRequestType.UPDATE_APPLICATION, @@ -633,6 +650,38 @@ def update_application_in_ahjo(application: Application, ahjo_auth_token: str): LOGGER.error(f"Improperly configured: {e}") +def send_new_attachment_records_to_ahjo( + token: AhjoToken, +) -> List[Tuple[Application, str]]: + """Send any new attachments, that have been added after opening a case, to Ahjo.""" + try: + applications = Application.objects.with_downloaded_attachments() + # TODO add a check for application status, + # so that only applications in the correct status have their attachments sent + responses = [] + for application in applications: + attachments = application.attachments.all() + + headers = prepare_headers( + token.access_token, application, AhjoRequestType.ADD_RECORDS + ) + + data = prepare_attachment_records_payload(attachments, application) + + application, response_text = send_request_to_ahjo( + AhjoRequestType.ADD_RECORDS, headers, application, data + ) + responses.append((application, response_text)) + create_status_for_application( + application, AhjoStatusEnum.NEW_RECORDS_REQUEST_SENT + ) + return responses + except ObjectDoesNotExist as e: + LOGGER.error(f"Object not found: {e}") + except ImproperlyConfigured as e: + LOGGER.error(f"Improperly configured: {e}") + + def send_decision_proposal_to_ahjo(application_id: uuid.UUID): """Open a case in Ahjo.""" try: diff --git a/backend/benefit/applications/services/ahjo_payload.py b/backend/benefit/applications/services/ahjo_payload.py index 1505235363..15db7feaa8 100644 --- a/backend/benefit/applications/services/ahjo_payload.py +++ b/backend/benefit/applications/services/ahjo_payload.py @@ -4,7 +4,7 @@ from django.conf import settings from django.urls import reverse -from applications.enums import AttachmentType +from applications.enums import AhjoRecordTitle, AhjoRecordType, AttachmentType from applications.models import Application, Attachment from common.utils import hash_file from users.models import User @@ -12,6 +12,23 @@ MANNER_OF_RECEIPT = "sähköinen asiointi" +def _prepare_record_title( + application: Application, + record_type: AhjoRecordType, + current: int = 0, + total: int = 0, +) -> str: + """Prepare the title for the application record in Ahjo in the format: + Hakemus 11.4.2024, 128123 + or for an attachment: + Hakemus 11.4.2024, liite 1/3, 128123 + """ + formatted_date = application.created_at.strftime("%d.%m.%Y") + if record_type == AhjoRecordType.APPLICATION: + return f"{AhjoRecordTitle.APPLICATION} {formatted_date}, {application.application_number}" + return f"{AhjoRecordTitle.APPLICATION} {formatted_date}, liite {current}/{total}, {application.application_number}" + + def _prepare_case_title(application: Application) -> str: full_title = f"Avustukset työnantajille, työllisyyspalvelut, \ Helsinki-lisä, {application.company_name}, \ @@ -83,7 +100,7 @@ def _prepare_record_document_dict(attachment: Attachment) -> dict: def _prepare_record( record_title: str, - record_type: str, + record_type: AhjoRecordType, acquired: datetime, documents: List[dict], handler: User, @@ -104,7 +121,7 @@ def _prepare_record( if ahjo_version_series_id is not None: record_dict["VersionSeriesId"] = ahjo_version_series_id - elif ahjo_version_series_id is None and record_title == "Hakemus": + elif ahjo_version_series_id is None and record_type == AhjoRecordType.APPLICATION: record_dict["MannerOfReceipt"] = MANNER_OF_RECEIPT record_dict["Documents"] = documents @@ -132,8 +149,8 @@ def _prepare_case_records( ) main_document_record = _prepare_record( - "Hakemus", - "hakemus", + _prepare_record_title(application, AhjoRecordType.APPLICATION), + AhjoRecordType.APPLICATION, application.created_at.isoformat("T", "seconds"), [_prepare_record_document_dict(pdf_summary)], handler, @@ -142,26 +159,33 @@ def _prepare_case_records( case_records.append(main_document_record) - for attachment in application.attachments.exclude( + open_case_attachments = application.attachments.exclude( attachment_type__in=[ AttachmentType.PDF_SUMMARY, + AttachmentType.FULL_APPLICATION, AttachmentType.DECISION_TEXT_XML, AttachmentType.DECISION_TEXT_SECRET_XML, ] - ): + ) + total_attachments = open_case_attachments.count() + position = 1 + for attachment in open_case_attachments: attachment_version_series_id = ( pdf_summary.ahjo_version_series_id if is_update else None ) document_record = _prepare_record( - "Hakemuksen liite", - "hakemuksen liite", - attachment.created_at.isoformat(), + _prepare_record_title( + application, AhjoRecordType.ATTACHMENT, position, total_attachments + ), + AhjoRecordType.ATTACHMENT, + attachment.created_at.isoformat("T", "seconds"), [_prepare_record_document_dict(attachment)], handler, ahjo_version_series_id=attachment_version_series_id, ) case_records.append(document_record) + position += 1 return case_records @@ -176,12 +200,48 @@ def prepare_open_case_payload( return payload +def prepare_attachment_records_payload( + attachments: List[Attachment], + application: Application, +) -> dict[str, list[dict]]: + """Prepare a payload for the new attachments of an application.""" + + attachment_list = [] + position = 1 + total_attachments = len(attachments) + for attachment in attachments: + attachment_list.append( + _prepare_record( + _prepare_record_title( + application, AhjoRecordType.ATTACHMENT, position, total_attachments + ), + AhjoRecordType.ATTACHMENT, + attachment.created_at.isoformat("T", "seconds"), + [_prepare_record_document_dict(attachment)], + application.calculation.handler, + ) + ) + position += 1 + + return {"records": attachment_list} + + def prepare_update_application_payload( - application: Application, pdf_summary: Attachment + pdf_summary: Attachment, application: Application ) -> dict: """Prepare the payload that is sent to Ahjo when an application is updated, \ in this case it only contains a Records dict""" - return {"records": _prepare_case_records(application, pdf_summary, is_update=True)} + return { + "records": [ + _prepare_record( + _prepare_record_title(application, AhjoRecordType.APPLICATION), + AhjoRecordType.APPLICATION, + pdf_summary.created_at.isoformat("T", "seconds"), + [_prepare_record_document_dict(pdf_summary)], + application.calculation.handler, + ) + ] + } def prepare_decision_proposal_payload( @@ -203,8 +263,8 @@ def prepare_decision_proposal_payload( proposal_dict = { "records": [ { - "Title": "Avustuksen myöntäminen, Työllisyyspalvelut, työllisyydenhoidon Helsinki-lisä vuonna 2024", - "Type": "viranhaltijan päätös", + "Title": AhjoRecordTitle.DECISION_PROPOSAL, + "Type": AhjoRecordType.DECISION_PROPOSAL, "PublicityClass": "Julkinen", "Language": language, "PersonalData": "Sisältää henkilötietoja", @@ -216,8 +276,8 @@ def prepare_decision_proposal_payload( ], }, { - "Title": "Päätöksen liite", - "Type": "viranhaltijan päätöksen liite", + "Title": AhjoRecordTitle.SECRET_ATTACHMENT, + "Type": AhjoRecordType.SECRET_ATTACHMENT, "PublicityClass": "Salassa pidettävä", "SecurityReasons": ["JulkL (621/1999) 24.1 § 25 k"], "Language": language, diff --git a/backend/benefit/applications/tests/conftest.py b/backend/benefit/applications/tests/conftest.py index 7062ff3567..b0f5fafa54 100755 --- a/backend/benefit/applications/tests/conftest.py +++ b/backend/benefit/applications/tests/conftest.py @@ -7,7 +7,13 @@ from django.conf import settings from django.utils import timezone -from applications.enums import ApplicationStatus, BenefitType, DecisionType +from applications.enums import ( + AhjoRecordTitle, + AhjoRecordType, + ApplicationStatus, + BenefitType, + DecisionType, +) from applications.models import Application from applications.services.ahjo_decision_service import ( replace_decision_template_placeholders, @@ -323,13 +329,21 @@ def ahjo_record(decided_application, ahjo_payload_agents): @pytest.fixture() def ahjo_payload_record_for_application(ahjo_record): - record = {**ahjo_record, "Title": "Hakemus", "Type": "hakemus"} + record = { + **ahjo_record, + "Title": AhjoRecordTitle.APPLICATION, + "Type": AhjoRecordType.APPLICATION, + } return record @pytest.fixture() def ahjo_payload_record_for_attachment(ahjo_record): - record = {**ahjo_record, "Title": "Liite", "Type": "liite"} + record = { + **ahjo_record, + "Title": AhjoRecordTitle.ATTACHMENT, + "Type": AhjoRecordType.ATTACHMENT, + } record.pop("MannerOfReceipt", None) return record @@ -350,8 +364,8 @@ def ahjo_payload_record_for_attachment_update( record = { **record, - "Title": "Liite", - "Type": "hakemuksen liite", + "Title": AhjoRecordTitle.ATTACHMENT, + "Type": AhjoRecordType.ATTACHMENT, "VersionSeriesId": dummy_version_series_id, "Documents": [], "Agents": ahjo_payload_agents, diff --git a/backend/benefit/applications/tests/test_ahjo_integration.py b/backend/benefit/applications/tests/test_ahjo_integration.py index 466881f7d2..a5f63ca6e3 100644 --- a/backend/benefit/applications/tests/test_ahjo_integration.py +++ b/backend/benefit/applications/tests/test_ahjo_integration.py @@ -575,7 +575,7 @@ def test_get_application_for_ahjo_no_ad_username(decided_application): @pytest.mark.django_db -def test_generate_pdf_summary_as_attachment(decided_application): +def test_create_or_update_pdf_summary_as_attachment_(decided_application): attachment = generate_application_attachment( decided_application, AttachmentType.PDF_SUMMARY ) @@ -593,6 +593,15 @@ def test_generate_pdf_summary_as_attachment(decided_application): if os.path.exists(attachment.attachment_file.path): os.remove(attachment.attachment_file.path) + attachment = generate_application_attachment( + decided_application, AttachmentType.PDF_SUMMARY + ) + + summaries = Attachment.objects.filter( + application=decided_application, attachment_type=AttachmentType.PDF_SUMMARY + ) + assert summaries.count() == 1 + @pytest.mark.django_db def test_generate_ahjo_public_decision_text_xml(decided_application): @@ -679,3 +688,36 @@ def test_prepare_delete_url(settings, decided_application): url = prepare_delete_url(url_base, decided_application) wanted_url = f"{url_base}/{case_id}?draftsmanid={handler.ad_username}&reason={reason}&apireqlang={lang}" assert url == wanted_url + + +@pytest.mark.django_db +def test_with_downloaded_attachments(decided_application): + applications = Application.objects.with_downloaded_attachments() + assert applications.count() == 0 + + decided_application.ahjo_case_id = "HEL 1999-123" + decided_application.save() + + applications = Application.objects.with_downloaded_attachments() + assert applications.count() == 1 + + attachments = applications[0].attachments.all() + + assert attachments.count() == 7 + for a in attachments: + assert a.downloaded_by_ahjo is None + assert a.attachment_type not in [ + AttachmentType.PDF_SUMMARY, + AttachmentType.FULL_APPLICATION, + AttachmentType.DECISION_TEXT_XML, + AttachmentType.DECISION_TEXT_SECRET_XML, + ] + + attachments[0].downloaded_by_ahjo = timezone.now() + attachments[0].save() + + applications = Application.objects.with_downloaded_attachments() + assert applications.count() == 1 + + attachments = applications[0].attachments.all() + assert attachments.count() == 6 diff --git a/backend/benefit/applications/tests/test_ahjo_payload.py b/backend/benefit/applications/tests/test_ahjo_payload.py index e886bed65f..b756ade883 100644 --- a/backend/benefit/applications/tests/test_ahjo_payload.py +++ b/backend/benefit/applications/tests/test_ahjo_payload.py @@ -1,14 +1,16 @@ from django.core.files.base import ContentFile from django.urls import reverse -from applications.enums import AttachmentType +from applications.enums import AhjoRecordTitle, AhjoRecordType, AttachmentType from applications.models import Attachment from applications.services.ahjo_payload import ( _prepare_case_records, _prepare_case_title, _prepare_record, _prepare_record_document_dict, + _prepare_record_title, _prepare_top_level_dict, + prepare_update_application_payload, ) from common.utils import hash_file @@ -22,6 +24,22 @@ def test_prepare_case_title(decided_application): assert wanted_title == got +def test_prepare_record_title(decided_application): + application = decided_application + formatted_date = application.created_at.strftime("%d.%m.%Y") + wanted_title = f"{AhjoRecordTitle.APPLICATION} {formatted_date}, {application.application_number}" + got = _prepare_record_title(application, AhjoRecordType.APPLICATION) + assert wanted_title == got + + +def test_prepare_record_title_for_attachment(decided_application): + application = decided_application + formatted_date = application.created_at.strftime("%d.%m.%Y") + wanted_title = f"{AhjoRecordTitle.APPLICATION} {formatted_date}, liite 1/3, {application.application_number}" + got = _prepare_record_title(application, AhjoRecordType.ATTACHMENT, 1, 3) + assert wanted_title == got + + def test_prepare_application_record( decided_application, ahjo_payload_record_for_application ): @@ -112,9 +130,9 @@ def test_prepare_case_records(decided_application, settings): handler_name = f"{handler.last_name}, {handler.first_name}" want = [ { - "Title": "Hakemus", - "Type": "hakemus", - "Acquired": application.created_at.isoformat(), + "Title": _prepare_record_title(application, AhjoRecordType.APPLICATION), + "Type": AhjoRecordType.APPLICATION, + "Acquired": application.created_at.isoformat("T", "seconds"), "PublicityClass": "Salassa pidettävä", "SecurityReasons": ["JulkL (621/1999) 24.1 § 25 k"], "Language": "fi", @@ -130,19 +148,30 @@ def test_prepare_case_records(decided_application, settings): ], } ] + open_case_attachments = application.attachments.exclude( + attachment_type__in=[ + AttachmentType.PDF_SUMMARY, + AttachmentType.FULL_APPLICATION, + AttachmentType.DECISION_TEXT_XML, + AttachmentType.DECISION_TEXT_SECRET_XML, + ] + ) + total_attachments = open_case_attachments.count() + pos = 1 - for attachment in application.attachments.exclude( - attachment_type=AttachmentType.PDF_SUMMARY - ): + for attachment in open_case_attachments: document_record = _prepare_record( - "Hakemuksen liite", - "hakemuksen liite", - attachment.created_at.isoformat(), + _prepare_record_title( + application, AhjoRecordType.ATTACHMENT, pos, total_attachments + ), + AhjoRecordType.ATTACHMENT, + attachment.created_at.isoformat("T", "seconds"), [_prepare_record_document_dict(attachment)], handler, ) want.append(document_record) + pos += 1 got = _prepare_case_records(application, fake_summary) @@ -155,3 +184,49 @@ def test_prepare_top_level_dict(decided_application, ahjo_open_case_top_level_di got = _prepare_top_level_dict(application, [], "message title") assert ahjo_open_case_top_level_dict == got + + +def test_prepare_update_application_payload(decided_application): + application = decided_application + handler = application.calculation.handler + handler_name = f"{handler.last_name}, {handler.first_name}" + handler_id = handler.ad_username + + fake_file = ContentFile( + b"fake file content", + f"application_summary_{application.application_number}.pdf", + ) + + fake_summary = Attachment.objects.create( + application=application, + attachment_file=fake_file, + content_type="application/pdf", + attachment_type=AttachmentType.PDF_SUMMARY, + ) + + want = { + "records": [ + { + "Title": _prepare_record_title(application, AhjoRecordType.APPLICATION), + "Type": AhjoRecordType.APPLICATION, + "Acquired": application.created_at.isoformat(), + "PublicityClass": "Salassa pidettävä", + "SecurityReasons": ["JulkL (621/1999) 24.1 § 25 k"], + "Language": "fi", + "PersonalData": "Sisältää erityisiä henkilötietoja", + "MannerOfReceipt": "sähköinen asiointi", + "Documents": [_prepare_record_document_dict(fake_summary)], + "Agents": [ + { + "Role": "mainCreator", + "Name": handler_name, + "ID": handler_id, + } + ], + } + ] + } + + got = prepare_update_application_payload(fake_summary, decided_application) + + assert want == got diff --git a/backend/benefit/helsinkibenefit/settings.py b/backend/benefit/helsinkibenefit/settings.py index 9ea35c017d..c6294b836e 100644 --- a/backend/benefit/helsinkibenefit/settings.py +++ b/backend/benefit/helsinkibenefit/settings.py @@ -172,6 +172,7 @@ AHJO_REQUEST_TIMEOUT=(int, 60), ENABLE_CLAMAV=(bool, False), CLAMAV_URL=(str, ""), + ENABLE_AHJO_AUTOMATION=(bool, False), ) if os.path.exists(env_file): env.read_env(env_file) @@ -545,3 +546,4 @@ ENABLE_CLAMAV = env.bool("ENABLE_CLAMAV") CLAMAV_URL = env.str("CLAMAV_URL") +ENABLE_AHJO_AUTOMATION = env.bool("ENABLE_AHJO_AUTOMATION")