diff --git a/.gitignore b/.gitignore
index 96386e40..134dc599 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,7 @@ infrastructure/templates/pgt-proxy/docker_image_builder_*
infrastructure/templates/application/docker_image_builder_*
application/frontend/node_modules
application/frontend/run.sh
+application/java-credit-card-product-service/target
+*.jar*
+*.class
+*.lst
diff --git a/README.MD b/README.MD
index 6c2b7a46..73870049 100644
--- a/README.MD
+++ b/README.MD
@@ -1,11 +1,90 @@
# Notes
+This repo is for the Ambar Event Sourcing / Microservices courses.
+You can use this repo to deploy a real world credit card application which leverages both event sourcing and microservices
+to create a larger application. Note that this is an academic example, implemented in multiple languages and with multiple
+components - you should not expect to deeply understand everything in all the languages.
+
## Preparation Steps
+These steps will be performed with your instructor during the first coding session of your course, and will allow you to
+deploy this repo into a cloud environment running on Google Cloud Platform (GCP)
+
1. Fork this repository
2. Create two GitHub action secrets (provided by your course instructor) as follows:
```
STATE_MANAGEMENT_BASE64=some_string_here
CREDENTIALS_BASE64=some_string_here
```
-3. Make a blank commit, and push it to GitHub.
+**N.B. Make sure to enter the entire string for each secret, with no quotes, and no newlines!**
+3. Make a trivial commit (such as updating this readme), and push it to GitHub.
+
+## Extending this program
+
+Our sample application attempts to model a financial institution which offers different credit cards (products). We have
+already modeled some services of the application - some multiple times in multiple languages! To get started, you should
+look at one of the already modeled services like the Credit-Card-Product service which is implemented in multiple languages
+and try to follow along in one of the languages you are familiar with (php, java). Reimplement the service in your language
+of choice, or add any missing features to the module.
+
+Once you have an understanding of the pattern as it works in your chosen implementation language, feel free to implement
+another service of the application. If you feel inclined, open a pull request to the Ambar repo, and we will review and merge
+it and extend the frontend application to leverage the capabilities!
+
+### Event Sourcing Additional Context / Reminders
+
+A quick reminder on Event Sourcing
+
+Event Sourcing is the inversion of state in an application. Instead of directly storing state in a record and performing CRUD
+operations on those records, we record series of events that describe what happened in our application and derive state from
+one or more of those events. This gives us a direct historical event sequence that we can use to build up state in many ways
+and to even 'time travel' to see what state was in the past, without the need for audit tables or trying to deal with the
+dual write problem.
+
+Note: This does not mean we do not directly store state in event sourcing! We can still distill information from our events
+to build useful models about the state of our application and its data. This is where projections and read models come in.
+
+An easy way to organize event sourcing conceptually, and in code, is by leveraging the Command Query Responsibility Separation
+pattern (CQRS) where we create distinct paths for our write (record events) and read (leverage events / state) actions.
+
+#### Terms refresher
+
+* **Command**: A request for something to happen in our application.
+ * E.G. Making a new credit card product for customers to apply for.
+* **Event**: A recording of something significant happening in our application containing things like who, what, and when.
+Events should be small and precise, and are written sequentially to an immutable log. Never change events or insert new ones
+except at the tail of the log. Events should belong to short-lived processes, such as handling a request or system notification.
+ * E.G. Activating a credit card product so customers can apply for it
+(Who: The product ID, What: Make it active, When: when was the event recorded)
+* **Aggregate**: Information Model built from a set of related events used to model a process such has handling a command / request.
+They should be small, and leverage event ordering to create a minified state for process handling.
+ * E.G. A Product Aggregate to model a credit card which is offered in our service. We can leverage the aggregate to determine
+if a product is already available (active) or not, to determine if a command (request) is valid.
+* **Projection**: Projection leverages building up some useful state from a filtered set of events and storing the derived
+state into a projection database. These projections can then be leveraged during command validations when determining if
+a command should be accepted (event written) or not.
+* **Query**: Queries are the read side of our application, and leverage the models created by projections to retrieve information
+store about the state of our application.
+
+```
+Comamnd (Read from EventStore/ReadModelStore, write back to EventStore)
+ -> [Command (Request)] -> [Build some state (EventStore Aggregate)] -> [Perform Validations (Aggregate, ReadModels)] ->
+ [Record whatever happened (New Event)] -> [EventStore]
+
+(Strongly Consistent) ^
+------------------------------------------------------------------------------------------------------------------------
+(Eventually Consistent) v
+
+Reaction (Event from EventStore, write back to EventStore)
+ -> [EventStore(Event)] -> [Build some state (EventStore Aggregate)] -> [Perform Validations (Aggregate, ReadModels)] ->
+ [Record whatever happened (New Event)] -> [EventStore]
+
+
+Projections (Read from ReadModelStore, write to ReadModelStore)
+ -> [EventStore(Event)] -> [Build some state] -> [Project some interesting state] -> [ReadModelStore]
+
+Querys (Read Commands) (Read from ReadModelStore)
+ -> [ReadModelStore (Modeled State)]
+```
+
+### Microservices Additional Context / Reminders
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/.dockerignore b/application/java-credit-card-product-service/.dockerignore
new file mode 100644
index 00000000..07c3a32b
--- /dev/null
+++ b/application/java-credit-card-product-service/.dockerignore
@@ -0,0 +1,2 @@
+code/vendor
+code/development-environment
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/CreditCardProduct.iml b/application/java-credit-card-product-service/CreditCardProduct.iml
new file mode 100644
index 00000000..1e8535b5
--- /dev/null
+++ b/application/java-credit-card-product-service/CreditCardProduct.iml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/Dockerfile b/application/java-credit-card-product-service/Dockerfile
new file mode 100644
index 00000000..b11b2143
--- /dev/null
+++ b/application/java-credit-card-product-service/Dockerfile
@@ -0,0 +1,30 @@
+# Step 1: Use Maven image to build the application
+FROM maven:3.9.5-eclipse-temurin-21 AS build
+
+# Set the working directory inside the container
+WORKDIR /app
+
+# Copy the pom.xml and download dependencies (layer caching)
+COPY pom.xml .
+RUN mvn dependency:go-offline
+
+# Copy the rest of the application source code
+COPY src ./src
+
+# Build the Spring Boot application
+RUN mvn clean package -DskipTests
+
+# Step 2: Use JDK 21 runtime image to run the application
+FROM eclipse-temurin:21-jdk
+
+# Set the working directory for the runtime container
+WORKDIR /app
+
+# Copy the JAR file from the build image to the runtime image
+COPY --from=build /app/target/*.jar app.jar
+
+# Expose port 8080 (Spring Boot default port)
+EXPOSE 8080
+
+# Set the entry point to run the application
+ENTRYPOINT ["java", "-jar", "app.jar"]
diff --git a/application/java-credit-card-product-service/development-environment/.gitignore b/application/java-credit-card-product-service/development-environment/.gitignore
new file mode 100644
index 00000000..6320cd24
--- /dev/null
+++ b/application/java-credit-card-product-service/development-environment/.gitignore
@@ -0,0 +1 @@
+data
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/development-environment/dev-commands.sh b/application/java-credit-card-product-service/development-environment/dev-commands.sh
new file mode 100644
index 00000000..9d9f7b7b
--- /dev/null
+++ b/application/java-credit-card-product-service/development-environment/dev-commands.sh
@@ -0,0 +1,41 @@
+endpoint="localhost:8080"
+
+# To create the Starter card
+curl -X POST "${endpoint}/api/v1/credit_card_product/product" \
+-H "Content-Type: application/json" \
+-d '{
+ "productIdentifierForAggregateIdHash": "STARTER_CREDIT_CARD",
+ "name": "Starter",
+ "interestInBasisPoints": 1200,
+ "annualFeeInCents": 5000,
+ "paymentCycle": "monthly",
+ "creditLimitInCents": 50000,
+ "maxBalanceTransferAllowedInCents": 0,
+ "reward": "none",
+ "cardBackgroundHex": "#7fffd4"
+}'
+
+# To create the Platinum card
+curl -X POST "${endpoint}/api/v1/credit_card_product/product" \
+-H "Content-Type: application/json" \
+-d '{
+ "productIdentifierForAggregateIdHash": "PLATINUM_CREDIT_CARD",
+ "name": "Platinum",
+ "interestInBasisPoints": 300,
+ "annualFeeInCents": 50000,
+ "paymentCycle": "monthly",
+ "creditLimitInCents": 500000,
+ "maxBalanceTransferAllowedInCents": 100000,
+ "reward": "points",
+ "cardBackgroundHex": "#E5E4E2"
+}'
+
+# To list the current card products
+curl -X POST "${endpoint}/api/v1/credit_card_product/product/list-items" | jq .
+
+productId=""
+# Activate a product
+curl -X POST "${endpoint}/api/v1/credit_card_product/product/activate/${productId}"
+
+# Deactivate a product
+curl -X POST "${endpoint}/api/v1/credit_card_product/product/deactivate/${productId}"
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/development-environment/dev_a_start.sh b/application/java-credit-card-product-service/development-environment/dev_a_start.sh
new file mode 100755
index 00000000..6b56cd94
--- /dev/null
+++ b/application/java-credit-card-product-service/development-environment/dev_a_start.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+set -e
+
+#docker run --rm -v $(pwd):/app composer:2.2.24 install --ignore-platform-reqs
+docker compose down
+docker compose up -d --build --force-recreate
+sleep 5
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/development-environment/dev_z_shutdown.sh b/application/java-credit-card-product-service/development-environment/dev_z_shutdown.sh
new file mode 100755
index 00000000..64432589
--- /dev/null
+++ b/application/java-credit-card-product-service/development-environment/dev_z_shutdown.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+set -e
+
+docker compose down
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/development-environment/docker-compose.yml b/application/java-credit-card-product-service/development-environment/docker-compose.yml
new file mode 100644
index 00000000..9ede4856
--- /dev/null
+++ b/application/java-credit-card-product-service/development-environment/docker-compose.yml
@@ -0,0 +1,84 @@
+services:
+
+ java-credit-card-product:
+ container_name: java-credit-card-product
+ build:
+ context: ./../
+ volumes:
+ - ./../:/srv
+ restart: always
+ environment:
+ EVENT_STORE_HOST: "172.30.0.102"
+ EVENT_STORE_PORT: 5432
+ EVENT_STORE_DATABASE_NAME: "my_es_database"
+ EVENT_STORE_USER: "my_es_username"
+ EVENT_STORE_PASSWORD: "my_es_password"
+ EVENT_STORE_CREATE_TABLE_WITH_NAME: "event_store"
+ EVENT_STORE_CREATE_REPLICATION_USER_WITH_USERNAME: "replication_username_test"
+ EVENT_STORE_CREATE_REPLICATION_USER_WITH_PASSWORD: "replication_password_test"
+ EVENT_STORE_CREATE_REPLICATION_PUBLICATION: "replication_publication_test"
+ MONGODB_PROJECTION_HOST: "172.30.0.103"
+ MONGODB_PROJECTION_PORT: 27017
+ MONGODB_PROJECTION_AUTHENTICATION_DATABASE: "admin"
+ MONGODB_PROJECTION_DATABASE_NAME: "projections"
+ MONGODB_PROJECTION_DATABASE_USERNAME: "my_mongo_username"
+ MONGODB_PROJECTION_DATABASE_PASSWORD: "my_mongo_password"
+ SESSION_TOKENS_EXPIRE_AFTER_SECONDS: 3600
+ depends_on:
+ - pg-event-store
+ - mongo-projection-reaction
+ networks:
+ TestNetwork:
+ ipv4_address: 172.30.0.101
+ deploy:
+ resources:
+ limits:
+ cpus: '0.500'
+ memory: 1024M
+
+ pg-event-store:
+ build:
+ context:
+ ./pg-event-store
+ container_name: pg-event-store
+ restart: always
+ volumes:
+ - ./data/event-store/pg-data:/var/lib/postgresql/data
+ environment:
+ POSTGRES_USER: my_es_username
+ POSTGRES_DB: my_es_database
+ POSTGRES_PASSWORD: my_es_password
+ command: >
+ postgres -c wal_level=logical
+ -c ssl=on
+ -c ssl_cert_file=/var/lib/postgresql/certs/server.crt
+ -c ssl_key_file=/var/lib/postgresql/certs/server.key
+ expose:
+ - 5432
+ networks:
+ TestNetwork:
+ ipv4_address: 172.30.0.102
+
+ mongo-projection-reaction:
+ build:
+ context:
+ ./mongo-projection-reaction
+ container_name: mongo-projection-reaction
+ restart: always
+ environment:
+ MONGO_INITDB_ROOT_USERNAME: my_mongo_username
+ MONGO_INITDB_ROOT_PASSWORD: my_mongo_password
+ volumes:
+ - ./data/mongo-projection-reaction/db-data:/data/db
+ expose:
+ - 27017
+ networks:
+ TestNetwork:
+ ipv4_address: 172.30.0.103
+
+networks:
+ TestNetwork:
+ driver: bridge
+ ipam:
+ config:
+ - subnet: 172.30.0.0/24
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/development-environment/mongo-projection-reaction/Dockerfile b/application/java-credit-card-product-service/development-environment/mongo-projection-reaction/Dockerfile
new file mode 100644
index 00000000..d0605658
--- /dev/null
+++ b/application/java-credit-card-product-service/development-environment/mongo-projection-reaction/Dockerfile
@@ -0,0 +1 @@
+FROM mongo:7.0.14
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/development-environment/pg-event-store/Dockerfile b/application/java-credit-card-product-service/development-environment/pg-event-store/Dockerfile
new file mode 100644
index 00000000..0e512219
--- /dev/null
+++ b/application/java-credit-card-product-service/development-environment/pg-event-store/Dockerfile
@@ -0,0 +1,9 @@
+FROM postgres:16.4
+
+RUN mkdir -p /var/lib/postgresql/certs
+RUN openssl genrsa -out /var/lib/postgresql/certs/server.key 2048
+RUN chmod 600 /var/lib/postgresql/certs/server.key
+RUN openssl req -new -key /var/lib/postgresql/certs/server.key -out /var/lib/postgresql/certs/server.csr -subj "/C=GB/ST=London/L=London/O=SnakeOil/OU=Org/CN=snake-oil.oil"
+RUN openssl x509 -req -in /var/lib/postgresql/certs/server.csr -signkey /var/lib/postgresql/certs/server.key -out /var/lib/postgresql/certs/server.crt -days 36500
+RUN chown 999 /var/lib/postgresql/certs
+RUN chown 999 /var/lib/postgresql/certs -Rf
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/pom.xml b/application/java-credit-card-product-service/pom.xml
new file mode 100644
index 00000000..fb24cdbe
--- /dev/null
+++ b/application/java-credit-card-product-service/pom.xml
@@ -0,0 +1,79 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.4
+
+
+ cloud.ambar
+ CreditCardProduct
+ 0.0.1-SNAPSHOT
+ CreditCardProduct
+ Ambar Credit Card Product service for course demo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 21
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-mongodb
+
+
+
+ org.postgresql
+ postgresql
+
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+ compile
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${project.parent.version}
+
+
+
+
+
diff --git a/application/java-credit-card-product-service/src/commands.sh b/application/java-credit-card-product-service/src/commands.sh
new file mode 100644
index 00000000..98c2cc34
--- /dev/null
+++ b/application/java-credit-card-product-service/src/commands.sh
@@ -0,0 +1,41 @@
+endpoint="your-endpoint-here"
+
+# To create the Starter card
+curl -X POST "${endpoint}/api/v1/credit_card_product/product" \
+-H "Content-Type: application/json" \
+-d '{
+ "productIdentifierForAggregateIdHash": "STARTER_CREDIT_CARD",
+ "name": "Starter",
+ "interestInBasisPoints": 1200,
+ "annualFeeInCents": 5000,
+ "paymentCycle": "monthly",
+ "creditLimitInCents": 50000,
+ "maxBalanceTransferAllowedInCents": 0,
+ "reward": "none",
+ "cardBackgroundHex": "#7fffd4"
+}'
+
+# To create the Platinum card
+curl -X POST "${endpoint}/api/v1/credit_card_product/product" \
+-H "Content-Type: application/json" \
+-d '{
+ "productIdentifierForAggregateIdHash": "PLATINUM_CREDIT_CARD",
+ "name": "Platinum",
+ "interestInBasisPoints": 300,
+ "annualFeeInCents": 50000,
+ "paymentCycle": "monthly",
+ "creditLimitInCents": 500000,
+ "maxBalanceTransferAllowedInCents": 100000,
+ "reward": "points",
+ "cardBackgroundHex": "#E5E4E2"
+}'
+
+# To list the current card products
+curl -X POST "${endpoint}/api/v1/credit_card_product/product/list-items" | jq .
+
+productId=""
+# Activate a product
+curl -X POST "${endpoint}/api/v1/credit_card_product/product/activate/${productId}"
+
+# Deactivate a product
+curl -X POST "${endpoint}/api/v1/credit_card_product/product/deactivate/${productId}"
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/Application.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/Application.java
new file mode 100644
index 00000000..8106b0e0
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/Application.java
@@ -0,0 +1,26 @@
+package cloud.ambar;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+ private static final Logger log = LogManager.getLogger(Application.class);
+
+
+ /**
+ * Spring Application which will run a webserver hosting our EventSourcing java application.
+ * On startup, this application will
+ * 1. Validate any tables for EventStorage / Projection & Reaction are created
+ * 2. Populate with any sample values (Sample credit card products)
+ * 3. Start the application and make it available.
+ * @param args
+ */
+ public static void main(String[] args) {
+ log.info("Starting up main application");
+
+ SpringApplication.run(Application.class, args);
+ }
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/aggregate/Aggregate.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/aggregate/Aggregate.java
new file mode 100644
index 00000000..983b2e2e
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/aggregate/Aggregate.java
@@ -0,0 +1,7 @@
+package cloud.ambar.creditCardProduct.aggregate;
+
+import cloud.ambar.creditCardProduct.events.Event;
+
+public interface Aggregate {
+ void transform(final Event event);
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/aggregate/AggregateTraits.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/aggregate/AggregateTraits.java
new file mode 100644
index 00000000..dd6de11d
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/aggregate/AggregateTraits.java
@@ -0,0 +1,39 @@
+package cloud.ambar.creditCardProduct.aggregate;
+
+import cloud.ambar.creditCardProduct.controllers.QueryController;
+import cloud.ambar.creditCardProduct.exceptions.InvalidEventException;
+import cloud.ambar.creditCardProduct.events.Event;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.Objects;
+
+@Data
+@NoArgsConstructor
+public abstract class AggregateTraits implements Aggregate {
+ private static final Logger log = LogManager.getLogger(AggregateTraits.class);
+
+ private String aggregateId;
+ private long aggregateVersion;
+
+ public AggregateTraits(String aggregateId, long aggregateVersion) {
+ this.aggregateId = aggregateId;
+ this.aggregateVersion = aggregateVersion;
+ }
+
+ public void apply(final Event event) {
+ log.info("Applying Event: " + event);
+ this.validateEvent(event);
+ transform(event);
+
+ this.aggregateVersion++;
+ }
+
+ private void validateEvent(final Event event) {
+ log.info("Validating Event: " + event);
+ if (Objects.isNull(event) || !event.getAggregateId().equals(this.aggregateId))
+ throw new InvalidEventException(event.toString());
+ }
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/aggregate/ProductAggregate.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/aggregate/ProductAggregate.java
new file mode 100644
index 00000000..e2da5afc
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/aggregate/ProductAggregate.java
@@ -0,0 +1,73 @@
+package cloud.ambar.creditCardProduct.aggregate;
+
+import cloud.ambar.creditCardProduct.events.Event;
+import cloud.ambar.creditCardProduct.events.ProductActivatedEvent;
+import cloud.ambar.creditCardProduct.events.ProductDeactivatedEvent;
+import cloud.ambar.creditCardProduct.events.ProductDefinedEvent;
+import cloud.ambar.creditCardProduct.data.models.PaymentCycle;
+import cloud.ambar.creditCardProduct.data.models.RewardsType;
+import cloud.ambar.creditCardProduct.exceptions.InvalidEventException;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@NoArgsConstructor
+public class ProductAggregate extends AggregateTraits {
+ private static final Logger log = LogManager.getLogger(ProductAggregate.class);
+ private String name;
+ private int interestInBasisPoints;
+ private int annualFeeInCents;
+ private String paymentCycle;
+ private int creditLimitInCents;
+ private int maxBalanceTransferAllowedInCents;
+ private String reward;
+ private String cardBackgroundHex;
+ private boolean active;
+
+ public ProductAggregate(String aggregateId, long aggregateVersion) {
+ super(aggregateId, aggregateVersion);
+ }
+
+ @Override
+ public void transform(Event event) {
+ switch(event.getEventName()) {
+ case ProductDefinedEvent.EVENT_NAME -> {
+ log.info("Transforming aggregate for ProductDefinedEvent");
+ ObjectMapper om = new ObjectMapper();
+ try {
+ ProductDefinedEvent definition = om.readValue(event.getData(), ProductDefinedEvent.class);
+ this.setAggregateId(event.getAggregateId());
+ this.setAggregateVersion(event.getVersion());
+ this.setName(definition.getName());
+ this.setInterestInBasisPoints(definition.getInterestInBasisPoints());
+ this.setAnnualFeeInCents(definition.getAnnualFeeInCents());
+ this.setPaymentCycle(definition.getPaymentCycle());
+ this.setCreditLimitInCents(definition.getCreditLimitInCents());
+ this.setMaxBalanceTransferAllowedInCents(definition.getMaxBalanceTransferAllowedInCents());
+ this.setReward(definition.getReward());
+ this.setCardBackgroundHex(definition.getCardBackgroundHex());
+ this.setActive(false);
+ } catch (JsonProcessingException e) {
+ log.error("Error creating initial product definition from event!");
+ throw new InvalidEventException("Error processing ProductDefinedEvent");
+ }
+ }
+ case ProductActivatedEvent.EVENT_NAME -> {
+ log.info("Transforming aggregate for ProductActivatedEvent");
+ this.active = true;
+ }
+ case ProductDeactivatedEvent.EVENT_NAME -> {
+ log.info("Transforming aggregate for ProductDeactivatedEvent");
+ this.active = false;
+ }
+ }
+ }
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/aggregate/README.md b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/aggregate/README.md
new file mode 100644
index 00000000..39c7dfcb
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/aggregate/README.md
@@ -0,0 +1,3 @@
+# Aggregates
+
+Aggregates are
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commandHandlers/CommandService.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commandHandlers/CommandService.java
new file mode 100644
index 00000000..64853f70
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commandHandlers/CommandService.java
@@ -0,0 +1,12 @@
+package cloud.ambar.creditCardProduct.commandHandlers;
+
+import cloud.ambar.creditCardProduct.commands.DefineProductCommand;
+import cloud.ambar.creditCardProduct.commands.ProductActivatedCommand;
+import cloud.ambar.creditCardProduct.commands.ProductDeactivatedCommand;
+import com.fasterxml.jackson.core.JsonProcessingException;
+
+public interface CommandService {
+ void handle(DefineProductCommand command) throws JsonProcessingException;
+ void handle(ProductActivatedCommand command) throws JsonProcessingException;
+ void handle(ProductDeactivatedCommand command) throws JsonProcessingException;
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commandHandlers/ProductCommandService.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commandHandlers/ProductCommandService.java
new file mode 100644
index 00000000..64fe6681
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commandHandlers/ProductCommandService.java
@@ -0,0 +1,194 @@
+package cloud.ambar.creditCardProduct.commandHandlers;
+
+import cloud.ambar.creditCardProduct.aggregate.ProductAggregate;
+import cloud.ambar.creditCardProduct.exceptions.InvalidEventException;
+import cloud.ambar.creditCardProduct.exceptions.InvalidPaymentCycleException;
+import cloud.ambar.creditCardProduct.exceptions.InvalidRewardException;
+import cloud.ambar.creditCardProduct.events.Event;
+import cloud.ambar.creditCardProduct.commands.DefineProductCommand;
+import cloud.ambar.creditCardProduct.commands.ProductActivatedCommand;
+import cloud.ambar.creditCardProduct.commands.ProductDeactivatedCommand;
+import cloud.ambar.creditCardProduct.data.postgre.EventRepository;
+import cloud.ambar.creditCardProduct.events.ProductActivatedEvent;
+import cloud.ambar.creditCardProduct.events.ProductDeactivatedEvent;
+import cloud.ambar.creditCardProduct.events.ProductDefinedEvent;
+import cloud.ambar.creditCardProduct.data.models.PaymentCycle;
+import cloud.ambar.creditCardProduct.data.models.RewardsType;
+import cloud.ambar.creditCardProduct.exceptions.NoSuchProductException;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+@RequiredArgsConstructor
+@Service
+@Transactional
+public class ProductCommandService implements CommandService {
+ private static final Logger log = LogManager.getLogger(ProductCommandService.class);
+
+ private final EventRepository eventStore;
+
+ private final ObjectMapper objectMapper;
+
+ @Override
+ public void handle(DefineProductCommand command) throws JsonProcessingException {
+ log.info("Handling " + ProductDefinedEvent.EVENT_NAME + " command.");
+ final String eventId = UUID.nameUUIDFromBytes(command.getProductIdentifierForAggregateIdHash().getBytes()).toString();
+ // First part of validation is to check if this event has already been processed. We expect to create a new
+ // unique aggregate from this and subsequent events. If it is already present, then we are processing a duplicate
+ // event.
+ Optional priorEntry = eventStore.findByEventId(eventId);
+ if (priorEntry.isPresent()) {
+ log.info("Found event(s) for eventId - skipping...");
+ return;
+ }
+
+ // Next, some simple business validations. These do not rely on any read models (queries)
+ if (Arrays.stream(PaymentCycle.values()).noneMatch(p -> p.name().equalsIgnoreCase(command.getPaymentCycle()))) {
+ log.error("Invalid payment cycle was specified in command: " + command.getPaymentCycle());
+ throw new InvalidPaymentCycleException();
+ }
+
+ // ...
+ if (Arrays.stream(RewardsType.values()).noneMatch(p -> p.name().equalsIgnoreCase(command.getReward()))) {
+ log.error("Invalid reward was specified in command: " + command.getReward());
+ throw new InvalidRewardException();
+ }
+
+ // Finally, we have passed all the validations, and want to 'accept' (store) the result event. So we will create
+ // the resultant event with related details (product definition) and write this to our event store.
+ final String aggregateId = UUID.randomUUID().toString();
+ final Event event = Event.builder()
+ .eventName(ProductDefinedEvent.EVENT_NAME)
+ .eventId(eventId)
+ .correlationId(eventId)
+ .causationID(eventId)
+ .aggregateId(aggregateId)
+ .version(1)
+ .timeStamp(LocalDateTime.now())
+ .metadata("")
+ .data(objectMapper.writeValueAsString(
+ ProductDefinedEvent.builder()
+ .name(command.getName())
+ .interestInBasisPoints(command.getInterestInBasisPoints())
+ .annualFeeInCents(command.getAnnualFeeInCents())
+ .paymentCycle(command.getPaymentCycle())
+ .creditLimitInCents(command.getCreditLimitInCents())
+ .maxBalanceTransferAllowedInCents(command.getMaxBalanceTransferAllowedInCents())
+ .reward(command.getReward())
+ .cardBackgroundHex(command.getCardBackgroundHex())
+ .build()
+ ))
+ .build();
+
+ log.info("Saving Event: " + objectMapper.writeValueAsString(event));
+ eventStore.save(event);
+ log.info("Successfully handled " + ProductDefinedEvent.EVENT_NAME + " command.");
+ }
+
+ @Override
+ public void handle(ProductActivatedCommand command) throws JsonProcessingException {
+ log.info("Handling " + ProductActivatedEvent.EVENT_NAME + " command.");
+ final String aggregateId = command.getId();
+
+ // 1. Hydrate the Aggregate
+ final ProductAggregate aggregate = hydrateAggregateForId(aggregateId);;
+
+ // 2. Validate the command
+ // -> card currently inactive
+ // This can be done with either a query to the projection DB (async)
+ // Or via the Aggregate (sync) for this trivial example, we will use the aggregate.
+ if (aggregate.isActive()) {
+ final String msg = "Product " + aggregateId + " is already active!";
+ throw new InvalidEventException(msg);
+ }
+ log.info("Product is currently inactive, updating to active!");
+
+ // 3. Update the aggregate (write new event to store)
+ final String eventId = UUID.randomUUID().toString();
+ final Event event = Event.builder()
+ .eventName(ProductActivatedEvent.EVENT_NAME)
+ .eventId(eventId)
+ .correlationId(aggregateId)
+ .causationID(eventId)
+ .aggregateId(aggregateId)
+ .version(aggregate.getAggregateVersion())
+ .timeStamp(LocalDateTime.now())
+ .metadata("")
+ .data(objectMapper.writeValueAsString(
+ ProductActivatedEvent.builder()
+ .aggregateId(aggregateId)
+ .build()
+ ))
+ .build();
+
+ log.info("Saving Event: " + objectMapper.writeValueAsString(event));
+ eventStore.save(event);
+ log.info("Successfully handled " + ProductDefinedEvent.EVENT_NAME + " command.");
+ }
+
+ @Override
+ public void handle(ProductDeactivatedCommand command) throws JsonProcessingException {
+ log.info("Handling " + ProductDeactivatedEvent.EVENT_NAME + " command.");
+ final String aggregateId = command.getId();
+
+ // 1. Hydrate the Aggregate
+ final ProductAggregate aggregate = hydrateAggregateForId(aggregateId);
+
+ // 2. Validate the command
+ // -> card currently inactive
+ // This can be done with either a query to the projection DB (async)
+ // Or via the Aggregate (sync) for this trivial example, we will use the aggregate.
+ if (!aggregate.isActive()) {
+ final String msg = "Product " + aggregateId + " is already inactive!";
+ throw new InvalidEventException(msg);
+ }
+ log.info("Product is currently active, updating to active!");
+
+ // 3. Update the aggregate (write new event to store)
+ final String eventId = UUID.randomUUID().toString();
+ final Event event = Event.builder()
+ .eventName(ProductDeactivatedEvent.EVENT_NAME)
+ .eventId(eventId)
+ .correlationId(aggregateId)
+ .causationID(eventId)
+ .aggregateId(aggregateId)
+ .version(aggregate.getAggregateVersion())
+ .timeStamp(LocalDateTime.now())
+ .metadata("")
+ .data(objectMapper.writeValueAsString(
+ ProductDeactivatedEvent.builder()
+ .aggregateId(aggregateId)
+ .build()
+ ))
+ .build();
+
+ log.info("Saving Event: " + objectMapper.writeValueAsString(event));
+ eventStore.save(event);
+ log.info("Successfully handled " + ProductDeactivatedEvent.EVENT_NAME + " command.");
+ }
+
+ private ProductAggregate hydrateAggregateForId(String id) {
+ final List productEvents = eventStore.findAllByAggregateId(id);
+ final ProductAggregate aggregate = new ProductAggregate(id, 0);
+ if (productEvents.isEmpty()) {
+ final String msg = "Unable to find a product with id: " + id;
+ throw new NoSuchProductException(msg);
+ }
+
+ for (Event event: productEvents) {
+ aggregate.apply(event);
+ }
+ log.info("Hydrated Aggregate: " + aggregate);
+ return aggregate;
+ }
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commandHandlers/README.md b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commandHandlers/README.md
new file mode 100644
index 00000000..25a6cd05
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commandHandlers/README.md
@@ -0,0 +1 @@
+Todo: Add details about the classes in this dir
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commands/DefineProductCommand.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commands/DefineProductCommand.java
new file mode 100644
index 00000000..b06b3ce8
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commands/DefineProductCommand.java
@@ -0,0 +1,21 @@
+package cloud.ambar.creditCardProduct.commands;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class DefineProductCommand {
+ private String productIdentifierForAggregateIdHash;
+ private String name;
+ private int interestInBasisPoints;
+ private int annualFeeInCents;
+ private String paymentCycle;
+ private int creditLimitInCents;
+ private int maxBalanceTransferAllowedInCents;
+ private String reward;
+ private String cardBackgroundHex;
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commands/ProductActivatedCommand.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commands/ProductActivatedCommand.java
new file mode 100644
index 00000000..16a764d6
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commands/ProductActivatedCommand.java
@@ -0,0 +1,12 @@
+package cloud.ambar.creditCardProduct.commands;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductActivatedCommand {
+ private String id;
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commands/ProductDeactivatedCommand.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commands/ProductDeactivatedCommand.java
new file mode 100644
index 00000000..833af53f
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commands/ProductDeactivatedCommand.java
@@ -0,0 +1,12 @@
+package cloud.ambar.creditCardProduct.commands;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductDeactivatedCommand {
+ private String id;
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commands/README.md b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commands/README.md
new file mode 100644
index 00000000..25a6cd05
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/commands/README.md
@@ -0,0 +1 @@
+Todo: Add details about the classes in this dir
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/controllers/CommandController.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/controllers/CommandController.java
new file mode 100644
index 00000000..50a48c47
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/controllers/CommandController.java
@@ -0,0 +1,61 @@
+package cloud.ambar.creditCardProduct.controllers;
+
+import cloud.ambar.creditCardProduct.commandHandlers.ProductCommandService;
+import cloud.ambar.creditCardProduct.commands.DefineProductCommand;
+import cloud.ambar.creditCardProduct.commands.ProductActivatedCommand;
+import cloud.ambar.creditCardProduct.commands.ProductDeactivatedCommand;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+/**
+ * This controller will handle requests from the frontend which are commands that result in events written
+ * to the event store. Events such as defining a product, activating a product, and deactivating a product.
+ * It will leverage the read side of the application to perform validations and determine if we should accept
+ * or reject a command before recording it.
+ * This is the write side of our application.
+ * Requests (Commands) to handle:
+ * - DefineProduct
+ * - ActivateProduct
+ * - DeactivateProduct
+ */
+@Controller
+@RequestMapping("/api/v1/credit_card_product/product")
+public class CommandController {
+ private static final Logger log = LogManager.getLogger(CommandController.class);
+
+ private final ProductCommandService productService;
+
+ @Autowired
+ public CommandController(final ProductCommandService productService) {
+ this.productService = productService;
+ }
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.OK)
+ public void defineProduct(@RequestBody DefineProductCommand defineProductCommand) throws JsonProcessingException {
+ log.info("Got request to define product.");
+ // Todo: Validate the request (Required args present, etc)
+ productService.handle(defineProductCommand);
+ }
+
+ @PostMapping("/activate/{aggregateId}")
+ @ResponseStatus(HttpStatus.OK)
+ public void activateProduct(@PathVariable String aggregateId) throws JsonProcessingException {
+ productService.handle(new ProductActivatedCommand(aggregateId));
+ }
+
+ @PostMapping("/deactivate/{aggregateId}")
+ @ResponseStatus(HttpStatus.OK)
+ public void deactivateProduct(@PathVariable String aggregateId) throws JsonProcessingException {
+ productService.handle(new ProductDeactivatedCommand(aggregateId));
+ }
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/controllers/EventController.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/controllers/EventController.java
new file mode 100644
index 00000000..79c10a74
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/controllers/EventController.java
@@ -0,0 +1,101 @@
+package cloud.ambar.creditCardProduct.controllers;
+
+import cloud.ambar.creditCardProduct.data.models.ambar.AmbarResponse;
+import cloud.ambar.creditCardProduct.data.models.ambar.Error;
+import cloud.ambar.creditCardProduct.data.models.ambar.ErrorPolicy;
+import cloud.ambar.creditCardProduct.data.models.ambar.Result;
+import cloud.ambar.creditCardProduct.data.models.ambar.Success;
+import cloud.ambar.creditCardProduct.data.models.projection.AmbarEvent;
+import cloud.ambar.creditCardProduct.projection.ProductProjectorService;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.ServletInputStream;
+import jakarta.servlet.http.HttpServletRequest;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * This controller is responsible for updating the read models from events written to the eventstore (postgre).
+ * It is notified of new events on an endpoint, and then will perform any necessary actions to retrieve
+ * and update corresponding models in the ReadModelRepository (mongo).
+ *
+ * This is the Projection/Reaction side of our application
+ * Note: This service does not write any new events in response to incoming events, and thus does not have a reaction portion
+ */
+@RestController
+public class EventController {
+ private static final Logger log = LogManager.getLogger(EventController.class);
+
+ private final ProductProjectorService productProjectorService;
+
+ private final ObjectMapper objectMapper;
+
+ public EventController(final ProductProjectorService productProjectorService) {
+ this.productProjectorService = productProjectorService;
+ this.objectMapper = new ObjectMapper();
+ }
+
+ @PostMapping(value = "/api/v1/credit_card_product/product/projection",
+ consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE)
+ public AmbarResponse handleEvent(HttpServletRequest httpServletRequest) {
+ try {
+ final AmbarEvent ambarEvent = extractEvent(httpServletRequest);
+ log.info("Got event: " + ambarEvent);
+
+ productProjectorService.project(ambarEvent.getPayload());
+ return successResponse();
+ } catch (Exception e) {
+ log.error("Failed to process projection event!");
+ log.error(e);
+ log.error(e.getMessage());
+ return retryResponse(e.getMessage());
+ }
+ }
+
+ private AmbarResponse retryResponse(String err) {
+ return AmbarResponse.builder()
+ .result(Result.builder()
+ .error(Error.builder()
+ .policy(ErrorPolicy.MUST_RETRY.toString())
+ .description(err)
+ .build())
+ .build())
+ .build();
+ }
+
+ private AmbarResponse successResponse() {
+ return AmbarResponse.builder()
+ .result(Result.builder()
+ .success(new Success())
+ .build())
+ .build();
+ }
+
+ // Pulls the Ambar event as a string from the request.
+ private AmbarEvent extractEvent(HttpServletRequest httpServletRequest) throws IOException {
+ final ServletInputStream inputStream;
+
+ try {
+ inputStream = httpServletRequest.getInputStream();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ final ByteArrayOutputStream result = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ for (int length; (length = inputStream.read(buffer)) != -1; ) {
+ result.write(buffer, 0, length);
+ }
+
+ log.info("Got message: " + result);
+
+ return objectMapper.readValue(result.toString(StandardCharsets.UTF_8), AmbarEvent.class);
+ }
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/controllers/QueryController.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/controllers/QueryController.java
new file mode 100644
index 00000000..2b22d382
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/controllers/QueryController.java
@@ -0,0 +1,43 @@
+package cloud.ambar.creditCardProduct.controllers;
+
+import cloud.ambar.creditCardProduct.data.models.projection.Product;
+import cloud.ambar.creditCardProduct.query.QueryService;
+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 org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * This controller will handle endpoints related to querying details about products for the front end.
+ * These endpoints do not handle any commands and just return things back from the ReadModelRepository as
+ * written by projections and reactions.
+ * This is the Read side of our application
+ * Requests to handle:
+ * - ListProducts
+ */
+@RestController
+public class QueryController {
+ private static final Logger log = LogManager.getLogger(QueryController.class);
+
+ private final QueryService queryService;
+
+ private final ObjectMapper objectMapper;
+
+ public QueryController(QueryService queryService) {
+ this.queryService = queryService;
+ this.objectMapper = new ObjectMapper();
+ }
+
+ @PostMapping(value = "/api/v1/credit_card_product/product/list-items")
+ public String listItems() throws JsonProcessingException {
+ log.info("Listing all products from ProjectionRepository");
+ List products = queryService.getAllProductListItems();
+ // Todo: Create the response shape and serialize it.
+ return objectMapper.writeValueAsString(products);
+ }
+
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/controllers/README.md b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/controllers/README.md
new file mode 100644
index 00000000..25a6cd05
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/controllers/README.md
@@ -0,0 +1 @@
+Todo: Add details about the classes in this dir
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/PaymentCycle.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/PaymentCycle.java
new file mode 100644
index 00000000..7de2e526
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/PaymentCycle.java
@@ -0,0 +1,7 @@
+package cloud.ambar.creditCardProduct.data.models;
+
+public enum PaymentCycle {
+ MONTHLY,
+ QUARTERLY,
+ UNKNOWN
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/RewardsType.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/RewardsType.java
new file mode 100644
index 00000000..07ba645a
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/RewardsType.java
@@ -0,0 +1,8 @@
+package cloud.ambar.creditCardProduct.data.models;
+
+public enum RewardsType {
+ POINTS,
+ CASHBACK,
+ NONE,
+ UNKNOWN
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/ambar/AmbarResponse.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/ambar/AmbarResponse.java
new file mode 100644
index 00000000..0ff0c1d7
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/ambar/AmbarResponse.java
@@ -0,0 +1,16 @@
+package cloud.ambar.creditCardProduct.data.models.ambar;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AmbarResponse {
+ @JsonProperty("result")
+ private Result result;
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/ambar/Error.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/ambar/Error.java
new file mode 100644
index 00000000..f2476a97
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/ambar/Error.java
@@ -0,0 +1,22 @@
+package cloud.ambar.creditCardProduct.data.models.ambar;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Error {
+ @JsonProperty("policy")
+ private String policy;
+
+ @JsonProperty("class")
+ private String errorClass;
+
+ @JsonProperty("description")
+ private String description;
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/ambar/ErrorPolicy.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/ambar/ErrorPolicy.java
new file mode 100644
index 00000000..018f40c2
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/ambar/ErrorPolicy.java
@@ -0,0 +1,10 @@
+package cloud.ambar.creditCardProduct.data.models.ambar;
+
+public enum ErrorPolicy {
+ KEEP_GOING,
+ MUST_RETRY;
+
+ public String toString() {
+ return name().toLowerCase();
+ }
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/ambar/README.md b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/ambar/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/ambar/Result.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/ambar/Result.java
new file mode 100644
index 00000000..f1677224
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/ambar/Result.java
@@ -0,0 +1,18 @@
+package cloud.ambar.creditCardProduct.data.models.ambar;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Result {
+ @JsonProperty("error")
+ private Error error;
+ @JsonProperty("success")
+ private Success success;
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/ambar/Success.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/ambar/Success.java
new file mode 100644
index 00000000..d1d57628
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/ambar/Success.java
@@ -0,0 +1,17 @@
+package cloud.ambar.creditCardProduct.data.models.ambar;
+
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Success {
+ private JsonNode node;
+}
+
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/projection/AmbarEvent.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/projection/AmbarEvent.java
new file mode 100644
index 00000000..5c344283
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/projection/AmbarEvent.java
@@ -0,0 +1,23 @@
+package cloud.ambar.creditCardProduct.data.models.projection;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AmbarEvent {
+ @JsonProperty("data_source_id")
+ private String dataSourceId;
+ @JsonProperty("data_source_description")
+ private String dataSourceDescription;
+ @JsonProperty("data_destination_id")
+ private String dataDestinationId;
+ @JsonProperty("data_destination_description")
+ private String dataDestinationDescription;
+ @JsonProperty("payload")
+ private Payload payload;
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/projection/Payload.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/projection/Payload.java
new file mode 100644
index 00000000..a75d447f
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/projection/Payload.java
@@ -0,0 +1,35 @@
+package cloud.ambar.creditCardProduct.data.models.projection;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Payload {
+ @JsonProperty("id")
+ private Long id;
+
+ @JsonProperty("event_id")
+ private String eventId;
+
+ @JsonProperty("aggregate_id")
+ private String aggregateId;
+
+ @JsonProperty("causation_id")
+ private String causationID;
+
+ @JsonProperty("correlation_id")
+ private String correlationId;
+
+ @JsonProperty("aggregate_version")
+ private long version;
+
+ @JsonProperty("json_payload")
+ private String data;
+
+ @JsonProperty("event_name")
+ private String eventName;
+}
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/projection/Product.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/projection/Product.java
new file mode 100644
index 00000000..155e1e73
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/projection/Product.java
@@ -0,0 +1,20 @@
+package cloud.ambar.creditCardProduct.data.models.projection;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import jakarta.persistence.Id;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+@Data
+@NoArgsConstructor
+@Document(collection = "ProductListItems")
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Product {
+ @Id
+ private String id;
+ private String name;
+ @JsonProperty("isActive")
+ private boolean active;
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/projection/README.md b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/models/projection/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/mongo/ProjectionRepository.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/mongo/ProjectionRepository.java
new file mode 100644
index 00000000..6460357a
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/mongo/ProjectionRepository.java
@@ -0,0 +1,9 @@
+package cloud.ambar.creditCardProduct.data.mongo;
+
+import cloud.ambar.creditCardProduct.data.models.projection.Product;
+import org.springframework.data.mongodb.repository.MongoRepository;
+
+import java.util.Optional;
+
+public interface ProjectionRepository extends MongoRepository {
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/postgre/EventRepository.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/postgre/EventRepository.java
new file mode 100644
index 00000000..60a15fe4
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/postgre/EventRepository.java
@@ -0,0 +1,15 @@
+package cloud.ambar.creditCardProduct.data.postgre;
+
+import cloud.ambar.creditCardProduct.events.Event;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+// @Repository is not strictly required. Added here for clarity.
+@Repository
+public interface EventRepository extends JpaRepository {
+ List findAllByAggregateId(String aggregateId);
+ Optional findByEventId(String eventId);
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/postgre/EventStoreInitializer.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/postgre/EventStoreInitializer.java
new file mode 100644
index 00000000..27830dad
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/data/postgre/EventStoreInitializer.java
@@ -0,0 +1,132 @@
+package cloud.ambar.creditCardProduct.data.postgre;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+
+/**
+ * By making this a component, we tell Spring to initialize this class and make it available to the Application Context
+ * by doing this, along with ApplicationRunner bean, we can have this code run on startup of the application and ensure
+ * that our event store is ready for us.
+ */
+@Component
+public class EventStoreInitializer {
+ private static final Logger log = LogManager.getLogger(EventStoreInitializer.class);
+
+ private final JdbcTemplate jdbcTemplate;
+
+ @Value("${EVENT_STORE_CREATE_TABLE_WITH_NAME}")
+ private String eventStoreTableName;
+
+ @Value("${EVENT_STORE_CREATE_REPLICATION_USER_WITH_USERNAME}")
+ private String eventStoreCreateReplicationUserWithUsername;
+
+ @Value("${EVENT_STORE_CREATE_REPLICATION_USER_WITH_PASSWORD}")
+ private String eventStoreCreateReplicationUserWithPassword;
+
+ @Value("${EVENT_STORE_DATABASE_NAME}")
+ private String eventStoreDatabaseName;
+
+ @Value("${EVENT_STORE_CREATE_REPLICATION_PUBLICATION}")
+ private String eventStoreCreateReplicationPublication;
+
+ public EventStoreInitializer(JdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ @Bean
+ ApplicationRunner initDatabase() {
+ return args -> {
+ // Create table
+ log.info("Creating table " + eventStoreTableName);
+ executeStatementIgnoreErrors(
+ String.format("""
+ CREATE TABLE IF NOT EXISTS %s (
+ id BIGSERIAL NOT NULL,
+ event_id TEXT NOT NULL UNIQUE,
+ aggregate_id TEXT NOT NULL,
+ aggregate_version BIGINT NOT NULL,
+ causation_id TEXT NOT NULL,
+ correlation_id TEXT NOT NULL,
+ recorded_on TEXT NOT NULL,
+ event_name TEXT NOT NULL,
+ json_payload TEXT NOT NULL,
+ json_metadata TEXT NOT NULL,
+ PRIMARY KEY (id));""",
+ eventStoreTableName)
+ );
+
+ // Create user
+ log.info("Creating replication user");
+ executeStatementIgnoreErrors(String.format(
+ "CREATE USER %s REPLICATION LOGIN PASSWORD '%s';",
+ eventStoreCreateReplicationUserWithUsername,
+ eventStoreCreateReplicationUserWithPassword
+ ));
+
+ // Grant permissions to user
+ log.info("Granting permissions to replication user");
+ executeStatementIgnoreErrors(String.format(
+ "GRANT CONNECT ON DATABASE \"%s\" TO %s;",
+ eventStoreDatabaseName,
+ eventStoreCreateReplicationUserWithUsername
+ ));
+
+ log.info("Granting select to replication user");
+ executeStatementIgnoreErrors(String.format(
+ "GRANT SELECT ON TABLE %s TO %s;",
+ eventStoreTableName,
+ eventStoreCreateReplicationUserWithUsername
+ ));
+
+ // Create publication
+ log.info("Creating publication for table");
+ executeStatementIgnoreErrors(String.format(
+ "CREATE PUBLICATION %s FOR TABLE %s;",
+ eventStoreCreateReplicationPublication,
+ eventStoreTableName
+ ));
+
+ // Create indices
+ log.info("Creating aggregate index");
+ executeStatementIgnoreErrors(String.format(
+ "CREATE UNIQUE INDEX event_store_idx_event_aggregate_id_version ON %s(aggregate_id, aggregate_version);",
+ eventStoreTableName
+ ));
+ log.info("Creating causation index");
+ executeStatementIgnoreErrors(String.format(
+ "CREATE INDEX event_store_idx_event_causation_id ON %s(causation_id);",
+ eventStoreTableName
+ ));
+ log.info("Creating correlation index");
+ executeStatementIgnoreErrors(String.format(
+ "CREATE INDEX event_store_idx_event_correlation_id ON %s(correlation_id);",
+ eventStoreTableName
+ ));
+ log.info("Creating recording index");
+ executeStatementIgnoreErrors(String.format(
+ "CREATE INDEX event_store_idx_occurred_on ON %s(recorded_on);",
+ eventStoreTableName
+ ));
+ log.info("Creating event name index");
+ executeStatementIgnoreErrors(String.format(
+ "CREATE INDEX event_store_idx_event_name ON %s(event_name);",
+ eventStoreTableName
+ ));
+ };
+ }
+
+ private void executeStatementIgnoreErrors(final String sqlStatement) {
+ try {
+ log.info("Executing SQL: " + sqlStatement);
+ jdbcTemplate.execute(sqlStatement);
+ } catch (Exception e) {
+ log.warn("Caught exception when executing SQL statement.");
+ log.warn(e);
+ }
+ }
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/Event.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/Event.java
new file mode 100644
index 00000000..f0b61044
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/Event.java
@@ -0,0 +1,61 @@
+package cloud.ambar.creditCardProduct.events;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Index;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@Entity
+@Table(name = "event_store", indexes = {
+ @Index(name = "event_store_idx_event_aggregate_id_version", columnList = "aggregate_id, aggregate_version", unique = true),
+ @Index(name = "event_store_idx_event_causation_id", columnList = "causation_id", unique = true),
+ @Index(name = "event_store_idx_event_correlation_id", columnList = "correlation_id"),
+ @Index(name = "event_store_idx_occurred_on", columnList = "recorded_on")
+})
+public class Event {
+ @Id
+ @GeneratedValue(strategy= GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name="event_id")
+ private String eventId;
+
+ @Column(name="aggregate_id")
+ private String aggregateId;
+
+ @Column(name="causation_id")
+ private String causationID;
+
+ @Column(name="correlation_id")
+ private String correlationId;
+
+ @Column(name="aggregate_version")
+ private long version;
+
+ @Column(name="json_payload")
+ private String data;
+
+ @Column(name="json_metadata")
+ private String metadata;
+
+ @Column(name="recorded_on")
+ private LocalDateTime timeStamp;
+
+ @Column(name="event_name")
+ private String eventName;
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/EventProjector.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/EventProjector.java
new file mode 100644
index 00000000..583fc16e
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/EventProjector.java
@@ -0,0 +1,8 @@
+package cloud.ambar.creditCardProduct.events;
+
+import cloud.ambar.creditCardProduct.data.models.projection.Payload;
+import com.fasterxml.jackson.core.JsonProcessingException;
+
+public interface EventProjector {
+ public void project(Payload eventPayload) throws JsonProcessingException;
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductActivatedEvent.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductActivatedEvent.java
new file mode 100644
index 00000000..32716fba
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductActivatedEvent.java
@@ -0,0 +1,15 @@
+package cloud.ambar.creditCardProduct.events;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductActivatedEvent {
+ public static final String EVENT_NAME = "CreditCardProduct_Product_ProductActivated";
+ private String aggregateId;
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductDeactivatedEvent.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductDeactivatedEvent.java
new file mode 100644
index 00000000..f328c564
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductDeactivatedEvent.java
@@ -0,0 +1,17 @@
+package cloud.ambar.creditCardProduct.events;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductDeactivatedEvent {
+ public static final String EVENT_NAME = "CreditCardProduct_Product_ProductDeactivated";
+
+ private String aggregateId;
+
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductDefinedEvent.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductDefinedEvent.java
new file mode 100644
index 00000000..f8a8744b
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductDefinedEvent.java
@@ -0,0 +1,27 @@
+package cloud.ambar.creditCardProduct.events;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ProductDefinedEvent {
+ public static final String EVENT_NAME = "CreditCardProduct_Product_ProductDefined";
+
+ // Below are the product details for the event, to be returned as the serialized data of the event
+ private String name;
+ private int interestInBasisPoints;
+ private int annualFeeInCents;
+ private String paymentCycle;
+ private int creditLimitInCents;
+ private int maxBalanceTransferAllowedInCents;
+ private String reward;
+ private String cardBackgroundHex;
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/README.md b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/README.md
new file mode 100644
index 00000000..25a6cd05
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/README.md
@@ -0,0 +1 @@
+Todo: Add details about the classes in this dir
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/exceptions/InvalidEventException.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/exceptions/InvalidEventException.java
new file mode 100644
index 00000000..f051c87f
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/exceptions/InvalidEventException.java
@@ -0,0 +1,10 @@
+package cloud.ambar.creditCardProduct.exceptions;
+
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor
+public class InvalidEventException extends RuntimeException {
+ public InvalidEventException(String msg) {
+ super(msg);
+ }
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/exceptions/InvalidPaymentCycleException.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/exceptions/InvalidPaymentCycleException.java
new file mode 100644
index 00000000..2bb72908
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/exceptions/InvalidPaymentCycleException.java
@@ -0,0 +1,10 @@
+package cloud.ambar.creditCardProduct.exceptions;
+
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor
+public class InvalidPaymentCycleException extends RuntimeException {
+ public InvalidPaymentCycleException(String msg) {
+ super(msg);
+ }
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/exceptions/InvalidRewardException.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/exceptions/InvalidRewardException.java
new file mode 100644
index 00000000..8ee2a2c3
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/exceptions/InvalidRewardException.java
@@ -0,0 +1,10 @@
+package cloud.ambar.creditCardProduct.exceptions;
+
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor
+public class InvalidRewardException extends RuntimeException {
+ public InvalidRewardException(String msg) {
+ super(msg);
+ }
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/exceptions/NoSuchProductException.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/exceptions/NoSuchProductException.java
new file mode 100644
index 00000000..6aa6b42c
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/exceptions/NoSuchProductException.java
@@ -0,0 +1,7 @@
+package cloud.ambar.creditCardProduct.exceptions;
+
+public class NoSuchProductException extends RuntimeException {
+ public NoSuchProductException(String msg) {
+ super(msg);
+ }
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/projection/ProductProjectorService.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/projection/ProductProjectorService.java
new file mode 100644
index 00000000..03fa0da6
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/projection/ProductProjectorService.java
@@ -0,0 +1,72 @@
+package cloud.ambar.creditCardProduct.projection;
+
+import cloud.ambar.creditCardProduct.data.models.projection.Payload;
+import cloud.ambar.creditCardProduct.data.mongo.ProjectionRepository;
+import cloud.ambar.creditCardProduct.events.EventProjector;
+import cloud.ambar.creditCardProduct.events.ProductActivatedEvent;
+import cloud.ambar.creditCardProduct.events.ProductDeactivatedEvent;
+import cloud.ambar.creditCardProduct.events.ProductDefinedEvent;
+import cloud.ambar.creditCardProduct.data.models.projection.Product;
+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 org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+/**
+ * Takes aggregates and projects them into a list of products for querying later.
+ */
+@Service
+public class ProductProjectorService implements EventProjector {
+ private static final Logger log = LogManager.getLogger(ProductProjectorService.class);
+
+ private final ProjectionRepository projectionRepository;
+
+ private final ObjectMapper objectMapper;
+
+ public ProductProjectorService(final ProjectionRepository projectionRepository) {
+ this.projectionRepository = projectionRepository;
+ this.objectMapper = new ObjectMapper();
+ }
+
+ @Override
+ public void project(Payload event) throws JsonProcessingException {
+ final Product product;
+ switch (event.getEventName()) {
+ case ProductDefinedEvent.EVENT_NAME -> {
+ log.info("Handling projection for ProductDefinedEvent");
+ product = objectMapper.readValue(event.getData(), Product.class);
+ product.setId(event.getAggregateId());
+ }
+ case ProductActivatedEvent.EVENT_NAME -> {
+ log.info("Handling projection for ProductActivatedEvent");
+ product = getProductOrThrow(event);
+ product.setActive(true);
+ }
+ case ProductDeactivatedEvent.EVENT_NAME -> {
+ log.info("Handling projection for ProductDeactivatedEvent");
+ product = getProductOrThrow(event);
+ product.setActive(false);
+ }
+ default -> {
+ log.info("Event is not a ProductEvent, doing nothing...");
+ return;
+ }
+ }
+
+ projectionRepository.save(product);
+ }
+
+ private Product getProductOrThrow(Payload event) {
+ Optional product = projectionRepository.findById(event.getAggregateId());
+ if (product.isEmpty()) {
+ final String msg = "Unable to find Product in projection repository for aggregate: " + event.getAggregateId();
+ log.error(msg);
+ throw new RuntimeException(msg);
+ }
+ return product.get();
+ }
+
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/projection/README.md b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/projection/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/query/QueryService.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/query/QueryService.java
new file mode 100644
index 00000000..9e6b0fb7
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/query/QueryService.java
@@ -0,0 +1,22 @@
+package cloud.ambar.creditCardProduct.query;
+
+import cloud.ambar.creditCardProduct.data.mongo.ProjectionRepository;
+import cloud.ambar.creditCardProduct.data.models.projection.Product;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class QueryService {
+
+ private final ProjectionRepository projectionRepository;
+
+ public QueryService(final ProjectionRepository projectionRepository) {
+ this.projectionRepository = projectionRepository;
+ }
+
+ public List getAllProductListItems() {
+ return projectionRepository.findAll();
+ }
+
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/query/README.md b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/query/README.md
new file mode 100644
index 00000000..25a6cd05
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/query/README.md
@@ -0,0 +1 @@
+Todo: Add details about the classes in this dir
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/src/main/resources/application.properties b/application/java-credit-card-product-service/src/main/resources/application.properties
new file mode 100644
index 00000000..dfcb00bd
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/resources/application.properties
@@ -0,0 +1,11 @@
+spring.application.name=CreditCardProduct
+# Configs related to our event store, in this case in postgres
+spring.datasource.url=jdbc:postgresql://${EVENT_STORE_HOST}:${EVENT_STORE_PORT}/${EVENT_STORE_DATABASE_NAME}
+spring.datasource.username=${EVENT_STORE_USER}
+spring.datasource.password=${EVENT_STORE_PASSWORD}
+spring.datasource.driver-class-name=org.postgresql.Driver
+spring.jpa.hibernate.ddl-auto=none
+# Configurations related to projection and reaction data store (mongo)
+spring.data.mongodb.uri=mongodb://${MONGODB_PROJECTION_DATABASE_USERNAME}:${MONGODB_PROJECTION_DATABASE_PASSWORD}@${MONGODB_PROJECTION_HOST}:${MONGODB_PROJECTION_PORT},${MONGODB_PROJECTION_HOST}:${MONGODB_PROJECTION_PORT}/${MONGODB_PROJECTION_DATABASE_NAME}?serverSelectionTimeoutMS=10000&connectTimeoutMS=10000&authSource=admin
+# Logging
+logging.level.org.springframework.web=INFO
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/target/classes/application.properties b/application/java-credit-card-product-service/target/classes/application.properties
new file mode 100644
index 00000000..dfcb00bd
--- /dev/null
+++ b/application/java-credit-card-product-service/target/classes/application.properties
@@ -0,0 +1,11 @@
+spring.application.name=CreditCardProduct
+# Configs related to our event store, in this case in postgres
+spring.datasource.url=jdbc:postgresql://${EVENT_STORE_HOST}:${EVENT_STORE_PORT}/${EVENT_STORE_DATABASE_NAME}
+spring.datasource.username=${EVENT_STORE_USER}
+spring.datasource.password=${EVENT_STORE_PASSWORD}
+spring.datasource.driver-class-name=org.postgresql.Driver
+spring.jpa.hibernate.ddl-auto=none
+# Configurations related to projection and reaction data store (mongo)
+spring.data.mongodb.uri=mongodb://${MONGODB_PROJECTION_DATABASE_USERNAME}:${MONGODB_PROJECTION_DATABASE_PASSWORD}@${MONGODB_PROJECTION_HOST}:${MONGODB_PROJECTION_PORT},${MONGODB_PROJECTION_HOST}:${MONGODB_PROJECTION_PORT}/${MONGODB_PROJECTION_DATABASE_NAME}?serverSelectionTimeoutMS=10000&connectTimeoutMS=10000&authSource=admin
+# Logging
+logging.level.org.springframework.web=INFO
\ No newline at end of file
diff --git a/infrastructure/ambar/production/data_destination_creditcardproduct_product.tf b/infrastructure/ambar/production/data_destination_creditcardproduct_product.tf
index b2d1fa45..63149b25 100644
--- a/infrastructure/ambar/production/data_destination_creditcardproduct_product.tf
+++ b/infrastructure/ambar/production/data_destination_creditcardproduct_product.tf
@@ -1,20 +1,9 @@
-resource "ambar_data_destination" "CreditCardProduct_Authentication_Session" {
- filter_ids = [
- ambar_filter.security_all.resource_id,
- ]
- description = "CreditCardProduct_Authentication_Session"
- destination_endpoint = "${var.data_destination_credit_card_product.endpoint_prefix}/api/v1/authentication_all_services/projection/session"
- username = "username"
- password = "password"
-}
-
-
-resource "ambar_data_destination" "CreditCardProduct_Product_ProductListItem" {
+resource "ambar_data_destination" "CreditCardProduct_Product" {
filter_ids = [
ambar_filter.credit_card_product.resource_id,
]
description = "CreditCardProduct_Product_ProductListItem"
- destination_endpoint = "${var.data_destination_credit_card_product.endpoint_prefix}/api/v1/credit_card_product/product/projection/product_list_item"
+ destination_endpoint = "${var.data_destination_credit_card_product.endpoint_prefix}/api/v1/credit_card_product/product/projection"
username = "username"
password = "password"
}
diff --git a/infrastructure/production.tf b/infrastructure/production.tf
index c16ab590..3957c5d0 100644
--- a/infrastructure/production.tf
+++ b/infrastructure/production.tf
@@ -42,7 +42,7 @@ module "production_credit_card_product" {
pgt_proxy_cert_common_name = local.credentials["pgt_proxy_certificate_common_name"]
pgtproxy_cert_in_base64 = local.credentials["pgtproxy_cert_in_base64"]
pgtproxy_key_in_base64 = local.credentials["pgtproxy_key_in_base64"]
- application_directory_name = "backend-monorepo-multiservice"
+ application_directory_name = "java-credit-card-product-service"
full_service_name_in_lowercase = "credit_card_product"
providers = {