From a50b2d50fa973ebcc88a91101380d5dcd30e0226 Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Thu, 21 Nov 2024 18:58:12 +0000
Subject: [PATCH 01/24] product-api: flyway and 1st migration

---
 pom.xml                                             |  8 ++++++++
 src/main/resources/application.properties           |  2 +-
 .../resources/db/migration/V1__Product_Table.sql    | 13 +++++++++++++
 3 files changed, 22 insertions(+), 1 deletion(-)
 create mode 100644 src/main/resources/db/migration/V1__Product_Table.sql

diff --git a/pom.xml b/pom.xml
index adc788a..0385289 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,6 +57,14 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-validation</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.flywaydb</groupId>
+            <artifactId>flyway-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.flywaydb</groupId>
+            <artifactId>flyway-database-postgresql</artifactId>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index a0398e6..feb5b79 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -4,7 +4,7 @@ spring.datasource.username=amigoscode
 spring.datasource.password=
 spring.datasource.driver-class-name=org.postgresql.Driver
 
-spring.jpa.hibernate.ddl-auto=create-drop
+spring.jpa.hibernate.ddl-auto=validate
 spring.jpa.show-sql=true
 spring.jpa.properties.hibernate.format_sql=true
 
diff --git a/src/main/resources/db/migration/V1__Product_Table.sql b/src/main/resources/db/migration/V1__Product_Table.sql
new file mode 100644
index 0000000..08d38cd
--- /dev/null
+++ b/src/main/resources/db/migration/V1__Product_Table.sql
@@ -0,0 +1,13 @@
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+
+CREATE TABLE IF NOT EXISTS product (
+    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+    name VARCHAR(50) NOT NULL CHECK (char_length(name) > 1),
+    description VARCHAR(500) CHECK (char_length(description) >= 5),
+    price NUMERIC(10, 2) NOT NULL CHECK (price > 0),
+    image_url VARCHAR(200) CHECK (char_length(image_url) > 0),
+    stock_level INT NOT NULL DEFAULT 1 CHECK (stock_level >= 0),
+    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+    updated_at TIMESTAMPTZ DEFAULT NOW(),
+    deleted_at TIMESTAMPTZ
+);

From 789755dfe370c17849f590bebe9cf7e6a5765c68 Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Fri, 22 Nov 2024 14:36:12 +0000
Subject: [PATCH 02/24] exercise 1 sol

---
 .../resources/db/migration/V2__Add_Is_Published_To_Product.sql  | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 src/main/resources/db/migration/V2__Add_Is_Published_To_Product.sql

diff --git a/src/main/resources/db/migration/V2__Add_Is_Published_To_Product.sql b/src/main/resources/db/migration/V2__Add_Is_Published_To_Product.sql
new file mode 100644
index 0000000..4df7de6
--- /dev/null
+++ b/src/main/resources/db/migration/V2__Add_Is_Published_To_Product.sql
@@ -0,0 +1,2 @@
+ALTER TABLE product
+ADD COLUMN is_published BOOLEAN DEFAULT TRUE;
\ No newline at end of file

From fc8280fdd4d11175304efc95dba085e02437314e Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Fri, 22 Nov 2024 14:36:21 +0000
Subject: [PATCH 03/24] exercise 2 sol

---
 src/main/java/com/amigoscode/Main.java        | 25 +++++++++++++------
 .../java/com/amigoscode/product/Product.java  | 12 ++++++++-
 ...__Set_Product_Is_Published_To_Not_Null.sql |  3 +++
 3 files changed, 32 insertions(+), 8 deletions(-)
 create mode 100644 src/main/resources/db/migration/V3__Set_Product_Is_Published_To_Not_Null.sql

diff --git a/src/main/java/com/amigoscode/Main.java b/src/main/java/com/amigoscode/Main.java
index 2c4c17b..a17a7b6 100644
--- a/src/main/java/com/amigoscode/Main.java
+++ b/src/main/java/com/amigoscode/Main.java
@@ -21,15 +21,26 @@ public static void main(String[] args) {
     public CommandLineRunner commandLineRunner(
             ProductRepository productRepository) {
         return args -> {
-            Product product = new Product();
-            product.setName("Macbook Pro");
-            product.setDescription("Macbook Pro M4");
-            product.setPrice(new BigDecimal(3000));
-            product.setId(UUID.fromString(
+            Product product1 = new Product();
+            product1.setName("Macbook Pro");
+            product1.setDescription("Macbook Pro M4");
+            product1.setPrice(new BigDecimal(3000));
+            product1.setId(UUID.fromString(
                     "d95062e6-9f0b-4224-bc9d-d0723949848f")
             );
-            product.setStockLevel(100);
-            productRepository.save(product);
+            product1.setStockLevel(100);
+            productRepository.save(product1);
+
+            Product product2 = new Product();
+            product2.setId(UUID.fromString(
+                    "94d2cc8a-ad09-4902-a321-a6bf658e2463"
+            ));
+            product2.setName("Mouse");
+            product2.setDescription("LG Mouse");
+            product2.setPrice(new BigDecimal(78));
+            product2.setStockLevel(1000);
+
+            productRepository.save(product2);
         };
     }
 
diff --git a/src/main/java/com/amigoscode/product/Product.java b/src/main/java/com/amigoscode/product/Product.java
index b8044c7..a7a620d 100644
--- a/src/main/java/com/amigoscode/product/Product.java
+++ b/src/main/java/com/amigoscode/product/Product.java
@@ -38,6 +38,8 @@ public class Product {
 
     private Instant deletedAt;
 
+    private Boolean isPublished = true;
+
     public Product() {
     }
 
@@ -141,11 +143,19 @@ public void setDeletedAt(Instant deletedAt) {
         this.deletedAt = deletedAt;
     }
 
+    public Boolean getPublished() {
+        return isPublished;
+    }
+
+    public void setPublished(Boolean published) {
+        isPublished = published;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (o == null || getClass() != o.getClass()) return false;
         Product product = (Product) o;
-        return stockLevel == product.stockLevel && Objects.equals(id, product.id) && Objects.equals(name, product.name) && Objects.equals(description, product.description) && Objects.equals(price, product.price) && Objects.equals(imageUrl, product.imageUrl) && Objects.equals(createdAt, product.createdAt) && Objects.equals(updatedAt, product.updatedAt) && Objects.equals(deletedAt, product.deletedAt);
+        return Objects.equals(stockLevel, product.stockLevel) && Objects.equals(id, product.id) && Objects.equals(name, product.name) && Objects.equals(description, product.description) && Objects.equals(price, product.price) && Objects.equals(imageUrl, product.imageUrl) && Objects.equals(createdAt, product.createdAt) && Objects.equals(updatedAt, product.updatedAt) && Objects.equals(deletedAt, product.deletedAt);
     }
 
     @Override
diff --git a/src/main/resources/db/migration/V3__Set_Product_Is_Published_To_Not_Null.sql b/src/main/resources/db/migration/V3__Set_Product_Is_Published_To_Not_Null.sql
new file mode 100644
index 0000000..015cfdf
--- /dev/null
+++ b/src/main/resources/db/migration/V3__Set_Product_Is_Published_To_Not_Null.sql
@@ -0,0 +1,3 @@
+ALTER TABLE product
+ALTER COLUMN is_published
+SET NOT NULL;
\ No newline at end of file

From b6ab94aad0de780526739245cc535b3f22e06ce9 Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Fri, 22 Nov 2024 14:38:53 +0000
Subject: [PATCH 04/24] exercise 3 sol

---
 src/main/java/com/amigoscode/product/ProductService.java      | 4 ++++
 .../java/com/amigoscode/product/UpdateProductRequest.java     | 4 +++-
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java
index aa91192..9d13815 100644
--- a/src/main/java/com/amigoscode/product/ProductService.java
+++ b/src/main/java/com/amigoscode/product/ProductService.java
@@ -95,6 +95,10 @@ public void updateProduct(UUID id,
             product.setStockLevel(updateRequest.stockLevel());
         }
 
+        if (updateRequest.isPublished() != null && !updateRequest.isPublished().equals(product.getPublished())) {
+            product.setPublished(updateRequest.isPublished());
+        }
+
         productRepository.save(product);
     }
 }
diff --git a/src/main/java/com/amigoscode/product/UpdateProductRequest.java b/src/main/java/com/amigoscode/product/UpdateProductRequest.java
index 50a02f7..5c8191f 100644
--- a/src/main/java/com/amigoscode/product/UpdateProductRequest.java
+++ b/src/main/java/com/amigoscode/product/UpdateProductRequest.java
@@ -28,6 +28,8 @@ public record UpdateProductRequest(
         BigDecimal price,
 
         @Min(value = 1, message = "Min Stock Level is 1")
-        Integer stockLevel
+        Integer stockLevel,
+
+        Boolean isPublished
 ) {
 }

From b408bfe0e778594d32d215df5affb181240351c3 Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Tue, 26 Nov 2024 09:48:26 +0000
Subject: [PATCH 05/24] product-service-docker: Exercise jar solution

---
 pom.xml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/pom.xml b/pom.xml
index 0385289..8c0139a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,6 +11,7 @@
     <groupId>com.amigoscode</groupId>
     <artifactId>jfs</artifactId>
     <version>0.0.1-SNAPSHOT</version>
+    <packaging>jar</packaging>
     <name>java-springboot-full-stack</name>
     <description>java-springboot-full-stack</description>
     <url/>
@@ -68,6 +69,7 @@
     </dependencies>
 
     <build>
+        <finalName>product-service</finalName>
         <plugins>
             <plugin>
                 <groupId>org.springframework.boot</groupId>

From 6966c02ce48c9a48921e612672bb54169cc2aa46 Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Tue, 26 Nov 2024 11:26:05 +0000
Subject: [PATCH 06/24] product-service-docker: docker compose

---
 Dockerfile | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)
 create mode 100644 Dockerfile

diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..3867cfa
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,16 @@
+# Build argument for Java version
+ARG JAVA_VERSION=21
+
+# Stage 1: Build the application
+FROM maven:3.9.4 AS builder
+WORKDIR /app
+COPY pom.xml .
+COPY src ./src
+RUN mvn clean package
+
+# Stage 2: Run the application
+FROM openjdk:${JAVA_VERSION}-jdk-slim
+WORKDIR /app
+COPY --from=builder /app/target/product-service.jar app.jar
+EXPOSE 8080
+ENTRYPOINT ["java", "-jar", "app.jar"]

From 37dd720651687c591d780e161b10aac52b35ca6b Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Tue, 26 Nov 2024 14:27:26 +0000
Subject: [PATCH 07/24] product-service-docker: exercise docker compose sol

---
 docker-compose.yml | 35 +++++++++++++++++++++++++++++++++++
 1 file changed, 35 insertions(+)
 create mode 100644 docker-compose.yml

diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..3683058
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,35 @@
+services:
+  product-service:
+      container_name: product
+      build:
+        context: .
+        dockerfile: Dockerfile
+      environment:
+        SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/jfs
+        SPRING_DATASOURCE_USERNAME: amigoscode
+        SPRING_DATASOURCE_PASSWORD: password
+      ports:
+        - "8090:8080"
+      networks:
+        - amigos
+  db:
+    container_name: jfs-postgres
+    image: postgres
+    environment:
+      POSTGRES_USER: amigoscode
+      POSTGRES_PASSWORD: password
+      POSTGRES_DB: jfs
+    ports:
+      - "5333:5432"
+    restart: unless-stopped
+    volumes:
+      - db:/data/postgres
+    networks:
+      - amigos
+
+networks:
+  amigos:
+    driver: bridge
+
+volumes:
+  db:

From 99c7dc821fc8716672b433fc70f0b5d3484be3b6 Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Tue, 26 Nov 2024 15:24:21 +0000
Subject: [PATCH 08/24] product-service-docker: jib and maven build

---
 docker-compose.yml |  1 +
 pom.xml            | 29 +++++++++++++++++++++++++++++
 2 files changed, 30 insertions(+)

diff --git a/docker-compose.yml b/docker-compose.yml
index 3683058..e8ac947 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,7 @@
 services:
   product-service:
       container_name: product
+#      image: amigoscode/product-service:jibMaven
       build:
         context: .
         dockerfile: Dockerfile
diff --git a/pom.xml b/pom.xml
index 8c0139a..87d8f49 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,6 +29,9 @@
     </scm>
     <properties>
         <java.version>21</java.version>
+        <docker.username>amigoscode</docker.username>
+        <docker.image.name>product-service</docker.image.name>
+        <docker.image.tag/>
     </properties>
     <dependencies>
         <dependency>
@@ -75,6 +78,32 @@
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
             </plugin>
+            <plugin>
+                <groupId>com.google.cloud.tools</groupId>
+                <artifactId>jib-maven-plugin</artifactId>
+                <version>3.3.2</version>
+                <configuration>
+                    <from>
+                        <image>eclipse-temurin:21-jre</image>
+                        <platforms>
+                            <platrom>
+                                <architecture>amd64</architecture>
+                                <os>linux</os>
+                            </platrom>
+                            <platrom>
+                                <architecture>arm64</architecture>
+                                <os>linux</os>
+                            </platrom>
+                        </platforms>
+                    </from>
+                    <to>
+                        <image>docker.io/${docker.username}/${docker.image.name}:${docker.image.tag}</image>
+                        <tags>
+                            <tag>latest</tag>
+                        </tags>
+                    </to>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 

From 7ecc61a19daed9293d2b1ccacf823524edfbf99c Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Tue, 3 Jun 2025 16:15:40 +0100
Subject: [PATCH 09/24] testcontainers

---
 docker-compose-db-only.yml                    |  22 ++++
 pom.xml                                       |  12 ++
 .../amigoscode/product/ProductRepository.java |   5 +
 .../amigoscode/product/ProductService.java    |   5 +-
 .../amigoscode/SharedPostgresContainer.java   |  42 +++++++
 .../product/ProductRepositoryTest.java        |  95 ++++++++++++++
 .../product/ProductServiceTest.java           | 117 ++++++++++++++++++
 7 files changed, 294 insertions(+), 4 deletions(-)
 create mode 100644 docker-compose-db-only.yml
 create mode 100644 src/test/java/com/amigoscode/SharedPostgresContainer.java
 create mode 100644 src/test/java/com/amigoscode/product/ProductRepositoryTest.java
 create mode 100644 src/test/java/com/amigoscode/product/ProductServiceTest.java

diff --git a/docker-compose-db-only.yml b/docker-compose-db-only.yml
new file mode 100644
index 0000000..abbc1fe
--- /dev/null
+++ b/docker-compose-db-only.yml
@@ -0,0 +1,22 @@
+services:
+  db-local-postgres:
+    container_name: jfs-postgres-local
+    image: postgres
+    environment:
+      POSTGRES_USER: amigoscode
+      POSTGRES_PASSWORD: password
+      POSTGRES_DB: jfs
+    ports:
+      - "5333:5432"
+    restart: unless-stopped
+    volumes:
+      - db-local:/data/postgres
+    networks:
+      - amigos
+
+networks:
+  amigos:
+    driver: bridge
+
+volumes:
+  db-local:
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 87d8f49..ba3a809 100644
--- a/pom.xml
+++ b/pom.xml
@@ -69,6 +69,18 @@
             <groupId>org.flywaydb</groupId>
             <artifactId>flyway-database-postgresql</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>junit-jupiter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>postgresql</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-testcontainers</artifactId>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/src/main/java/com/amigoscode/product/ProductRepository.java b/src/main/java/com/amigoscode/product/ProductRepository.java
index 0762e60..fc88d02 100644
--- a/src/main/java/com/amigoscode/product/ProductRepository.java
+++ b/src/main/java/com/amigoscode/product/ProductRepository.java
@@ -1,9 +1,14 @@
 package com.amigoscode.product;
 
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
 
+import java.util.List;
 import java.util.UUID;
 
 public interface ProductRepository
         extends JpaRepository<Product, UUID> {
+
+    @Query("SELECT p FROM Product p WHERE p.isPublished AND p.stockLevel > 0 ORDER BY p.price ASC")
+    List<Product> findAvailablePublishedProducts();
 }
diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java
index 9d13815..15c35d9 100644
--- a/src/main/java/com/amigoscode/product/ProductService.java
+++ b/src/main/java/com/amigoscode/product/ProductService.java
@@ -2,9 +2,6 @@
 
 import com.amigoscode.exception.ResourceNotFound;
 import org.springframework.stereotype.Service;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.RequestBody;
 
 import java.util.List;
 import java.util.UUID;
@@ -57,7 +54,7 @@ public UUID saveNewProduct(NewProductRequest product) {
         return id;
     }
 
-    private Function<Product, ProductResponse> mapToResponse() {
+    Function<Product, ProductResponse> mapToResponse() {
         return p -> new ProductResponse(
                 p.getId(),
                 p.getName(),
diff --git a/src/test/java/com/amigoscode/SharedPostgresContainer.java b/src/test/java/com/amigoscode/SharedPostgresContainer.java
new file mode 100644
index 0000000..83ce976
--- /dev/null
+++ b/src/test/java/com/amigoscode/SharedPostgresContainer.java
@@ -0,0 +1,42 @@
+package com.amigoscode;
+
+import org.flywaydb.core.Flyway;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.utility.DockerImageName;
+
+public class SharedPostgresContainer extends PostgreSQLContainer<SharedPostgresContainer> {
+    private static final DockerImageName IMAGE_NAME
+            = DockerImageName.parse("postgres:17-alpine");
+
+    private static volatile SharedPostgresContainer sharedPostgresContainer;
+
+    public SharedPostgresContainer(DockerImageName dockerImageName) {
+        super(dockerImageName);
+        this.withReuse(true)
+                .withUsername("amigoscode")
+                .withDatabaseName("amigos")
+                .withLabel("name", "amigscode")
+                .withPassword("password");
+    }
+
+    public static SharedPostgresContainer getInstance() {
+        if(sharedPostgresContainer == null) {
+            synchronized (SharedPostgresContainer.class) {
+                sharedPostgresContainer = new SharedPostgresContainer(
+                        IMAGE_NAME
+                );
+                sharedPostgresContainer.start();
+                Flyway flyway = Flyway.configure()
+                        .dataSource(
+                                sharedPostgresContainer.getJdbcUrl(),
+                                sharedPostgresContainer.getUsername(),
+                                sharedPostgresContainer.getPassword()
+                        )
+                        .load();
+                flyway.migrate();
+                System.out.println("flyway applied migrations");
+            }
+        }
+        return sharedPostgresContainer;
+    }
+}
diff --git a/src/test/java/com/amigoscode/product/ProductRepositoryTest.java b/src/test/java/com/amigoscode/product/ProductRepositoryTest.java
new file mode 100644
index 0000000..96ee6ad
--- /dev/null
+++ b/src/test/java/com/amigoscode/product/ProductRepositoryTest.java
@@ -0,0 +1,95 @@
+package com.amigoscode.product;
+
+import com.amigoscode.SharedPostgresContainer;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DataJpaTest
+@AutoConfigureTestDatabase(
+        replace = AutoConfigureTestDatabase.Replace.NONE
+)
+@Testcontainers
+class ProductRepositoryTest {
+
+    @Container
+    @ServiceConnection
+    private static final SharedPostgresContainer POSTGRES =
+            SharedPostgresContainer.getInstance();
+
+
+    @Autowired
+    private ProductRepository underTest;
+
+    @BeforeEach
+    void setUp() {
+        underTest.deleteAll();
+    }
+
+    @Test
+    void canFindAvailablePublishedProducts() {
+        // given
+        Product product1 = new Product(
+                UUID.randomUUID(),
+                "iphone",
+                "bardnjknjkndsjknkjnajkndjksandsajkndkjasnkdjank",
+                new BigDecimal("1000"),
+                "https://amigoscode.com/logo.png",
+                10
+        );
+
+        Product product2 = new Product(
+                UUID.randomUUID(),
+                "samsung",
+                "bardnjknjkndsjknkjnajkndjksandsajkndkjasnkdjank",
+                new BigDecimal("1200"),
+                "https://amigoscode.com/logo.png",
+                5
+        );
+        product2.setPublished(false);
+
+        Product product3 = new Product(
+                UUID.randomUUID(),
+                "watch",
+                "bardnjknjkndsjknkjnajkndjksandsajkndkjasnkdjank",
+                new BigDecimal("5000"),
+                "https://amigoscode.com/logo.png",
+                0
+        );
+
+        Product product4 = new Product(
+                UUID.randomUUID(),
+                "PS5",
+                "bardnjknjkndsjknkjnajkndjksandsajkndkjasnkdjank",
+                new BigDecimal("300"),
+                "https://amigoscode.com/logo.png",
+                90
+        );
+
+        underTest.saveAll(
+                List.of(product1, product2, product3, product4)
+        );
+        // when
+        List<Product> availablePublishedProducts =
+                underTest.findAvailablePublishedProducts();
+        // then
+        assertThat(availablePublishedProducts)
+                .usingRecursiveFieldByFieldElementComparatorIgnoringFields(
+                        "updatedAt", "createdAt"
+                )
+                .containsExactly(
+                        product4, product1
+                );
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/com/amigoscode/product/ProductServiceTest.java b/src/test/java/com/amigoscode/product/ProductServiceTest.java
new file mode 100644
index 0000000..c5b3f31
--- /dev/null
+++ b/src/test/java/com/amigoscode/product/ProductServiceTest.java
@@ -0,0 +1,117 @@
+package com.amigoscode.product;
+
+import com.amigoscode.SharedPostgresContainer;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.context.annotation.Import;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@DataJpaTest
+@AutoConfigureTestDatabase(
+        replace = AutoConfigureTestDatabase.Replace.NONE
+)
+@Import({
+        ProductService.class
+})
+@Testcontainers
+class ProductServiceTest {
+
+    @Container
+    @ServiceConnection
+    private static final SharedPostgresContainer POSTGRES =
+            SharedPostgresContainer.getInstance();
+
+    @Autowired
+    private ProductRepository productRepository;
+
+    @Autowired
+    private ProductService underTest;
+
+    @BeforeAll
+    static void beforeAll() {
+        System.out.println(POSTGRES.getDatabaseName());
+        System.out.println(POSTGRES.getJdbcUrl());
+        System.out.println(POSTGRES.getPassword());
+        System.out.println(POSTGRES.getDriverClassName());
+        System.out.println(POSTGRES.getTestQueryString());
+    }
+
+    @BeforeEach
+    void setUp() {
+        productRepository.deleteAll();
+    }
+
+    @Test
+    @Disabled
+    void canGetAllProducts() {
+        // given
+        Product product = new Product(
+                UUID.randomUUID(),
+                "foo",
+                "bardnjknjkndsjknkjnajkndjksandsajkndkjasnkdjank",
+                BigDecimal.TEN,
+                "https://amigoscode.com/logo.png",
+                10
+        );
+
+        productRepository.save(product);
+
+        // when
+        List<ProductResponse> allProducts =
+                underTest.getAllProducts();
+        // then
+        ProductResponse expected = underTest.mapToResponse().apply(product);
+
+        assertThat(allProducts)
+                .usingRecursiveFieldByFieldElementComparatorIgnoringFields(
+                        "updatedAt", "createdAt"
+                )
+                .containsOnly(expected);
+
+    }
+
+    @Test
+    @Disabled
+    void getProductById() {
+        // given
+        // when
+        // then
+    }
+
+    @Test
+    @Disabled
+    void deleteProductById() {
+        // given
+        // when
+        // then
+    }
+
+    @Test
+    @Disabled
+    void saveNewProduct() {
+        // given
+        // when
+        // then
+    }
+
+    @Test
+    @Disabled
+    void updateProduct() {
+        // given
+        // when
+        // then
+    }
+}
\ No newline at end of file

From 3a7ff6004ba2ea134d381f1cc4c671b891579375 Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Thu, 26 Jun 2025 18:01:34 +0100
Subject: [PATCH 10/24] integration tests

---
 pom.xml                                       |  25 ++
 src/main/java/com/amigoscode/Main.java        |   2 +-
 .../amigoscode/product/ProductController.java |   3 +-
 .../amigoscode/product/ProductResponse.java   |   2 +-
 .../amigoscode/product/ProductService.java    |   1 +
 src/main/resources/application.properties     |   4 +-
 .../com/amigoscode/AbstractTestConfig.java    |  20 ++
 .../com/amigoscode/journey/ProductIT.java     | 252 ++++++++++++++++++
 8 files changed, 304 insertions(+), 5 deletions(-)
 create mode 100644 src/test/java/com/amigoscode/AbstractTestConfig.java
 create mode 100644 src/test/java/com/amigoscode/journey/ProductIT.java

diff --git a/pom.xml b/pom.xml
index ba3a809..3b304bb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -81,6 +81,11 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-testcontainers</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
@@ -116,6 +121,26 @@
                     </to>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>**/*IntegrationTest.java</exclude>
+                        <exclude>**/*IT.java</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-failsafe-plugin</artifactId>
+                <configuration>
+                    <includes>
+                        <include>**/*IntegrationTest.java</include>
+                        <include>**/*IT.java</include>
+                    </includes>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 
diff --git a/src/main/java/com/amigoscode/Main.java b/src/main/java/com/amigoscode/Main.java
index a17a7b6..2278728 100644
--- a/src/main/java/com/amigoscode/Main.java
+++ b/src/main/java/com/amigoscode/Main.java
@@ -17,7 +17,7 @@ public static void main(String[] args) {
         SpringApplication.run(Main.class, args);
     }
 
-    @Bean
+    // @Bean
     public CommandLineRunner commandLineRunner(
             ProductRepository productRepository) {
         return args -> {
diff --git a/src/main/java/com/amigoscode/product/ProductController.java b/src/main/java/com/amigoscode/product/ProductController.java
index 42786c7..ef95609 100644
--- a/src/main/java/com/amigoscode/product/ProductController.java
+++ b/src/main/java/com/amigoscode/product/ProductController.java
@@ -1,7 +1,7 @@
 package com.amigoscode.product;
 
 import jakarta.validation.Valid;
-import org.springframework.validation.annotation.Validated;
+import org.springframework.http.HttpStatus;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
@@ -33,6 +33,7 @@ public void deleteProductById(@PathVariable("id") UUID id) {
     }
 
     @PostMapping
+    @ResponseStatus(HttpStatus.CREATED)
     public UUID saveProduct(@RequestBody @Valid NewProductRequest product) {
         return productService.saveNewProduct(product);
     }
diff --git a/src/main/java/com/amigoscode/product/ProductResponse.java b/src/main/java/com/amigoscode/product/ProductResponse.java
index 9df785a..8ff5d16 100644
--- a/src/main/java/com/amigoscode/product/ProductResponse.java
+++ b/src/main/java/com/amigoscode/product/ProductResponse.java
@@ -11,7 +11,7 @@ public record ProductResponse(
         BigDecimal price,
         String imageUrl,
         Integer stockLevel,
-        Instant createdAt,
+        boolean isPublished, Instant createdAt,
         Instant updatedAt,
         Instant deletedAt
 ) {
diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java
index 15c35d9..8bcfb9d 100644
--- a/src/main/java/com/amigoscode/product/ProductService.java
+++ b/src/main/java/com/amigoscode/product/ProductService.java
@@ -62,6 +62,7 @@ Function<Product, ProductResponse> mapToResponse() {
                 p.getPrice(),
                 p.getImageUrl(),
                 p.getStockLevel(),
+                p.getPublished(),
                 p.getCreatedAt(),
                 p.getUpdatedAt(),
                 p.getDeletedAt()
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index feb5b79..0d37076 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,7 +1,7 @@
 spring.application.name=jfs
-spring.datasource.url=jdbc:postgresql://localhost:5432/jfs
+spring.datasource.url=jdbc:postgresql://localhost:5333/jfs
 spring.datasource.username=amigoscode
-spring.datasource.password=
+spring.datasource.password=password
 spring.datasource.driver-class-name=org.postgresql.Driver
 
 spring.jpa.hibernate.ddl-auto=validate
diff --git a/src/test/java/com/amigoscode/AbstractTestConfig.java b/src/test/java/com/amigoscode/AbstractTestConfig.java
new file mode 100644
index 0000000..a9ff141
--- /dev/null
+++ b/src/test/java/com/amigoscode/AbstractTestConfig.java
@@ -0,0 +1,20 @@
+package com.amigoscode;
+
+import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.test.annotation.DirtiesContext;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@AutoConfigureWebTestClient
+@Testcontainers
+@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
+public abstract class AbstractTestConfig {
+
+    @Container
+    @ServiceConnection
+    private static final SharedPostgresContainer POSTGRES =
+            SharedPostgresContainer.getInstance();
+}
diff --git a/src/test/java/com/amigoscode/journey/ProductIT.java b/src/test/java/com/amigoscode/journey/ProductIT.java
new file mode 100644
index 0000000..dae920e
--- /dev/null
+++ b/src/test/java/com/amigoscode/journey/ProductIT.java
@@ -0,0 +1,252 @@
+package com.amigoscode.journey;
+
+import com.amigoscode.AbstractTestConfig;
+import com.amigoscode.product.NewProductRequest;
+import com.amigoscode.product.ProductResponse;
+import com.amigoscode.product.UpdateProductRequest;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.reactive.server.WebTestClient;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ProductIT extends AbstractTestConfig {
+
+    public static final String PRODUCT_BASE_URL = "api/v1/products";
+    @Autowired
+    private WebTestClient webTestClient;
+
+    @Test
+    void canCreateProduct() {
+        createProduct(new NewProductRequest(
+                "Laptop",
+                "1gb ram etc",
+                BigDecimal.TEN,
+                100,
+                "https://amigoscode.com/laptop.png"
+        ));
+    }
+
+    private UUID createProduct(NewProductRequest request) {
+        // when
+        return webTestClient
+                .post()
+                .uri(PRODUCT_BASE_URL)
+                .bodyValue(request)
+                .exchange()
+                .expectStatus()
+                .isCreated()
+                .expectBody(UUID.class)
+                .value(uuid -> assertThat(uuid).isNotNull())
+                .returnResult()
+                .getResponseBody();
+    }
+
+    @Test
+    void canGetAllProducts() {
+        // given 1st product
+        NewProductRequest laptop = new NewProductRequest(
+                "Laptop",
+                "1gb ram etc",
+                new BigDecimal("10.00"),
+                100,
+                "https://amigoscode.com/laptop.png"
+        );
+        var laptopId = createProduct(laptop);
+
+        // given 2nd product
+        NewProductRequest tv = new NewProductRequest(
+                "Tv",
+                "LED with crystal clear nano...",
+                new BigDecimal("1.00"),
+                50,
+                "https://amigoscode.com/tv.png"
+        );
+        var tvId = createProduct(tv);
+
+        // when
+        List<ProductResponse> products = webTestClient.get()
+                .uri(PRODUCT_BASE_URL)
+                .accept(MediaType.APPLICATION_JSON)
+                .exchange()
+                .expectStatus()
+                .isOk()
+                .expectBodyList(new ParameterizedTypeReference<ProductResponse>() {
+                })
+                .returnResult()
+                .getResponseBody();
+        // then
+        var productsFromDb = products.stream()
+                .filter(
+                        p -> p.id().equals(tvId) || p.id().equals(laptopId)
+                )
+                .toList();
+
+        assertThat(productsFromDb)
+                .hasSize(2)
+                .usingRecursiveFieldByFieldElementComparatorIgnoringFields(
+                        "createdAt", "updatedAt", "deletedAt"
+                )
+                .containsExactlyInAnyOrder(
+                        new ProductResponse(
+                                tvId,
+                                tv.name(),
+                                tv.description(),
+                                tv.price(),
+                                tv.imageUrl(),
+                                tv.stockLevel(),
+                                true, null, null, null
+                        ),
+                        new ProductResponse(
+                                laptopId,
+                                laptop.name(),
+                                laptop.description(),
+                                laptop.price(),
+                                laptop.imageUrl(),
+                                laptop.stockLevel(),
+                                true, null, null, null
+                        )
+                );
+    }
+
+    @Test
+    void canGetProductById() {
+        // given
+        NewProductRequest laptop = new NewProductRequest(
+                "Laptop",
+                "1gb ram etc",
+                new BigDecimal("10.00"),
+                100,
+                "https://amigoscode.com/laptop.png"
+        );
+        var laptopId = createProduct(laptop);
+
+        // when
+        ProductResponse responseBody = webTestClient.get()
+                .uri(PRODUCT_BASE_URL + "/" + laptopId)
+                .accept(MediaType.APPLICATION_JSON)
+                .exchange()
+                .expectStatus()
+                .isOk()
+                .expectBody(new ParameterizedTypeReference<ProductResponse>() {
+                })
+                .returnResult()
+                .getResponseBody();
+
+        // then
+        assertThat(responseBody)
+                .usingRecursiveComparison()
+                .ignoringFields("createdAt", "updatedAt", "deletedAt")
+                .isEqualTo(
+                        new ProductResponse(
+                                laptopId,
+                                laptop.name(),
+                                laptop.description(),
+                                laptop.price(),
+                                laptop.imageUrl(),
+                                laptop.stockLevel(),
+                                true, null, null, null
+                        )
+                );
+    }
+
+    @Test
+    void canGetDeleteProductById() {
+        // given
+        NewProductRequest laptop = new NewProductRequest(
+                "Laptop",
+                "1gb ram etc",
+                new BigDecimal("10.00"),
+                100,
+                "https://amigoscode.com/laptop.png"
+        );
+        var laptopId = createProduct(laptop);
+
+        // when
+        webTestClient.delete()
+                .uri(PRODUCT_BASE_URL + "/" + laptopId)
+                .accept(MediaType.APPLICATION_JSON)
+                .exchange()
+                .expectStatus()
+                .isOk()
+                .expectBody()
+                .isEmpty();
+
+        // then
+        webTestClient.get()
+                .uri(PRODUCT_BASE_URL + "/" + laptopId)
+                .accept(MediaType.APPLICATION_JSON)
+                .exchange()
+                .expectStatus()
+                .isNotFound()
+                .expectBody()
+                .jsonPath("$.message").isEqualTo("product with id [" + laptopId +"] not found")
+                .jsonPath("$.error").isEqualTo("Not Found")
+                .jsonPath("$.statusCode").isEqualTo("404")
+                .jsonPath("$.path").isEqualTo("/api/v1/products/" + laptopId)
+                .jsonPath("$.fieldError").doesNotExist();
+
+//        {"message":"product with id [02682eec-6d8e-43ac-a5c8-60dd7bb6fa14] not found","error":"Not Found","statusCode":404,"path":"/api/v1/products/02682eec-6d8e-43ac-a5c8-60dd7bb6fa14","timestamp":"2025-06-26T16:00:01.854366Z","fieldErrors":null}
+
+
+    }
+
+    @Test
+    void canUpdateProduct() {
+        // given
+        NewProductRequest laptop = new NewProductRequest(
+                "Laptop",
+                "1gb ram etc",
+                new BigDecimal("10.00"),
+                100,
+                "https://amigoscode.com/laptop.png"
+        );
+        var laptopId = createProduct(laptop);
+
+        // when
+        webTestClient.put()
+                .uri(PRODUCT_BASE_URL + "/" + laptopId)
+                .accept(MediaType.APPLICATION_JSON)
+                .bodyValue(new UpdateProductRequest(
+                        null, null, null, new BigDecimal("200.00"), 500, false
+                ))
+                .exchange()
+                .expectStatus()
+                .isOk()
+                .expectBody()
+                .isEmpty();
+        // then
+        ProductResponse responseBody = webTestClient.get()
+                .uri(PRODUCT_BASE_URL + "/" + laptopId)
+                .accept(MediaType.APPLICATION_JSON)
+                .exchange()
+                .expectStatus()
+                .isOk()
+                .expectBody(new ParameterizedTypeReference<ProductResponse>() {
+                })
+                .returnResult()
+                .getResponseBody();
+
+        // then
+        assertThat(responseBody)
+                .usingRecursiveComparison()
+                .ignoringFields("createdAt", "updatedAt", "deletedAt")
+                .isEqualTo(
+                        new ProductResponse(
+                                laptopId,
+                                laptop.name(),
+                                laptop.description(),
+                                new BigDecimal("200.00"),
+                                laptop.imageUrl(),
+                                500,
+                                false, null, null, null
+                        )
+                );
+    }
+}

From 8f237f67932511aadb6e0d9490255ad35c087adb Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Mon, 14 Jul 2025 12:14:08 +0100
Subject: [PATCH 11/24] build workflow

---
 .github/workflows/build.yml | 11 +++++++++++
 1 file changed, 11 insertions(+)
 create mode 100644 .github/workflows/build.yml

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..4519a61
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,11 @@
+name: Build workflow
+on:
+  pull_request:
+    branches:
+      - main
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Test
+        run: echo "Amigoscode"
\ No newline at end of file

From bb886966c6bc6484b38894cabc69258240b492c6 Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Mon, 14 Jul 2025 12:31:42 +0100
Subject: [PATCH 12/24] add checkout and jdk actions

---
 .github/workflows/build.yml | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 4519a61..8e4ff7f 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,5 +7,12 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - name: Test
-        run: echo "Amigoscode"
\ No newline at end of file
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: JDK
+        uses: actions/setup-java@v4
+        with:
+          distribution: 'temurin'
+          java-version: '21'
+      - name: Maven Clean Verify
+        run: mvn -B -ntb clean verify

From 30aca28226c40299d9670fa7e20dfb885002a70f Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Mon, 14 Jul 2025 12:33:00 +0100
Subject: [PATCH 13/24] fix typo

---
 .github/workflows/build.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 8e4ff7f..b2e599a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -15,4 +15,4 @@ jobs:
           distribution: 'temurin'
           java-version: '21'
       - name: Maven Clean Verify
-        run: mvn -B -ntb clean verify
+        run: mvn -B -ntp clean verify

From 446d06f6f253ccb9026761fcb3a3b478ca99f778 Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Mon, 14 Jul 2025 12:38:41 +0100
Subject: [PATCH 14/24] change name to foo

---
 src/main/java/com/amigoscode/product/ProductService.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java
index 8bcfb9d..a1bb12a 100644
--- a/src/main/java/com/amigoscode/product/ProductService.java
+++ b/src/main/java/com/amigoscode/product/ProductService.java
@@ -57,7 +57,7 @@ public UUID saveNewProduct(NewProductRequest product) {
     Function<Product, ProductResponse> mapToResponse() {
         return p -> new ProductResponse(
                 p.getId(),
-                p.getName(),
+                "foo",
                 p.getDescription(),
                 p.getPrice(),
                 p.getImageUrl(),

From ecb02ddd2309a647d6d451017ba14466f5675540 Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Mon, 14 Jul 2025 12:46:05 +0100
Subject: [PATCH 15/24] fix bug

---
 src/main/java/com/amigoscode/product/ProductService.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java
index a1bb12a..8bcfb9d 100644
--- a/src/main/java/com/amigoscode/product/ProductService.java
+++ b/src/main/java/com/amigoscode/product/ProductService.java
@@ -57,7 +57,7 @@ public UUID saveNewProduct(NewProductRequest product) {
     Function<Product, ProductResponse> mapToResponse() {
         return p -> new ProductResponse(
                 p.getId(),
-                "foo",
+                p.getName(),
                 p.getDescription(),
                 p.getPrice(),
                 p.getImageUrl(),

From 5d83483c8f79774d8a76e06c6aa98e77c5f0810d Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Mon, 14 Jul 2025 13:09:39 +0100
Subject: [PATCH 16/24] introduce bug

---
 src/main/java/com/amigoscode/product/ProductService.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java
index 8bcfb9d..dcc9ae8 100644
--- a/src/main/java/com/amigoscode/product/ProductService.java
+++ b/src/main/java/com/amigoscode/product/ProductService.java
@@ -51,7 +51,7 @@ public UUID saveNewProduct(NewProductRequest product) {
                 product.stockLevel()
         );
         productRepository.save(newProduct);
-        return id;
+        return null;
     }
 
     Function<Product, ProductResponse> mapToResponse() {

From 467588602ff1c0a9931239b86147cadbd3d42b3d Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Mon, 14 Jul 2025 13:11:26 +0100
Subject: [PATCH 17/24] fix bug

---
 src/main/java/com/amigoscode/product/ProductService.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/com/amigoscode/product/ProductService.java b/src/main/java/com/amigoscode/product/ProductService.java
index dcc9ae8..8bcfb9d 100644
--- a/src/main/java/com/amigoscode/product/ProductService.java
+++ b/src/main/java/com/amigoscode/product/ProductService.java
@@ -51,7 +51,7 @@ public UUID saveNewProduct(NewProductRequest product) {
                 product.stockLevel()
         );
         productRepository.save(newProduct);
-        return null;
+        return id;
     }
 
     Function<Product, ProductResponse> mapToResponse() {

From 654ddddf2da8b968a6e9d6ac4d2a17910065ec3b Mon Sep 17 00:00:00 2001
From: Nelson Djalo <154710368+mama-samba-braima@users.noreply.github.com>
Date: Mon, 14 Jul 2025 16:19:26 +0100
Subject: [PATCH 18/24] Add Continuous Deployment with AWS

---
 .github/workflows/deploy.yml | 51 ++++++++++++++++++++++++++++++++++++
 1 file changed, 51 insertions(+)
 create mode 100644 .github/workflows/deploy.yml

diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..2313fa0
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,51 @@
+name: Deploy Workflow
+on:
+  push:
+    branches:
+      - main
+jobs:
+  build-and-deploy:
+    runs-on: ubuntu-latest
+    env:
+      DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
+      DOCKER_IMAGE_NAME: product-service
+      DOCKER_COMPOSE_APP_SERVICE_NAME: product-service
+      DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
+      EC2_SSH_PRIVATE_KEY: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
+      EC2_PUBLIC_IP_ADDRESS: ${{ secrets.EC2_PUBLIC_IP_ADDRESS }}
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: JDK
+        uses: actions/setup-java@v4
+        with:
+          distribution: 'temurin'
+          java-version: '21'
+      - name: Generate Build Number
+        run: |
+          BUILD_NUMBER=$(date +'%d%m%Y%H%M%S')
+          echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_ENV
+          echo "Generated Build Number: $BUILD_NUMBER"
+      - name: Login to Docker Hub
+        uses: docker/login-action@v3
+        with:
+          username: ${{ env.DOCKER_USERNAME }}
+          password: ${{ env.DOCKER_PASSWORD }}
+      - name: Maven Clean Verify
+        run: mvn -B -ntp clean verify jib:build -Ddocker.image.tag=$BUILD_NUMBER
+      - name: Setup SSH Agent
+        uses: webfactory/ssh-agent@v0.9.0
+        with:
+          ssh-private-key: ${{ env.EC2_SSH_PRIVATE_KEY }}
+      - name: SSH To EC2 and Deploy
+        run: |
+          ssh -o StrictHostKeyChecking=no ec2-user@$EC2_PUBLIC_IP_ADDRESS << EOF
+            cd ~
+            sed -i 's|image: '"$DOCKER_USERNAME"'/'"$DOCKER_IMAGE_NAME"':.*$|image: '"$DOCKER_USERNAME"'/'"$DOCKER_IMAGE_NAME"':'"$BUILD_NUMBER"'|' docker-compose.yml
+            docker-compose pull
+            docker-compose up -d '$DOCKER_COMPOSE_APP_SERVICE_NAME'
+            docker image prune -f
+            docker ps
+            echo "$BUILD_NUMBER" > build-number.txt
+          EOF

From 3b9d1a924555705aff1e2ab0529450cbd2d8b20c Mon Sep 17 00:00:00 2001
From: Nelson Djalo <154710368+mama-samba-braima@users.noreply.github.com>
Date: Mon, 14 Jul 2025 16:43:56 +0100
Subject: [PATCH 19/24] ping pong controller (#4)

---
 .../amigoscode/pingpong/PingPongController.java  | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)
 create mode 100644 src/main/java/com/amigoscode/pingpong/PingPongController.java

diff --git a/src/main/java/com/amigoscode/pingpong/PingPongController.java b/src/main/java/com/amigoscode/pingpong/PingPongController.java
new file mode 100644
index 0000000..69577c6
--- /dev/null
+++ b/src/main/java/com/amigoscode/pingpong/PingPongController.java
@@ -0,0 +1,16 @@
+package com.amigoscode.pingpong;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("api/v1/ping")
+public class PingPongController {
+
+    @GetMapping
+    public String pingPong() {
+        return "pong";
+    }
+
+}

From b601860569593e36344f3116f89719bda6b6f179 Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Sat, 19 Jul 2025 11:34:33 +0100
Subject: [PATCH 20/24] add workflow dispatch build number as input

---
 .github/workflows/deploy.yml | 28 ++++++++++++++++++++++++++--
 1 file changed, 26 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 2313fa0..2a43b67 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -3,6 +3,12 @@ on:
   push:
     branches:
       - main
+  workflow_dispatch:
+    inputs:
+      build_number:
+        description: "The build number to deploy (optional)"
+        required: false
+        type: string
 jobs:
   build-and-deploy:
     runs-on: ubuntu-latest
@@ -16,23 +22,41 @@ jobs:
 
     steps:
       - name: Checkout
+        if: ${{ github.event.inputs.build_number == '' }}
         uses: actions/checkout@v4
       - name: JDK
+        if: ${{ github.event.inputs.build_number == '' }}
         uses: actions/setup-java@v4
         with:
           distribution: 'temurin'
           java-version: '21'
       - name: Generate Build Number
         run: |
-          BUILD_NUMBER=$(date +'%d%m%Y%H%M%S')
+          if [ -n "${{ github.event.inputs.build_number }}"]; then
+            BUILD_NUMBER = ${{ github.event.inputs.build_number }}
+            echo "using provided build number: $BUILD_NUMBER"
+          else
+            BUILD_NUMBER=$(date +'%d%m%Y%H%M%S')
+            echo "using generated build number: $BUILD_NUMBER"
+          fi
           echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_ENV
-          echo "Generated Build Number: $BUILD_NUMBER"
       - name: Login to Docker Hub
         uses: docker/login-action@v3
         with:
           username: ${{ env.DOCKER_USERNAME }}
           password: ${{ env.DOCKER_PASSWORD }}
+      - name: Check if provided docker image exists
+        if: ${{ github.event.inputs.build_number != '' }}
+        run: |
+          IMAGE_NAME="${{ env.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGE_NAME }}:$BUILD_NUMBER"
+          echo "checking if $IMAGE_NAME exists"
+          if ! docker manifest inspect "$IMAGE_NAME" > /dev/null 2>&1; then
+            echo "provided image $IMAGE_NAME does not exits. exiting..."
+            exit 1
+          fi
+          echo "provided image $IMAGE_NAME exits ✅"
       - name: Maven Clean Verify
+        if: ${{ github.event.inputs.build_number == '' }}
         run: mvn -B -ntp clean verify jib:build -Ddocker.image.tag=$BUILD_NUMBER
       - name: Setup SSH Agent
         uses: webfactory/ssh-agent@v0.9.0

From b5fd46c1467e84e6fa8165052be189aaa9953ded Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Sat, 19 Jul 2025 11:36:39 +0100
Subject: [PATCH 21/24] fix error

---
 .github/workflows/deploy.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 2a43b67..1f4c31c 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -32,8 +32,8 @@ jobs:
           java-version: '21'
       - name: Generate Build Number
         run: |
-          if [ -n "${{ github.event.inputs.build_number }}"]; then
-            BUILD_NUMBER = ${{ github.event.inputs.build_number }}
+          if [ -n "${{ github.event.inputs.build_number }}" ]; then
+            BUILD_NUMBER=${{ github.event.inputs.build_number }}
             echo "using provided build number: $BUILD_NUMBER"
           else
             BUILD_NUMBER=$(date +'%d%m%Y%H%M%S')

From 8868d40949e7da3d0b7411f64ae4caa64ee19174 Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Sat, 19 Jul 2025 13:11:02 +0100
Subject: [PATCH 22/24] add slack notifications to deploy.yml

---
 .github/workflows/deploy.yml | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 1f4c31c..777fbca 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -19,11 +19,17 @@ jobs:
       DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
       EC2_SSH_PRIVATE_KEY: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
       EC2_PUBLIC_IP_ADDRESS: ${{ secrets.EC2_PUBLIC_IP_ADDRESS }}
+      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
 
     steps:
       - name: Checkout
         if: ${{ github.event.inputs.build_number == '' }}
         uses: actions/checkout@v4
+      - name: Send slack message with github commit url
+        run: >
+          curl -X POST -H 'Content-type: application/json'
+          --data '{"text":"${{ github.event.head_commit.message }} - https://${{ github.repository }}/commit/${{ github.sha }}"}' 
+          ${{ env.SLACK_WEBHOOK_URL }}
       - name: JDK
         if: ${{ github.event.inputs.build_number == '' }}
         uses: actions/setup-java@v4
@@ -73,3 +79,9 @@ jobs:
             docker ps
             echo "$BUILD_NUMBER" > build-number.txt
           EOF
+      - name: Send slack message with status
+        if: always()
+        run: >
+          curl -X POST -H 'Content-type: application/json'
+          --data '{"text":"Job status ${{ job.status }}"}' 
+          ${{ env.SLACK_WEBHOOK_URL }}

From eb9e8f7b1ab9782583115454d4631348c18bfa29 Mon Sep 17 00:00:00 2001
From: mama-samba-braima <djalonelson@gmail.com>
Date: Sat, 19 Jul 2025 16:46:12 +0100
Subject: [PATCH 23/24] add more msgs to slack

---
 .github/workflows/deploy.yml | 38 +++++++++++++++++++++++++-----------
 1 file changed, 27 insertions(+), 11 deletions(-)

diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 777fbca..a3095af 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -28,7 +28,7 @@ jobs:
       - name: Send slack message with github commit url
         run: >
           curl -X POST -H 'Content-type: application/json'
-          --data '{"text":"${{ github.event.head_commit.message }} - https://${{ github.repository }}/commit/${{ github.sha }}"}' 
+          --data '{"text":":rocket: Deployment started for commit: ${{ github.event.head_commit.message }} - https://github.com/${{ github.repository }}/commit/${{ github.sha }}"}' 
           ${{ env.SLACK_WEBHOOK_URL }}
       - name: JDK
         if: ${{ github.event.inputs.build_number == '' }}
@@ -41,9 +41,11 @@ jobs:
           if [ -n "${{ github.event.inputs.build_number }}" ]; then
             BUILD_NUMBER=${{ github.event.inputs.build_number }}
             echo "using provided build number: $BUILD_NUMBER"
+            curl -X POST -H 'Content-type: application/json' --data "{\"text\":\":package: Using provided build number: $BUILD_NUMBER\"}" ${{ env.SLACK_WEBHOOK_URL }}
           else
             BUILD_NUMBER=$(date +'%d%m%Y%H%M%S')
             echo "using generated build number: $BUILD_NUMBER"
+            curl -X POST -H 'Content-type: application/json' --data "{\"text\":\":gear: Generated new build number: $BUILD_NUMBER\"}" ${{ env.SLACK_WEBHOOK_URL }}
           fi
           echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_ENV
       - name: Login to Docker Hub
@@ -54,22 +56,30 @@ jobs:
       - name: Check if provided docker image exists
         if: ${{ github.event.inputs.build_number != '' }}
         run: |
-          IMAGE_NAME="${{ env.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGE_NAME }}:$BUILD_NUMBER"
-          echo "checking if $IMAGE_NAME exists"
-          if ! docker manifest inspect "$IMAGE_NAME" > /dev/null 2>&1; then
-            echo "provided image $IMAGE_NAME does not exits. exiting..."
+          IMAGE="${{ env.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGE_NAME }}:$BUILD_NUMBER"
+          echo "checking if $IMAGE exists"
+          if ! docker manifest inspect "$IMAGE" > /dev/null 2>&1; then
+            echo "provided image $IMAGE does not exist. exiting..."
+            curl -X POST -H 'Content-type: application/json' --data "{\"text\":\":x: :warning: Provided image $IMAGE does not exist! Exiting deployment...\"}" ${{ env.SLACK_WEBHOOK_URL }}
             exit 1
           fi
-          echo "provided image $IMAGE_NAME exits ✅"
+          echo "provided image $IMAGE exists ✅"
+          curl -X POST -H 'Content-type: application/json' --data "{\"text\":\":white_check_mark: Image $IMAGE exists and ready for deployment\"}" ${{ env.SLACK_WEBHOOK_URL }}
+          echo "IMAGE=$IMAGE" >> $GITHUB_ENV
+
       - name: Maven Clean Verify
         if: ${{ github.event.inputs.build_number == '' }}
-        run: mvn -B -ntp clean verify jib:build -Ddocker.image.tag=$BUILD_NUMBER
+        run: |
+          curl -X POST -H 'Content-type: application/json' --data "{\"text\":\":hammer_and_wrench: Building and deploying to Docker Hub...\"}" ${{ env.SLACK_WEBHOOK_URL }}
+          mvn -B -ntp clean verify jib:build -Ddocker.image.tag=$BUILD_NUMBER
+          curl -X POST -H 'Content-type: application/json' --data "{\"text\":\":white_check_mark: Successfully built and pushed to Docker Hub\"}" ${{ env.SLACK_WEBHOOK_URL }}
       - name: Setup SSH Agent
         uses: webfactory/ssh-agent@v0.9.0
         with:
           ssh-private-key: ${{ env.EC2_SSH_PRIVATE_KEY }}
       - name: SSH To EC2 and Deploy
         run: |
+          curl -X POST -H 'Content-type: application/json' --data '{"text":":cloud: Deploying to AWS EC2..."}' ${{ env.SLACK_WEBHOOK_URL }}
           ssh -o StrictHostKeyChecking=no ec2-user@$EC2_PUBLIC_IP_ADDRESS << EOF
             cd ~
             sed -i 's|image: '"$DOCKER_USERNAME"'/'"$DOCKER_IMAGE_NAME"':.*$|image: '"$DOCKER_USERNAME"'/'"$DOCKER_IMAGE_NAME"':'"$BUILD_NUMBER"'|' docker-compose.yml
@@ -79,9 +89,15 @@ jobs:
             docker ps
             echo "$BUILD_NUMBER" > build-number.txt
           EOF
+          curl -X POST -H 'Content-type: application/json' --data "{\"text\":\":tada: Successfully deployed to AWS EC2\"}" ${{ env.SLACK_WEBHOOK_URL }}
       - name: Send slack message with status
         if: always()
-        run: >
-          curl -X POST -H 'Content-type: application/json'
-          --data '{"text":"Job status ${{ job.status }}"}' 
-          ${{ env.SLACK_WEBHOOK_URL }}
+        run: |
+          if [ "${{ job.status }}" == "success" ]; then
+            EMOJI=":tada:"
+            MESSAGE="Deployment completed successfully!"
+          else
+            EMOJI=":x:"
+            MESSAGE="Deployment failed!"
+          fi
+          curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"$EMOJI $MESSAGE - Job status: ${{ job.status }}\"}" ${{ env.SLACK_WEBHOOK_URL }}

From 47c4929b8fab51276c2ea69f68dfc878b2451462 Mon Sep 17 00:00:00 2001
From: Nelson Djalo <154710368+mama-samba-braima@users.noreply.github.com>
Date: Tue, 29 Jul 2025 00:50:32 +0100
Subject: [PATCH 24/24] add cors configuration via
 WebMvcConfigurer.addCorsMappings (#6)

---
 .../com/amigoscode/config/WebMvcConfig.java   | 28 +++++++++++++++++++
 src/main/resources/application.properties     |  5 +++-
 2 files changed, 32 insertions(+), 1 deletion(-)
 create mode 100644 src/main/java/com/amigoscode/config/WebMvcConfig.java

diff --git a/src/main/java/com/amigoscode/config/WebMvcConfig.java b/src/main/java/com/amigoscode/config/WebMvcConfig.java
new file mode 100644
index 0000000..f863e39
--- /dev/null
+++ b/src/main/java/com/amigoscode/config/WebMvcConfig.java
@@ -0,0 +1,28 @@
+package com.amigoscode.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.util.List;
+
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+//    cors.allowed.origins=*
+    @Value("#{'${cors.allowed.origins}'.split(',')}")
+    private List<String> allowedOrigins;
+
+//    cors.allowed.methods=*
+    @Value("#{'${cors.allowed.methods}'.split(',')}")
+    private List<String> allowedMethods;
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        CorsRegistration corsRegistration = registry.addMapping("/api/**");
+        allowedOrigins.forEach(corsRegistration::allowedOrigins);
+        allowedMethods.forEach(corsRegistration::allowedMethods);
+    }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 0d37076..ee25d64 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -8,4 +8,7 @@ spring.jpa.hibernate.ddl-auto=validate
 spring.jpa.show-sql=true
 spring.jpa.properties.hibernate.format_sql=true
 
-server.error.include-message=always
\ No newline at end of file
+server.error.include-message=always
+
+cors.allowed.origins=*
+cors.allowed.methods=*
