diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/cove-details/cove-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/cove-details/cove-details.component.html index 343a3368b3..9baa11c58f 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/cove-details/cove-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/cove-details/cove-details.component.html @@ -11,7 +11,7 @@
{{ transferee.displayName }}
{{ transferee.organizationName }} - +
{{ transferee.phoneNumber | mask : '(000) 000-0000' }} @@ -19,18 +19,18 @@
{{ transferee.email }}
- +
How many hectares will the covenant impact?
{{ _applicationSubmission.coveAreaImpacted }} ha - +
What is the purpose of this covenant?
{{ _applicationSubmission.purpose }} - +
Explain how the covenant impacts the use of agricultural land for farm purposes.
@@ -38,7 +38,7 @@ {{ _applicationSubmission.coveFarmImpact }} - +
Proposal Map / Site Plan
@@ -46,7 +46,7 @@ {{ map.fileName }} - +
Do you have a draft copy of the covenant?
@@ -54,7 +54,7 @@ {{ _applicationSubmission.coveHasDraft ? 'Yes' : 'No' }} - +
Draft Covenant
@@ -65,7 +65,6 @@ diff --git a/bin/migrate-oats-data/applications/__init__.py b/bin/migrate-oats-data/applications/__init__.py index c4deade59c..f0ad56389a 100644 --- a/bin/migrate-oats-data/applications/__init__.py +++ b/bin/migrate-oats-data/applications/__init__.py @@ -9,6 +9,6 @@ init_applications, ) -from .set_hide_from_portal_on_application import ( - set_hide_from_portal_on_application, -) +from .set_application_visibility import set_application_visibility + +from .application_decision_date import process_alcs_application_decision_date diff --git a/bin/migrate-oats-data/applications/application_decision_date.py b/bin/migrate-oats-data/applications/application_decision_date.py new file mode 100644 index 0000000000..e55b8f45be --- /dev/null +++ b/bin/migrate-oats-data/applications/application_decision_date.py @@ -0,0 +1,105 @@ +from common import ( + BATCH_UPLOAD_SIZE, + setup_and_get_logger, + add_timezone_and_keep_date_part, +) +from db import inject_conn_pool +from psycopg2.extras import RealDictCursor, execute_batch + +etl_name = "process_alcs_application_decision_date" +logger = setup_and_get_logger(etl_name) + + +@inject_conn_pool +def process_alcs_application_decision_date(conn=None, batch_size=BATCH_UPLOAD_SIZE): + """ + Imports decision_date from oats to alcs.application.decision_date. alcs.application.decision_date is the date of the first decision + In OATS the first decision date is stored in oats.oats_alr_appl_decisions. All subsequent decisions in OATS are the linked to reconsiderations and not application directly. + """ + + logger.info(f"Start {etl_name}") + with conn.cursor(cursor_factory=RealDictCursor) as cursor: + with open( + "applications/sql/application_decision_date/application_decision_date_count.sql", + "r", + encoding="utf-8", + ) as sql_file: + count_query = sql_file.read() + cursor.execute(count_query) + count_total = dict(cursor.fetchone())["count"] + + logger.info(f" Total Application data to update: {count_total}") + + failed_inserts = 0 + successful_updates_count = 0 + last_application_id = 0 + + with open( + "applications/sql/application_decision_date/application_decision_date.sql", + "r", + encoding="utf-8", + ) as sql_file: + application_sql = sql_file.read() + while True: + cursor.execute( + f"{application_sql} WHERE decision_date_for_applications.alr_application_id > {last_application_id} ORDER BY decision_date_for_applications.alr_application_id;" + ) + + rows = cursor.fetchmany(batch_size) + + if not rows: + break + try: + records_to_be_updated_count = len(rows) + + _update_fee_fields_records(conn, batch_size, cursor, rows) + + successful_updates_count = ( + successful_updates_count + records_to_be_updated_count + ) + last_application_id = dict(rows[-1])["alr_application_id"] + + logger.debug( + f"retrieved/updated items count: {records_to_be_updated_count}; total successfully updated applications so far {successful_updates_count}; last updated application_id: {last_application_id}" + ) + except Exception as err: + # this is NOT going to be caused by actual data update failure. This code is only executed when the code error appears or connection to DB is lost + logger.exception(err) + conn.rollback() + failed_inserts = count_total - successful_updates_count + last_application_id = last_application_id + 1 + + logger.info( + f"Finished {etl_name}: total amount of successful updates {successful_updates_count}, total failed updates {failed_inserts}" + ) + + +def _update_fee_fields_records(conn, batch_size, cursor, rows): + query = _get_update_query_from_oats_alr_applications_fields() + parsed_fee_data_list = _prepare_oats_alr_applications_data(rows) + + if len(parsed_fee_data_list) > 0: + execute_batch(cursor, query, parsed_fee_data_list, page_size=batch_size) + + conn.commit() + + +def _get_update_query_from_oats_alr_applications_fields(): + query = """ + UPDATE alcs.application + SET decision_date = COALESCE(%(decision_date)s, decision_date) + WHERE + file_number = %(alr_application_id)s::TEXT; + """ + return query + + +def _prepare_oats_alr_applications_data(row_data_list): + data_list = [] + for row in row_data_list: + data = dict(row) + data["decision_date"] = add_timezone_and_keep_date_part( + row.get("decision_date") + ) + data_list.append(data) + return data_list diff --git a/bin/migrate-oats-data/applications/migrate_application.py b/bin/migrate-oats-data/applications/migrate_application.py index 52fbc7d2a6..4e2422bfae 100644 --- a/bin/migrate-oats-data/applications/migrate_application.py +++ b/bin/migrate-oats-data/applications/migrate_application.py @@ -10,7 +10,7 @@ clean_primary_contacts, insert_application_submission_review, clean_reviews, - update_application_submissions + update_application_submissions, ) from .base_applications import process_applications, clean_applications from .app_prep import process_alcs_application_prep_fields @@ -49,7 +49,8 @@ update_application_reconsiderations, ) -from .set_hide_from_portal_on_application import set_hide_from_portal_on_application +from .set_application_visibility import set_application_visibility +from .application_decision_date import process_alcs_application_decision_date def process_application_etl(batch_size): @@ -58,12 +59,13 @@ def process_application_etl(batch_size): process_alcs_app_submissions(batch_size) update_application_submissions() insert_application_submission_review(batch_size) + process_alcs_application_decision_date(batch_size) process_application_statuses(batch_size) process_application_parcels(batch_size) process_application_owners(batch_size) process_app_staff_journal(batch_size) process_application_decisions(batch_size) - set_hide_from_portal_on_application() + set_application_visibility() process_application_submission_status_emails() @@ -96,7 +98,6 @@ def process_application_decisions(batch_size): link_application_conditions(batch_size) - def clean_application_decisions_etl(): # modifications do not have clean since all of them were created in ALCS and ETL is not introducing new records. clean_application_conditions_to_components() diff --git a/bin/migrate-oats-data/applications/set_application_visibility.py b/bin/migrate-oats-data/applications/set_application_visibility.py new file mode 100644 index 0000000000..663942ccd1 --- /dev/null +++ b/bin/migrate-oats-data/applications/set_application_visibility.py @@ -0,0 +1,158 @@ +from common import setup_and_get_logger +from db import inject_conn_pool +from psycopg2.extras import RealDictCursor + + +etl_name = "set_application_visibility" +logger = setup_and_get_logger(etl_name) + + +@inject_conn_pool +def set_application_visibility(conn=None): + """This function is responsible for setting source column on application and mapping it to public portal visibility visibility.""" + _set_source_on_applications(conn) + _set_hide_from_portal_on_application(conn) + + +def _set_hide_from_portal_on_application(conn): + """ + This function is responsible for setting hide_from_portal colum on application table based on the source. + + Args: + conn (psycopg2.extensions.connection): PostgreSQL database connection. Provided by the decorator. + """ + logger.info(f"Start {etl_name}") + + try: + with conn.cursor(cursor_factory=RealDictCursor) as cursor: + count_visible_to_portal_query = f""" + SELECT count(*) FROM alcs.application a + WHERE a."source" = 'APPLICANT'; + """ + cursor.execute(count_visible_to_portal_query) + visible_to_portal_count = dict(cursor.fetchone())["count"] + + count_hidden_from_portal_query = f""" + SELECT count(*) FROM alcs.application a + WHERE a."source" <> 'APPLICANT'; + """ + cursor.execute(count_hidden_from_portal_query) + hidden_from_portal_count = dict(cursor.fetchone())["count"] + + count_total = visible_to_portal_count + hidden_from_portal_count + + logger.info(f"Total application to set hide_from_portal: {count_total}") + + set_hide_from_portal_false_query = f""" + UPDATE alcs.application + SET hide_from_portal = FALSE + WHERE alcs.application."source" = 'APPLICANT'; + """ + cursor.execute(set_hide_from_portal_false_query) + updated_visible_to_portal_count = cursor.rowcount + + set_hide_from_portal_true_query = f""" + UPDATE alcs.application + SET hide_from_portal = TRUE + WHERE alcs.application."source" <> 'APPLICANT'; + """ + cursor.execute(set_hide_from_portal_true_query) + updated_hidden_from_portal_count = cursor.rowcount + + updated_total_count = ( + updated_visible_to_portal_count + updated_hidden_from_portal_count + ) + + logger.info( + f"Total hide_from_portal flag on applications updated: {updated_total_count}" + ) + conn.commit() + except Exception as err: + logger.exception(err) + cursor.close() + conn.close() + + logger.info(f"Finished {etl_name}") + + +def _set_source_on_applications(conn): + """""" + logger.info(f"Start {etl_name} => set_source_on_applications") + + try: + with conn.cursor(cursor_factory=RealDictCursor) as cursor: + count_created_by_applicant_query = f""" + SELECT count(*) FROM oats.oats_alr_applications oaa + JOIN alcs.application app ON app.file_number = oaa.alr_application_id::TEXT + WHERE oaa.who_created = 'PROXY_OATS_APPLICANT'; + """ + cursor.execute(count_created_by_applicant_query) + created_by_applicant_count = dict(cursor.fetchone())["count"] + + count_created_by_oats_query = f""" + SELECT count(*) FROM oats.oats_alr_applications oaa + JOIN alcs.application app ON app.file_number = oaa.alr_application_id::TEXT + WHERE oaa.who_created <> 'PROXY_OATS_APPLICANT'; + """ + cursor.execute(count_created_by_oats_query) + created_by_oats_count = dict(cursor.fetchone())["count"] + + count_created_in_alcs_query = f""" + SELECT count(*) FROM alcs.application a + WHERE a."source" <> 'APPLICANT' + AND a.audit_created_by <> 'oats_etl'; + """ + cursor.execute(count_created_in_alcs_query) + created_by_alcs_count = dict(cursor.fetchone())["count"] + + count_total = ( + created_by_applicant_count + + created_by_oats_count + + created_by_alcs_count + ) + + logger.info(f"Total application to set hide_from_portal: {count_total}") + + update_created_by_applicant_query = f""" + UPDATE alcs.application + SET "source" = 'APPLICANT' + FROM oats.oats_alr_applications oaa + WHERE alcs.application.file_number = oaa.alr_application_id::TEXT + AND oaa.who_created = 'PROXY_OATS_APPLICANT'; + """ + cursor.execute(update_created_by_applicant_query) + updated_created_by_applicant_count = cursor.rowcount + + update_created_by_oats_query = f""" + UPDATE alcs.application + SET "source" = 'OATS' + FROM oats.oats_alr_applications oaa + WHERE alcs.application.file_number = oaa.alr_application_id::TEXT + AND oaa.who_created <> 'PROXY_OATS_APPLICANT'; + """ + cursor.execute(update_created_by_oats_query) + updated_created_by_oats_count = cursor.rowcount + + update_created_by_alcs_query = f""" + UPDATE alcs.application + SET "source" = 'APPLICANT' + WHERE alcs.application."source" <> 'APPLICANT' + AND alcs.application.audit_created_by <> 'oats_etl'; + """ + cursor.execute(update_created_by_alcs_query) + updated_created_by_alcs_count = cursor.rowcount + + total_processed = ( + updated_created_by_applicant_count + + updated_created_by_oats_count + + updated_created_by_alcs_count + ) + + logger.info(f"Total source on 'application' updated: {total_processed}") + conn.commit() + except Exception as err: + logger.exception(err) + cursor.close() + conn.close() + + logger.info(f"Finished {etl_name}") diff --git a/bin/migrate-oats-data/applications/set_hide_from_portal_on_application.py b/bin/migrate-oats-data/applications/set_hide_from_portal_on_application.py deleted file mode 100644 index 60fdfc6a31..0000000000 --- a/bin/migrate-oats-data/applications/set_hide_from_portal_on_application.py +++ /dev/null @@ -1,48 +0,0 @@ -from common import setup_and_get_logger -from db import inject_conn_pool -from psycopg2.extras import RealDictCursor - - -etl_name = "set_hide_from_portal_on_application" -logger = setup_and_get_logger(etl_name) - - -@inject_conn_pool -def set_hide_from_portal_on_application(conn=None): - """ - This function is responsible for setting hide_from_portal colum on application table where oats.who_create is not PROXY user - - Args: - conn (psycopg2.extensions.connection): PostgreSQL database connection. Provided by the decorator. - """ - logger.info(f"Start {etl_name}") - - try: - with conn.cursor(cursor_factory=RealDictCursor) as cursor: - count_query = f""" - SELECT count(*) FROM oats.oats_alr_applications oaa - JOIN alcs.application a ON a.file_number = oaa.alr_application_id::TEXT - WHERE oaa.who_created <> 'PROXY_OATS_APPLICANT'; - """ - cursor.execute(count_query) - count_total = dict(cursor.fetchone())["count"] - logger.info(f"Total application to set hide_from_portal: {count_total}") - - update_query = f""" - UPDATE alcs.application AS app - SET hide_from_portal = TRUE - FROM oats.oats_alr_applications AS oaa - WHERE app.file_number = oaa.alr_application_id::TEXT - AND oaa.who_created <> 'PROXY_OATS_APPLICANT'; - """ - cursor.execute(update_query) - logger.info( - f"Total hide_from_portal flag on applications updated: {cursor.rowcount}" - ) - conn.commit() - except Exception as err: - logger.exception(err) - cursor.close() - conn.close() - - logger.info(f"Finished {etl_name}") diff --git a/bin/migrate-oats-data/applications/sql/application_decision_date/application_decision_date.sql b/bin/migrate-oats-data/applications/sql/application_decision_date/application_decision_date.sql new file mode 100644 index 0000000000..a75eb51f30 --- /dev/null +++ b/bin/migrate-oats-data/applications/sql/application_decision_date/application_decision_date.sql @@ -0,0 +1,12 @@ +WITH decision_date_for_applications AS ( + SELECT oaad.alr_appl_decision_id, + oaad.alr_application_id, + oaad.decision_date + FROM oats.oats_alr_appl_decisions oaad + JOIN alcs.application oaa ON oaad.alr_application_id::TEXT = oaa.file_number + WHERE oaad.decision_date IS NOT NULL +) +SELECT alr_appl_decision_id, + alr_application_id, + decision_date +FROM decision_date_for_applications \ No newline at end of file diff --git a/bin/migrate-oats-data/applications/sql/application_decision_date/application_decision_date_count.sql b/bin/migrate-oats-data/applications/sql/application_decision_date/application_decision_date_count.sql new file mode 100644 index 0000000000..dd248c8c8b --- /dev/null +++ b/bin/migrate-oats-data/applications/sql/application_decision_date/application_decision_date_count.sql @@ -0,0 +1,10 @@ +WITH decision_date_for_applications AS ( + SELECT oaad.alr_appl_decision_id, + oaad.alr_application_id, + oaad.decision_date + FROM oats.oats_alr_appl_decisions oaad + JOIN alcs.application oaa ON oaad.alr_application_id::TEXT = oaa.file_number + WHERE oaad.decision_date IS NOT NULL +) +SELECT count(*) +FROM decision_date_for_applications \ No newline at end of file diff --git a/bin/migrate-oats-data/applications/sql/application_decision_date/application_decision_date_validation.sql b/bin/migrate-oats-data/applications/sql/application_decision_date/application_decision_date_validation.sql new file mode 100644 index 0000000000..a95198fb8f --- /dev/null +++ b/bin/migrate-oats-data/applications/sql/application_decision_date/application_decision_date_validation.sql @@ -0,0 +1,21 @@ +WITH nois_with_one_or_zero_component_only AS ( + SELECT oaa.alr_application_id + FROM oats.alcs_etl_applications_nois oaa + WHERE oaa.application_class_code IN ('LOA', 'BLK', 'SCH', 'NAN') + and oaa.alr_change_code <> 'SRW' +), +oats_decision_date_for_applications AS ( + SELECT oaad.alr_appl_decision_id, + oaa.alr_application_id, + oaad.decision_date + FROM oats.oats_alr_appl_decisions oaad + JOIN applications_with_one_or_zero_component_only oaa ON oaad.alr_application_id = oaa.alr_application_id + WHERE oaad.decision_date IS NOT NULL +) +SELECT oats_decision_date.alr_appl_decision_id, + oats_decision_date.alr_application_id, + oats_decision_date.decision_date, + appl.decision_date +FROM oats_decision_date_for_applications AS oats_decision_date + JOIN alcs.application appl ON oats_decision_date.alr_application_id::text = appl.file_number +WHERE oats_decision_date.decision_date != appl.decision_date AT TIME ZONE 'UTC' \ No newline at end of file diff --git a/bin/migrate-oats-data/applications/sql/insert-batch-application.sql b/bin/migrate-oats-data/applications/sql/insert-batch-application.sql index 2af920d2ee..dfb4802884 100644 --- a/bin/migrate-oats-data/applications/sql/insert-batch-application.sql +++ b/bin/migrate-oats-data/applications/sql/insert-batch-application.sql @@ -57,6 +57,7 @@ WITH WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Saturna Island' THEN 'Islands Trust Saturna Island (Historical)' WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Sidney Island' THEN 'Islands Trust Sidney Island (Historical)' WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Comox Strathcona' THEN 'Islands Trust Comox Strathcona (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Comox-Strathcona (Historical)' THEN 'Comox-Strathcona Regional District (Historical)' WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Nanaimo' THEN 'Islands Trust Nanaimo (Historical)' WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Capital' THEN 'Islands Trust Capital (Historical)' WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Powell River' THEN 'Islands Trust Powell River (Historical)' diff --git a/bin/migrate-oats-data/applications/submissions/review/lg_submission_review.py b/bin/migrate-oats-data/applications/submissions/review/lg_submission_review.py index 66d649fa8b..195c78c8a4 100644 --- a/bin/migrate-oats-data/applications/submissions/review/lg_submission_review.py +++ b/bin/migrate-oats-data/applications/submissions/review/lg_submission_review.py @@ -143,9 +143,10 @@ def _map_data(row): def _map_ocp_designation(row): - if row["community_pln_compliance_ind"] == "X": + compliance_ind = row.get("community_pln_compliance_ind") + if compliance_ind == "X": return False - elif row["community_pln_compliance_ind"] == "N": + elif compliance_ind in ["N", "Y"]: return True else: return None @@ -161,9 +162,10 @@ def _map_ocp_consistency(row): def _map_zoning(row): - if row["zoning_compliance_ind"] == "X": + compliance_ind = row.get("zoning_compliance_ind") + if compliance_ind == "X": return False - elif row["zoning_compliance_ind"] == "N": + elif compliance_ind in ["N", "Y"]: return True else: return None diff --git a/bin/migrate-oats-data/noi/__init__.py b/bin/migrate-oats-data/noi/__init__.py index 17d9fc895c..e60755682e 100644 --- a/bin/migrate-oats-data/noi/__init__.py +++ b/bin/migrate-oats-data/noi/__init__.py @@ -1,3 +1,3 @@ from .notice_of_intent_init import init_notice_of_intents, clean_notice_of_intents from .notice_of_intent_migration import process_notice_of_intent -from .set_hide_from_portal_on_noi import set_hide_from_portal_on_notice_of_intent +from .set_notice_of_intent_visibility import set_notice_of_intent_visibility diff --git a/bin/migrate-oats-data/noi/notice_of_intent_migration.py b/bin/migrate-oats-data/noi/notice_of_intent_migration.py index 93cf8f3fe2..776e074801 100644 --- a/bin/migrate-oats-data/noi/notice_of_intent_migration.py +++ b/bin/migrate-oats-data/noi/notice_of_intent_migration.py @@ -80,7 +80,7 @@ clean_notice_of_intent_conditions_to_components, ) -from .set_hide_from_portal_on_noi import set_hide_from_portal_on_notice_of_intent +from .set_notice_of_intent_visibility import set_notice_of_intent_visibility def init_notice_of_intent(batch_size): @@ -173,7 +173,7 @@ def process_notice_of_intent(batch_size): process_notice_of_intent_decisions(batch_size) - set_hide_from_portal_on_notice_of_intent() + set_notice_of_intent_visibility() # this script must be the last one process_notice_of_intent_submission_status_emails() diff --git a/bin/migrate-oats-data/noi/oats_to_alcs_notice_of_intent_table_etl/notice_of_intent_decision_date.py b/bin/migrate-oats-data/noi/oats_to_alcs_notice_of_intent_table_etl/notice_of_intent_decision_date.py index 97b4152883..1e5a787dde 100644 --- a/bin/migrate-oats-data/noi/oats_to_alcs_notice_of_intent_table_etl/notice_of_intent_decision_date.py +++ b/bin/migrate-oats-data/noi/oats_to_alcs_notice_of_intent_table_etl/notice_of_intent_decision_date.py @@ -22,7 +22,7 @@ def process_alcs_notice_of_intent_decision_date( logger.info(f"Start {etl_name}") with conn.cursor(cursor_factory=RealDictCursor) as cursor: with open( - "noi/sql/notice_of_intent_base/notice_of_intent_base.count.sql", + "noi/sql/notice_of_intent_base/notice_of_intent_decision_date/notice_of_intent_decision_date_count.sql", "r", encoding="utf-8", ) as sql_file: diff --git a/bin/migrate-oats-data/noi/set_hide_from_portal_on_noi.py b/bin/migrate-oats-data/noi/set_hide_from_portal_on_noi.py deleted file mode 100644 index 2e45514cc7..0000000000 --- a/bin/migrate-oats-data/noi/set_hide_from_portal_on_noi.py +++ /dev/null @@ -1,48 +0,0 @@ -from common import setup_and_get_logger -from db import inject_conn_pool -from psycopg2.extras import RealDictCursor - - -etl_name = "set_hide_from_portal_on_notice_of_intent" -logger = setup_and_get_logger(etl_name) - - -@inject_conn_pool -def set_hide_from_portal_on_notice_of_intent(conn=None): - """ - This function is responsible for setting hide_from_portal colum on notice_of_intent table where oats.who_create is not PROXY user - - Args: - conn (psycopg2.extensions.connection): PostgreSQL database connection. Provided by the decorator. - """ - logger.info(f"Start {etl_name}") - - try: - with conn.cursor(cursor_factory=RealDictCursor) as cursor: - count_query = f""" - SELECT count(*) FROM oats.oats_alr_applications oaa - JOIN alcs.notice_of_intent a ON a.file_number = oaa.alr_application_id::TEXT - WHERE oaa.who_created <> 'PROXY_OATS_APPLICANT'; - """ - cursor.execute(count_query) - count_total = dict(cursor.fetchone())["count"] - logger.info(f"Total application to set hide_from_portal: {count_total}") - - update_query = f""" - UPDATE alcs.notice_of_intent AS noi - SET hide_from_portal = TRUE - FROM oats.oats_alr_applications AS oaa - WHERE noi.file_number = oaa.alr_application_id::TEXT - AND oaa.who_created <> 'PROXY_OATS_APPLICANT'; - """ - cursor.execute(update_query) - logger.info( - f"Total hide_from_portal flag on notifications updated: {cursor.rowcount}" - ) - conn.commit() - except Exception as err: - logger.exception(err) - cursor.close() - conn.close() - - logger.info(f"Finished {etl_name}") diff --git a/bin/migrate-oats-data/noi/set_notice_of_intent_visibility.py b/bin/migrate-oats-data/noi/set_notice_of_intent_visibility.py new file mode 100644 index 0000000000..7b3c2ce334 --- /dev/null +++ b/bin/migrate-oats-data/noi/set_notice_of_intent_visibility.py @@ -0,0 +1,160 @@ +from common import setup_and_get_logger +from db import inject_conn_pool +from psycopg2.extras import RealDictCursor + + +etl_name = "set_notice_of_intent_visibility" +logger = setup_and_get_logger(etl_name) + + +@inject_conn_pool +def set_notice_of_intent_visibility(conn=None): + """This function is responsible for setting source column on application and mapping it to public portal visibility visibility.""" + _set_source_on_notice_of_intent(conn) + _set_hide_from_portal_on_notice_of_intent(conn) + + +def _set_hide_from_portal_on_notice_of_intent(conn): + """ + This function is responsible for setting hide_from_portal colum on notice_of_intent table based on the source. + + Args: + conn (psycopg2.extensions.connection): PostgreSQL database connection. Provided by the decorator. + """ + logger.info(f"Start {etl_name}") + + try: + with conn.cursor(cursor_factory=RealDictCursor) as cursor: + count_visible_to_portal_query = f""" + SELECT count(*) FROM alcs.notice_of_intent a + WHERE a."source" = 'APPLICANT'; + """ + cursor.execute(count_visible_to_portal_query) + visible_to_portal_count = dict(cursor.fetchone())["count"] + + count_hidden_from_portal_query = f""" + SELECT count(*) FROM alcs.notice_of_intent a + WHERE a."source" <> 'APPLICANT'; + """ + cursor.execute(count_hidden_from_portal_query) + hidden_from_portal_count = dict(cursor.fetchone())["count"] + + count_total = visible_to_portal_count + hidden_from_portal_count + + logger.info(f"Total application to set hide_from_portal: {count_total}") + + set_hide_from_portal_false_query = f""" + UPDATE alcs.notice_of_intent + SET hide_from_portal = FALSE + WHERE "source" = 'APPLICANT'; + """ + cursor.execute(set_hide_from_portal_false_query) + updated_visible_to_portal_count = cursor.rowcount + + set_hide_from_portal_true_query = f""" + UPDATE alcs.notice_of_intent + SET hide_from_portal = TRUE + WHERE "source" <> 'APPLICANT'; + """ + cursor.execute(set_hide_from_portal_true_query) + updated_hidden_from_portal_count = cursor.rowcount + + updated_total_count = ( + updated_visible_to_portal_count + updated_hidden_from_portal_count + ) + + logger.info( + f"Total hide_from_portal flag on notice_of_intent updated: {updated_total_count}" + ) + conn.commit() + except Exception as err: + logger.exception(err) + cursor.close() + conn.close() + + logger.info(f"Finished {etl_name}") + + +def _set_source_on_notice_of_intent(conn): + """""" + logger.info(f"Start {etl_name} => _set_source_on_notice_of_intent") + + try: + with conn.cursor(cursor_factory=RealDictCursor) as cursor: + count_created_by_applicant_query = f""" + SELECT count(*) FROM oats.oats_alr_applications oaa + JOIN alcs.notice_of_intent noi ON noi.file_number = oaa.alr_application_id::TEXT + WHERE oaa.who_created = 'PROXY_OATS_APPLICANT'; + """ + cursor.execute(count_created_by_applicant_query) + created_by_applicant_count = dict(cursor.fetchone())["count"] + + count_created_by_oats_query = f""" + SELECT count(*) FROM oats.oats_alr_applications oaa + JOIN alcs.notice_of_intent noi ON noi.file_number = oaa.alr_application_id::TEXT + WHERE oaa.who_created <> 'PROXY_OATS_APPLICANT'; + """ + cursor.execute(count_created_by_oats_query) + created_by_oats_count = dict(cursor.fetchone())["count"] + + count_created_in_alcs_query = f""" + SELECT count(*) FROM alcs.notice_of_intent a + WHERE a."source" <> 'APPLICANT' + AND a.audit_created_by <> 'oats_etl'; + """ + cursor.execute(count_created_in_alcs_query) + created_by_alcs_count = dict(cursor.fetchone())["count"] + + count_total = ( + created_by_applicant_count + + created_by_oats_count + + created_by_alcs_count + ) + + logger.info(f"Total application to set hide_from_portal: {count_total}") + + update_created_by_applicant_query = f""" + UPDATE alcs.notice_of_intent + SET "source" = 'APPLICANT' + FROM oats.oats_alr_applications oaa + WHERE alcs.notice_of_intent.file_number = oaa.alr_application_id::TEXT + AND oaa.who_created = 'PROXY_OATS_APPLICANT'; + """ + cursor.execute(update_created_by_applicant_query) + updated_created_by_applicant_count = cursor.rowcount + + update_created_by_oats_query = f""" + UPDATE alcs.notice_of_intent + SET "source" = 'OATS' + FROM oats.oats_alr_applications oaa + WHERE alcs.notice_of_intent.file_number = oaa.alr_application_id::TEXT + AND oaa.who_created <> 'PROXY_OATS_APPLICANT'; + """ + cursor.execute(update_created_by_oats_query) + updated_created_by_oats_count = cursor.rowcount + + update_created_by_alcs_query = f""" + UPDATE alcs.notice_of_intent + SET "source" = 'APPLICANT' + WHERE alcs.notice_of_intent."source" <> 'APPLICANT' + AND alcs.notice_of_intent.audit_created_by <> 'oats_etl'; + """ + cursor.execute(update_created_by_alcs_query) + updated_created_by_alcs_count = cursor.rowcount + + total_processed = ( + updated_created_by_applicant_count + + updated_created_by_oats_count + + updated_created_by_alcs_count + ) + + logger.info( + f"Total source on 'notice_of_intent' updated: {total_processed}" + ) + conn.commit() + except Exception as err: + logger.exception(err) + cursor.close() + conn.close() + + logger.info(f"Finished {etl_name}") diff --git a/bin/migrate-oats-data/noi/sql/insert_noi.sql b/bin/migrate-oats-data/noi/sql/insert_noi.sql index 5444c66958..327ad82c53 100644 --- a/bin/migrate-oats-data/noi/sql/insert_noi.sql +++ b/bin/migrate-oats-data/noi/sql/insert_noi.sql @@ -52,6 +52,7 @@ alcs_gov AS ( WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Saturna Island' THEN 'Islands Trust Saturna Island (Historical)' WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Sidney Island' THEN 'Islands Trust Sidney Island (Historical)' WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Comox Strathcona' THEN 'Islands Trust Comox Strathcona (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Comox-Strathcona (Historical)' THEN 'Comox-Strathcona Regional District (Historical)' WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Nanaimo' THEN 'Islands Trust Nanaimo (Historical)' WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Capital' THEN 'Islands Trust Capital (Historical)' WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Powell River' THEN 'Islands Trust Powell River (Historical)' diff --git a/bin/migrate-oats-data/noi/sql/notice_of_intent_base/notice_of_intent_decision_date/notice_of_intent_decision_date.sql b/bin/migrate-oats-data/noi/sql/notice_of_intent_base/notice_of_intent_decision_date/notice_of_intent_decision_date.sql index b82ea08191..f265d220df 100644 --- a/bin/migrate-oats-data/noi/sql/notice_of_intent_base/notice_of_intent_decision_date/notice_of_intent_decision_date.sql +++ b/bin/migrate-oats-data/noi/sql/notice_of_intent_base/notice_of_intent_decision_date/notice_of_intent_decision_date.sql @@ -1,17 +1,10 @@ -WITH nois_with_one_or_zero_component_only AS ( - SELECT oaa.alr_application_id - FROM oats.alcs_etl_applications_nois oaa - WHERE oaa.application_class_code = 'NOI' - and oaa.alr_change_code <> 'SRW' -), -decision_date_for_nois AS ( +WITH decision_date_for_nois AS ( SELECT oaad.alr_appl_decision_id, - oaa.alr_application_id, + oaad.alr_application_id, oaad.decision_date FROM oats.oats_alr_appl_decisions oaad - JOIN oats.oats_alr_applications oaa ON oaad.alr_application_id = oaa.alr_application_id - WHERE oaa.application_class_code = 'NOI' - AND oaad.decision_date IS NOT NULL + JOIN alcs.notice_of_intent noi ON oaad.alr_application_id::TEXT = noi.file_number + WHERE oaad.decision_date IS NOT NULL ) SELECT alr_appl_decision_id, alr_application_id, diff --git a/bin/migrate-oats-data/noi/sql/notice_of_intent_base/notice_of_intent_decision_date/notice_of_intent_decision_date_count.sql b/bin/migrate-oats-data/noi/sql/notice_of_intent_base/notice_of_intent_decision_date/notice_of_intent_decision_date_count.sql new file mode 100644 index 0000000000..52cd335dbb --- /dev/null +++ b/bin/migrate-oats-data/noi/sql/notice_of_intent_base/notice_of_intent_decision_date/notice_of_intent_decision_date_count.sql @@ -0,0 +1,10 @@ +WITH decision_date_for_nois AS ( + SELECT oaad.alr_appl_decision_id, + oaad.alr_application_id, + oaad.decision_date + FROM oats.oats_alr_appl_decisions oaad + JOIN alcs.notice_of_intent noi ON oaad.alr_application_id::TEXT = noi.file_number + WHERE oaad.decision_date IS NOT NULL +) +SELECT count(*) +FROM decision_date_for_nois \ No newline at end of file diff --git a/portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.ts b/portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.ts index 11e5929c6d..7710c7f394 100644 --- a/portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.ts +++ b/portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.ts @@ -245,7 +245,7 @@ export class AlcsEditSubmissionComponent implements OnInit, OnDestroy, AfterView async onDownloadPdf(fileNumber: string | undefined) { if (fileNumber) { - await this.pdfGenerationService.generateSubmission(fileNumber); + await this.pdfGenerationService.generateAppSubmission(fileNumber); } } diff --git a/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.ts index eb88423a7b..c0863a6f81 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.ts @@ -289,7 +289,7 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit async onDownloadPdf(fileNumber: string | undefined) { if (fileNumber) { - await this.pdfGenerationService.generateSubmission(fileNumber); + await this.pdfGenerationService.generateAppSubmission(fileNumber); } } diff --git a/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.ts b/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.ts index d8195e4445..3fba56b498 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.ts @@ -60,7 +60,7 @@ export class ReviewAndSubmitComponent extends StepComponent implements OnInit, O async onDownloadPdf(fileNumber: string | undefined) { if (fileNumber) { - await this.pdfGenerationService.generateSubmission(fileNumber); + await this.pdfGenerationService.generateAppSubmission(fileNumber); } } } diff --git a/portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.ts b/portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.ts index 83a64af5ef..3d273f824d 100644 --- a/portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.ts +++ b/portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.ts @@ -115,7 +115,7 @@ export class ViewApplicationSubmissionComponent implements OnInit, OnDestroy { async onDownloadSubmissionPdf(fileNumber: string | undefined) { if (fileNumber) { - await this.pdfGenerationService.generateSubmission(fileNumber); + await this.pdfGenerationService.generateAppSubmission(fileNumber); } } } diff --git a/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.spec.ts index dc910ac2ac..d73e471d43 100644 --- a/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.spec.ts +++ b/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.spec.ts @@ -3,11 +3,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatDialogModule } from '@angular/material/dialog'; import { RouterTestingModule } from '@angular/router/testing'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { CodeService } from '../../../services/code/code.service'; import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; import { NoticeOfIntentSubmissionDraftService } from '../../../services/notice-of-intent-submission/notice-of-intent-submission-draft.service'; -import { NoticeOfIntentSubmissionService } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; import { ToastService } from '../../../services/toast/toast.service'; import { AlcsEditSubmissionComponent } from './alcs-edit-submission.component'; @@ -15,11 +13,8 @@ import { AlcsEditSubmissionComponent } from './alcs-edit-submission.component'; describe('AlcsEditSubmissionComponent', () => { let component: AlcsEditSubmissionComponent; let fixture: ComponentFixture; - let mockSubmissionService: DeepMocked; beforeEach(async () => { - mockSubmissionService = createMock(); - await TestBed.configureTestingModule({ declarations: [AlcsEditSubmissionComponent], providers: [ @@ -43,10 +38,6 @@ describe('AlcsEditSubmissionComponent', () => { provide: PdfGenerationService, useValue: {}, }, - { - provide: NoticeOfIntentSubmissionService, - useValue: mockSubmissionService, - }, ], imports: [RouterTestingModule, MatAutocompleteModule, MatDialogModule], schemas: [NO_ERRORS_SCHEMA], diff --git a/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.ts b/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.ts index 15714dc0b3..c042ae32ca 100644 --- a/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.ts @@ -8,7 +8,6 @@ import { NoticeOfIntentDocumentDto } from '../../../services/notice-of-intent-do import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; import { NoticeOfIntentSubmissionDraftService } from '../../../services/notice-of-intent-submission/notice-of-intent-submission-draft.service'; import { NoticeOfIntentSubmissionDetailedDto } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; -import { NoticeOfIntentSubmissionService } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; import { ToastService } from '../../../services/toast/toast.service'; import { CustomStepperComponent } from '../../../shared/custom-stepper/custom-stepper.component'; import { OverlaySpinnerService } from '../../../shared/overlay-spinner/overlay-spinner.service'; @@ -59,7 +58,6 @@ export class AlcsEditSubmissionComponent implements OnInit, OnDestroy, AfterView constructor( private noticeOfIntentSubmissionDraftService: NoticeOfIntentSubmissionDraftService, - private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, private activatedRoute: ActivatedRoute, private dialog: MatDialog, diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.ts index 2c4b8a1528..7c378d718c 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.ts @@ -57,7 +57,7 @@ export class ReviewAndSubmitComponent extends StepComponent implements OnInit, O async onDownloadPdf(fileNumber: string | undefined) { if (fileNumber) { - await this.pdfGenerationService.generateSubmission(fileNumber); + await this.pdfGenerationService.generateAppSubmission(fileNumber); } } } diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.ts b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.ts index 1a8be450de..dc207c8289 100644 --- a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.ts @@ -1,8 +1,6 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { ApplicationOwnerDto } from '../../../services/application-owner/application-owner.dto'; -import { PARCEL_OWNERSHIP_TYPE } from '../../../services/application-parcel/application-parcel.dto'; import { LocalGovernmentDto } from '../../../services/code/code.dto'; import { CodeService } from '../../../services/code/code.service'; import { NoticeOfIntentDocumentDto } from '../../../services/notice-of-intent-document/notice-of-intent-document.dto'; diff --git a/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.spec.ts index 05314f9cdf..dba8aecae4 100644 --- a/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.spec.ts +++ b/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.spec.ts @@ -2,6 +2,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; import { BehaviorSubject } from 'rxjs'; import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; @@ -17,11 +18,13 @@ describe('ViewNoticeOfIntentSubmissionComponent', () => { let mockNoiDocumentService: DeepMocked; let mockActivatedRoute: DeepMocked; let mockDialogService: DeepMocked; + let mockPDFGenerationService: DeepMocked; beforeEach(async () => { mockNoiSubmissionService = createMock(); mockNoiDocumentService = createMock(); mockActivatedRoute = createMock(); + mockPDFGenerationService = createMock(); mockActivatedRoute.paramMap = new BehaviorSubject(new Map()); @@ -44,6 +47,10 @@ describe('ViewNoticeOfIntentSubmissionComponent', () => { provide: ConfirmationDialogService, useValue: mockDialogService, }, + { + provide: PdfGenerationService, + useValue: mockPDFGenerationService, + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.ts b/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.ts index 94f8c28728..93a45aa022 100644 --- a/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.ts @@ -1,6 +1,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; +import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; import { NoticeOfIntentDocumentDto } from '../../../services/notice-of-intent-document/notice-of-intent-document.dto'; import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; @@ -26,6 +27,7 @@ export class ViewNoticeOfIntentSubmissionComponent implements OnInit, OnDestroy private noiSubmissionService: NoticeOfIntentSubmissionService, private noiDocumentService: NoticeOfIntentDocumentService, private confirmationDialogService: ConfirmationDialogService, + private pdfGenerationService: PdfGenerationService, private route: ActivatedRoute, private router: Router ) {} @@ -80,7 +82,9 @@ export class ViewNoticeOfIntentSubmissionComponent implements OnInit, OnDestroy }); } - onDownloadSubmissionPdf(fileNumber: string) { - //TODO: When we add PDFs + async onDownloadSubmissionPdf(fileNumber: string) { + if (fileNumber) { + await this.pdfGenerationService.generateNoiSubmission(fileNumber); + } } } diff --git a/portal-frontend/src/app/services/pdf-generation/pdf-generation.service.spec.ts b/portal-frontend/src/app/services/pdf-generation/pdf-generation.service.spec.ts index 1c54b380fe..c589044fc7 100644 --- a/portal-frontend/src/app/services/pdf-generation/pdf-generation.service.spec.ts +++ b/portal-frontend/src/app/services/pdf-generation/pdf-generation.service.spec.ts @@ -52,7 +52,7 @@ describe('PdfGenerationService', () => { const getPdfFile = jest.spyOn(fileModule, 'openPdfFile'); getPdfFile.mockReturnValue(undefined); - await service.generateSubmission('fake'); + await service.generateAppSubmission('fake'); expect(mockHttpClient.get).toHaveBeenCalledTimes(1); expect(mockHttpClient.get).toBeCalledWith(`${environment.apiUrl}/pdf-generation/fake/submission`, { diff --git a/portal-frontend/src/app/services/pdf-generation/pdf-generation.service.ts b/portal-frontend/src/app/services/pdf-generation/pdf-generation.service.ts index cb1472283b..4f5f27ca7b 100644 --- a/portal-frontend/src/app/services/pdf-generation/pdf-generation.service.ts +++ b/portal-frontend/src/app/services/pdf-generation/pdf-generation.service.ts @@ -24,36 +24,36 @@ export class PdfGenerationService { private overlayService: OverlaySpinnerService ) {} - async generateSubmission(fileNumber: string) { - try { - this.overlayService.showSpinner(); - const httpOptions = { - responseType: 'blob' as 'json', - }; - const data = await firstValueFrom( - this.httpClient.get(`${this.serviceUrl}/${fileNumber}/submission`, httpOptions) - ); - - return openPdfFile(`${fileNumber}_${moment().format('MMM_DD_YYYY_hh-mm_Z')}`, data); - } catch (e) { - console.error(e); - this.toastService.showErrorToast('Failed to generate pdf, please try again later'); - } finally { - this.overlayService.hideSpinner(); - } + async generateAppSubmission(fileNumber: string) { + return await this.downloadPdf( + `${this.serviceUrl}/${fileNumber}/submission`, + `${fileNumber}_${moment().format('MMM_DD_YYYY_hh-mm_Z')}` + ); + } - return; + async generateNoiSubmission(fileNumber: string) { + return await this.downloadPdf( + `${this.serviceUrl}/${fileNumber}/noi-submission`, + `${fileNumber}_${moment().format('MMM_DD_YYYY_hh-mm_Z')}` + ); } async generateReview(fileNumber: string) { + return await this.downloadPdf( + `${this.serviceUrl}/${fileNumber}/review`, + `${fileNumber}-Review_${moment().format('MMM_DD_YYYY_hh-mm_Z')}` + ); + } + + private async downloadPdf(url: string, fileName: string) { try { this.overlayService.showSpinner(); const httpOptions = { responseType: 'blob' as 'json', }; - const data = await firstValueFrom(this.httpClient.get(`${this.serviceUrl}/${fileNumber}/review`, httpOptions)); + const data = await firstValueFrom(this.httpClient.get(url, httpOptions)); - return openPdfFile(`${fileNumber}-Review_${moment().format('MMM_DD_YYYY_hh-mm_Z')}`, data); + return openPdfFile(fileName, data); } catch (e) { console.error(e); this.toastService.showErrorToast('Failed to generate pdf, please try again later'); diff --git a/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.spec.ts b/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.spec.ts index 85b4d0f0d2..a1d18e56f9 100644 --- a/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.spec.ts +++ b/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.spec.ts @@ -53,6 +53,7 @@ describe('ApplicationAdvancedSearchService', () => { andWhere: jest.fn().mockReturnThis(), setParameters: jest.fn().mockReturnThis(), leftJoin: jest.fn().mockReturnThis(), + withDeleted: jest.fn().mockReturnThis(), }; const module: TestingModule = await Test.createTestingModule({ diff --git a/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.ts b/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.ts index 9b8533ef8d..a777635829 100644 --- a/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.ts +++ b/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.ts @@ -80,6 +80,10 @@ export class ApplicationAdvancedSearchService { private compileApplicationGroupBySearchQuery(query) { query = query + // FIXME: This is a quick fix for the search performance issues. It temporarily allows + // submissions with deleted submission types to be shown. For now, there are no + // deleted submission types, so this should be fine, but should be fixed soon. + .withDeleted() .innerJoinAndMapOne( 'appSearch.applicationType', 'appSearch.applicationType', diff --git a/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.spec.ts b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.spec.ts index 9099e7fa6a..ec12ea3290 100644 --- a/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.spec.ts +++ b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.spec.ts @@ -52,6 +52,7 @@ describe('NoticeOfIntentService', () => { andWhere: jest.fn().mockReturnThis(), setParameters: jest.fn().mockReturnThis(), leftJoin: jest.fn().mockReturnThis(), + withDeleted: jest.fn().mockReturnThis(), }; const module: TestingModule = await Test.createTestingModule({ diff --git a/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts index 4c8a8e0d32..f1c8a912ec 100644 --- a/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts +++ b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts @@ -75,6 +75,10 @@ export class NoticeOfIntentAdvancedSearchService { query: SelectQueryBuilder, ) { query = query + // FIXME: This is a quick fix for the search performance issues. It temporarily allows + // submissions with deleted submission types to be shown. For now, there are no + // deleted submission types, so this should be fine, but should be fixed soon. + .withDeleted() .innerJoinAndMapOne( 'noiSearch.noticeOfIntentType', 'noiSearch.noticeOfIntentType', diff --git a/services/apps/alcs/src/alcs/search/notification/notification-advanced-search.service.spec.ts b/services/apps/alcs/src/alcs/search/notification/notification-advanced-search.service.spec.ts index 4bf9118d6c..05cacfbc75 100644 --- a/services/apps/alcs/src/alcs/search/notification/notification-advanced-search.service.spec.ts +++ b/services/apps/alcs/src/alcs/search/notification/notification-advanced-search.service.spec.ts @@ -52,6 +52,7 @@ describe('NotificationAdvancedSearchService', () => { andWhere: jest.fn().mockReturnThis(), setParameters: jest.fn().mockReturnThis(), leftJoin: jest.fn().mockReturnThis(), + withDeleted: jest.fn().mockReturnThis(), }; const module: TestingModule = await Test.createTestingModule({ diff --git a/services/apps/alcs/src/alcs/search/notification/notification-advanced-search.service.ts b/services/apps/alcs/src/alcs/search/notification/notification-advanced-search.service.ts index aa07e6a865..228e62a7c7 100644 --- a/services/apps/alcs/src/alcs/search/notification/notification-advanced-search.service.ts +++ b/services/apps/alcs/src/alcs/search/notification/notification-advanced-search.service.ts @@ -74,6 +74,10 @@ export class NotificationAdvancedSearchService { query: SelectQueryBuilder, ) { query = query + // FIXME: This is a quick fix for the search performance issues. It temporarily allows + // submissions with deleted submission types to be shown. For now, there are no + // deleted submission types, so this should be fine, but should be fixed soon. + .withDeleted() .innerJoinAndMapOne( 'notificationSearch.notificationType', 'notificationSearch.notificationType', diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission-draft/notice-of-intent-submission-draft.service.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission-draft/notice-of-intent-submission-draft.service.spec.ts index 39462d1bc0..20240d2878 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission-draft/notice-of-intent-submission-draft.service.spec.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission-draft/notice-of-intent-submission-draft.service.spec.ts @@ -1,17 +1,14 @@ import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { create } from 'handlebars'; import { Repository } from 'typeorm'; import { NoticeOfIntentSubmissionStatusService } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service'; import { User } from '../../user/user.entity'; -import { CovenantTransferee } from '../application-submission/covenant-transferee/covenant-transferee.entity'; -import { CovenantTransfereeService } from '../application-submission/covenant-transferee/covenant-transferee.service'; import { NoticeOfIntentOwnerService } from '../notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service'; import { NoticeOfIntentParcelService } from '../notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service'; import { NoticeOfIntentSubmission } from '../notice-of-intent-submission/notice-of-intent-submission.entity'; import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission/notice-of-intent-submission.service'; -import { GenerateSubmissionDocumentService } from '../pdf-generation/generate-submission-document.service'; +import { GenerateNoiSubmissionDocumentService } from '../pdf-generation/generate-noi-submission-document.service'; import { NoticeOfIntentSubmissionDraftService } from './notice-of-intent-submission-draft.service'; describe('NoticeOfIntentSubmissionDraftService', () => { @@ -20,7 +17,7 @@ describe('NoticeOfIntentSubmissionDraftService', () => { let mockNoiSubmissionService: DeepMocked; let mockParcelService: DeepMocked; let mockAppOwnerService: DeepMocked; - let mockGenerateSubmissionDocumentService: DeepMocked; + let mockGenerateSubmissionDocumentService: DeepMocked; let mockNoiSubmissionStatusService: DeepMocked; let mockUser; @@ -57,7 +54,7 @@ describe('NoticeOfIntentSubmissionDraftService', () => { useValue: mockAppOwnerService, }, { - provide: GenerateSubmissionDocumentService, + provide: GenerateNoiSubmissionDocumentService, useValue: mockGenerateSubmissionDocumentService, }, { diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission-draft/notice-of-intent-submission-draft.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission-draft/notice-of-intent-submission-draft.service.ts index 0694fa8a79..2331ac4795 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission-draft/notice-of-intent-submission-draft.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission-draft/notice-of-intent-submission-draft.service.ts @@ -9,6 +9,7 @@ import { NoticeOfIntentParcelUpdateDto } from '../notice-of-intent-submission/no import { NoticeOfIntentParcelService } from '../notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service'; import { NoticeOfIntentSubmission } from '../notice-of-intent-submission/notice-of-intent-submission.entity'; import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission/notice-of-intent-submission.service'; +import { GenerateNoiSubmissionDocumentService } from '../pdf-generation/generate-noi-submission-document.service'; @Injectable() export class NoticeOfIntentSubmissionDraftService { @@ -21,6 +22,7 @@ export class NoticeOfIntentSubmissionDraftService { private noticeOfIntentParcelService: NoticeOfIntentParcelService, private noticeOfIntentOwnerService: NoticeOfIntentOwnerService, private noticeOfIntentSubmissionStatusService: NoticeOfIntentSubmissionStatusService, + private generateNoiSubmissionDocumentService: GenerateNoiSubmissionDocumentService, ) {} async getOrCreateDraft(fileNumber: string, user: User) { @@ -89,9 +91,8 @@ export class NoticeOfIntentSubmissionDraftService { parcels: [], owners: [], }); - const savedSubmission = await this.noticeOfIntentSubmissionRepository.save( - newSubmission, - ); + const savedSubmission = + await this.noticeOfIntentSubmissionRepository.save(newSubmission); const statuses = await this.noticeOfIntentSubmissionStatusService.getCopiedStatuses( originalSubmission.uuid, @@ -217,12 +218,11 @@ export class NoticeOfIntentSubmissionDraftService { draft.isDraft = false; await this.noticeOfIntentSubmissionRepository.save(draft); - //Generate PDF - //TODO: Turn this back on for PDFs - // await this.generateSubmissionDocumentService.generateUpdate( - // fileNumber, - // user, - // ); + await this.generateNoiSubmissionDocumentService.generateUpdate( + fileNumber, + user, + ); + this.logger.debug(`Published Draft for file number ${fileNumber}`); } diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts index 3672193f25..60ae112991 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts @@ -199,6 +199,7 @@ export class NoticeOfIntentSubmissionController { await this.noticeOfIntentSubmissionService.submitToAlcs( validatedApplicationSubmission, + req.user.entity, ); const { primaryContact, submissionGovernment } = diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts index d67b02181b..e42578b6f5 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts @@ -13,6 +13,7 @@ import { ParcelOwnershipType } from '../../common/entities/parcel-ownership-type import { OwnerType } from '../../common/owner-type/owner-type.entity'; import { DocumentModule } from '../../document/document.module'; import { FileNumberModule } from '../../file-number/file-number.module'; +import { PdfGenerationModule } from '../pdf-generation/pdf-generation.module'; import { NoticeOfIntentOwnerController } from './notice-of-intent-owner/notice-of-intent-owner.controller'; import { NoticeOfIntentOwner } from './notice-of-intent-owner/notice-of-intent-owner.entity'; import { NoticeOfIntentOwnerService } from './notice-of-intent-owner/notice-of-intent-owner.service'; @@ -41,6 +42,7 @@ import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.s forwardRef(() => BoardModule), LocalGovernmentModule, FileNumberModule, + PdfGenerationModule, ], controllers: [ NoticeOfIntentSubmissionController, diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts index ab2c5b609c..7f32f328de 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts @@ -20,6 +20,7 @@ import { NoticeOfIntentService } from '../../alcs/notice-of-intent/notice-of-int import { NoticeOfIntentSubmissionProfile } from '../../common/automapper/notice-of-intent-submission.automapper.profile'; import { FileNumberService } from '../../file-number/file-number.service'; import { User } from '../../user/user.entity'; +import { GenerateNoiSubmissionDocumentService } from '../pdf-generation/generate-noi-submission-document.service'; import { ValidatedNoticeOfIntentSubmission } from './notice-of-intent-submission-validator.service'; import { NoticeOfIntentSubmission, @@ -38,6 +39,7 @@ describe('NoticeOfIntentSubmissionService', () => { let mockNoiDocService: DeepMocked; let mockFileNumberService: DeepMocked; let mockNoiStatusService: DeepMocked; + let mockGenerateNoiSubmissionDocumentService: DeepMocked; let mockNoiSubmission; let mockQueryBuilder; @@ -49,6 +51,7 @@ describe('NoticeOfIntentSubmissionService', () => { mockNoiDocService = createMock(); mockFileNumberService = createMock(); mockNoiStatusService = createMock(); + mockGenerateNoiSubmissionDocumentService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -71,6 +74,10 @@ describe('NoticeOfIntentSubmissionService', () => { provide: NoticeOfIntentService, useValue: mockNoiService, }, + { + provide: GenerateNoiSubmissionDocumentService, + useValue: mockGenerateNoiSubmissionDocumentService, + }, { provide: LocalGovernmentService, useValue: mockLGService, @@ -252,6 +259,7 @@ describe('NoticeOfIntentSubmissionService', () => { await expect( service.submitToAlcs( noticeOfIntentSubmission as ValidatedNoticeOfIntentSubmission, + new User(), ), ).rejects.toMatchObject( new BaseServiceException( @@ -267,14 +275,19 @@ describe('NoticeOfIntentSubmissionService', () => { mockNoiStatusService.setStatusDate.mockResolvedValue( new NoticeOfIntentSubmissionToSubmissionStatus(), ); + mockGenerateNoiSubmissionDocumentService.generateAndAttach.mockResolvedValue(); mockNoiService.submit.mockResolvedValue(mockNoticeOfIntent); await service.submitToAlcs( mockNoiSubmission as ValidatedNoticeOfIntentSubmission, + new User(), ); expect(mockNoiService.submit).toBeCalledTimes(1); expect(mockNoiStatusService.setStatusDate).toHaveBeenCalledTimes(1); + expect( + mockGenerateNoiSubmissionDocumentService.generateAndAttach, + ).toHaveBeenCalledTimes(1); }); it('should populate noi subtypes', async () => { @@ -283,6 +296,8 @@ describe('NoticeOfIntentSubmissionService', () => { const fileNumber = 'fake'; const localGovernmentUuid = 'fake-uuid'; + mockGenerateNoiSubmissionDocumentService.generateAndAttach.mockResolvedValue(); + const mockDate = new Date('2022-01-01'); jest.useFakeTimers().setSystemTime(mockDate); @@ -318,6 +333,7 @@ describe('NoticeOfIntentSubmissionService', () => { mockNoiService.submit.mockResolvedValue(mockNoticeOfIntent); await service.submitToAlcs( mockNoiSubmission as ValidatedNoticeOfIntentSubmission, + new User(), ); expect(mockNoiService.submit).toHaveBeenCalledTimes(1); @@ -337,6 +353,9 @@ describe('NoticeOfIntentSubmissionService', () => { 'AEPM', ], }); + expect( + mockGenerateNoiSubmissionDocumentService.generateAndAttach, + ).toHaveBeenCalledTimes(1); }); it('should update fields if notice of intent exists', async () => { diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index 3831d7c01c..b42428f12e 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -1,8 +1,8 @@ import { BaseServiceException } from '@app/common/exceptions/base.exception'; +import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; import { Mapper } from 'automapper-core'; import { InjectMapper } from 'automapper-nestjs'; -import { Injectable, Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; import { FindOptionsRelations, FindOptionsWhere, @@ -10,10 +10,10 @@ import { Not, Repository, } from 'typeorm'; -import { ApplicationDocument } from '../../alcs/application/application-document/application-document.entity'; import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { NoticeOfIntentDocument } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; import { NoticeOfIntentDocumentService } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentSubmissionStatusType } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status-type.entity'; import { NOI_SUBMISSION_STATUS } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; import { NoticeOfIntentSubmissionStatusService } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service'; import { NoticeOfIntentService } from '../../alcs/notice-of-intent/notice-of-intent.service'; @@ -24,6 +24,8 @@ import { FileNumberService } from '../../file-number/file-number.service'; import { User } from '../../user/user.entity'; import { FALLBACK_APPLICANT_NAME } from '../../utils/owner.constants'; import { filterUndefined } from '../../utils/undefined'; +import { GenerateNoiSubmissionDocumentService } from '../pdf-generation/generate-noi-submission-document.service'; +import { GenerateReviewDocumentService } from '../pdf-generation/generate-review-document.service'; import { ValidatedNoticeOfIntentSubmission } from './notice-of-intent-submission-validator.service'; import { NoticeOfIntentSubmissionDetailedDto, @@ -31,10 +33,9 @@ import { NoticeOfIntentSubmissionUpdateDto, } from './notice-of-intent-submission.dto'; import { - PORTAL_TO_ALCS_STRUCTURE_MAP, NoticeOfIntentSubmission, + PORTAL_TO_ALCS_STRUCTURE_MAP, } from './notice-of-intent-submission.entity'; -import { NoticeOfIntentSubmissionStatusType } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status-type.entity'; @Injectable() export class NoticeOfIntentSubmissionService { @@ -57,6 +58,8 @@ export class NoticeOfIntentSubmissionService { @InjectRepository(NoticeOfIntentSubmissionStatusType) private noticeOfIntentStatusRepository: Repository, private noticeOfIntentService: NoticeOfIntentService, + @Inject(forwardRef(() => GenerateNoiSubmissionDocumentService)) + private generateNoiSubmissionDocumentService: GenerateNoiSubmissionDocumentService, private localGovernmentService: LocalGovernmentService, private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, private fileNumberService: FileNumberService, @@ -311,6 +314,7 @@ export class NoticeOfIntentSubmissionService { async submitToAlcs( noticeOfIntentSubmission: ValidatedNoticeOfIntentSubmission, + user: User, ) { try { const subtypes = this.populateNoiSubtype(noticeOfIntentSubmission); @@ -330,6 +334,11 @@ export class NoticeOfIntentSubmissionService { submittedNoi.dateSubmittedToAlc, ); + await this.generateNoiSubmissionDocumentService.generateAndAttach( + submittedNoi.fileNumber, + user, + ); + return submittedNoi; } catch (ex) { this.logger.error(ex); diff --git a/services/apps/alcs/src/portal/pdf-generation/generate-noi-submission-document.service.spec.ts b/services/apps/alcs/src/portal/pdf-generation/generate-noi-submission-document.service.spec.ts new file mode 100644 index 0000000000..65dc3f6284 --- /dev/null +++ b/services/apps/alcs/src/portal/pdf-generation/generate-noi-submission-document.service.spec.ts @@ -0,0 +1,215 @@ +import { CdogsService } from '@app/common/cdogs/cdogs.service'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { HttpStatus } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import * as dayjs from 'dayjs'; +import * as timezone from 'dayjs/plugin/timezone'; +import * as utc from 'dayjs/plugin/utc'; +import { SUBMISSION_STATUS } from '../../alcs/application/application-submission-status/submission-status.dto'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +import { NoticeOfIntentDocument } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; +import { NoticeOfIntentDocumentService } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentSubmissionToSubmissionStatus } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.entity'; +import { NoticeOfIntentType } from '../../alcs/notice-of-intent/notice-of-intent-type/notice-of-intent-type.entity'; +import { NoticeOfIntent } from '../../alcs/notice-of-intent/notice-of-intent.entity'; +import { NoticeOfIntentService } from '../../alcs/notice-of-intent/notice-of-intent.service'; +import { DOCUMENT_TYPE } from '../../document/document-code.entity'; +import { DOCUMENT_SOURCE } from '../../document/document.dto'; +import { Document } from '../../document/document.entity'; +import { User } from '../../user/user.entity'; +import { NoticeOfIntentOwnerService } from '../notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentParcelService } from '../notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { NoticeOfIntentSubmission } from '../notice-of-intent-submission/notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission/notice-of-intent-submission.service'; +import { GenerateNoiSubmissionDocumentService } from './generate-noi-submission-document.service'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +describe('GenerateNoiSubmissionDocumentService', () => { + let service: GenerateNoiSubmissionDocumentService; + let mockCdogsService: DeepMocked; + let mockNoiSubmissionService: DeepMocked; + let mockNoiLocalGovernmentService: DeepMocked; + let mockNoiService: DeepMocked; + let mockNoiParcelService: DeepMocked; + let mockNoinOwnerService: DeepMocked; + let mockNoiDocumentService: DeepMocked; + + let mockSubmissionStatus; + + beforeEach(async () => { + mockCdogsService = createMock(); + mockNoiSubmissionService = createMock(); + mockNoiLocalGovernmentService = createMock(); + mockNoiService = createMock(); + mockNoiParcelService = createMock(); + mockNoinOwnerService = createMock(); + mockNoiDocumentService = createMock(); + + mockNoiLocalGovernmentService.getByUuid.mockResolvedValue( + new LocalGovernment(), + ); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + GenerateNoiSubmissionDocumentService, + { provide: CdogsService, useValue: mockCdogsService }, + { + provide: NoticeOfIntentSubmissionService, + useValue: mockNoiSubmissionService, + }, + { + provide: LocalGovernmentService, + useValue: mockNoiLocalGovernmentService, + }, + { provide: NoticeOfIntentService, useValue: mockNoiService }, + { + provide: NoticeOfIntentParcelService, + useValue: mockNoiParcelService, + }, + { + provide: NoticeOfIntentOwnerService, + useValue: mockNoinOwnerService, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockNoiDocumentService, + }, + ], + }).compile(); + + mockSubmissionStatus = new NoticeOfIntentSubmissionToSubmissionStatus({ + statusTypeCode: SUBMISSION_STATUS.IN_REVIEW_BY_ALC, + submissionUuid: 'fake', + }); + + service = module.get( + GenerateNoiSubmissionDocumentService, + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should call cdogs service to generate pdf for pfrs', async () => { + mockCdogsService.generateDocument.mockResolvedValue({} as any); + + mockNoiSubmissionService.getByFileNumber.mockResolvedValue( + new NoticeOfIntentSubmission({ + fileNumber: 'fake', + localGovernmentUuid: 'fake-lg', + typeCode: 'PFRS', + status: mockSubmissionStatus, + soilProposedStructures: [], + }), + ); + mockNoiDocumentService.list.mockResolvedValue([]); + mockNoiService.getByFileNumber.mockResolvedValue( + new NoticeOfIntent({ + type: new NoticeOfIntentType({ portalLabel: 'fake-label' }), + }), + ); + mockNoiParcelService.fetchByFileId.mockResolvedValue([]); + mockNoinOwnerService.fetchByApplicationFileId.mockResolvedValue([]); + const user = { user: { entity: 'Bruce' } }; + const userEntity = new User({ + name: user.user.entity, + }); + + const res = await service.generate('fake', userEntity); + + expect(mockCdogsService.generateDocument).toBeCalledTimes(1); + expect(mockNoiLocalGovernmentService.getByUuid).toBeCalledTimes(1); + expect(res).toBeDefined(); + }); + + it('should fail if wrong submission type used', async () => { + mockCdogsService.generateDocument.mockResolvedValue({} as any); + + mockNoiSubmissionService.getByFileNumber.mockResolvedValue( + new NoticeOfIntentSubmission({ + fileNumber: 'fake', + localGovernmentUuid: 'fake-lg', + typeCode: 'not a type', + status: mockSubmissionStatus, + soilProposedStructures: [], + }), + ); + mockNoiDocumentService.list.mockResolvedValue([]); + mockNoiService.getByFileNumber.mockResolvedValue( + new NoticeOfIntent({ + type: new NoticeOfIntentType({ portalLabel: 'fake-label' }), + }), + ); + mockNoiParcelService.fetchByFileId.mockResolvedValue([]); + mockNoinOwnerService.fetchByApplicationFileId.mockResolvedValue([]); + const user = { user: { entity: 'Bruce' } }; + const userEntity = new User({ + name: user.user.entity, + }); + + const res = await service.generate('fake', userEntity); + + expect(res).toBeUndefined(); + expect(mockCdogsService.generateDocument).toBeCalledTimes(0); + }); + + it('should clear visibility for existing submissions on update', async () => { + mockCdogsService.generateDocument.mockResolvedValue({ + status: HttpStatus.OK, + data: [], + } as any); + + mockNoiSubmissionService.getByFileNumber.mockResolvedValue( + new NoticeOfIntentSubmission({ + fileNumber: 'fake', + localGovernmentUuid: 'fake-lg', + typeCode: 'PFRS', + status: mockSubmissionStatus, + soilProposedStructures: [], + }), + ); + mockNoiDocumentService.list.mockResolvedValue([ + new NoticeOfIntentDocument({ + typeCode: DOCUMENT_TYPE.ORIGINAL_SUBMISSION, + document: new Document({ + source: DOCUMENT_SOURCE.APPLICANT, + fileName: 'Cats', + }), + }), + new NoticeOfIntentDocument({ document: new Document() }), + ]); + mockNoiService.getByFileNumber.mockResolvedValue( + new NoticeOfIntent({ + type: new NoticeOfIntentType({ portalLabel: 'fake-label' }), + }), + ); + mockNoiParcelService.fetchByFileId.mockResolvedValue([]); + mockNoinOwnerService.fetchByApplicationFileId.mockResolvedValue([]); + const user = { user: { entity: 'Bruce' } }; + const userEntity = new User({ + name: user.user.entity, + }); + mockNoiDocumentService.update.mockResolvedValue( + new NoticeOfIntentDocument(), + ); + mockNoiDocumentService.attachDocumentAsBuffer.mockResolvedValue( + new NoticeOfIntentDocument(), + ); + + await service.generateUpdate('fake', userEntity); + + expect(mockCdogsService.generateDocument).toBeCalledTimes(1); + expect(mockNoiDocumentService.update).toHaveBeenCalledTimes(1); + expect( + mockNoiDocumentService.update.mock.calls[0][0].visibilityFlags, + ).toEqual([]); + expect(mockNoiLocalGovernmentService.getByUuid).toBeCalledTimes(1); + expect(mockNoiDocumentService.attachDocumentAsBuffer).toHaveBeenCalledTimes( + 1, + ); + }); +}); diff --git a/services/apps/alcs/src/portal/pdf-generation/generate-noi-submission-document.service.ts b/services/apps/alcs/src/portal/pdf-generation/generate-noi-submission-document.service.ts new file mode 100644 index 0000000000..91afa395b9 --- /dev/null +++ b/services/apps/alcs/src/portal/pdf-generation/generate-noi-submission-document.service.ts @@ -0,0 +1,380 @@ +import { CdogsService } from '@app/common/cdogs/cdogs.service'; +import { + forwardRef, + HttpStatus, + Inject, + Injectable, + Logger, +} from '@nestjs/common'; +import * as config from 'config'; +import * as dayjs from 'dayjs'; +import { VISIBILITY_FLAG } from '../../alcs/application/application-document/application-document.entity'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +import { NoticeOfIntentDocument } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; +import { NoticeOfIntentDocumentService } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentService } from '../../alcs/notice-of-intent/notice-of-intent.service'; +import { OWNER_TYPE } from '../../common/owner-type/owner-type.entity'; +import { DOCUMENT_TYPE } from '../../document/document-code.entity'; +import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM } from '../../document/document.dto'; +import { User } from '../../user/user.entity'; +import { formatBooleanToYesNoString } from '../../utils/boolean-formatter'; +import { NoticeOfIntentOwnerService } from '../notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentParcel } from '../notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity'; +import { NoticeOfIntentParcelService } from '../notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { NoticeOfIntentSubmission } from '../notice-of-intent-submission/notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission/notice-of-intent-submission.service'; + +export enum NOI_SUBMISSION_TYPES { + POFO = 'POFO', + ROSO = 'ROSO', + PFRS = 'PFRS', +} + +class PdfTemplate { + templateName: string; + payload: any; +} + +const NO_DATA = 'No Data'; +const NOT_APPLICABLE = 'Not Applicable'; + +@Injectable() +export class GenerateNoiSubmissionDocumentService { + private logger = new Logger(GenerateNoiSubmissionDocumentService.name); + + constructor( + private documentGenerationService: CdogsService, + @Inject(forwardRef(() => NoticeOfIntentSubmissionService)) + private noiSubmissionService: NoticeOfIntentSubmissionService, + private localGovernmentService: LocalGovernmentService, + private noticeOfIntentService: NoticeOfIntentService, + @Inject(forwardRef(() => NoticeOfIntentParcelService)) + private parcelService: NoticeOfIntentParcelService, + @Inject(forwardRef(() => NoticeOfIntentOwnerService)) + private ownerService: NoticeOfIntentOwnerService, + private noiDocumentService: NoticeOfIntentDocumentService, + ) {} + + async generate(fileNumber: string, user: User) { + const submission = await this.noiSubmissionService.getByFileNumber( + fileNumber, + user, + ); + + const template = await this.getPdfTemplateBySubmissionType(submission); + + if (template) { + return await this.documentGenerationService.generateDocument( + `${fileNumber}_submission_Date_Time`, + `${config.get('CDOGS.TEMPLATE_FOLDER')}/noi-submissions/${ + template.templateName + }`, + template.payload, + ); + } + return; + } + + async generateAndAttach(fileNumber: string, user: User) { + const generatedRes = await this.generate(fileNumber, user); + + if (generatedRes && generatedRes.status === HttpStatus.OK) { + await this.noiDocumentService.attachDocumentAsBuffer({ + fileNumber: fileNumber, + fileName: `${fileNumber}_APP_Submission.pdf`, + user: user, + file: generatedRes.data, + mimeType: 'application/pdf', + fileSize: generatedRes.data.length, + documentType: DOCUMENT_TYPE.ORIGINAL_SUBMISSION, + source: DOCUMENT_SOURCE.APPLICANT, + system: DOCUMENT_SYSTEM.PORTAL, + visibilityFlags: [ + VISIBILITY_FLAG.APPLICANT, + VISIBILITY_FLAG.COMMISSIONER, + VISIBILITY_FLAG.GOVERNMENT, + ], + }); + } + } + + async generateUpdate(fileNumber: string, user: User) { + const generatedRes = await this.generate(fileNumber, user); + + if (generatedRes && generatedRes.status === HttpStatus.OK) { + const documents = await this.noiDocumentService.list(fileNumber); + + const submissionDocuments = documents.filter( + (document) => + [DOCUMENT_SOURCE.APPLICANT, DOCUMENT_SOURCE.ALC].includes( + document.document.source as DOCUMENT_SOURCE, + ) && + [ + DOCUMENT_TYPE.ORIGINAL_SUBMISSION, + DOCUMENT_TYPE.UPDATED_SUBMISSION, + ].includes(document.typeCode as DOCUMENT_TYPE), + ); + + //Clear Visibility Flags of existing documents + for (const submissionDocument of submissionDocuments) { + await this.noiDocumentService.update({ + uuid: submissionDocument.uuid, + visibilityFlags: [], + user, + source: submissionDocument.document.source as DOCUMENT_SOURCE, + fileName: submissionDocument.document.fileName, + documentType: submissionDocument.type?.code as DOCUMENT_TYPE, + }); + } + + await this.noiDocumentService.attachDocumentAsBuffer({ + fileNumber: fileNumber, + fileName: `${fileNumber}_Submission_Updated`, + user: user, + file: generatedRes.data, + mimeType: 'application/pdf', + fileSize: generatedRes.data.length, + documentType: DOCUMENT_TYPE.UPDATED_SUBMISSION, + source: DOCUMENT_SOURCE.ALC, + system: DOCUMENT_SYSTEM.PORTAL, + visibilityFlags: [ + VISIBILITY_FLAG.APPLICANT, + VISIBILITY_FLAG.COMMISSIONER, + VISIBILITY_FLAG.GOVERNMENT, + ], + }); + } + } + + private async getPdfTemplateBySubmissionType( + submission: NoticeOfIntentSubmission, + ): Promise { + const documents = await this.noiDocumentService.list(submission.fileNumber); + + let payload: any = await this.prepareSubmissionPdfData( + submission, + documents, + ); + + switch (submission.typeCode as NOI_SUBMISSION_TYPES) { + case NOI_SUBMISSION_TYPES.ROSO: + payload = this.populateSoilFields(payload, submission, documents); + return { payload, templateName: 'noi-roso-submission-template.docx' }; + case NOI_SUBMISSION_TYPES.POFO: + payload = this.populateSoilFields(payload, submission, documents); + return { payload, templateName: 'noi-pofo-submission-template.docx' }; + case NOI_SUBMISSION_TYPES.PFRS: + payload = this.populateSoilFields(payload, submission, documents); + return { payload, templateName: 'noi-pfrs-submission-template.docx' }; + default: + this.logger.error( + `Could not find template for application submission type ${submission.typeCode}`, + ); + return; + } + } + + private async prepareSubmissionPdfData( + submission: NoticeOfIntentSubmission, + documents: NoticeOfIntentDocument[], + ) { + const noticeOfIntent = await this.noticeOfIntentService.getByFileNumber( + submission.fileNumber, + ); + + let localGovernment: LocalGovernment | undefined; + if (submission.localGovernmentUuid) { + localGovernment = await this.localGovernmentService.getByUuid( + submission.localGovernmentUuid, + ); + } + + const parcels = await this.parcelService.fetchByFileId( + submission.fileNumber, + ); + + const owners = await this.ownerService.fetchByApplicationFileId( + submission.fileNumber, + ); + + const primaryContact = owners.find( + (e) => e.uuid === submission.primaryContactOwnerUuid, + ); + + const otherDocuments = documents.filter( + (e) => + !e.typeCode || + [ + DOCUMENT_TYPE.PHOTOGRAPH, + DOCUMENT_TYPE.PROFESSIONAL_REPORT, + DOCUMENT_TYPE.OTHER, + ].includes((e.typeCode ?? 'undefined') as DOCUMENT_TYPE), + ); + + const proposalMap = documents.filter( + (document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP, + ); + + const data = { + noData: NO_DATA, + generatedDateTime: dayjs + .tz(new Date(), 'Canada/Pacific') + .format('MMM DD, YYYY hh:mm:ss Z'), + purpose: submission.purpose, + fileNumber: submission.fileNumber, + localGovernment: localGovernment?.name, + status: submission.status.statusType, + applicant: submission.applicant, + selectedThirdPartyAgent: primaryContact?.type.code === OWNER_TYPE.AGENT, + primaryContactFirstName: primaryContact?.firstName, + primaryContactLastName: primaryContact?.lastName, + primaryContactOrganizationName: primaryContact?.organizationName, + primaryContactEmail: primaryContact?.email, + primaryContactPhoneNumber: primaryContact?.phoneNumber, + primaryContactType: primaryContact?.type?.label, + organizationText: + primaryContact?.type.code === OWNER_TYPE.CROWN + ? 'Department' + : 'Organization (If Applicable)', + isGovernmentSetup: + !localGovernment || localGovernment.bceidBusinessGuid !== null, + + // Land use + parcelsAgricultureDescription: submission.parcelsAgricultureDescription, + parcelsAgricultureImprovementDescription: + submission.parcelsAgricultureImprovementDescription, + parcelsNonAgricultureUseDescription: + submission.parcelsNonAgricultureUseDescription, + northLandUseType: submission.northLandUseType, + northLandUseTypeDescription: submission.northLandUseTypeDescription, + eastLandUseType: submission.eastLandUseType, + eastLandUseTypeDescription: submission.eastLandUseTypeDescription, + southLandUseType: submission.southLandUseType, + southLandUseTypeDescription: submission.southLandUseTypeDescription, + westLandUseType: submission.westLandUseType, + westLandUseTypeDescription: submission.westLandUseTypeDescription, + + //Common File Types + proposalMap: proposalMap.find((d) => d)?.document.fileName, + + // Other attachments + otherAttachments: otherDocuments.map((e) => ({ + type: e.type?.description ?? '', + description: e.description ?? '', + name: e.document.fileName, + noData: NO_DATA, + })), + + applicationTypePortalLabel: noticeOfIntent?.type.portalLabel, + parcels: this.mapParcelsWithOwners(parcels), + }; + + return data; + } + + private mapParcelsWithOwners(parcels: NoticeOfIntentParcel[]) { + return parcels.map((e) => ({ + ...e, + pid: this.formatPid(e.pid), + noData: NO_DATA, + purchasedDate: e.purchasedDate ? e.purchasedDate : undefined, + certificateOfTitle: e.certificateOfTitle?.document.fileName, + ownershipType: e.ownershipType?.label, + owners: e.owners.map((o) => ({ + ...o, + noData: NO_DATA, + notApplicable: NOT_APPLICABLE, + name: `${o.firstName} ${o.lastName}`, + organizationName: o.organizationName, + corporateSummary: o.corporateSummary?.document.fileName, + })), + })); + } + + private populateSoilFields( + pdfData: any, + submission: NoticeOfIntentSubmission, + documents: NoticeOfIntentDocument[], + ) { + const crossSections = documents.filter( + (document) => document.type?.code === DOCUMENT_TYPE.CROSS_SECTIONS, + ); + + const reclamationPlans = documents.filter( + (document) => document.type?.code === DOCUMENT_TYPE.RECLAMATION_PLAN, + ); + + const noticesOfWork = documents.filter( + (document) => document.type?.code === DOCUMENT_TYPE.NOTICE_OF_WORK, + ); + + const buildingPlans = documents.filter( + (document) => document.type?.code === DOCUMENT_TYPE.BUILDING_PLAN, + ); + + pdfData = { + ...pdfData, + fillProjectDuration: submission.fillProjectDuration, + soilIsFollowUp: formatBooleanToYesNoString(submission.soilIsFollowUp), + soilFollowUpIDs: submission.soilFollowUpIDs, + soilProjectDuration: submission.soilProjectDuration, + soilTypeRemoved: submission.soilTypeRemoved, + soilToRemoveVolume: submission.soilToRemoveVolume, + soilToRemoveArea: submission.soilToRemoveArea, + soilToRemoveMaximumDepth: submission.soilToRemoveMaximumDepth, + soilToRemoveAverageDepth: submission.soilToRemoveAverageDepth, + soilAlreadyRemovedVolume: submission.soilAlreadyRemovedVolume, + soilAlreadyRemovedArea: submission.soilAlreadyRemovedArea, + soilAlreadyRemovedMaximumDepth: submission.soilAlreadyRemovedMaximumDepth, + soilAlreadyRemovedAverageDepth: submission.soilAlreadyRemovedAverageDepth, + + soilToPlaceVolume: submission.soilToPlaceVolume, + soilToPlaceArea: submission.soilToPlaceArea, + soilToPlaceMaximumDepth: submission.soilToPlaceMaximumDepth, + soilToPlaceAverageDepth: submission.soilToPlaceAverageDepth, + soilAlreadyPlacedVolume: submission.soilAlreadyPlacedVolume, + soilAlreadyPlacedArea: submission.soilAlreadyPlacedArea, + soilAlreadyPlacedMaximumDepth: submission.soilAlreadyPlacedMaximumDepth, + soilAlreadyPlacedAverageDepth: submission.soilAlreadyPlacedAverageDepth, + soilFillTypeToPlace: submission.soilFillTypeToPlace, + + crossSections: crossSections.map((d) => d.document), + reclamationPlans: reclamationPlans.map((d) => d.document), + noticesOfWork: noticesOfWork.map((d) => d.document), + buildingPlans: buildingPlans.map((d) => d.document), + soilIsExtractionOrMining: formatBooleanToYesNoString( + submission.soilIsExtractionOrMining, + ), + soilIsAreaWideFilling: formatBooleanToYesNoString( + submission.soilIsAreaWideFilling, + ), + soilHasSubmittedNotice: formatBooleanToYesNoString( + submission.soilHasSubmittedNotice, + ), + soilIsRemovingSoilForNewStructure: formatBooleanToYesNoString( + submission.soilIsRemovingSoilForNewStructure, + ), + soilProposedStructures: submission.soilProposedStructures.map( + (structure, index) => ({ ...structure, index }), + ), + soilStructureFarmUseReason: submission.soilStructureFarmUseReason, + soilStructureResidentialUseReason: + submission.soilStructureResidentialUseReason, + soilAgriParcelActivity: submission.soilAgriParcelActivity, + soilStructureResidentialAccessoryUseReason: + submission.soilStructureResidentialAccessoryUseReason, + soilStructureOtherUseReason: submission.soilStructureOtherUseReason, + }; + + return pdfData; + } + + private formatPid(pid?: string | null) { + const matches = pid?.match(/(.{1,3})/g); + if (matches) { + return matches.join('-'); + } + return undefined; + } +} diff --git a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts index 767e5c715e..41d0c437ae 100644 --- a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts +++ b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts @@ -179,6 +179,9 @@ export class GenerateSubmissionDocumentService { case APPLICATION_SUBMISSION_TYPES.NFUP: payload = this.populateNfuData(payload, submission); return { payload, templateName: 'nfu-submission-template.docx' }; + case APPLICATION_SUBMISSION_TYPES.NARU: + payload = this.populateNaruData(payload, submission); + return { payload, templateName: 'naru-submission-template.docx' }; case APPLICATION_SUBMISSION_TYPES.TURP: payload = this.populateTurData(payload, submission, documents); return { payload, templateName: 'tur-submission-template.docx' }; @@ -365,6 +368,34 @@ export class GenerateSubmissionDocumentService { }; } + private populateNaruData(pdfData: any, submission: ApplicationSubmission) { + return { + ...pdfData, + naruSubtypeLabel: submission.naruSubtype?.label, + naruSubtypeCode: submission.naruSubtypeCode, + naruFloorArea: submission.naruFloorArea, + naruResidenceNecessity: submission.naruResidenceNecessity, + naruLocationRationale: submission.naruLocationRationale, + naruInfrastructure: submission.naruInfrastructure, + naruExistingStructures: submission.naruExistingStructures, + naruSleepingUnits: submission.naruSleepingUnits, + naruAgriTourism: submission.naruAgriTourism, + showImportFill: submission.naruWillImportFill, + naruWillImportFill: formatBooleanToYesNoString( + submission.naruWillImportFill, + ), + + // NFU Proposal => Soil and Fill + naruFillType: submission.naruFillType, + naruFillOrigin: submission.naruFillOrigin, + naruProjectDuration: submission.naruProjectDuration, + naruToPlaceMaximumDepth: submission.naruToPlaceMaximumDepth, + naruToPlaceAverageDepth: submission.naruToPlaceAverageDepth, + naruToPlaceVolume: submission.naruToPlaceVolume, + naruToPlaceArea: submission.naruToPlaceArea, + }; + } + private populateTurData( pdfData: any, submission: ApplicationSubmission, diff --git a/services/apps/alcs/src/portal/pdf-generation/pdf-generation.controller.spec.ts b/services/apps/alcs/src/portal/pdf-generation/pdf-generation.controller.spec.ts index a5cb0e14cc..9503c28c23 100644 --- a/services/apps/alcs/src/portal/pdf-generation/pdf-generation.controller.spec.ts +++ b/services/apps/alcs/src/portal/pdf-generation/pdf-generation.controller.spec.ts @@ -4,6 +4,7 @@ import { FastifyReply } from 'fastify'; import { ClsService } from 'nestjs-cls'; import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; import { User } from '../../user/user.entity'; +import { GenerateNoiSubmissionDocumentService } from './generate-noi-submission-document.service'; import { GenerateReviewDocumentService } from './generate-review-document.service'; import { GenerateSubmissionDocumentService } from './generate-submission-document.service'; import { PdfGenerationController } from './pdf-generation.controller'; @@ -11,10 +12,12 @@ import { PdfGenerationController } from './pdf-generation.controller'; describe('PdfGenerationController', () => { let controller: PdfGenerationController; let mockSubmissionDocumentService: DeepMocked; + let mockNoiSubmissionDocumentService: DeepMocked; let mockReviewDocumentService: DeepMocked; beforeEach(async () => { mockSubmissionDocumentService = createMock(); + mockNoiSubmissionDocumentService = createMock(); mockReviewDocumentService = createMock(); const module: TestingModule = await Test.createTestingModule({ @@ -24,6 +27,10 @@ describe('PdfGenerationController', () => { provide: GenerateSubmissionDocumentService, useValue: mockSubmissionDocumentService, }, + { + provide: GenerateNoiSubmissionDocumentService, + useValue: mockNoiSubmissionDocumentService, + }, { provide: GenerateReviewDocumentService, useValue: mockReviewDocumentService, @@ -61,6 +68,24 @@ describe('PdfGenerationController', () => { expect(mockResponse.send).toBeCalledWith('fake'); }); + it('should successfully call service for generateNoiSubmission', async () => { + mockNoiSubmissionDocumentService.generate.mockResolvedValue({ + data: 'fake', + } as any); + + const mockResponse = createMock>(); + + await controller.generateNoiSubmission(mockResponse, 'fake-id', { + user: { + entity: new User(), + }, + }); + + expect(mockNoiSubmissionDocumentService.generate).toBeCalledTimes(1); + expect(mockResponse.type).toBeCalledWith('application/pdf'); + expect(mockResponse.send).toBeCalledWith('fake'); + }); + it('should successfully call service for generateReview', async () => { mockReviewDocumentService.generate.mockResolvedValue({ data: 'fake', diff --git a/services/apps/alcs/src/portal/pdf-generation/pdf-generation.controller.ts b/services/apps/alcs/src/portal/pdf-generation/pdf-generation.controller.ts index d488a8ab44..d6a462a142 100644 --- a/services/apps/alcs/src/portal/pdf-generation/pdf-generation.controller.ts +++ b/services/apps/alcs/src/portal/pdf-generation/pdf-generation.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Param, Req, Res, UseGuards } from '@nestjs/common'; import { FastifyReply } from 'fastify'; import { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.service'; import { User } from '../../user/user.entity'; +import { GenerateNoiSubmissionDocumentService } from './generate-noi-submission-document.service'; import { GenerateReviewDocumentService } from './generate-review-document.service'; import { GenerateSubmissionDocumentService } from './generate-submission-document.service'; @@ -10,6 +11,7 @@ import { GenerateSubmissionDocumentService } from './generate-submission-documen export class PdfGenerationController { constructor( private submissionDocumentService: GenerateSubmissionDocumentService, + private noiSubmissionDocumentService: GenerateNoiSubmissionDocumentService, private reviewDocumentService: GenerateReviewDocumentService, ) {} @@ -31,6 +33,24 @@ export class PdfGenerationController { } } + @Get(':fileNumber/noi-submission') + async generateNoiSubmission( + @Res() resp: FastifyReply, + @Param('fileNumber') fileNumber: string, + @Req() req, + ) { + const user = req.user.entity as User; + const result = await this.noiSubmissionDocumentService.generate( + fileNumber, + user, + ); + + if (result) { + resp.type('application/pdf'); + resp.send(result.data); + } + } + @Get(':fileNumber/review') async generateReview( @Res() resp: FastifyReply, diff --git a/services/apps/alcs/src/portal/pdf-generation/pdf-generation.module.ts b/services/apps/alcs/src/portal/pdf-generation/pdf-generation.module.ts index 7bc26faea1..0226a1e1cf 100644 --- a/services/apps/alcs/src/portal/pdf-generation/pdf-generation.module.ts +++ b/services/apps/alcs/src/portal/pdf-generation/pdf-generation.module.ts @@ -1,10 +1,13 @@ import { CdogsModule } from '@app/common/cdogs/cdogs.module'; import { forwardRef, Module } from '@nestjs/common'; import { ApplicationModule } from '../../alcs/application/application.module'; +import { NoticeOfIntentModule } from '../../alcs/notice-of-intent/notice-of-intent.module'; import { NotificationModule } from '../../alcs/notification/notification.module'; import { ApplicationSubmissionReviewModule } from '../application-submission-review/application-submission-review.module'; import { ApplicationSubmissionModule } from '../application-submission/application-submission.module'; +import { NoticeOfIntentSubmissionModule } from '../notice-of-intent-submission/notice-of-intent-submission.module'; import { NotificationSubmissionModule } from '../notification-submission/notification-submission.module'; +import { GenerateNoiSubmissionDocumentService } from './generate-noi-submission-document.service'; import { GenerateReviewDocumentService } from './generate-review-document.service'; import { GenerateSrwDocumentService } from './generate-srw-document.service'; import { GenerateSubmissionDocumentService } from './generate-submission-document.service'; @@ -18,14 +21,18 @@ import { PdfGenerationController } from './pdf-generation.controller'; forwardRef(() => ApplicationSubmissionReviewModule), forwardRef(() => NotificationModule), forwardRef(() => NotificationSubmissionModule), + forwardRef(() => NoticeOfIntentModule), + forwardRef(() => NoticeOfIntentSubmissionModule), ], providers: [ GenerateSubmissionDocumentService, GenerateReviewDocumentService, GenerateSrwDocumentService, + GenerateNoiSubmissionDocumentService, ], controllers: [PdfGenerationController], exports: [ + GenerateNoiSubmissionDocumentService, GenerateSubmissionDocumentService, GenerateReviewDocumentService, GenerateSrwDocumentService, diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1706056918403-remove_dupe_govs.ts b/services/apps/alcs/src/providers/typeorm/migrations/1706056918403-remove_dupe_govs.ts new file mode 100644 index 0000000000..67a6743a65 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1706056918403-remove_dupe_govs.ts @@ -0,0 +1,44 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveDupeGovs1706056918403 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + //City of Port Alberni + await queryRunner.query(` + DELETE FROM "alcs"."local_government" WHERE "uuid" = 'b8a622d7-66da-4598-9687-2e8727fb2561'; + `); + + //District of Barriere + await queryRunner.query(` + DELETE FROM "alcs"."local_government" WHERE "uuid" = 'd3f74ba5-0dc3-43ea-a7e2-004882d65c33'; + `); + + //District of Fort St. James + await queryRunner.query(` + DELETE FROM "alcs"."local_government" WHERE "uuid" = 'e0684ddf-fa61-47c2-87b3-1c72bed5c365'; + `); + + //District of Hudson’s Hope + await queryRunner.query(` + DELETE FROM "alcs"."local_government" WHERE "uuid" = '79c3e16e-fdb9-4bb0-a427-f044a5d4bb95'; + `); + + //Northern Rockies (Historical) + await queryRunner.query(` + DELETE FROM "alcs"."local_government" WHERE "uuid" = '02afc23a-088c-4c09-aa69-b5c6c516cabc'; + `); + + //Village of Canal Flats + await queryRunner.query(` + DELETE FROM "alcs"."local_government" WHERE "uuid" = '6c0079df-2647-445f-9718-4433e9761eac'; + `); + + //Village of Port Clements + await queryRunner.query(` + DELETE FROM "alcs"."local_government" WHERE "uuid" = '18bd941a-7c0f-44fe-9d12-a084d2a5f372'; + `); + } + + public async down(): Promise { + //No + } +} diff --git a/services/templates/pdf/noi-submissions/noi-pfrs-submission-template.docx b/services/templates/pdf/noi-submissions/noi-pfrs-submission-template.docx new file mode 100644 index 0000000000..e518388aa6 Binary files /dev/null and b/services/templates/pdf/noi-submissions/noi-pfrs-submission-template.docx differ diff --git a/services/templates/pdf/noi-submissions/noi-pofo-submission-template.docx b/services/templates/pdf/noi-submissions/noi-pofo-submission-template.docx new file mode 100644 index 0000000000..4f49ab16c4 Binary files /dev/null and b/services/templates/pdf/noi-submissions/noi-pofo-submission-template.docx differ diff --git a/services/templates/pdf/noi-submissions/noi-roso-submission-template.docx b/services/templates/pdf/noi-submissions/noi-roso-submission-template.docx new file mode 100644 index 0000000000..bd285fd098 Binary files /dev/null and b/services/templates/pdf/noi-submissions/noi-roso-submission-template.docx differ diff --git a/services/templates/pdf/submissions/naru-submission-template.docx b/services/templates/pdf/submissions/naru-submission-template.docx new file mode 100644 index 0000000000..9dca6d4062 Binary files /dev/null and b/services/templates/pdf/submissions/naru-submission-template.docx differ diff --git a/services/templates/pdf/submissions/pfrs-submission-template.docx b/services/templates/pdf/submissions/pfrs-submission-template.docx index d183d3adc7..55725f4462 100644 Binary files a/services/templates/pdf/submissions/pfrs-submission-template.docx and b/services/templates/pdf/submissions/pfrs-submission-template.docx differ diff --git a/services/templates/pdf/submissions/subd-submission-template.docx b/services/templates/pdf/submissions/subd-submission-template.docx index ba97237c73..e8cee18015 100644 Binary files a/services/templates/pdf/submissions/subd-submission-template.docx and b/services/templates/pdf/submissions/subd-submission-template.docx differ