Skip to content

Commit c321472

Browse files
authored
Added the code to insert quarter data per service (#2345)
1 parent 0901513 commit c321472

9 files changed

Lines changed: 300 additions & 0 deletions

app/celery/reporting_tasks.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
from app import notify_celery
99
from app.config import QueueNames
1010
from app.cronitor import cronitor
11+
from app.dao.annual_limits_data_dao import get_previous_quarter, insert_quarter_data
1112
from app.dao.fact_billing_dao import fetch_billing_data_for_day, update_fact_billing
1213
from app.dao.fact_notification_status_dao import (
1314
fetch_notification_status_for_day,
15+
fetch_quarter_data,
1416
update_fact_notification_status,
1517
)
1618
from app.models import Service
@@ -117,3 +119,61 @@ def create_nightly_notification_status_for_day(process_day):
117119
process_day, chunk, e
118120
)
119121
)
122+
123+
124+
@notify_celery.task(name="insert-quarter-data-for-annual-limits")
125+
@statsd(namespace="tasks")
126+
def insert_quarter_data_for_annual_limits(process_day):
127+
"""
128+
This function gets all the service ids and fetches all the notification_count
129+
for the given quarter for the service_ids. It then inserts that data
130+
into the annaual_limits_data_table.
131+
132+
The process_day determines which quarter to fetch data for.
133+
134+
Args:
135+
process_day = datetime object
136+
"""
137+
138+
quarter, dates = get_previous_quarter(process_day)
139+
start_date = dates[0]
140+
end_date = dates[1]
141+
142+
service_info = {x.id: (x.email_annual_limit, x.sms_annual_limit) for x in Service.query.all()}
143+
service_ids = [service_id for service_id in service_info]
144+
chunk_size = 20
145+
iter_service_ids = iter(service_ids)
146+
147+
while True:
148+
chunk = list(islice(iter_service_ids, chunk_size))
149+
150+
if not chunk:
151+
current_app.logger.info(
152+
"insert_quarter_data_for_annual_limits completed for quarter {} on {}".format(
153+
quarter, datetime.now(timezone.utc).date()
154+
)
155+
)
156+
break
157+
158+
try:
159+
start = datetime.now(timezone.utc)
160+
transit_data = fetch_quarter_data(start_date, end_date, service_ids=chunk)
161+
end = datetime.now(timezone.utc)
162+
current_app.logger.info(
163+
"fetch_quarter_data_for_annual_limits for time period {} to {} fetched in {} seconds".format(
164+
start_date, end_date, (end - start).seconds
165+
)
166+
)
167+
insert_quarter_data(transit_data, quarter, service_info)
168+
169+
current_app.logger.info(
170+
"insert_quarter_data task complete: {} rows updated for time period {} to {} for service_ids {}".format(
171+
len(transit_data), start_date, end_date, chunk
172+
)
173+
)
174+
except Exception as e:
175+
current_app.logger.error(
176+
"insert_quarter_data_for_annual_limits task failed for for time period {} to {} for service_ids {}. Error: {}".format(
177+
start_date, end_date, chunk, e
178+
)
179+
)

app/dao/annual_limits_data_dao.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from datetime import datetime
2+
3+
from sqlalchemy.dialects.postgresql import insert
4+
5+
from app import db
6+
from app.models import AnnualLimitsData
7+
8+
9+
def get_previous_quarter(date_to_check):
10+
year = date_to_check.year
11+
month = date_to_check.month
12+
13+
quarter = ""
14+
start_date = None
15+
end_date = None
16+
if month in [1, 2, 3]:
17+
quarter = "Q3"
18+
year -= 1
19+
start_date = datetime(year, 10, 1)
20+
end_date = datetime(year, 12, 31, 23, 59, 59)
21+
elif month in [4, 5, 6]:
22+
quarter = "Q4"
23+
start_date = datetime(year, 1, 1)
24+
end_date = datetime(year, 3, 31, 23, 59, 59)
25+
year -= 1 # Cause we want to store it as Q4 of the previous year
26+
elif month in [7, 8, 9]:
27+
quarter = "Q1"
28+
start_date = datetime(year, 4, 1)
29+
end_date = datetime(year, 6, 30, 23, 59, 59)
30+
elif month in [10, 11, 12]:
31+
quarter = "Q2"
32+
start_date = datetime(year, 7, 1)
33+
end_date = datetime(year, 9, 30, 23, 59, 59)
34+
35+
quarter_name = f"{quarter}-{year}" if quarter else ""
36+
return quarter_name, (start_date, end_date)
37+
38+
39+
def insert_quarter_data(data, quarter, service_info):
40+
"""
41+
Insert data for each quarter into the database.
42+
43+
Each row in transit_data is a namedtuple with the following fields:
44+
- service_id,
45+
- notification_type,
46+
- notification_count
47+
"""
48+
49+
table = AnnualLimitsData.__table__
50+
51+
for row in data:
52+
stmt = (
53+
insert(table)
54+
.values(
55+
service_id=row.service_id,
56+
time_period=quarter,
57+
annual_email_limit=service_info[row.service_id][0],
58+
annual_sms_limit=service_info[row.service_id][1],
59+
notification_type=row.notification_type,
60+
notification_count=row.notification_count,
61+
)
62+
.on_conflict_do_update(
63+
index_elements=["service_id", "time_period", "notification_type"],
64+
set_={
65+
"annual_email_limit": insert(table).excluded.annual_email_limit,
66+
"annual_sms_limit": insert(table).excluded.annual_sms_limit,
67+
"notification_count": insert(table).excluded.notification_count,
68+
},
69+
)
70+
)
71+
db.session.connection().execute(stmt)
72+
db.session.commit()

app/dao/fact_notification_status_dao.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,3 +885,28 @@ def fetch_monthly_notification_statuses_per_service(start_date, end_date):
885885
)
886886
.all()
887887
)
888+
889+
890+
def fetch_quarter_data(start_date, end_date, service_ids):
891+
"""
892+
Get data for each quarter for the given year and service_ids
893+
894+
Args:
895+
start_date (datetime obj)
896+
end_date (datetime obj)
897+
service_ids (list): list of service_ids
898+
"""
899+
query = (
900+
db.session.query(
901+
FactNotificationStatus.service_id,
902+
FactNotificationStatus.notification_type,
903+
func.sum(FactNotificationStatus.notification_count).label("notification_count"),
904+
)
905+
.filter(
906+
FactNotificationStatus.service_id.in_(service_ids),
907+
FactNotificationStatus.bst_date >= start_date,
908+
FactNotificationStatus.bst_date <= end_date,
909+
)
910+
.group_by(FactNotificationStatus.service_id, FactNotificationStatus.notification_type)
911+
)
912+
return query.all()

app/models.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2620,3 +2620,22 @@ class BounceRateStatus(Enum):
26202620
NORMAL = "normal"
26212621
WARNING = "warning"
26222622
CRITICAL = "critical"
2623+
2624+
2625+
class AnnualLimitsData(BaseModel):
2626+
__tablename__ = "annual_limits_data"
2627+
2628+
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey("services.id"), primary_key=True)
2629+
time_period = db.Column(db.String, primary_key=True)
2630+
annual_email_limit = db.Column(db.BigInteger, nullable=False)
2631+
annual_sms_limit = db.Column(db.BigInteger, nullable=False)
2632+
notification_type = db.Column(notification_types, nullable=False, primary_key=True)
2633+
notification_count = db.Column(db.BigInteger, nullable=False)
2634+
2635+
__table_args__ = (
2636+
# Add the composite unique constraint on service_id, time_period, and notification_type
2637+
UniqueConstraint("service_id", "time_period", "notification_type", name="uix_service_time_notification"),
2638+
# Define the indexes within __table_args__
2639+
db.Index("ix_service_id_notification_type", "service_id", "notification_type"),
2640+
db.Index("ix_service_id_notification_type_time", "time_period", "service_id", "notification_type"),
2641+
)
File renamed without changes.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
Revision ID: 0465_add_constraints
3+
Revises: 0464_add_annual_limits_data
4+
Create Date: 2024-10-31 13:32:00
5+
"""
6+
from alembic import op
7+
8+
revision = "0465_add_constraints"
9+
down_revision = "0464_add_annual_limits_data"
10+
11+
12+
def upgrade():
13+
op.create_index(
14+
"ix_service_id_notification_type_time", "annual_limits_data", ["time_period", "service_id", "notification_type"]
15+
)
16+
op.create_unique_constraint(
17+
"uq_service_id_notification_type_time_period", "annual_limits_data", ["service_id", "notification_type", "time_period"]
18+
)
19+
20+
21+
def downgrade():
22+
op.drop_constraint("uq_service_id_notification_type_time_period", "annual_limits_data")
23+
op.drop_index("ix_service_id_notification_type_time", "annual_limits_data")

tests/app/celery/test_reporting_tasks.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,20 @@
1010
create_nightly_billing_for_day,
1111
create_nightly_notification_status,
1212
create_nightly_notification_status_for_day,
13+
insert_quarter_data_for_annual_limits,
1314
)
1415
from app.dao.fact_billing_dao import get_rate
1516
from app.models import (
1617
EMAIL_TYPE,
1718
LETTER_TYPE,
1819
SMS_TYPE,
20+
AnnualLimitsData,
1921
FactBilling,
2022
FactNotificationStatus,
2123
Notification,
2224
)
2325
from tests.app.db import (
26+
create_ft_notification_status,
2427
create_letter_rate,
2528
create_notification,
2629
create_notification_history,
@@ -579,3 +582,26 @@ def test_create_nightly_notification_status_for_day_respects_local_timezone(
579582

580583
assert noti_status[0].bst_date == date(2019, 4, 1)
581584
assert noti_status[0].notification_status == "created"
585+
586+
587+
class TestInsertQuarterData:
588+
def test_insert_quarter_data(self, notify_db_session):
589+
service_1 = create_service(service_name="service_1")
590+
service_2 = create_service(service_name="service_2")
591+
592+
create_ft_notification_status(date(2018, 1, 1), "sms", service_1, count=4)
593+
create_ft_notification_status(date(2018, 5, 2), "sms", service_1, count=10)
594+
create_ft_notification_status(date(2018, 3, 20), "sms", service_2, count=100)
595+
create_ft_notification_status(date(2018, 2, 1), "sms", service_2, count=1000)
596+
597+
# Data for Q4 2017
598+
insert_quarter_data_for_annual_limits(datetime(2018, 4, 1))
599+
600+
assert AnnualLimitsData.query.count() == 2
601+
assert AnnualLimitsData.query.filter_by(service_id=service_1.id).first().notification_count == 4
602+
assert AnnualLimitsData.query.filter_by(service_id=service_2.id).first().notification_count == 1100
603+
assert AnnualLimitsData.query.filter_by(service_id=service_1.id).first().time_period == "Q4-2017"
604+
605+
# Data for Q1 2018
606+
insert_quarter_data_for_annual_limits(datetime(2018, 7, 1))
607+
assert AnnualLimitsData.query.filter_by(service_id=service_1.id, time_period="Q1-2018").first().notification_count == 10
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from collections import namedtuple
2+
from datetime import datetime
3+
4+
import pytest
5+
6+
from app.dao.annual_limits_data_dao import get_previous_quarter, insert_quarter_data
7+
from app.models import AnnualLimitsData, Service
8+
from tests.app.db import create_service
9+
10+
11+
class TestGetPreviousQuarter:
12+
@pytest.mark.parametrize(
13+
"date_to_check, expected",
14+
[
15+
(datetime(2021, 4, 1), ("Q4-2020", (datetime(2021, 1, 1, 0, 0), datetime(2021, 3, 31, 23, 59, 59)))),
16+
(datetime(2021, 6, 1), ("Q4-2020", (datetime(2021, 1, 1, 0, 0), datetime(2021, 3, 31, 23, 59, 59)))),
17+
(datetime(2021, 9, 1), ("Q1-2021", (datetime(2021, 4, 1, 0, 0), datetime(2021, 6, 30, 23, 59, 59)))),
18+
(datetime(2021, 12, 1), ("Q2-2021", (datetime(2021, 7, 1, 0, 0), datetime(2021, 9, 30, 23, 59, 59)))),
19+
(datetime(2022, 1, 1), ("Q3-2021", (datetime(2021, 10, 1, 0, 0), datetime(2021, 12, 31, 23, 59, 59)))),
20+
(datetime(2022, 3, 31), ("Q3-2021", (datetime(2021, 10, 1, 0, 0), datetime(2021, 12, 31, 23, 59, 59)))),
21+
(datetime(2023, 5, 5), ("Q4-2022", (datetime(2023, 1, 1, 0, 0), datetime(2023, 3, 31, 23, 59, 59)))),
22+
],
23+
)
24+
def test_get_previous_quarter(self, date_to_check, expected):
25+
assert get_previous_quarter(date_to_check) == expected
26+
27+
28+
class TestInsertQuarterData:
29+
def test_insert_quarter_data(self, notify_db_session):
30+
service_1 = create_service(service_name="service_1")
31+
service_2 = create_service(service_name="service_2")
32+
33+
service_info = {x.id: (x.email_annual_limit, x.sms_annual_limit) for x in Service.query.all()}
34+
NotificationData = namedtuple("NotificationData", ["service_id", "notification_type", "notification_count"])
35+
36+
data = [
37+
NotificationData(service_1.id, "sms", 4),
38+
NotificationData(service_2.id, "sms", 1100),
39+
NotificationData(service_1.id, "email", 2),
40+
]
41+
insert_quarter_data(data, "Q1-2018", service_info)
42+
43+
assert AnnualLimitsData.query.count() == 3
44+
45+
# We want to check what happens when the same primary key but a new notification count is introduced
46+
47+
assert AnnualLimitsData.query.filter_by(service_id=service_2.id).first().notification_count == 1100
48+
insert_quarter_data(
49+
[
50+
NotificationData(service_2.id, "sms", 500),
51+
],
52+
"Q1-2018",
53+
service_info,
54+
)
55+
assert AnnualLimitsData.query.filter_by(service_id=service_2.id).first().notification_count == 500

tests/app/dao/test_fact_notification_status_dao.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
fetch_notification_status_for_service_for_today_and_7_previous_days,
1919
fetch_notification_status_totals_for_all_services,
2020
fetch_notification_statuses_for_job,
21+
fetch_quarter_data,
2122
fetch_stats_for_all_services_by_date_range,
2223
get_api_key_ranked_by_notifications_created,
2324
get_last_send_for_api_key,
@@ -1345,3 +1346,22 @@ def test_fetch_monthly_notification_statuses_per_service_for_rows_that_should_be
13451346

13461347
results = fetch_monthly_notification_statuses_per_service(date(2019, 3, 1), date(2019, 3, 31))
13471348
assert len(results) == 0
1349+
1350+
1351+
class TestFetchQuarterData:
1352+
def test_fetch_quarter_data(self, notify_db_session):
1353+
service_1 = create_service(service_name="service_1")
1354+
service_2 = create_service(service_name="service_2")
1355+
1356+
create_ft_notification_status(date(2018, 1, 1), "sms", service_1, count=4)
1357+
create_ft_notification_status(date(2018, 5, 2), "sms", service_1, count=10)
1358+
create_ft_notification_status(date(2018, 3, 20), "sms", service_2, count=100)
1359+
create_ft_notification_status(date(2018, 2, 1), "sms", service_2, count=1000)
1360+
1361+
assert fetch_quarter_data(date(2018, 1, 1), date(2018, 3, 27), service_ids=[service_1.id, service_2.id]) == [
1362+
(service_1.id, "sms", 4),
1363+
(service_2.id, "sms", 1100),
1364+
]
1365+
assert fetch_quarter_data(date(2018, 4, 1), date(2018, 6, 30), service_ids=[service_1.id, service_2.id]) == [
1366+
(service_1.id, "sms", 10)
1367+
]

0 commit comments

Comments
 (0)