Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
e414a50
Bump hypothesis from 6.141.0 to 6.141.1 (#2042)
dependabot[bot] Oct 16, 2025
66a8554
Bump cyclonedx-python-lib from 11.1.0 to 11.2.0 (#2040)
dependabot[bot] Oct 16, 2025
95f24f7
Bump numpy from 2.3.3 to 2.3.4 (#2041)
dependabot[bot] Oct 16, 2025
7febff7
Add organization message totals endpoint
heyitsmebev Oct 16, 2025
1a4eafb
update route name
heyitsmebev Oct 16, 2025
27d7efb
update test
heyitsmebev Oct 16, 2025
fdf69df
Bump aiohttp from 3.13.0 to 3.13.1 (#2047)
dependabot[bot] Oct 18, 2025
c8b1049
Bump moto from 5.1.14 to 5.1.15 (#2046)
dependabot[bot] Oct 18, 2025
a0c2565
Bump hypothesis from 6.141.1 to 6.142.1 (#2045)
dependabot[bot] Oct 18, 2025
1501547
Bump cryptography from 46.0.2 to 46.0.3 (#2044)
dependabot[bot] Oct 18, 2025
82a11ed
Merge pull request #2043 from GSA/2967-calculate-an-organizations-mes…
ccostino Oct 20, 2025
b852e6e
Bump hypothesis from 6.142.1 to 6.142.2 (#2048)
dependabot[bot] Oct 21, 2025
ccec575
get service usage
heyitsmebev Oct 21, 2025
0267c52
updating imports
heyitsmebev Oct 21, 2025
09907cc
removed the next() lookup
heyitsmebev Oct 21, 2025
3a82ef7
we don't need created_at here
heyitsmebev Oct 21, 2025
ec300c0
Bump regex from 2025.9.18 to 2025.10.23 (#2049)
dependabot[bot] Oct 22, 2025
92ecdff
Merge pull request #2050 from GSA/2968-per-service-quota-display
heyitsmebev Oct 22, 2025
dd6124b
Bump cyclonedx-python-lib from 11.2.0 to 11.3.0
dependabot[bot] Oct 22, 2025
8699e65
Merge pull request #2051 from GSA/dependabot/pip/cyclonedx-python-lib…
ccostino Oct 22, 2025
edf3cd3
Bump hypothesis from 6.142.2 to 6.142.3
dependabot[bot] Oct 22, 2025
375336d
Merge pull request #2053 from GSA/dependabot/pip/hypothesis-6.142.3
ccostino Oct 22, 2025
1af8e7c
start upgrading terraform provider
Oct 23, 2025
41f12cc
whoops
Oct 23, 2025
f686fee
Merge pull request #2055 from GSA/terraform_upgrade1
ccostino Oct 23, 2025
6d644a9
restore module redis-v70
Oct 23, 2025
9fa3db8
restore module redis-v70
Oct 23, 2025
a907a63
Merge pull request #2056 from GSA/terraform_upgrade1
ccostino Oct 23, 2025
b81fe0b
migrate csv_upload_bucket to new provider
Oct 23, 2025
7d63d3b
try again
Oct 23, 2025
2db396e
try again
Oct 23, 2025
43beeae
try again
Oct 23, 2025
bb6438f
Merge pull request #2057 from GSA/terraform_upgrade1
terrazoon Oct 24, 2025
e3e44f6
rebuild module.ses_email
Oct 24, 2025
08f8104
rebuild module.ses_email
Oct 24, 2025
cfb645c
rebuild module.ses_email
Oct 24, 2025
4579270
rebuild module.ses_email
Oct 24, 2025
c88c25d
rebuild module.ses_email
Oct 24, 2025
1081073
revert
Oct 24, 2025
96a584a
revert
Oct 24, 2025
f447f2b
revert
Oct 24, 2025
287c051
revert
Oct 24, 2025
3eff196
Merge pull request #2059 from GSA/terraform_upgrade1
ccostino Oct 24, 2025
9d33247
Bump cyclonedx-python-lib from 11.3.0 to 11.4.0 (#2058)
dependabot[bot] Oct 24, 2025
b52df1a
add second provider to all tiers
Oct 24, 2025
134ff85
Bump faker from 37.11.0 to 37.12.0
dependabot[bot] Oct 24, 2025
585fef8
Merge pull request #2064 from GSA/dependabot/pip/faker-37.12.0
ccostino Oct 27, 2025
a9f09f5
Bump phonenumbers from 9.0.16 to 9.0.17
dependabot[bot] Oct 27, 2025
61ce3b0
Merge pull request #2065 from GSA/dependabot/pip/phonenumbers-9.0.17
ccostino Oct 27, 2025
b760d45
fix test
Oct 27, 2025
9ba2e8b
Bump flake8-bugbear from 24.12.12 to 25.10.21 (#2052)
dependabot[bot] Oct 27, 2025
d0a7ba5
remove logging messages
Oct 27, 2025
5b897c9
Bump pip from 25.2 to 25.3
dependabot[bot] Oct 27, 2025
81a68ba
Merge pull request #2067 from GSA/dependabot/pip/pip-25.3
ccostino Oct 27, 2025
9ff6811
Merge pull request #2066 from GSA/org_tests
ccostino Oct 27, 2025
d7c716b
Merge pull request #2062 from GSA/terraform_upgrade2
ccostino Oct 27, 2025
5d025df
Merge pull request #2068 from GSA/invalidphone
ccostino Oct 27, 2025
d7cd9a4
Bump hypothesis from 6.142.3 to 6.142.4
dependabot[bot] Oct 27, 2025
6115e0f
Created a dashboard endpoint combines service usage data with recent …
heyitsmebev Oct 24, 2025
bc08521
added get_organization_dashboard
heyitsmebev Oct 24, 2025
e6226dd
formatting
heyitsmebev Oct 24, 2025
5b76a75
isort
heyitsmebev Oct 24, 2025
2d730be
fix testing
heyitsmebev Oct 27, 2025
608aa3b
test
heyitsmebev Oct 27, 2025
2714b4b
Merge pull request #2069 from GSA/dependabot/pip/hypothesis-6.142.4
ccostino Oct 28, 2025
00afb7d
Bump bleach from 6.2.0 to 6.3.0
dependabot[bot] Oct 28, 2025
a313ace
Merge pull request #2070 from GSA/dependabot/pip/bleach-6.3.0
ccostino Oct 28, 2025
aeef6bd
Bump python-dotenv from 1.1.1 to 1.2.1 (#2071)
dependabot[bot] Oct 28, 2025
e9a0137
removed converting uuid to strings
heyitsmebev Oct 29, 2025
e5546dd
Remove unnecessary UUID/string conversions
heyitsmebev Oct 29, 2025
cb4b769
Bump aiohttp from 3.13.1 to 3.13.2 (#2074)
dependabot[bot] Oct 29, 2025
fe3c547
Disable demo infrastructure drift check
ccostino Oct 29, 2025
bf68dde
Merge pull request #2075 from GSA/disable-demo-infra-drift-check
ccostino Oct 29, 2025
99649b4
Merge pull request #2063 from GSA/2971-include-recent-template-used-p…
ccostino Oct 29, 2025
838fef1
Bump virtualenv from 20.35.3 to 20.35.4 (#2078)
dependabot[bot] Oct 30, 2025
ffd23dc
Bump alembic from 1.17.0 to 1.17.1 (#2076)
dependabot[bot] Oct 30, 2025
4a57413
Bump deprecated from 1.2.18 to 1.3.0 (#2077)
dependabot[bot] Oct 30, 2025
62b8ea7
Bump hypothesis from 6.142.4 to 6.142.5 (#2082)
dependabot[bot] Nov 2, 2025
19c8f26
Bump cyclonedx-python-lib from 11.4.0 to 11.5.0 (#2081)
dependabot[bot] Nov 2, 2025
065f862
Bump deprecated from 1.3.0 to 1.3.1 (#2080)
dependabot[bot] Nov 2, 2025
b660c40
Bump marshmallow from 4.0.1 to 4.1.0
dependabot[bot] Nov 3, 2025
b776bff
Merge pull request #2083 from GSA/dependabot/pip/marshmallow-4.1.0
ccostino Nov 4, 2025
ceea463
Bump hypothesis from 6.142.5 to 6.145.0 (#2085)
dependabot[bot] Nov 4, 2025
864f4ca
Bump moto from 5.1.15 to 5.1.16 (#2084)
dependabot[bot] Nov 4, 2025
2f3cdb3
Bump regex from 2025.10.23 to 2025.11.3 (#2087)
dependabot[bot] Nov 5, 2025
3cbda2f
Bump newrelic from 11.0.1 to 11.1.0 (#2086)
dependabot[bot] Nov 5, 2025
0845e9f
Bump hypothesis from 6.145.1 to 6.146.0 (#2088)
dependabot[bot] Nov 6, 2025
8baf02a
Bump hypothesis from 6.146.0 to 6.147.0
dependabot[bot] Nov 6, 2025
55acb27
Merge pull request #2089 from GSA/dependabot/pip/hypothesis-6.147.0
ccostino Nov 6, 2025
2713efd
Bump phonenumbers from 9.0.17 to 9.0.18
dependabot[bot] Nov 7, 2025
2ea202a
Merge pull request #2090 from GSA/dependabot/pip/phonenumbers-9.0.18
ccostino Nov 7, 2025
8e51a33
Bump pre-commit from 4.3.0 to 4.4.0 (#2093)
dependabot[bot] Nov 11, 2025
592049a
Bump black from 25.9.0 to 25.11.0 (#2091)
dependabot[bot] Nov 11, 2025
e2f874b
Bump pytest from 8.4.2 to 9.0.0 (#2092)
dependabot[bot] Nov 11, 2025
72c2061
Bump pytest from 9.0.0 to 9.0.1
dependabot[bot] Nov 12, 2025
c90eb11
Bump certifi from 2025.10.5 to 2025.11.12
dependabot[bot] Nov 12, 2025
c38637c
Merge pull request #2097 from GSA/dependabot/pip/certifi-2025.11.12
ccostino Nov 12, 2025
4999623
Merge pull request #2096 from GSA/dependabot/pip/pytest-9.0.1
ccostino Nov 12, 2025
c70bc7c
Bump faker from 37.12.0 to 38.0.0
dependabot[bot] Nov 12, 2025
3ad3b00
Merge pull request #2095 from GSA/dependabot/pip/faker-38.0.0
ccostino Nov 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
run: terraform init

- name: Terraform apply
working-directory: terraform/staging
env:
Expand Down
72 changes: 36 additions & 36 deletions .github/workflows/drift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,44 +44,44 @@ jobs:
exit $exit_code
fi

check_demo_drift:
runs-on: ubuntu-latest
name: Check for drift of demo terraform configuration
environment: demo
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: 'production'
# check_demo_drift:
# runs-on: ubuntu-latest
# name: Check for drift of demo terraform configuration
# environment: demo
# steps:
# - name: Checkout
# uses: actions/checkout@v4
# with:
# ref: 'production'

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

- name: Check for drift
env:
AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
TF_VAR_cf_user: ${{ secrets.CLOUDGOV_USERNAME }}
TF_VAR_cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
run: |
cd terraform/demo
terraform init
terraform plan -detailed-exitcode
exit_code=$?
if [ $exit_code -eq 0 ]; then
echo "No changes detected. Intrastructure is up-to-date."
elif [ $exit_code -eq 2 ]; then
echo "Changes detected. Infrastructure drift found."
exit 1
else
echo "Error running terraform plan."
exit $exit_code
fi
# - name: Check for drift
# env:
# AWS_ACCESS_KEY_ID: ${{ secrets.TERRAFORM_STATE_ACCESS_KEY }}
# AWS_SECRET_ACCESS_KEY: ${{ secrets.TERRAFORM_STATE_SECRET_ACCESS_KEY }}
# TF_VAR_cf_user: ${{ secrets.CLOUDGOV_USERNAME }}
# TF_VAR_cf_password: ${{ secrets.CLOUDGOV_PASSWORD }}
# run: |
# cd terraform/demo
# terraform init
# terraform plan -detailed-exitcode
# exit_code=$?
# if [ $exit_code -eq 0 ]; then
# echo "No changes detected. Intrastructure is up-to-date."
# elif [ $exit_code -eq 2 ]; then
# echo "Changes detected. Infrastructure drift found."
# exit 1
# else
# echo "Error running terraform plan."
# exit $exit_code
# fi

check_prod_drift:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/terraform-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ jobs:
id: validation
run: terraform validate -no-color


- name: Terraform plan
id: plan
env:
Expand Down
1 change: 1 addition & 0 deletions app/authentication/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

class AuthError(Exception):
def __init__(self, message, code, service_id=None, api_key_id=None):
super().__init__(message, code, service_id, api_key_id)
self.message = {"token": [message]}
self.short_message = message
self.code = code
Expand Down
1 change: 1 addition & 0 deletions app/clients/document_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

class DocumentDownloadError(Exception):
def __init__(self, message, status_code):
super().__init__(message, status_code)
self.message = message
self.status_code = status_code

Expand Down
1 change: 1 addition & 0 deletions app/clients/sms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class SmsClientResponseException(ClientException):
"""

def __init__(self, message):
super().__init__(message)
self.message = message

def __str__(self):
Expand Down
24 changes: 18 additions & 6 deletions app/dao/fact_billing_dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@

from app import db
from app.dao.date_util import get_calendar_year_dates, get_calendar_year_for_datetime
from app.dao.organization_dao import dao_get_organization_live_services
from app.dao.organization_dao import (
dao_get_organization_live_services,
dao_get_organization_services,
)
from app.enums import KeyType, NotificationStatus, NotificationType
from app.models import (
AnnualBilling,
Expand Down Expand Up @@ -613,6 +616,7 @@ def fetch_sms_billing_for_organization(organization_id, financial_year):
func.coalesce(chargeable_sms, 0).label("chargeable_billable_sms"),
func.coalesce(sms_cost, 0).label("sms_cost"),
Service.active,
Service.restricted,
)
.select_from(Service)
.outerjoin(
Expand Down Expand Up @@ -695,10 +699,16 @@ def query_organization_sms_usage_for_year(organization_id, year):
)


def fetch_usage_year_for_organization(organization_id, year):
def fetch_usage_year_for_organization(
organization_id, year, include_all_services=False
):
year_start, year_end = get_calendar_year_dates(year)
today = utc_now().date()
services = dao_get_organization_live_services(organization_id)

if include_all_services:
services = dao_get_organization_services(organization_id)
else:
services = dao_get_organization_live_services(organization_id)

# if year end date is less than today, we are calculating for data in the past and have no need for deltas.
if year_end >= today:
Expand All @@ -709,7 +719,7 @@ def fetch_usage_year_for_organization(organization_id, year):
service_with_usage = {}
# initialise results
for service in services:
service_with_usage[str(service.id)] = {
service_with_usage[service.id] = {
"service_id": service.id,
"service_name": service.name,
"free_sms_limit": 0,
Expand All @@ -719,13 +729,14 @@ def fetch_usage_year_for_organization(organization_id, year):
"sms_cost": 0.0,
"emails_sent": 0,
"active": service.active,
"restricted": service.restricted,
}
sms_usages = fetch_sms_billing_for_organization(organization_id, year)
email_usages = fetch_email_usage_for_organization(
organization_id, year_start, year_end
)
for usage in sms_usages:
service_with_usage[str(usage.service_id)] = {
service_with_usage[usage.service_id] = {
"service_id": usage.service_id,
"service_name": usage.service_name,
"free_sms_limit": usage.free_sms_fragment_limit,
Expand All @@ -735,9 +746,10 @@ def fetch_usage_year_for_organization(organization_id, year):
"sms_cost": float(usage.sms_cost),
"emails_sent": 0,
"active": usage.active,
"restricted": usage.restricted,
}
for email_usage in email_usages:
service_with_usage[str(email_usage.service_id)][
service_with_usage[email_usage.service_id][
"emails_sent"
] = email_usage.emails_sent

Expand Down
87 changes: 86 additions & 1 deletion app/dao/notifications_dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@
from app.dao.dao_utils import autocommit
from app.dao.inbound_sms_dao import Pagination
from app.enums import KeyType, NotificationStatus, NotificationType
from app.models import FactNotificationStatus, Notification, NotificationHistory
from app.models import (
FactNotificationStatus,
Notification,
NotificationHistory,
Template,
)
from app.utils import (
escape_special_characters,
get_midnight_in_utc,
Expand Down Expand Up @@ -340,6 +345,86 @@ def dao_get_notification_count_for_service_message_ratio(service_id, current_yea
return recent_count + old_count


def dao_get_notification_counts_per_service(service_ids, current_year):
"""
Get notification counts for multiple services in a single organization.
"""
if not service_ids:
return {}

start_date = datetime(current_year, 6, 16)
end_date = datetime(current_year + 1, 6, 16)

stmt1 = (
select(Notification.service_id, func.count().label("count"))
.where(
Notification.service_id.in_(service_ids),
Notification.status
not in [
NotificationStatus.CANCELLED,
NotificationStatus.CREATED,
NotificationStatus.SENDING,
],
Notification.created_at >= start_date,
Notification.created_at < end_date,
)
.group_by(Notification.service_id)
)

stmt2 = (
select(NotificationHistory.service_id, func.count().label("count"))
.where(
NotificationHistory.service_id.in_(service_ids),
NotificationHistory.status
not in [
NotificationStatus.CANCELLED,
NotificationStatus.CREATED,
NotificationStatus.SENDING,
],
NotificationHistory.created_at >= start_date,
NotificationHistory.created_at < end_date,
)
.group_by(NotificationHistory.service_id)
)

result_dict = {}

recent_results = db.session.execute(stmt1).all()
for service_id, count in recent_results:
result_dict[service_id] = count

history_results = db.session.execute(stmt2).all()
for service_id, count in history_results:
result_dict[service_id] = result_dict.get(service_id, 0) + count

return result_dict


def dao_get_recent_sms_template_per_service(service_ids):

if not service_ids:
return {}

stmt = (
select(
Notification.service_id,
Template.name.label("template_name"),
)
.join(Template, Template.id == Notification.template_id)
.where(
Notification.service_id.in_(service_ids),
Notification.notification_type == NotificationType.SMS,
Notification.key_type != KeyType.TEST,
)
.distinct(Notification.service_id)
.order_by(Notification.service_id, desc(Notification.created_at))
)

results = db.session.execute(stmt).all()

return {service_id: template_name for service_id, template_name in results}


def dao_get_failed_notification_count():
stmt = select(func.count(Notification.id)).where(
Notification.status == NotificationStatus.FAILED
Expand Down
15 changes: 15 additions & 0 deletions app/dao/services_dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,21 @@ def dao_fetch_all_services_created_by_user(user_id):
return db.session.execute(stmt).scalars().all()


def dao_get_service_primary_contacts(service_ids):

if not service_ids:
return {}

stmt = select(
Service.id.label("service_id"),
Service.billing_contact_email_addresses.label("email_address"),
).where(Service.id.in_(service_ids))

results = db.session.execute(stmt).all()

return {service_id: email_address for service_id, email_address in results}


@autocommit
@version_class(
VersionOptions(ApiKey, must_write_history=False),
Expand Down
21 changes: 15 additions & 6 deletions app/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class InvalidRequest(Exception):
fields = []

def __init__(self, message, status_code):
super().__init__()
super().__init__(message, status_code)
self.message = message
self.status_code = status_code

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

def __init__(self, sending_limit):
def __init__(self, sending_limit): # noqa: B042
self.message = self.message_template.format(sending_limit)
self.sending_limit = sending_limit
super().__init__(self.message, self.status_code)


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

def __init__(self, sending_limit):
def __init__(self, sending_limit): # noqa: B042
self.message = self.message_template.format(sending_limit)
self.sending_limit = sending_limit
super().__init__(self.message, self.status_code)


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

def __init__(self, sending_limit, interval, key_type):
def __init__(self, sending_limit, interval, key_type): # noqa: B042
# normal keys are spoken of as "live" in the documentation
# so using this in the error messaging
if key_type == KeyType.NORMAL:
Expand All @@ -142,12 +146,17 @@ def __init__(self, sending_limit, interval, key_type):
self.message = self.message_template.format(
key_type.upper(), sending_limit, interval
)
self.sending_limit = sending_limit
self.interval = interval
self.key_type = key_type
super().__init__(self.message, self.status_code)


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

def __init__(self, fields=None, message=None, status_code=400):
self.status_code = status_code
def __init__(self, fields=None, message=None, status_code=400): # noqa: B042
self.fields = fields or []
self.message = message if message else self.message
self.status_code = status_code
super().__init__(self.message, self.status_code)
1 change: 1 addition & 0 deletions app/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class DVLAException(Exception):
def __init__(self, message):
super().__init__(message)
self.message = message


Expand Down
Loading