diff --git a/unicorn_approvals/ApprovalsService/pom.xml b/unicorn_approvals/ApprovalsService/pom.xml index 24f69f3..05d471b 100644 --- a/unicorn_approvals/ApprovalsService/pom.xml +++ b/unicorn_approvals/ApprovalsService/pom.xml @@ -136,8 +136,16 @@ handler + test-table + + + org.apache.maven.surefire + surefire-junit4 + 3.5.3 + + org.apache.maven.plugins diff --git a/unicorn_approvals/ApprovalsService/src/main/java/approvals/ContractStatusChangedHandlerFunction.java b/unicorn_approvals/ApprovalsService/src/main/java/approvals/ContractStatusChangedHandlerFunction.java index 0a82802..0d9f089 100644 --- a/unicorn_approvals/ApprovalsService/src/main/java/approvals/ContractStatusChangedHandlerFunction.java +++ b/unicorn_approvals/ApprovalsService/src/main/java/approvals/ContractStatusChangedHandlerFunction.java @@ -45,15 +45,24 @@ public class ContractStatusChangedHandlerFunction { @Metrics(captureColdStart = true) @Logging(logEvent = true) public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { + logger.info("TABLE_NAME environment variable: {}", TABLE_NAME); + Event event = Marshaller.unmarshal(inputStream, Event.class); ContractStatusChanged contractStatusChanged = event.getDetail(); + logger.info("Received event: {}", contractStatusChanged); - saveContractStatus( - contractStatusChanged.getPropertyId(), - contractStatusChanged.getContractStatus(), - contractStatusChanged.getContractId(), - contractStatusChanged.getContractLastModifiedOn() - ); + try { + saveContractStatus( + contractStatusChanged.getPropertyId(), + contractStatusChanged.getContractStatus(), + contractStatusChanged.getContractId(), + contractStatusChanged.getContractLastModifiedOn() + ); + logger.info("Successfully updated contract status for property: {}", contractStatusChanged.getPropertyId()); + } catch (Exception e) { + logger.error("Failed to update contract status for property: {}", contractStatusChanged.getPropertyId(), e); + throw e; + } try (OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) { writer.write(objectMapper.writeValueAsString(event.getDetail())); @@ -62,6 +71,12 @@ public void handleRequest(InputStream inputStream, OutputStream outputStream, Co @Tracing void saveContractStatus(String propertyId, String contractStatus, String contractId, Long contractLastModifiedOn) { + if (TABLE_NAME == null || TABLE_NAME.isEmpty()) { + throw new RuntimeException("CONTRACT_STATUS_TABLE environment variable is not set"); + } + + logger.info("Updating DynamoDB table: {} for property: {}", TABLE_NAME, propertyId); + Map key = Map.of("property_id", AttributeValue.fromS(propertyId)); Map expressionAttributeValues = Map.of( @@ -78,6 +93,7 @@ void saveContractStatus(String propertyId, String contractStatus, String contrac .build(); dynamodbClient.updateItem(updateItemRequest); + logger.info("DynamoDB update completed for property: {}", propertyId); } public void setDynamodbClient(DynamoDbClient dynamodbClient) { diff --git a/unicorn_web/Common/src/main/java/dao/Property.java b/unicorn_web/Common/src/main/java/dao/Property.java index de0be86..1cf4dbe 100644 --- a/unicorn_web/Common/src/main/java/dao/Property.java +++ b/unicorn_web/Common/src/main/java/dao/Property.java @@ -35,7 +35,7 @@ public String getPk() { if (country == null || city == null) { return pk; // Return stored value if components are null } - return ("PROPERTY#" + country + "#" + city).replace(' ', '-').toLowerCase(); + return ("PROPERTY#" + country.toLowerCase() + "#" + city.toLowerCase()).replace(' ', '-'); } public void setPk(String pk) { diff --git a/unicorn_web/Data/property_data.json b/unicorn_web/Data/property_data.json index b7b16d9..aa2a3ac 100644 --- a/unicorn_web/Data/property_data.json +++ b/unicorn_web/Data/property_data.json @@ -1,6 +1,6 @@ [ { - "PK": "property#usa#anytown", + "PK": "PROPERTY#usa#anytown", "SK": "main-street#111", "country": "USA", "city": "Anytown", @@ -19,7 +19,7 @@ "status": "PENDING" }, { - "PK": "property#usa#main-town", + "PK": "PROPERTY#usa#main-town", "SK": "my-street#222", "country": "USA", "city": "Main Town", @@ -38,7 +38,7 @@ "status": "PENDING" }, { - "PK": "property#usa#anytown", + "PK": "PROPERTY#usa#anytown", "SK": "main-street#333", "country": "USA", "city": "Anytown", diff --git a/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/PublicationApprovedEventHandler.java b/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/PublicationApprovedEventHandler.java index 1faef8b..29adc26 100644 --- a/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/PublicationApprovedEventHandler.java +++ b/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/PublicationApprovedEventHandler.java @@ -96,15 +96,20 @@ public void handleRequest(InputStream inputStream, OutputStream outputStream, Co @Tracing private void updatePropertyStatus(String evaluationResult, String propertyId) { + logger.info("Updating property status for property ID: {}", propertyId); + logger.info("Evaluation result: {}", evaluationResult); 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 partitionKey = ("PROPERTY#" + parts[0] + "#" + parts[1]).replace(' ', '-'); String sortKey = (parts[2] + "#" + parts[3]).replace(' ', '-').toLowerCase(); + logger.info("Paritition Key: {}", partitionKey); + logger.info("Sort Key: {}", sortKey); + Key key = Key.builder().partitionValue(partitionKey).sortValue(sortKey).build(); Property existingProperty = propertyTable.getItem(key).join(); @@ -112,7 +117,8 @@ private void updatePropertyStatus(String evaluationResult, String propertyId) { logger.error("Property not found for ID: {}", propertyId); throw new RuntimeException("Property not found with ID: " + propertyId); } - + logger.info("Existing property: {}", existingProperty); + existingProperty.setPropertyNumber(parts[3]); existingProperty.setStatus(evaluationResult); diff --git a/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/RequestApprovalFunction.java b/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/RequestApprovalFunction.java index 47e944f..76386fa 100644 --- a/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/RequestApprovalFunction.java +++ b/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/RequestApprovalFunction.java @@ -12,8 +12,7 @@ import java.util.regex.Pattern; import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -36,7 +35,6 @@ import software.amazon.awssdk.services.eventbridge.EventBridgeAsyncClient; import software.amazon.awssdk.services.eventbridge.model.PutEventsRequest; import software.amazon.awssdk.services.eventbridge.model.PutEventsRequestEntry; -import software.amazon.lambda.powertools.logging.CorrelationIdPathConstants; import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.metrics.Metrics; import software.amazon.lambda.powertools.tracing.Tracing; @@ -49,7 +47,6 @@ public class RequestApprovalFunction { 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"); @@ -60,6 +57,7 @@ public class RequestApprovalFunction { private final ObjectMapper objectMapper = new ObjectMapper(); public RequestApprovalFunction() { + DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder() .httpClientBuilder(NettyNioAsyncHttpClient.builder()) .build(); @@ -76,72 +74,74 @@ public RequestApprovalFunction() { @Tracing @Metrics(captureColdStart = true) - @Logging(logEvent = true, correlationIdPath = CorrelationIdPathConstants.API_GATEWAY_REST) - public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, - 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()); - JsonNode propertyIdNode = rootNode.get("property_id"); - - if (propertyIdNode == null) { - return createErrorResponse(400, "property_id field is required"); - } + @Logging(logEvent = true) + public void handleRequest(final SQSEvent input, final Context context) { + logger.info("Environment variables - DYNAMODB_TABLE: {}, EVENT_BUS: {}", tableName, eventBus); + logger.info("Starting approval request processing for {} messages", input.getRecords().size()); - String propertyId = propertyIdNode.asText(); - if (!propertyIdPattern.matcher(propertyId).matches()) { - return createErrorResponse(400, "Invalid property_id format. Must match: " + PROPERTY_ID_PATTERN); - } - - 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"); + + for (SQSEvent.SQSMessage message : input.getRecords()) { + try { + String body = message.getBody(); + logger.info("Processing SQS message: {}", body); + + if (body == null || body.trim().isEmpty()) { + logger.warn("Message body is null or empty"); + continue; + } + + JsonNode rootNode = objectMapper.readTree(body); + JsonNode propertyIdNode = rootNode.get("property_id"); + + if (propertyIdNode == null) { + logger.warn("property_id field missing from message"); + continue; + } + + String propertyId = propertyIdNode.asText(); + logger.info("Processing approval request for property: {}", propertyId); + + if (!propertyIdPattern.matcher(propertyId).matches()) { + logger.warn("Invalid property_id format: {}", propertyId); + continue; + } + + PropertyComponents components = parsePropertyId(propertyId); + logger.info("Parsed property ID components: {}", components); + List properties = queryTable(components.partitionKey, components.sortKey); + + if (properties.isEmpty()) { + logger.warn("Property not found in database: {}", propertyId); + continue; + } + + Property property = properties.get(0); + logger.info("Found property with status: {}", property.getStatus()); + + if (NO_ACTION_STATUSES.contains(property.getStatus())) { + logger.info("Property already approved, no action needed: {}", propertyId); + continue; + } + + sendEvent(property); + logger.info("Approval request completed successfully for property: {}", propertyId); + + } catch (JsonProcessingException e) { + logger.error("Invalid JSON in message body: {}", message.getBody(), e); + } catch (Exception e) { + logger.error("Error processing approval request for message: {}", message.getBody(), e); } - - 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 partitionKey = ("PROPERTY#" + parts[0] + "#" + parts[1]).replace(' ', '-'); String sortKey = (parts[2] + "#" + parts[3]).replace(' ', '-').toLowerCase(); return new PropertyComponents(partitionKey, sortKey); } - 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); - } - 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); - } private static class PropertyComponents { final String partitionKey; @@ -154,7 +154,10 @@ private static class PropertyComponents { } private List queryTable(String partitionKey, String sortKey) throws Exception { + logger.info("Starting DynamoDB query with partitionKey: {}, sortKey: {}", partitionKey, sortKey); + if (partitionKey == null || sortKey == null) { + logger.error("Null keys provided - partitionKey: {}, sortKey: {}", partitionKey, sortKey); throw new IllegalArgumentException("Partition key and sort key cannot be null"); } @@ -166,12 +169,15 @@ private List queryTable(String partitionKey, String sortKey) throws Ex .build(); try { + logger.debug("Executing DynamoDB query on table: {}", tableName); SdkPublisher properties = propertyTable.query(request).items(); CompletableFuture future = properties.subscribe(result::add); future.get(); + logger.info("DynamoDB query completed successfully, found {} properties", result.size()); return result; } catch (DynamoDbException | InterruptedException | ExecutionException e) { - logger.error("Error querying DynamoDB", e); + logger.error("Error querying DynamoDB with partitionKey: {}, sortKey: {}, table: {}", + partitionKey, sortKey, tableName, e); throw new Exception("Database query failed: " + e.getMessage()); } } @@ -179,6 +185,8 @@ private List queryTable(String partitionKey, String sortKey) throws Ex @Tracing @Metrics private void sendEvent(Property property) throws JsonProcessingException { + logger.info("Creating approval event for property: {}", property.getId()); + RequestApproval event = new RequestApproval(); event.setPropertyId(property.getId()); @@ -189,6 +197,7 @@ private void sendEvent(Property property) throws JsonProcessingException { event.setAddress(address); String eventString = objectMapper.writeValueAsString(event); + logger.info("Event payload created: {}", eventString); PutEventsRequestEntry requestEntry = PutEventsRequestEntry.builder() .eventBusName(eventBus) @@ -202,8 +211,15 @@ private void sendEvent(Property property) throws JsonProcessingException { .entries(requestEntry) .build(); - eventBridgeClient.putEvents(eventsRequest).join(); - logger.info("Event sent successfully for property: {}", property.getId()); + logger.debug("Sending event to EventBridge bus: {}", eventBus); + try { + eventBridgeClient.putEvents(eventsRequest).join(); + logger.info("Event sent successfully for property: {}", property.getId()); + } catch (Exception e) { + logger.error("Failed to send event to EventBridge for property: {}, bus: {}", + property.getId(), eventBus, e); + throw e; + } } } diff --git a/unicorn_web/SearchService/src/main/java/search/PropertySearchFunction.java b/unicorn_web/SearchService/src/main/java/search/PropertySearchFunction.java index 883f512..ba1da35 100644 --- a/unicorn_web/SearchService/src/main/java/search/PropertySearchFunction.java +++ b/unicorn_web/SearchService/src/main/java/search/PropertySearchFunction.java @@ -76,6 +76,7 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv String partitionKey = buildPartitionKey(pathParams.get("country"), pathParams.get("city")); String sortKey = buildSortKey(input.getResource(), pathParams); + logger.info("Partition Key: {}, Sort Key: {}", partitionKey, sortKey); List properties = queryTable(partitionKey, sortKey); String responseBody = objectMapper.writeValueAsString(properties); @@ -89,7 +90,7 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv } private String buildPartitionKey(String country, String city) { - return ("search#" + country + "#" + city).replace(' ', '-').toLowerCase(); + return ("PROPERTY#" + country + "#" + city).replace(' ', '-'); } private String buildSortKey(String resource, Map pathParams) {