Skip to content

Commit 9f24031

Browse files
whabanksCopilot
andauthored
Task/delete a service (#2682)
* Notify all team members when service is deleted * Add migration for delete a service template * Fix migration * Fix tests * Update FR translation Co-authored-by: Copilot <[email protected]> * Add exception handling during service archival * Fix test caused by silly mistake * pseduo-FF deactivation email sending - Do not send service deactivated confirmation email if we're in prod --------- Co-authored-by: Copilot <[email protected]>
1 parent 5569d62 commit 9f24031

File tree

7 files changed

+173
-7
lines changed

7 files changed

+173
-7
lines changed

.vscode/launch.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,12 @@
101101
"jinja": true,
102102
"justMyCode": false
103103
}
104+
],
105+
"compounds": [
106+
{
107+
"name": "API/Celery",
108+
"configurations": ["Python: Flask", "Python: Celery"],
109+
"stopAll": true
110+
}
104111
]
105112
}

app/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ class Config(object):
330330
"65bbee1b-9c2a-48a6-b95a-d7d70e8f6726" # Sent when a service is deactivated due to a user being deactivated
331331
)
332332
USER_DEACTIVATED_TEMPLATE_ID = "d0fe2b8c-ddcf-4f9b-8bb7-d79006e7cfa7" # Sent when a user deactivates their own account
333+
SERVICE_DEACTIVATED_TEMPLATE_ID = "71263145-8606-43b0-9f42-08a2c227523a" # Sent when a user deactivates a service
333334

334335
# Templates for annual limits
335336
REACHED_ANNUAL_LIMIT_TEMPLATE_ID = "ca6d9205-d923-4198-acdd-d0aa37725c37"

app/dao/dao_utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def version_class(*version_options):
3636
def versioned(func):
3737
@wraps(func)
3838
def record_version(*args, **kwargs):
39-
func(*args, **kwargs)
39+
result = func(*args, **kwargs)
4040

4141
session_objects = []
4242

@@ -62,6 +62,8 @@ def record_version(*args, **kwargs):
6262
for session_object, history_class in session_objects:
6363
db.session.add(create_history(session_object, history_cls=history_class))
6464

65+
return result
66+
6567
return record_version
6668

6769
return versioned

app/dao/services_dao.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ def dao_archive_service(service_id):
250250
atomicity.
251251
"""
252252

253-
dao_archive_service_no_transaction(service_id)
253+
return dao_archive_service_no_transaction(service_id)
254254

255255

256256
@version_class(
@@ -283,6 +283,7 @@ def dao_archive_service_no_transaction(service_id):
283283
.filter(Service.id == service_id)
284284
.one()
285285
)
286+
original_service_name = service.name
286287

287288
time = datetime.utcnow().strftime("%Y-%m-%d_%H:%M:%S")
288289
service.active = False
@@ -297,6 +298,8 @@ def dao_archive_service_no_transaction(service_id):
297298
if not api_key.expiry_date:
298299
api_key.expiry_date = datetime.utcnow()
299300

301+
return original_service_name
302+
300303

301304
def dao_fetch_service_by_id_and_user(service_id, user_id):
302305
return Service.query.filter(Service.users.any(id=user_id), Service.id == service_id).options(joinedload("users")).one()

app/service/rest.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
)
1414
from psycopg2.errors import UniqueViolation
1515
from sqlalchemy import func
16-
from sqlalchemy.exc import IntegrityError
16+
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
1717
from sqlalchemy.orm.exc import NoResultFound
1818

1919
from app import redis_store, salesforce_client
@@ -778,10 +778,29 @@ def archive_service(service_id):
778778
:param service_id:
779779
:return:
780780
"""
781-
service = dao_fetch_service_by_id(service_id)
781+
service: Service = dao_fetch_service_by_id(service_id)
782782

783783
if service.active:
784-
dao_archive_service(service.id)
784+
try:
785+
service_name = dao_archive_service(service.id)
786+
# FF_USER_SERVICE_DEACTIVATION
787+
if current_app.config["NOTIFY_ENVIRONMENT"].lower() != "production":
788+
send_notification_to_service_users(
789+
service_id=service_id,
790+
template_id=current_app.config["SERVICE_DEACTIVATED_TEMPLATE_ID"],
791+
personalisation={
792+
"service_name": service_name,
793+
},
794+
)
795+
except SQLAlchemyError as e:
796+
current_app.logger.exception(e)
797+
raise InvalidRequest(
798+
f"A dao error occurred while archiving service {service_id}. Deactivation confirmation emails were not sent to service users",
799+
status_code=500,
800+
)
801+
except Exception as e:
802+
current_app.logger.exception(e)
803+
785804
if current_app.config["FF_SALESFORCE_CONTACT"]:
786805
try:
787806
salesforce_client.engagement_close(service)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""
2+
3+
Revision ID: 0492_add_service_del_template
4+
Revises: 0491_split_deactivate_templates
5+
Create Date: 2025-10-21 00:00:00
6+
7+
"""
8+
from datetime import datetime
9+
10+
from alembic import op
11+
from flask import current_app
12+
13+
revision = "0492_add_service_del_template"
14+
down_revision = "0491_split_deactivate_templates"
15+
16+
service_deactivated_template_id = current_app.config["SERVICE_DEACTIVATED_TEMPLATE_ID"]
17+
template_ids = [service_deactivated_template_id]
18+
19+
def upgrade():
20+
template_insert = """
21+
INSERT INTO templates (id, name, template_type, created_at, content, archived, service_id, subject,
22+
created_by_id, version, process_type, hidden)
23+
VALUES ('{}', '{}', '{}', '{}', '{}', False, '{}', '{}', '{}', 1, '{}', false)
24+
"""
25+
template_history_insert = """
26+
INSERT INTO templates_history (id, name, template_type, created_at, content, archived, service_id, subject,
27+
created_by_id, version, process_type, hidden)
28+
VALUES ('{}', '{}', '{}', '{}', '{}', False, '{}', '{}', '{}', 1, '{}', false)
29+
"""
30+
31+
service_suspended_content = "\n".join(
32+
[
33+
"[[fr]](la version française suit)[[/fr]]",
34+
"",
35+
"[[en]]",
36+
"You or one of your team members has deleted ((service_name)).",
37+
"",
38+
"You cannot:",
39+
"- Access, manage or make changes to ((service_name)).",
40+
"- Send messages from ((service_name)).",
41+
"",
42+
"If no one on your team deleted this service, immediately [contact us](https://notification.canada.ca/en/contact).",
43+
"",
44+
"The GC Notify Team",
45+
"[[/en]]",
46+
"",
47+
"---",
48+
"",
49+
"[[fr]]",
50+
"Vous ou un membre de votre équipe avez supprimé le service ((service_name)).",
51+
"",
52+
"Vous ne pouvez plus :",
53+
"- Accéder, gérer ou apporter des changements au service ((service_name)).",
54+
"- Envoyer des messages provenant du service ((service_name)).",
55+
"",
56+
"Si vous ni votre équipe n’avez demandé de supprimer ce service, contactez-nous immédiatement.",
57+
"",
58+
"L’équipe Notification GC",
59+
"[[/fr]]",
60+
]
61+
)
62+
63+
templates = [
64+
{
65+
"id": service_deactivated_template_id,
66+
"name": "Service deleted",
67+
"subject": "((service_name)) deleted | ((service_name)) supprimé",
68+
"content": service_suspended_content,
69+
"template_type": "email",
70+
"process_type": "normal",
71+
},
72+
]
73+
74+
for template in templates:
75+
op.execute(
76+
template_insert.format(
77+
template["id"],
78+
template["name"],
79+
template["template_type"],
80+
datetime.utcnow(),
81+
template["content"],
82+
current_app.config["NOTIFY_SERVICE_ID"],
83+
template["subject"],
84+
current_app.config["NOTIFY_USER_ID"],
85+
template["process_type"],
86+
)
87+
)
88+
89+
op.execute(
90+
template_history_insert.format(
91+
template["id"],
92+
template["name"],
93+
template["template_type"],
94+
datetime.utcnow(),
95+
template["content"],
96+
current_app.config["NOTIFY_SERVICE_ID"],
97+
template["subject"],
98+
current_app.config["NOTIFY_USER_ID"],
99+
template["process_type"],
100+
)
101+
)
102+
103+
def downgrade():
104+
for template_id in template_ids:
105+
op.execute("DELETE FROM notifications WHERE template_id = '{}'".format(template_id))
106+
op.execute("DELETE FROM notification_history WHERE template_id = '{}'".format(template_id))
107+
op.execute("DELETE FROM template_redacted WHERE template_id = '{}'".format(template_id))
108+
op.execute("DELETE FROM templates_history WHERE id = '{}'".format(template_id))
109+
op.execute("DELETE FROM templates WHERE id = '{}'".format(template_id))
110+

tests/app/service/test_archived_service.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from datetime import datetime
33

44
import pytest
5+
from flask import current_app
56
from freezegun import freeze_time
67

78
from app import db
@@ -25,6 +26,27 @@ def test_archive_service_errors_with_bad_service_id(client, notify_db_session):
2526
assert response.status_code == 404
2627

2728

29+
def test_archiving_service_sends_deletion_email_to_all_users(client, sample_service, mocker):
30+
"""Test that archiving a service sends deletion email to all service users"""
31+
# Mock the send_notification_to_service_users function
32+
mock_send_notification = mocker.patch("app.service.rest.send_notification_to_service_users")
33+
service_name = sample_service.name
34+
35+
auth_header = create_authorization_header()
36+
response = client.post("/service/{}/archive".format(sample_service.id), headers=[auth_header])
37+
38+
assert response.status_code == 204
39+
40+
# Verify that send_notification_to_service_users was called with correct parameters
41+
mock_send_notification.assert_called_once_with(
42+
service_id=sample_service.id,
43+
template_id=current_app.config["SERVICE_DEACTIVATED_TEMPLATE_ID"],
44+
personalisation={
45+
"service_name": service_name,
46+
},
47+
)
48+
49+
2850
def test_deactivating_inactive_service_does_nothing(client, sample_service):
2951
auth_header = create_authorization_header()
3052
sample_service.active = False
@@ -35,7 +57,8 @@ def test_deactivating_inactive_service_does_nothing(client, sample_service):
3557

3658
@pytest.fixture
3759
@freeze_time("2018-04-21 14:00")
38-
def archived_service(client, notify_db, sample_service):
60+
def archived_service(client, notify_db, sample_service, mocker):
61+
mocker.patch("app.service.rest.send_notification_to_service_users")
3962
create_template(sample_service, template_name="a")
4063
create_template(sample_service, template_name="b")
4164
create_api_key(sample_service)
@@ -78,7 +101,8 @@ def test_deactivating_service_creates_history(archived_service):
78101

79102

80103
@pytest.fixture
81-
def archived_service_with_deleted_stuff(client, sample_service):
104+
def archived_service_with_deleted_stuff(client, sample_service, mocker):
105+
mocker.patch("app.service.rest.send_notification_to_service_users")
82106
with freeze_time("2001-01-01"):
83107
template = create_template(sample_service, template_name="a")
84108
api_key = create_api_key(sample_service)

0 commit comments

Comments
 (0)