From 2388b8cf684a0bfa3a2afb522019bc96243c00e3 Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Mon, 16 Mar 2026 09:50:24 +0800 Subject: [PATCH 1/4] refactor(contracts): Change timestamp fields from Long to String - Update Contract model to use String type for contractCreated and contractLastModifiedOn fields - Remove Long::valueOf conversions in ResponseParser when parsing timestamp values from DynamoDB - Align timestamp handling with string-based storage in DynamoDB AttributeValue - Simplifies type conversion and maintains consistency with data source format --- .../src/main/java/contracts/utils/Contract.java | 14 +++++++------- .../main/java/contracts/utils/ResponseParser.java | 2 -- 2 files changed, 7 insertions(+), 9 deletions(-) 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 14069c3..379af4d 100644 --- a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Contract.java +++ b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Contract.java @@ -28,11 +28,11 @@ public class Contract { @JsonProperty("contract_created") @JsonAlias("contract_created") - private Long contractCreated; - + private String contractCreated; + @JsonProperty("contract_last_modified_on") @JsonAlias("contract_last_modified_on") - private Long contractLastModifiedOn; + private String contractLastModifiedOn; public Contract() {} @@ -76,19 +76,19 @@ public void setContractStatus(ContractStatusEnum contractStatus) { this.contractStatus = contractStatus; } - public Long getContractCreated() { + public String getContractCreated() { return contractCreated; } - public void setContractCreated(Long contractCreated) { + public void setContractCreated(String contractCreated) { this.contractCreated = contractCreated; } - public Long getContractLastModifiedOn() { + public String getContractLastModifiedOn() { return contractLastModifiedOn; } - public void setContractLastModifiedOn(Long contractLastModifiedOn) { + public void setContractLastModifiedOn(String contractLastModifiedOn) { this.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 a17201a..d40abe8 100644 --- a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ResponseParser.java +++ b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ResponseParser.java @@ -33,7 +33,6 @@ public Contract parseResponse(Map queryResponse) throws // Parse other fields Optional.ofNullable(queryResponse.get("contract_created")) .map(AttributeValue::s) - .map(Long::valueOf) .ifPresent(contract::setContractCreated); Optional.ofNullable(queryResponse.get("contract_id")) @@ -42,7 +41,6 @@ public Contract parseResponse(Map queryResponse) throws Optional.ofNullable(queryResponse.get("contract_last_modified_on")) .map(AttributeValue::s) - .map(Long::valueOf) .ifPresent(contract::setContractLastModifiedOn); Optional.ofNullable(queryResponse.get("contract_status")) From 5a2b34e8f3cb3f207aa876c5c1add514c0429209 Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Mon, 16 Mar 2026 09:52:14 +0800 Subject: [PATCH 2/4] refactor(contracts): Rename ContractEventHandler to ContractEventHandlerFunction and add metrics - Rename class from ContractEventHandler to ContractEventHandlerFunction for consistency - Update all constructor references to use new class name - Update logger initialization to reference new class name - Add metrics tracking for ContractCreated and ContractUpdated events - Reorganize imports to follow alphabetical ordering - Change timestamp fields from Long (epoch millis) to String (ISO-8601 format) - Update DynamoDB attribute builders to use string type for timestamp fields - Clean up whitespace inconsistencies throughout the file --- ...java => ContractEventHandlerFunction.java} | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) rename unicorn_contracts/ContractsService/src/main/java/contracts/{ContractEventHandler.java => ContractEventHandlerFunction.java} (90%) diff --git a/unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandler.java b/unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandlerFunction.java similarity index 90% rename from unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandler.java rename to unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandlerFunction.java index 4de4200..7e26d13 100644 --- a/unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandler.java +++ b/unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandlerFunction.java @@ -13,28 +13,31 @@ import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.*; import software.amazon.lambda.powertools.logging.Logging; -import software.amazon.lambda.powertools.tracing.Tracing; import software.amazon.lambda.powertools.metrics.FlushMetrics; +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; +import software.amazon.lambda.powertools.tracing.Tracing; import java.time.Instant; import java.util.Map; import java.util.Optional; import java.util.UUID; -public class ContractEventHandler implements RequestHandler { +public class ContractEventHandlerFunction implements RequestHandler { 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 Logger LOGGER = LogManager.getLogger(ContractEventHandlerFunction.class); private static final String HTTP_METHOD_ATTR = "HttpMethod"; private final DynamoDbClient dynamodbClient; - public ContractEventHandler() { + public ContractEventHandlerFunction() { this(DynamoDbClient.builder().build()); } - public ContractEventHandler(DynamoDbClient dynamodbClient) { + public ContractEventHandlerFunction(DynamoDbClient dynamodbClient) { this.dynamodbClient = dynamodbClient; } @@ -56,11 +59,11 @@ public Void handleRequest(SQSEvent event, Context context) { 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; @@ -95,15 +98,15 @@ private String extractHttpMethod(SQSMessage msg) { private void createContract(String contractJson) throws JsonProcessingException { Contract contract = OBJECT_MAPPER.readValue(contractJson, Contract.class); validateContract(contract); - + String contractId = UUID.randomUUID().toString(); - long timestamp = Instant.now().toEpochMilli(); + String timestamp = Instant.now().toString(); 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_created", AttributeValue.builder().s(timestamp).build(), + "contract_last_modified_on", AttributeValue.builder().s(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() @@ -124,6 +127,7 @@ private void createContract(String contractJson) throws JsonProcessingException try { dynamodbClient.putItem(request); + MetricsFactory.getMetricsInstance().addMetric("ContractCreated", 1, MetricUnit.COUNT); } catch (ConditionalCheckFailedException e) { LOGGER.error("Active contract already exists for property: {}", contract.getPropertyId()); throw new IllegalStateException("Contract already exists for property: " + contract.getPropertyId(), e); @@ -134,7 +138,7 @@ private void createContract(String contractJson) throws JsonProcessingException 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( @@ -144,7 +148,7 @@ private void updateContract(String contractJson) throws JsonProcessingException 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() + ":modifiedDate", AttributeValue.builder().s(Instant.now().toString()).build() ); UpdateItemRequest request = UpdateItemRequest.builder() @@ -157,6 +161,7 @@ private void updateContract(String contractJson) throws JsonProcessingException try { dynamodbClient.updateItem(request); + MetricsFactory.getMetricsInstance().addMetric("ContractUpdated", 1, MetricUnit.COUNT); } 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); From e972e8c9a6d3656b0cbf83c4784011d000a76400 Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Mon, 16 Mar 2026 09:52:32 +0800 Subject: [PATCH 3/4] test(contracts): Update CreateContractTests to use renamed ContractEventHandlerFunction - Update handler field type from ContractEventHandler to ContractEventHandlerFunction - Update handler instantiation in setUp() to use ContractEventHandlerFunction constructor - Align test class with recent refactoring that renamed ContractEventHandler to ContractEventHandlerFunction --- .../src/test/java/contracts/CreateContractTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unicorn_contracts/ContractsService/src/test/java/contracts/CreateContractTests.java b/unicorn_contracts/ContractsService/src/test/java/contracts/CreateContractTests.java index aec7b53..8f3d2be 100644 --- a/unicorn_contracts/ContractsService/src/test/java/contracts/CreateContractTests.java +++ b/unicorn_contracts/ContractsService/src/test/java/contracts/CreateContractTests.java @@ -28,11 +28,11 @@ public class CreateContractTests { @Mock private DynamoDbClient dynamoDbClient; - private ContractEventHandler handler; + private ContractEventHandlerFunction handler; @Before public void setUp() { - handler = new ContractEventHandler(dynamoDbClient); + handler = new ContractEventHandlerFunction(dynamoDbClient); } @Test From c6681e9e564a723361a3ee9c1c95f3bff853456f Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Mon, 16 Mar 2026 09:52:59 +0800 Subject: [PATCH 4/4] feat(publicationmanager): Add metrics tracking and enhance event payload - Import Powertools metrics classes (Metrics, MetricsFactory, MetricUnit) - Add validation for evaluation results (APPROVED/DECLINED) in PublicationEvaluationEventHandler - Track PropertiesApproved metric when property status is updated - Add SERVICE_NAMESPACE environment variable to RequestApprovalFunction - Expand RequestApproval event payload with status, listprice, images, description, and currency fields - Add corresponding getters and setters for new RequestApproval properties - Use SERVICE_NAMESPACE for EventBridge event source instead of hardcoded "Unicorn.Web" - Track ApprovalsRequested metric when events are sent to EventBridge - Improve code formatting and consistency --- .../PublicationEvaluationEventHandler.java | 11 ++- .../RequestApprovalFunction.java | 73 ++++++++++++++++++- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/PublicationEvaluationEventHandler.java b/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/PublicationEvaluationEventHandler.java index 964cb3f..eb63cdc 100644 --- a/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/PublicationEvaluationEventHandler.java +++ b/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/PublicationEvaluationEventHandler.java @@ -22,6 +22,9 @@ import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.metrics.FlushMetrics; +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; import software.amazon.lambda.powertools.tracing.Tracing; import schema.unicorn_approvals.publicationevaluationcompleted.marshaller.Marshaller; import schema.unicorn_approvals.publicationevaluationcompleted.AWSEvent; @@ -98,6 +101,11 @@ public void handleRequest(InputStream inputStream, OutputStream outputStream, Co private void updatePropertyStatus(String evaluationResult, String propertyId) { logger.info("Updating property status for property ID: {}", propertyId); logger.info("Evaluation result: {}", evaluationResult); + if (!"APPROVED".equalsIgnoreCase(evaluationResult) && !"DECLINED".equalsIgnoreCase(evaluationResult)) { + logger.warn("Unknown evaluation result '{}', skipping DynamoDB update", evaluationResult); + return; + } + try { String[] parts = propertyId.split("/"); if (parts.length != 4) { @@ -124,7 +132,8 @@ private void updatePropertyStatus(String evaluationResult, String propertyId) { logger.info("Updating property {} with status: {}", propertyId, evaluationResult); propertyTable.putItem(existingProperty).join(); - + MetricsFactory.getMetricsInstance().addMetric("PropertiesApproved", 1, MetricUnit.COUNT); + } 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 83773a1..29551be 100644 --- a/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/RequestApprovalFunction.java +++ b/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/RequestApprovalFunction.java @@ -37,6 +37,9 @@ import software.amazon.awssdk.services.eventbridge.model.PutEventsRequestEntry; import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.metrics.FlushMetrics; +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.metrics.MetricsFactory; +import software.amazon.lambda.powertools.metrics.model.MetricUnit; import software.amazon.lambda.powertools.tracing.Tracing; /** @@ -48,6 +51,8 @@ public class RequestApprovalFunction { 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 SERVICE_NAMESPACE = System.getenv("SERVICE_NAMESPACE"); + 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"); @@ -188,19 +193,25 @@ 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); + event.setStatus("PENDING"); + event.setListprice(property.getListprice()); + event.setImages(property.getImages()); + event.setDescription(property.getDescription()); + event.setCurrency(property.getCurrency()); + String eventString = objectMapper.writeValueAsString(event); logger.info("Event payload created: {}", eventString); PutEventsRequestEntry requestEntry = PutEventsRequestEntry.builder() .eventBusName(eventBus) - .source("Unicorn.Web") + .source(SERVICE_NAMESPACE) .resources(property.getId()) .detailType("PublicationApprovalRequested") .detail(eventString) @@ -213,9 +224,10 @@ private void sendEvent(Property property) throws JsonProcessingException { logger.debug("Sending event to EventBridge bus: {}", eventBus); try { eventBridgeClient.putEvents(eventsRequest).join(); + MetricsFactory.getMetricsInstance().addMetric("ApprovalsRequested", 1, MetricUnit.COUNT); logger.info("Event sent successfully for property: {}", property.getId()); } catch (Exception e) { - logger.error("Failed to send event to EventBridge for property: {}, bus: {}", + logger.error("Failed to send event to EventBridge for property: {}, bus: {}", property.getId(), eventBus, e); throw e; } @@ -227,6 +239,21 @@ class RequestApproval { String propertyId; Address address; + @JsonProperty("status") + String status; + + @JsonProperty("listprice") + Float listprice; + + @JsonProperty("images") + java.util.List images; + + @JsonProperty("description") + String description; + + @JsonProperty("currency") + String currency; + public String getPropertyId() { return propertyId; } @@ -242,6 +269,46 @@ public Address getAddress() { public void setAddress(Address address) { this.address = address; } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Float getListprice() { + return listprice; + } + + public void setListprice(Float listprice) { + this.listprice = listprice; + } + + public java.util.List getImages() { + return images; + } + + public void setImages(java.util.List images) { + this.images = images; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getCurrency() { + return currency; + } + + public void setCurrency(String currency) { + this.currency = currency; + } } class Address {