-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Annual limit sending validation + budget component on ready to send s…
…creen (#2010) * feat: validate annual limit * fix: modify wrapper to work with older redis keys * test: add tests! * chore: `utcnow` is deprecated, use `now` instead * feat(annual limits): show error if user is over annual limits * fix: move daily limit message into the appropriate template * feat: add a client to get daily/yearly sent stats using caching where possible * add translations * chore: formatting * fix send to work without FF too * feat: fix sending; write tests; 🙏 * fix: only check for `send_exceeds_annual_limit` when FF is on * fix(tests): only run the new tests with the FF on (they cant pass with it off!) * chore: remove unused imports * fix: move secondary message outside banner * fix: undo ruff change made by accident * feat(error message): split annual into its own * feat(send): refactor limit checker to use `notification_counts_client` class when sending * feat(remaining messages summary): add heading mode; fix missing translations * feat(ready to send): show RMS componont on page, prevent users from moving forward if service is at either limit * feat(ready to send): update view template * feat(annual error mesage email): refactor page a bit, add case for remaining = 0 * fix(notifications_count_client): cast bytes to int (why are they stored as bytes?) * fix: refactor error message views to make them re-usable * chore: translations * tests: add tests for new send code, template code, and changes to notification_counts_client * chore: fix translation * chore: add missing translations * chore: fix failing tests due to merge * fix: remove commented out code * test fixes * chore: refactor a bit so other methods that render _template.html all have the info needed for budget component * fix failing tests; remove letter test * fix(test_templates): update mocks in tests * fix(ready to send): always show the buttons when the FF is off; if the FF is on, show them as long as they have remaining cap today and this year * fix(test_template): only run the test with the FF on * fix(rms): update text only versions to use new signature --------- Co-authored-by: William B <[email protected]>
- Loading branch information
1 parent
d7f02c0
commit 51b10a9
Showing
16 changed files
with
971 additions
and
191 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
from datetime import datetime | ||
|
||
from notifications_utils.clients.redis import ( | ||
email_daily_count_cache_key, | ||
sms_daily_count_cache_key, | ||
) | ||
|
||
from app import redis_client, service_api_client, template_statistics_client | ||
from app.models.service import Service | ||
|
||
|
||
class NotificationCounts: | ||
def get_all_notification_counts_for_today(self, service_id): | ||
# try to get today's stats from redis | ||
todays_sms = redis_client.get(sms_daily_count_cache_key(service_id)) | ||
todays_sms = int(todays_sms) if todays_sms is not None else None | ||
|
||
todays_email = redis_client.get(email_daily_count_cache_key(service_id)) | ||
todays_email = int(todays_email) if todays_email is not None else None | ||
|
||
if todays_sms is not None and todays_email is not None: | ||
return {"sms": todays_sms, "email": todays_email} | ||
# fallback to the API if the stats are not in redis | ||
else: | ||
stats = template_statistics_client.get_template_statistics_for_service(service_id, limit_days=1) | ||
transformed_stats = _aggregate_notifications_stats(stats) | ||
|
||
return transformed_stats | ||
|
||
def get_all_notification_counts_for_year(self, service_id, year): | ||
""" | ||
Get total number of notifications by type for the current service for the current year | ||
Return value: | ||
{ | ||
'sms': int, | ||
'email': int | ||
} | ||
""" | ||
stats_today = self.get_all_notification_counts_for_today(service_id) | ||
stats_this_year = service_api_client.get_monthly_notification_stats(service_id, year)["data"] | ||
stats_this_year = _aggregate_stats_from_service_api(stats_this_year) | ||
# aggregate stats_today and stats_this_year | ||
for template_type in ["sms", "email"]: | ||
stats_this_year[template_type] += stats_today[template_type] | ||
|
||
return stats_this_year | ||
|
||
def get_limit_stats(self, service: Service): | ||
""" | ||
Get the limit stats for the current service, by notification type, including: | ||
- how many notifications were sent today and this year | ||
- the monthy and daily limits | ||
- the number of notifications remaining today and this year | ||
Returns: | ||
dict: A dictionary containing the limit stats for email and SMS notifications. The structure is as follows: | ||
{ | ||
"email": { | ||
"annual": { | ||
"limit": int, # The annual limit for email notifications | ||
"sent": int, # The number of email notifications sent this year | ||
"remaining": int, # The number of email notifications remaining this year | ||
}, | ||
"daily": { | ||
"limit": int, # The daily limit for email notifications | ||
"sent": int, # The number of email notifications sent today | ||
"remaining": int, # The number of email notifications remaining today | ||
}, | ||
}, | ||
"sms": { | ||
"annual": { | ||
"limit": int, # The annual limit for SMS notifications | ||
"sent": int, # The number of SMS notifications sent this year | ||
"remaining": int, # The number of SMS notifications remaining this year | ||
}, | ||
"daily": { | ||
"limit": int, # The daily limit for SMS notifications | ||
"sent": int, # The number of SMS notifications sent today | ||
"remaining": int, # The number of SMS notifications remaining today | ||
}, | ||
} | ||
} | ||
""" | ||
|
||
sent_today = self.get_all_notification_counts_for_today(service.id) | ||
sent_thisyear = self.get_all_notification_counts_for_year(service.id, datetime.now().year) | ||
|
||
limit_stats = { | ||
"email": { | ||
"annual": { | ||
"limit": service.email_annual_limit, | ||
"sent": sent_thisyear["email"], | ||
"remaining": service.email_annual_limit - sent_thisyear["email"], | ||
}, | ||
"daily": { | ||
"limit": service.message_limit, | ||
"sent": sent_today["email"], | ||
"remaining": service.message_limit - sent_today["email"], | ||
}, | ||
}, | ||
"sms": { | ||
"annual": { | ||
"limit": service.sms_annual_limit, | ||
"sent": sent_thisyear["sms"], | ||
"remaining": service.sms_annual_limit - sent_thisyear["sms"], | ||
}, | ||
"daily": { | ||
"limit": service.sms_daily_limit, | ||
"sent": sent_today["sms"], | ||
"remaining": service.sms_daily_limit - sent_today["sms"], | ||
}, | ||
}, | ||
} | ||
|
||
return limit_stats | ||
|
||
|
||
# TODO: consolidate this function and other functions that transform the results of template_statistics_client calls | ||
def _aggregate_notifications_stats(template_statistics): | ||
template_statistics = _filter_out_cancelled_stats(template_statistics) | ||
notifications = {"sms": 0, "email": 0} | ||
for stat in template_statistics: | ||
notifications[stat["template_type"]] += stat["count"] | ||
|
||
return notifications | ||
|
||
|
||
def _filter_out_cancelled_stats(template_statistics): | ||
return [s for s in template_statistics if s["status"] != "cancelled"] | ||
|
||
|
||
def _aggregate_stats_from_service_api(stats): | ||
"""Aggregate monthly notification stats excluding cancelled""" | ||
total_stats = {"sms": {}, "email": {}} | ||
|
||
for month_data in stats.values(): | ||
for msg_type in ["sms", "email"]: | ||
if msg_type in month_data: | ||
for status, count in month_data[msg_type].items(): | ||
if status != "cancelled": | ||
if status not in total_stats[msg_type]: | ||
total_stats[msg_type][status] = 0 | ||
total_stats[msg_type][status] += count | ||
|
||
return {msg_type: sum(counts.values()) for msg_type, counts in total_stats.items()} | ||
|
||
|
||
notification_counts_client = NotificationCounts() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
24 changes: 24 additions & 0 deletions
24
app/templates/partials/check/too-many-messages-annual.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
{% from "components/links.html" import content_link %} | ||
|
||
{% if template.template_type == 'email' %} | ||
{% set units = _('email messages') %} | ||
{% else %} | ||
{% set units = _('text messages') %} | ||
{% endif %} | ||
|
||
<p data-testid="exceeds-annual"> | ||
{%- if current_service.trial_mode %} | ||
{{ _("Your service is in trial mode. To send more messages, <a href='{}'>request to go live</a>").format(url_for('main.request_to_go_live', service_id=current_service.id)) }} | ||
{% else %} | ||
{% if recipients_remaining_messages > 0 %} | ||
<p>{{ _('<strong>{}</strong> can only send <strong>{}</strong> more {} until annual limit resets'.format(current_service.name, recipients_remaining_messages, units)) }}</p> | ||
<p> | ||
{{ _('To send some of these messages now, edit the spreadsheet to <strong>{}</strong> recipients maximum. '.format(recipients_remaining_messages)) }} | ||
{{ _('To send to recipients you removed, wait until <strong>April 1, {}</strong> or contact them some other way.'.format(now().year)) }} | ||
</p> | ||
{% else %} | ||
<p>{{ _('<strong>{}</strong> cannot send any more {} until <strong>April 1, {}</strong>'.format(current_service.name, units, now().year)) }}</p> | ||
<p>{{ _('For more information, visit the <a href={}>usage report for {}</a>.'.format(url_for('.monthly', service_id=current_service.id), current_service.name)) }}</p> | ||
{% endif %} | ||
{%- endif -%} | ||
</p> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,9 @@ | ||
{% from "components/links.html" import content_link %} | ||
|
||
<p> | ||
<p data-testid="exceeds-daily"> | ||
{%- if current_service.trial_mode %} | ||
{{ _("Your service is in trial mode. To send more messages, <a href='{}'>request to go live</a>").format(url_for('main.request_to_go_live', service_id=current_service.id)) }} | ||
{% else %} | ||
{{ _("To request a daily limit above {} text messages, {}").format(current_service.sms_daily_limit, content_link(_("contact us"), url_for('main.contact'), is_external_link=true)) }} | ||
{%- endif -%} | ||
</p> | ||
</p> |
Oops, something went wrong.