Add organization message totals endpoint

- Add GET /organizations/<org_id>/message-totals endpoint
  - Returns aggregated messages_sent, messages_remaining, and total_message_limit
  - Add dao_get_notification_counts_for_organization() for bulk queries
This commit is contained in:
Beverly Nguyen
2025-10-16 12:37:44 -07:00
parent 95f24f7341
commit 7febff7628
3 changed files with 152 additions and 0 deletions

View File

@@ -340,6 +340,67 @@ def dao_get_notification_count_for_service_message_ratio(service_id, current_yea
return recent_count + old_count
def dao_get_notification_counts_for_organization(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_failed_notification_count():
stmt = select(func.count(Notification.id)).where(
Notification.status == NotificationStatus.FAILED

View File

@@ -1,4 +1,6 @@
import json
from datetime import datetime
from zoneinfo import ZoneInfo
from flask import Blueprint, abort, current_app, jsonify, request
from sqlalchemy.exc import IntegrityError
@@ -8,6 +10,7 @@ from app.config import QueueNames
from app.dao.annual_billing_dao import set_default_free_allowance_for_service
from app.dao.dao_utils import transaction
from app.dao.fact_billing_dao import fetch_usage_year_for_organization
from app.dao.notifications_dao import dao_get_notification_counts_for_organization
from app.dao.organization_dao import (
dao_add_service_to_organization,
dao_add_user_to_organization,
@@ -259,3 +262,37 @@ def send_notifications_on_mou_signed(organization_id):
organization.agreement_signed_by.email_address,
personalisation,
)
@organization_blueprint.route("/<uuid:organization_id>/message-totals", methods=["GET"])
def get_organization_message_totals(organization_id):
check_suspicious_id(organization_id)
dao_get_organization_by_id(organization_id)
services = dao_get_organization_services(organization_id)
if not services:
return jsonify({
"messages_sent": 0,
"messages_remaining": 0,
"total_message_limit": 0,
}), 200
current_year = datetime.now(tz=ZoneInfo("UTC")).year
service_ids = [service.id for service in services]
messages_by_service = dao_get_notification_counts_for_organization(
service_ids, current_year
)
total_messages_sent = sum(messages_by_service.get(s.id, 0) for s in services)
total_message_limit = sum(s.total_message_limit for s in services)
total_messages_remaining = total_message_limit - total_messages_sent
return jsonify({
"messages_sent": total_messages_sent,
"messages_remaining": total_messages_remaining,
"total_message_limit": total_message_limit,
}), 200

View File

@@ -928,3 +928,57 @@ def test_missing_both():
{"org_id": ["Can't be empty"]},
{"name": ["Can't be empty"]},
]
@freeze_time("2025-01-15 10:00:00")
def test_get_organization_message_totals(admin_request, sample_organization, mocker):
service_1 = create_service(service_name="Service 1")
service_2 = create_service(service_name="Service 2")
service_1.total_message_limit = 100000
service_2.total_message_limit = 50000
dao_add_service_to_organization(service_1, sample_organization.id)
dao_add_service_to_organization(service_2, sample_organization.id)
mock_get_counts = mocker.patch(
"app.organization.rest.dao_get_notification_counts_for_organization"
)
mock_get_counts.return_value = {
service_1.id: 30000,
service_2.id: 20000,
}
response = admin_request.get(
"organization.get_organization_message_totals",
organization_id=sample_organization.id,
_expected_status=200,
)
assert response["messages_sent"] == 50000
assert response["messages_remaining"] == 100000
assert response["total_message_limit"] == 150000
assert mock_get_counts.call_count == 1
mock_get_counts.assert_called_once_with([service_1.id, service_2.id], 2025)
def test_get_organization_message_totals_no_services(admin_request, sample_organization):
response = admin_request.get(
"organization.get_organization_message_totals",
organization_id=sample_organization.id,
_expected_status=200,
)
assert response["messages_sent"] == 0
assert response["messages_remaining"] == 0
assert response["total_message_limit"] == 0
def test_get_organization_message_totals_invalid_org_id(admin_request):
fake_uuid = "00000000-0000-0000-0000-000000000000"
admin_request.get(
"organization.get_organization_message_totals",
organization_id=fake_uuid,
_expected_status=404,
)