// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0
using System.Text.Json;
using System.Text.RegularExpressions;
using Amazon;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;
using Amazon.DynamoDBv2.DocumentModel;
using Amazon.EventBridge;
using Amazon.EventBridge.Model;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using Amazon.Util;
using Amazon.XRay.Recorder.Handlers.AwsSdk;
using AWS.Lambda.Powertools.Logging;
using AWS.Lambda.Powertools.Metrics;
using AWS.Lambda.Powertools.Tracing;
using Unicorn.Web.Common;
using DynamoDBContextConfig = Amazon.DynamoDBv2.DataModel.DynamoDBContextConfig;
// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace Unicorn.Web.ApprovalService;
public class RequestApprovalFunction
{
private readonly IDynamoDBContext _dynamoDbContext;
private readonly IAmazonEventBridge _eventBindingClient;
private readonly string _eventBusName;
private readonly string _serviceNamespace;
///
/// Default constructor. Initialises global variables for function.
///
/// Init exception
public RequestApprovalFunction()
{
// Instrument all AWS SDK calls
AWSSDKHandler.RegisterXRayForAllServices();
var dynamodbTable = Environment.GetEnvironmentVariable("DYNAMODB_TABLE") ?? "";
if (string.IsNullOrEmpty(dynamodbTable))
throw new Exception("Environment variable DYNAMODB_TABLE is not defined.");
_eventBusName = Environment.GetEnvironmentVariable("EVENT_BUS") ?? "";
if (string.IsNullOrEmpty(_eventBusName))
throw new Exception("Environment variable EVENT_BUS is not defined.");
_serviceNamespace = Environment.GetEnvironmentVariable("SERVICE_NAMESPACE") ?? "";
if (string.IsNullOrEmpty(_eventBusName))
throw new Exception("Environment variable SERVICE_NAMESPACE is not defined.");
AWSConfigsDynamoDB.Context.TypeMappings[typeof(PropertyRecord)] =
new TypeMapping(typeof(PropertyRecord), dynamodbTable);
var config = new DynamoDBContextConfig { Conversion = DynamoDBEntryConversion.V2 };
_dynamoDbContext = new DynamoDBContext(new AmazonDynamoDBClient(), config);
_eventBindingClient = new AmazonEventBridgeClient();
}
///
/// Testing constructor for PropertySearchFunction
///
///
///
///
///
public RequestApprovalFunction(IDynamoDBContext dynamoDbContext,
IAmazonEventBridge eventBindingClient,
string eventBusName,
string serviceNamespace)
{
_dynamoDbContext = dynamoDbContext;
_eventBindingClient = eventBindingClient;
_eventBusName = eventBusName;
_serviceNamespace = serviceNamespace;
}
///
/// Lambda Handler for creating new Contracts.
///
/// API Gateway Lambda Proxy Request that triggers the function.
/// The context for the Lambda function.
/// API Gateway Lambda Proxy Response.
[Logging(LogEvent = true, CorrelationIdPath = CorrelationIdPaths.ApiGatewayRest)]
[Metrics(CaptureColdStart = true)]
[Tracing(CaptureMode = TracingCaptureMode.ResponseAndError)]
public async Task FunctionHandler(APIGatewayProxyRequest apigProxyEvent,
ILambdaContext context)
{
var response = new APIGatewayProxyResponse
{
Body = string.Empty,
StatusCode = 200,
Headers = new Dictionary
{
{ "Content-Type", "application/json" },
{ "X-Custom-Header", "application/json" }
}
};
string propertyId;
try
{
var request = JsonSerializer.Deserialize(apigProxyEvent.Body);
propertyId = request?.PropertyId ?? "";
Logger.LogInformation($"Requesting approval for property: {propertyId}");
}
catch (Exception e)
{
Logger.LogError(e);
var body = new Dictionary
{
{ "message", $"Unable to parse event input as JSON: {e.Message}" }
};
response.Body = JsonSerializer.Serialize(body);
response.StatusCode = 400;
return response;
}
var pattern = @"[a-z-]+\/[a-z-]+\/[a-z][a-z0-9-]*\/[0-9-]+";
if (string.IsNullOrWhiteSpace(propertyId) || !Regex.Match(propertyId, pattern).Success)
{
var body = new Dictionary
{
{ "message", $"Input invalid; must conform to regular expression: {pattern}" }
};
response.Body = JsonSerializer.Serialize(body);
response.StatusCode = 400;
return response;
}
var splitString = propertyId.Split('/');
var country = splitString[0];
var city = splitString[1];
var street = splitString[2];
var number = splitString[3];
var pk = PropertyRecordHelper.GetPartitionKey(country, city);
var sk = PropertyRecordHelper.GetSortKey(street, number);
try
{
var properties = await QueryTableAsync(pk, sk).ConfigureAwait(false);
if (!properties.Any())
{
var body = new Dictionary
{
{ "message", "No property found in database with the requested property id" }
};
response.Body = JsonSerializer.Serialize(body);
response.StatusCode = 500;
return response;
}
var property = properties.First();
if (string.Equals(property.Status, PropertyStatus.Approved, StringComparison.CurrentCultureIgnoreCase) ||
string.Equals(property.Status, PropertyStatus.Declined, StringComparison.CurrentCultureIgnoreCase) ||
string.Equals(property.Status, PropertyStatus.Pending, StringComparison.CurrentCultureIgnoreCase))
{
response.Body = JsonSerializer.Serialize(new Dictionary
{
{ "message", $"Property is already {property.Status}; no action taken" }
});
return response;
}
property.Status = PropertyStatus.Pending;
await SendEventAsync(propertyId, property).ConfigureAwait(false);
Logger.LogInformation($"Storing new property in DynamoDB with PK {pk} and SK {sk}");
await _dynamoDbContext.SaveAsync(property).ConfigureAwait(false);
Logger.LogInformation($"Stored item in DynamoDB;");
}
catch (Exception e)
{
Logger.LogError(e);
var body = new Dictionary
{
{ "message", e.Message }
};
response.Body = JsonSerializer.Serialize(body);
response.StatusCode = 500;
return response;
}
response.Body = JsonSerializer.Serialize(new Dictionary
{
{ "message", "Approval Requested" }
});
return response;
}
private async Task> QueryTableAsync(string partitionKey, string sortKey)
{
var filter = new QueryFilter(PropertyNames.PrimaryKey, QueryOperator.Equal, partitionKey);
filter.AddCondition(PropertyNames.SortKey, QueryOperator.BeginsWith, sortKey);
return await _dynamoDbContext
.FromQueryAsync(new QueryOperationConfig { Filter = filter })
.GetRemainingAsync()
.ConfigureAwait(false);
}
private async Task SendEventAsync(string propertyId, PropertyRecord property)
{
var requestApprovalEvent = new RequestApprovalEvent
{
PropertyId = propertyId,
Status = property.Status,
Images = property.Images,
Description = property.Description,
Address = new RequestApprovalEventAddress
{
Country = property.Country,
City = property.City,
Number = property.PropertyNumber
}
};
var message = new PutEventsRequestEntry
{
EventBusName = _eventBusName,
Resources = new List { propertyId },
Detail = JsonSerializer.Serialize(requestApprovalEvent),
DetailType = "PublicationApprovalRequested",
Source = _serviceNamespace
};
var putRequest = new PutEventsRequest
{
Entries = new List { message }
};
var response = await _eventBindingClient.PutEventsAsync(putRequest).ConfigureAwait(false);
Logger.LogInformation(response);
if (response.FailedEntryCount > 0)
{
throw new Exception($"Error sending requests to Event Bus; {response.FailedEntryCount} message(s) failed");
}
Logger.LogInformation(
$"Sent event to EventBridge; {response.FailedEntryCount} records failed; {response.Entries.Count} entries received");
Metrics.AddMetric("ApprovalsRequested", 1, MetricUnit.Count);
}
}