Skip to content

Commit 355187a

Browse files
authored
Merge pull request #2094 from GSA/main
11/12/2025 Production Deploy
2 parents d5c808b + 3ad3b00 commit 355187a

File tree

24 files changed

+1008
-599
lines changed

24 files changed

+1008
-599
lines changed

.github/workflows/deploy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ jobs:
3636
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
3737
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
3838
run: terraform init
39+
3940
- name: Terraform apply
4041
working-directory: terraform/staging
4142
env:

.github/workflows/drift.yml

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -44,44 +44,44 @@ jobs:
4444
exit $exit_code
4545
fi
4646
47-
check_demo_drift:
48-
runs-on: ubuntu-latest
49-
name: Check for drift of demo terraform configuration
50-
environment: demo
51-
steps:
52-
- name: Checkout
53-
uses: actions/checkout@v4
54-
with:
55-
ref: 'production'
47+
# check_demo_drift:
48+
# runs-on: ubuntu-latest
49+
# name: Check for drift of demo terraform configuration
50+
# environment: demo
51+
# steps:
52+
# - name: Checkout
53+
# uses: actions/checkout@v4
54+
# with:
55+
# ref: 'production'
5656

57-
# Looks like we need to install Terraform ourselves now!
58-
# https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
59-
- name: Setup Terraform
60-
uses: hashicorp/setup-terraform@v3
61-
with:
62-
terraform_version: "^1.7.5"
63-
terraform_wrapper: false
57+
# # Looks like we need to install Terraform ourselves now!
58+
# # https://github.com/actions/runner-images/issues/10796#issuecomment-2417064348
59+
# - name: Setup Terraform
60+
# uses: hashicorp/setup-terraform@v3
61+
# with:
62+
# terraform_version: "^1.7.5"
63+
# terraform_wrapper: false
6464

65-
- name: Check for drift
66-
env:
67-
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
68-
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
69-
TF_VAR_cf_user: ${{ secrets.CLOUDGOV_USERNAME }}
70-
TF_VAR_cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
71-
run: |
72-
cd terraform/demo
73-
terraform init
74-
terraform plan -detailed-exitcode
75-
exit_code=$?
76-
if [ $exit_code -eq 0 ]; then
77-
echo "No changes detected. Intrastructure is up-to-date."
78-
elif [ $exit_code -eq 2 ]; then
79-
echo "Changes detected. Infrastructure drift found."
80-
exit 1
81-
else
82-
echo "Error running terraform plan."
83-
exit $exit_code
84-
fi
65+
# - name: Check for drift
66+
# env:
67+
# AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
68+
# AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
69+
# TF_VAR_cf_user: ${{ secrets.CLOUDGOV_USERNAME }}
70+
# TF_VAR_cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
71+
# run: |
72+
# cd terraform/demo
73+
# terraform init
74+
# terraform plan -detailed-exitcode
75+
# exit_code=$?
76+
# if [ $exit_code -eq 0 ]; then
77+
# echo "No changes detected. Intrastructure is up-to-date."
78+
# elif [ $exit_code -eq 2 ]; then
79+
# echo "Changes detected. Infrastructure drift found."
80+
# exit 1
81+
# else
82+
# echo "Error running terraform plan."
83+
# exit $exit_code
84+
# fi
8585

8686
check_prod_drift:
8787
runs-on: ubuntu-latest

.github/workflows/terraform-staging.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ jobs:
4141
id: validation
4242
run: terraform validate -no-color
4343

44+
4445
- name: Terraform plan
4546
id: plan
4647
env:

app/authentication/auth.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
class AuthError(Exception):
3131
def __init__(self, message, code, service_id=None, api_key_id=None):
32+
super().__init__(message, code, service_id, api_key_id)
3233
self.message = {"token": [message]}
3334
self.short_message = message
3435
self.code = code

app/clients/document_download.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
class DocumentDownloadError(Exception):
66
def __init__(self, message, status_code):
7+
super().__init__(message, status_code)
78
self.message = message
89
self.status_code = status_code
910

app/clients/sms/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class SmsClientResponseException(ClientException):
1010
"""
1111

1212
def __init__(self, message):
13+
super().__init__(message)
1314
self.message = message
1415

1516
def __str__(self):

app/dao/fact_billing_dao.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77

88
from app import db
99
from app.dao.date_util import get_calendar_year_dates, get_calendar_year_for_datetime
10-
from app.dao.organization_dao import dao_get_organization_live_services
10+
from app.dao.organization_dao import (
11+
dao_get_organization_live_services,
12+
dao_get_organization_services,
13+
)
1114
from app.enums import KeyType, NotificationStatus, NotificationType
1215
from app.models import (
1316
AnnualBilling,
@@ -613,6 +616,7 @@ def fetch_sms_billing_for_organization(organization_id, financial_year):
613616
func.coalesce(chargeable_sms, 0).label("chargeable_billable_sms"),
614617
func.coalesce(sms_cost, 0).label("sms_cost"),
615618
Service.active,
619+
Service.restricted,
616620
)
617621
.select_from(Service)
618622
.outerjoin(
@@ -695,10 +699,16 @@ def query_organization_sms_usage_for_year(organization_id, year):
695699
)
696700

697701

698-
def fetch_usage_year_for_organization(organization_id, year):
702+
def fetch_usage_year_for_organization(
703+
organization_id, year, include_all_services=False
704+
):
699705
year_start, year_end = get_calendar_year_dates(year)
700706
today = utc_now().date()
701-
services = dao_get_organization_live_services(organization_id)
707+
708+
if include_all_services:
709+
services = dao_get_organization_services(organization_id)
710+
else:
711+
services = dao_get_organization_live_services(organization_id)
702712

703713
# if year end date is less than today, we are calculating for data in the past and have no need for deltas.
704714
if year_end >= today:
@@ -709,7 +719,7 @@ def fetch_usage_year_for_organization(organization_id, year):
709719
service_with_usage = {}
710720
# initialise results
711721
for service in services:
712-
service_with_usage[str(service.id)] = {
722+
service_with_usage[service.id] = {
713723
"service_id": service.id,
714724
"service_name": service.name,
715725
"free_sms_limit": 0,
@@ -719,13 +729,14 @@ def fetch_usage_year_for_organization(organization_id, year):
719729
"sms_cost": 0.0,
720730
"emails_sent": 0,
721731
"active": service.active,
732+
"restricted": service.restricted,
722733
}
723734
sms_usages = fetch_sms_billing_for_organization(organization_id, year)
724735
email_usages = fetch_email_usage_for_organization(
725736
organization_id, year_start, year_end
726737
)
727738
for usage in sms_usages:
728-
service_with_usage[str(usage.service_id)] = {
739+
service_with_usage[usage.service_id] = {
729740
"service_id": usage.service_id,
730741
"service_name": usage.service_name,
731742
"free_sms_limit": usage.free_sms_fragment_limit,
@@ -735,9 +746,10 @@ def fetch_usage_year_for_organization(organization_id, year):
735746
"sms_cost": float(usage.sms_cost),
736747
"emails_sent": 0,
737748
"active": usage.active,
749+
"restricted": usage.restricted,
738750
}
739751
for email_usage in email_usages:
740-
service_with_usage[str(email_usage.service_id)][
752+
service_with_usage[email_usage.service_id][
741753
"emails_sent"
742754
] = email_usage.emails_sent
743755

app/dao/notifications_dao.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@
2727
from app.dao.dao_utils import autocommit
2828
from app.dao.inbound_sms_dao import Pagination
2929
from app.enums import KeyType, NotificationStatus, NotificationType
30-
from app.models import FactNotificationStatus, Notification, NotificationHistory
30+
from app.models import (
31+
FactNotificationStatus,
32+
Notification,
33+
NotificationHistory,
34+
Template,
35+
)
3136
from app.utils import (
3237
escape_special_characters,
3338
get_midnight_in_utc,
@@ -340,6 +345,86 @@ def dao_get_notification_count_for_service_message_ratio(service_id, current_yea
340345
return recent_count + old_count
341346

342347

348+
def dao_get_notification_counts_per_service(service_ids, current_year):
349+
"""
350+
Get notification counts for multiple services in a single organization.
351+
"""
352+
if not service_ids:
353+
return {}
354+
355+
start_date = datetime(current_year, 6, 16)
356+
end_date = datetime(current_year + 1, 6, 16)
357+
358+
stmt1 = (
359+
select(Notification.service_id, func.count().label("count"))
360+
.where(
361+
Notification.service_id.in_(service_ids),
362+
Notification.status
363+
not in [
364+
NotificationStatus.CANCELLED,
365+
NotificationStatus.CREATED,
366+
NotificationStatus.SENDING,
367+
],
368+
Notification.created_at >= start_date,
369+
Notification.created_at < end_date,
370+
)
371+
.group_by(Notification.service_id)
372+
)
373+
374+
stmt2 = (
375+
select(NotificationHistory.service_id, func.count().label("count"))
376+
.where(
377+
NotificationHistory.service_id.in_(service_ids),
378+
NotificationHistory.status
379+
not in [
380+
NotificationStatus.CANCELLED,
381+
NotificationStatus.CREATED,
382+
NotificationStatus.SENDING,
383+
],
384+
NotificationHistory.created_at >= start_date,
385+
NotificationHistory.created_at < end_date,
386+
)
387+
.group_by(NotificationHistory.service_id)
388+
)
389+
390+
result_dict = {}
391+
392+
recent_results = db.session.execute(stmt1).all()
393+
for service_id, count in recent_results:
394+
result_dict[service_id] = count
395+
396+
history_results = db.session.execute(stmt2).all()
397+
for service_id, count in history_results:
398+
result_dict[service_id] = result_dict.get(service_id, 0) + count
399+
400+
return result_dict
401+
402+
403+
def dao_get_recent_sms_template_per_service(service_ids):
404+
405+
if not service_ids:
406+
return {}
407+
408+
stmt = (
409+
select(
410+
Notification.service_id,
411+
Template.name.label("template_name"),
412+
)
413+
.join(Template, Template.id == Notification.template_id)
414+
.where(
415+
Notification.service_id.in_(service_ids),
416+
Notification.notification_type == NotificationType.SMS,
417+
Notification.key_type != KeyType.TEST,
418+
)
419+
.distinct(Notification.service_id)
420+
.order_by(Notification.service_id, desc(Notification.created_at))
421+
)
422+
423+
results = db.session.execute(stmt).all()
424+
425+
return {service_id: template_name for service_id, template_name in results}
426+
427+
343428
def dao_get_failed_notification_count():
344429
stmt = select(func.count(Notification.id)).where(
345430
Notification.status == NotificationStatus.FAILED

app/dao/services_dao.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,21 @@ def dao_fetch_all_services_created_by_user(user_id):
254254
return db.session.execute(stmt).scalars().all()
255255

256256

257+
def dao_get_service_primary_contacts(service_ids):
258+
259+
if not service_ids:
260+
return {}
261+
262+
stmt = select(
263+
Service.id.label("service_id"),
264+
Service.billing_contact_email_addresses.label("email_address"),
265+
).where(Service.id.in_(service_ids))
266+
267+
results = db.session.execute(stmt).all()
268+
269+
return {service_id: email_address for service_id, email_address in results}
270+
271+
257272
@autocommit
258273
@version_class(
259274
VersionOptions(ApiKey, must_write_history=False),

app/errors.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class InvalidRequest(Exception):
1515
fields = []
1616

1717
def __init__(self, message, status_code):
18-
super().__init__()
18+
super().__init__(message, status_code)
1919
self.message = message
2020
self.status_code = status_code
2121

@@ -115,16 +115,20 @@ class TooManyRequestsError(InvalidRequest):
115115
status_code = 429
116116
message_template = "Exceeded send limits ({}) for today"
117117

118-
def __init__(self, sending_limit):
118+
def __init__(self, sending_limit): # noqa: B042
119119
self.message = self.message_template.format(sending_limit)
120+
self.sending_limit = sending_limit
121+
super().__init__(self.message, self.status_code)
120122

121123

122124
class TotalRequestsError(InvalidRequest):
123125
status_code = 429
124126
message_template = "Exceeded total application limits ({}) for today"
125127

126-
def __init__(self, sending_limit):
128+
def __init__(self, sending_limit): # noqa: B042
127129
self.message = self.message_template.format(sending_limit)
130+
self.sending_limit = sending_limit
131+
super().__init__(self.message, self.status_code)
128132

129133

130134
class RateLimitError(InvalidRequest):
@@ -133,7 +137,7 @@ class RateLimitError(InvalidRequest):
133137
"Exceeded rate limit for key type {} of {} requests per {} seconds"
134138
)
135139

136-
def __init__(self, sending_limit, interval, key_type):
140+
def __init__(self, sending_limit, interval, key_type): # noqa: B042
137141
# normal keys are spoken of as "live" in the documentation
138142
# so using this in the error messaging
139143
if key_type == KeyType.NORMAL:
@@ -142,12 +146,17 @@ def __init__(self, sending_limit, interval, key_type):
142146
self.message = self.message_template.format(
143147
key_type.upper(), sending_limit, interval
144148
)
149+
self.sending_limit = sending_limit
150+
self.interval = interval
151+
self.key_type = key_type
152+
super().__init__(self.message, self.status_code)
145153

146154

147155
class BadRequestError(InvalidRequest):
148156
message = "An error occurred"
149157

150-
def __init__(self, fields=None, message=None, status_code=400):
151-
self.status_code = status_code
158+
def __init__(self, fields=None, message=None, status_code=400): # noqa: B042
152159
self.fields = fields or []
153160
self.message = message if message else self.message
161+
self.status_code = status_code
162+
super().__init__(self.message, self.status_code)

0 commit comments

Comments
 (0)