mirror of
https://github.com/GSA/notifications-api.git
synced 2026-08-25 00:33:41 -04:00
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:
@@ -340,6 +340,67 @@ def dao_get_notification_count_for_service_message_ratio(service_id, current_yea
|
|||||||
return recent_count + old_count
|
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():
|
def dao_get_failed_notification_count():
|
||||||
stmt = select(func.count(Notification.id)).where(
|
stmt = select(func.count(Notification.id)).where(
|
||||||
Notification.status == NotificationStatus.FAILED
|
Notification.status == NotificationStatus.FAILED
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from flask import Blueprint, abort, current_app, jsonify, request
|
from flask import Blueprint, abort, current_app, jsonify, request
|
||||||
from sqlalchemy.exc import IntegrityError
|
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.annual_billing_dao import set_default_free_allowance_for_service
|
||||||
from app.dao.dao_utils import transaction
|
from app.dao.dao_utils import transaction
|
||||||
from app.dao.fact_billing_dao import fetch_usage_year_for_organization
|
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 (
|
from app.dao.organization_dao import (
|
||||||
dao_add_service_to_organization,
|
dao_add_service_to_organization,
|
||||||
dao_add_user_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,
|
organization.agreement_signed_by.email_address,
|
||||||
personalisation,
|
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
|
||||||
|
|||||||
@@ -928,3 +928,57 @@ def test_missing_both():
|
|||||||
{"org_id": ["Can't be empty"]},
|
{"org_id": ["Can't be empty"]},
|
||||||
{"name": ["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,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user