diff --git a/application/frontend/src/main.js b/application/frontend/src/main.js
index e14fe2de..59ecd996 100644
--- a/application/frontend/src/main.js
+++ b/application/frontend/src/main.js
@@ -23,7 +23,9 @@ const endpoints = {
"refresh-token": domains.security + "/session/refresh-token",
"sign-in": domains.security + "/session/sign-in",
"sign-out": domains.security + "/session/sign-out",
- "list-credit-card-products": domains.card + "/product/list-items"
+ "list-credit-card-products": domains.card + "/product/list-items",
+ "activate-product": domains.card + "/product/activate",
+ "deactivate-product": domains.card + "/product/deactivate"
}
// Accept JSON bodies
@@ -103,6 +105,7 @@ app.post('/sign-in', unauthenticated, routeSignIn);
app.post('/sign-up', unauthenticated, routeSignUp);
app.get('/verification-emails', routeVerificationEmails);
app.get('/card/products', authenticated, routeCardProducts);
+app.post('/card/products', authenticated, cardToggle);
async function userDetails(token) {
const response = await fetch(endpoints["user-details"], {
@@ -385,6 +388,44 @@ async function routeCardProducts(req, res) {
});
}
+async function cardToggle(req, res) {
+ const productId = req.body.productId;
+ const active = req.body.active;
+
+ if (!productId) {
+ return res.status(400).json({ error: 'Product ID is required' });
+ }
+
+ try {
+ // Make the appropriate fetch request based on the product's current status
+ const endpoint = active === "true"
+ ? endpoints["deactivate-product"] + "/" + productId
+ : endpoints["activate-product"] + "/" + productId;
+
+ const response = await fetch(endpoint, {
+ method: "POST",
+ body: '{}',
+ headers: {
+ 'Content-Type': 'application/json',
+ }
+ });
+
+ // Bit of a hack, the request -> event -> projection will take some time.
+ // Realistically you would update the interface locally, and refresh state async
+ await sleep(2000);
+
+ // After successfully toggling the product status, render the updated product list
+ return await routeCardProducts(req, res);
+ } catch (error) {
+ console.error('Error in cardToggle:', error);
+ return await routeCardProducts(req, res);
+ }
+}
+
+function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
app.get("*", authenticated, render("404", { title: "Not Found" }))
app.listen(port, () => {
diff --git a/application/frontend/src/views/card/products.handlebars b/application/frontend/src/views/card/products.handlebars
index 0419d54f..81f5c673 100644
--- a/application/frontend/src/views/card/products.handlebars
+++ b/application/frontend/src/views/card/products.handlebars
@@ -22,36 +22,55 @@
-
-{{#each locals.products}}
- -
-
-
-
-
- {{this.name}}
-
-
- {{#if this.isActive}}
- Active
- {{else}}
- Inactive
- {{/if}}
-
-
-
- {{this.id}}
-
-
-
-{{/each}}
+
+ {{#each locals.products}}
+ -
+
+
+
+ {{#if this.isActive}}
+
+ {{this.name}}
+
+
+ Active
+
+
+ Reward Program: {{this.reward}}
+
+
+ Annual Fee: ${{this.annualFee}}.00
+
+ {{else}}
+
+ {{this.name}}
+
+
+ Inactive
+
+ {{/if}}
+
+
+ {{this.id}}
+
+
+
+
+
+
+
+ {{/each}}
-
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
index 9d9f7b7b..f05682f5 100644
--- a/application/java-credit-card-product-service/development-environment/dev-commands.sh
+++ b/application/java-credit-card-product-service/development-environment/dev-commands.sh
@@ -38,4 +38,19 @@ productId=""
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
+curl -X POST "${endpoint}/api/v1/credit_card_product/product/deactivate/${productId}"
+
+# To modify a card product
+curl -X PATCH "${endpoint}/api/v1/credit_card_product/product" \
+-H "Content-Type: application/json" \
+-d '{
+ "id": "806ea870-56aa-4289-ac8f-76861b27a702",
+ "annualFeeInCents": 50000,
+ "creditLimitInCents": 500000,
+ "paymentCycle": "monthly",
+ "cardBackgroundHex": "#E5E4E2"
+}'
+
+# For connecting to the postgres container (To See the events)
+# It will prompt for the password: my_es_password
+psql -h 172.30.0.102 -p 5432 -U my_es_username -d my_es_database
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/src/commands.sh b/application/java-credit-card-product-service/src/commands.sh
index 98c2cc34..fa0d96f0 100644
--- a/application/java-credit-card-product-service/src/commands.sh
+++ b/application/java-credit-card-product-service/src/commands.sh
@@ -15,6 +15,48 @@ curl -X POST "${endpoint}/api/v1/credit_card_product/product" \
"cardBackgroundHex": "#7fffd4"
}'
+curl -X POST "${endpoint}/api/v1/credit_card_product/product" \
+-H "Content-Type: application/json" \
+-d '{
+ "productIdentifierForAggregateIdHash": "BASIC_CREDIT_CARD",
+ "name": "Basic",
+ "interestInBasisPoints": 1500,
+ "annualFeeInCents": 7500,
+ "paymentCycle": "monthly",
+ "creditLimitInCents": 500000,
+ "maxBalanceTransferAllowedInCents": 100000,
+ "reward": "none",
+ "cardBackgroundHex": "#34eb37"
+}'
+
+curl -X POST "${endpoint}/api/v1/credit_card_product/product" \
+-H "Content-Type: application/json" \
+-d '{
+ "productIdentifierForAggregateIdHash": "BASIC_CASH_BACK_CREDIT_CARD",
+ "name": "Cash Back - Basic",
+ "interestInBasisPoints": 2000,
+ "annualFeeInCents": 8500,
+ "paymentCycle": "monthly",
+ "creditLimitInCents": 500000,
+ "maxBalanceTransferAllowedInCents": 100000,
+ "reward": "CASHBACK",
+ "cardBackgroundHex": "#e396ff"
+}'
+
+curl -X POST "${endpoint}/api/v1/credit_card_product/product" \
+-H "Content-Type: application/json" \
+-d '{
+ "productIdentifierForAggregateIdHash": "BASIC_POINTS_CREDIT_CARD",
+ "name": "Travel Points - Basic",
+ "interestInBasisPoints": 2000,
+ "annualFeeInCents": 8500,
+ "paymentCycle": "monthly",
+ "creditLimitInCents": 500000,
+ "maxBalanceTransferAllowedInCents": 100000,
+ "reward": "POINTS",
+ "cardBackgroundHex": "#3a34eb"
+}'
+
# To create the Platinum card
curl -X POST "${endpoint}/api/v1/credit_card_product/product" \
-H "Content-Type: application/json" \
@@ -38,4 +80,12 @@ productId=""
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
+curl -X POST "${endpoint}/api/v1/credit_card_product/product/deactivate/${productId}"
+
+# To modify a card product
+curl -X PATCH "${endpoint}/api/v1/credit_card_product/product" \
+-H "Content-Type: application/json" \
+-d '{
+ "id": "806ea870-56aa-4289-ac8f-76861b27a702",
+ "annualFeeInCents": 2500
+}'
\ No newline at end of file
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/aggregate/CreditCardProductAggregate.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/aggregate/CreditCardProductAggregate.java
index 214e890d..b211193a 100644
--- a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/aggregate/CreditCardProductAggregate.java
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/aggregate/CreditCardProductAggregate.java
@@ -2,8 +2,12 @@
import cloud.ambar.creditCardProduct.events.Event;
import cloud.ambar.creditCardProduct.events.ProductActivatedEventData;
+import cloud.ambar.creditCardProduct.events.ProductAnnualFeeChangedEventData;
+import cloud.ambar.creditCardProduct.events.ProductBackgroundChangedEventData;
+import cloud.ambar.creditCardProduct.events.ProductCreditLimitChangedEventData;
import cloud.ambar.creditCardProduct.events.ProductDeactivatedEventData;
import cloud.ambar.creditCardProduct.events.ProductDefinedEventData;
+import cloud.ambar.creditCardProduct.events.ProductPaymentCycleChangedEventData;
import cloud.ambar.creditCardProduct.exceptions.InvalidEventException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -34,10 +38,9 @@ public CreditCardProductAggregate(String aggregateId) {
@Override
public void transform(Event event) {
+ ObjectMapper om = new ObjectMapper();
switch(event.getEventName()) {
case ProductDefinedEventData.EVENT_NAME -> {
- log.info("Transforming aggregate for ProductDefinedEvent");
- ObjectMapper om = new ObjectMapper();
try {
ProductDefinedEventData definition = om.readValue(event.getData(), ProductDefinedEventData.class);
this.setAggregateId(event.getAggregateId());
@@ -57,13 +60,43 @@ public void transform(Event event) {
}
}
case ProductActivatedEventData.EVENT_NAME -> {
- log.info("Transforming aggregate for ProductActivatedEvent");
this.active = true;
}
case ProductDeactivatedEventData.EVENT_NAME -> {
- log.info("Transforming aggregate for ProductDeactivatedEvent");
this.active = false;
}
+ case ProductAnnualFeeChangedEventData.EVENT_NAME -> {
+ try {
+ ProductAnnualFeeChangedEventData modification = om.readValue(event.getData(), ProductAnnualFeeChangedEventData.class);
+ this.setAnnualFeeInCents(modification.getAnnualFeeInCents());
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e.getMessage());
+ }
+ }
+ case ProductPaymentCycleChangedEventData.EVENT_NAME -> {
+ try {
+ ProductPaymentCycleChangedEventData modification = om.readValue(event.getData(), ProductPaymentCycleChangedEventData.class);
+ this.setPaymentCycle(modification.getPaymentCycle());
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e.getMessage());
+ }
+ }
+ case ProductCreditLimitChangedEventData.EVENT_NAME -> {
+ try {
+ ProductCreditLimitChangedEventData modification = om.readValue(event.getData(), ProductCreditLimitChangedEventData.class);
+ this.setCreditLimitInCents(modification.getCreditLimitInCents());
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e.getMessage());
+ }
+ }
+ case ProductBackgroundChangedEventData.EVENT_NAME -> {
+ try {
+ ProductBackgroundChangedEventData modification = om.readValue(event.getData(), ProductBackgroundChangedEventData.class);
+ this.setCardBackgroundHex(modification.getCardBackgroundHex());
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e.getMessage());
+ }
+ }
}
}
}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/command/CreditCardProductCommandService.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/command/CreditCardProductCommandService.java
index 4902ae9c..14650bef 100644
--- a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/command/CreditCardProductCommandService.java
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/command/CreditCardProductCommandService.java
@@ -1,20 +1,29 @@
package cloud.ambar.creditCardProduct.command;
import cloud.ambar.creditCardProduct.aggregate.CreditCardProductAggregate;
-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.command.models.commands.DefineCreditCardProductCommand;
import cloud.ambar.creditCardProduct.command.models.commands.ActivateCreditCardProductCommand;
import cloud.ambar.creditCardProduct.command.models.commands.DeactivateCreditCardProductCommand;
+import cloud.ambar.creditCardProduct.command.models.commands.DefineCreditCardProductCommand;
+import cloud.ambar.creditCardProduct.command.models.commands.ModifyCreditCardCommand;
+import cloud.ambar.creditCardProduct.command.models.validation.HexColorValidator;
+import cloud.ambar.creditCardProduct.command.models.validation.PaymentCycle;
+import cloud.ambar.creditCardProduct.command.models.validation.RewardsType;
import cloud.ambar.creditCardProduct.database.postgre.EventRepository;
+import cloud.ambar.creditCardProduct.events.Event;
import cloud.ambar.creditCardProduct.events.ProductActivatedEventData;
+import cloud.ambar.creditCardProduct.events.ProductAnnualFeeChangedEventData;
+import cloud.ambar.creditCardProduct.events.ProductBackgroundChangedEventData;
+import cloud.ambar.creditCardProduct.events.ProductCreditLimitChangedEventData;
import cloud.ambar.creditCardProduct.events.ProductDeactivatedEventData;
import cloud.ambar.creditCardProduct.events.ProductDefinedEventData;
-import cloud.ambar.creditCardProduct.command.models.validation.PaymentCycle;
-import cloud.ambar.creditCardProduct.command.models.validation.RewardsType;
+import cloud.ambar.creditCardProduct.events.ProductPaymentCycleChangedEventData;
+import cloud.ambar.creditCardProduct.exceptions.InvalidEventException;
+import cloud.ambar.creditCardProduct.exceptions.InvalidHexColorException;
+import cloud.ambar.creditCardProduct.exceptions.InvalidPaymentCycleException;
+import cloud.ambar.creditCardProduct.exceptions.InvalidRewardException;
import cloud.ambar.creditCardProduct.exceptions.NoSuchProductException;
+import cloud.ambar.creditCardProduct.projection.models.CreditCardProduct;
+import cloud.ambar.creditCardProduct.query.QueryService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
@@ -22,6 +31,7 @@
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.ObjectUtils;
import java.time.LocalDateTime;
import java.util.Arrays;
@@ -30,7 +40,11 @@
import java.util.UUID;
@Service
-@Transactional
+// @Transactional
+// We define this on a Method level instead, as some handle methods will call multiple private helpers to write multiple
+// events, and we want an all or nothing transaction on the command handler method, not on each and every method.
+// Write all events for a command in a single transaction so either the whole command is accepted or not. No partial
+// applications!
@RequiredArgsConstructor
public class CreditCardProductCommandService {
private static final Logger log = LogManager.getLogger(CreditCardProductCommandService.class);
@@ -39,7 +53,10 @@ public class CreditCardProductCommandService {
private final ObjectMapper objectMapper;
- public void handle(DefineCreditCardProductCommand command) throws JsonProcessingException {
+ private final QueryService queryService;
+
+ @Transactional
+ public void handle(final DefineCreditCardProductCommand command) throws JsonProcessingException {
log.info("Handling " + ProductDefinedEventData.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
@@ -51,18 +68,20 @@ public void handle(DefineCreditCardProductCommand command) throws JsonProcessing
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();
}
+ if (!HexColorValidator.isValidHexCode(command.getCardBackgroundHex())) {
+ throw new InvalidHexColorException();
+ }
+
// 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();
@@ -94,7 +113,8 @@ public void handle(DefineCreditCardProductCommand command) throws JsonProcessing
log.info("Successfully handled " + ProductDefinedEventData.EVENT_NAME + " command.");
}
- public void handle(ActivateCreditCardProductCommand command) throws JsonProcessingException {
+ @Transactional
+ public void handle(final ActivateCreditCardProductCommand command) throws JsonProcessingException {
log.info("Handling " + ProductActivatedEventData.EVENT_NAME + " command.");
final String aggregateId = command.getId();
@@ -131,7 +151,8 @@ public void handle(ActivateCreditCardProductCommand command) throws JsonProcessi
log.info("Successfully handled " + ProductDefinedEventData.EVENT_NAME + " command.");
}
- public void handle(DeactivateCreditCardProductCommand command) throws JsonProcessingException {
+ @Transactional
+ public void handle(final DeactivateCreditCardProductCommand command) throws JsonProcessingException {
log.info("Handling " + ProductDeactivatedEventData.EVENT_NAME + " command.");
final String aggregateId = command.getId();
@@ -143,9 +164,21 @@ public void handle(DeactivateCreditCardProductCommand command) throws JsonProces
// 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()) {
+ // Todo: Our error could be more clear about why this is invalid. An exercise for later.
throw new InvalidEventException();
}
- log.info("Product is currently active, updating to active!");
+ // Leveraging the read side of our CQRS application. We can have a business rule that there must be at least
+ // one active product.
+ final List allProducts = queryService.getAllCreditCardProducts();
+ final long activeProductCount = allProducts.stream()
+ .filter(CreditCardProduct::isActive)
+ .count();
+ // If < 2, then we would end up with 0 active cards after accepting this command.
+ if (activeProductCount < 2) {
+ // Todo: Our error could be more clear about why this is invalid. An exercise for later.
+ throw new InvalidEventException();
+ }
+ log.info("Product is currently inactive, updating to active!");
// 3. Update the aggregate (write new event to store)
final String eventId = UUID.randomUUID().toString();
@@ -168,6 +201,136 @@ public void handle(DeactivateCreditCardProductCommand command) throws JsonProces
log.info("Successfully handled " + ProductDeactivatedEventData.EVENT_NAME + " command.");
}
+ @Transactional
+ public void handle(final ModifyCreditCardCommand command) throws JsonProcessingException {
+ log.info("Handling ModifyCreditCardCommand command.");
+ final String aggregateId = command.getId();
+
+ // 1. Hydrate the Aggregate
+ final CreditCardProductAggregate aggregate = hydrateAggregateForId(aggregateId);
+
+ if (command.getAnnualFeeInCents() > 0 && command.getAnnualFeeInCents() != aggregate.getAnnualFeeInCents()) {
+ aggregate.apply(saveProductAnnualFeeChangeEvent(command, aggregate));
+ }
+
+ if (command.getCreditLimitInCents() > 0 && command.getCreditLimitInCents() != aggregate.getCreditLimitInCents()) {
+ aggregate.apply(saveProductCreditLimitChangeEvent(command, aggregate));
+ }
+
+ if (!ObjectUtils.isEmpty(command.getPaymentCycle())
+ && !command.getPaymentCycle().equalsIgnoreCase(aggregate.getPaymentCycle())) {
+ aggregate.apply(saveProductPaymentCycleChangedEvent(command, aggregate));
+ }
+
+ if (!ObjectUtils.isEmpty(command.getCardBackgroundHex())
+ && !command.getCardBackgroundHex().equalsIgnoreCase(aggregate.getCardBackgroundHex())
+ && HexColorValidator.isValidHexCode(command.getCardBackgroundHex())) {
+ aggregate.apply(saveProductBackgroundChangedEvent(command, aggregate));
+ }
+
+ log.info("Successfully handled ModifyCreditCardCommand command.");
+ }
+
+ private Event saveProductBackgroundChangedEvent(ModifyCreditCardCommand command, CreditCardProductAggregate aggregate) throws JsonProcessingException {
+ final String aggregateId = command.getId();
+ final String eventId = UUID.randomUUID().toString();
+ final Event event = Event.builder()
+ .eventName(ProductBackgroundChangedEventData.EVENT_NAME)
+ .eventId(eventId)
+ .correlationId(aggregateId)
+ .causationID(eventId)
+ .aggregateId(aggregateId)
+ .version(aggregate.getAggregateVersion())
+ .timestamp(LocalDateTime.now())
+ .metadata("{\"prior_value\":\"" + aggregate.getCardBackgroundHex() + "\"}")
+ .data(objectMapper.writeValueAsString(
+ ProductBackgroundChangedEventData.builder()
+ .cardBackgroundHex(command.getCardBackgroundHex())
+ .build()))
+ .build();
+
+ log.info("Saving Event: " + objectMapper.writeValueAsString(event));
+ eventStore.save(event);
+ log.info("Successfully handled " + ProductBackgroundChangedEventData.EVENT_NAME + " command.");
+
+ return event;
+ }
+
+ private Event saveProductCreditLimitChangeEvent(ModifyCreditCardCommand command, CreditCardProductAggregate aggregate) throws JsonProcessingException {
+ final String aggregateId = command.getId();
+ final String eventId = UUID.randomUUID().toString();
+ final Event event = Event.builder()
+ .eventName(ProductCreditLimitChangedEventData.EVENT_NAME)
+ .eventId(eventId)
+ .correlationId(aggregateId)
+ .causationID(eventId)
+ .aggregateId(aggregateId)
+ .version(aggregate.getAggregateVersion())
+ .timestamp(LocalDateTime.now())
+ .metadata("{\"prior_value\":" + aggregate.getCreditLimitInCents() + "}")
+ .data(objectMapper.writeValueAsString(
+ ProductCreditLimitChangedEventData.builder()
+ .creditLimitInCents(command.getCreditLimitInCents())
+ .build()))
+ .build();
+
+ log.info("Saving Event: " + objectMapper.writeValueAsString(event));
+ eventStore.save(event);
+ log.info("Successfully handled " + ProductCreditLimitChangedEventData.EVENT_NAME + " command.");
+
+ return event;
+ }
+
+ private Event saveProductPaymentCycleChangedEvent(ModifyCreditCardCommand command, CreditCardProductAggregate aggregate) throws JsonProcessingException {
+ final String aggregateId = command.getId();
+ final String eventId = UUID.randomUUID().toString();
+ final Event event = Event.builder()
+ .eventName(ProductPaymentCycleChangedEventData.EVENT_NAME)
+ .eventId(eventId)
+ .correlationId(aggregateId)
+ .causationID(eventId)
+ .aggregateId(aggregateId)
+ .version(aggregate.getAggregateVersion())
+ .timestamp(LocalDateTime.now())
+ .metadata("{\"prior_value\":\"" + aggregate.getPaymentCycle() + "\"}")
+ .data(objectMapper.writeValueAsString(
+ ProductPaymentCycleChangedEventData.builder()
+ .paymentCycle(command.getPaymentCycle())
+ .build()))
+ .build();
+
+ log.info("Saving Event: " + objectMapper.writeValueAsString(event));
+ eventStore.save(event);
+ log.info("Successfully handled " + ProductPaymentCycleChangedEventData.EVENT_NAME + " command.");
+
+ return event;
+ }
+
+ private Event saveProductAnnualFeeChangeEvent(ModifyCreditCardCommand command, CreditCardProductAggregate aggregate) throws JsonProcessingException {
+ final String aggregateId = command.getId();
+ final String eventId = UUID.randomUUID().toString();
+ final Event event = Event.builder()
+ .eventName(ProductAnnualFeeChangedEventData.EVENT_NAME)
+ .eventId(eventId)
+ .correlationId(aggregateId)
+ .causationID(eventId)
+ .aggregateId(aggregateId)
+ .version(aggregate.getAggregateVersion())
+ .timestamp(LocalDateTime.now())
+ .metadata("{\"prior_value\":" + aggregate.getAnnualFeeInCents() + "}")
+ .data(objectMapper.writeValueAsString(
+ ProductAnnualFeeChangedEventData.builder()
+ .annualFeeInCents(command.getAnnualFeeInCents())
+ .build()))
+ .build();
+
+ log.info("Saving Event: " + objectMapper.writeValueAsString(event));
+ eventStore.save(event);
+ log.info("Successfully handled " + ProductDeactivatedEventData.EVENT_NAME + " command.");
+
+ return event;
+ }
+
private CreditCardProductAggregate hydrateAggregateForId(String id) {
final List productEvents = eventStore.findAllByAggregateId(id);
final CreditCardProductAggregate aggregate = new CreditCardProductAggregate(id);
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/command/models/commands/DefineCreditCardProductCommand.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/command/models/commands/DefineCreditCardProductCommand.java
index ce6d5671..a1e1f2c1 100644
--- a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/command/models/commands/DefineCreditCardProductCommand.java
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/command/models/commands/DefineCreditCardProductCommand.java
@@ -1,11 +1,13 @@
package cloud.ambar.creditCardProduct.command.models.commands;
import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
+@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DefineCreditCardProductCommand {
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/command/models/commands/ModifyCreditCardCommand.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/command/models/commands/ModifyCreditCardCommand.java
new file mode 100644
index 00000000..5c12ed04
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/command/models/commands/ModifyCreditCardCommand.java
@@ -0,0 +1,16 @@
+package cloud.ambar.creditCardProduct.command.models.commands;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ModifyCreditCardCommand {
+ private String id;
+ private int annualFeeInCents;
+ private String paymentCycle;
+ private int creditLimitInCents;
+ private String cardBackgroundHex;
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/command/models/validation/HexColorValidator.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/command/models/validation/HexColorValidator.java
new file mode 100644
index 00000000..044eda92
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/command/models/validation/HexColorValidator.java
@@ -0,0 +1,30 @@
+package cloud.ambar.creditCardProduct.command.models.validation;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class HexColorValidator {
+ public static boolean isValidHexCode(String str)
+ {
+ // Regex to check valid hexadecimal color code.
+ String regex = "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$";
+
+ // Compile the ReGex
+ Pattern p = Pattern.compile(regex);
+
+ // If the string is empty
+ // return false
+ if (str == null) {
+ return false;
+ }
+
+ // Pattern class contains matcher() method
+ // to find matching between given string
+ // and regular expression.
+ Matcher m = p.matcher(str);
+
+ // Return if the string
+ // matched the ReGex
+ return m.matches();
+ }
+}
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
index 57faf863..9907eea6 100644
--- 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
@@ -4,18 +4,27 @@
import cloud.ambar.creditCardProduct.command.models.commands.DefineCreditCardProductCommand;
import cloud.ambar.creditCardProduct.command.models.commands.ActivateCreditCardProductCommand;
import cloud.ambar.creditCardProduct.command.models.commands.DeactivateCreditCardProductCommand;
+import cloud.ambar.creditCardProduct.command.models.commands.ModifyCreditCardCommand;
+import cloud.ambar.creditCardProduct.command.models.validation.HexColorValidator;
+import cloud.ambar.creditCardProduct.command.models.validation.PaymentCycle;
+import cloud.ambar.creditCardProduct.command.models.validation.RewardsType;
+import cloud.ambar.creditCardProduct.exceptions.InvalidPaymentCycleException;
+import cloud.ambar.creditCardProduct.exceptions.InvalidRewardException;
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.PatchMapping;
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;
+import java.util.Arrays;
+
/**
* 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.
@@ -30,7 +39,6 @@
@Controller
@RequestMapping("/api/v1/credit_card_product/product")
public class CommandController {
- private static final Logger log = LogManager.getLogger(CommandController.class);
private final CreditCardProductCommandService productService;
@@ -41,21 +49,27 @@ public CommandController(final CreditCardProductCommandService productService) {
@PostMapping
@ResponseStatus(HttpStatus.OK)
- public void defineProduct(@RequestBody DefineCreditCardProductCommand defineProductCommand) throws JsonProcessingException {
- log.info("Got request to define product.");
- // Todo: Validate the request (Required args present, etc)
- productService.handle(defineProductCommand);
+ public void defineProduct(@RequestBody DefineCreditCardProductCommand command) throws JsonProcessingException {
+ productService.handle(command);
}
@PostMapping("/activate/{aggregateId}")
@ResponseStatus(HttpStatus.OK)
- public void activateProduct(@PathVariable String aggregateId) throws JsonProcessingException {
+ public void activateProduct(@PathVariable final String aggregateId) throws JsonProcessingException {
productService.handle(new ActivateCreditCardProductCommand(aggregateId));
}
@PostMapping("/deactivate/{aggregateId}")
@ResponseStatus(HttpStatus.OK)
- public void deactivateProduct(@PathVariable String aggregateId) throws JsonProcessingException {
+ public void deactivateProduct(@PathVariable final String aggregateId) throws JsonProcessingException {
productService.handle(new DeactivateCreditCardProductCommand(aggregateId));
}
+
+ // Todo: Add new URI Mapping to handle ModifyCreditCardColorCommands
+ // PATCH '/api/v1/credit_card_product/product'
+ @PatchMapping
+ @ResponseStatus(HttpStatus.OK)
+ public void modifyProduct(@RequestBody ModifyCreditCardCommand defineProductCommand) throws JsonProcessingException {
+ productService.handle(defineProductCommand);
+ }
}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/controllers/ProjectionReactionController.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/controllers/ProjectionReactionController.java
index cc09e55b..caaf66b3 100644
--- a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/controllers/ProjectionReactionController.java
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/controllers/ProjectionReactionController.java
@@ -1,5 +1,6 @@
package cloud.ambar.creditCardProduct.controllers;
+import cloud.ambar.creditCardProduct.exceptions.UnexpectedEventException;
import cloud.ambar.creditCardProduct.projection.models.response.AmbarResponse;
import cloud.ambar.creditCardProduct.projection.models.response.Error;
import cloud.ambar.creditCardProduct.projection.models.response.ErrorPolicy;
@@ -51,6 +52,10 @@ public AmbarResponse handleEvent(HttpServletRequest httpServletRequest) {
creditCardProductProjectionService.project(ambarEvent.getPayload());
return successResponse();
+ } catch (UnexpectedEventException e) {
+ log.warn("Got unexpected event at projection endpoint from Ambar...");
+ log.warn("Check Filter configuration for Ambar, dropping event and continuing...");
+ return keepGoingResponse(e.getMessage());
} catch (Exception e) {
log.error("Failed to process projection event!");
log.error(e);
@@ -70,6 +75,17 @@ private AmbarResponse retryResponse(String err) {
.build();
}
+ private AmbarResponse keepGoingResponse(String err) {
+ return AmbarResponse.builder()
+ .result(Result.builder()
+ .error(Error.builder()
+ .policy(ErrorPolicy.KEEP_GOING.toString())
+ .description(err)
+ .build())
+ .build())
+ .build();
+ }
+
private AmbarResponse successResponse() {
return AmbarResponse.builder()
.result(Result.builder()
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/database/postgre/EventStoreInitializer.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/database/postgre/EventStoreInitializer.java
index 3b750b0a..cf48abe8 100644
--- a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/database/postgre/EventStoreInitializer.java
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/database/postgre/EventStoreInitializer.java
@@ -117,9 +117,14 @@ ApplicationRunner initDatabase() {
"CREATE INDEX event_store_idx_event_name ON %s(event_name);",
eventStoreTableName
));
+
+ defineInitialCards();
};
}
+ private void defineInitialCards() {
+ }
+
private void executeStatementIgnoreErrors(final String sqlStatement) {
try {
log.info("Executing SQL: " + sqlStatement);
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductAnnualFeeChangedEventData.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductAnnualFeeChangedEventData.java
new file mode 100644
index 00000000..ce7e5e11
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductAnnualFeeChangedEventData.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 ProductAnnualFeeChangedEventData {
+ public static final String EVENT_NAME = "CreditCardProduct_Product_AnnualFeeChanged";
+ private int annualFeeInCents;
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductBackgroundChangedEventData.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductBackgroundChangedEventData.java
new file mode 100644
index 00000000..c5066153
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductBackgroundChangedEventData.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 ProductBackgroundChangedEventData {
+ public static final String EVENT_NAME = "CreditCardProduct_Product_BackgroundChanged";
+ private String cardBackgroundHex;
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductCreditLimitChangedEventData.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductCreditLimitChangedEventData.java
new file mode 100644
index 00000000..ac20cbb1
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductCreditLimitChangedEventData.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 ProductCreditLimitChangedEventData {
+ public static final String EVENT_NAME = "CreditCardProduct_Product_CreditLimitChanged";
+ private int creditLimitInCents;
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductPaymentCycleChangedEventData.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductPaymentCycleChangedEventData.java
new file mode 100644
index 00000000..154bc695
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/events/ProductPaymentCycleChangedEventData.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 ProductPaymentCycleChangedEventData {
+ public static final String EVENT_NAME = "CreditCardProduct_Product_PaymentCycleChanged";
+ private String paymentCycle;
+}
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
index 3e8ad788..aab165d0 100644
--- 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
@@ -3,6 +3,6 @@
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
-@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Request conflicts with current state of Credit Card Product")
+@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Request conflicts with current state of Credit Card Product(s)")
public class InvalidEventException extends RuntimeException {
}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/exceptions/InvalidHexColorException.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/exceptions/InvalidHexColorException.java
new file mode 100644
index 00000000..e4ad527d
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/exceptions/InvalidHexColorException.java
@@ -0,0 +1,7 @@
+package cloud.ambar.creditCardProduct.exceptions;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Invalid Background Color specific")
+public class InvalidHexColorException extends RuntimeException {}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/exceptions/UnexpectedEventException.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/exceptions/UnexpectedEventException.java
new file mode 100644
index 00000000..86e2ff3a
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/exceptions/UnexpectedEventException.java
@@ -0,0 +1,7 @@
+package cloud.ambar.creditCardProduct.exceptions;
+
+public class UnexpectedEventException extends RuntimeException {
+ public UnexpectedEventException(String msg) {
+ super(msg);
+ }
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/projection/CreditCardProductProjectionService.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/projection/CreditCardProductProjectionService.java
index e010590b..697bfd8b 100644
--- a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/projection/CreditCardProductProjectionService.java
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/projection/CreditCardProductProjectionService.java
@@ -1,5 +1,9 @@
package cloud.ambar.creditCardProduct.projection;
+import cloud.ambar.creditCardProduct.events.ProductAnnualFeeChangedEventData;
+import cloud.ambar.creditCardProduct.events.ProductBackgroundChangedEventData;
+import cloud.ambar.creditCardProduct.events.ProductCreditLimitChangedEventData;
+import cloud.ambar.creditCardProduct.exceptions.UnexpectedEventException;
import cloud.ambar.creditCardProduct.projection.models.event.Payload;
import cloud.ambar.creditCardProduct.database.mongo.ProjectionRepository;
import cloud.ambar.creditCardProduct.events.ProductActivatedEventData;
@@ -35,8 +39,14 @@ public void project(Payload event) throws JsonProcessingException {
switch (event.getEventName()) {
case ProductDefinedEventData.EVENT_NAME -> {
log.info("Handling projection for ProductDefinedEvent");
- creditCardProduct = objectMapper.readValue(event.getData(), CreditCardProduct.class);
+ final ProductDefinedEventData data = objectMapper.readValue(event.getData(), ProductDefinedEventData.class);
+ creditCardProduct = new CreditCardProduct();
creditCardProduct.setId(event.getAggregateId());
+ creditCardProduct.setName(data.getName());
+ creditCardProduct.setActive(false);
+ creditCardProduct.setRewardType(data.getReward());
+ creditCardProduct.setAnnualFee(data.getAnnualFeeInCents()/100);
+ creditCardProduct.setBackgroundColorHex(data.getCardBackgroundHex());
}
case ProductActivatedEventData.EVENT_NAME -> {
log.info("Handling projection for ProductActivatedEvent");
@@ -48,10 +58,22 @@ public void project(Payload event) throws JsonProcessingException {
creditCardProduct = getProductOrThrow(event);
creditCardProduct.setActive(false);
}
- default -> {
- log.info("Event is not a ProductEvent, doing nothing...");
- return;
+ case ProductAnnualFeeChangedEventData.EVENT_NAME -> {
+ log.info("Handling projection for ProductDeactivatedEvent");
+ final ProductAnnualFeeChangedEventData data = objectMapper.readValue(event.getData(), ProductAnnualFeeChangedEventData.class);
+ creditCardProduct = getProductOrThrow(event);
+ creditCardProduct.setAnnualFee(data.getAnnualFeeInCents()/100);
+ }
+ case ProductBackgroundChangedEventData.EVENT_NAME -> {
+ log.info("Handling projection for ProductBackgroundChangedEvent");
+ final ProductBackgroundChangedEventData data = objectMapper.readValue(event.getData(), ProductBackgroundChangedEventData.class);
+ creditCardProduct = getProductOrThrow(event);
+ creditCardProduct.setBackgroundColorHex(data.getCardBackgroundHex());
}
+ // For now Ambar is sending all events. But we could update the filter to only give us events related to
+ // the properties of products which we actually display.
+ // Throwing this will tell ambar to keep going despite something unexpected.
+ default -> throw new UnexpectedEventException(event.getEventName());
}
projectionRepository.save(creditCardProduct);
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/projection/models/CreditCardProduct.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/projection/models/CreditCardProduct.java
index 4cdefcc7..11dfe5be 100644
--- a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/projection/models/CreditCardProduct.java
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/projection/models/CreditCardProduct.java
@@ -17,4 +17,8 @@ public class CreditCardProduct {
private String name;
@JsonProperty("isActive")
private boolean active;
+ @JsonProperty("reward")
+ private String rewardType;
+ private int annualFee;
+ private String backgroundColorHex;
}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/util/Constants.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/util/Constants.java
new file mode 100644
index 00000000..640444b3
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/util/Constants.java
@@ -0,0 +1,68 @@
+package cloud.ambar.creditCardProduct.util;
+
+import cloud.ambar.creditCardProduct.command.models.commands.DefineCreditCardProductCommand;
+import cloud.ambar.creditCardProduct.command.models.validation.PaymentCycle;
+import cloud.ambar.creditCardProduct.command.models.validation.RewardsType;
+
+public class Constants {
+
+ public static final DefineCreditCardProductCommand STARTER = DefineCreditCardProductCommand.builder()
+ .productIdentifierForAggregateIdHash("STARTER_CREDIT_CARD")
+ .name("Starter")
+ .interestInBasisPoints(1200)
+ .annualFeeInCents(5000)
+ .paymentCycle(PaymentCycle.MONTHLY.name())
+ .creditLimitInCents(50000)
+ .maxBalanceTransferAllowedInCents(0)
+ .reward(RewardsType.NONE.name())
+ .cardBackgroundHex("#7fffd4")
+ .build();
+
+ public static final DefineCreditCardProductCommand BASIC = DefineCreditCardProductCommand.builder()
+ .productIdentifierForAggregateIdHash("BASIC_CREDIT_CARD")
+ .name("Basic")
+ .interestInBasisPoints(1500)
+ .annualFeeInCents(7500)
+ .paymentCycle(PaymentCycle.MONTHLY.name())
+ .creditLimitInCents(500000)
+ .maxBalanceTransferAllowedInCents(100000)
+ .reward(RewardsType.NONE.name())
+ .cardBackgroundHex("#34eb37")
+ .build();
+
+ public static final DefineCreditCardProductCommand BASIC_CASH_BACK = DefineCreditCardProductCommand.builder()
+ .productIdentifierForAggregateIdHash("BASIC_CASH_BACK_CREDIT_CARD")
+ .name("Cash Back - Basic")
+ .interestInBasisPoints(2000)
+ .annualFeeInCents(8500)
+ .paymentCycle(PaymentCycle.MONTHLY.name())
+ .creditLimitInCents(500000)
+ .maxBalanceTransferAllowedInCents(100000)
+ .reward(RewardsType.CASHBACK.name())
+ .cardBackgroundHex("#e396ff")
+ .build();
+
+ public static final DefineCreditCardProductCommand BASIC_POINTS = DefineCreditCardProductCommand.builder()
+ .productIdentifierForAggregateIdHash("BASIC_POINTS_CREDIT_CARD")
+ .name("Travel Points - Basic")
+ .interestInBasisPoints(2000)
+ .annualFeeInCents(8500)
+ .paymentCycle(PaymentCycle.MONTHLY.name())
+ .creditLimitInCents(500000)
+ .maxBalanceTransferAllowedInCents(100000)
+ .reward(RewardsType.POINTS.name())
+ .cardBackgroundHex("#3a34eb")
+ .build();
+
+ public static final DefineCreditCardProductCommand PLATINUM = DefineCreditCardProductCommand.builder()
+ .productIdentifierForAggregateIdHash("PLATINUM_CREDIT_CARD")
+ .name("Platinum")
+ .interestInBasisPoints(300)
+ .annualFeeInCents(50000)
+ .paymentCycle(PaymentCycle.QUARTERLY.name())
+ .creditLimitInCents(5000000)
+ .maxBalanceTransferAllowedInCents(100000)
+ .reward(RewardsType.POINTS.name())
+ .cardBackgroundHex("#E5E4E2")
+ .build();
+}
diff --git a/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/util/DefaultCardCreator.java b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/util/DefaultCardCreator.java
new file mode 100644
index 00000000..a3d3bed7
--- /dev/null
+++ b/application/java-credit-card-product-service/src/main/java/cloud/ambar/creditCardProduct/util/DefaultCardCreator.java
@@ -0,0 +1,37 @@
+package cloud.ambar.creditCardProduct.util;
+
+import cloud.ambar.creditCardProduct.command.CreditCardProductCommandService;
+import lombok.RequiredArgsConstructor;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.stereotype.Component;
+
+import static cloud.ambar.creditCardProduct.util.Constants.BASIC;
+import static cloud.ambar.creditCardProduct.util.Constants.STARTER;
+import static cloud.ambar.creditCardProduct.util.Constants.BASIC_CASH_BACK;
+import static cloud.ambar.creditCardProduct.util.Constants.BASIC_POINTS;
+import static cloud.ambar.creditCardProduct.util.Constants.PLATINUM;
+
+/**
+ * This is just a simple component which on startup of the application will try to define a few initial cards. This is
+ * just so that we have something to play with in our application.
+ */
+@Component
+@RequiredArgsConstructor
+public class DefaultCardCreator implements ApplicationRunner {
+ private static final Logger log = LogManager.getLogger(DefaultCardCreator.class);
+
+ private final CreditCardProductCommandService commandService;
+
+ @Override
+ public void run(ApplicationArguments args) throws Exception {
+ log.info("Defining initial card products");
+ commandService.handle(STARTER);
+ commandService.handle(BASIC);
+ commandService.handle(BASIC_CASH_BACK);
+ commandService.handle(BASIC_POINTS);
+ commandService.handle(PLATINUM);
+ }
+}
diff --git a/infrastructure/ambar/production/data_destination_creditcardproduct_product.tf b/infrastructure/ambar/production/data_destination_creditcardproduct_product.tf
index 63149b25..534390a5 100644
--- a/infrastructure/ambar/production/data_destination_creditcardproduct_product.tf
+++ b/infrastructure/ambar/production/data_destination_creditcardproduct_product.tf
@@ -1,4 +1,4 @@
-resource "ambar_data_destination" "CreditCardProduct_Product" {
+resource "ambar_data_destination" "CreditCardProduct_Product_projector" {
filter_ids = [
ambar_filter.credit_card_product.resource_id,
]