diff --git a/.github/workflows/auto_assign.yml b/.github/workflows/auto_assign.yml
index 5728867..093e194 100644
--- a/.github/workflows/auto_assign.yml
+++ b/.github/workflows/auto_assign.yml
@@ -7,4 +7,4 @@ jobs:
add-reviews:
runs-on: ubuntu-latest
steps:
- - uses: kentaro-m/auto-assign-action@v1.2.5
\ No newline at end of file
+ - uses: kentaro-m/auto-assign-action@v2.0.0
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index dae9ea7..36acbf8 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -5,13 +5,13 @@ on:
branches: [develop, main]
paths:
- 'unicorn_contracts/**'
- - 'unicorn_properties/**'
+ - 'unicorn_approvals/**'
- 'unicorn_web/**'
pull_request:
branches: [develop, main]
paths:
- 'unicorn_contracts/**'
- - 'unicorn_properties/**'
+ - 'unicorn_approvals/**'
- 'unicorn_web/**'
defaults:
@@ -21,22 +21,22 @@ defaults:
jobs:
build:
runs-on: ubuntu-latest
- timeout-minutes: 5
+ timeout-minutes: 10
strategy:
max-parallel: 4
matrix:
- # test against latest update of each major Java version, as well as specific updates of LTS versions:
- java: [17]
+ # test against latest LTS Java versions
+ java: [17, 21]
name: Java ${{ matrix.java }}
env:
JAVA: ${{ matrix.java }}
AWS_REGION: us-west-2
steps:
- - uses: actions/checkout@v3
- - name: Setup java
+ - uses: actions/checkout@v4
+ - name: Setup Java
uses: actions/setup-java@v4
with:
- distribution: 'zulu'
+ distribution: 'temurin'
java-version: ${{ matrix.java }}
cache: maven
cache-dependency-path: '**/pom.xml'
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index a0a806a..61866de 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -26,24 +26,24 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup Java JDK
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
distribution: 'temurin'
- java-version: 17
+ java-version: 21
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@2ca79b6fa8d3ec278944088b4aa5f46912db5d63 #v2
+ uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@2ca79b6fa8d3ec278944088b4aa5f46912db5d63 #v2
+ uses: github/codeql-action/autobuild@v3
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -56,4 +56,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@2ca79b6fa8d3ec278944088b4aa5f46912db5d63 #v2
+ uses: github/codeql-action/analyze@v3
diff --git a/.github/workflows/label_pr_on_title.yml b/.github/workflows/label_pr_on_title.yml
index 3815a49..e6ce47d 100644
--- a/.github/workflows/label_pr_on_title.yml
+++ b/.github/workflows/label_pr_on_title.yml
@@ -22,9 +22,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: "Label PR based on title"
- uses: actions/github-script@v6
+ uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }}
PR_TITLE: ${{ needs.get_pr_details.outputs.prTitle }}
diff --git a/.github/workflows/on_label_added.yml b/.github/workflows/on_label_added.yml
index e9180d8..ab04444 100644
--- a/.github/workflows/on_label_added.yml
+++ b/.github/workflows/on_label_added.yml
@@ -23,10 +23,10 @@ jobs:
issues: write
pull-requests: write
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
# Maintenance: Persist state per PR as an artifact to avoid spam on label add
- name: "Suggest split large Pull Request"
- uses: actions/github-script@v6
+ uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }}
PR_ACTION: ${{ needs.get_pr_details.outputs.prAction }}
diff --git a/.github/workflows/on_merged_pr.yml b/.github/workflows/on_merged_pr.yml
index cd97e1c..2bce046 100644
--- a/.github/workflows/on_merged_pr.yml
+++ b/.github/workflows/on_merged_pr.yml
@@ -20,9 +20,9 @@ jobs:
runs-on: ubuntu-latest
if: needs.get_pr_details.outputs.prIsMerged == 'true'
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: "Label PR related issue for release"
- uses: actions/github-script@v6
+ uses: actions/github-script@v7
env:
PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }}
PR_BODY: ${{ needs.get_pr_details.outputs.prBody }}
diff --git a/.github/workflows/on_opened_pr.yml b/.github/workflows/on_opened_pr.yml
index 043ff96..9712a3f 100644
--- a/.github/workflows/on_opened_pr.yml
+++ b/.github/workflows/on_opened_pr.yml
@@ -19,9 +19,9 @@ jobs:
needs: get_pr_details
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: "Ensure related issue is present"
- uses: actions/github-script@v6
+ uses: actions/github-script@v7
env:
PR_BODY: ${{ needs.get_pr_details.outputs.prBody }}
PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }}
@@ -36,9 +36,9 @@ jobs:
needs: get_pr_details
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: "Ensure acknowledgement section is present"
- uses: actions/github-script@v6
+ uses: actions/github-script@v7
env:
PR_BODY: ${{ needs.get_pr_details.outputs.prBody }}
PR_NUMBER: ${{ needs.get_pr_details.outputs.prNumber }}
diff --git a/.github/workflows/record_pr.yml b/.github/workflows/record_pr.yml
index 44f445a..7ef50e4 100644
--- a/.github/workflows/record_pr.yml
+++ b/.github/workflows/record_pr.yml
@@ -9,14 +9,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: "Extract PR details"
- uses: actions/github-script@v6
+ uses: actions/github-script@v7
with:
script: |
const script = require('.github/scripts/save_pr_details.js')
await script({github, context, core})
- - uses: actions/upload-artifact@v3
+ - uses: actions/upload-artifact@v4
with:
name: pr
path: pr.txt
diff --git a/.github/workflows/reusable_export_pr_details.yml b/.github/workflows/reusable_export_pr_details.yml
index c3b2aaf..e834e60 100644
--- a/.github/workflows/reusable_export_pr_details.yml
+++ b/.github/workflows/reusable_export_pr_details.yml
@@ -53,9 +53,9 @@ jobs:
prIsMerged: ${{ steps.prIsMerged.outputs.prIsMerged }}
steps:
- name: Checkout repository # in case caller workflow doesn't checkout thus failing with file not found
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: "Download previously saved PR"
- uses: actions/github-script@v6
+ uses: actions/github-script@v7
env:
WORKFLOW_ID: ${{ inputs.record_pr_workflow_id }}
# For security, we only download artifacts tied to the successful PR recording workflow
diff --git a/.gitignore b/.gitignore
index 50ad28e..856e984 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,9 @@
-unicorn_properties/PropertyFunctions/target/*
-unicorn_contracts/ContractsFunction/target/**
-unicorn_web/PropertyFunctions/target/**
+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**
.vscode/settings.json
@@ -8,7 +11,8 @@ buildall.sh
deleteall.sh
/.idea/**
/cloudapp.iml
-/unicorn_properties/PropertyFunctions/PropertyService.iml
+/unicorn_approvals/PropertyFunctions/PropertyService.iml
/unicorn_web/PropertyFunctions/PropertyWeb.iml
/unicorn_contracts/ContractsFunction/ContractsModule.iml
-**/cdk.out/
\ No newline at end of file
+**/cdk.out/
+**/dependency-reduced-pom.xml
diff --git a/README.md b/README.md
index 580b489..f55f4b3 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,32 @@
-
+[](https://github.com/aws-samples/aws-serverless-developer-experience-workshop-java/actions/workflows/build.yml)
# AWS Serverless Developer Experience workshop reference architecture (Java)
-This repository contains the reference architecture for the AWS Serverless Developer Experience workshop.
+
+
+This repository contains the Java reference architecture for the AWS Serverless Developer Experience workshop.
-The AWS Serverless Developer Experience workshop provides you with an immersive experience as a serverless developer. The goal of this workshop is to provide you with hands-on experience building a serverless solution using the [**AWS Serverless Application Model (AWS SAM)**](https://aws.amazon.com/serverless/sam/) and **AWS SAM CLI**.
+The AWS Serverless Developer Experience Workshop is a comprehensive, hands-on training program designed to equip developers with practical serverless development skills using the [**AWS Serverless Application Model (AWS SAM)**](https://aws.amazon.com/serverless/sam/) and **AWS SAM CLI**.
-Along the way, you will learn about principals of distributed event-driven architectures, messaging patterns, orchestration, and observability and how to apply them in code. You will explore exciting open-source tools, the core features of Powertools for AWS Lambda, and simplified CI/CD deployments supported by AWS SAM Pipelines.
+The workshop employs a practical, code-centric approach, emphasizing direct implementation and real-world scenario exploration to ensure you develop serverless development skills across several critical areas including distributed event-driven architectures, messaging patterns, orchestration, and observability. You will explore open-source tools, [Powertools for AWS](https://powertools.aws.dev/), and simplified CI/CD deployments with AWS SAM Pipelines. By the end, you will be familiar with serverless developer workflows, microservice composition using AWS SAM, serverless development best practices, and applied event-driven architectures.
-At the end of this workshop, you will be familiar with Serverless developer workflows and microservice composition using AWS SAM, Serverless development best practices, and applied event-driven architectures.
+The 6-8 hour workshop assumes your practical development skills in Python, TypeScript, Java, or .NET, and familiarity with [Amazon API Gateway](https://aws.amazon.com/apigateway/), [AWS Lambda](https://aws.amazon.com/lambda/), [Amazon EventBridge](https://aws.amazon.com/eventbridge/), [AWS Step Functions](https://aws.amazon.com/step-functions/), and [Amazon DynamoDB](https://aws.amazon.com/dynamodb/).
## Introducing the Unicorn Properties architecture

-Our use case is based on a real estate company called **Unicorn Properties**.
-
-As a real estate agency, **Unicorn Properties** needs to manage the publishing of new property listings and sale contracts linked to individual properties, and provide a way for their customers to view approved property listings.
-
-To support their needs, Unicorn Properties have adopted a serverless, event-driven approach to designing their architecture. This architecture is centred around two primary domains: **Contracts** (managed by the Contracts Service) and **Properties** (managed by the Web and Properties Services).
-
-The **Unicorn Contracts** service (namespace: `Unicorn.Contracts`) is a simplified service that manages the contractual relationship between a seller of a property and Unicorn Properties. Contracts are drawn up that define the property for sale, the terms and conditions that Unicorn Properties sets, and how much it will cost the seller to engage the services of the agency.
+Real estate company **Unicorn Properties** needs to manage publishing of new property listings and sale contracts linked to individual properties, and provide a way for customers to view approved listings. They adopted a serverless, event-driven architecture with two primary domains: **Contracts** (managed by the Contracts Service) and **Properties** (managed by the Web and Approvals Services).
-The **Unicorn Web** (namespace: `Unicorn.Web`) manages the details of a property listing to be published on the Unicorn Properties website. Every property listing has an address, a sale price, a description of the property, and some photos that members of the public can look at to get them interested in purchasing the property. Only properties that have been approved for publication can be made visible to the public.
+**Unicorn Contracts** (using the `Unicorn.Contracts` namespace) service manages contractual relationships between property sellers and Unicorn Approvals, defining properties for sale, terms, and engagement costs.
-The **Unicorn Properties** service (namespace: `Unicorn.Properties`) approves a property listings. This service implements a workflow that checks for the existence of a contract, makes sure that the content and the images are safe to publish, and finally checks that the contract has been approved. We don’t want to publish a property until we have an approved contract!
+**Unicorn Approvals** (using the `Unicorn.Approvals` namespace) service approves property listings by implementing a workflow that checks for contract existence, content and image safety, and contract approval before publishing.
-Have a go at building this architecture yourself! Head over to the [Serverless Developer Experience Workshop](https://catalog.workshops.aws/serverless-developer-experience) for more details.
+**Unicorn Web** (using the `Unicorn.Web` namespace) manages property listing details (address, sale price, description, photos) to be published on the website, with only approved listings visible to the public.
## Credits
-Throughout this workshop we wanted to introduce you to some Open Source tools that can help you build serverless applications. This is not an exhaustive list, just a small selection of what we will be using in the workshop.
+This workshop introduces you to some open-source tools that can help you build serverless applications. This is not an exhaustive list, but a small selection of what you will be using in the workshop.
Many thanks to all the AWS teams and community builders who have contributed to this list:
diff --git a/docs/architecture.png b/docs/architecture.png
index 1e741f2..903363b 100644
Binary files a/docs/architecture.png and b/docs/architecture.png differ
diff --git a/pom.xml b/pom.xml
index 2b009f0..8063031 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,13 +3,15 @@
4.0.0
unicornproperties
- cloudapp
+ UnicornProperties
1.0
pom
- unicorn_contracts/ContractsFunction
- unicorn_properties/PropertyFunctions
- unicorn_web/PropertyFunctions
+ unicorn_contracts/ContractsService
+ unicorn_approvals/ApprovalsService
+ unicorn_web/PublicationManagerService
+ unicorn_web/SearchService
+ unicorn_web/Common
\ No newline at end of file
diff --git a/unicorn_properties/.gitignore b/unicorn_approvals/.gitignore
similarity index 100%
rename from unicorn_properties/.gitignore
rename to unicorn_approvals/.gitignore
diff --git a/unicorn_properties/PropertyFunctions/pom.xml b/unicorn_approvals/ApprovalsService/pom.xml
similarity index 82%
rename from unicorn_properties/PropertyFunctions/pom.xml
rename to unicorn_approvals/ApprovalsService/pom.xml
index 4d5d7fd..24f69f3 100644
--- a/unicorn_properties/PropertyFunctions/pom.xml
+++ b/unicorn_approvals/ApprovalsService/pom.xml
@@ -1,23 +1,22 @@
4.0.0
- property
- PropertyService
+ approval
+ ApprovalsService
1.0
jar
- Property service module for java reference architecture.
+ Unicorn Approvals Service module
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
@@ -72,52 +71,39 @@
eventbridge
${aws.java.sdk.version}
-
- com.amazonaws
- aws-lambda-java-events
- ${aws-lambda-java-events.version}
-
-
software.amazon.awssdk
netty-nio-client
${netty-nio-client.version}
-
-
- com.amazonaws
- aws-lambda-java-core
- 1.2.2
-
-
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
@@ -146,7 +132,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- 2.22.2
+ 3.5.3
handler
@@ -156,7 +142,7 @@
org.apache.maven.plugins
maven-shade-plugin
- 3.2.4
+ 3.6.0
@@ -171,11 +157,10 @@
dev.aspectj
aspectj-maven-plugin
- 1.13.1
+ 1.14.1
- 17
- 17
17
+ 17
software.amazon.lambda
@@ -198,13 +183,21 @@
+
+
+ org.aspectj
+ aspectjtools
+ 1.9.24
+
+
org.apache.maven.plugins
maven-compiler-plugin
+ 3.14.0
- 16
- 16
+ 17
+ 17
diff --git a/unicorn_approvals/ApprovalsService/src/main/java/approvals/ContractStatusChangedHandlerFunction.java b/unicorn_approvals/ApprovalsService/src/main/java/approvals/ContractStatusChangedHandlerFunction.java
new file mode 100644
index 0000000..0a82802
--- /dev/null
+++ b/unicorn_approvals/ApprovalsService/src/main/java/approvals/ContractStatusChangedHandlerFunction.java
@@ -0,0 +1,86 @@
+package approvals;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import schema.unicorn_contracts.contractstatuschanged.ContractStatusChanged;
+import schema.unicorn_contracts.contractstatuschanged.Event;
+import schema.unicorn_contracts.contractstatuschanged.marshaller.Marshaller;
+import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
+import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
+import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
+import software.amazon.lambda.powertools.logging.Logging;
+import software.amazon.lambda.powertools.metrics.Metrics;
+import software.amazon.lambda.powertools.tracing.Tracing;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Lambda handler to update the contract status change
+ */
+public class ContractStatusChangedHandlerFunction {
+
+ 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_properties/PropertyFunctions/src/main/java/properties/ContractStatusNotFoundException.java b/unicorn_approvals/ApprovalsService/src/main/java/approvals/ContractStatusNotFoundException.java
similarity index 89%
rename from unicorn_properties/PropertyFunctions/src/main/java/properties/ContractStatusNotFoundException.java
rename to unicorn_approvals/ApprovalsService/src/main/java/approvals/ContractStatusNotFoundException.java
index 34e2608..7ece33c 100644
--- a/unicorn_properties/PropertyFunctions/src/main/java/properties/ContractStatusNotFoundException.java
+++ b/unicorn_approvals/ApprovalsService/src/main/java/approvals/ContractStatusNotFoundException.java
@@ -1,4 +1,4 @@
-package properties;
+package approvals;
public class ContractStatusNotFoundException extends Exception {
public ContractStatusNotFoundException(String errorMessage) {
diff --git a/unicorn_approvals/ApprovalsService/src/main/java/approvals/PropertiesApprovalSyncFunction.java b/unicorn_approvals/ApprovalsService/src/main/java/approvals/PropertiesApprovalSyncFunction.java
new file mode 100644
index 0000000..afcfb45
--- /dev/null
+++ b/unicorn_approvals/ApprovalsService/src/main/java/approvals/PropertiesApprovalSyncFunction.java
@@ -0,0 +1,116 @@
+package approvals;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.DynamodbEvent;
+import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse;
+import com.amazonaws.services.lambda.runtime.events.models.dynamodb.AttributeValue;
+import com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import approvals.dao.ContractStatus;
+import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
+import software.amazon.awssdk.services.sfn.SfnAsyncClient;
+import software.amazon.awssdk.services.sfn.model.SendTaskSuccessRequest;
+import software.amazon.lambda.powertools.logging.Logging;
+import software.amazon.lambda.powertools.metrics.Metrics;
+import software.amazon.lambda.powertools.tracing.Tracing;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+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 {
+
+ 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<>();
+
+ for (DynamodbEvent.DynamodbStreamRecord dynamodbStreamRecord : input.getRecords()) {
+ String sequenceNumber = dynamodbStreamRecord.getDynamodb().getSequenceNumber();
+
+ try {
+ if (!processRecord(dynamodbStreamRecord)) {
+ continue; // Skip this record but don't fail
+ }
+ } catch (Exception e) {
+ logger.error("Failed to process record with sequence number: {}", sequenceNumber, e);
+ batchItemFailures.add(new StreamsEventResponse.BatchItemFailure(sequenceNumber));
+ return new StreamsEventResponse(batchItemFailures);
+ }
+ }
+
+ return new StreamsEventResponse();
+ }
+
+ private boolean processRecord(DynamodbEvent.DynamodbStreamRecord streamRecord) throws JsonProcessingException {
+ StreamRecord dynamodbRecord = streamRecord.getDynamodb();
+ Map newImage = dynamodbRecord.getNewImage();
+ Map oldImage = dynamodbRecord.getOldImage();
+
+ 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
new file mode 100644
index 0000000..c1f5f3e
--- /dev/null
+++ b/unicorn_approvals/ApprovalsService/src/main/java/approvals/WaitForContractApprovalFunction.java
@@ -0,0 +1,95 @@
+package approvals;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
+import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
+import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
+import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
+import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
+import software.amazon.lambda.powertools.logging.Logging;
+import software.amazon.lambda.powertools.metrics.Metrics;
+import software.amazon.lambda.powertools.tracing.Tracing;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.concurrent.CompletionException;
+
+/**
+ * Lambda handler to wait for contract approval in Step Functions workflow
+ */
+public class WaitForContractApprovalFunction {
+
+ 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 = 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
new file mode 100644
index 0000000..7244edc
--- /dev/null
+++ b/unicorn_approvals/ApprovalsService/src/main/java/approvals/dao/ContractStatus.java
@@ -0,0 +1,89 @@
+package approvals.dao;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Data class representing contract status information
+ */
+public class ContractStatus {
+
+ @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;
+
+ private ContractStatus(Builder builder) {
+ this.contractId = builder.contractId;
+ this.contractStatus = builder.contractStatus;
+ this.propertyId = builder.propertyId;
+ this.sfnWaitApprovedTaskToken = builder.sfnWaitApprovedTaskToken;
+ }
+
+ public String getContractId() {
+ return contractId;
+ }
+
+ public String getContractStatus() {
+ return contractStatus;
+ }
+
+ public String getPropertyId() {
+ return propertyId;
+ }
+
+ public String getSfnWaitApprovedTaskToken() {
+ return sfnWaitApprovedTaskToken;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private String contractId;
+ private String contractStatus;
+ private String propertyId;
+ private String sfnWaitApprovedTaskToken;
+
+ public Builder contractId(String contractId) {
+ this.contractId = contractId;
+ return this;
+ }
+
+ public Builder contractStatus(String contractStatus) {
+ this.contractStatus = contractStatus;
+ return this;
+ }
+
+ 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_properties/PropertyFunctions/src/main/java/schema/unicorn_contracts/contractstatuschanged/ContractStatusChanged.java b/unicorn_approvals/ApprovalsService/src/main/java/schema/unicorn_contracts/contractstatuschanged/ContractStatusChanged.java
similarity index 96%
rename from unicorn_properties/PropertyFunctions/src/main/java/schema/unicorn_contracts/contractstatuschanged/ContractStatusChanged.java
rename to unicorn_approvals/ApprovalsService/src/main/java/schema/unicorn_contracts/contractstatuschanged/ContractStatusChanged.java
index 9bc2295..ed24f9a 100644
--- a/unicorn_properties/PropertyFunctions/src/main/java/schema/unicorn_contracts/contractstatuschanged/ContractStatusChanged.java
+++ b/unicorn_approvals/ApprovalsService/src/main/java/schema/unicorn_contracts/contractstatuschanged/ContractStatusChanged.java
@@ -1,10 +1,9 @@
package schema.unicorn_contracts.contractstatuschanged;
-import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonValue;
+
import java.io.Serializable;
+import java.util.Objects;
public class ContractStatusChanged implements Serializable {
private static final long serialVersionUID = 1L;
diff --git a/unicorn_properties/PropertyFunctions/src/main/java/schema/unicorn_contracts/contractstatuschanged/Event.java b/unicorn_approvals/ApprovalsService/src/main/java/schema/unicorn_contracts/contractstatuschanged/Event.java
similarity index 96%
rename from unicorn_properties/PropertyFunctions/src/main/java/schema/unicorn_contracts/contractstatuschanged/Event.java
rename to unicorn_approvals/ApprovalsService/src/main/java/schema/unicorn_contracts/contractstatuschanged/Event.java
index 763098d..8a8b6fe 100644
--- a/unicorn_properties/PropertyFunctions/src/main/java/schema/unicorn_contracts/contractstatuschanged/Event.java
+++ b/unicorn_approvals/ApprovalsService/src/main/java/schema/unicorn_contracts/contractstatuschanged/Event.java
@@ -1,14 +1,12 @@
package schema.unicorn_contracts.contractstatuschanged;
-import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonValue;
+
+import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
-import schema.unicorn_contracts.contractstatuschanged.ContractStatusChanged;
-import java.io.Serializable;
+import java.util.Objects;
public class Event implements Serializable {
private static final long serialVersionUID = 1L;
diff --git a/unicorn_properties/PropertyFunctions/src/main/java/schema/unicorn_contracts/contractstatuschanged/marshaller/Marshaller.java b/unicorn_approvals/ApprovalsService/src/main/java/schema/unicorn_contracts/contractstatuschanged/marshaller/Marshaller.java
similarity index 93%
rename from unicorn_properties/PropertyFunctions/src/main/java/schema/unicorn_contracts/contractstatuschanged/marshaller/Marshaller.java
rename to unicorn_approvals/ApprovalsService/src/main/java/schema/unicorn_contracts/contractstatuschanged/marshaller/Marshaller.java
index e4ecf39..1ce5c6f 100644
--- a/unicorn_properties/PropertyFunctions/src/main/java/schema/unicorn_contracts/contractstatuschanged/marshaller/Marshaller.java
+++ b/unicorn_approvals/ApprovalsService/src/main/java/schema/unicorn_contracts/contractstatuschanged/marshaller/Marshaller.java
@@ -2,7 +2,6 @@
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.type.TypeFactory;
import java.io.IOException;
import java.io.InputStream;
diff --git a/unicorn_contracts/ContractsFunction/src/main/resources/log4j2.xml b/unicorn_approvals/ApprovalsService/src/main/resources/log4j2.xml
similarity index 100%
rename from unicorn_contracts/ContractsFunction/src/main/resources/log4j2.xml
rename to unicorn_approvals/ApprovalsService/src/main/resources/log4j2.xml
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/dbb_stream_events/contract_status_changed_draft.json b/unicorn_approvals/ApprovalsService/src/test/events/dbb_stream_events/contract_status_changed_draft.json
similarity index 100%
rename from unicorn_properties/PropertyFunctions/src/test/events/dbb_stream_events/contract_status_changed_draft.json
rename to unicorn_approvals/ApprovalsService/src/test/events/dbb_stream_events/contract_status_changed_draft.json
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/dbb_stream_events/sfn_check_exists.json b/unicorn_approvals/ApprovalsService/src/test/events/dbb_stream_events/sfn_check_exists.json
similarity index 100%
rename from unicorn_properties/PropertyFunctions/src/test/events/dbb_stream_events/sfn_check_exists.json
rename to unicorn_approvals/ApprovalsService/src/test/events/dbb_stream_events/sfn_check_exists.json
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/dbb_stream_events/sfn_wait_approval.json b/unicorn_approvals/ApprovalsService/src/test/events/dbb_stream_events/sfn_wait_approval.json
similarity index 100%
rename from unicorn_properties/PropertyFunctions/src/test/events/dbb_stream_events/sfn_wait_approval.json
rename to unicorn_approvals/ApprovalsService/src/test/events/dbb_stream_events/sfn_wait_approval.json
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/dbb_stream_events/status_approved_waiting_for_approval.json b/unicorn_approvals/ApprovalsService/src/test/events/dbb_stream_events/status_approved_waiting_for_approval.json
similarity index 100%
rename from unicorn_properties/PropertyFunctions/src/test/events/dbb_stream_events/status_approved_waiting_for_approval.json
rename to unicorn_approvals/ApprovalsService/src/test/events/dbb_stream_events/status_approved_waiting_for_approval.json
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/dbb_stream_events/status_approved_with_no_workflow.json b/unicorn_approvals/ApprovalsService/src/test/events/dbb_stream_events/status_approved_with_no_workflow.json
similarity index 100%
rename from unicorn_properties/PropertyFunctions/src/test/events/dbb_stream_events/status_approved_with_no_workflow.json
rename to unicorn_approvals/ApprovalsService/src/test/events/dbb_stream_events/status_approved_with_no_workflow.json
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/contract_status_changed_event_contract_1_approved.json b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/contract_status_changed_event_contract_1_approved.json
similarity index 85%
rename from unicorn_properties/PropertyFunctions/src/test/events/eventbridge/contract_status_changed_event_contract_1_approved.json
rename to unicorn_approvals/ApprovalsService/src/test/events/eventbridge/contract_status_changed_event_contract_1_approved.json
index e8b8f23..c7881ba 100644
--- a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/contract_status_changed_event_contract_1_approved.json
+++ b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/contract_status_changed_event_contract_1_approved.json
@@ -2,7 +2,7 @@
{
"DetailType": "ContractStatusChanged",
"Source": "unicorn.contracts",
- "EventBusName": "UnicornPropertiesBus-local",
+ "EventBusName": "UnicornApprovalsBus-local",
"Detail": "{ \"contract_last_modified_on\": 1669385541019, \"contract_id\": \"f2bedc80-3dc8-4544-9140-9b606d71a6ee\", \"property_id\": \"usa/anytown/main-street/111\", \"contract_status\": \"APPROVED\" }"
}
]
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/contract_status_changed_event_contract_1_draft.json b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/contract_status_changed_event_contract_1_draft.json
similarity index 85%
rename from unicorn_properties/PropertyFunctions/src/test/events/eventbridge/contract_status_changed_event_contract_1_draft.json
rename to unicorn_approvals/ApprovalsService/src/test/events/eventbridge/contract_status_changed_event_contract_1_draft.json
index 65a8a9d..af33ac3 100644
--- a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/contract_status_changed_event_contract_1_draft.json
+++ b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/contract_status_changed_event_contract_1_draft.json
@@ -2,7 +2,7 @@
{
"DetailType": "ContractStatusChanged",
"Source": "unicorn.contracts",
- "EventBusName": "UnicornPropertiesBus-local",
+ "EventBusName": "UnicornApprovalsBus-local",
"Detail": "{ \"contract_last_modified_on\": 1669385541019, \"contract_id\": \"f2bedc80-3dc8-4544-9140-9b606d71a6ee\", \"property_id\": \"usa/anytown/main-street/111\", \"contract_status\": \"DRAFT\" }"
}
]
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/contract_status_changed_event_contract_2_approved.json b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/contract_status_changed_event_contract_2_approved.json
similarity index 85%
rename from unicorn_properties/PropertyFunctions/src/test/events/eventbridge/contract_status_changed_event_contract_2_approved.json
rename to unicorn_approvals/ApprovalsService/src/test/events/eventbridge/contract_status_changed_event_contract_2_approved.json
index bf21376..2e8a644 100644
--- a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/contract_status_changed_event_contract_2_approved.json
+++ b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/contract_status_changed_event_contract_2_approved.json
@@ -2,7 +2,7 @@
{
"DetailType": "ContractStatusChanged",
"Source": "unicorn.contracts",
- "EventBusName": "UnicornPropertiesBus-local",
+ "EventBusName": "UnicornApprovalsBus-local",
"Detail": "{ \"contract_last_modified_on\": 1669385541019, \"contract_id\": \"9183453b-d284-4466-a2d9-f00b1d569ad7\", \"property_id\": \"usa/anytown/main-street/222\", \"contract_status\": \"APPROVED\" }"
}
]
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/contract_status_changed_event_contract_2_draft.json b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/contract_status_changed_event_contract_2_draft.json
similarity index 85%
rename from unicorn_properties/PropertyFunctions/src/test/events/eventbridge/contract_status_changed_event_contract_2_draft.json
rename to unicorn_approvals/ApprovalsService/src/test/events/eventbridge/contract_status_changed_event_contract_2_draft.json
index 52f5659..9d6d0e8 100644
--- a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/contract_status_changed_event_contract_2_draft.json
+++ b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/contract_status_changed_event_contract_2_draft.json
@@ -2,7 +2,7 @@
{
"DetailType": "ContractStatusChanged",
"Source": "unicorn.contracts",
- "EventBusName": "UnicornPropertiesBus-local",
+ "EventBusName": "UnicornApprovalsBus-local",
"Detail": "{ \"contract_last_modified_on\": 1669385541019, \"contract_id\": \"9183453b-d284-4466-a2d9-f00b1d569ad7\", \"property_id\": \"usa/anytown/main-street/222\", \"contract_status\": \"DRAFT\" }"
}
]
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event.json b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_approval_requested_event.json
similarity index 100%
rename from unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event.json
rename to unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_approval_requested_event.json
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_all_good.json b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_approval_requested_event_all_good.json
similarity index 94%
rename from unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_all_good.json
rename to unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_approval_requested_event_all_good.json
index 677eaec..4e8ff6b 100644
--- a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_all_good.json
+++ b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_approval_requested_event_all_good.json
@@ -2,7 +2,7 @@
{
"DetailType": "PublicationApprovalRequested",
"Source": "unicorn.web",
- "EventBusName": "UnicornPropertiesBus-local",
+ "EventBusName": "UnicornApprovalsBus-local",
"Detail": "{\"property_id\":\"usa/anytown/main-street/222\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":222},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"property_images/prop1_exterior1.jpg\",\"property_images/prop1_interior1.jpg\",\"property_images/prop1_interior2.jpg\",\"property_images/prop1_interior3.jpg\"]}"
}
]
\ No newline at end of file
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_inappropriate_description.json b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_approval_requested_event_inappropriate_description.json
similarity index 93%
rename from unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_inappropriate_description.json
rename to unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_approval_requested_event_inappropriate_description.json
index b69426a..1126588 100644
--- a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_inappropriate_description.json
+++ b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_approval_requested_event_inappropriate_description.json
@@ -2,7 +2,7 @@
{
"DetailType": "PublicationApprovalRequested",
"Source": "unicorn.web",
- "EventBusName": "UnicornPropertiesBus-local",
+ "EventBusName": "UnicornApprovalsBus-local",
"Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":111},\"description\":\"This is a property for goblins. The property has the worst quality and is atrocious when it comes to design. The property is not clean whatsoever, and will make any property owner have buyers' remorse as soon the property is bought. Keep away from this property as much as possible!\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"property_images/prop1_exterior1.jpg\",\"property_images/prop1_interior1.jpg\",\"property_images/prop1_interior2.jpg\",\"property_images/prop1_interior3.jpg\"]}"
}
]
\ No newline at end of file
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_inappropriate_images.json b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_approval_requested_event_inappropriate_images.json
similarity index 94%
rename from unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_inappropriate_images.json
rename to unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_approval_requested_event_inappropriate_images.json
index 964e12b..1b7f88a 100644
--- a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_inappropriate_images.json
+++ b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_approval_requested_event_inappropriate_images.json
@@ -2,7 +2,7 @@
{
"DetailType": "PublicationApprovalRequested",
"Source": "unicorn.web",
- "EventBusName": "UnicornPropertiesBus-local",
+ "EventBusName": "UnicornApprovalsBus-local",
"Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":111},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"property_images/prop1_exterior1.jpg\",\"property_images/prop1_interior1.jpg\",\"property_images/prop1_interior2.jpg\",\"property_images/prop1_interior3.jpg\",\"property_images/prop1_interior4-bad.jpg\"]}"
}
]
\ No newline at end of file
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_non_existing_contract.json b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_approval_requested_event_non_existing_contract.json
similarity index 94%
rename from unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_non_existing_contract.json
rename to unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_approval_requested_event_non_existing_contract.json
index a6ebb52..8bc45c3 100644
--- a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_non_existing_contract.json
+++ b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_approval_requested_event_non_existing_contract.json
@@ -2,7 +2,7 @@
{
"DetailType": "PublicationApprovalRequested",
"Source": "unicorn.web",
- "EventBusName": "UnicornPropertiesBus-local",
+ "EventBusName": "UnicornApprovalsBus-local",
"Detail": "{\"property_id\":\"usa/anytown/main-street/333\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":333},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"property_images/prop1_exterior1.jpg\",\"property_images/prop1_interior1.jpg\",\"property_images/prop1_interior2.jpg\",\"property_images/prop1_interior3.jpg\"]}"
}
]
\ No newline at end of file
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_pause_workflow.json b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_approval_requested_event_pause_workflow.json
similarity index 94%
rename from unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_pause_workflow.json
rename to unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_approval_requested_event_pause_workflow.json
index 1f6ecd8..5657437 100644
--- a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_approval_requested_event_pause_workflow.json
+++ b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_approval_requested_event_pause_workflow.json
@@ -2,7 +2,7 @@
{
"DetailType": "PublicationApprovalRequested",
"Source": "unicorn.web",
- "EventBusName": "UnicornPropertiesBus-local",
+ "EventBusName": "UnicornApprovalsBus-local",
"Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"address\":{\"country\":\"USA\",\"city\":\"Anytown\",\"street\":\"Main Street\",\"number\":111},\"description\":\"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\":\"sale\",\"listprice\":200,\"currency\":\"SPL\",\"images\":[\"property_images/prop1_exterior1.jpg\",\"property_images/prop1_interior1.jpg\",\"property_images/prop1_interior2.jpg\",\"property_images/prop1_interior3.jpg\"]}"
}
]
\ No newline at end of file
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_evaluation_completed_event.json b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_evaluation_completed_event.json
similarity index 92%
rename from unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_evaluation_completed_event.json
rename to unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_evaluation_completed_event.json
index 6ffe791..57b0842 100644
--- a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/publication_evaluation_completed_event.json
+++ b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/publication_evaluation_completed_event.json
@@ -2,7 +2,7 @@
"version": "0",
"id": "f849f683-76e1-1c84-669d-544a9828dfef",
"detail-type": "PublicationEvaluationCompleted",
- "source": "unicorn.properties",
+ "source": "unicorn.approvals",
"account": "123456789",
"time": "2022-08-16T06:33:05Z",
"region": "ap-southeast-2",
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/put_event_property_approval_requested.json b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/put_event_property_approval_requested.json
similarity index 94%
rename from unicorn_properties/PropertyFunctions/src/test/events/eventbridge/put_event_property_approval_requested.json
rename to unicorn_approvals/ApprovalsService/src/test/events/eventbridge/put_event_property_approval_requested.json
index fed3b1f..b4e536f 100644
--- a/unicorn_properties/PropertyFunctions/src/test/events/eventbridge/put_event_property_approval_requested.json
+++ b/unicorn_approvals/ApprovalsService/src/test/events/eventbridge/put_event_property_approval_requested.json
@@ -3,6 +3,6 @@
"Source": "unicorn.web",
"Detail": "{ \"property_id\": \"usa/anytown/main-street/111\", \"country\": \"USA\", \"city\": \"Anytown\", \"street\": \"Main Street\", \"number\": 111, \"description\": \"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\", \"contract\": \"sale\", \"listprice\": 200, \"currency\": \"SPL\", \"images\": [ \"prop1_exterior1.jpg\", \"prop1_interior1.jpg\", \"prop1_interior2.jpg\", \"prop1_interior3.jpg\", \"prop1_interior4-bad.jpg\" ] }",
"DetailType": "PublicationApprovalRequested",
- "EventBusName": "UnicornPropertiesBus-local"
+ "EventBusName": "UnicornApprovalsBus-local"
}
]
\ No newline at end of file
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/lambda/contract_status_changed.json b/unicorn_approvals/ApprovalsService/src/test/events/lambda/contract_status_changed.json
similarity index 100%
rename from unicorn_properties/PropertyFunctions/src/test/events/lambda/contract_status_changed.json
rename to unicorn_approvals/ApprovalsService/src/test/events/lambda/contract_status_changed.json
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/lambda/contract_status_checker.json b/unicorn_approvals/ApprovalsService/src/test/events/lambda/contract_status_checker.json
similarity index 100%
rename from unicorn_properties/PropertyFunctions/src/test/events/lambda/contract_status_checker.json
rename to unicorn_approvals/ApprovalsService/src/test/events/lambda/contract_status_checker.json
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/put_event_contract_status_changed.json b/unicorn_approvals/ApprovalsService/src/test/events/put_event_contract_status_changed.json
similarity index 82%
rename from unicorn_properties/PropertyFunctions/src/test/events/put_event_contract_status_changed.json
rename to unicorn_approvals/ApprovalsService/src/test/events/put_event_contract_status_changed.json
index 3d395a1..1ef4ab4 100644
--- a/unicorn_properties/PropertyFunctions/src/test/events/put_event_contract_status_changed.json
+++ b/unicorn_approvals/ApprovalsService/src/test/events/put_event_contract_status_changed.json
@@ -3,6 +3,6 @@
"Source": "unicorn.contracts",
"Detail": "{\"contract_updated_on\":\"10/08/2022 20:36:30\",\"contract_id\": \"199\",\"property_id\":\"bbb\",\"contract_status\":\"APPROVED\"}",
"DetailType": "ContractStatusChanged",
- "EventBusName": "Dev-UnicornPropertiesEventBus"
+ "EventBusName": "Dev-UnicornApprovalsEventBus"
}
]
\ No newline at end of file
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/put_event_publication_approval_requested.json b/unicorn_approvals/ApprovalsService/src/test/events/put_event_publication_approval_requested.json
similarity index 87%
rename from unicorn_properties/PropertyFunctions/src/test/events/put_event_publication_approval_requested.json
rename to unicorn_approvals/ApprovalsService/src/test/events/put_event_publication_approval_requested.json
index 3791334..596d093 100644
--- a/unicorn_properties/PropertyFunctions/src/test/events/put_event_publication_approval_requested.json
+++ b/unicorn_approvals/ApprovalsService/src/test/events/put_event_publication_approval_requested.json
@@ -1,7 +1,7 @@
[
{
- "EventBusName": "Dev-UnicornPropertiesEventBus",
- "Source": "unicorn.properties.web",
+ "EventBusName": "Dev-UnicornApprovalsEventBus",
+ "Source": "unicorn.web",
"DetailType": "PublicationApprovalRequested",
"Detail": "{\"property_id\": \"usa/anytown/main-street/123\",\"country\": \"USA\",\"city\": \"Anytown\",\"street\": \"Main Street\",\"number\": 123,\"description\": \"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\": \"sale\",\"listprice\": 200,\"currency\": \"SPL\",\"images\": [ \"usa/anytown/main-street-123-0d61b4e3\"]}"
}
diff --git a/unicorn_properties/PropertyFunctions/src/test/events/send_events_cli.json b/unicorn_approvals/ApprovalsService/src/test/events/send_events_cli.json
similarity index 89%
rename from unicorn_properties/PropertyFunctions/src/test/events/send_events_cli.json
rename to unicorn_approvals/ApprovalsService/src/test/events/send_events_cli.json
index abf6541..6770ed2 100644
--- a/unicorn_properties/PropertyFunctions/src/test/events/send_events_cli.json
+++ b/unicorn_approvals/ApprovalsService/src/test/events/send_events_cli.json
@@ -1,7 +1,7 @@
[
{
- "EventBusName": "Dev-UnicornPropertiesEventBus",
- "Source": "Unicorn.Web",
+ "EventBusName": "Dev-UnicornApprovalsEventBus",
+ "Source": "unicorn.web",
"DetailType": "PublicationApprovalRequested",
"Detail": "{\"property_id\": \"usa/anytown/main-street/123\",\"country\": \"USA\",\"city\": \"Anytown\",\"street\": \"Main Street\",\"number\": 123,\"description\": \"This classic Anytown estate comes with a covetable lake view. The romantic and comfortable backyard is the perfect setting for unicorn get-togethers. The open concept Main Stable is fully equipped with all the desired amenities. Second floor features 6 straw bales including large Rainbow Suite with private training pool terrace and Jr Sparkles Suite.\",\"contract\": \"sale\",\"listprice\": 200,\"currency\": \"SPL\",\"images\": [ \"usa/anytown/main-street-123-0d61b4e3\"]}"
}
diff --git a/unicorn_approvals/ApprovalsService/src/test/java/approvals/ContractStatusTests.java b/unicorn_approvals/ApprovalsService/src/test/java/approvals/ContractStatusTests.java
new file mode 100644
index 0000000..c8e01da
--- /dev/null
+++ b/unicorn_approvals/ApprovalsService/src/test/java/approvals/ContractStatusTests.java
@@ -0,0 +1,51 @@
+package approvals;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import org.junit.Before;
+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 java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static org.junit.Assert.assertTrue;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ContractStatusTests {
+
+ @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_properties/PropertyFunctions/src/main/resources/log4j2.xml b/unicorn_approvals/ApprovalsService/target/classes/log4j2.xml
similarity index 100%
rename from unicorn_properties/PropertyFunctions/src/main/resources/log4j2.xml
rename to unicorn_approvals/ApprovalsService/target/classes/log4j2.xml
diff --git a/unicorn_approvals/README.md b/unicorn_approvals/README.md
new file mode 100644
index 0000000..78cbc75
--- /dev/null
+++ b/unicorn_approvals/README.md
@@ -0,0 +1,15 @@
+# Developing Unicorn Approvals
+
+
+
+## Architecture overview
+
+**Unicorn Approvals** uses an AWS Step Functions state machine to approve property listings for Unicorn Web. The workflow checks for contract information, description sentiment and safe images, and verifies the contract is approved before approving the listing. It publishes the result via the `PublicationEvaluationCompleted` event.
+
+A Unicorn Properties agent initiates the workflow by requesting to approve a listing, generating a `PublicationApprovalRequested` event with property information. To decouple from the Contracts Service, the Approvals service maintains a local copy of contract status by consuming the ContractStatusChanged event.
+
+The workflow checks the contract state. If the contract is in the WaitForContractApproval state, it updates the contract status for the property with its task token, triggering a DynamoDB stream event. The Property Approval Sync function handles these events and passes the task token back to the state machine based on the contract state.
+
+If the workflow completes successfully, it emits a PublicationEvaluationCompleted event with an **approved** or **declined** evaluation result, which Unicorn Web listens to update its publication flag.
+
+**Note:** Upon deleting the CloudFormation stack for this service, check if the `ApprovalStateMachine` StepFunction doesn't have any executions in `RUNNING` state. If there are, cancel those execution prior to deleting the CloudFormation stack.
\ No newline at end of file
diff --git a/unicorn_properties/integration/PublicationEvaluationCompleted.json b/unicorn_approvals/integration/PublicationEvaluationCompleted.json
similarity index 100%
rename from unicorn_properties/integration/PublicationEvaluationCompleted.json
rename to unicorn_approvals/integration/PublicationEvaluationCompleted.json
diff --git a/unicorn_properties/integration/event-schemas.yaml b/unicorn_approvals/integration/event-schemas.yaml
similarity index 95%
rename from unicorn_properties/integration/event-schemas.yaml
rename to unicorn_approvals/integration/event-schemas.yaml
index 9a9f318..1817b39 100644
--- a/unicorn_properties/integration/event-schemas.yaml
+++ b/unicorn_approvals/integration/event-schemas.yaml
@@ -18,7 +18,7 @@ Resources:
Properties:
Description: 'Event schemas for Unicorn Properties'
RegistryName:
- Fn::Sub: "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}-${Stage}"
+ Fn::Sub: "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}-${Stage}"
EventRegistryPolicy:
Type: AWS::EventSchemas::RegistryPolicy
@@ -52,7 +52,7 @@ Resources:
RegistryName:
Fn::GetAtt: EventRegistry.RegistryName
SchemaName:
- Fn::Sub: '{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}@PublicationEvaluationCompleted'
+ Fn::Sub: '{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}@PublicationEvaluationCompleted'
Description: 'The schema for when a property evaluation is completed'
Content:
Fn::Sub: |
diff --git a/unicorn_properties/integration/subscriber-policies.yaml b/unicorn_approvals/integration/subscriber-policies.yaml
similarity index 89%
rename from unicorn_properties/integration/subscriber-policies.yaml
rename to unicorn_approvals/integration/subscriber-policies.yaml
index 6dfd3b2..87b9770 100644
--- a/unicorn_properties/integration/subscriber-policies.yaml
+++ b/unicorn_approvals/integration/subscriber-policies.yaml
@@ -20,7 +20,7 @@ Resources:
Type: AWS::Events::EventBusPolicy
Properties:
EventBusName:
- Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBus}}"
+ Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornApprovalsEventBus}}"
StatementId:
Fn::Sub: "OnlyRulesForPropertiesServiceEvents-${Stage}"
Statement:
@@ -40,12 +40,12 @@ Resources:
- Fn::Sub:
- arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:rule/${eventBusName}/*
- eventBusName:
- Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBus}}"
+ Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornApprovalsEventBus}}"
Condition:
StringEqualsIfExists:
"events:creatorAccount": "${aws:PrincipalAccount}"
StringEquals:
"events:source":
- - "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}"
+ - "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}"
"Null":
"events:source": "false"
diff --git a/unicorn_properties/integration/subscriptions.yaml b/unicorn_approvals/integration/subscriptions.yaml
similarity index 88%
rename from unicorn_properties/integration/subscriptions.yaml
rename to unicorn_approvals/integration/subscriptions.yaml
index 100ecf8..0c3be95 100644
--- a/unicorn_properties/integration/subscriptions.yaml
+++ b/unicorn_approvals/integration/subscriptions.yaml
@@ -17,7 +17,7 @@ Resources:
ContractStatusChangedSubscriptionRule:
Type: AWS::Events::Rule
Properties:
- Name: unicorn.properties-ContractStatusChanged
+ Name: unicorn.approvals-ContractStatusChanged
Description: Contract Status Changed subscription
EventBusName:
Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornContractsEventBusArn}}"
@@ -30,7 +30,7 @@ Resources:
Targets:
- Id: SendEventTo
Arn:
- Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}"
+ Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornApprovalsEventBusArn}}"
RoleArn:
Fn::GetAtt: [ UnicornPropertiesSubscriptionRole, Arn ]
@@ -38,7 +38,7 @@ Resources:
PublicationApprovalRequestedSubscriptionRule:
Type: AWS::Events::Rule
Properties:
- Name: unicorn.properties-PublicationApprovalRequested
+ Name: unicorn.approvals-PublicationApprovalRequested
Description: Publication evaluation completed subscription
EventBusName:
Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}"
@@ -51,13 +51,13 @@ Resources:
Targets:
- Id: SendEventTo
Arn:
- Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}"
+ Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornApprovalsEventBusArn}}"
RoleArn:
Fn::GetAtt: [ UnicornPropertiesSubscriptionRole, Arn ]
# This IAM role allows EventBridge to assume the permissions necessary to send events
- # from the publishing event bus, to the subscribing event bus (UnicornPropertiesEventBusArn)
+ # from the publishing event bus, to the subscribing event bus (UnicornApprovalsEventBusArn)
UnicornPropertiesSubscriptionRole:
Type: AWS::IAM::Role
Properties:
@@ -75,7 +75,7 @@ Resources:
- Effect: Allow
Action: events:PutEvents
Resource:
- Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}"
+ Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornApprovalsEventBusArn}}"
Outputs:
ContractStatusChangedSubscription:
diff --git a/unicorn_properties/samconfig.toml b/unicorn_approvals/samconfig.toml
similarity index 87%
rename from unicorn_properties/samconfig.toml
rename to unicorn_approvals/samconfig.toml
index 80abe0b..e8b9e83 100644
--- a/unicorn_properties/samconfig.toml
+++ b/unicorn_approvals/samconfig.toml
@@ -1,8 +1,8 @@
version = 0.1
[default.global.parameters]
-stack_name = "uni-prop-local-properties"
-s3_prefix = "uni-prop-local-properties"
+stack_name = "uni-prop-local-approvals"
+s3_prefix = "uni-prop-local-approvals"
resolve_s3 = true
resolve_image_repositories = true
diff --git a/unicorn_properties/state_machine/property_approval.asl.yaml b/unicorn_approvals/state_machine/property_approval.asl.yaml
similarity index 100%
rename from unicorn_properties/state_machine/property_approval.asl.yaml
rename to unicorn_approvals/state_machine/property_approval.asl.yaml
diff --git a/unicorn_properties/template.yaml b/unicorn_approvals/template.yaml
similarity index 80%
rename from unicorn_properties/template.yaml
rename to unicorn_approvals/template.yaml
index be633bc..a89caa8 100644
--- a/unicorn_properties/template.yaml
+++ b/unicorn_approvals/template.yaml
@@ -4,7 +4,7 @@ AWSTemplateFormatVersion: "2010-09-09"
Transform:
- AWS::Serverless-2016-10-31
Description: >
- Unicorn Properties Service. Validate the content, images and contract of property listings.
+ Unicorn Approvals Service. Validate the content, images and contract of property listings.
Metadata:
cfn-lint:
@@ -16,7 +16,6 @@ Metadata:
- WS2001 # Rule disabled because check does not support !ToJsonString transform
- ES1001 # Rule disabled because our Lambda functions don't need DestinationConfig.OnFailure
- W3002 # Rule disabled as nested templates are being packaged
- - E3030 # Rule disabled due to using cfn-lint-serverless rules v0.3
Parameters:
Stage:
@@ -53,48 +52,48 @@ Globals:
Environment:
Variables:
CONTRACT_STATUS_TABLE: !Ref ContractStatusTable
- EVENT_BUS: !Ref UnicornPropertiesEventBus
- SERVICE_NAMESPACE: "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}"
- POWERTOOLS_SERVICE_NAME: "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}"
+ EVENT_BUS: !Ref UnicornApprovalsEventBus
+ SERVICE_NAMESPACE: "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}"
+ POWERTOOLS_SERVICE_NAME: "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}"
POWERTOOLS_TRACE_DISABLED: "false" # Explicitly disables tracing, default
POWERTOOLS_LOGGER_LOG_EVENT: !If [IsProd, "false", "true"] # Logs incoming event, default
POWERTOOLS_LOGGER_SAMPLE_RATE: !If [IsProd, "0.1", "0"] # Debug log sampling percentage, default
- POWERTOOLS_METRICS_NAMESPACE: "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}"
+ POWERTOOLS_METRICS_NAMESPACE: "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}"
POWERTOOLS_LOG_LEVEL: INFO # Log level for Logger (INFO, DEBUG, etc.), default
LOG_LEVEL: INFO # Log level for Logger
Tags:
stage: !Ref Stage
project: !FindInMap [Constants, ProjectName, Value]
- namespace: "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}"
+ namespace: "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}"
Resources:
#### SSM PARAMETERS
# Services share their event bus name and arn
- UnicornPropertiesEventBusNameParam:
+ UnicornApprovalsEventBusNameParam:
Type: AWS::SSM::Parameter
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
Type: String
- Name: !Sub /uni-prop/${Stage}/UnicornPropertiesEventBus
- Value: !GetAtt UnicornPropertiesEventBus.Name
+ Name: !Sub /uni-prop/${Stage}/UnicornApprovalsEventBus
+ Value: !GetAtt UnicornApprovalsEventBus.Name
- UnicornPropertiesEventBusArnParam:
+ UnicornApprovalsEventBusArnParam:
Type: AWS::SSM::Parameter
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
Type: String
- Name: !Sub /uni-prop/${Stage}/UnicornPropertiesEventBusArn
- Value: !GetAtt UnicornPropertiesEventBus.Arn
+ Name: !Sub /uni-prop/${Stage}/UnicornApprovalsEventBusArn
+ Value: !GetAtt UnicornApprovalsEventBus.Arn
#### LAMBDA FUNCTIONS
# Listens to ContractStatusChanged events from EventBridge
ContractStatusChangedHandlerFunction:
Type: AWS::Serverless::Function
Properties:
- CodeUri: ./PropertyFunctions
- Handler: properties.ContractStatusChangedHandlerFunction::handleRequest
+ CodeUri: ApprovalsService
+ Handler: approvals.ContractStatusChangedHandlerFunction::handleRequest
Policies:
- DynamoDBWritePolicy:
TableName: !Ref ContractStatusTable
@@ -104,8 +103,8 @@ Resources:
StatusChangedEvent:
Type: EventBridgeRule
Properties:
- RuleName: unicorn.properties-ContractStatusChanged
- EventBusName: !GetAtt UnicornPropertiesEventBus.Name
+ RuleName: unicorn.approvals-ContractStatusChanged
+ EventBusName: !GetAtt UnicornApprovalsEventBus.Name
Pattern:
source:
- "{{resolve:ssm:/uni-prop/UnicornContractsNamespace}}"
@@ -115,12 +114,12 @@ Resources:
MaximumRetryAttempts: 5
MaximumEventAgeInSeconds: 900
DeadLetterConfig:
- Arn: !GetAtt PropertiesEventBusRuleDLQ.Arn
+ Arn: !GetAtt ApprovalsEventBusRuleDLQ.Arn
EventInvokeConfig:
DestinationConfig:
OnFailure:
Type: SQS
- Destination: !GetAtt PropertiesServiceDLQ.Arn
+ Destination: !GetAtt ApprovalsServiceDLQ.Arn
# Log group for the ContractStatusChangedHandlerFunction
ContractStatusChangedHandlerFunctionLogGroup:
@@ -135,8 +134,8 @@ Resources:
PropertiesApprovalSyncFunction:
Type: AWS::Serverless::Function
Properties:
- CodeUri: ./PropertyFunctions
- Handler: properties.PropertiesApprovalSyncFunction::handleRequest
+ CodeUri: ApprovalsService
+ Handler: approvals.PropertiesApprovalSyncFunction::handleRequest
Policies:
- DynamoDBReadPolicy:
TableName: !Ref ContractStatusTable
@@ -145,7 +144,7 @@ Resources:
StreamName:
!Select [3, !Split ["/", !GetAtt ContractStatusTable.StreamArn]]
- SQSSendMessagePolicy:
- QueueName: !GetAtt PropertiesServiceDLQ.QueueName
+ QueueName: !GetAtt ApprovalsServiceDLQ.QueueName
- Statement:
- Effect: Allow
Action:
@@ -162,12 +161,12 @@ Resources:
MaximumRetryAttempts: 3
DestinationConfig:
OnFailure:
- Destination: !GetAtt PropertiesServiceDLQ.Arn
+ Destination: !GetAtt ApprovalsServiceDLQ.Arn
EventInvokeConfig:
DestinationConfig:
OnFailure:
Type: SQS
- Destination: !GetAtt PropertiesServiceDLQ.Arn
+ Destination: !GetAtt ApprovalsServiceDLQ.Arn
# Log group for the PropertiesApprovalSyncFunction
PropertiesApprovalSyncFunctionLogGroup:
@@ -182,8 +181,8 @@ Resources:
WaitForContractApprovalFunction:
Type: AWS::Serverless::Function
Properties:
- CodeUri: PropertyFunctions
- Handler: properties.WaitForContractApprovalFunction::handleRequest
+ CodeUri: ApprovalsService
+ Handler: approvals.WaitForContractApprovalFunction::handleRequest
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ContractStatusTable
@@ -218,7 +217,7 @@ Resources:
- S3ReadPolicy:
BucketName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/ImagesBucket}}"
- EventBridgePutEventsPolicy:
- EventBusName: !GetAtt UnicornPropertiesEventBus.Name
+ EventBusName: !GetAtt UnicornApprovalsEventBus.Name
- Statement:
- Effect: Allow
Action:
@@ -242,8 +241,8 @@ Resources:
PublicationApprovalRequestedEvent:
Type: EventBridgeRule
Properties:
- RuleName: unicorn.properties-PublicationApprovalRequested
- EventBusName: !GetAtt UnicornPropertiesEventBus.Name
+ RuleName: unicorn.approvals-PublicationApprovalRequested
+ EventBusName: !GetAtt UnicornApprovalsEventBus.Name
Pattern:
source:
- "{{resolve:ssm:/uni-prop/UnicornWebNamespace}}"
@@ -254,13 +253,13 @@ Resources:
MaximumEventAgeInSeconds: 900
DeadLetterConfig:
Type: SQS
- Destination: !GetAtt PropertiesServiceDLQ.Arn
+ Destination: !GetAtt ApprovalsServiceDLQ.Arn
DefinitionSubstitutions:
WaitForContractApprovalArn: !GetAtt WaitForContractApprovalFunction.Arn
TableName: !Ref ContractStatusTable
ImageUploadBucketName: !Sub "{{resolve:ssm:/uni-prop/${Stage}/ImagesBucket}}"
- EventBusName: !GetAtt UnicornPropertiesEventBus.Name
- ServiceName: "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}"
+ EventBusName: !GetAtt UnicornApprovalsEventBus.Name
+ ServiceName: "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}"
# Store ApprovalStateMachineLogGroup workflow execution logs
ApprovalStateMachineLogGroup:
@@ -273,7 +272,7 @@ Resources:
#### DEAD LETTER QUEUES
# Store EventBridge events that failed to be DELIVERED to ContractStatusChangedHandlerFunction
- PropertiesEventBusRuleDLQ:
+ ApprovalsEventBusRuleDLQ:
Type: AWS::SQS::Queue
UpdateReplacePolicy: Delete
DeletionPolicy: Delete
@@ -284,12 +283,12 @@ Resources:
- Key: project
Value: !FindInMap [Constants, ProjectName, Value]
- Key: namespace
- Value: "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}"
+ Value: "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}"
- Key: stage
Value: !Ref Stage
- # Store failed INVOCATIONS to each Lambda function in Unicorn Properties Service
- PropertiesServiceDLQ:
+ # Store failed INVOCATIONS to each Lambda function in Unicorn Approvals Service
+ ApprovalsServiceDLQ:
Type: AWS::SQS::Queue
UpdateReplacePolicy: Delete
DeletionPolicy: Delete
@@ -300,7 +299,7 @@ Resources:
- Key: project
Value: !FindInMap [Constants, ProjectName, Value]
- Key: namespace
- Value: "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}"
+ Value: "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}"
- Key: stage
Value: !Ref Stage
@@ -323,62 +322,62 @@ Resources:
- Key: project
Value: !FindInMap [Constants, ProjectName, Value]
- Key: namespace
- Value: "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}"
+ Value: "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}"
- Key: stage
Value: !Ref Stage
#### EVENT BUS
- # Event bus for Unicorn Properties Service, used to publish and consume events
- UnicornPropertiesEventBus:
+ # Event bus for Unicorn Approvals Service, used to publish and consume events
+ UnicornApprovalsEventBus:
Type: AWS::Events::EventBus
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
- Name: !Sub UnicornPropertiesBus-${Stage}
+ Name: !Sub UnicornApprovalsBus-${Stage}
- # Event bus policy to restrict who can publish events (should only be services from UnicornPropertiesNamespace)
- UnicornPropertiesEventsBusPublishPolicy:
+ # Event bus policy to restrict who can publish events (should only be services from UnicornApprovalsNamespace)
+ UnicornApprovalsEventsBusPublishPolicy:
Type: AWS::Events::EventBusPolicy
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
- EventBusName: !Ref UnicornPropertiesEventBus
- StatementId: !Sub OnlyPropertiesServiceCanPublishToEventBus-${Stage}
+ EventBusName: !Ref UnicornApprovalsEventBus
+ StatementId: !Sub OnlyApprovalsServiceCanPublishToEventBus-${Stage}
Statement:
Effect: Allow
Principal:
AWS:
- !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
Action: events:PutEvents
- Resource: !GetAtt UnicornPropertiesEventBus.Arn
+ Resource: !GetAtt UnicornApprovalsEventBus.Arn
Condition:
StringEquals:
events:source:
- - "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}"
+ - "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}"
# Catchall rule used for development purposes. Logs all events matching any of the services to CloudWatch Logs
- UnicornPropertiesCatchAllRule:
+ UnicornApprovalsCatchAllRule:
Type: AWS::Events::Rule
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
- Name: properties.catchall
+ Name: approvals.catchall
Description: Catchall rule used for development purposes.
- EventBusName: !Ref UnicornPropertiesEventBus
+ EventBusName: !Ref UnicornApprovalsEventBus
EventPattern:
account:
- !Ref AWS::AccountId
source:
- "{{resolve:ssm:/uni-prop/UnicornContractsNamespace}}"
- - "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}"
+ - "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}"
- "{{resolve:ssm:/uni-prop/UnicornWebNamespace}}"
State: ENABLED #You may want to disable this rule in production
Targets:
- - Arn: !GetAtt UnicornPropertiesCatchAllLogGroup.Arn
- Id: !Sub UnicornPropertiesCatchAllLogGroupTarget-${Stage}
+ - Arn: !GetAtt UnicornApprovalsCatchAllLogGroup.Arn
+ Id: !Sub UnicornApprovalsCatchAllLogGroupTarget-${Stage}
# CloudWatch log group used to catch all events
- UnicornPropertiesCatchAllLogGroup:
+ UnicornApprovalsCatchAllLogGroup:
Type: AWS::Logs::LogGroup
UpdateReplacePolicy: Delete
DeletionPolicy: Delete
@@ -386,7 +385,7 @@ Resources:
LogGroupName: !Sub
- "/aws/events/${Stage}/${NS}-catchall"
- Stage: !Ref Stage
- NS: "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}"
+ NS: "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}"
RetentionInDays: !FindInMap [LogsRetentionPeriodMap, !Ref Stage, Days]
# Permissions to allow EventBridge to send logs to CloudWatch
@@ -412,14 +411,14 @@ Resources:
"logs:PutLogEvents"
],
"Resource": [
- "${UnicornPropertiesCatchAllLogGroup.Arn}"
+ "${UnicornApprovalsCatchAllLogGroup.Arn}"
]
}
]
}
#### CLOUDFORMATION NESTED STACKS
- # CloudFormation Stack with the Properties Service Event Registry and Schemas
+ # CloudFormation Stack with the Approvals Service Event Registry and Schemas
EventSchemasStack:
Type: AWS::Serverless::Application
UpdateReplacePolicy: Delete
@@ -429,25 +428,25 @@ Resources:
Parameters:
Stage: !Ref Stage
- # CloudFormation Stack with the Cross-service EventBus policy for Properties Service
+ # CloudFormation Stack with the Cross-service EventBus policy for Approvals Service
SubscriberPoliciesStack:
Type: AWS::Serverless::Application
UpdateReplacePolicy: Delete
DeletionPolicy: Delete
DependsOn:
- - UnicornPropertiesEventBusNameParam
+ - UnicornApprovalsEventBusNameParam
Properties:
Location: "integration/subscriber-policies.yaml"
Parameters:
Stage: !Ref Stage
- # CloudFormation Stack with the Cross-service EventBus Rules for Properties Service
+ # CloudFormation Stack with the Cross-service EventBus Rules for Approvals Service
SubscriptionsStack:
Type: AWS::Serverless::Application
UpdateReplacePolicy: Delete
DeletionPolicy: Delete
DependsOn:
- - UnicornPropertiesEventBusArnParam
+ - UnicornApprovalsEventBusArnParam
Properties:
Location: "integration/subscriptions.yaml"
Parameters:
@@ -482,13 +481,13 @@ Outputs:
Value: !Ref ApprovalStateMachine
#### EVENT BRIDGE OUTPUTS
- UnicornPropertiesEventBusName:
- Value: !GetAtt UnicornPropertiesEventBus.Name
+ UnicornApprovalsEventBusName:
+ Value: !GetAtt UnicornApprovalsEventBus.Name
#### CLOUDWATCH LOGS OUTPUTS
- UnicornPropertiesCatchAllLogGroupArn:
+ UnicornApprovalsCatchAllLogGroupArn:
Description: Log all events on the service's EventBridge Bus
- Value: !GetAtt UnicornPropertiesCatchAllLogGroup.Arn
+ Value: !GetAtt UnicornApprovalsCatchAllLogGroup.Arn
ApprovalStateMachineLogGroupName:
Value: !Ref ApprovalStateMachineLogGroup
diff --git a/unicorn_contracts/ContractsFunction/src/main/java/contracts/ContractEventHandler.java b/unicorn_contracts/ContractsFunction/src/main/java/contracts/ContractEventHandler.java
deleted file mode 100644
index cb2fc81..0000000
--- a/unicorn_contracts/ContractsFunction/src/main/java/contracts/ContractEventHandler.java
+++ /dev/null
@@ -1,158 +0,0 @@
-package contracts;
-
-import com.amazonaws.services.lambda.runtime.Context;
-import com.amazonaws.services.lambda.runtime.RequestHandler;
-
-import com.amazonaws.services.lambda.runtime.events.SQSEvent;
-import com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import contracts.utils.Contract;
-
-import contracts.utils.ContractStatusEnum;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
-import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
-import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
-import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
-import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException;
-import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
-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.util.Map;
-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();
-
- DynamoDbClient dynamodbClient = DynamoDbClient.builder()
- .build();
-
- Logger logger = LogManager.getLogger();
- MetricsLogger metricsLogger = MetricsUtils.metricsLogger();
-
- @Override
- public Void handleRequest(SQSEvent event, Context context) {
-
- 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();
- }
-
- }
-
- }
- return null;
- }
-
- @Tracing
- private void createContract(String strContract) throws JsonProcessingException {
- 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)")
- .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);
- }
- }
-
- @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());
-
- 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)")
- .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");
- }
- }
-
- public void setDynamodbClient(DynamoDbClient dynamodbClient) {
- this.dynamodbClient = dynamodbClient;
- }
-
-}
diff --git a/unicorn_contracts/ContractsFunction/src/main/java/contracts/utils/Address.java b/unicorn_contracts/ContractsFunction/src/main/java/contracts/utils/Address.java
deleted file mode 100644
index ec1be8d..0000000
--- a/unicorn_contracts/ContractsFunction/src/main/java/contracts/utils/Address.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package contracts.utils;
-
-public class Address {
-
- String country;
- String city;
- String street;
- int number;
-
- public String getCountry() {
- return this.country;
- }
-
- public void setCountry(String country) {
- this.country = country;
- }
-
- public String getCity() {
- return this.city;
- }
-
- public void setCity(String city) {
- this.city = city;
- }
-
- public String getStreet() {
- return this.street;
- }
-
- public void setStreet(String street) {
- this.street = street;
- }
-
- public int getNumber() {
- return this.number;
- }
-
- public void setNumber(int number) {
- this.number = number;
- }
-
-}
diff --git a/unicorn_contracts/ContractsFunction/src/main/java/contracts/utils/Contract.java b/unicorn_contracts/ContractsFunction/src/main/java/contracts/utils/Contract.java
deleted file mode 100644
index 067842f..0000000
--- a/unicorn_contracts/ContractsFunction/src/main/java/contracts/utils/Contract.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package contracts.utils;
-
-import com.fasterxml.jackson.annotation.JsonAlias;
-
-public class Contract {
-
- Address address;
- @JsonAlias("property_id")
- String propertyId;
- @JsonAlias("contract_id")
- String contractId;
- @JsonAlias("seller_name")
- String sellerName;
- @JsonAlias("contract_status")
- ContractStatusEnum contractStatus;
- @JsonAlias("contract_created")
- Long contractCreated;
- @JsonAlias("contract_last_modified_on")
- Long contractLastModifiedOn;
-
- public Address getAddress() {
- return this.address;
- }
-
- public void setAddress(Address address) {
- this.address = address;
- }
-
- public String getPropertyId() {
- return this.propertyId;
- }
-
- public void setPropertyId(String propertyId) {
- this.propertyId = propertyId;
- }
-
- public String getContractId() {
- return this.contractId;
- }
-
- public void setContractId(String contractId) {
- this.contractId = contractId;
- }
-
- public String getSellerName() {
- return this.sellerName;
- }
-
- public void setSellerName(String sellerName) {
- this.sellerName = sellerName;
- }
-
- public ContractStatusEnum getContractStatus() {
- return this.contractStatus;
- }
-
- public void setContractStatus(ContractStatusEnum contractStatus) {
- this.contractStatus = contractStatus;
- }
-
- public Long getContractCreated() {
- return this.contractCreated;
- }
-
- public void setContractCreated(Long contractCreated) {
- this.contractCreated = contractCreated;
- }
-
- public Long getContractLastModifiedOn() {
- return this.contractLastModifiedOn;
- }
-
- public void setContractLastModifiedOn(Long contractLastModifiedOn) {
- this.contractLastModifiedOn = contractLastModifiedOn;
- }
-
-}
diff --git a/unicorn_contracts/ContractsFunction/src/main/java/contracts/utils/ContractStatusChangedEvent.java b/unicorn_contracts/ContractsFunction/src/main/java/contracts/utils/ContractStatusChangedEvent.java
deleted file mode 100644
index 362d0bd..0000000
--- a/unicorn_contracts/ContractsFunction/src/main/java/contracts/utils/ContractStatusChangedEvent.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package contracts.utils;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-public class ContractStatusChangedEvent {
- @JsonProperty("contract_last_modified_on")
- Long contractLastModifiedOn;
- @JsonProperty("contract_id")
- String contractId;
- @JsonProperty("property_id")
- String propertyId;
- @JsonProperty("contract_status")
- ContractStatusEnum contractStatus;
-
- public Long getContractLastModifiedOn() {
- return contractLastModifiedOn;
- }
-
- public void setContractLastModifiedOn(Long contractLastModifiedOn) {
- this.contractLastModifiedOn = contractLastModifiedOn;
- }
-
- public String getContractId() {
- return contractId;
- }
-
- public void setContractId(String contractId) {
- this.contractId = contractId;
- }
-
- public String getPropertyId() {
- return propertyId;
- }
-
- public void setPropertyId(String propertyId) {
- this.propertyId = propertyId;
- }
-
- public ContractStatusEnum getContractStatus() {
- return contractStatus;
- }
-
- public void setContractStatus(ContractStatusEnum contractStatus) {
- this.contractStatus = contractStatus;
- }
-
-}
diff --git a/unicorn_contracts/ContractsFunction/src/main/java/contracts/utils/ResponseParser.java b/unicorn_contracts/ContractsFunction/src/main/java/contracts/utils/ResponseParser.java
deleted file mode 100644
index 54eda4c..0000000
--- a/unicorn_contracts/ContractsFunction/src/main/java/contracts/utils/ResponseParser.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package contracts.utils;
-
-import java.util.Map;
-
-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;
-
-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;
-
- }
-}
diff --git a/unicorn_contracts/ContractsFunction/src/test/java/contracts/CreateContractTests.java b/unicorn_contracts/ContractsFunction/src/test/java/contracts/CreateContractTests.java
deleted file mode 100644
index 9d579f7..0000000
--- a/unicorn_contracts/ContractsFunction/src/test/java/contracts/CreateContractTests.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package contracts;
-
-import static org.mockito.Mockito.mock;
-
-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 software.amazon.awssdk.services.dynamodb.DynamoDbClient;
-
-import org.junit.Before;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.runner.RunWith;
-import org.mockito.junit.MockitoJUnitRunner;
-
-@RunWith(MockitoJUnitRunner.class)
-public class CreateContractTests {
-
- Context context;
-
- ContractEventHandler handler;
-
- DynamoDbClient client;
-
- @Before
- public void setUp() throws Exception {
-
- client = mock(DynamoDbClient.class);
- context = mock(Context.class);
-
- }
-
- @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);
- }
-
-}
diff --git a/unicorn_contracts/ContractsFunction/pom.xml b/unicorn_contracts/ContractsService/pom.xml
similarity index 80%
rename from unicorn_contracts/ContractsFunction/pom.xml
rename to unicorn_contracts/ContractsService/pom.xml
index 2d697b9..ebd87fd 100644
--- a/unicorn_contracts/ContractsFunction/pom.xml
+++ b/unicorn_contracts/ContractsService/pom.xml
@@ -2,19 +2,19 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
4.0.0
contracts
- ContractsModule
+ ContractsService
1.0
jar
- Module for Contract service
+ Unicorn Contracts Service module
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
new file mode 100644
index 0000000..c1f2918
--- /dev/null
+++ b/unicorn_contracts/ContractsService/src/main/java/contracts/ContractEventHandler.java
@@ -0,0 +1,197 @@
+package contracts;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import contracts.utils.Contract;
+import contracts.utils.ContractStatusEnum;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
+import software.amazon.awssdk.services.dynamodb.model.*;
+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 {
+
+ 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";
+
+ private final DynamoDbClient dynamodbClient;
+
+ 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()) {
+ 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);
+ }
+ }
+
+ 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 contractJson) throws JsonProcessingException {
+ Contract contract = OBJECT_MAPPER.readValue(contractJson, Contract.class);
+ validateContract(contract);
+
+ String contractId = UUID.randomUUID().toString();
+ 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(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 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(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 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);
+ }
+ }
+
+ 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
new file mode 100644
index 0000000..d44a398
--- /dev/null
+++ b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Address.java
@@ -0,0 +1,87 @@
+package contracts.utils;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Objects;
+
+public class Address {
+
+ @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 country;
+ }
+
+ public void setCountry(String country) {
+ this.country = country;
+ }
+
+ public String getCity() {
+ return city;
+ }
+
+ public void setCity(String city) {
+ this.city = city;
+ }
+
+ public String getStreet() {
+ return street;
+ }
+
+ public void setStreet(String street) {
+ this.street = street;
+ }
+
+ public int getNumber() {
+ 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
new file mode 100644
index 0000000..14069c3
--- /dev/null
+++ b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/Contract.java
@@ -0,0 +1,118 @@
+package contracts.utils;
+
+import com.fasterxml.jackson.annotation.JsonAlias;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Objects;
+
+public class Contract {
+
+ @JsonProperty("address")
+ private Address address;
+
+ @JsonProperty("property_id")
+ @JsonAlias("property_id")
+ private String propertyId;
+
+ @JsonProperty("contract_id")
+ @JsonAlias("contract_id")
+ private String contractId;
+
+ @JsonProperty("seller_name")
+ @JsonAlias("seller_name")
+ private String sellerName;
+
+ @JsonProperty("contract_status")
+ @JsonAlias("contract_status")
+ private ContractStatusEnum contractStatus;
+
+ @JsonProperty("contract_created")
+ @JsonAlias("contract_created")
+ private Long contractCreated;
+
+ @JsonProperty("contract_last_modified_on")
+ @JsonAlias("contract_last_modified_on")
+ private Long contractLastModifiedOn;
+
+ public Contract() {}
+
+ public Address getAddress() {
+ return address;
+ }
+
+ public void setAddress(Address address) {
+ this.address = address;
+ }
+
+ public String getPropertyId() {
+ return propertyId;
+ }
+
+ public void setPropertyId(String propertyId) {
+ this.propertyId = propertyId;
+ }
+
+ public String getContractId() {
+ return contractId;
+ }
+
+ public void setContractId(String contractId) {
+ this.contractId = contractId;
+ }
+
+ public String getSellerName() {
+ return sellerName;
+ }
+
+ public void setSellerName(String sellerName) {
+ this.sellerName = sellerName;
+ }
+
+ public ContractStatusEnum getContractStatus() {
+ return contractStatus;
+ }
+
+ public void setContractStatus(ContractStatusEnum contractStatus) {
+ this.contractStatus = contractStatus;
+ }
+
+ public Long getContractCreated() {
+ return contractCreated;
+ }
+
+ public void setContractCreated(Long contractCreated) {
+ this.contractCreated = contractCreated;
+ }
+
+ public Long getContractLastModifiedOn() {
+ 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
new file mode 100644
index 0000000..0d0995a
--- /dev/null
+++ b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ContractStatusChangedEvent.java
@@ -0,0 +1,86 @@
+package contracts.utils;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Objects;
+
+public class ContractStatusChangedEvent {
+
+ @JsonProperty("contract_last_modified_on")
+ private Long contractLastModifiedOn;
+
+ @JsonProperty("contract_id")
+ private String contractId;
+
+ @JsonProperty("property_id")
+ private String propertyId;
+
+ @JsonProperty("contract_status")
+ 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;
+ }
+
+ public void setContractLastModifiedOn(Long contractLastModifiedOn) {
+ this.contractLastModifiedOn = contractLastModifiedOn;
+ }
+
+ public String getContractId() {
+ return contractId;
+ }
+
+ public void setContractId(String contractId) {
+ this.contractId = contractId;
+ }
+
+ public String getPropertyId() {
+ return propertyId;
+ }
+
+ public void setPropertyId(String propertyId) {
+ this.propertyId = propertyId;
+ }
+
+ public ContractStatusEnum getContractStatus() {
+ return contractStatus;
+ }
+
+ 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/ContractsFunction/src/main/java/contracts/utils/ContractStatusEnum.java b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ContractStatusEnum.java
similarity index 100%
rename from unicorn_contracts/ContractsFunction/src/main/java/contracts/utils/ContractStatusEnum.java
rename to unicorn_contracts/ContractsService/src/main/java/contracts/utils/ContractStatusEnum.java
diff --git a/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ResponseParser.java b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ResponseParser.java
new file mode 100644
index 0000000..a17201a
--- /dev/null
+++ b/unicorn_contracts/ContractsService/src/main/java/contracts/utils/ResponseParser.java
@@ -0,0 +1,63 @@
+package contracts.utils;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+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 {
+
+ 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/main/resources/log4j2.xml b/unicorn_contracts/ContractsService/src/main/resources/log4j2.xml
new file mode 100644
index 0000000..0cc0953
--- /dev/null
+++ b/unicorn_contracts/ContractsService/src/main/resources/log4j2.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/unicorn_contracts/ContractsFunction/src/test/events/create_empty_dict_body_event.json b/unicorn_contracts/ContractsService/src/test/events/create_empty_dict_body_event.json
similarity index 100%
rename from unicorn_contracts/ContractsFunction/src/test/events/create_empty_dict_body_event.json
rename to unicorn_contracts/ContractsService/src/test/events/create_empty_dict_body_event.json
diff --git a/unicorn_contracts/ContractsFunction/src/test/events/create_missing_body_event.json b/unicorn_contracts/ContractsService/src/test/events/create_missing_body_event.json
similarity index 100%
rename from unicorn_contracts/ContractsFunction/src/test/events/create_missing_body_event.json
rename to unicorn_contracts/ContractsService/src/test/events/create_missing_body_event.json
diff --git a/unicorn_contracts/ContractsFunction/src/test/events/create_valid_event.json b/unicorn_contracts/ContractsService/src/test/events/create_valid_event.json
similarity index 100%
rename from unicorn_contracts/ContractsFunction/src/test/events/create_valid_event.json
rename to unicorn_contracts/ContractsService/src/test/events/create_valid_event.json
diff --git a/unicorn_contracts/ContractsFunction/src/test/events/create_wrong_event.json b/unicorn_contracts/ContractsService/src/test/events/create_wrong_event.json
similarity index 100%
rename from unicorn_contracts/ContractsFunction/src/test/events/create_wrong_event.json
rename to unicorn_contracts/ContractsService/src/test/events/create_wrong_event.json
diff --git a/unicorn_contracts/ContractsFunction/src/test/events/update_empty_dict_body_event.json b/unicorn_contracts/ContractsService/src/test/events/update_empty_dict_body_event.json
similarity index 100%
rename from unicorn_contracts/ContractsFunction/src/test/events/update_empty_dict_body_event.json
rename to unicorn_contracts/ContractsService/src/test/events/update_empty_dict_body_event.json
diff --git a/unicorn_contracts/ContractsFunction/src/test/events/update_missing_body_event.json b/unicorn_contracts/ContractsService/src/test/events/update_missing_body_event.json
similarity index 100%
rename from unicorn_contracts/ContractsFunction/src/test/events/update_missing_body_event.json
rename to unicorn_contracts/ContractsService/src/test/events/update_missing_body_event.json
diff --git a/unicorn_contracts/ContractsFunction/src/test/events/update_valid_event.json b/unicorn_contracts/ContractsService/src/test/events/update_valid_event.json
similarity index 100%
rename from unicorn_contracts/ContractsFunction/src/test/events/update_valid_event.json
rename to unicorn_contracts/ContractsService/src/test/events/update_valid_event.json
diff --git a/unicorn_contracts/ContractsFunction/src/test/events/update_wrong_event.json b/unicorn_contracts/ContractsService/src/test/events/update_wrong_event.json
similarity index 100%
rename from unicorn_contracts/ContractsFunction/src/test/events/update_wrong_event.json
rename to unicorn_contracts/ContractsService/src/test/events/update_wrong_event.json
diff --git a/unicorn_contracts/ContractsService/src/test/java/contracts/CreateContractTests.java b/unicorn_contracts/ContractsService/src/test/java/contracts/CreateContractTests.java
new file mode 100644
index 0000000..aec7b53
--- /dev/null
+++ b/unicorn_contracts/ContractsService/src/test/java/contracts/CreateContractTests.java
@@ -0,0 +1,109 @@
+package contracts;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.events.SQSEvent;
+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.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 java.util.Collections;
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class CreateContractTests {
+
+ @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());
+
+ // When
+ handler.handleRequest(event, context);
+
+ // Then
+ verify(dynamoDbClient, times(1)).putItem(any(PutItemRequest.class));
+ }
+
+ @Test
+ public void shouldHandleNullEvent() {
+ // When
+ handler.handleRequest(null, context);
+
+ // Then
+ verifyNoInteractions(dynamoDbClient);
+ }
+
+ @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);
+ }
+
+ 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_contracts/ContractsService/target/classes/log4j2.xml b/unicorn_contracts/ContractsService/target/classes/log4j2.xml
new file mode 100644
index 0000000..0cc0953
--- /dev/null
+++ b/unicorn_contracts/ContractsService/target/classes/log4j2.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/unicorn_contracts/README.md b/unicorn_contracts/README.md
index 1cfad3b..61fdd99 100644
--- a/unicorn_contracts/README.md
+++ b/unicorn_contracts/README.md
@@ -4,13 +4,22 @@
## Architecture overview
-Unicorn Contract manages the contractual relationship between the customers and the Unicorn Properties agency. It's primary function is to allow Unicorn Properties agents to create a new contract for a property listing, and to have the contract approved once it's ready.
+The **Unicorn Contracts** service manages contractual relationships between customers and Unicorn Properties agency. The service handles standard terms and conditions, property service rates, fees, and additional services.
-The architecture is fairly straight forward. An API exposes the create contract and update contract methods. This information is recorded in a Amazon DynamoDB table which will contain all latest information about the contract and it's status.
+Each property can have only one active contract. Properties use their address as a unique identifier instead of a GUID, which correlates across services.
-Each time a new contract is created or updated, Unicorn Contracts publishes a `ContractStatusChanged` event to Amazon EventBridge signalling changes to the contract status. These events are consumed by **Unicorn Properties**, so it can track changes to contracts, without needing to take a direct dependency on Unicorn Contracts and it's database.
+For example: `usa/anytown/main-street/111`.
-Here is an example of an event that is published to EventBridge:
+The contract workflow operates as follows:
+
+1. Agents submit contract creation/update commands through the Contracts API
+1. The API sends requests to Amazon SQS
+1. A Contracts function processes the queue messages and updates Amazon DynamoDB
+1. DynamoDB Streams captures contract changes
+1. Amazon EventBridge Pipes transforms the DynamoDB records into ContractStatusChanged events
+1. Unicorn Approvals consumes these events to track contract changes without direct database dependencies
+
+An example of `ContractStatusChanged` event:
```json
{
diff --git a/unicorn_contracts/events/put_events.json b/unicorn_contracts/events/put_events.json
index c3cfa63..4f81222 100644
--- a/unicorn_contracts/events/put_events.json
+++ b/unicorn_contracts/events/put_events.json
@@ -3,12 +3,12 @@
"Source": "Unicorn.Contracts",
"Detail": "{\"contract_updated_on\":\"10/08/2022 20:36:30\",\"contract_id\": 111,\"property_id\":\"aaa\",\"contract_status\":\"DRAFT\"}",
"DetailType": "ContractStatusChanged",
- "EventBusName": "Dev-UnicornPropertiesEventBus"
+ "EventBusName": "Dev-UnicornApprovalsEventBus"
},
{
"Source": "Unicorn.Contracts",
"Detail": "{\"contract_updated_on\":\"10/08/2022 19:56:30\",\"contract_id\":222,\"property_id\":\"bbb\",\"contract_status\":\"DRAFT\"}",
"DetailType": "ContractStatusChanged",
- "EventBusName": "Dev-UnicornPropertiesEventBus"
+ "EventBusName": "Dev-UnicornApprovalsEventBus"
}
]
diff --git a/unicorn_contracts/template.yaml b/unicorn_contracts/template.yaml
index 9ff3280..673c658 100644
--- a/unicorn_contracts/template.yaml
+++ b/unicorn_contracts/template.yaml
@@ -15,8 +15,6 @@ Metadata:
- WS2001 # Rule disabled because check does not support !ToJsonString transform
- ES1001 # Rule disabled because our Lambda functions don't need DestinationConfig.OnFailure
- W3002 # Rule disabled as nested templates are being packaged
- - E3030 # Rule disabled due to using cfn-lint-serverless rules v0.3
- - E3002 # Rule disabled due to using cfn-lint-serverless rules v0.3
Parameters:
Stage:
@@ -90,7 +88,7 @@ Resources:
ContractEventHandlerFunction:
Type: AWS::Serverless::Function
Properties:
- CodeUri: ContractsFunction
+ CodeUri: ContractsService
Handler: contracts.ContractEventHandler::handleRequest
Policies:
- DynamoDBWritePolicy:
@@ -292,7 +290,7 @@ Resources:
- !Ref AWS::AccountId
source:
- "{{resolve:ssm:/uni-prop/UnicornContractsNamespace}}"
- - "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}"
+ - "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}"
- "{{resolve:ssm:/uni-prop/UnicornWebNamespace}}"
State: ENABLED #You may want to disable this rule in production
Targets:
diff --git a/unicorn_properties/PropertyFunctions/src/main/java/properties/ContractStatusChangedHandlerFunction.java b/unicorn_properties/PropertyFunctions/src/main/java/properties/ContractStatusChangedHandlerFunction.java
deleted file mode 100644
index 31954fc..0000000
--- a/unicorn_properties/PropertyFunctions/src/main/java/properties/ContractStatusChangedHandlerFunction.java
+++ /dev/null
@@ -1,98 +0,0 @@
-package properties;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.Map;
-
-import com.amazonaws.services.lambda.runtime.Context;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import schema.unicorn_contracts.contractstatuschanged.Event;
-import schema.unicorn_contracts.contractstatuschanged.ContractStatusChanged;
-import schema.unicorn_contracts.contractstatuschanged.marshaller.Marshaller;
-import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
-import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
-import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
-import software.amazon.lambda.powertools.logging.Logging;
-import software.amazon.lambda.powertools.metrics.Metrics;
-import software.amazon.lambda.powertools.tracing.Tracing;
-
-/**
- * Lambda handler to update the contract status change
- */
-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;
- }
-}
diff --git a/unicorn_properties/PropertyFunctions/src/main/java/properties/PropertiesApprovalSyncFunction.java b/unicorn_properties/PropertyFunctions/src/main/java/properties/PropertiesApprovalSyncFunction.java
deleted file mode 100644
index 5ad7349..0000000
--- a/unicorn_properties/PropertyFunctions/src/main/java/properties/PropertiesApprovalSyncFunction.java
+++ /dev/null
@@ -1,113 +0,0 @@
-package properties;
-
-import com.amazonaws.services.lambda.runtime.Context;
-import com.amazonaws.services.lambda.runtime.RequestHandler;
-import com.amazonaws.services.lambda.runtime.events.DynamodbEvent;
-import com.amazonaws.services.lambda.runtime.events.StreamsEventResponse;
-import com.amazonaws.services.lambda.runtime.events.models.dynamodb.AttributeValue;
-import com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import properties.dao.ContractStatus;
-import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
-import software.amazon.awssdk.services.sfn.SfnAsyncClient;
-import software.amazon.awssdk.services.sfn.model.SendTaskSuccessRequest;
-
-import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import software.amazon.lambda.powertools.logging.Logging;
-import software.amazon.lambda.powertools.metrics.Metrics;
-import software.amazon.lambda.powertools.tracing.Tracing;
-
-public class PropertiesApprovalSyncFunction implements RequestHandler {
-
- Logger logger = LogManager.getLogger();
- SfnAsyncClient snfClient = 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()) {
- 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();
- }
- 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));
- return new StreamsEventResponse(batchItemFailures);
- }
- }
-
- 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);
-
- SendTaskSuccessRequest request = SendTaskSuccessRequest.builder()
- .taskToken(contractStatus.getSfn_wait_approved_task_token())
- .output(taskResult)
- .build();
- snfClient.sendTaskSuccess(request).join();
-
- }
-}
\ No newline at end of file
diff --git a/unicorn_properties/PropertyFunctions/src/main/java/properties/WaitForContractApprovalFunction.java b/unicorn_properties/PropertyFunctions/src/main/java/properties/WaitForContractApprovalFunction.java
deleted file mode 100644
index 9dbe5b1..0000000
--- a/unicorn_properties/PropertyFunctions/src/main/java/properties/WaitForContractApprovalFunction.java
+++ /dev/null
@@ -1,109 +0,0 @@
-package properties;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.Map;
-
-import com.amazonaws.services.lambda.runtime.Context;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
-import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
-import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
-import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
-import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;
-import software.amazon.lambda.powertools.logging.Logging;
-import software.amazon.lambda.powertools.metrics.Metrics;
-import software.amazon.lambda.powertools.tracing.Tracing;
-
-/**
- * Lambda handler to update the contract status change
- */
-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 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;
- }
-
-}
diff --git a/unicorn_properties/PropertyFunctions/src/main/java/properties/dao/ContractStatus.java b/unicorn_properties/PropertyFunctions/src/main/java/properties/dao/ContractStatus.java
deleted file mode 100644
index db40a77..0000000
--- a/unicorn_properties/PropertyFunctions/src/main/java/properties/dao/ContractStatus.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package properties.dao;
-
-public class ContractStatus {
- String contract_id;
- String contract_status;
- String property_id;
- String sfn_wait_approved_task_token;
-
- @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 + "]";
- }
-
- public String getContract_id() {
- return contract_id;
- }
-
- public void setContract_id(String contract_id) {
- this.contract_id = contract_id;
- }
-
- public String getContract_status() {
- return contract_status;
- }
-
- public void setContract_status(String contract_status) {
- this.contract_status = contract_status;
- }
-
- public String getProperty_id() {
- return property_id;
- }
-
- public void setProperty_id(String property_id) {
- this.property_id = property_id;
- }
-
- public String getSfn_wait_approved_task_token() {
- return sfn_wait_approved_task_token;
- }
-
- public void setSfn_wait_approved_task_token(String sfn_wait_approved_task_token) {
- this.sfn_wait_approved_task_token = sfn_wait_approved_task_token;
- }
-
-}
diff --git a/unicorn_properties/PropertyFunctions/src/test/java/properties/ContractStatusTests.java b/unicorn_properties/PropertyFunctions/src/test/java/properties/ContractStatusTests.java
deleted file mode 100644
index 135aea3..0000000
--- a/unicorn_properties/PropertyFunctions/src/test/java/properties/ContractStatusTests.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package properties;
-
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-
-import com.amazonaws.services.lambda.runtime.Context;
-
-import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
-import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
-
-import org.junit.Before;
-import org.junit.jupiter.api.Test;
-import org.junit.runner.RunWith;
-import org.mockito.junit.MockitoJUnitRunner;
-
-@RunWith(MockitoJUnitRunner.class)
-public class ContractStatusTests {
-
- Context context;
- DynamoDbClient client;
-
- ContractStatusChangedHandlerFunction contractStatusChangedHandler;
-
- Map response = new HashMap();
-
- @Before
- public void setUp() throws Exception {
-
- 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"));
-
- }
-
-}
diff --git a/unicorn_properties/README.md b/unicorn_properties/README.md
deleted file mode 100644
index 158150e..0000000
--- a/unicorn_properties/README.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Developing Unicorn Properties
-
-
-
-## Architecture overview
-
-Unicorn Properties is primarily responsible for approving property listings for Unicorn Web.
-
-A core component of Unicorn Properties is the approvals workflow. The approvals workflow is implemented using an AWS Step Functions state machine. At a high level, the workflow will:
-
-* Check whether or not it has any contract information for the property it needs to approve. If there is no contract information, the approval process cannot be completed.
-* Ensure the sentiment of the property description is positive and that there no unsafe images. All checks must pass for the listing to be made public.
-* Ensure that the contract is in an APPROVED state before it can approve the listing. This accounts for a situation where the property listings are created before the contract has been signed and the services for Unicorn Properties are paid for.
-* Publish the result of the workflow via the `PublicationEvaluationCompleted` event.
-
-The workflow is initiated by a request made by an Unicorn Properties **agent** to have the property approved for publication. Once they have created a property listing (added property details and photos), they initiate the request in Unicorn Web, which generates a `PublicationApprovalRequested` event. This event contains the property information which the workflow processes.
-
-In order process the approvals workflow successfully, the properties service needs to know the current status of a contract. To remain fully decoupled from the **Contracts Service**, it maintains a local copy of contract status by consuming the `ContractStatusChanged` event. This is eliminates the need for the Contracts service to expose an API that gives other services access to its database, and allows the Properties service to function autonomously.
-
-When the workflow is paused to check to see whether or not the contract is in an approved state, the `WaitForContractApproval` state will update a contract status for a specified property with its task token. This initiates a stream event on the DynamoDB table. The Property approvals sync function handles DynamoDB stream events. It determines whether or not to pass AWS Step Function task token back to the state machine based on the contract state.
-
-If workflow is completed successfully, it will emit a `PublicationEvaluationCompleted` event, with an evaluation result of `APPROVED` or `DECLINED`. This is what the Property Web will listen to in order to make the list available for publication.
-
-## Note:
-
-Upon deleting the CloudFormation stack for this service, check if the `ApprovalStateMachine` StepFunction doesn't have any executions in `RUNNING` state. If there are, cancel those execution prior to deleting the CloudFormation stack.
diff --git a/unicorn_shared/Makefile b/unicorn_shared/Makefile
index ffc3833..415997c 100644
--- a/unicorn_shared/Makefile
+++ b/unicorn_shared/Makefile
@@ -32,3 +32,7 @@ delete-images: ## Deletes all shared images stacks
--stack-name "uni-prop-$$env-images"; \
fi; \
done
+
+list-parameters: ## Lists all parameters in the Unicorn Properties namespace
+ aws ssm get-parameters-by-path --path "/uni-prop" --recursive --with-decryption --query 'Parameters[*].[Name,Value,Type]' --output table
+
\ No newline at end of file
diff --git a/unicorn_shared/uni-prop-namespaces.yaml b/unicorn_shared/uni-prop-namespaces.yaml
index 55602fb..7df02f4 100644
--- a/unicorn_shared/uni-prop-namespaces.yaml
+++ b/unicorn_shared/uni-prop-namespaces.yaml
@@ -14,12 +14,12 @@ Resources:
Name: !Sub /uni-prop/UnicornContractsNamespace
Value: "unicorn.contracts"
- UnicornPropertiesNamespaceParam:
+ UnicornApprovalsNamespaceParam:
Type: AWS::SSM::Parameter
Properties:
Type: String
- Name: !Sub /uni-prop/UnicornPropertiesNamespace
- Value: "unicorn.properties"
+ Name: !Sub /uni-prop/UnicornApprovalsNamespace
+ Value: "unicorn.approvals"
UnicornWebNamespaceParam:
Type: AWS::SSM::Parameter
@@ -35,9 +35,9 @@ Outputs:
Description: Unicorn Contracts namespace parameter
Value: !Ref UnicornContractsNamespaceParam
- UnicornPropertiesNamespace:
+ UnicornApprovalsNamespace:
Description: Unicorn Properties namespace parameter
- Value: !Ref UnicornPropertiesNamespaceParam
+ Value: !Ref UnicornApprovalsNamespaceParam
UnicornWebNamespace:
Description: Unicorn Web namespace parameter
@@ -49,7 +49,7 @@ Outputs:
UnicornPropertiesNamespaceValue:
Description: Unicorn Properties namespace parameter value
- Value: !GetAtt UnicornPropertiesNamespaceParam.Value
+ Value: !GetAtt UnicornApprovalsNamespaceParam.Value
UnicornWebNamespaceValue:
Description: Unicorn Web namespace parameter value
diff --git a/unicorn_web/.gitignore b/unicorn_web/.gitignore
index dace86b..12b83f2 100644
--- a/unicorn_web/.gitignore
+++ b/unicorn_web/.gitignore
@@ -82,3 +82,12 @@
# We ignore Python's requirements.txt as we use Poetry instead
**/requirements.txt
**/.aws-sam
+
+### Maven ###
+**/target/
+**/dependency-reduced-pom.xml
+**/*.jar
+**/*.war
+**/*.ear
+**/*.class
+**/builddef.lst
diff --git a/unicorn_web/Common/pom.xml b/unicorn_web/Common/pom.xml
new file mode 100644
index 0000000..2d86d38
--- /dev/null
+++ b/unicorn_web/Common/pom.xml
@@ -0,0 +1,36 @@
+
+
+ 4.0.0
+
+ unicorn.web
+ unicorn-web-parent
+ 1.0
+ ../pom.xml
+
+
+ common
+ Common
+
+
+ 17
+ 17
+ UTF-8
+
+
+
+ software.amazon.awssdk
+ dynamodb-enhanced
+ 2.32.29
+ compile
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ 2.18.4
+ compile
+
+
+
+
\ No newline at end of file
diff --git a/unicorn_web/PropertyFunctions/src/main/java/property/dao/Property.java b/unicorn_web/Common/src/main/java/dao/Property.java
similarity index 58%
rename from unicorn_web/PropertyFunctions/src/main/java/property/dao/Property.java
rename to unicorn_web/Common/src/main/java/dao/Property.java
index c0c6405..de0be86 100644
--- a/unicorn_web/PropertyFunctions/src/main/java/property/dao/Property.java
+++ b/unicorn_web/Common/src/main/java/dao/Property.java
@@ -1,6 +1,7 @@
-package property.dao;
+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/PropertyFunctions/src/main/java/property/populate/PopulateDataFunction.java b/unicorn_web/PropertyFunctions/src/main/java/property/populate/PopulateDataFunction.java
deleted file mode 100644
index 3b15534..0000000
--- a/unicorn_web/PropertyFunctions/src/main/java/property/populate/PopulateDataFunction.java
+++ /dev/null
@@ -1,106 +0,0 @@
-package property.populate;
-
-import java.util.List;
-import java.util.Map;
-
-import com.amazonaws.services.lambda.runtime.Context;
-import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent;
-
-import property.dao.Property;
-import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable;
-import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient;
-import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
-
-import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
-import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
-import software.amazon.lambda.powertools.cloudformation.AbstractCustomResourceHandler;
-import software.amazon.lambda.powertools.cloudformation.Response;
-
-public class PopulateDataFunction extends AbstractCustomResourceHandler {
-
- String DYNAMODB_TABLE = System.getenv("DYNAMODB_TABLE");
-
- String[] validKeys = { "country", "city", "street", "number", "description", "contract", "listprice", "currency",
- "images" };
-
- DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder()
- .httpClientBuilder(NettyNioAsyncHttpClient.builder()
- .maxConcurrency(100)
- .maxPendingConnectionAcquires(10_000))
- .build();
-
- DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder()
- .dynamoDbClient(dynamodbClient)
- .build();
-
- @Override
- protected Response create(CloudFormationCustomResourceEvent createEvent, Context context) {
- try {
- return handleEvent(createEvent);
- } catch (Exception e) {
- return Response.builder()
- .value(Map.of("Resource", DYNAMODB_TABLE + "-" + createEvent.getLogicalResourceId()))
- .status(Response.Status.FAILED).build();
- }
-
- }
-
- @Override
- protected Response update(CloudFormationCustomResourceEvent updateEvent, Context context) {
- try {
- return handleEvent(updateEvent);
- } catch (Exception e) {
- return Response.builder()
- .value(Map.of("Resource", DYNAMODB_TABLE + "-" + updateEvent.getLogicalResourceId()))
- .status(Response.Status.FAILED).build();
- }
- }
-
- @Override
- protected Response delete(CloudFormationCustomResourceEvent deleteEvent, Context context) {
- return null;
- }
-
- private void saveInDatabase(Property property, String table_name) {
-
- DynamoDbAsyncTable propertyTable = enhancedClient.table(table_name,
- TableSchema.fromBean(Property.class));
- propertyTable.putItem(property).join();
-
- }
-
- Property createPropertyFromEvent(CloudFormationCustomResourceEvent event) throws Exception {
- Map propertyMap = event.getResourceProperties();
- // Iterate over map and check the keys
- for (String strKey : validKeys) {
-
- if (!propertyMap.containsKey(strKey)) {
- throw new Exception("Invalid input: missing mandatory field " + strKey);
- }
-
- }
-
- Property property = new Property();
- property.setCountry((String) propertyMap.get("country"));
- property.setCity((String) propertyMap.get("city"));
- property.setStreet((String) propertyMap.get("street"));
- property.setPropertyNumber((String) propertyMap.get("number"));
- property.setDescription((String) propertyMap.get("description"));
- property.setContract((String) propertyMap.get("contract"));
- property.setListprice(Float.parseFloat((String) propertyMap.get("listprice")));
- property.setCurrency((String) propertyMap.get("currency"));
- List images = (List) propertyMap.get("images");
- property.setImages(images);
- property.setStatus("NEW");
- return property;
-
- }
-
- private Response handleEvent(CloudFormationCustomResourceEvent createEvent) throws Exception {
-
- Property property = createPropertyFromEvent(createEvent);
- saveInDatabase(property, DYNAMODB_TABLE);
- return Response.builder().value(Map.of("Resource", DYNAMODB_TABLE + "-" + createEvent.getLogicalResourceId()))
- .status(Response.Status.SUCCESS).build();
- }
-}
diff --git a/unicorn_web/PropertyFunctions/src/main/java/property/requestapproval/PublicationApprovedFunction.java b/unicorn_web/PropertyFunctions/src/main/java/property/requestapproval/PublicationApprovedFunction.java
deleted file mode 100644
index 1295ae3..0000000
--- a/unicorn_web/PropertyFunctions/src/main/java/property/requestapproval/PublicationApprovedFunction.java
+++ /dev/null
@@ -1,103 +0,0 @@
-package property.requestapproval;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.nio.charset.StandardCharsets;
-
-import com.amazonaws.services.lambda.runtime.Context;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import property.dao.Property;
-import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable;
-import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient;
-import software.amazon.awssdk.enhanced.dynamodb.Key;
-import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
-import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest;
-import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
-import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
-import software.amazon.lambda.powertools.logging.Logging;
-import software.amazon.lambda.powertools.metrics.Metrics;
-import software.amazon.lambda.powertools.tracing.Tracing;
-import schema.unicorn_properties.publicationevaluationcompleted.marshaller.Marshaller;
-import schema.unicorn_properties.publicationevaluationcompleted.AWSEvent;
-import schema.unicorn_properties.publicationevaluationcompleted.PublicationEvaluationCompleted;
-
-/**
- * Function checks for the existence of a contract status entry for a specified
- * property.
- *
- * If an entry exists, pause the workflow, and update the record with task
- * token.
- */
-public class PublicationApprovedFunction {
-
- Logger logger = LogManager.getLogger();
-
- final String TABLE_NAME = System.getenv("DYNAMODB_TABLE");
- 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));
-
- @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 property status'"));
- writer.close();
-
- }
-
- @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 = ("property#" + 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 property number explicitly to ensure it's correct
- existingProperty.setPropertyNumber(number);
- existingProperty.setStatus(evaluationResult);
-
- logger.info("Updating property with status: {} and propertyNumber: {}",
- evaluationResult, existingProperty.getPropertyNumber());
- propertyTable.putItem(existingProperty).join();
- }
-
-}
diff --git a/unicorn_web/PropertyFunctions/src/main/java/property/requestapproval/RequestApprovalFunction.java b/unicorn_web/PropertyFunctions/src/main/java/property/requestapproval/RequestApprovalFunction.java
deleted file mode 100644
index 46d1bf2..0000000
--- a/unicorn_web/PropertyFunctions/src/main/java/property/requestapproval/RequestApprovalFunction.java
+++ /dev/null
@@ -1,263 +0,0 @@
-package property.requestapproval;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-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;
-import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
-import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JsonMappingException;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import property.dao.Property;
-import software.amazon.awssdk.core.async.SdkPublisher;
-import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable;
-import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient;
-import software.amazon.awssdk.enhanced.dynamodb.Expression;
-import software.amazon.awssdk.enhanced.dynamodb.Key;
-import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
-import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional;
-import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest;
-import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
-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.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;
-
-/**
- * Validates the integrity of the property content
- */
-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();
-
- @Tracing
- @Metrics(captureColdStart = true)
- @Logging(logEvent = true, correlationIdPath = CorrelationIdPathConstants.API_GATEWAY_REST)
- public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input,
- final Context context) throws JsonMappingException, JsonProcessingException {
- {
-
- Map headers = new HashMap<>();
- headers.put("Content-Type", "application/json");
- headers.put("X-Custom-Header", "application/json");
- APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
- .withHeaders(headers);
-
- JsonNode rootNode = objectMapper.readTree(input.getBody());
- String propertyId = rootNode.get("property_id").asText();
- Matcher matcher = pattern.matcher(propertyId);
- boolean valid = matcher.matches();
- if (!valid) {
- APIGatewayProxyResponseEvent errorResponse = response
- .withBody("Input invalid; must conform to regular expression: " + EXPRESSION)
- .withStatusCode(500);
- return errorResponse;
- }
- String[] splitString = propertyId.split("/");
- String country = splitString[0];
- String city = splitString[1];
- String street = splitString[2];
- String number = splitString[3];
- String strPartionKey = ("property#" + country + "#" + city).replace(' ', '-').toLowerCase();
- String strSortKey = (street + "#" + number).replace(' ', '-').toLowerCase();
- try {
- List properties = queryTable(strPartionKey, strSortKey);
- if (properties.size() <= 0) {
- APIGatewayProxyResponseEvent errorResponse = response
- .withBody("No property found in database with the requested property id")
- .withStatusCode(500);
- return errorResponse;
- }
- 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) {
- APIGatewayProxyResponseEvent errorResponse = response
- .withBody("Error in searching")
- .withStatusCode(500);
- return errorResponse;
- }
- return response
- .withStatusCode(200)
- .withBody("'result': 'Approval Requested'");
-
- }
-
- }
-
- public List queryTable(String partitionkey, String sortKey) throws Exception {
-
- try {
- if (partitionkey == null || sortKey == null) {
- throw new Exception("Invalid Input");
- }
- List result = new ArrayList();
- SdkPublisher properties = null;
-
- Key key = Key.builder().partitionValue(partitionkey).sortValue(sortKey).build();
-
- QueryConditional queryConditional = QueryConditional.sortBeginsWith(key);
- QueryEnhancedRequest request = QueryEnhancedRequest.builder().queryConditional(queryConditional)
- .build();
- properties = propertyTable.query(request).items();
-
- CompletableFuture future = properties.subscribe(res -> {
- // Add response to the list
- result.add(res);
- });
- future.get();
-
- return result;
-
- } catch (DynamoDbException | InterruptedException | ExecutionException e) {
- throw new Exception(e.getMessage());
- }
- }
-
- @Tracing
- @Metrics
- public String 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();
-
- requestEntries.add(PutEventsRequestEntry.builder()
- .eventBusName(EVENT_BUS)
- .source("Unicorn.Web")
- .resources(property.getId())
- .detailType("PublicationApprovalRequested")
- .detail(event_string).build());
-
- PutEventsRequest eventsRequest = PutEventsRequest.builder().entries(requestEntries).build();
-
- eventBridgeClient.putEvents(eventsRequest).join();
-
- return event_string;
- }
-}
-
-class RequestApproval {
- @JsonProperty("property_id")
- String propertyId;
- Address address;
-
- public String getPropertyId() {
- return propertyId;
- }
-
- public void setPropertyId(String propertyId) {
- this.propertyId = propertyId;
- }
-
- public Address getAddress() {
- return address;
- }
-
- public void setAddress(Address address) {
- this.address = address;
- }
-
-}
-
-class Address {
- String country;
-
- public String getCountry() {
- return country;
- }
-
- public void setCountry(String country) {
- this.country = country;
- }
-
- public String getCity() {
- return city;
- }
-
- public void setCity(String city) {
- this.city = city;
- }
-
- public String getState() {
- return state;
- }
-
- public void setState(String state) {
- this.state = state;
- }
-
- public String getNumber() {
- return number;
- }
-
- public void setNumber(String number) {
- this.number = number;
- }
-
- String city;
- String state;
- String number;
-
-}
diff --git a/unicorn_web/PropertyFunctions/src/main/java/property/search/PropertySearchFunction.java b/unicorn_web/PropertyFunctions/src/main/java/property/search/PropertySearchFunction.java
deleted file mode 100644
index e3a3e7d..0000000
--- a/unicorn_web/PropertyFunctions/src/main/java/property/search/PropertySearchFunction.java
+++ /dev/null
@@ -1,200 +0,0 @@
-package property.search;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-
-import com.amazonaws.services.lambda.runtime.Context;
-import com.amazonaws.services.lambda.runtime.RequestHandler;
-import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
-import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import property.dao.Property;
-import software.amazon.awssdk.core.async.SdkPublisher;
-import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable;
-import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient;
-import software.amazon.awssdk.enhanced.dynamodb.Expression;
-import software.amazon.awssdk.enhanced.dynamodb.Key;
-import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
-import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional;
-import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest;
-import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
-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.
- */
-public class PropertySearchFunction
- implements RequestHandler {
-
- private static final Logger logger = LogManager.getLogger(PropertySearchFunction.class);
-
- String TABLE_NAME = System.getenv("DYNAMODB_TABLE");
-
- 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));
-
- 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();
-
- @Tracing
- @Metrics(captureColdStart = true)
- @Logging(logEvent = true, correlationIdPath = CorrelationIdPathConstants.API_GATEWAY_REST)
- public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) {
-
- 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 requestPath = input.getResource();
- String responseString = null;
- String strPartitionKey = ("property#" + input.getPathParameters().get("country") + "#"
- + input.getPathParameters().get("city")).replace(' ', '-').toLowerCase();
-
- String strSortKey = null;
- switch (requestPath) {
- 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;
- 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;
- 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;
- default:
- return response
- .withStatusCode(400)
- .withBody("{ \"message\": \"ErrorInRequest\", \"requestdetails\": \"Input Invalid\" }");
-
- }
-
- return response
- .withStatusCode(200)
- .withBody(responseString);
- }
-
- public List queryTable(String partitionkey, String sortKey) throws Exception {
-
- 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();
- }
-
- CompletableFuture future = properties.subscribe(res -> {
- // Add response to the list
- result.add(res);
- });
- future.get();
-
- return result;
-
- } catch (DynamoDbException | InterruptedException | ExecutionException e) {
- throw new Exception(e.getMessage());
- }
- }
-
-}
diff --git a/unicorn_web/PublicationManagerService/pom.xml b/unicorn_web/PublicationManagerService/pom.xml
new file mode 100644
index 0000000..ace1280
--- /dev/null
+++ b/unicorn_web/PublicationManagerService/pom.xml
@@ -0,0 +1,235 @@
+
+ 4.0.0
+ approval
+ PublicationManagerService
+ 1.0
+ jar
+ Unicorn Web Approval Service module
+
+ 17
+ 17
+ 2.32.29
+ 1.20.2
+ 3.16.1
+ 5.18.0
+ 4.13.2
+ 1.1.2
+ 1.3.0
+ 2.32.29
+
+
+
+
+ software.amazon.awssdk
+ bom
+ ${aws.java.sdk.version}
+ pom
+ import
+
+
+
+
+
+
+
+ software.amazon.lambda
+ powertools-tracing
+ ${aws.java.powertool.version}
+
+
+ software.amazon.lambda
+ powertools-logging
+ ${aws.java.powertool.version}
+
+
+ software.amazon.lambda
+ powertools-metrics
+ ${aws.java.powertool.version}
+
+
+ software.amazon.lambda
+ powertools-cloudformation
+ ${aws.java.powertool.version}
+
+
+ software.amazon.cloudwatchlogs
+ aws-embedded-metrics
+ 4.2.0
+
+
+ software.amazon.awssdk
+ lambda
+ ${aws.java.sdk.version}
+
+
+ software.amazon.awssdk
+ dynamodb
+ ${aws.java.sdk.version}
+
+
+ software.amazon.awssdk
+ dynamodb-enhanced
+ ${aws.java.sdk.version}
+
+
+
+ software.amazon.awssdk
+ sfn
+ ${aws.java.sdk.version}
+
+
+
+ software.amazon.awssdk
+ eventbridge
+ ${aws.java.sdk.version}
+
+
+ com.amazonaws
+ aws-lambda-java-events
+ ${aws-lambda-java-events.version}
+
+
+ software.amazon.awssdk
+ netty-nio-client
+ ${netty-nio-client.version}
+
+
+
+ org.mockito
+ mockito-core
+ ${mockito-core.version}
+ test
+
+
+ junit
+ junit
+ ${junit.version}
+ test
+
+
+ com.amazonaws
+ aws-lambda-java-tests
+ ${aws-lambda-java-tests.version}
+ test
+
+
+
+ com.amazonaws
+ aws-lambda-java-core
+ ${aws-lambda-java-core.version}
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.18.4
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ 2.18.4
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ 2.18.4
+
+
+
+
+ org.apache.logging.log4j
+ log4j-api
+ 2.25.1
+
+
+ org.apache.logging.log4j
+ log4j-core
+ 2.25.1
+
+
+ common
+ Common
+ 1.0
+ compile
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.3
+
+
+ handler
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.6.0
+
+
+
+
+ package
+
+ shade
+
+
+
+
+
+ org.codehaus.mojo
+ aspectj-maven-plugin
+ 1.15.0
+
+ 17
+
+
+ software.amazon.lambda
+ powertools-tracing
+
+
+ software.amazon.lambda
+ powertools-logging
+
+
+ software.amazon.lambda
+ powertools-metrics
+
+
+
+
+
+
+ compile
+
+
+
+
+
+ org.aspectj
+ aspectjtools
+ 1.9.22.1
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 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
new file mode 100644
index 0000000..1faef8b
--- /dev/null
+++ b/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/PublicationApprovedEventHandler.java
@@ -0,0 +1,127 @@
+package publicationmanager;
+
+import java.io.IOException;
+import java.io.InputStream;
+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;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import dao.Property;
+import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable;
+import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient;
+import software.amazon.awssdk.enhanced.dynamodb.Key;
+import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
+import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
+import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
+import software.amazon.lambda.powertools.logging.Logging;
+import software.amazon.lambda.powertools.metrics.Metrics;
+import software.amazon.lambda.powertools.tracing.Tracing;
+import schema.unicorn_approvals.publicationevaluationcompleted.marshaller.Marshaller;
+import schema.unicorn_approvals.publicationevaluationcompleted.AWSEvent;
+import schema.unicorn_approvals.publicationevaluationcompleted.PublicationEvaluationCompleted;
+
+/**
+ * Processes publication evaluation completed events and updates property status.
+ */
+public class PublicationApprovedEventHandler {
+
+ 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();
+
+ public PublicationApprovedEventHandler() {
+ DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder()
+ .httpClientBuilder(NettyNioAsyncHttpClient.builder())
+ .build();
+
+ DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder()
+ .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 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
new file mode 100644
index 0000000..47e944f
--- /dev/null
+++ b/unicorn_web/PublicationManagerService/src/main/java/publicationmanager/RequestApprovalFunction.java
@@ -0,0 +1,269 @@
+package publicationmanager;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+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.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import dao.Property;
+import software.amazon.awssdk.core.async.SdkPublisher;
+import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable;
+import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient;
+import software.amazon.awssdk.enhanced.dynamodb.Key;
+import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional;
+import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest;
+import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
+import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
+import software.amazon.awssdk.services.dynamodb.model.DynamoDbException;
+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;
+
+/**
+ * Validates property requests and sends approval events.
+ */
+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");
+ 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) {
+ 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");
+ }
+
+ 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");
+ }
+
+ 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);
+ }
+
+ 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;
+ final String sortKey;
+
+ PropertyComponents(String partitionKey, String sortKey) {
+ this.partitionKey = partitionKey;
+ this.sortKey = sortKey;
+ }
+ }
+
+ 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");
+ }
+
+ 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) {
+ logger.error("Error querying DynamoDB", e);
+ throw new Exception("Database query failed: " + e.getMessage());
+ }
+ }
+
+ @Tracing
+ @Metrics
+ 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 eventString = objectMapper.writeValueAsString(event);
+
+ PutEventsRequestEntry requestEntry = PutEventsRequestEntry.builder()
+ .eventBusName(eventBus)
+ .source("Unicorn.Web")
+ .resources(property.getId())
+ .detailType("PublicationApprovalRequested")
+ .detail(eventString)
+ .build();
+
+ PutEventsRequest eventsRequest = PutEventsRequest.builder()
+ .entries(requestEntry)
+ .build();
+
+ eventBridgeClient.putEvents(eventsRequest).join();
+ logger.info("Event sent successfully for property: {}", property.getId());
+ }
+}
+
+class RequestApproval {
+ @JsonProperty("property_id")
+ String propertyId;
+ Address address;
+
+ public String getPropertyId() {
+ return propertyId;
+ }
+
+ public void setPropertyId(String propertyId) {
+ this.propertyId = propertyId;
+ }
+
+ public Address getAddress() {
+ return address;
+ }
+
+ public void setAddress(Address address) {
+ this.address = address;
+ }
+}
+
+class Address {
+ String country;
+ String city;
+ String state;
+ String number;
+
+ public String getCountry() {
+ return country;
+ }
+
+ public void setCountry(String country) {
+ this.country = country;
+ }
+
+ public String getCity() {
+ return city;
+ }
+
+ public void setCity(String city) {
+ this.city = city;
+ }
+
+ public String getState() {
+ return state;
+ }
+
+ public void setState(String state) {
+ this.state = state;
+ }
+
+ public String getNumber() {
+ return number;
+ }
+
+ public void setNumber(String number) {
+ this.number = number;
+ }
+}
diff --git a/unicorn_web/PropertyFunctions/src/main/java/schema/unicorn_properties/publicationevaluationcompleted/AWSEvent.java b/unicorn_web/PublicationManagerService/src/main/java/schema/unicorn_approvals/publicationevaluationcompleted/AWSEvent.java
similarity index 96%
rename from unicorn_web/PropertyFunctions/src/main/java/schema/unicorn_properties/publicationevaluationcompleted/AWSEvent.java
rename to unicorn_web/PublicationManagerService/src/main/java/schema/unicorn_approvals/publicationevaluationcompleted/AWSEvent.java
index af891fb..0f2634c 100644
--- a/unicorn_web/PropertyFunctions/src/main/java/schema/unicorn_properties/publicationevaluationcompleted/AWSEvent.java
+++ b/unicorn_web/PublicationManagerService/src/main/java/schema/unicorn_approvals/publicationevaluationcompleted/AWSEvent.java
@@ -1,4 +1,4 @@
-package schema.unicorn_properties.publicationevaluationcompleted;
+package schema.unicorn_approvals.publicationevaluationcompleted;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -7,7 +7,6 @@
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
-import schema.unicorn_properties.publicationevaluationcompleted.PublicationEvaluationCompleted;
import java.io.Serializable;
public class AWSEvent {
diff --git a/unicorn_web/PropertyFunctions/src/main/java/schema/unicorn_properties/publicationevaluationcompleted/PublicationEvaluationCompleted.java b/unicorn_web/PublicationManagerService/src/main/java/schema/unicorn_approvals/publicationevaluationcompleted/PublicationEvaluationCompleted.java
similarity index 97%
rename from unicorn_web/PropertyFunctions/src/main/java/schema/unicorn_properties/publicationevaluationcompleted/PublicationEvaluationCompleted.java
rename to unicorn_web/PublicationManagerService/src/main/java/schema/unicorn_approvals/publicationevaluationcompleted/PublicationEvaluationCompleted.java
index 01e5948..0b45c97 100644
--- a/unicorn_web/PropertyFunctions/src/main/java/schema/unicorn_properties/publicationevaluationcompleted/PublicationEvaluationCompleted.java
+++ b/unicorn_web/PublicationManagerService/src/main/java/schema/unicorn_approvals/publicationevaluationcompleted/PublicationEvaluationCompleted.java
@@ -1,4 +1,4 @@
-package schema.unicorn_properties.publicationevaluationcompleted;
+package schema.unicorn_approvals.publicationevaluationcompleted;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
diff --git a/unicorn_web/PropertyFunctions/src/main/java/schema/unicorn_properties/publicationevaluationcompleted/marshaller/Marshaller.java b/unicorn_web/PublicationManagerService/src/main/java/schema/unicorn_approvals/publicationevaluationcompleted/marshaller/Marshaller.java
similarity index 87%
rename from unicorn_web/PropertyFunctions/src/main/java/schema/unicorn_properties/publicationevaluationcompleted/marshaller/Marshaller.java
rename to unicorn_web/PublicationManagerService/src/main/java/schema/unicorn_approvals/publicationevaluationcompleted/marshaller/Marshaller.java
index 6a8395a..732ad60 100644
--- a/unicorn_web/PropertyFunctions/src/main/java/schema/unicorn_properties/publicationevaluationcompleted/marshaller/Marshaller.java
+++ b/unicorn_web/PublicationManagerService/src/main/java/schema/unicorn_approvals/publicationevaluationcompleted/marshaller/Marshaller.java
@@ -1,9 +1,9 @@
-package schema.unicorn_properties.publicationevaluationcompleted.marshaller;
+package schema.unicorn_approvals.publicationevaluationcompleted.marshaller;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
-import schema.unicorn_properties.publicationevaluationcompleted.AWSEvent;
+import schema.unicorn_approvals.publicationevaluationcompleted.AWSEvent;
import java.io.IOException;
import java.io.InputStream;
diff --git a/unicorn_web/PropertyFunctions/src/main/resources/log4j2.xml b/unicorn_web/PublicationManagerService/src/main/resources/log4j2.xml
similarity index 88%
rename from unicorn_web/PropertyFunctions/src/main/resources/log4j2.xml
rename to unicorn_web/PublicationManagerService/src/main/resources/log4j2.xml
index fcffc72..e4b9c1a 100644
--- a/unicorn_web/PropertyFunctions/src/main/resources/log4j2.xml
+++ b/unicorn_web/PublicationManagerService/src/main/resources/log4j2.xml
@@ -9,6 +9,6 @@
-
+
\ No newline at end of file
diff --git a/unicorn_web/PublicationManagerService/target/classes/log4j2.xml b/unicorn_web/PublicationManagerService/target/classes/log4j2.xml
new file mode 100644
index 0000000..e4b9c1a
--- /dev/null
+++ b/unicorn_web/PublicationManagerService/target/classes/log4j2.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/unicorn_web/PublicationManagerService/tests/events/eventbridge/put_event_publication_evaluation_completed.json b/unicorn_web/PublicationManagerService/tests/events/eventbridge/put_event_publication_evaluation_completed.json
new file mode 100644
index 0000000..c022083
--- /dev/null
+++ b/unicorn_web/PublicationManagerService/tests/events/eventbridge/put_event_publication_evaluation_completed.json
@@ -0,0 +1,8 @@
+[
+ {
+ "Source": "unicorn.approvals",
+ "Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"evaluation_result\": \"APPROVED\"}",
+ "DetailType": "PublicationEvaluationCompleted",
+ "EventBusName": "UnicornWebBus-local"
+ }
+]
diff --git a/unicorn_web/README.md b/unicorn_web/README.md
index 9e70253..38e17ea 100644
--- a/unicorn_web/README.md
+++ b/unicorn_web/README.md
@@ -4,18 +4,15 @@
## Architecture Overview
-Unicorn Web is primarily responsible for allowing customers to search and view property listings. It also supports ability for agents to request approval for specific property. Those approval requests are sent to Property service for validation, before Properties table is updated with approval evaluation results.
+Unicorn Web lets customers search for and view property listings. The Web API also allows Unicorn Properties agents to request approval for specific properties that they want to publish so they may be returned in customer searches results. These requests are sent to the Unicorn Approvals service for validation.
-A core component of Unicorn Web are the Lambda functions which are responsible with completing API Gateway requests to:
+Lambda functions handle API Gateway requests to:
-- search approved property listings
-This function interacts with DynamoDB table to retrieve property listings marked as `APPROVED`. The API Gateway implementation and lambda code support multiple types of search patterns, and allow searching by city, street, or house number.
+- Search approved property listings: The **Search function** retrieves property listings marked as APPROVED from the DynamoDB table using multiple search patterns.
-- request approval of property listing
-This function sends an event to EventBridge requesting an approval for a property listing specified in the payload sent from client
+- Request property listing approval: The **Request Approval function** sends an EventBridge event requesting approval for a property listing specified in the payload.
-- publication approved function
-There is also a lambda function responsible for receiving any "Approval Evaluation Completed" events from EventBridge. This function writes the evaluation result to DynamoDB table.
+- Process approved listings: The **Publication Evaluation Event Handler function** processes `PublicationEvaluationCompleted` events from the Unicorn Approvals service and writes the evaluation result to the DynamoDB table.
### Testing the APIs
@@ -24,10 +21,10 @@ export API=`aws cloudformation describe-stacks --stack-name uni-prop-local-web -
curl --location --request POST "${API}request_approval" \
--header 'Content-Type: application/json' \
---data-raw '{"property_id": "usa/anytown/main-street/111"}'
+--data-raw '{"PropertyId": "usa/anytown/main-street/111"}'
curl -X POST ${API_URL}request_approval \
-H 'Content-Type: application/json' \
- -d '{"property_id":"usa/anytown/main-street/111"}' | jq
+ -d '{"PropertyId":"usa/anytown/main-street/111"}' | jq
```
diff --git a/unicorn_web/PropertyFunctions/pom.xml b/unicorn_web/SearchService/pom.xml
similarity index 83%
rename from unicorn_web/PropertyFunctions/pom.xml
rename to unicorn_web/SearchService/pom.xml
index 2142889..b627290 100644
--- a/unicorn_web/PropertyFunctions/pom.xml
+++ b/unicorn_web/SearchService/pom.xml
@@ -1,23 +1,22 @@
4.0.0
- property
- PropertyWeb
+ search
+ SearchService
1.0
jar
- Property web module for java reference architecture.
+ Unicorn Web Search Service module
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,42 @@
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
+ Common
+ 1.0
+ compile
@@ -164,7 +169,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- 2.22.2
+ 3.5.3
handler
@@ -174,7 +179,7 @@
org.apache.maven.plugins
maven-shade-plugin
- 3.2.4
+ 3.6.0
@@ -187,12 +192,10 @@
- dev.aspectj
+ org.codehaus.mojo
aspectj-maven-plugin
- 1.13.1
+ 1.15.0
- 17
- 17
17
@@ -216,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
new file mode 100644
index 0000000..883f512
--- /dev/null
+++ b/unicorn_web/SearchService/src/main/java/search/PropertySearchFunction.java
@@ -0,0 +1,160 @@
+package search;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import dao.Property;
+import software.amazon.awssdk.core.async.SdkPublisher;
+import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable;
+import software.amazon.awssdk.enhanced.dynamodb.DynamoDbEnhancedAsyncClient;
+import software.amazon.awssdk.enhanced.dynamodb.Expression;
+import software.amazon.awssdk.enhanced.dynamodb.Key;
+import software.amazon.awssdk.enhanced.dynamodb.TableSchema;
+import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional;
+import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest;
+import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
+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.lambda.powertools.metrics.Metrics;
+import software.amazon.lambda.powertools.tracing.Tracing;
+import software.amazon.lambda.powertools.logging.CorrelationIdPathConstants;
+import software.amazon.lambda.powertools.logging.Logging;
+
+/**
+ * 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";
+
+ private final String tableName = System.getenv("DYNAMODB_TABLE");
+ private final DynamoDbAsyncTable propertyTable;
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ public PropertySearchFunction() {
+ DynamoDbAsyncClient dynamodbClient = DynamoDbAsyncClient.builder()
+ .httpClientBuilder(NettyNioAsyncHttpClient.builder())
+ .build();
+
+ DynamoDbEnhancedAsyncClient enhancedClient = DynamoDbEnhancedAsyncClient.builder()
+ .dynamoDbClient(dynamodbClient)
+ .build();
+
+ 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");
+ }
+
+ 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");
+ }
+ }
+
+ 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}":
+ return null;
+ case "/search/{country}/{city}/{street}":
+ return pathParams.get("street").replace(' ', '-').toLowerCase();
+ case "/properties/{country}/{city}/{street}/{number}":
+ return (pathParams.get("street") + "#" + pathParams.get("number")).replace(' ', '-').toLowerCase();
+ default:
+ throw new IllegalArgumentException("Unsupported resource path: " + resource);
+ }
+ }
+
+ private APIGatewayProxyResponseEvent createSuccessResponse(String body) {
+ return new APIGatewayProxyResponseEvent()
+ .withStatusCode(200)
+ .withHeaders(Map.of("Content-Type", CONTENT_TYPE))
+ .withBody(body);
+ }
+
+ 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);
+ }
+
+ private List queryTable(String partitionKey, String sortKey) throws Exception {
+ if (partitionKey == null) {
+ throw new IllegalArgumentException("Partition key cannot be null");
+ }
+
+ 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);
+ }
+
+ 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) {
+ logger.error("Error querying DynamoDB", e);
+ throw new Exception("Database query failed: " + e.getMessage());
+ }
+ }
+}
diff --git a/unicorn_web/SearchService/src/main/resources/log4j2.xml b/unicorn_web/SearchService/src/main/resources/log4j2.xml
new file mode 100644
index 0000000..e4b9c1a
--- /dev/null
+++ b/unicorn_web/SearchService/src/main/resources/log4j2.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/unicorn_web/SearchService/tests/events/eventbridge/put_event_publication_evaluation_completed.json b/unicorn_web/SearchService/tests/events/eventbridge/put_event_publication_evaluation_completed.json
new file mode 100644
index 0000000..c022083
--- /dev/null
+++ b/unicorn_web/SearchService/tests/events/eventbridge/put_event_publication_evaluation_completed.json
@@ -0,0 +1,8 @@
+[
+ {
+ "Source": "unicorn.approvals",
+ "Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"evaluation_result\": \"APPROVED\"}",
+ "DetailType": "PublicationEvaluationCompleted",
+ "EventBusName": "UnicornWebBus-local"
+ }
+]
diff --git a/unicorn_web/integration/subscriptions.yaml b/unicorn_web/integration/subscriptions.yaml
index d455cea..a5033d1 100644
--- a/unicorn_web/integration/subscriptions.yaml
+++ b/unicorn_web/integration/subscriptions.yaml
@@ -13,17 +13,17 @@ Parameters:
- prod
Resources:
- #### UNICORN PROPERTIES EVENT SUBSCRIPTIONS
+ #### UNICORN APPROVALS EVENT SUBSCRIPTIONS
PublicationEvaluationCompletedSubscriptionRule:
Type: AWS::Events::Rule
Properties:
Name: unicorn.web-PublicationEvaluationCompleted
Description: PublicationEvaluationCompleted subscription
EventBusName:
- Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornPropertiesEventBusArn}}"
+ Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornApprovalsEventBusArn}}"
EventPattern:
source:
- - "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}"
+ - "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}"
detail-type:
- PublicationEvaluationCompleted
State: ENABLED
@@ -32,11 +32,11 @@ Resources:
Arn:
Fn::Sub: "{{resolve:ssm:/uni-prop/${Stage}/UnicornWebEventBusArn}}"
RoleArn:
- Fn::GetAtt: [ UnicornPropertiesEventBusToUnicornWebEventBusRole, Arn ]
+ Fn::GetAtt: [ UnicornApprovalsEventBusToUnicornWebEventBusRole, Arn ]
# This IAM role allows EventBridge to assume the permissions necessary to send events
# from the publishing event bus, to the subscribing event bus (UnicornWebEventBusArn)
- UnicornPropertiesEventBusToUnicornWebEventBusRole:
+ UnicornApprovalsEventBusToUnicornWebEventBusRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
diff --git a/unicorn_web/pom.xml b/unicorn_web/pom.xml
new file mode 100644
index 0000000..d6e816a
--- /dev/null
+++ b/unicorn_web/pom.xml
@@ -0,0 +1,26 @@
+
+
+ 4.0.0
+
+ unicorn.web
+ unicorn-web-parent
+ 1.0
+ pom
+
+ Unicorn Web Services
+
+
+ 17
+ 17
+ UTF-8
+
+
+
+ Common
+ PublicationManagerService
+ SearchService
+
+
+
\ No newline at end of file
diff --git a/unicorn_web/template.yaml b/unicorn_web/template.yaml
index 6d36831..e510753 100644
--- a/unicorn_web/template.yaml
+++ b/unicorn_web/template.yaml
@@ -15,7 +15,6 @@ Metadata:
- WS2001 # Rule disabled because check does not support !ToJsonString transform
- ES1001 # Rule disabled because our Lambda functions don't need DestinationConfig.OnFailure
- W3002 # Rule disabled as nested templates are being packaged
- - E3030 # Rule disabled due to using cfn-lint-serverless rules v0.3
Parameters:
Stage:
@@ -90,8 +89,8 @@ Resources:
SearchFunction:
Type: AWS::Serverless::Function
Properties:
- CodeUri: PropertyFunctions
- Handler: property.search.PropertySearchFunction::handleRequest
+ CodeUri: SearchService
+ Handler: search.PropertySearchFunction::handleRequest
Policies:
- DynamoDBReadPolicy:
TableName: !Ref WebTable
@@ -109,8 +108,8 @@ Resources:
RequestApprovalFunction:
Type: AWS::Serverless::Function
Properties:
- CodeUri: PropertyFunctions
- Handler: property.requestapproval.RequestApprovalFunction::handleRequest
+ CodeUri: PublicationManagerService
+ Handler: publicationmanager.RequestApprovalFunction::handleRequest
Policies:
- EventBridgePutEventsPolicy:
EventBusName: !Ref UnicornWebEventBus
@@ -139,8 +138,8 @@ Resources:
PublicationApprovedEventHandlerFunction:
Type: AWS::Serverless::Function
Properties:
- CodeUri: PropertyFunctions
- Handler: property.requestapproval.PublicationApprovedFunction::handleRequest
+ CodeUri: PublicationManagerService
+ Handler: publicationmanager.PublicationApprovedEventHandler::handleRequest
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref WebTable
@@ -156,7 +155,7 @@ Resources:
EventBusName: !Ref UnicornWebEventBus
Pattern:
source:
- - "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}"
+ - "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}"
detail-type:
- PublicationEvaluationCompleted
@@ -361,7 +360,7 @@ Resources:
- !Ref AWS::AccountId
source:
- "{{resolve:ssm:/uni-prop/UnicornContractsNamespace}}"
- - "{{resolve:ssm:/uni-prop/UnicornPropertiesNamespace}}"
+ - "{{resolve:ssm:/uni-prop/UnicornApprovalsNamespace}}"
- "{{resolve:ssm:/uni-prop/UnicornWebNamespace}}"
State: ENABLED #You may want to disable this rule in production
Targets:
@@ -461,7 +460,7 @@ Outputs:
Description: "GET request to list all properties in a given street"
Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/search/{country}/{city}/{street}"
ApiPropertyApproval:
- Description: "POST request to add a property to the database"
+ Description: "POST request approval to allow property to be searchable"
Value: !Sub "https://${UnicornWebApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Stage}/request_approval"
ApiPropertyDetails:
Description: "GET request to get the full details of a single property"
diff --git a/unicorn_web/tests/events/eventbridge/put_event_publication_evaluation_completed.json b/unicorn_web/tests/events/eventbridge/put_event_publication_evaluation_completed.json
index c4f3c7f..c022083 100644
--- a/unicorn_web/tests/events/eventbridge/put_event_publication_evaluation_completed.json
+++ b/unicorn_web/tests/events/eventbridge/put_event_publication_evaluation_completed.json
@@ -1,6 +1,6 @@
[
{
- "Source": "unicorn.properties",
+ "Source": "unicorn.approvals",
"Detail": "{\"property_id\":\"usa/anytown/main-street/111\",\"evaluation_result\": \"APPROVED\"}",
"DetailType": "PublicationEvaluationCompleted",
"EventBusName": "UnicornWebBus-local"