diff --git a/.gitignore b/.gitignore index d9c73d8..856e984 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ unicorn_approvals/ApprovalsService/target/** unicorn_contracts/ContractsService/target/** unicorn_web/ApprovalService/target/** unicorn_web/SearchService/target/** +unicorn_web/PublicationManagerService/target/** unicorn_web/Common/target/** **/.aws-sam/ .DS_Store** diff --git a/unicorn_approvals/ApprovalsService/pom.xml b/unicorn_approvals/ApprovalsService/pom.xml index 0fd849c..24f69f3 100644 --- a/unicorn_approvals/ApprovalsService/pom.xml +++ b/unicorn_approvals/ApprovalsService/pom.xml @@ -9,14 +9,14 @@ 17 17 - 2.27.21 - 1.18.0 - 3.13.0 - 5.13.0 + 2.32.29 + 1.20.2 + 3.16.1 + 5.18.0 4.13.2 - 1.1.1 - 1.2.3 - 2.27.21 + 1.1.2 + 1.3.0 + 2.32.29 @@ -81,29 +81,29 @@ com.fasterxml.jackson.core jackson-databind - 2.15.2 + 2.18.4 com.fasterxml.jackson.core jackson-core - 2.15.2 + 2.18.4 com.fasterxml.jackson.core jackson-annotations - 2.15.2 + 2.18.4 org.apache.logging.log4j log4j-api - 2.20.0 + 2.25.1 org.apache.logging.log4j log4j-core - 2.20.0 + 2.25.1 org.mockito @@ -132,7 +132,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.2 + 3.5.3 handler @@ -142,7 +142,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.4 + 3.6.0 @@ -157,11 +157,10 @@ dev.aspectj aspectj-maven-plugin - 1.13.1 + 1.14.1 - 17 - 17 17 + 17 software.amazon.lambda @@ -184,11 +183,18 @@ + + + org.aspectj + aspectjtools + 1.9.24 + + org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.14.0 17 17 diff --git a/unicorn_approvals/ApprovalsService/src/main/java/approvals/ContractStatusChangedHandlerFunction.java b/unicorn_approvals/ApprovalsService/src/main/java/approvals/ContractStatusChangedHandlerFunction.java index 7358dc0..0a82802 100644 --- a/unicorn_approvals/ApprovalsService/src/main/java/approvals/ContractStatusChangedHandlerFunction.java +++ b/unicorn_approvals/ApprovalsService/src/main/java/approvals/ContractStatusChangedHandlerFunction.java @@ -27,70 +27,60 @@ */ public class ContractStatusChangedHandlerFunction { - Logger logger = LogManager.getLogger(); - - final String TABLE_NAME = System.getenv("CONTRACT_STATUS_TABLE"); - - ObjectMapper objectMapper = new ObjectMapper(); - - DynamoDbClient dynamodbClient = DynamoDbClient.builder() - .build(); - - /** - * - * @param inputStream - * @param outputStream - * @param context - * @return - * @throws IOException - * - */ - @Tracing - @Metrics(captureColdStart = true) - @Logging(logEvent = true) - public void handleRequest(InputStream inputStream, OutputStream outputStream, - Context context) throws IOException { - - // deseralised and save contract status change in dynamodb table - - Event event = Marshaller.unmarshal(inputStream, - Event.class); - // save to database - ContractStatusChanged contractStatusChanged = event.getDetail(); - saveContractStatus(contractStatusChanged.getPropertyId(), contractStatusChanged.getContractStatus(), - contractStatusChanged.getContractId(), - contractStatusChanged.getContractLastModifiedOn()); - - OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); - writer.write(objectMapper.writeValueAsString(event.getDetail())); - writer.close(); - } - - @Tracing - void saveContractStatus(String propertyId, - String contractStatus, String contractId, Long contractLastModifiedOn) { - Map key = new HashMap(); - AttributeValue keyvalue = AttributeValue.fromS(propertyId); - key.put("property_id", keyvalue); - - Map expressionAttributeValues = new HashMap(); - expressionAttributeValues.put(":t", AttributeValue.fromS(contractStatus)); - expressionAttributeValues.put(":c", AttributeValue.fromS(contractId)); - expressionAttributeValues.put(":m", AttributeValue - .fromN(String.valueOf(contractLastModifiedOn))); - - UpdateItemRequest updateItemRequest = UpdateItemRequest.builder() - .key(key) - .tableName(TABLE_NAME) - .updateExpression( - "set contract_status=:t, contract_last_modified_on=:m, contract_id=:c") - .expressionAttributeValues(expressionAttributeValues) - .build(); - - dynamodbClient.updateItem(updateItemRequest); - } - - public void setDynamodbClient(DynamoDbClient dynamodbClient) { - this.dynamodbClient = dynamodbClient; + private static final Logger logger = LogManager.getLogger(); + private static final String TABLE_NAME = System.getenv("CONTRACT_STATUS_TABLE"); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private DynamoDbClient dynamodbClient = DynamoDbClient.builder().build(); + + /** + * Handles contract status change events from EventBridge + * + * @param inputStream the input stream containing the event + * @param outputStream the output stream for the response + * @param context the Lambda context + * @throws IOException if there's an error processing the event + */ + @Tracing + @Metrics(captureColdStart = true) + @Logging(logEvent = true) + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { + Event event = Marshaller.unmarshal(inputStream, Event.class); + ContractStatusChanged contractStatusChanged = event.getDetail(); + + saveContractStatus( + contractStatusChanged.getPropertyId(), + contractStatusChanged.getContractStatus(), + contractStatusChanged.getContractId(), + contractStatusChanged.getContractLastModifiedOn() + ); + + try (OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) { + writer.write(objectMapper.writeValueAsString(event.getDetail())); } + } + + @Tracing + void saveContractStatus(String propertyId, String contractStatus, String contractId, Long contractLastModifiedOn) { + Map key = Map.of("property_id", AttributeValue.fromS(propertyId)); + + Map expressionAttributeValues = Map.of( + ":t", AttributeValue.fromS(contractStatus), + ":c", AttributeValue.fromS(contractId), + ":m", AttributeValue.fromN(String.valueOf(contractLastModifiedOn)) + ); + + UpdateItemRequest updateItemRequest = UpdateItemRequest.builder() + .key(key) + .tableName(TABLE_NAME) + .updateExpression("set contract_status=:t, contract_last_modified_on=:m, contract_id=:c") + .expressionAttributeValues(expressionAttributeValues) + .build(); + + dynamodbClient.updateItem(updateItemRequest); + } + + public void setDynamodbClient(DynamoDbClient dynamodbClient) { + this.dynamodbClient = dynamodbClient; + } } diff --git a/unicorn_approvals/ApprovalsService/src/main/java/approvals/PropertiesApprovalSyncFunction.java b/unicorn_approvals/ApprovalsService/src/main/java/approvals/PropertiesApprovalSyncFunction.java index 5d70818..afcfb45 100644 --- a/unicorn_approvals/ApprovalsService/src/main/java/approvals/PropertiesApprovalSyncFunction.java +++ b/unicorn_approvals/ApprovalsService/src/main/java/approvals/PropertiesApprovalSyncFunction.java @@ -20,69 +20,41 @@ import java.io.Serializable; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; +/** + * Lambda function that processes DynamoDB stream events to sync property approval status + * with Step Functions workflows + */ public class PropertiesApprovalSyncFunction implements RequestHandler { - Logger logger = LogManager.getLogger(); - SfnAsyncClient snfClient = SfnAsyncClient.builder() - .httpClientBuilder(NettyNioAsyncHttpClient.builder() - .maxConcurrency(100) - .maxPendingConnectionAcquires(10_000)) - .build(); + private static final Logger logger = LogManager.getLogger(); + private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final String APPROVED_STATUS = "APPROVED"; + + private final SfnAsyncClient sfnClient = SfnAsyncClient.builder() + .httpClientBuilder(NettyNioAsyncHttpClient.builder() + .maxConcurrency(100) + .maxPendingConnectionAcquires(10_000)) + .build(); @Tracing @Metrics(captureColdStart = true) @Logging(logEvent = true) public StreamsEventResponse handleRequest(DynamodbEvent input, Context context) { - List batchItemFailures = new ArrayList<>(); - String curRecordSequenceNumber = ""; for (DynamodbEvent.DynamodbStreamRecord dynamodbStreamRecord : input.getRecords()) { + String sequenceNumber = dynamodbStreamRecord.getDynamodb().getSequenceNumber(); + try { - // Process your record - - StreamRecord dynamodbRecord = dynamodbStreamRecord.getDynamodb(); - curRecordSequenceNumber = dynamodbRecord.getSequenceNumber(); - Map newImage = dynamodbRecord.getNewImage(); - Map oldImage = dynamodbRecord.getOldImage(); - if (oldImage == null) { - oldImage = new HashMap(); - } - if (newImage == null) { - logger.debug("New image is null. Hence return empty stream response"); - return new StreamsEventResponse(); - } - // if there is no token do nothing - if (newImage.get("sfn_wait_approved_task_token") == null - && oldImage.get("sfn_wait_approved_task_token") == null) { - logger.debug("No task token in both the images. Hence return empty stream response"); - return new StreamsEventResponse(); - } - - // if contract status is approved, send the task token - - if (!newImage.get("contract_status").getS().equalsIgnoreCase("APPROVED")) { - logger.debug("Contract status for property is not APPROVED : " + - newImage.get("property_id").getS()); - return new StreamsEventResponse(); + if (!processRecord(dynamodbStreamRecord)) { + continue; // Skip this record but don't fail } - logger.debug("Contract status for property is APPROVED : " + - newImage.get("property_id").getS()); - - // send task successful token - taskSuccessful(newImage.get("sfn_wait_approved_task_token").getS(), newImage); - } catch (Exception e) { - /* - * Since we are working with streams, we can return the failed item immediately. - * Lambda will immediately begin to retry processing from this failed item - * onwards. - */ - batchItemFailures.add(new StreamsEventResponse.BatchItemFailure(curRecordSequenceNumber)); + logger.error("Failed to process record with sequence number: {}", sequenceNumber, e); + batchItemFailures.add(new StreamsEventResponse.BatchItemFailure(sequenceNumber)); return new StreamsEventResponse(batchItemFailures); } } @@ -90,21 +62,55 @@ public StreamsEventResponse handleRequest(DynamodbEvent input, Context context) return new StreamsEventResponse(); } - private void taskSuccessful(String s, Map item) throws JsonProcessingException { - // create the json structure and send the token - ObjectMapper mapper = new ObjectMapper(); - ContractStatus contractStatus = new ContractStatus(); - contractStatus.setContract_id(item.get("contract_id").getS()); - contractStatus.setContract_status(item.get("contract_status").getS()); - contractStatus.setProperty_id(item.get("property_id").getS()); - contractStatus.setSfn_wait_approved_task_token(item.get("sfn_wait_approved_task_token").getS()); - String taskResult = mapper.writeValueAsString(contractStatus); + private boolean processRecord(DynamodbEvent.DynamodbStreamRecord streamRecord) throws JsonProcessingException { + StreamRecord dynamodbRecord = streamRecord.getDynamodb(); + Map newImage = dynamodbRecord.getNewImage(); + Map oldImage = dynamodbRecord.getOldImage(); - SendTaskSuccessRequest request = SendTaskSuccessRequest.builder() - .taskToken(contractStatus.getSfn_wait_approved_task_token()) - .output(taskResult) - .build(); - snfClient.sendTaskSuccess(request).join(); + if (newImage == null) { + logger.debug("New image is null, skipping record"); + return false; + } + + if (!hasTaskToken(newImage, oldImage)) { + logger.debug("No task token found in either image, skipping record"); + return false; + } + + String contractStatus = newImage.get("contract_status").getS(); + String propertyId = newImage.get("property_id").getS(); + + if (!APPROVED_STATUS.equalsIgnoreCase(contractStatus)) { + logger.debug("Contract status for property {} is not APPROVED: {}", propertyId, contractStatus); + return false; + } + + logger.info("Contract approved for property: {}", propertyId); + sendTaskSuccess(newImage.get("sfn_wait_approved_task_token").getS(), newImage); + return true; + } + + private boolean hasTaskToken(Map newImage, Map oldImage) { + return (newImage.get("sfn_wait_approved_task_token") != null) || + (oldImage != null && oldImage.get("sfn_wait_approved_task_token") != null); + } + + private void sendTaskSuccess(String taskToken, Map item) throws JsonProcessingException { + ContractStatus contractStatus = ContractStatus.builder() + .contractId(item.get("contract_id").getS()) + .contractStatus(item.get("contract_status").getS()) + .propertyId(item.get("property_id").getS()) + .sfnWaitApprovedTaskToken(item.get("sfn_wait_approved_task_token").getS()) + .build(); + String taskResult = objectMapper.writeValueAsString(contractStatus); + + SendTaskSuccessRequest request = SendTaskSuccessRequest.builder() + .taskToken(taskToken) + .output(taskResult) + .build(); + + sfnClient.sendTaskSuccess(request).join(); + logger.info("Task success sent for property: {}", contractStatus.getPropertyId()); } } \ No newline at end of file diff --git a/unicorn_approvals/ApprovalsService/src/main/java/approvals/WaitForContractApprovalFunction.java b/unicorn_approvals/ApprovalsService/src/main/java/approvals/WaitForContractApprovalFunction.java index 7d0202e..c1f5f3e 100644 --- a/unicorn_approvals/ApprovalsService/src/main/java/approvals/WaitForContractApprovalFunction.java +++ b/unicorn_approvals/ApprovalsService/src/main/java/approvals/WaitForContractApprovalFunction.java @@ -19,89 +19,77 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletionException; /** - * Lambda handler to update the contract status change + * Lambda handler to wait for contract approval in Step Functions workflow */ public class WaitForContractApprovalFunction { - Logger logger = LogManager.getLogger(); - - final String TABLE_NAME = System.getenv("CONTRACT_STATUS_TABLE"); - - DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder() - .httpClientBuilder(NettyNioAsyncHttpClient.builder() - .maxConcurrency(100) - .maxPendingConnectionAcquires(10_000)) - .build(); - - @Tracing - @Metrics(captureColdStart = true) - @Logging(logEvent = true) - public void handleRequest(InputStream inputStream, OutputStream outputStream, - Context context) throws IOException, ContractStatusNotFoundException { - - // deseralised to contract status - ObjectMapper objectMapper = new ObjectMapper(); - String srtInput = new String(inputStream.readAllBytes()); - JsonNode event = objectMapper.readTree(srtInput); - String propertyId = event.get("Input").get("property_id").asText(); - String taskToken = event.get("TaskToken").asText(); - - logger.info("task Token : ", taskToken); - logger.info("Property Id : ", propertyId); - - // get contract status - Map dynamodbItem = getContractStatus(propertyId); - updateTokenAndPauseExecution(taskToken, dynamodbItem.get("property_id").s()); - - String responseString = event.get("Input").asText(); - OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); - logger.debug(responseString); - writer.write(responseString); - writer.close(); - + private static final Logger logger = LogManager.getLogger(); + private static final String TABLE_NAME = System.getenv("CONTRACT_STATUS_TABLE"); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private final DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder() + .httpClientBuilder(NettyNioAsyncHttpClient.builder() + .maxConcurrency(100) + .maxPendingConnectionAcquires(10_000)) + .build(); + + @Tracing + @Metrics(captureColdStart = true) + @Logging(logEvent = true) + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) + throws IOException, ContractStatusNotFoundException { + + String input = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + JsonNode event = objectMapper.readTree(input); + + String propertyId = event.get("Input").get("property_id").asText(); + String taskToken = event.get("TaskToken").asText(); + + logger.info("Processing property: {} with task token: {}", propertyId, taskToken); + + Map contractItem = getContractStatus(propertyId); + updateTokenAndPauseExecution(taskToken, contractItem.get("property_id").s()); + + String responseString = event.get("Input").toString(); + try (OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) { + writer.write(responseString); } - - private void updateTokenAndPauseExecution(String taskToken, String propertyId) { - Map key = new HashMap(); - AttributeValue keyvalue = AttributeValue.fromS(propertyId); - key.put("property_id", keyvalue); - - Map expressionAttributeValues = new HashMap(); - expressionAttributeValues.put(":g", AttributeValue.fromS(taskToken)); - - UpdateItemRequest updateItemRequest = UpdateItemRequest.builder() - .key(key) - .tableName(TABLE_NAME) - .updateExpression( - "set sfn_wait_approved_task_token = :g") - .expressionAttributeValues(expressionAttributeValues) - .build(); - dynamodbClient.updateItem(updateItemRequest).join(); - } - - private Map getContractStatus(String propertyId) - throws ContractStatusNotFoundException { - HashMap keyToGet = new HashMap(); - - keyToGet.put("property_id", AttributeValue.builder() - .s(propertyId).build()); - - GetItemRequest request = GetItemRequest.builder() - .key(keyToGet) - .tableName(TABLE_NAME) - .build(); - Map returnvalue = null; - try { - returnvalue = dynamodbClient.getItem(request).join().item(); - } catch (Exception exception) { - throw new ContractStatusNotFoundException(exception.getLocalizedMessage()); - } - - return returnvalue; + } + + private void updateTokenAndPauseExecution(String taskToken, String propertyId) { + Map key = Map.of("property_id", AttributeValue.fromS(propertyId)); + Map expressionAttributeValues = Map.of(":g", AttributeValue.fromS(taskToken)); + + UpdateItemRequest updateItemRequest = UpdateItemRequest.builder() + .key(key) + .tableName(TABLE_NAME) + .updateExpression("set sfn_wait_approved_task_token = :g") + .expressionAttributeValues(expressionAttributeValues) + .build(); + + dynamodbClient.updateItem(updateItemRequest).join(); + } + + private Map getContractStatus(String propertyId) throws ContractStatusNotFoundException { + Map key = Map.of("property_id", AttributeValue.fromS(propertyId)); + + GetItemRequest request = GetItemRequest.builder() + .key(key) + .tableName(TABLE_NAME) + .build(); + + try { + Map item = dynamodbClient.getItem(request).join().item(); + if (item == null || item.isEmpty()) { + throw new ContractStatusNotFoundException("Contract status not found for property: " + propertyId); + } + return item; + } catch (CompletionException e) { + throw new ContractStatusNotFoundException("Failed to retrieve contract status: " + e.getCause().getMessage()); } - + } } diff --git a/unicorn_approvals/ApprovalsService/src/main/java/approvals/dao/ContractStatus.java b/unicorn_approvals/ApprovalsService/src/main/java/approvals/dao/ContractStatus.java index df99fcd..7244edc 100644 --- a/unicorn_approvals/ApprovalsService/src/main/java/approvals/dao/ContractStatus.java +++ b/unicorn_approvals/ApprovalsService/src/main/java/approvals/dao/ContractStatus.java @@ -1,47 +1,89 @@ package approvals.dao; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Data class representing contract status information + */ public class ContractStatus { - String contract_id; - String contract_status; - String property_id; - String sfn_wait_approved_task_token; + + @JsonProperty("contract_id") + private final String contractId; + + @JsonProperty("contract_status") + private final String contractStatus; + + @JsonProperty("property_id") + private final String propertyId; + + @JsonProperty("sfn_wait_approved_task_token") + private final String sfnWaitApprovedTaskToken; - @Override - public String toString() { - return "Property [contract_id=" + contract_id + ", contract_status=" + contract_status + ", property_id=" - + property_id + ", sfn_wait_approved_task_token=" + sfn_wait_approved_task_token + "]"; + private ContractStatus(Builder builder) { + this.contractId = builder.contractId; + this.contractStatus = builder.contractStatus; + this.propertyId = builder.propertyId; + this.sfnWaitApprovedTaskToken = builder.sfnWaitApprovedTaskToken; } - public String getContract_id() { - return contract_id; + public String getContractId() { + return contractId; } - public void setContract_id(String contract_id) { - this.contract_id = contract_id; + public String getContractStatus() { + return contractStatus; } - public String getContract_status() { - return contract_status; + public String getPropertyId() { + return propertyId; } - public void setContract_status(String contract_status) { - this.contract_status = contract_status; + public String getSfnWaitApprovedTaskToken() { + return sfnWaitApprovedTaskToken; } - public String getProperty_id() { - return property_id; + public static Builder builder() { + return new Builder(); } - public void setProperty_id(String property_id) { - this.property_id = property_id; - } + public static class Builder { + private String contractId; + private String contractStatus; + private String propertyId; + private String sfnWaitApprovedTaskToken; - public String getSfn_wait_approved_task_token() { - return sfn_wait_approved_task_token; - } + public Builder contractId(String contractId) { + this.contractId = contractId; + return this; + } + + public Builder contractStatus(String contractStatus) { + this.contractStatus = contractStatus; + return this; + } - public void setSfn_wait_approved_task_token(String sfn_wait_approved_task_token) { - this.sfn_wait_approved_task_token = sfn_wait_approved_task_token; + public Builder propertyId(String propertyId) { + this.propertyId = propertyId; + return this; + } + + public Builder sfnWaitApprovedTaskToken(String sfnWaitApprovedTaskToken) { + this.sfnWaitApprovedTaskToken = sfnWaitApprovedTaskToken; + return this; + } + + public ContractStatus build() { + return new ContractStatus(this); + } } + @Override + public String toString() { + return "ContractStatus{" + + "contractId='" + contractId + '\'' + + ", contractStatus='" + contractStatus + '\'' + + ", propertyId='" + propertyId + '\'' + + ", sfnWaitApprovedTaskToken='" + sfnWaitApprovedTaskToken + '\'' + + '}'; + } } diff --git a/unicorn_approvals/ApprovalsService/src/test/java/approvals/ContractStatusTests.java b/unicorn_approvals/ApprovalsService/src/test/java/approvals/ContractStatusTests.java index 63caac2..c8e01da 100644 --- a/unicorn_approvals/ApprovalsService/src/test/java/approvals/ContractStatusTests.java +++ b/unicorn_approvals/ApprovalsService/src/test/java/approvals/ContractStatusTests.java @@ -2,53 +2,50 @@ import com.amazonaws.services.lambda.runtime.Context; import org.junit.Before; -import org.junit.jupiter.api.Test; +import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; -import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import java.io.*; -import java.util.HashMap; -import java.util.Map; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; @RunWith(MockitoJUnitRunner.class) public class ContractStatusTests { - Context context; - DynamoDbClient client; - - ContractStatusChangedHandlerFunction contractStatusChangedHandler; - - Map response = new HashMap(); - - @Before - public void setUp() { - - context = mock(Context.class); - client = mock(DynamoDbClient.class); - - } - - @Test - public void validStatusCheckEvent() throws IOException { - - contractStatusChangedHandler = new ContractStatusChangedHandlerFunction(); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - File resourceFile = new File("src/test/events/lambda/contract_status_changed.json"); - client = mock(DynamoDbClient.class); - contractStatusChangedHandler.setDynamodbClient(client); - - FileInputStream fis = new FileInputStream(resourceFile); - - contractStatusChangedHandler.handleRequest(fis, outputStream, context); - ByteArrayInputStream inStream = new ByteArrayInputStream(outputStream.toByteArray()); - String response = new String(inStream.readAllBytes()); - assertTrue("Successful Response", response.contains("contract_id")); - - } - + @Mock + private Context context; + + @Mock + private DynamoDbClient dynamoDbClient; + + private ContractStatusChangedHandlerFunction contractStatusChangedHandler; + + @Before + public void setUp() { + contractStatusChangedHandler = new ContractStatusChangedHandlerFunction(); + contractStatusChangedHandler.setDynamodbClient(dynamoDbClient); + } + + @Test + public void shouldProcessValidContractStatusChangeEvent() throws IOException { + // Given + Path testEventPath = Paths.get("src/test/events/lambda/contract_status_changed.json"); + + // When + try (InputStream inputStream = Files.newInputStream(testEventPath); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + + contractStatusChangedHandler.handleRequest(inputStream, outputStream, context); + + // Then + String response = outputStream.toString(); + assertTrue("Response should contain contract_id", response.contains("contract_id")); + } + } } diff --git a/unicorn_contracts/ContractsService/pom.xml b/unicorn_contracts/ContractsService/pom.xml index f9b730e..ebd87fd 100644 --- a/unicorn_contracts/ContractsService/pom.xml +++ b/unicorn_contracts/ContractsService/pom.xml @@ -9,12 +9,12 @@ 17 17 - 2.27.21 - 1.18.0 - 3.13.0 - 5.13.0 + 2.32.29 + 1.20.2 + 3.16.1 + 5.18.0 4.13.2 - 1.1.1 + 1.1.2 @@ -37,36 +37,36 @@ com.amazonaws aws-lambda-java-core - 1.2.2 + 1.3.0 com.fasterxml.jackson.core jackson-databind - 2.15.2 + 2.18.4 com.fasterxml.jackson.core jackson-core - 2.15.2 + 2.18.4 com.fasterxml.jackson.core jackson-annotations - 2.15.2 + 2.18.4 org.apache.logging.log4j log4j-api - 2.20.0 + 2.25.1 org.apache.logging.log4j log4j-core - 2.20.0 + 2.25.1 @@ -123,17 +123,25 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.2 + 3.5.3 + false handler + + + org.apache.maven.surefire + surefire-junit4 + 3.5.3 + + org.apache.maven.plugins maven-shade-plugin - 3.2.4 + 3.6.0 @@ -146,13 +154,13 @@ - dev.aspectj + org.codehaus.mojo aspectj-maven-plugin - 1.13.1 + 1.15.0 + 17 17 17 - 17 software.amazon.lambda @@ -168,6 +176,13 @@ + + + org.aspectj + aspectjtools + 1.9.22.1 + + @@ -179,9 +194,10 @@ org.apache.maven.plugins maven-compiler-plugin + 3.14.0 - 16 - 16 + 17 + 17 diff --git a/unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandler.java b/unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandler.java index e6c3722..c1f2918 100644 --- a/unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandler.java +++ b/unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandler.java @@ -12,139 +12,186 @@ import org.apache.logging.log4j.Logger; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.*; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; -import software.amazon.lambda.powertools.metrics.MetricsUtils; import software.amazon.lambda.powertools.tracing.Tracing; -import java.util.Date; -import java.util.HashMap; +import java.time.Instant; import java.util.Map; +import java.util.Optional; import java.util.UUID; public class ContractEventHandler implements RequestHandler { - // Initialise environment variables - private static String DDB_TABLE = System.getenv("DYNAMODB_TABLE"); - ObjectMapper objectMapper = new ObjectMapper(); + private static final String DDB_TABLE = System.getenv("DYNAMODB_TABLE"); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final Logger LOGGER = LogManager.getLogger(ContractEventHandler.class); + private static final String HTTP_METHOD_ATTR = "HttpMethod"; - DynamoDbClient dynamodbClient = DynamoDbClient.builder() - .build(); + private final DynamoDbClient dynamodbClient; - Logger logger = LogManager.getLogger(); - MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); + public ContractEventHandler() { + this(DynamoDbClient.builder().build()); + } + + public ContractEventHandler(DynamoDbClient dynamodbClient) { + this.dynamodbClient = dynamodbClient; + } @Override + @Tracing public Void handleRequest(SQSEvent event, Context context) { + if (event == null || event.getRecords() == null) { + LOGGER.warn("Received null or empty SQS event"); + return null; + } for (SQSMessage msg : event.getRecords()) { - // cehck in message attributes about the http method (HttpMethod) - logger.debug(msg.toString()); - String httpMethod = msg.getMessageAttributes().get("HttpMethod").getStringValue(); - if ("POST".equalsIgnoreCase(httpMethod)) { - try { - createContract(msg.getBody()); - logger.debug("Contract Saved"); - } catch (JsonProcessingException jsonException) { - logger.error("Unknown Exception occoured: " + jsonException.getMessage()); - logger.fatal(jsonException); - jsonException.printStackTrace(); - } - - } else if ("PUT".equalsIgnoreCase(httpMethod)) { - try { - // update the event - updateContract(msg.getBody()); - } catch (JsonProcessingException jsonException) { - logger.error("Unknown Exception occoured: " + jsonException.getMessage()); - logger.fatal(jsonException); - jsonException.printStackTrace(); - } + processMessage(msg); + } + return null; + } + private void processMessage(SQSMessage msg) { + LOGGER.debug("Processing message: {}", msg.getMessageId()); + + try { + String httpMethod = extractHttpMethod(msg); + String body = msg.getBody(); + + if (body == null || body.trim().isEmpty()) { + LOGGER.warn("Empty message body for message: {}", msg.getMessageId()); + return; } + switch (httpMethod.toUpperCase()) { + case "POST": + createContract(body); + LOGGER.info("Contract created successfully for message: {}", msg.getMessageId()); + break; + case "PUT": + updateContract(body); + LOGGER.info("Contract updated successfully for message: {}", msg.getMessageId()); + break; + default: + LOGGER.warn("Unsupported HTTP method: {} for message: {}", httpMethod, msg.getMessageId()); + } + } catch (Exception e) { + LOGGER.error("Error processing message {}: {}", msg.getMessageId(), e.getMessage(), e); + throw new RuntimeException("Failed to process contract message", e); } - return null; + } + + private String extractHttpMethod(SQSMessage msg) { + return Optional.ofNullable(msg.getMessageAttributes()) + .map(attrs -> attrs.get(HTTP_METHOD_ATTR)) + .map(attr -> attr.getStringValue()) + .orElseThrow(() -> new IllegalArgumentException("Missing HttpMethod attribute")); } @Tracing - private void createContract(String strContract) throws JsonProcessingException { + private void createContract(String contractJson) throws JsonProcessingException { + Contract contract = OBJECT_MAPPER.readValue(contractJson, Contract.class); + validateContract(contract); + String contractId = UUID.randomUUID().toString(); - Long createDate = new Date().getTime(); - Contract contract = objectMapper.readValue(strContract, Contract.class); - - Map expressionValues = new HashMap<>(); - expressionValues.put(":cancelled", AttributeValue.builder().s(ContractStatusEnum.CANCELLED.name()).build()); - expressionValues.put(":closed", AttributeValue.builder().s(ContractStatusEnum.CLOSED.name()).build()); - expressionValues.put(":expired", AttributeValue.builder().s(ContractStatusEnum.EXPIRED.name()).build()); - - HashMap itemValues = new HashMap<>(); - itemValues.put("property_id", AttributeValue.builder().s(contract.getPropertyId()).build()); - itemValues.put("seller_name", AttributeValue.builder().s(contract.getSellerName()).build()); - itemValues.put("contract_created", - AttributeValue.builder().n(createDate.toString()).build()); - itemValues.put("contract_last_modified_on", - AttributeValue.builder().n(createDate.toString()).build()); - itemValues.put("contract_id", AttributeValue.builder().s(contractId).build()); - itemValues.put("contract_status", AttributeValue.builder().s(ContractStatusEnum.DRAFT.name()).build()); - - HashMap address = new HashMap<>(); - address.put("country", AttributeValue.builder().s(contract.getAddress().getCountry()).build()); - address.put("city", AttributeValue.builder().s(contract.getAddress().getCity()).build()); - address.put("street", AttributeValue.builder().s(contract.getAddress().getStreet()).build()); - address.put("number", - AttributeValue.builder().n(Integer.valueOf(contract.getAddress().getNumber()).toString()).build()); - - itemValues.put("address", - AttributeValue.builder().m(address).build()); - PutItemRequest putItemRequest = PutItemRequest.builder().tableName(DDB_TABLE) - .item(itemValues) - .conditionExpression( - "attribute_not_exists(property_id) OR contract_status IN (:cancelled , :closed, :expired)") + long timestamp = Instant.now().toEpochMilli(); + + Map item = Map.of( + "property_id", AttributeValue.builder().s(contract.getPropertyId()).build(), + "seller_name", AttributeValue.builder().s(contract.getSellerName()).build(), + "contract_created", AttributeValue.builder().n(String.valueOf(timestamp)).build(), + "contract_last_modified_on", AttributeValue.builder().n(String.valueOf(timestamp)).build(), + "contract_id", AttributeValue.builder().s(contractId).build(), + "contract_status", AttributeValue.builder().s(ContractStatusEnum.DRAFT.name()).build(), + "address", AttributeValue.builder().m(buildAddressMap(contract.getAddress())).build() + ); + + Map expressionValues = Map.of( + ":cancelled", AttributeValue.builder().s(ContractStatusEnum.CANCELLED.name()).build(), + ":closed", AttributeValue.builder().s(ContractStatusEnum.CLOSED.name()).build(), + ":expired", AttributeValue.builder().s(ContractStatusEnum.EXPIRED.name()).build() + ); + + PutItemRequest request = PutItemRequest.builder() + .tableName(DDB_TABLE) + .item(item) + .conditionExpression("attribute_not_exists(property_id) OR contract_status IN (:cancelled, :closed, :expired)") .expressionAttributeValues(expressionValues) .build(); + try { - dynamodbClient.putItem(putItemRequest); - } catch (ConditionalCheckFailedException conditionalCheckFailedException) { - logger.error("Unable to create contract for Property '" + contract.getPropertyId() - + "'.There already is a contract for this property in status " + ContractStatusEnum.DRAFT + " or " - + ContractStatusEnum.APPROVED); + dynamodbClient.putItem(request); + } catch (ConditionalCheckFailedException e) { + LOGGER.error("Active contract already exists for property: {}", contract.getPropertyId()); + throw new IllegalStateException("Contract already exists for property: " + contract.getPropertyId(), e); } } @Tracing - private void updateContract(String strContract) throws JsonProcessingException { - Contract contract = objectMapper.readValue(strContract, Contract.class); - logger.info("Property ID is : " + contract.getPropertyId()); - HashMap itemKey = new HashMap<>(); - - itemKey.put("property_id", AttributeValue.builder().s(contract.getPropertyId()).build()); - - Map expressionAttributeValues = new HashMap<>(); - expressionAttributeValues.put(":draft", AttributeValue.builder().s(ContractStatusEnum.DRAFT.name()).build()); - expressionAttributeValues.put(":t", AttributeValue.builder().s(ContractStatusEnum.APPROVED.name()).build()); - expressionAttributeValues.put(":m", AttributeValue.builder().s(String.valueOf(new Date().getTime())).build()); + private void updateContract(String contractJson) throws JsonProcessingException { + Contract contract = OBJECT_MAPPER.readValue(contractJson, Contract.class); + validateContractForUpdate(contract); + + LOGGER.info("Updating contract for Property ID: {}", contract.getPropertyId()); + + Map key = Map.of( + "property_id", AttributeValue.builder().s(contract.getPropertyId()).build() + ); + + Map expressionValues = Map.of( + ":draft", AttributeValue.builder().s(ContractStatusEnum.DRAFT.name()).build(), + ":approved", AttributeValue.builder().s(ContractStatusEnum.APPROVED.name()).build(), + ":modifiedDate", AttributeValue.builder().n(String.valueOf(Instant.now().toEpochMilli())).build() + ); UpdateItemRequest request = UpdateItemRequest.builder() .tableName(DDB_TABLE) - .key(itemKey) - .updateExpression("set contract_status=:t, modified_date=:m") - .expressionAttributeValues(expressionAttributeValues) - .conditionExpression( - "attribute_exists(property_id) AND contract_status IN (:draft)") + .key(key) + .updateExpression("SET contract_status = :approved, contract_last_modified_on = :modifiedDate") + .expressionAttributeValues(expressionValues) + .conditionExpression("attribute_exists(property_id) AND contract_status = :draft") .build(); + try { dynamodbClient.updateItem(request); - } catch (ConditionalCheckFailedException conditionalCheckFailedException) { - logger.error("Unable to update contract for Property '" + contract.getPropertyId() - + "'.Status is not in " + ContractStatusEnum.DRAFT); - } catch (ResourceNotFoundException conditionalCheckFailedException) { - logger.error("Unable to update contract for Property '" + contract.getPropertyId() - + "'. Not Found"); + } catch (ConditionalCheckFailedException e) { + LOGGER.error("Contract not in DRAFT status for property: {}", contract.getPropertyId()); + throw new IllegalStateException("Contract not in valid state for update: " + contract.getPropertyId(), e); + } catch (ResourceNotFoundException e) { + LOGGER.error("Contract not found for property: {}", contract.getPropertyId()); + throw new IllegalArgumentException("Contract not found: " + contract.getPropertyId(), e); } } - public void setDynamodbClient(DynamoDbClient dynamodbClient) { - this.dynamodbClient = dynamodbClient; + private void validateContract(Contract contract) { + if (contract == null) { + throw new IllegalArgumentException("Contract cannot be null"); + } + if (contract.getPropertyId() == null || contract.getPropertyId().trim().isEmpty()) { + throw new IllegalArgumentException("Property ID is required"); + } + if (contract.getSellerName() == null || contract.getSellerName().trim().isEmpty()) { + throw new IllegalArgumentException("Seller name is required"); + } + if (contract.getAddress() == null) { + throw new IllegalArgumentException("Address is required"); + } } + private void validateContractForUpdate(Contract contract) { + if (contract == null) { + throw new IllegalArgumentException("Contract cannot be null"); + } + if (contract.getPropertyId() == null || contract.getPropertyId().trim().isEmpty()) { + throw new IllegalArgumentException("Property ID is required for update"); + } + } + + private Map buildAddressMap(contracts.utils.Address address) { + return Map.of( + "country", AttributeValue.builder().s(address.getCountry()).build(), + "city", AttributeValue.builder().s(address.getCity()).build(), + "street", AttributeValue.builder().s(address.getStreet()).build(), + "number", AttributeValue.builder().n(String.valueOf(address.getNumber())).build() + ); + } } diff --git a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Address.java b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Address.java index ec1be8d..d44a398 100644 --- a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Address.java +++ b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Address.java @@ -1,14 +1,34 @@ package contracts.utils; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + public class Address { - String country; - String city; - String street; - int number; + @JsonProperty("country") + private String country; + + @JsonProperty("city") + private String city; + + @JsonProperty("street") + private String street; + + @JsonProperty("number") + private int number; + + public Address() {} + + public Address(String country, String city, String street, int number) { + this.country = country; + this.city = city; + this.street = street; + this.number = number; + } public String getCountry() { - return this.country; + return country; } public void setCountry(String country) { @@ -16,7 +36,7 @@ public void setCountry(String country) { } public String getCity() { - return this.city; + return city; } public void setCity(String city) { @@ -24,7 +44,7 @@ public void setCity(String city) { } public String getStreet() { - return this.street; + return street; } public void setStreet(String street) { @@ -32,11 +52,36 @@ public void setStreet(String street) { } public int getNumber() { - return this.number; + return number; } public void setNumber(int number) { this.number = number; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Address address = (Address) o; + return number == address.number && + Objects.equals(country, address.country) && + Objects.equals(city, address.city) && + Objects.equals(street, address.street); + } + + @Override + public int hashCode() { + return Objects.hash(country, city, street, number); + } + + @Override + public String toString() { + return "Address{" + + "country='" + country + '\'' + + ", city='" + city + '\'' + + ", street='" + street + '\'' + + ", number=" + number + + '}'; + } } diff --git a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Contract.java b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Contract.java index 067842f..14069c3 100644 --- a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Contract.java +++ b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Contract.java @@ -1,25 +1,43 @@ package contracts.utils; import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; public class Contract { - Address address; + @JsonProperty("address") + private Address address; + + @JsonProperty("property_id") @JsonAlias("property_id") - String propertyId; + private String propertyId; + + @JsonProperty("contract_id") @JsonAlias("contract_id") - String contractId; + private String contractId; + + @JsonProperty("seller_name") @JsonAlias("seller_name") - String sellerName; + private String sellerName; + + @JsonProperty("contract_status") @JsonAlias("contract_status") - ContractStatusEnum contractStatus; + private ContractStatusEnum contractStatus; + + @JsonProperty("contract_created") @JsonAlias("contract_created") - Long contractCreated; + private Long contractCreated; + + @JsonProperty("contract_last_modified_on") @JsonAlias("contract_last_modified_on") - Long contractLastModifiedOn; + private Long contractLastModifiedOn; + + public Contract() {} public Address getAddress() { - return this.address; + return address; } public void setAddress(Address address) { @@ -27,7 +45,7 @@ public void setAddress(Address address) { } public String getPropertyId() { - return this.propertyId; + return propertyId; } public void setPropertyId(String propertyId) { @@ -35,7 +53,7 @@ public void setPropertyId(String propertyId) { } public String getContractId() { - return this.contractId; + return contractId; } public void setContractId(String contractId) { @@ -43,7 +61,7 @@ public void setContractId(String contractId) { } public String getSellerName() { - return this.sellerName; + return sellerName; } public void setSellerName(String sellerName) { @@ -51,7 +69,7 @@ public void setSellerName(String sellerName) { } public ContractStatusEnum getContractStatus() { - return this.contractStatus; + return contractStatus; } public void setContractStatus(ContractStatusEnum contractStatus) { @@ -59,7 +77,7 @@ public void setContractStatus(ContractStatusEnum contractStatus) { } public Long getContractCreated() { - return this.contractCreated; + return contractCreated; } public void setContractCreated(Long contractCreated) { @@ -67,11 +85,34 @@ public void setContractCreated(Long contractCreated) { } public Long getContractLastModifiedOn() { - return this.contractLastModifiedOn; + return contractLastModifiedOn; } public void setContractLastModifiedOn(Long contractLastModifiedOn) { this.contractLastModifiedOn = contractLastModifiedOn; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Contract contract = (Contract) o; + return Objects.equals(propertyId, contract.propertyId) && + Objects.equals(contractId, contract.contractId); + } + + @Override + public int hashCode() { + return Objects.hash(propertyId, contractId); + } + + @Override + public String toString() { + return "Contract{" + + "propertyId='" + propertyId + '\'' + + ", contractId='" + contractId + '\'' + + ", sellerName='" + sellerName + '\'' + + ", contractStatus=" + contractStatus + + '}'; + } } diff --git a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ContractStatusChangedEvent.java b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ContractStatusChangedEvent.java index 362d0bd..0d0995a 100644 --- a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ContractStatusChangedEvent.java +++ b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ContractStatusChangedEvent.java @@ -2,15 +2,31 @@ import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Objects; + public class ContractStatusChangedEvent { + @JsonProperty("contract_last_modified_on") - Long contractLastModifiedOn; + private Long contractLastModifiedOn; + @JsonProperty("contract_id") - String contractId; + private String contractId; + @JsonProperty("property_id") - String propertyId; + private String propertyId; + @JsonProperty("contract_status") - ContractStatusEnum contractStatus; + private ContractStatusEnum contractStatus; + + public ContractStatusChangedEvent() {} + + public ContractStatusChangedEvent(String contractId, String propertyId, + ContractStatusEnum contractStatus, Long contractLastModifiedOn) { + this.contractId = contractId; + this.propertyId = propertyId; + this.contractStatus = contractStatus; + this.contractLastModifiedOn = contractLastModifiedOn; + } public Long getContractLastModifiedOn() { return contractLastModifiedOn; @@ -44,4 +60,27 @@ public void setContractStatus(ContractStatusEnum contractStatus) { this.contractStatus = contractStatus; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ContractStatusChangedEvent that = (ContractStatusChangedEvent) o; + return Objects.equals(contractId, that.contractId) && + Objects.equals(propertyId, that.propertyId); + } + + @Override + public int hashCode() { + return Objects.hash(contractId, propertyId); + } + + @Override + public String toString() { + return "ContractStatusChangedEvent{" + + "contractId='" + contractId + '\'' + + ", propertyId='" + propertyId + '\'' + + ", contractStatus=" + contractStatus + + ", contractLastModifiedOn=" + contractLastModifiedOn + + '}'; + } } diff --git a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ResponseParser.java b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ResponseParser.java index 107963b..a17201a 100644 --- a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ResponseParser.java +++ b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ResponseParser.java @@ -1,29 +1,63 @@ package contracts.utils; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import java.util.Map; +import java.util.Optional; public class ResponseParser { - Contract parseResponse(Map queryResponse) - throws JsonMappingException, JsonProcessingException { - Contract response = new Contract(); - ObjectMapper objectMapper = new ObjectMapper(); - Address address = objectMapper.readValue(queryResponse.get("address").s(), Address.class); - response.setAddress(address); - response.setContractCreated( - Long.valueOf(queryResponse.get("contract_created").s())); - response.setContractId(queryResponse.get("contract_id").s()); - response.setContractLastModifiedOn( - Long.valueOf(queryResponse.get("contract_last_modified_on").s())); - response.setContractStatus(ContractStatusEnum.valueOf(queryResponse.get("contract_status").s())); - response.setPropertyId(queryResponse.get("property_id").s()); - response.setSellerName(queryResponse.get("seller_name").s()); - return response; + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + public Contract parseResponse(Map queryResponse) throws JsonProcessingException { + if (queryResponse == null || queryResponse.isEmpty()) { + throw new IllegalArgumentException("Query response cannot be null or empty"); } + + Contract contract = new Contract(); + + // Parse address + Optional.ofNullable(queryResponse.get("address")) + .map(AttributeValue::s) + .ifPresent(addressJson -> { + try { + Address address = OBJECT_MAPPER.readValue(addressJson, Address.class); + contract.setAddress(address); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to parse address", e); + } + }); + + // Parse other fields + Optional.ofNullable(queryResponse.get("contract_created")) + .map(AttributeValue::s) + .map(Long::valueOf) + .ifPresent(contract::setContractCreated); + + Optional.ofNullable(queryResponse.get("contract_id")) + .map(AttributeValue::s) + .ifPresent(contract::setContractId); + + Optional.ofNullable(queryResponse.get("contract_last_modified_on")) + .map(AttributeValue::s) + .map(Long::valueOf) + .ifPresent(contract::setContractLastModifiedOn); + + Optional.ofNullable(queryResponse.get("contract_status")) + .map(AttributeValue::s) + .map(ContractStatusEnum::valueOf) + .ifPresent(contract::setContractStatus); + + Optional.ofNullable(queryResponse.get("property_id")) + .map(AttributeValue::s) + .ifPresent(contract::setPropertyId); + + Optional.ofNullable(queryResponse.get("seller_name")) + .map(AttributeValue::s) + .ifPresent(contract::setSellerName); + + return contract; + } } diff --git a/unicorn_contracts/ContractsService/src/test/java/contracts/CreateContractTests.java b/unicorn_contracts/ContractsService/src/test/java/contracts/CreateContractTests.java index b3b10af..aec7b53 100644 --- a/unicorn_contracts/ContractsService/src/test/java/contracts/CreateContractTests.java +++ b/unicorn_contracts/ContractsService/src/test/java/contracts/CreateContractTests.java @@ -2,39 +2,108 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.events.SQSEvent; -import com.amazonaws.services.lambda.runtime.tests.annotations.Event; +import com.amazonaws.services.lambda.runtime.events.SQSEvent.MessageAttribute; +import com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; import org.junit.Before; -import org.junit.jupiter.params.ParameterizedTest; +import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.PutItemResponse; -import static org.mockito.Mockito.mock; +import java.util.Collections; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.class) public class CreateContractTests { - Context context; + @Mock + private Context context; + + @Mock + private DynamoDbClient dynamoDbClient; + + private ContractEventHandler handler; + + @Before + public void setUp() { + handler = new ContractEventHandler(dynamoDbClient); + } + + @Test + public void shouldProcessValidCreateEvent() { + // Given + SQSEvent event = createTestEvent("POST", + "{ \"address\": { \"country\": \"USA\", \"city\": \"Anytown\", \"street\": \"Main Street\", \"number\": 123 }, \"seller_name\": \"John Smith\", \"property_id\": \"usa/anytown/main-street/123\"}"); + + when(dynamoDbClient.putItem(any(PutItemRequest.class))) + .thenReturn(PutItemResponse.builder().build()); - ContractEventHandler handler; + // When + handler.handleRequest(event, context); - DynamoDbClient client; + // Then + verify(dynamoDbClient, times(1)).putItem(any(PutItemRequest.class)); + } - @Before - public void setUp() { + @Test + public void shouldHandleNullEvent() { + // When + handler.handleRequest(null, context); + + // Then + verifyNoInteractions(dynamoDbClient); + } - client = mock(DynamoDbClient.class); - context = mock(Context.class); + @Test + public void shouldHandleEmptyEvent() { + // Given + SQSEvent emptyEvent = new SQSEvent(); + + // When + handler.handleRequest(emptyEvent, context); + + // Then + verifyNoInteractions(dynamoDbClient); + } - } + @Test(expected = RuntimeException.class) + public void shouldThrowExceptionForMissingHttpMethod() { + // Given + SQSEvent event = createTestEventWithoutHttpMethod( + "{ \"address\": { \"country\": \"USA\", \"city\": \"Anytown\", \"street\": \"Main Street\", \"number\": 123 }, \"seller_name\": \"John Smith\", \"property_id\": \"usa/anytown/main-street/123\"}"); + + // When + handler.handleRequest(event, context); + } - @ParameterizedTest - @Event(value = "src/test/events/create_valid_event.json", type = SQSEvent.class) - public void validEvent(SQSEvent event) { - DynamoDbClient client = mock(DynamoDbClient.class); - handler = new ContractEventHandler(); - handler.setDynamodbClient(client); - handler.handleRequest(event, context); - } + private SQSEvent createTestEvent(String httpMethod, String body) { + SQSEvent event = new SQSEvent(); + SQSMessage message = new SQSMessage(); + message.setMessageId("test-message-id"); + message.setBody(body); + + MessageAttribute httpMethodAttr = new MessageAttribute(); + httpMethodAttr.setStringValue(httpMethod); + message.setMessageAttributes(Map.of("HttpMethod", httpMethodAttr)); + + event.setRecords(Collections.singletonList(message)); + return event; + } + private SQSEvent createTestEventWithoutHttpMethod(String body) { + SQSEvent event = new SQSEvent(); + SQSMessage message = new SQSMessage(); + message.setMessageId("test-message-id"); + message.setBody(body); + message.setMessageAttributes(Collections.emptyMap()); + + event.setRecords(Collections.singletonList(message)); + return event; + } } diff --git a/unicorn_web/Common/pom.xml b/unicorn_web/Common/pom.xml index 4178d15..2d86d38 100644 --- a/unicorn_web/Common/pom.xml +++ b/unicorn_web/Common/pom.xml @@ -22,13 +22,13 @@ software.amazon.awssdk dynamodb-enhanced - 2.27.21 + 2.32.29 compile com.fasterxml.jackson.core jackson-annotations - 2.15.3 + 2.18.4 compile diff --git a/unicorn_web/Common/src/main/java/dao/Property.java b/unicorn_web/Common/src/main/java/dao/Property.java index d4b53af..de0be86 100644 --- a/unicorn_web/Common/src/main/java/dao/Property.java +++ b/unicorn_web/Common/src/main/java/dao/Property.java @@ -1,6 +1,7 @@ package dao; import java.util.List; +import java.util.Objects; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -12,26 +13,29 @@ @DynamoDbBean public class Property { - String country; - String city; - String street; - String propertyNumber; - String description; - String contract; - Float listprice; - String currency; - List images; - String status; + private String country; + private String city; + private String street; + private String propertyNumber; + private String description; + private String contract; + private Float listprice; + private String currency; + private List images; + private String status; @JsonIgnore - String pk; + private String pk; @JsonIgnore - String sk; - String id; + private String sk; + private String id; @DynamoDbPartitionKey @DynamoDbAttribute("PK") public String getPk() { - return ("PROPERTY#" + getCountry() + "#" + getCity()).replace(' ', '-').toLowerCase(); + if (country == null || city == null) { + return pk; // Return stored value if components are null + } + return ("PROPERTY#" + country + "#" + city).replace(' ', '-').toLowerCase(); } public void setPk(String pk) { @@ -41,7 +45,10 @@ public void setPk(String pk) { @DynamoDbSortKey @DynamoDbAttribute("SK") public String getSk() { - return (getStreet() + "#" + getPropertyNumber()).replace(' ', '-').toLowerCase(); + if (street == null || propertyNumber == null) { + return sk; // Return stored value if components are null + } + return (street + "#" + propertyNumber).replace(' ', '-').toLowerCase(); } public void setSk(String sk) { @@ -51,7 +58,15 @@ public void setSk(String sk) { @JsonIgnore @software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnore public String getId() { - return (getPk() + '/' + getSk()).replace('#', '/'); + if (id != null) { + return id; + } + String partitionKey = getPk(); + String sortKey = getSk(); + if (partitionKey != null && sortKey != null) { + return (partitionKey + '/' + sortKey).replace('#', '/'); + } + return null; } public void setId(String id) { @@ -140,11 +155,28 @@ public void setStatus(String status) { } @Override - public String toString() { - return "Property [city=" + city + ", contract=" + contract + ", country=" + country + ", currency=" + currency - + ", description=" + description + ", id=" + getId() + ", images=" + images + ", listprice=" + listprice - + ", pk=" + getPk() + ", propertyNumber=" + propertyNumber + ", sk=" + getSk() + ", status=" + status - + ", street=" + street + "]"; + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Property property = (Property) o; + return Objects.equals(getId(), property.getId()); + } + + @Override + public int hashCode() { + return Objects.hash(getId()); } + @Override + public String toString() { + return "Property{" + + "country='" + country + '\'' + + ", city='" + city + '\'' + + ", street='" + street + '\'' + + ", propertyNumber='" + propertyNumber + '\'' + + ", status='" + status + '\'' + + ", listprice=" + listprice + + ", currency='" + currency + '\'' + + '}'; + } } diff --git a/unicorn_web/PublicationManagerService/pom.xml b/unicorn_web/PublicationManagerService/pom.xml index 9ec828c..ace1280 100644 --- a/unicorn_web/PublicationManagerService/pom.xml +++ b/unicorn_web/PublicationManagerService/pom.xml @@ -9,15 +9,14 @@ 17 17 - 2.27.21 - 1.18.0 - 3.13.0 - 5.13.0 + 2.32.29 + 1.20.2 + 3.16.1 + 5.18.0 4.13.2 - 1.1.1 - 1.2.3 - 3.13.0 - 2.27.21 + 1.1.2 + 1.3.0 + 2.32.29 @@ -118,36 +117,36 @@ com.amazonaws aws-lambda-java-core - 1.2.2 + ${aws-lambda-java-core.version} com.fasterxml.jackson.core jackson-databind - 2.15.2 + 2.18.4 com.fasterxml.jackson.core jackson-core - 2.15.2 + 2.18.4 com.fasterxml.jackson.core jackson-annotations - 2.15.2 + 2.18.4 org.apache.logging.log4j log4j-api - 2.20.0 + 2.25.1 org.apache.logging.log4j log4j-core - 2.20.0 + 2.25.1 common @@ -164,7 +163,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.2 + 3.5.3 handler @@ -174,7 +173,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.4 + 3.6.0 @@ -187,12 +186,10 @@ - dev.aspectj + org.codehaus.mojo aspectj-maven-plugin - 1.13.1 + 1.15.0 - 17 - 17 17 @@ -216,11 +213,18 @@ + + + org.aspectj + aspectjtools + 1.9.22.1 + + org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.14.0 17 17 diff --git a/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/PublicationApprovedEventHandler.java b/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/PublicationApprovedEventHandler.java index ef882de..1faef8b 100644 --- a/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/PublicationApprovedEventHandler.java +++ b/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/PublicationApprovedEventHandler.java @@ -5,6 +5,7 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; +import java.util.Map; import com.amazonaws.services.lambda.runtime.Context; import com.fasterxml.jackson.databind.ObjectMapper; @@ -27,74 +28,100 @@ import schema.unicorn_approvals.publicationevaluationcompleted.PublicationEvaluationCompleted; /** - * Function checks for the existence of a contract status entry for a specified - * search. - * If an entry exists, pause the workflow, and update the record with task - * token. + * Processes publication evaluation completed events and updates property status. */ public class PublicationApprovedEventHandler { - Logger logger = LogManager.getLogger(); + private static final Logger logger = LogManager.getLogger(PublicationApprovedEventHandler.class); + + private final String tableName = System.getenv("DYNAMODB_TABLE"); + private final DynamoDbAsyncTable propertyTable; + private final ObjectMapper objectMapper = new ObjectMapper(); - final String TABLE_NAME = System.getenv("DYNAMODB_TABLE"); + public PublicationApprovedEventHandler() { DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder() - .httpClientBuilder(NettyNioAsyncHttpClient.builder()) - .build(); + .httpClientBuilder(NettyNioAsyncHttpClient.builder()) + .build(); DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder() - .dynamoDbClient(dynamodbClient) - .build(); - - DynamoDbAsyncTable propertyTable = enhancedClient.table(TABLE_NAME, - TableSchema.fromBean(Property.class)); - - @Tracing - @Metrics(captureColdStart = true) - @Logging(logEvent = true) - public void handleRequest(InputStream inputStream, OutputStream outputStream, - Context context) throws IOException { - - AWSEvent event = Marshaller.unmarshalEvent(inputStream, - PublicationEvaluationCompleted.class); - - String propertyId = event.getDetail().getPropertyId(); - String evaluationResult = event.getDetail().getEvaluationResult(); - - publicationApproved(evaluationResult, propertyId); - - ObjectMapper objectMapper = new ObjectMapper(); - OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); - writer.write(objectMapper.writeValueAsString("'result': 'Successfully updated search status'")); - writer.close(); - + .dynamoDbClient(dynamodbClient) + .build(); + + this.propertyTable = enhancedClient.table(tableName, TableSchema.fromBean(Property.class)); + } + + @Tracing + @Metrics(captureColdStart = true) + @Logging(logEvent = true) + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { + try { + AWSEvent event = Marshaller.unmarshalEvent(inputStream, + PublicationEvaluationCompleted.class); + + if (event.getDetail() == null) { + throw new IllegalArgumentException("Event detail is null"); + } + + String propertyId = event.getDetail().getPropertyId(); + String evaluationResult = event.getDetail().getEvaluationResult(); + + if (propertyId == null || propertyId.trim().isEmpty()) { + throw new IllegalArgumentException("Property ID is null or empty"); + } + + if (evaluationResult == null || evaluationResult.trim().isEmpty()) { + throw new IllegalArgumentException("Evaluation result is null or empty"); + } + + updatePropertyStatus(evaluationResult, propertyId); + + String response = objectMapper.writeValueAsString( + Map.of("result", "Successfully updated property status")); + + try (OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) { + writer.write(response); + } + + } catch (Exception e) { + logger.error("Error processing publication evaluation event", e); + String errorResponse = objectMapper.writeValueAsString( + Map.of("error", "Failed to process event: " + e.getMessage())); + + try (OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) { + writer.write(errorResponse); + } + throw new RuntimeException("Event processing failed", e); } - - @Tracing - private void publicationApproved(String evaluationResult, String propertyId) { - - String[] splitString = propertyId.split("/"); - String country = splitString[0]; - String city = splitString[1]; - String street = splitString[2]; - String number = splitString[3]; - String strPartionKey = ("search#" + country + "#" + city).replace(' ', '-').toLowerCase(); - String strSortKey = (street + "#" + number).replace(' ', '-').toLowerCase(); - - Key key = Key.builder().partitionValue(strPartionKey).sortValue(strSortKey).build(); - Property existingProperty = propertyTable.getItem(key).join(); - - if (existingProperty == null) { - logger.error("Property not found for ID: {}", propertyId); - throw new RuntimeException("Property not found with ID: " + propertyId); - } - - // Always set the search number explicitly to ensure it's correct - existingProperty.setPropertyNumber(number); - existingProperty.setStatus(evaluationResult); - - logger.info("Updating search with status: {} and propertyNumber: {}", - evaluationResult, existingProperty.getPropertyNumber()); - propertyTable.putItem(existingProperty).join(); + } + + @Tracing + private void updatePropertyStatus(String evaluationResult, String propertyId) { + try { + String[] parts = propertyId.split("/"); + if (parts.length != 4) { + throw new IllegalArgumentException("Invalid property ID format: " + propertyId); + } + + String partitionKey = ("search#" + parts[0] + "#" + parts[1]).replace(' ', '-').toLowerCase(); + String sortKey = (parts[2] + "#" + parts[3]).replace(' ', '-').toLowerCase(); + + Key key = Key.builder().partitionValue(partitionKey).sortValue(sortKey).build(); + Property existingProperty = propertyTable.getItem(key).join(); + + if (existingProperty == null) { + logger.error("Property not found for ID: {}", propertyId); + throw new RuntimeException("Property not found with ID: " + propertyId); + } + + existingProperty.setPropertyNumber(parts[3]); + existingProperty.setStatus(evaluationResult); + + logger.info("Updating property {} with status: {}", propertyId, evaluationResult); + propertyTable.putItem(existingProperty).join(); + + } catch (Exception e) { + logger.error("Failed to update property status for ID: {}", propertyId, e); + throw new RuntimeException("Property update failed", e); } - + } } diff --git a/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/RequestApprovalFunction.java b/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/RequestApprovalFunction.java index ed954fe..47e944f 100644 --- a/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/RequestApprovalFunction.java +++ b/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/RequestApprovalFunction.java @@ -9,7 +9,6 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import java.util.regex.Matcher; import java.util.regex.Pattern; import com.amazonaws.services.lambda.runtime.Context; @@ -43,152 +42,168 @@ import software.amazon.lambda.powertools.tracing.Tracing; /** - * Validates the integrity of the search content + * Validates property requests and sends approval events. */ public class RequestApprovalFunction { - Logger logger = LogManager.getLogger(); - Set noActionSet = new HashSet(Arrays.asList("APPROVED")); - String SERVICE = "Unicorn.Web"; - String EXPRESSION = "[a-z-]+\\/[a-z-]+\\/[a-z][a-z0-9-]*\\/[0-9-]+"; - String TARGET_STATE = "PENDING"; - Pattern pattern = Pattern.compile(EXPRESSION); - - String TABLE_NAME = System.getenv("DYNAMODB_TABLE"); - String EVENT_BUS = System.getenv("EVENT_BUS"); - - DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder() - .httpClientBuilder(NettyNioAsyncHttpClient.builder()) - .build(); - - DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder() - .dynamoDbClient(dynamodbClient) - .build(); - - DynamoDbAsyncTable propertyTable = enhancedClient.table(TABLE_NAME, - TableSchema.fromBean(Property.class)); - - EventBridgeAsyncClient eventBridgeClient = EventBridgeAsyncClient.builder() - .httpClientBuilder(NettyNioAsyncHttpClient.builder()) - .build(); - - ObjectMapper objectMapper = new ObjectMapper(); + private static final Logger logger = LogManager.getLogger(RequestApprovalFunction.class); + private static final Set NO_ACTION_STATUSES = new HashSet<>(Arrays.asList("APPROVED")); + private static final String PROPERTY_ID_PATTERN = "[a-z-]+\\/[a-z-]+\\/[a-z][a-z0-9-]*\\/[0-9-]+"; + private static final String CONTENT_TYPE = "application/json"; + + private final Pattern propertyIdPattern = Pattern.compile(PROPERTY_ID_PATTERN); + private final String tableName = System.getenv("DYNAMODB_TABLE"); + private final String eventBus = System.getenv("EVENT_BUS"); + + private final DynamoDbAsyncTable propertyTable; + private final EventBridgeAsyncClient eventBridgeClient; + private final ObjectMapper objectMapper = new ObjectMapper(); + + public RequestApprovalFunction() { + DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder() + .httpClientBuilder(NettyNioAsyncHttpClient.builder()) + .build(); + + DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(dynamodbClient) + .build(); + + this.propertyTable = enhancedClient.table(tableName, TableSchema.fromBean(Property.class)); + this.eventBridgeClient = EventBridgeAsyncClient.builder() + .httpClientBuilder(NettyNioAsyncHttpClient.builder()) + .build(); + } @Tracing @Metrics(captureColdStart = true) @Logging(logEvent = true, correlationIdPath = CorrelationIdPathConstants.API_GATEWAY_REST) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, - final Context context) throws JsonProcessingException { - { - - Map headers = new HashMap<>(); - headers.put("Content-Type", "application/json"); - headers.put("X-Custom-Header", "application/json"); - APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() - .withHeaders(headers); + final Context context) { + try { + if (input.getBody() == null || input.getBody().trim().isEmpty()) { + return createErrorResponse(400, "Request body is required"); + } JsonNode rootNode = objectMapper.readTree(input.getBody()); - String propertyId = rootNode.get("property_id").asText(); - Matcher matcher = pattern.matcher(propertyId); - boolean valid = matcher.matches(); - if (!valid) { - return response - .withBody("Input invalid; must conform to regular expression: " + EXPRESSION) - .withStatusCode(500); + JsonNode propertyIdNode = rootNode.get("property_id"); + + if (propertyIdNode == null) { + return createErrorResponse(400, "property_id field is required"); } - String[] splitString = propertyId.split("/"); - String country = splitString[0]; - String city = splitString[1]; - String street = splitString[2]; - String number = splitString[3]; - String strPartionKey = ("search#" + country + "#" + city).replace(' ', '-').toLowerCase(); - String strSortKey = (street + "#" + number).replace(' ', '-').toLowerCase(); - try { - List properties = queryTable(strPartionKey, strSortKey); - if (properties.size() <= 0) { - return response - .withBody("No search found in database with the requested search id") - .withStatusCode(500); - } - Property property = properties.get(0); - if (noActionSet.contains(property.getStatus())) { - return response - .withStatusCode(200) - .withBody("'result': 'Property is already " + property.getStatus() + "; no action taken'"); - } - sendEvent(property); - - } catch (Exception e) { - return response - .withBody("Error in searching") - .withStatusCode(500); + + String propertyId = propertyIdNode.asText(); + if (!propertyIdPattern.matcher(propertyId).matches()) { + return createErrorResponse(400, "Invalid property_id format. Must match: " + PROPERTY_ID_PATTERN); } - return response - .withStatusCode(200) - .withBody("'result': 'Approval Requested'"); + PropertyComponents components = parsePropertyId(propertyId); + List properties = queryTable(components.partitionKey, components.sortKey); + + if (properties.isEmpty()) { + return createErrorResponse(404, "Property not found"); + } + + Property property = properties.get(0); + if (NO_ACTION_STATUSES.contains(property.getStatus())) { + return createSuccessResponse("Property is already " + property.getStatus() + "; no action taken"); + } + + sendEvent(property); + return createSuccessResponse("Approval requested successfully"); + + } catch (JsonProcessingException e) { + logger.error("Invalid JSON in request body", e); + return createErrorResponse(400, "Invalid JSON format"); + } catch (Exception e) { + logger.error("Error processing approval request", e); + return createErrorResponse(500, "Internal server error"); } + } + private PropertyComponents parsePropertyId(String propertyId) { + String[] parts = propertyId.split("/"); + String partitionKey = ("search#" + parts[0] + "#" + parts[1]).replace(' ', '-').toLowerCase(); + String sortKey = (parts[2] + "#" + parts[3]).replace(' ', '-').toLowerCase(); + return new PropertyComponents(partitionKey, sortKey); } - public List queryTable(String partitionkey, String sortKey) throws Exception { + private APIGatewayProxyResponseEvent createSuccessResponse(String message) { + String body = String.format("{\"result\":\"%s\"}", message); + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withHeaders(Map.of("Content-Type", CONTENT_TYPE)) + .withBody(body); + } - try { - if (partitionkey == null || sortKey == null) { - throw new Exception("Invalid Input"); - } - List result = new ArrayList(); - SdkPublisher properties = null; + private APIGatewayProxyResponseEvent createErrorResponse(int statusCode, String message) { + String body = String.format("{\"error\":\"%s\"}", message); + return new APIGatewayProxyResponseEvent() + .withStatusCode(statusCode) + .withHeaders(Map.of("Content-Type", CONTENT_TYPE)) + .withBody(body); + } - Key key = Key.builder().partitionValue(partitionkey).sortValue(sortKey).build(); + private static class PropertyComponents { + final String partitionKey; + final String sortKey; - QueryConditional queryConditional = QueryConditional.sortBeginsWith(key); - QueryEnhancedRequest request = QueryEnhancedRequest.builder().queryConditional(queryConditional) - .build(); - properties = propertyTable.query(request).items(); + PropertyComponents(String partitionKey, String sortKey) { + this.partitionKey = partitionKey; + this.sortKey = sortKey; + } + } - CompletableFuture future = properties.subscribe(res -> { - // Add response to the list - result.add(res); - }); - future.get(); + private List queryTable(String partitionKey, String sortKey) throws Exception { + if (partitionKey == null || sortKey == null) { + throw new IllegalArgumentException("Partition key and sort key cannot be null"); + } - return result; + List result = new ArrayList<>(); + Key key = Key.builder().partitionValue(partitionKey).sortValue(sortKey).build(); + QueryConditional queryConditional = QueryConditional.sortBeginsWith(key); + QueryEnhancedRequest request = QueryEnhancedRequest.builder() + .queryConditional(queryConditional) + .build(); + try { + SdkPublisher properties = propertyTable.query(request).items(); + CompletableFuture future = properties.subscribe(result::add); + future.get(); + return result; } catch (DynamoDbException | InterruptedException | ExecutionException e) { - throw new Exception(e.getMessage()); + logger.error("Error querying DynamoDB", e); + throw new Exception("Database query failed: " + e.getMessage()); } } @Tracing @Metrics - public String sendEvent(Property property) - throws JsonProcessingException { - + private void sendEvent(Property property) throws JsonProcessingException { RequestApproval event = new RequestApproval(); event.setPropertyId(property.getId()); + Address address = new Address(); address.setCity(property.getCity()); address.setCountry(property.getCountry()); address.setNumber(property.getPropertyNumber()); event.setAddress(address); - String event_string = objectMapper.writeValueAsString(event); - - List requestEntries = new ArrayList(); + String eventString = objectMapper.writeValueAsString(event); - requestEntries.add(PutEventsRequestEntry.builder() - .eventBusName(EVENT_BUS) + PutEventsRequestEntry requestEntry = PutEventsRequestEntry.builder() + .eventBusName(eventBus) .source("Unicorn.Web") .resources(property.getId()) .detailType("PublicationApprovalRequested") - .detail(event_string).build()); + .detail(eventString) + .build(); - PutEventsRequest eventsRequest = PutEventsRequest.builder().entries(requestEntries).build(); + PutEventsRequest eventsRequest = PutEventsRequest.builder() + .entries(requestEntry) + .build(); eventBridgeClient.putEvents(eventsRequest).join(); - - return event_string; + logger.info("Event sent successfully for property: {}", property.getId()); } } @@ -212,11 +227,13 @@ public Address getAddress() { public void setAddress(Address address) { this.address = address; } - } class Address { String country; + String city; + String state; + String number; public String getCountry() { return country; @@ -249,9 +266,4 @@ public String getNumber() { public void setNumber(String number) { this.number = number; } - - String city; - String state; - String number; - } diff --git a/unicorn_web/PublicationManagerService/target/classes/publicationmanager/Address.class b/unicorn_web/PublicationManagerService/target/classes/publicationmanager/Address.class deleted file mode 100644 index a3589f5..0000000 Binary files a/unicorn_web/PublicationManagerService/target/classes/publicationmanager/Address.class and /dev/null differ diff --git a/unicorn_web/PublicationManagerService/target/classes/publicationmanager/PublicationApprovedEventHandler.class b/unicorn_web/PublicationManagerService/target/classes/publicationmanager/PublicationApprovedEventHandler.class deleted file mode 100644 index bab0b47..0000000 Binary files a/unicorn_web/PublicationManagerService/target/classes/publicationmanager/PublicationApprovedEventHandler.class and /dev/null differ diff --git a/unicorn_web/PublicationManagerService/target/classes/publicationmanager/RequestApproval.class b/unicorn_web/PublicationManagerService/target/classes/publicationmanager/RequestApproval.class deleted file mode 100644 index 5087c6d..0000000 Binary files a/unicorn_web/PublicationManagerService/target/classes/publicationmanager/RequestApproval.class and /dev/null differ diff --git a/unicorn_web/PublicationManagerService/target/classes/publicationmanager/RequestApprovalFunction.class b/unicorn_web/PublicationManagerService/target/classes/publicationmanager/RequestApprovalFunction.class deleted file mode 100644 index d8631ed..0000000 Binary files a/unicorn_web/PublicationManagerService/target/classes/publicationmanager/RequestApprovalFunction.class and /dev/null differ diff --git a/unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/AWSEvent.class b/unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/AWSEvent.class deleted file mode 100644 index 05fd90d..0000000 Binary files a/unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/AWSEvent.class and /dev/null differ diff --git a/unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/PublicationEvaluationCompleted.class b/unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/PublicationEvaluationCompleted.class deleted file mode 100644 index 546342c..0000000 Binary files a/unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/PublicationEvaluationCompleted.class and /dev/null differ diff --git a/unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/marshaller/Marshaller.class b/unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/marshaller/Marshaller.class deleted file mode 100644 index 94d71b9..0000000 Binary files a/unicorn_web/PublicationManagerService/target/classes/schema/unicorn_approvals/publicationevaluationcompleted/marshaller/Marshaller.class and /dev/null differ diff --git a/unicorn_web/SearchService/pom.xml b/unicorn_web/SearchService/pom.xml index 2e7b01e..b627290 100644 --- a/unicorn_web/SearchService/pom.xml +++ b/unicorn_web/SearchService/pom.xml @@ -9,15 +9,14 @@ 17 17 - 2.27.21 - 1.18.0 - 3.13.0 - 5.13.0 + 2.32.29 + 1.20.2 + 3.16.1 + 5.18.0 4.13.2 - 1.1.1 - 1.2.3 - 3.13.0 - 2.27.21 + 1.1.2 + 1.3.0 + 2.32.29 @@ -124,36 +123,36 @@ com.amazonaws aws-lambda-java-core - 1.2.2 + ${aws-lambda-java-core.version} com.fasterxml.jackson.core jackson-databind - 2.15.2 + 2.18.4 com.fasterxml.jackson.core jackson-core - 2.15.2 + 2.18.4 com.fasterxml.jackson.core jackson-annotations - 2.15.2 + 2.18.4 org.apache.logging.log4j log4j-api - 2.20.0 + 2.25.1 org.apache.logging.log4j log4j-core - 2.20.0 + 2.25.1 common @@ -170,7 +169,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.2 + 3.5.3 handler @@ -180,7 +179,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.4 + 3.6.0 @@ -193,12 +192,10 @@ - dev.aspectj + org.codehaus.mojo aspectj-maven-plugin - 1.13.1 + 1.15.0 - 17 - 17 17 @@ -222,11 +219,18 @@ + + + org.aspectj + aspectjtools + 1.9.22.1 + + org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.14.0 17 17 diff --git a/unicorn_web/SearchService/src/main/java/search/PropertySearchFunction.java b/unicorn_web/SearchService/src/main/java/search/PropertySearchFunction.java index 4191570..883f512 100644 --- a/unicorn_web/SearchService/src/main/java/search/PropertySearchFunction.java +++ b/unicorn_web/SearchService/src/main/java/search/PropertySearchFunction.java @@ -29,172 +29,132 @@ import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.DynamoDbException; -import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; import software.amazon.lambda.powertools.metrics.Metrics; -import software.amazon.lambda.powertools.metrics.MetricsUtils; import software.amazon.lambda.powertools.tracing.Tracing; import software.amazon.lambda.powertools.logging.CorrelationIdPathConstants; import software.amazon.lambda.powertools.logging.Logging; /** - * Handler for requests to Lambda function. + * Handler for property search requests. */ public class PropertySearchFunction implements RequestHandler { private static final Logger logger = LogManager.getLogger(PropertySearchFunction.class); + private static final String APPROVED_STATUS = "APPROVED"; + private static final String CONTENT_TYPE = "application/json"; - String TABLE_NAME = System.getenv("DYNAMODB_TABLE"); + private final String tableName = System.getenv("DYNAMODB_TABLE"); + private final DynamoDbAsyncTable propertyTable; + private final ObjectMapper objectMapper = new ObjectMapper(); - DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder() - .httpClientBuilder(NettyNioAsyncHttpClient.builder()) - .build(); + public PropertySearchFunction() { + DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder() + .httpClientBuilder(NettyNioAsyncHttpClient.builder()) + .build(); - DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder() - .dynamoDbClient(dynamodbClient) - .build(); + DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder() + .dynamoDbClient(dynamodbClient) + .build(); - DynamoDbAsyncTable propertyTable = enhancedClient.table(TABLE_NAME, - TableSchema.fromBean(Property.class)); - - final String SERVICE_NAME = System.getenv("POWERTOOLS_SERVICE_NAME"); - final String METRICS_NAMESPACE = System.getenv("POWERTOOLS_METRICS_NAMESPACE"); - final String EVENT_BUS = System.getenv("EVENT_BUS"); - - MetricsLogger metricsLogger = MetricsUtils.metricsLogger(); - ObjectMapper objectMapper = new ObjectMapper(); + this.propertyTable = enhancedClient.table(tableName, TableSchema.fromBean(Property.class)); + } @Tracing @Metrics(captureColdStart = true) @Logging(logEvent = true, correlationIdPath = CorrelationIdPathConstants.API_GATEWAY_REST) public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + try { + if (!"GET".equalsIgnoreCase(input.getHttpMethod())) { + return createErrorResponse(400, "Method not allowed"); + } + + Map pathParams = input.getPathParameters(); + if (pathParams == null || pathParams.get("country") == null || pathParams.get("city") == null) { + return createErrorResponse(400, "Missing required path parameters"); + } - Map headers = new HashMap<>(); - headers.put("Content-Type", "application/json"); - headers.put("X-Custom-Header", "application/json"); - APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() - .withHeaders(headers); - String method = input.getHttpMethod(); - if (!method.equalsIgnoreCase("get")) { - return response - .withStatusCode(400) - .withBody("{ \"message\": \"ErrorInRequest\", \"requestdetails\": \"Input Invalid\" }"); + String partitionKey = buildPartitionKey(pathParams.get("country"), pathParams.get("city")); + String sortKey = buildSortKey(input.getResource(), pathParams); + + List properties = queryTable(partitionKey, sortKey); + String responseBody = objectMapper.writeValueAsString(properties); + + return createSuccessResponse(responseBody); + + } catch (Exception e) { + logger.error("Error processing request", e); + return createErrorResponse(500, "Internal server error"); } - String requestPath = input.getResource(); - String responseString = null; - String strPartitionKey = ("search#" + input.getPathParameters().get("country") + "#" - + input.getPathParameters().get("city")).replace(' ', '-').toLowerCase(); + } - String strSortKey = null; - switch (requestPath) { + private String buildPartitionKey(String country, String city) { + return ("search#" + country + "#" + city).replace(' ', '-').toLowerCase(); + } + + private String buildSortKey(String resource, Map pathParams) { + switch (resource) { case "/search/{country}/{city}": - // code to call - logger.info("path is " + requestPath); - - try { - List result = queryTable(strPartitionKey, null); - responseString = objectMapper.writeValueAsString(result); - } catch (Exception e) { - return response - .withStatusCode(500) - .withBody( - "{ \"message\": \"ErrorInRequest\", \"requestdetails\": \"Cannot Process Request\" }"); - } - break; + return null; case "/search/{country}/{city}/{street}": - // code to call - logger.info("path is " + requestPath); - strSortKey = input.getPathParameters().get("street"); - strSortKey = strSortKey.replace(' ', '-').toLowerCase(); - - try { - List result = queryTable(strPartitionKey, strSortKey); - responseString = objectMapper.writeValueAsString(result); - } catch (Exception e) { - return response - .withStatusCode(500) - .withBody( - "{ \"message\": \"ErrorInRequest\", \"requestdetails\": \"Cannot Process Request\" }"); - } - break; + return pathParams.get("street").replace(' ', '-').toLowerCase(); case "/properties/{country}/{city}/{street}/{number}": - logger.info("path is " + requestPath); - strSortKey = input.getPathParameters().get("street") + "#" + input.getPathParameters().get("number"); - strSortKey = strSortKey.replace(' ', '-').toLowerCase(); - - try { - List result = queryTable(strPartitionKey, strSortKey); - responseString = objectMapper.writeValueAsString(result); - } catch (Exception e) { - return response - .withStatusCode(500) - .withBody( - "{ \"message\": \"ErrorInRequest\", \"requestdetails\": \"Cannot Process Request\" }"); - } - break; + return (pathParams.get("street") + "#" + pathParams.get("number")).replace(' ', '-').toLowerCase(); default: - return response - .withStatusCode(400) - .withBody("{ \"message\": \"ErrorInRequest\", \"requestdetails\": \"Input Invalid\" }"); - + throw new IllegalArgumentException("Unsupported resource path: " + resource); } + } - return response + private APIGatewayProxyResponseEvent createSuccessResponse(String body) { + return new APIGatewayProxyResponseEvent() .withStatusCode(200) - .withBody(responseString); + .withHeaders(Map.of("Content-Type", CONTENT_TYPE)) + .withBody(body); } - public List queryTable(String partitionkey, String sortKey) throws Exception { + private APIGatewayProxyResponseEvent createErrorResponse(int statusCode, String message) { + String errorBody = String.format("{\"error\":\"%s\"}", message); + return new APIGatewayProxyResponseEvent() + .withStatusCode(statusCode) + .withHeaders(Map.of("Content-Type", CONTENT_TYPE)) + .withBody(errorBody); + } - try { - if (partitionkey == null) { - throw new Exception("Invalid Input"); - } - List result = new ArrayList(); - SdkPublisher properties = null; - - AttributeValue attributeValue = AttributeValue.builder() - .s("APPROVED") - .build(); - Map expressionValues = new HashMap<>(); - expressionValues.put(":value", attributeValue); - - Map expressionNames = new HashMap<>(); - expressionNames.put("#property_status", "status"); - - Expression expression = Expression.builder() - .expressionNames(expressionNames) - .expression("#property_status = :value") - .expressionValues(expressionValues) - .build(); - - if (sortKey != null) { - Key key = Key.builder().partitionValue(partitionkey).sortValue(sortKey).build(); - - QueryConditional queryConditional = QueryConditional.sortBeginsWith(key); - QueryEnhancedRequest request = QueryEnhancedRequest.builder().queryConditional(queryConditional) - .filterExpression(expression).build(); - properties = propertyTable.query(request).items(); - - } else { - Key key = Key.builder().partitionValue(partitionkey).build(); - QueryConditional queryConditional = QueryConditional.keyEqualTo(key); - QueryEnhancedRequest request = QueryEnhancedRequest.builder().queryConditional(queryConditional) - .filterExpression(expression).build(); - properties = propertyTable.query(request).items(); - } + private List queryTable(String partitionKey, String sortKey) throws Exception { + if (partitionKey == null) { + throw new IllegalArgumentException("Partition key cannot be null"); + } - CompletableFuture future = properties.subscribe(res -> { - // Add response to the list - result.add(res); - }); - future.get(); + List result = new ArrayList<>(); + + Expression filterExpression = Expression.builder() + .expressionNames(Map.of("#property_status", "status")) + .expression("#property_status = :value") + .expressionValues(Map.of(":value", AttributeValue.builder().s(APPROVED_STATUS).build())) + .build(); + + QueryConditional queryConditional; + if (sortKey != null) { + Key key = Key.builder().partitionValue(partitionKey).sortValue(sortKey).build(); + queryConditional = QueryConditional.sortBeginsWith(key); + } else { + Key key = Key.builder().partitionValue(partitionKey).build(); + queryConditional = QueryConditional.keyEqualTo(key); + } - return result; + QueryEnhancedRequest request = QueryEnhancedRequest.builder() + .queryConditional(queryConditional) + .filterExpression(filterExpression) + .build(); + try { + SdkPublisher properties = propertyTable.query(request).items(); + CompletableFuture future = properties.subscribe(result::add); + future.get(); + return result; } catch (DynamoDbException | InterruptedException | ExecutionException e) { - throw new Exception(e.getMessage()); + logger.error("Error querying DynamoDB", e); + throw new Exception("Database query failed: " + e.getMessage()); } } - }