
AWSTemplateFormatVersion: "2010-09-09"
Description: Sample async AWS Lambda flow execution with Amazon Connect.
Parameters: 
  TrackerDynamoTableName: 
    Type: String
    Default: async_tracker
    Description: Provide a name for your async function tracker DB
  TrackerRecordTimeToLive: 
    Type: Number
    Default: 300
    Description: Amount of time, in seconds, before the tracker expires
  AWSRegion: 
    Type: String
    Default: us-east-1
    AllowedValues:
      - us-east-1
      - us-west-2
      - af-south-1
      - ap-southeast-1
      - ap-southeast-2
      - ap-northeast-1
      - ap-northeast-2
      - ca-central-1
      - eu-central-1
      - eu-west-2
      - us-gov-west-1
    Description: The region code that you are deploying to. Must be a region that supports Amazon Connect
  ConnectInstanceARN:
    Type: String
    Default: Looks like - arn:aws:connect:REGION:ACCOUNT:instance/REPLACEME
    Description: Provide the instance ARN for your Amazon Connect istance.
Resources:
  AsyncTrackerTable:
    Type: AWS::DynamoDB::Table
    Properties:
      AttributeDefinitions:
        - 
          AttributeName: "contact_id"
          AttributeType: "S"
      KeySchema:
        - 
          AttributeName: "contact_id"
          KeyType: "HASH"
      ProvisionedThroughput:
        ReadCapacityUnits: "5"
        WriteCapacityUnits: "5"
      TableName:
        Ref: TrackerDynamoTableName
      TimeToLiveSpecification: 
        AttributeName: "item_ttl"
        Enabled: true
      StreamSpecification:
        StreamViewType: NEW_IMAGE
  AsyncSampleRole:
    Type: 'AWS::IAM::Role'
    DependsOn: AsyncTrackerTable
    Properties:
      RoleName: AsyncLambdaSampleRole
      Description: IAM Role that provides the access required for this sample
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
              - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: AsyncLambdaAddOn
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - dynamodb:PutItem
                  - dynamodb:GetShardIterator
                  - dynamodb:UpdateItem
                  - dynamodb:GetRecords
                  - dynamodb:DescribeStream
                  - dynamodb:ListStreams
                Resource: '*'
  AsyncWriteFunction:
    Type: AWS::Lambda::Function
    DependsOn: AsyncSampleRole
    Properties: 
      Code:
        ZipFile: |
            import json, boto3, time, os, logging
            logger = logging.getLogger()
            logger.setLevel(logging.DEBUG)
            
            def lambda_handler(event, context):
                logger.debug(event)

                #Establish tracker and response containers
                tracker_item = {}
                response = {}

                #Update the tracker item with the required tracker data
                tracker_item.update(
                    {
                        'contact_id': event['Details']['ContactData']['ContactId'],
                        'has_processed': 0
                    }
                )

                #Update the tracker item with the passed Parameters
                tracker_item.update(event['Details']['Parameters'])

                #Define DB connection
                dynamodb_client = boto3.resource('dynamodb')
                dynamodb_table = dynamodb_client.Table(os.environ['tracker_table_name'])

                #define the put with data
                try:
                    async_write = dynamodb_table.put_item(
                        Item = tracker_item
                    )
                    response.update({'is_processing': 'true'})

                except:
                    response.update({'is_processing': 'false'})
                    
                #Send the response back
                return response
      Description: Creates the tracker record
      Environment: 
        Variables:
          tracker_table_name:
            Ref: TrackerDynamoTableName
      FunctionName: AsyncWriteTrackerSample
      Handler: index.lambda_handler
      Role: !GetAtt AsyncSampleRole.Arn
      Runtime: python3.10
      Timeout: 8
  AsyncEventProcessorFunction:
    Type: AWS::Lambda::Function
    DependsOn: AsyncSampleRole
    Properties: 
      Code:
        ZipFile: |
            import json, boto3, time, os, logging
            logger = logging.getLogger()
            logger.setLevel(logging.DEBUG)

            def lambda_handler(event, context):
                logger.debug(event)

                #Establish the event response container
                event_response = {}

                #Check incoming record to see if it is an Insert
                for Record in event['Records']:
                    if Record['eventName'] == 'INSERT' and Record['dynamodb']['NewImage']['has_processed']['N'] == '0':

                        #Establish the async data container
                        async_data = {}

                        #Load the tracker data
                        incoming_item_params = Record['dynamodb']['NewImage']
                        boto3.resource('dynamodb')
                        deserializer = boto3.dynamodb.types.TypeDeserializer()
                        loaded_params = {k: deserializer.deserialize(v) for k,v in incoming_item_params.items()}
                        async_data.update(loaded_params)

                        #Generate the TTL
                        ttlEpochTime = round(time.time())+int(os.environ['item_timeout'])
                        async_data.update(
                            {
                                'item_ttl': ttlEpochTime,
                                'has_processed': 1
                            }
                        )

                        ########## Start Your Async Code ##########

                        #You can reference your passed params like so: 
                        logger.debug(async_data['contact_id'])

                        #Fake response with delay
                        async_data.update({'payment_confirmation':'OU812','result':'success'})
                        time.sleep(15)

                        ########## End Your Async Code ##########

                        if async_data['result'] == 'success':

                            try:
                                #Define the DynamoDB Config
                                dynamodb_client = boto3.resource('dynamodb')
                                dynamodb_tracker_table = dynamodb_client.Table(os.environ['tracker_table_name'])

                                #Update the tracker table
                                async_update = dynamodb_tracker_table.put_item(
                                    Item = async_data
                                )
                                event_response.update({'status':'complete','result':'updated'})

                            except: 
                                event_response.update({'status':'incomplete','result':'failed_tracker_update'})

                        else:
                            event_response.update(async_data)
                            event_response.update({'status':'incomplete','result':'api_function_fail'})

                    else:
                        event_response.update({'job_status':'complete','job_result':'no_more_records_to_process'})

                return event_response
      Description: Processes the tracker event and fires off long function
      Environment: 
        Variables:
          tracker_table_name:
            Ref: TrackerDynamoTableName
          item_timeout:
            Ref: TrackerRecordTimeToLive
      FunctionName: AsyncEventProcessorSample
      Handler: index.lambda_handler
      Role: !GetAtt AsyncSampleRole.Arn
      Runtime: python3.10
      Timeout: 60
  AsyncGetResultFunction:
    Type: AWS::Lambda::Function
    DependsOn: AsyncSampleRole
    Properties: 
      Code:
        ZipFile: |
            import json, boto3, os, logging
            logger = logging.getLogger()
            logger.setLevel(logging.DEBUG)

            def lambda_handler(event, context):
                logger.debug(event)

                #Establish response container
                response = {}
                #Extract contact id from incoming event
                extracted_contact_id = event["Details"]["ContactData"]["ContactId"]

                #Define the DynamoDB Config
                dynamodb_client = boto3.resource('dynamodb')
                dynamodb_tracker_table = dynamodb_client.Table(os.environ['tracker_table_name'])

                #Perform the search based on extracted contact ID
                try:
                    lookup = dynamodb_tracker_table.get_item(
                        Key={
                            'contact_id' : extracted_contact_id
                        }
                    )
                    response.update({'dbquery_status': 'success'})

                    #If we found a record, check for processing
                    if 'Item' in lookup:
                        tracker = lookup['Item']

                        if tracker['has_processed'] == 1:
                            #If it has completed, add the full record to the response and return complete
                            response.update(tracker)
                            response.update({'dbquery_status': 'found'})
                            response.update({'async_status':'complete'})

                        else:
                            #If it has not completed, return incomplete
                            response.update({'async_status':'incomplete'})

                    #if we didnt find a record, return not found
                    else:
                        response.update({'async_status': 'not_found'})

                except: 
                    response.update({'async_status': 'fail'})

                return response
        
      Description: Checks for the result from the long function
      Environment: 
        Variables:
          tracker_table_name:
            Ref: TrackerDynamoTableName
      FunctionName: AsyncGetResultSample
      Handler: index.lambda_handler
      Role: !GetAtt AsyncSampleRole.Arn
      Runtime: python3.10
      Timeout: 8
  AsyncSampleFlow:
    Type: AWS::Connect::ContactFlow
    DependsOn:
        - AsyncGetResultFunction
        - AsyncWriteFunction
    Properties:
      Content: !Sub
        - "{\"Version\":\"2019-10-30\",\"StartAction\":\"9b591740-5b62-4f40-ba6e-a027af9b6ad0\",\"Metadata\":{\"entryPointPosition\":{\"x\":20,\"y\":20},\"ActionMetadata\":{\"c07bc2be-e195-44f3-a70b-66fe61eb83fb\":{\"position\":{\"x\":636.8,\"y\":849.6}},\"faa33c01-f866-414a-9b25-bf337cff3a12\":{\"position\":{\"x\":343.2,\"y\":700.8}},\"cfb7ff34-a6eb-4f3a-be07-73ef6744bac5\":{\"position\":{\"x\":322.4,\"y\":288}},\"c34224ef-2915-4724-ba65-cdf2f517c5bd\":{\"position\":{\"x\":1168.8,\"y\":828.8}},\"127f79ba-17e1-476f-8048-15c7db0f57e0\":{\"position\":{\"x\":929.6,\"y\":944.8}},\"39dcaad3-1307-4221-828a-fb2aa971c03f\":{\"position\":{\"x\":1139.2,\"y\":466.4}},\"27aba9a1-581e-46da-b1b7-a0737857fcad\":{\"position\":{\"x\":1513.6,\"y\":769.6}},\"ef382a99-b301-457d-a953-8ae078ccf1ed\":{\"position\":{\"x\":1193.6,\"y\":652}},\"c3363a08-f6ad-4fe4-9562-18a31feebbb9\":{\"position\":{\"x\":874.4,\"y\":465.6},\"conditions\":[],\"conditionMetadata\":[{\"id\":\"67e46f2f-e3e8-4c64-8bc0-fc442e4ba43d\",\"operator\":{\"name\":\"Equals\",\"value\":\"Equals\",\"shortDisplay\":\"=\"},\"value\":\"incomplete\"},{\"id\":\"6509f084-ef6c-4083-bb99-0550e0b1b206\",\"operator\":{\"name\":\"Equals\",\"value\":\"Equals\",\"shortDisplay\":\"=\"},\"value\":\"complete\"},{\"id\":\"aa376b3f-7908-4fc5-90ef-ecdd5830710f\",\"operator\":{\"name\":\"Equals\",\"value\":\"Equals\",\"shortDisplay\":\"=\"},\"value\":\"fail\"},{\"id\":\"59973b3b-07f7-4c98-9f1c-95212f23fe20\",\"operator\":{\"name\":\"Equals\",\"value\":\"Equals\",\"shortDisplay\":\"=\"},\"value\":\"not_found\"}]},\"df36e2da-bc3e-4433-aa5c-91c6abb713f2\":{\"position\":{\"x\":633.6,\"y\":680}},\"3ce2ec90-a969-4179-a302-da42cd51875d\":{\"position\":{\"x\":341.6,\"y\":462.4}},\"d9a9c449-447b-4136-b0c7-8e07be999158\":{\"position\":{\"x\":632.8,\"y\":461.6},\"parameters\":{\"LambdaFunctionARN\":{\"useDynamic\":true}},\"dynamicMetadata\":{},\"useDynamic\":true},\"5b3b2100-a040-4c41-8c1d-4488cd41a1f4\":{\"position\":{\"x\":84.8,\"y\":457.6}},\"572feb8b-b843-4a54-9a39-53c72948c4e4\":{\"position\":{\"x\":90.4,\"y\":262.4}},\"01d08df9-46b3-406f-9e19-539d195a576b\":{\"position\":{\"x\":946.4,\"y\":32},\"parameters\":{\"LambdaFunctionARN\":{\"useDynamic\":true}},\"dynamicMetadata\":{\"payment_account\":false},\"useDynamic\":true},\"9b591740-5b62-4f40-ba6e-a027af9b6ad0\":{\"position\":{\"x\":154.4,\"y\":26.4}},\"e0473f0f-6a3f-4763-841d-cda2ee68858c\":{\"position\":{\"x\":724.8,\"y\":29.6}},\"d4073578-d663-4864-9319-e75a37e74495\":{\"position\":{\"x\":420.8,\"y\":29.6},\"dynamicParams\":[]}}},\"Actions\":[{\"Parameters\":{},\"Identifier\":\"c07bc2be-e195-44f3-a70b-66fe61eb83fb\",\"Type\":\"DisconnectParticipant\",\"Transitions\":{}},{\"Parameters\":{\"Text\":\"the loop never completed.\"},\"Identifier\":\"faa33c01-f866-414a-9b25-bf337cff3a12\",\"Type\":\"MessageParticipant\",\"Transitions\":{\"NextAction\":\"c07bc2be-e195-44f3-a70b-66fe61eb83fb\"}},{\"Parameters\":{},\"Identifier\":\"cfb7ff34-a6eb-4f3a-be07-73ef6744bac5\",\"Type\":\"DisconnectParticipant\",\"Transitions\":{}},{\"Parameters\":{\"Text\":\"There is no dynamo record for this contact ID.\"},\"Identifier\":\"c34224ef-2915-4724-ba65-cdf2f517c5bd\",\"Type\":\"MessageParticipant\",\"Transitions\":{\"NextAction\":\"27aba9a1-581e-46da-b1b7-a0737857fcad\"}},{\"Parameters\":{\"Text\":\"Something terrible has happened.\"},\"Identifier\":\"127f79ba-17e1-476f-8048-15c7db0f57e0\",\"Type\":\"MessageParticipant\",\"Transitions\":{\"NextAction\":\"27aba9a1-581e-46da-b1b7-a0737857fcad\"}},{\"Parameters\":{\"SSML\":\"<speak>nothing yet. checking again in 5 seconds.<break time=\\\"5s\\\"/></speak>\"},\"Identifier\":\"39dcaad3-1307-4221-828a-fb2aa971c03f\",\"Type\":\"MessageParticipant\",\"Transitions\":{\"NextAction\":\"3ce2ec90-a969-4179-a302-da42cd51875d\"}},{\"Parameters\":{},\"Identifier\":\"27aba9a1-581e-46da-b1b7-a0737857fcad\",\"Type\":\"DisconnectParticipant\",\"Transitions\":{}},{\"Parameters\":{\"SSML\":\"<speak>Thank you for waiting. Your payment has been made. The confirmation code is <prosody rate=\\\"slow\\\"><say-as interpret-as=\\\"characters\\\">$.External.payment_confirmation</say-as></prosody>. This demonstrated an asynchronous call to a lambda function. Goodbye.</speak>\"},\"Identifier\":\"ef382a99-b301-457d-a953-8ae078ccf1ed\",\"Type\":\"MessageParticipant\",\"Transitions\":{\"NextAction\":\"27aba9a1-581e-46da-b1b7-a0737857fcad\"}},{\"Parameters\":{\"ComparisonValue\":\"$.External.async_status\"},\"Identifier\":\"c3363a08-f6ad-4fe4-9562-18a31feebbb9\",\"Type\":\"Compare\",\"Transitions\":{\"NextAction\":\"127f79ba-17e1-476f-8048-15c7db0f57e0\",\"Conditions\":[{\"NextAction\":\"39dcaad3-1307-4221-828a-fb2aa971c03f\",\"Condition\":{\"Operator\":\"Equals\",\"Operands\":[\"incomplete\"]}},{\"NextAction\":\"ef382a99-b301-457d-a953-8ae078ccf1ed\",\"Condition\":{\"Operator\":\"Equals\",\"Operands\":[\"complete\"]}},{\"NextAction\":\"c34224ef-2915-4724-ba65-cdf2f517c5bd\",\"Condition\":{\"Operator\":\"Equals\",\"Operands\":[\"fail\"]}},{\"NextAction\":\"c34224ef-2915-4724-ba65-cdf2f517c5bd\",\"Condition\":{\"Operator\":\"Equals\",\"Operands\":[\"not_found\"]}}],\"Errors\":[{\"NextAction\":\"127f79ba-17e1-476f-8048-15c7db0f57e0\",\"ErrorType\":\"NoMatchingCondition\"}]}},{\"Parameters\":{\"Text\":\"tracker lookup fail\"},\"Identifier\":\"df36e2da-bc3e-4433-aa5c-91c6abb713f2\",\"Type\":\"MessageParticipant\",\"Transitions\":{\"NextAction\":\"c07bc2be-e195-44f3-a70b-66fe61eb83fb\"}},{\"Parameters\":{\"LoopCount\":\"100\"},\"Identifier\":\"3ce2ec90-a969-4179-a302-da42cd51875d\",\"Type\":\"Loop\",\"Transitions\":{\"NextAction\":\"faa33c01-f866-414a-9b25-bf337cff3a12\",\"Conditions\":[{\"NextAction\":\"d9a9c449-447b-4136-b0c7-8e07be999158\",\"Condition\":{\"Operator\":\"Equals\",\"Operands\":[\"ContinueLooping\"]}},{\"NextAction\":\"faa33c01-f866-414a-9b25-bf337cff3a12\",\"Condition\":{\"Operator\":\"Equals\",\"Operands\":[\"DoneLooping\"]}}]}},{\"Parameters\":{\"LambdaFunctionARN\":\"$.Attributes.get_results_function_arn\",\"InvocationTimeLimitSeconds\":\"3\",\"ResponseValidation\":{\"ResponseType\":\"STRING_MAP\"}},\"Identifier\":\"d9a9c449-447b-4136-b0c7-8e07be999158\",\"Type\":\"InvokeLambdaFunction\",\"Transitions\":{\"NextAction\":\"c3363a08-f6ad-4fe4-9562-18a31feebbb9\",\"Errors\":[{\"NextAction\":\"df36e2da-bc3e-4433-aa5c-91c6abb713f2\",\"ErrorType\":\"NoMatchingError\"}]}},{\"Parameters\":{\"Text\":\"Now that the function kicked off, we will hop into a loop, and check the tracker DB to see if there was a result.\"},\"Identifier\":\"5b3b2100-a040-4c41-8c1d-4488cd41a1f4\",\"Type\":\"MessageParticipant\",\"Transitions\":{\"NextAction\":\"3ce2ec90-a969-4179-a302-da42cd51875d\"}},{\"Parameters\":{\"Text\":\"async kickoff failed. bad things happened.\"},\"Identifier\":\"572feb8b-b843-4a54-9a39-53c72948c4e4\",\"Type\":\"MessageParticipant\",\"Transitions\":{\"NextAction\":\"cfb7ff34-a6eb-4f3a-be07-73ef6744bac5\"}},{\"Parameters\":{\"LambdaFunctionARN\":\"$.Attributes.write_tracker_function_arn\",\"InvocationTimeLimitSeconds\":\"3\",\"LambdaInvocationAttributes\":{\"payment_account\":\"987654321\"},\"ResponseValidation\":{\"ResponseType\":\"STRING_MAP\"}},\"Identifier\":\"01d08df9-46b3-406f-9e19-539d195a576b\",\"Type\":\"InvokeLambdaFunction\",\"Transitions\":{\"NextAction\":\"5b3b2100-a040-4c41-8c1d-4488cd41a1f4\",\"Errors\":[{\"NextAction\":\"572feb8b-b843-4a54-9a39-53c72948c4e4\",\"ErrorType\":\"NoMatchingError\"}]}},{\"Parameters\":{\"FlowLoggingBehavior\":\"Enabled\"},\"Identifier\":\"9b591740-5b62-4f40-ba6e-a027af9b6ad0\",\"Type\":\"UpdateFlowLoggingBehavior\",\"Transitions\":{\"NextAction\":\"d4073578-d663-4864-9319-e75a37e74495\"}},{\"Parameters\":{\"Text\":\"Welcome to the sample flow. First, we will initiate the launcher Lambda function. It will kick off the second function, which takes a while to execute, and immediately return a success.\"},\"Identifier\":\"e0473f0f-6a3f-4763-841d-cda2ee68858c\",\"Type\":\"MessageParticipant\",\"Transitions\":{\"NextAction\":\"01d08df9-46b3-406f-9e19-539d195a576b\"}},{\"Parameters\":{\"Attributes\":{\"write_tracker_function_arn\":\"${WRITE_REPLACE}\",\"get_results_function_arn\":\"${GET_REPLACE}\"},\"TargetContact\":\"Current\"},\"Identifier\":\"d4073578-d663-4864-9319-e75a37e74495\",\"Type\":\"UpdateContactAttributes\",\"Transitions\":{\"NextAction\":\"e0473f0f-6a3f-4763-841d-cda2ee68858c\",\"Errors\":[{\"NextAction\":\"e0473f0f-6a3f-4763-841d-cda2ee68858c\",\"ErrorType\":\"NoMatchingError\"}]}}]}"
        - WRITE_REPLACE: !GetAtt AsyncWriteFunction.Arn
          GET_REPLACE: !GetAtt AsyncGetResultFunction.Arn
      Description: Test flow for voicemail express that allows testing of both Salesforce cases and custom objects.
      InstanceArn:
        Ref: ConnectInstanceARN
      Name: AsyncSampleFlow
      State: ACTIVE
      Type: CONTACT_FLOW
  AsyncEventProcessorTrigger:
    Type: AWS::Lambda::EventSourceMapping
    DependsOn: 
      - AsyncEventProcessorFunction
      - AsyncTrackerTable
    Properties: 
      BatchSize: 100
      Enabled: true
      EventSourceArn: !GetAtt AsyncTrackerTable.StreamArn
      FunctionName: !GetAtt AsyncEventProcessorFunction.Arn
      StartingPosition: LATEST
  AssociateWriteFunction:
    Type: AWS::Connect::IntegrationAssociation
    Properties: 
      InstanceId: !Ref ConnectInstanceARN
      IntegrationArn: !GetAtt AsyncWriteFunction.Arn
      IntegrationType: LAMBDA_FUNCTION
  AssociateGetFunction:
    Type: AWS::Connect::IntegrationAssociation
    Properties: 
      InstanceId: !Ref ConnectInstanceARN
      IntegrationArn: !GetAtt AsyncGetResultFunction.Arn
      IntegrationType: LAMBDA_FUNCTION

Outputs:
  WriteTrackerLambdaFuntionARN:
    Description: ARN for the write tracker Lambda function
    Value: !GetAtt AsyncWriteFunction.Arn
  GetResultsLambdaFuntionARN:
    Description: ARN for the get get results Lambda function
    Value: !GetAtt AsyncGetResultFunction.Arn

      