// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 using System.Text.Json; using Amazon; using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.DataModel; using Amazon.Lambda.Core; using Amazon.Lambda.DynamoDBEvents; using Amazon.StepFunctions; using Amazon.StepFunctions.Model; 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 DynamoDBContextConfig = Amazon.DynamoDBv2.DataModel.DynamoDBContextConfig; namespace Unicorn.Properties.PropertiesService; /// <summary> /// Represents the AWS Lambda function that processes stream events from the /// Unicorn Properties Contract Status table /// </summary> public class PropertiesApprovalSyncFunction { private readonly AmazonStepFunctionsClient _amazonStepFunctionsClient; private readonly IDynamoDBContext _dynamoDbContext; /// <summary> /// Default constructor. Initialises global variables for function. /// </summary> public PropertiesApprovalSyncFunction() { // Instrument all AWS SDK calls AWSSDKHandler.RegisterXRayForAllServices(); // Initialise DynamoDB client var dynamodbTable = Environment.GetEnvironmentVariable("CONTRACT_STATUS_TABLE"); if (string.IsNullOrEmpty(dynamodbTable)) { throw new Exception("Environment variable CONTRACT_STATUS_TABLE is not defined."); } AWSConfigsDynamoDB.Context.TypeMappings[typeof(ContractStatusItem)] = new TypeMapping(typeof(ContractStatusItem), dynamodbTable); var config = new DynamoDBContextConfig { Conversion = DynamoDBEntryConversion.V2 }; _dynamoDbContext = new DynamoDBContext(new AmazonDynamoDBClient(), config); // Initialise Step Functions client _amazonStepFunctionsClient = new AmazonStepFunctionsClient(); } /// <summary> /// Testing constructor /// </summary> /// <param name="mockStepFunctionsClient"></param> /// <param name="mockDynamoDbContext"></param> public PropertiesApprovalSyncFunction(AmazonStepFunctionsClient mockStepFunctionsClient, IDynamoDBContext mockDynamoDbContext) { _dynamoDbContext = mockDynamoDbContext; _amazonStepFunctionsClient = mockStepFunctionsClient; } /// <summary> /// Event handler for processing DynamoDB stream data for changes to Contract Status /// </summary> /// <param name="dynamoEvent">Instance of <see cref="DynamoDBEvent"/></param> /// <param name="context">Lambda Context runtime methods and attributes</param> [Logging(LogEvent = true)] [Metrics(CaptureColdStart = true)] [Tracing(CaptureMode = TracingCaptureMode.ResponseAndError)] public async Task FunctionHandler(DynamoDBEvent? dynamoEvent, ILambdaContext context) { // process DDB foreach (var record in dynamoEvent.Records) { var propertyId = record.Dynamodb.Keys["PropertyId"].S; var item = await GetContractStatusItem(propertyId).ConfigureAwait(false); if (string.IsNullOrEmpty(item.SfnWaitApprovedTaskToken)) { Logger.LogInformation("Contract status has no approval token, nothing to sync"); return; } if (item.ContractStatus != "APPROVED") { Logger.LogInformation("Contract status for property is not APPROVED, cannot sync."); return; } if (item.ContractStatus == "APPROVED") { var sendTaskSuccessRequest = new SendTaskSuccessRequest { TaskToken = item.SfnWaitApprovedTaskToken, Output = JsonSerializer.Serialize(item) }; try { await _amazonStepFunctionsClient.SendTaskSuccessAsync(sendTaskSuccessRequest).ConfigureAwait(false); } catch (Exception e) { Logger.LogError(e); throw; } } else { Logger.LogInformation("Contract status for property is not APPROVED, cannot sync."); return; } } Logger.LogInformation("Property approval sync complete."); } /// <summary> /// Retrieves the contract status for a specifies property /// </summary> /// <param name="propertyId">Property ID</param> /// <returns>Instance of <see cref="ContractStatusItem"/></returns> /// <exception cref="ContractStatusNotFoundException"></exception> [Tracing(SegmentName = "Get Contract Status")] private async Task<ContractStatusItem> GetContractStatusItem(string propertyId) { ContractStatusItem? item; try { Logger.LogInformation($"Getting Contract Status for {propertyId}"); item = await _dynamoDbContext.LoadAsync<ContractStatusItem>(propertyId).ConfigureAwait(false); Logger.LogInformation($"Found contact: {item != null}"); } catch (Exception e) { Logger.LogInformation($"Error loading contract status {propertyId}: {e.Message}"); item = null; } if (item == null) { throw new ContractStatusNotFoundException($"Could not find property with ID: {propertyId}"); } return item; } }