1
- from sqlalchemy import func , text
1
+ from sqlalchemy import case , func , text
2
2
from sqlalchemy .orm import aliased
3
3
4
4
from app import db
5
5
from app .aws .s3 import stream_to_s3
6
- from app .models import Job , Notification , Template , User
6
+ from app .models import EMAIL_STATUS_FORMATTED , SMS_STATUS_FORMATTED , Job , Notification , Template , User
7
7
8
8
FR_TRANSLATIONS = {
9
9
"Recipient" : "Destinataire" ,
14
14
"Job" : "Tâche" ,
15
15
"Status" : "État" ,
16
16
"Sent Time" : "Heure d’envoi" ,
17
+ # notification types
18
+ "email" : "courriel" ,
19
+ "sms" : "sms" ,
20
+ # notification statuses
21
+ "Failed" : "Échec" ,
22
+ "Tech issue" : "Problème technique" ,
23
+ "Content or inbox issue" : "Problème de contenu ou de boîte de réception" ,
24
+ "Attachment has virus" : "La pièce jointe contient un virus" ,
25
+ "Delivered" : "Livraison réussie" ,
26
+ "In transit" : "Envoi en cours" ,
27
+ "Exceeds Protected A" : "Niveau supérieur à Protégé A" ,
28
+ "Carrier issue" : "Problème du fournisseur" ,
29
+ "No such number" : "Numéro inexistant" ,
30
+ "Sent" : "Envoyé" ,
31
+ "Blocked" : "Message bloqué" ,
32
+ "No such address" : "Adresse inexistante" ,
33
+ # "Can't send to this international number": "" # no translation exists for this yet
17
34
}
18
35
19
36
@@ -51,19 +68,19 @@ def build_notifications_query(service_id, notification_type, language, days_limi
51
68
j = aliased (Job )
52
69
u = aliased (User )
53
70
54
- translate = Translate (language ).translate
55
-
56
- # Build the query using SQLAlchemy
57
- return (
71
+ # Build the inner subquery (returns enum values, cast as text for notification_type)
72
+ inner_query = (
58
73
db .session .query (
59
- n .to .label (translate ("Recipient" )),
60
- t .name .label (translate ("Template" )),
61
- n .notification_type .label (translate ("Type" )),
62
- func .coalesce (u .name , "" ).label (translate ("Sent by" )),
63
- func .coalesce (u .email_address , "" ).label (translate ("Sent by email" )),
64
- func .coalesce (j .original_file_name , "" ).label (translate ("Job" )),
65
- n .status .label (translate ("Status" )),
66
- func .to_char (n .created_at , "YYYY-MM-DD HH24:MI:SS" ).label (translate ("Sent Time" )),
74
+ n .to .label ("to" ),
75
+ t .name .label ("template_name" ),
76
+ n .notification_type .cast (db .String ).label ("notification_type" ),
77
+ u .name .label ("user_name" ),
78
+ u .email_address .label ("user_email" ),
79
+ j .original_file_name .label ("job_name" ),
80
+ n .status .label ("status" ),
81
+ n .created_at .label ("created_at" ),
82
+ n .feedback_subtype .label ("feedback_subtype" ),
83
+ n .feedback_reason .label ("feedback_reason" ),
67
84
)
68
85
.join (t , t .id == n .template_id )
69
86
.outerjoin (j , j .id == n .job_id )
@@ -74,6 +91,63 @@ def build_notifications_query(service_id, notification_type, language, days_limi
74
91
n .created_at > func .now () - text (f"interval '{ days_limit } days'" ),
75
92
)
76
93
.order_by (n .created_at .desc ())
94
+ .subquery ()
95
+ )
96
+
97
+ # Map statuses for translation
98
+ translate = Translate (language ).translate
99
+ # Provider-failure logic for email
100
+ provider_failure_email = case (
101
+ [(inner_query .c .feedback_subtype .in_ (["suppressed" , "on-account-suppression-list" ]), "Blocked" )], else_ = "No such address"
102
+ )
103
+ # Provider-failure logic for sms
104
+ provider_failure_sms = case (
105
+ [
106
+ (
107
+ inner_query .c .feedback_reason .in_ (["NO_ORIGINATION_IDENTITIES_FOUND" , "DESTINATION_COUNTRY_BLOCKED" ]),
108
+ "Can't send to this international number" ,
109
+ )
110
+ ],
111
+ else_ = "No such number" ,
112
+ )
113
+
114
+ email_status_cases = [(inner_query .c .status == k , translate (v )) for k , v in EMAIL_STATUS_FORMATTED .items ()]
115
+ sms_status_cases = [(inner_query .c .status == k , translate (v )) for k , v in SMS_STATUS_FORMATTED .items ()]
116
+ # Add provider-failure logic
117
+ if notification_type == "email" :
118
+ email_status_cases .append ((inner_query .c .status == "provider-failure" , translate (provider_failure_email )))
119
+ status_expr = case (email_status_cases , else_ = inner_query .c .status )
120
+ elif notification_type == "sms" :
121
+ sms_status_cases .append ((inner_query .c .status == "provider-failure" , translate (provider_failure_sms )))
122
+ status_expr = case (sms_status_cases , else_ = inner_query .c .status )
123
+ else :
124
+ status_expr = inner_query .c .status
125
+ if language == "fr" :
126
+ status_expr = func .coalesce (func .nullif (status_expr , "" ), "" ).label (translate ("Status" ))
127
+ else :
128
+ status_expr = status_expr .label (translate ("Status" ))
129
+
130
+ # Outer query: translate notification_type for display
131
+ notification_type_translated = case (
132
+ [
133
+ (inner_query .c .notification_type == "email" , translate ("email" )),
134
+ (inner_query .c .notification_type == "sms" , translate ("sms" )),
135
+ ],
136
+ else_ = inner_query .c .notification_type ,
137
+ ).label (translate ("Type" ))
138
+
139
+ return db .session .query (
140
+ inner_query .c .to .label (translate ("Recipient" )),
141
+ inner_query .c .template_name .label (translate ("Template" )),
142
+ notification_type_translated ,
143
+ func .coalesce (inner_query .c .user_name , "" ).label (translate ("Sent by" )),
144
+ func .coalesce (inner_query .c .user_email , "" ).label (translate ("Sent by email" )),
145
+ func .coalesce (inner_query .c .job_name , "" ).label (translate ("Job" )),
146
+ status_expr ,
147
+ # Explicitly cast created_at to UTC, then to America/Toronto
148
+ func .to_char (
149
+ func .timezone ("America/Toronto" , func .timezone ("UTC" , inner_query .c .created_at )), "YYYY-MM-DD HH24:MI:SS"
150
+ ).label (translate ("Sent Time" )),
77
151
)
78
152
79
153
0 commit comments