Skip to content

Commit 0e41476

Browse files
committed
feat: write a job to send email to admin for recent opened learner credit requests
1 parent 05f319b commit 0e41476

File tree

4 files changed

+116
-42
lines changed

4 files changed

+116
-42
lines changed

enterprise_access/apps/api/v1/tests/test_browse_and_request_views.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1801,8 +1801,7 @@ def test_create_success(self):
18011801
SubsidyRequestStates.EXPIRED,
18021802
SubsidyRequestStates.REVERSED,
18031803
)
1804-
@mock.patch(BNR_VIEW_PATH + '.send_learner_credit_bnr_admins_email_with_new_requests_task.delay')
1805-
def test_create_reuse_existing_request_success(self, reusable_state, mock_email_task):
1804+
def test_create_reuse_existing_request_success(self, reusable_state):
18061805
"""
18071806
Test that an existing request in reusable states (CANCELLED, EXPIRED, REVERSED)
18081807
gets reused instead of creating a new one.
@@ -1882,13 +1881,6 @@ def test_create_reuse_existing_request_success(self, reusable_state, mock_email_
18821881
assert action is not None
18831882
assert action.status == get_user_message_choice(SubsidyRequestStates.REQUESTED)
18841883

1885-
# Verify email notification task was called
1886-
mock_email_task.assert_called_once_with(
1887-
str(self.policy.uuid),
1888-
str(self.policy.learner_credit_request_config.uuid),
1889-
str(existing_request.enterprise_customer_uuid)
1890-
)
1891-
18921884
def test_overview_happy_path(self):
18931885
"""
18941886
Test the overview endpoint returns correct state counts.

enterprise_access/apps/api/v1/views/browse_and_request.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@
6565
SubsidyRequestCustomerConfiguration
6666
)
6767
from enterprise_access.apps.subsidy_request.tasks import (
68-
send_learner_credit_bnr_admins_email_with_new_requests_task,
6968
send_learner_credit_bnr_request_approve_task,
7069
send_reminder_email_for_pending_learner_credit_request
7170
)
@@ -895,12 +894,6 @@ def create(self, request, *args, **kwargs):
895894
recent_action=get_action_choice(SubsidyRequestStates.REQUESTED),
896895
status=get_user_message_choice(SubsidyRequestStates.REQUESTED),
897896
)
898-
# Trigger admin email notification with the latest request
899-
send_learner_credit_bnr_admins_email_with_new_requests_task.delay(
900-
str(policy.uuid),
901-
str(policy.learner_credit_request_config.uuid),
902-
str(existing_request.enterprise_customer_uuid)
903-
)
904897
response_data = serializers.LearnerCreditRequestSerializer(existing_request).data
905898
return Response(response_data, status=status.HTTP_200_OK)
906899
except Exception as exc: # pylint: disable=broad-except
@@ -938,13 +931,6 @@ def create(self, request, *args, **kwargs):
938931
recent_action=get_action_choice(SubsidyRequestStates.REQUESTED),
939932
status=get_user_message_choice(SubsidyRequestStates.REQUESTED),
940933
)
941-
942-
# Trigger admin email notification with the latest request
943-
send_learner_credit_bnr_admins_email_with_new_requests_task.delay(
944-
str(policy.uuid),
945-
str(policy.learner_credit_request_config.uuid),
946-
str(lcr.enterprise_customer_uuid)
947-
)
948934
except LearnerCreditRequest.DoesNotExist:
949935
logger.warning(f"LearnerCreditRequest {lcr_uuid} not found for action creation.")
950936

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""
2+
Management command to send daily Browse & Request learner credit digest emails to enterprise admins.
3+
4+
Simplified version: run once per day (e.g., via cron). It scans all BNR-enabled policies and
5+
queues a digest task for each policy that has one or more REQUESTED learner credit requests
6+
(open requests, regardless of creation date). Supports a --dry-run mode.
7+
"""
8+
import logging
9+
10+
from django.core.management.base import BaseCommand
11+
12+
from enterprise_access.apps.subsidy_access_policy.models import SubsidyAccessPolicy
13+
from enterprise_access.apps.subsidy_request.constants import SubsidyRequestStates
14+
from enterprise_access.apps.subsidy_request.models import LearnerCreditRequest
15+
from enterprise_access.apps.subsidy_request.tasks import send_learner_credit_bnr_admins_email_with_new_requests_task
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
class Command(BaseCommand):
21+
"""Django management command to enqueue daily Browse & Request learner credit digest tasks.
22+
23+
Scans active, non-retired policies with an active learner credit request config and enqueues
24+
one Celery task per policy that has at least one open (REQUESTED) learner credit request.
25+
Supports an optional dry-run mode for visibility without enqueuing tasks.
26+
"""
27+
28+
help = ('Queue celery tasks that send daily digest emails for Browse & Request learner credit '
29+
'requests per BNR-enabled policy (simple mode).')
30+
31+
LOCK_KEY_TEMPLATE = 'bnr-lc-digest-{date}'
32+
LOCK_TIMEOUT_SECONDS = 2 * 60 * 60 # 2 hours
33+
34+
def add_arguments(self, parser): # noqa: D401 - intentionally left minimal
35+
parser.add_argument(
36+
'--dry-run',
37+
action='store_true',
38+
dest='dry_run',
39+
help='Show which tasks would be enqueued without sending them.'
40+
)
41+
42+
def handle(self, *args, **options):
43+
dry_run = options.get('dry_run')
44+
45+
policies_qs = SubsidyAccessPolicy.objects.filter(
46+
active=True,
47+
retired=False,
48+
learner_credit_request_config__isnull=False,
49+
learner_credit_request_config__active=True,
50+
).select_related('learner_credit_request_config')
51+
52+
total_policies = 0
53+
policies_with_requests = 0
54+
tasks_enqueued = 0
55+
56+
for policy in policies_qs.iterator():
57+
total_policies += 1
58+
config = policy.learner_credit_request_config
59+
if not config:
60+
continue
61+
62+
count_open = LearnerCreditRequest.objects.filter(
63+
learner_credit_request_config=config,
64+
enterprise_customer_uuid=policy.enterprise_customer_uuid,
65+
state=SubsidyRequestStates.REQUESTED,
66+
).count()
67+
if count_open == 0:
68+
continue
69+
70+
policies_with_requests += 1
71+
if dry_run:
72+
logger.info('[DRY RUN] Policy %s enterprise %s would enqueue digest task (%s open requests).',
73+
policy.uuid, policy.enterprise_customer_uuid, count_open)
74+
continue
75+
76+
logger.info(
77+
'Policy %s enterprise %s has %s open learner credit requests. Enqueuing digest task.',
78+
policy.uuid, policy.enterprise_customer_uuid, count_open
79+
)
80+
send_learner_credit_bnr_admins_email_with_new_requests_task.delay(
81+
str(policy.uuid),
82+
str(config.uuid),
83+
str(policy.enterprise_customer_uuid),
84+
)
85+
tasks_enqueued += 1
86+
87+
summary = (
88+
f"BNR daily digest summary: scanned={total_policies} policies, "
89+
f"with_requests={policies_with_requests}, tasks_enqueued={tasks_enqueued}, dry_run={dry_run}"
90+
)
91+
logger.info(summary)
92+
self.stdout.write(self.style.SUCCESS(summary))
93+
return 0

enterprise_access/apps/subsidy_request/tasks.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -152,32 +152,33 @@ def send_learner_credit_bnr_admins_email_with_new_requests_task(
152152
policy_uuid, lc_request_config_uuid, enterprise_customer_uuid
153153
):
154154
"""
155-
Task to send new learner credit request emails to admins.
155+
Daily digest email to enterprise admins for Browse & Request learner credit requests.
156156
157-
This task can be manually triggered from browse_and_request when a new
158-
LearnerCreditRequest is created. It will send the latest 10 requests in
159-
REQUESTED state to enterprise admins.
157+
Intended to be called once per day (via a scheduler). It aggregates all open learner credit
158+
requests in REQUESTED state for the given policy and enterprise, and sends a summary email
159+
containing up to the 10 most recent requests plus the total count of open requests.
160160
161161
Args:
162-
policy_uuid (str): subsidy access policy uuid identifier
163-
lc_request_config_uuid (str): learner credit request config uuid identifier
164-
enterprise_customer_uuid (str): enterprise customer uuid identifier
162+
policy_uuid (str): Subsidy access policy UUID identifier.
163+
lc_request_config_uuid (str): Learner credit request config UUID identifier.
164+
enterprise_customer_uuid (str): Enterprise customer UUID identifier.
165165
Raises:
166-
HTTPError if Braze client call fails with an HTTPError
166+
HTTPError: If Braze client call fails with an HTTPError.
167167
"""
168-
169168
subsidy_model = apps.get_model('subsidy_request.LearnerCreditRequest')
170169
subsidy_requests = subsidy_model.objects.filter(
171170
enterprise_customer_uuid=enterprise_customer_uuid,
172171
learner_credit_request_config__uuid=lc_request_config_uuid,
173172
state=SubsidyRequestStates.REQUESTED,
174173
)
174+
175175
latest_subsidy_requests = subsidy_requests.order_by('-created')[:10]
176+
total_open = subsidy_requests.count()
176177

177-
if not subsidy_requests:
178+
if total_open == 0:
178179
logger.info(
179-
'No learner credit requests in REQUESTED state. Not sending new requests '
180-
f'email to admins for enterprise {enterprise_customer_uuid}.'
180+
'No open learner credit requests in REQUESTED state. Not sending digest email to admins '
181+
'for enterprise %s.', enterprise_customer_uuid
181182
)
182183
return
183184

@@ -192,10 +193,11 @@ def send_learner_credit_bnr_admins_email_with_new_requests_task(
192193

193194
if not admin_users:
194195
logger.info(
195-
f'No admin users found for enterprise {enterprise_customer_uuid}. '
196-
'Not sending new requests email.'
196+
'No admin users found for enterprise %s. Not sending learner credit digest email.',
197+
enterprise_customer_uuid
197198
)
198199
return
200+
199201
braze_client = BrazeApiClient()
200202
recipients = [
201203
braze_client.create_recipient(
@@ -208,7 +210,7 @@ def send_learner_credit_bnr_admins_email_with_new_requests_task(
208210
braze_trigger_properties = {
209211
'manage_requests_url': manage_requests_url,
210212
'requests': [],
211-
'total_requests': len(subsidy_requests),
213+
'total_requests': total_open,
212214
'organization': organization,
213215
}
214216

@@ -219,9 +221,9 @@ def send_learner_credit_bnr_admins_email_with_new_requests_task(
219221
})
220222

221223
logger.info(
222-
f'Sending learner credit requests email to admins for enterprise {enterprise_customer_uuid}. '
223-
f'This includes {len(subsidy_requests)} requests. '
224-
f'Sending to: {admin_users}'
224+
'Sending learner credit DAILY DIGEST email to admins for enterprise %s. '
225+
'Total open requests=%s. Sending to %s',
226+
enterprise_customer_uuid, total_open, admin_users
225227
)
226228

227229
try:
@@ -232,7 +234,8 @@ def send_learner_credit_bnr_admins_email_with_new_requests_task(
232234
)
233235
except Exception:
234236
logger.exception(
235-
f'Exception sending Braze campaign email for enterprise {enterprise_customer_uuid}.'
237+
'Exception sending Braze learner credit daily digest for enterprise %s.',
238+
enterprise_customer_uuid
236239
)
237240
raise
238241

0 commit comments

Comments
 (0)