Skip to content

Commit

Permalink
Merge pull request #120 from communitiesuk/FS-3845
Browse files Browse the repository at this point in the history
FS-3845
  • Loading branch information
aaronwilliamsv1 authored Jan 29, 2024
2 parents b3b7276 + 50f272a commit e6217e5
Show file tree
Hide file tree
Showing 5 changed files with 380 additions and 0 deletions.
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']]
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 apps/pre-award/lambdas/application-deadline-reminder/config.py
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 apps/pre-award/lambdas/application-deadline-reminder/data.py
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

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(),
}

0 comments on commit e6217e5

Please sign in to comment.