-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #120 from communitiesuk/FS-3845
FS-3845
- Loading branch information
Showing
5 changed files
with
380 additions
and
0 deletions.
There are no files selected for viewing
113 changes: 113 additions & 0 deletions
113
apps/pre-award/copilot/environments/addons/application-deadline-reminder.yml
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,113 @@ | ||
Parameters: | ||
App: | ||
Type: String | ||
Description: Your application's name. | ||
Env: | ||
Type: String | ||
Description: The environment name your service, job, or workflow is being deployed to. | ||
|
||
Resources: | ||
ApplicationDeadlineReminderRole: | ||
Type: AWS::IAM::Role | ||
Properties: | ||
Policies: | ||
- PolicyName: !Sub ApplicationDeadlineReminderPolicy${Env} | ||
PolicyDocument: | ||
Version: 2012-10-17 | ||
Statement: | ||
- Action: | ||
- 'logs:CreateLogGroup' | ||
- 'logs:CreateLogStream' | ||
- 'logs:PutLogEvents' | ||
Resource: | ||
- 'arn:aws:logs:*:*:*' | ||
Effect: Allow | ||
- Action: | ||
- 'ec2:DescribeNetworkInterfaces' | ||
- 'ec2:CreateNetworkInterface' | ||
- 'ec2:DeleteNetworkInterface' | ||
- 'ec2:DescribeInstances' | ||
- 'ec2:AttachNetworkInterface' | ||
Resource: | ||
- '*' | ||
Effect: Allow | ||
AssumeRolePolicyDocument: | ||
Version: 2012-10-17 | ||
Statement: | ||
- Action: | ||
- 'sts:AssumeRole' | ||
Effect: Allow | ||
Principal: | ||
Service: | ||
- lambda.amazonaws.com | ||
- edgelambda.amazonaws.com | ||
|
||
ApplicationDeadlineReminderLambdaFunction: | ||
Type: AWS::Lambda::Function | ||
Properties: | ||
Code: lambdas/application-deadline-reminder/ | ||
Handler: lambda_function.lambda_handler | ||
Timeout: 900 | ||
MemorySize: 512 | ||
Role: !GetAtt ApplicationDeadlineReminderRole.Arn | ||
Runtime: python3.11 | ||
Environment: | ||
Variables: | ||
ACCOUNTS_ENDPOINT: /accounts | ||
ACCOUNT_STORE_API_HOST: http://fsd-account-store.${Env}.pre-award.local:8080 | ||
APPLICATIONS_ENDPOINT: /applications | ||
APPLICATION_REMINDER_STATUS: /funds/{round_id}/application_reminder_status?status=true | ||
APPLICATION_STORE_API_HOST: http://fsd-application-store.${Env}.pre-award.local:8080 | ||
FUND_ENDPOINT: /funds/{fund_id} | ||
FUNDS_ENDPOINT: /funds | ||
FUND_ROUNDS_ENDPOINT: /funds/{fund_id}/rounds | ||
FUND_STORE_API_HOST: !Sub http://fsd-fund-store.${Env}.pre-award.local:8080 | ||
NOTIFICATION_SERVICE_API_HOST: http://fsd-notification.${Env}.pre-award.local:8080 | ||
NOTIFY_TEMPLATE_APPLICATION_DEADLINE_REMINDER: APPLICATION_DEADLINE_REMINDER | ||
SEND_ENDPOINT: /send | ||
VpcConfig: | ||
SecurityGroupIds: | ||
- Fn::ImportValue: "fsdfundstoreclusterSecurityGroup" | ||
- Fn::ImportValue: !Sub ${App}-${Env}-InternalLoadBalancerSecurityGroup | ||
- Fn::ImportValue: !Sub ${App}-${Env}-EnvironmentSecurityGroup | ||
SubnetIds: | ||
!Split | ||
- ',' | ||
- Fn::ImportValue: !Sub ${App}-${Env}-PrivateSubnets | ||
|
||
ApplicationDeadlineReminderLambdaVersion: | ||
Type: AWS::Lambda::Version | ||
Properties: | ||
Description: Creation a version of the Application Deadline Reminder Lambda | ||
FunctionName: !Ref ApplicationDeadlineReminderLambdaFunction | ||
|
||
ApplicationDeadlineReminderScheduledRule: | ||
Type: AWS::Events::Rule | ||
Properties: | ||
Description: "Application Deadline Reminder Scheduled Rule" | ||
ScheduleExpression: "cron(30 09 * * ? *)" | ||
State: "ENABLED" | ||
Targets: | ||
- | ||
Arn: !GetAtt ApplicationDeadlineReminderLambdaFunction.Arn | ||
Id: "TargetApplicationDeadlineReminderFunctionV1" | ||
|
||
ApplicationDeadlineReminderPermissionForEventsToInvokeLambda: | ||
Type: AWS::Lambda::Permission | ||
Properties: | ||
FunctionName: !Ref ApplicationDeadlineReminderLambdaFunction | ||
Action: "lambda:InvokeFunction" | ||
Principal: "events.amazonaws.com" | ||
SourceArn: !GetAtt ApplicationDeadlineReminderScheduledRule.Arn | ||
|
||
Outputs: | ||
ApplicationDeadlineReminderLambdaArn: | ||
Description: The ARN of the Application Deadline Reminder Lambda | ||
Value: !GetAtt ApplicationDeadlineReminderLambdaFunction.Arn | ||
Export: | ||
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ApplicationDeadlineReminderLambdaArn']] | ||
ApplicationDeadlineReminderLambdaVersion: | ||
Description: The version of the Application Deadline Reminder Lambda | ||
Value: !GetAtt ApplicationDeadlineReminderLambdaVersion.Version | ||
Export: | ||
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ApplicationDeadlineReminderLambdaVersion']] |
167 changes: 167 additions & 0 deletions
167
apps/pre-award/lambdas/application-deadline-reminder/application_reminder.py
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,167 @@ | ||
from datetime import datetime | ||
|
||
from dateutil import tz | ||
|
||
from config import Config | ||
from data import get_data | ||
from data import get_account | ||
from data import post_notification | ||
|
||
import requests | ||
import json | ||
|
||
import logging | ||
|
||
# Logging to output to CloudWatch Logs | ||
logging.getLogger('lambda_runtime').setLevel(logging.INFO) | ||
logging.getLogger().setLevel(logging.DEBUG) | ||
|
||
|
||
def application_deadline_reminder(): | ||
|
||
logging.info("Application deadline reminder task is now running!") | ||
uk_timezone = tz.gettz("Europe/London") | ||
current_datetime = datetime.now(uk_timezone).replace(tzinfo=None) | ||
|
||
funds = get_data(Config.FUND_STORE_API_HOST + Config.FUNDS_ENDPOINT) | ||
|
||
for fund in funds: | ||
fund_id = fund.get("id") | ||
fund_info = get_data( | ||
Config.FUND_STORE_API_HOST | ||
+ Config.FUND_ENDPOINT.format(fund_id=fund_id) | ||
) | ||
fund_name = fund_info.get("name") | ||
round_info = get_data( | ||
Config.FUND_STORE_API_HOST | ||
+ Config.FUND_ROUNDS_ENDPOINT.format(fund_id=fund_id) | ||
) | ||
|
||
for round in round_info: | ||
round_deadline_str = round.get("deadline") | ||
reminder_date_str = round.get("reminder_date") | ||
round_id = round.get("id") | ||
round_name = round.get("title") | ||
contact_email = round.get("contact_email") | ||
|
||
if not reminder_date_str: | ||
logging.info( | ||
f"No reminder is set for the round {fund_name} {round_name}" | ||
) | ||
continue | ||
|
||
application_reminder_sent = round.get("application_reminder_sent") | ||
|
||
round_deadline = datetime.strptime( | ||
round_deadline_str, "%Y-%m-%dT%H:%M:%S" | ||
) | ||
|
||
reminder_date = datetime.strptime( | ||
reminder_date_str, "%Y-%m-%dT%H:%M:%S" | ||
) | ||
|
||
if (not application_reminder_sent | ||
and reminder_date < current_datetime < round_deadline | ||
): | ||
status = { | ||
"status_only": ["IN_PROGRESS", "NOT_STARTED", "COMPLETED"], | ||
"fund_id": fund_id, | ||
"round_id": round_id, | ||
} | ||
|
||
endpoint = Config.APPLICATION_STORE_API_HOST + Config.APPLICATIONS_ENDPOINT | ||
not_submitted_applications = requests.get(endpoint, params=status) | ||
|
||
all_applications = [] | ||
for application in not_submitted_applications.json(): | ||
application["round_name"] = round_name | ||
application["fund_name"] = fund_name | ||
application["contact_help_email"] = contact_email | ||
account = get_account( | ||
account_id=application.get("account_id") | ||
) | ||
|
||
application["account_email"] = account.get('email_address') | ||
application["deadline_date"] = round_deadline_str | ||
all_applications.append({"application": application}) | ||
|
||
logging.info(f"Total unsubmitted applications: {len(all_applications)}") | ||
# Only one email per account_email | ||
unique_email_account = {} | ||
for application in all_applications: | ||
unique_email_account[ | ||
application["application"]["account_email"] | ||
] = application | ||
|
||
logging.info(f"Total unique email accounts: {len(unique_email_account)}") | ||
unique_application_email_addresses = list(unique_email_account.values()) | ||
|
||
if len(unique_application_email_addresses) > 0: | ||
for count, application in enumerate( | ||
unique_application_email_addresses, start=1 | ||
): | ||
email = application["application"]["account_email"] | ||
logging.info( | ||
f"Sending reminder {count} of {len(unique_email_account)}" | ||
f" for {fund_name} {round_name}" | ||
f" to {email}" | ||
) | ||
|
||
try: | ||
response = post_notification( | ||
template_type=Config.NOTIFY_TEMPLATE_APPLICATION_DEADLINE_REMINDER, | ||
to_email=email, content=application) | ||
|
||
if response == 200 and len(unique_application_email_addresses) == count: | ||
logging.info( | ||
"The application reminder has been" | ||
" sent successfully for" | ||
f" {fund_name} {round_name}" | ||
) | ||
|
||
try: | ||
application_reminder_endpoint = ( | ||
Config.FUND_STORE_API_HOST | ||
+ Config.APPLICATION_REMINDER_STATUS.format( | ||
round_id=round_id | ||
) | ||
) | ||
response = requests.put( | ||
application_reminder_endpoint | ||
) | ||
if response.status_code == 200: | ||
logging.info( | ||
"The application_reminder_sent has been" | ||
" set to True for" | ||
f" {fund_name} {round_name}" | ||
) | ||
except Exception as e: | ||
logging.error( | ||
"There was an issue updating the" | ||
" application_reminder_sent column in the" | ||
f" Round store for {fund_name} {round_name}." | ||
f" Error {e}." | ||
) | ||
|
||
except Exception as e: | ||
logging.info("There was a problem sending application(s)" | ||
f" for {fund_name} {round_name}" | ||
f" Error: {e}") | ||
|
||
else: | ||
logging.info( | ||
"Currently, there are no non-submitted applications" | ||
f" for {fund_name} {round_name}" | ||
) | ||
else: | ||
if (current_datetime < reminder_date < round_deadline and | ||
not application_reminder_sent): | ||
days_to_reminder = reminder_date-current_datetime | ||
logging.info( | ||
"Application deadline reminder is due in " | ||
f" {days_to_reminder.days} days" | ||
f" for {fund_name} {round_name}." | ||
) | ||
continue | ||
continue | ||
|
26 changes: 26 additions & 0 deletions
26
apps/pre-award/lambdas/application-deadline-reminder/config.py
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,26 @@ | ||
from os import environ | ||
|
||
class Config: | ||
# fund store | ||
FUND_STORE_API_HOST=environ.get("FUND_STORE_API_HOST") | ||
FUNDS_ENDPOINT=environ.get("FUNDS_ENDPOINT") | ||
FUND_ENDPOINT=environ.get("FUND_ENDPOINT") | ||
FUND_ROUNDS_ENDPOINT=environ.get("FUND_ROUNDS_ENDPOINT") | ||
|
||
# account store | ||
ACCOUNT_STORE_API_HOST = environ.get("ACCOUNT_STORE_API_HOST") | ||
ACCOUNTS_ENDPOINT = environ.get("ACCOUNTS_ENDPOINT") | ||
|
||
# application store | ||
APPLICATION_STORE_API_HOST = environ.get("APPLICATION_STORE_API_HOST") | ||
APPLICATION_REMINDER_STATUS = environ.get("APPLICATION_REMINDER_STATUS") | ||
APPLICATIONS_ENDPOINT = environ.get("APPLICATIONS_ENDPOINT") | ||
|
||
# notification service | ||
NOTIFICATION_SERVICE_API_HOST = environ.get("NOTIFICATION_SERVICE_API_HOST") | ||
NOTIFY_TEMPLATE_APPLICATION_DEADLINE_REMINDER = environ.get("NOTIFY_TEMPLATE_APPLICATION_DEADLINE_REMINDER") | ||
SEND_ENDPOINT = environ.get("SEND_ENDPOINT") | ||
|
||
|
||
|
||
|
66 changes: 66 additions & 0 deletions
66
apps/pre-award/lambdas/application-deadline-reminder/data.py
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,66 @@ | ||
import json | ||
from typing import Optional | ||
from urllib.parse import urlencode | ||
from config import Config | ||
|
||
import requests | ||
import logging | ||
|
||
# Logging to output to CloudWatch Logs | ||
logging.getLogger('lambda_runtime').setLevel(logging.INFO) | ||
logging.getLogger().setLevel(logging.DEBUG) | ||
|
||
|
||
def get_data(endpoint, params: Optional[dict] = None): | ||
query_string = "" | ||
if params: | ||
params = {k: v for k, v in params.items() if v is not None} | ||
query_string = urlencode(params) | ||
|
||
endpoint = endpoint + "?" + query_string | ||
response = requests.get(endpoint) | ||
|
||
if response.status_code == 200: | ||
data = response.json() | ||
return data | ||
else: | ||
logging.error("There was a problem retrieving response from" | ||
f" {endpoint}. Status code: {response.status_code}") | ||
return None | ||
|
||
|
||
def post_notification(template_type: str, to_email: str, content): | ||
endpoint = Config.NOTIFICATION_SERVICE_API_HOST + Config.SEND_ENDPOINT | ||
json_payload = { | ||
"type": template_type, | ||
"to": to_email, | ||
"content": content, | ||
} | ||
|
||
response = requests.post(endpoint, json=json_payload) | ||
if response.status_code in [200, 201]: | ||
logging.info( | ||
f"Post successfully sent to {endpoint} with response code:" | ||
f" '{response.status_code}'." | ||
) | ||
return response.status_code | ||
|
||
else: | ||
logging.error("Sorry, the notification could not be sent for endpoint:" | ||
f" '{endpoint}', params: '{json_payload}', response:" | ||
f" '{response.json()}'") | ||
|
||
|
||
def get_account( | ||
email: Optional[str] = None, account_id: Optional[str] = None | ||
): | ||
if email is account_id is None: | ||
raise TypeError("Requires an email address or account_id") | ||
|
||
url = Config.ACCOUNT_STORE_API_HOST + Config.ACCOUNTS_ENDPOINT | ||
params = {"email_address": email, "account_id": account_id} | ||
response = get_data(url, params) | ||
|
||
if response and "account_id" in response: | ||
return response | ||
|
8 changes: 8 additions & 0 deletions
8
apps/pre-award/lambdas/application-deadline-reminder/lambda_function.py
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,8 @@ | ||
from application_reminder import application_deadline_reminder | ||
|
||
def lambda_handler(event, context): | ||
|
||
return { | ||
'statusCode': 200, | ||
'body': application_deadline_reminder(), | ||
} |