From d2ee99ac5ea50949d89e9a1d6591d74a33290ce6 Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Mon, 16 Mar 2026 09:31:48 +0800 Subject: [PATCH 1/5] fix(approvals_service): Remove unused imports and replace InternalServerError - Remove unused datetime import and current_date variable calculation - Replace InternalServerError with EnvironmentError for environment variable validation - Simplify error handling for SERVICE_NAMESPACE and CONTRACT_STATUS_TABLE checks - InternalServerError is not appropriate for environment configuration issues that occur at module initialization --- .../contract_status_changed_event_handler.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/unicorn_approvals/src/approvals_service/contract_status_changed_event_handler.py b/unicorn_approvals/src/approvals_service/contract_status_changed_event_handler.py index 11f952d..e1b707b 100644 --- a/unicorn_approvals/src/approvals_service/contract_status_changed_event_handler.py +++ b/unicorn_approvals/src/approvals_service/contract_status_changed_event_handler.py @@ -2,20 +2,18 @@ # SPDX-License-Identifier: MIT-0 import os -from datetime import datetime import boto3 from aws_lambda_powertools.logging import Logger from aws_lambda_powertools.metrics import Metrics from aws_lambda_powertools.tracing import Tracer -from aws_lambda_powertools.event_handler.exceptions import InternalServerError from schema.unicorn_contracts.contractstatuschanged import AWSEvent, ContractStatusChanged, Marshaller # Initialise Environment variables if (SERVICE_NAMESPACE := os.environ.get("SERVICE_NAMESPACE")) is None: - raise InternalServerError("SERVICE_NAMESPACE environment variable is undefined") + raise EnvironmentError("SERVICE_NAMESPACE environment variable is undefined") if (CONTRACT_STATUS_TABLE := os.environ.get("CONTRACT_STATUS_TABLE")) is None: - raise InternalServerError("CONTRACT_STATUS_TABLE environment variable is undefined") + raise EnvironmentError("CONTRACT_STATUS_TABLE environment variable is undefined") # Initialise PowerTools logger: Logger = Logger() @@ -26,10 +24,6 @@ dynamodb = boto3.resource("dynamodb") table = dynamodb.Table(CONTRACT_STATUS_TABLE) # type: ignore -# Get current date -now = datetime.now() -current_date = now.strftime("%d/%m/%Y %H:%M:%S") - @logger.inject_lambda_context(log_event=True) # type: ignore @metrics.log_metrics(capture_cold_start_metric=True) # type: ignore From 9832a2e6f83c4e6d1a7f97e610e6b070f4ef1f15 Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Mon, 16 Mar 2026 09:39:48 +0800 Subject: [PATCH 2/5] fix(publication_manager_service): Add evaluation result validation and remove unused metric - Add validation for evaluation_result to ensure only "APPROVED" or "DECLINED" values are processed - Return early with skip message when unknown evaluation result is encountered - Log warning when evaluation result is invalid for debugging purposes - Remove unused PropertiesAdded metric that was not being utilized - Improve robustness by preventing invalid data from being written to DynamoDB --- .../publication_evaluation_event_handler.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/unicorn_web/src/publication_manager_service/publication_evaluation_event_handler.py b/unicorn_web/src/publication_manager_service/publication_evaluation_event_handler.py index 2c252bb..1b137c5 100644 --- a/unicorn_web/src/publication_manager_service/publication_evaluation_event_handler.py +++ b/unicorn_web/src/publication_manager_service/publication_evaluation_event_handler.py @@ -74,14 +74,18 @@ def publication_approved(event_detail, errors): property_id = event_detail.property_id evaluation_result = event_detail.evaluation_result + + valid_results = {"APPROVED", "DECLINED"} + if evaluation_result.upper() not in valid_results: + logger.warning(f"Unknown evaluation_result '{evaluation_result}'; skipping DynamoDB update") + return {"result": "Skipped — unknown evaluation result"} + country, city, street, number = property_id.split("/") pk_details = f"{country}#{city}".replace(" ", "-").lower() pk = f"PROPERTY#{pk_details}" sk = f"{street}#{str(number)}".replace(" ", "-").lower() - metrics.add_metric(name="PropertiesAdded", unit=MetricUnit.Count, value=1) - logger.info(f"Storing new property in DynamoDB with PK {pk} and SK {sk}") dynamodb_response = table.update_item( Key={ From 25e292c318192fafefff138e16e0b48a4391b4e3 Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Mon, 16 Mar 2026 09:41:56 +0800 Subject: [PATCH 3/5] fix(contracts_service): Improve datetime handling and add contract metrics - Import timezone from datetime module for UTC timestamp generation - Import MetricUnit from aws_lambda_powertools.metrics for proper metric typing - Replace strftime formatting with isoformat() for ISO 8601 compliant timestamps in create_contract and update_contract functions - Add metric tracking for successful contract creation events - Rename UpdateExpression attribute from modified_date to contract_last_modified_on for consistency - Ensures timestamps are timezone-aware and use UTC for consistency across distributed systems --- .../src/contracts_service/contract_event_handler.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/unicorn_contracts/src/contracts_service/contract_event_handler.py b/unicorn_contracts/src/contracts_service/contract_event_handler.py index 93b6a80..2bbb447 100644 --- a/unicorn_contracts/src/contracts_service/contract_event_handler.py +++ b/unicorn_contracts/src/contracts_service/contract_event_handler.py @@ -3,14 +3,14 @@ import os import uuid -from datetime import datetime +from datetime import datetime, timezone import boto3 from boto3.dynamodb.conditions import Attr from botocore.exceptions import ClientError from aws_lambda_powertools.logging import Logger -from aws_lambda_powertools.metrics import Metrics +from aws_lambda_powertools.metrics import Metrics, MetricUnit from aws_lambda_powertools.tracing import Tracer from aws_lambda_powertools.utilities.data_classes import event_source, SQSEvent from aws_lambda_powertools.utilities.typing import LambdaContext @@ -76,7 +76,7 @@ def create_contract(event: dict) -> None: DynamoDB put Item response """ - current_date = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + current_date = datetime.now(timezone.utc).isoformat() contract = { "property_id": event["property_id"], # PK "address": event["address"], @@ -105,6 +105,7 @@ def create_contract(event: dict) -> None: # Annotate trace with contract status tracer.put_annotation(key="ContractStatus", value=contract["contract_status"]) + metrics.add_metric(name="ContractCreated", unit=MetricUnit.Count, value=1) except ClientError as e: code = e.response["Error"]["Code"] @@ -147,13 +148,13 @@ def update_contract(contract: dict) -> None: try: contract["contract_status"] = ContractStatus.APPROVED.name - current_date = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + current_date = datetime.now(timezone.utc).isoformat() response = table.update_item( Key={ "property_id": contract["property_id"], }, - UpdateExpression="set contract_status=:t, modified_date=:m", + UpdateExpression="set contract_status=:t, contract_last_modified_on=:m", ConditionExpression=Attr("property_id").exists() & Attr("contract_status").is_in([ContractStatus.DRAFT.name]), ExpressionAttributeValues={ From 9f266dd91d487ed2d96c900aae7e8d1536c40a39 Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Mon, 16 Mar 2026 09:46:10 +0800 Subject: [PATCH 4/5] chore(deps): bump dependencies to latest versions --- unicorn_contracts/uv.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/unicorn_contracts/uv.lock b/unicorn_contracts/uv.lock index f5ee7b3..1226f38 100644 --- a/unicorn_contracts/uv.lock +++ b/unicorn_contracts/uv.lock @@ -241,16 +241,16 @@ dev = [ [package.metadata] requires-dist = [ { name = "arnparse", marker = "extra == 'dev'", specifier = ">=0.0.2" }, - { name = "aws-lambda-powertools", extras = ["all"], marker = "extra == 'dev'", specifier = ">=3.24.0" }, - { name = "aws-lambda-powertools", extras = ["tracer"], specifier = ">=3.24.0" }, + { name = "aws-lambda-powertools", extras = ["all"], marker = "extra == 'dev'", specifier = ">=3.25.0" }, + { name = "aws-lambda-powertools", extras = ["tracer"], specifier = ">=3.25.0" }, { name = "aws-xray-sdk", specifier = ">=2.15.0" }, - { name = "boto3", specifier = ">=1.42.43" }, + { name = "boto3", specifier = ">=1.42.68" }, { name = "importlib-metadata", marker = "extra == 'dev'", specifier = ">=8.7.1" }, - { name = "moto", extras = ["dynamodb", "events", "sqs"], marker = "extra == 'dev'", specifier = ">=5.1.20" }, + { name = "moto", extras = ["dynamodb", "events", "sqs"], marker = "extra == 'dev'", specifier = ">=5.1.22" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" }, { name = "pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.3" }, { name = "requests", marker = "extra == 'dev'", specifier = ">=2.32.5" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.6" }, { name = "tomli", marker = "extra == 'dev'", specifier = ">=2.4.0" }, ] provides-extras = ["dev"] From 6a0825ace24f4cb6128616c38990de10d1b76b0c Mon Sep 17 00:00:00 2001 From: Stephen Liedig Date: Mon, 16 Mar 2026 14:05:20 +0800 Subject: [PATCH 5/5] ci: Add explicit permissions to workflow jobs - Add actions: read permission for workflow execution access - Add contents: read permission for repository content access - Scope down GitHub Token permissions following security best practices --- .github/workflows/on_merged_pr.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/on_merged_pr.yml b/.github/workflows/on_merged_pr.yml index 8508999..4fb154c 100644 --- a/.github/workflows/on_merged_pr.yml +++ b/.github/workflows/on_merged_pr.yml @@ -7,6 +7,8 @@ on: - completed permissions: + actions: read + contents: read issues: write jobs: