Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
26ca072
feat(catalog): add the Category and Product entities and dto classes
EdgarEldy Sep 19, 2026
eef96e8
feat(catalog): add the RowMappers and entity mappers of the catalog
EdgarEldy Sep 19, 2026
e7ff607
feat(catalog): add the category and product DAOs
EdgarEldy Sep 19, 2026
3760e00
feat(catalog): implement the category and product services
EdgarEldy Sep 19, 2026
fecd079
feat(catalog): wire the catalog DAOs and services explicitly
EdgarEldy Sep 19, 2026
53f4b96
feat(ws): add the catalog payloads and converters
EdgarEldy Sep 19, 2026
bab6a02
feat(ws): expose the category and product endpoints
EdgarEldy Sep 19, 2026
f79baf8
feat(ws): import the catalog module into the shared context
EdgarEldy Sep 19, 2026
d966788
build(catalog): add Testcontainers test dependencies to the catalog m…
EdgarEldy Sep 19, 2026
1b535c1
test(catalog): cover the category and product DAOs against PostgreSQL
EdgarEldy Sep 19, 2026
c247a96
test(catalog): test the catalog RowMappers and entity mappers
EdgarEldy Sep 19, 2026
aeee5c5
test(catalog): unit test the category and product services
EdgarEldy Sep 19, 2026
2bfaa85
test(ws): test the catalog controllers with MockMvc
EdgarEldy Sep 19, 2026
84fb7dd
test(ws): check the permission matrix of the 10 catalog endpoints
EdgarEldy Sep 19, 2026
1e85f54
test(ws): run the catalog flows on the real stack
EdgarEldy Sep 19, 2026
df70d22
docs(readme): tick the feature/catalog tasks
EdgarEldy Sep 19, 2026
de86bd6
fix(catalog): bound the price before scaling and handle a vanished ca…
EdgarEldy Sep 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,11 +401,11 @@ Full CRUD for users, roles, and permissions, still inside `core/auth`. Assignmen

### Tasks

- [ ] `core/catalog`: `Category`, `Product` entity classes (`@Table("categories")`/`@Table("products")`) and `CategoryDto`/`ProductDto`, `CategoryRowMapper`/`ProductRowMapper` (entity) plus `CategoryMapper`/`ProductMapper` (entity ↔ dto), `CategoryDao`/`ProductDao` + impl, `CategoryService`/`ProductService` + impl, `DaoConfig`/`ServiceConfig`
- [ ] Business rule: deleting a category that still has products is rejected (`BusinessRuleException` → 422)
- [ ] `ws`: `CategoryController`, `ProductController`, `CategoryConverter`, `ProductConverter`
- [ ] `WebMvcConfig` updated to import `core/catalog`'s config
- [ ] Tests at all five layers - `CategoryDao`/`ProductDao` each with their own fixture dataset, the category-deletion rejection case, a permission-denied case
- [x] `core/catalog`: `Category`, `Product` entity classes (`@Table("categories")`/`@Table("products")`) and `CategoryDto`/`ProductDto`, `CategoryRowMapper`/`ProductRowMapper` (entity) plus `CategoryMapper`/`ProductMapper` (entity ↔ dto), `CategoryDao`/`ProductDao` + impl, `CategoryService`/`ProductService` + impl, `DaoConfig`/`ServiceConfig`
- [x] Business rule: deleting a category that still has products is rejected (`BusinessRuleException` → 422)
- [x] `ws`: `CategoryController`, `ProductController`, `CategoryConverter`, `ProductConverter`
- [x] `WebMvcConfig` updated to import `core/catalog`'s config
- [x] Tests at all five layers - `CategoryDao`/`ProductDao` each with their own fixture dataset, the category-deletion rejection case, a permission-denied case

## feature/customer

Expand Down
17 changes: 17 additions & 0 deletions core/catalog/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,22 @@
<groupId>com.edgareldy.springjdbctutorial</groupId>
<artifactId>common</artifactId>
</dependency>
<!-- Test scope only: the DAO tests reuse the PostgreSQL Testcontainer support published by common -->
<dependency>
<groupId>com.edgareldy.springjdbctutorial</groupId>
<artifactId>common</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.edgareldy.springjdbctutorial.core.catalog.config;

import com.edgareldy.springjdbctutorial.core.catalog.dao.CategoryDao;
import com.edgareldy.springjdbctutorial.core.catalog.dao.ProductDao;
import com.edgareldy.springjdbctutorial.core.catalog.dao.impl.CategoryDaoImpl;
import com.edgareldy.springjdbctutorial.core.catalog.dao.impl.ProductDaoImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

/**
* Declares the DAO beans of the catalog module, typed by interface (no stereotype on the implementations).
* <p>
* Created edgar.muhamyangabo on 9/20/26
* Author : edgar.muhamyangabo
* Date : 9/20/26
* Project : spring-jdbc-tutorial
*/
@Configuration
public class DaoConfig {

@Bean
public CategoryDao categoryDao(JdbcTemplate jdbcTemplate) {
return new CategoryDaoImpl(jdbcTemplate);
}

@Bean
public ProductDao productDao(JdbcTemplate jdbcTemplate) {
return new ProductDaoImpl(jdbcTemplate);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.edgareldy.springjdbctutorial.core.catalog.config;

import com.edgareldy.springjdbctutorial.core.catalog.dao.CategoryDao;
import com.edgareldy.springjdbctutorial.core.catalog.dao.ProductDao;
import com.edgareldy.springjdbctutorial.core.catalog.service.CategoryService;
import com.edgareldy.springjdbctutorial.core.catalog.service.ProductService;
import com.edgareldy.springjdbctutorial.core.catalog.service.impl.CategoryServiceImpl;
import com.edgareldy.springjdbctutorial.core.catalog.service.impl.ProductServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
* Declares the service beans of the catalog module. Imports its DaoConfig so importing this class alone is enough.
* <p>
* Created edgar.muhamyangabo on 9/20/26
* Author : edgar.muhamyangabo
* Date : 9/20/26
* Project : spring-jdbc-tutorial
*/
@Configuration
@Import(DaoConfig.class)
public class ServiceConfig {

@Bean
public CategoryService categoryService(CategoryDao categoryDao, ProductDao productDao) {
return new CategoryServiceImpl(categoryDao, productDao);
}

@Bean
public ProductService productService(ProductDao productDao, CategoryDao categoryDao) {
return new ProductServiceImpl(productDao, categoryDao);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.edgareldy.springjdbctutorial.core.catalog.dao;

import com.edgareldy.springjdbctutorial.core.catalog.entity.Category;

import java.util.List;
import java.util.Optional;

/**
* Data access for categories. Entity types only.
* <p>
* Created edgar.muhamyangabo on 9/20/26
* Author : edgar.muhamyangabo
* Date : 9/20/26
* Project : spring-jdbc-tutorial
*/
public interface CategoryDao {

/** One page of categories ordered by id. */
List<Category> findPage(int page, int size);

long countAll();

Optional<Category> findById(Long id);

boolean existsByCategoryName(String categoryName);

/** Inserts the category and returns it with its generated id. */
Category insert(Category category);

void update(Category category);

void delete(Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.edgareldy.springjdbctutorial.core.catalog.dao;

import com.edgareldy.springjdbctutorial.core.catalog.entity.Product;

import java.util.List;
import java.util.Optional;

/**
* Data access for products. Entity types only.
* <p>
* Created edgar.muhamyangabo on 9/20/26
* Author : edgar.muhamyangabo
* Date : 9/20/26
* Project : spring-jdbc-tutorial
*/
public interface ProductDao {

/** One page of products ordered by id, restricted to a category when categoryId is not null. */
List<Product> findPage(int page, int size, Long categoryId);

/** Number of products, restricted to a category when categoryId is not null. */
long count(Long categoryId);

Optional<Product> findById(Long id);

/** Inserts the product and returns it with its generated id. */
Product insert(Product product);

void update(Product product);

void delete(Long id);

long countByCategoryId(Long categoryId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.edgareldy.springjdbctutorial.core.catalog.dao.impl;

import com.edgareldy.springjdbctutorial.core.catalog.dao.CategoryDao;
import com.edgareldy.springjdbctutorial.core.catalog.entity.Category;
import com.edgareldy.springjdbctutorial.core.catalog.mapper.CategoryRowMapper;
import com.edgareldy.springjdbctutorial.core.common.dao.support.AbstractDao;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.List;
import java.util.Optional;

/**
* JdbcTemplate implementation of CategoryDao. Declared by @Bean in DaoConfig, no stereotype.
* <p>
* Created edgar.muhamyangabo on 9/20/26
* Author : edgar.muhamyangabo
* Date : 9/20/26
* Project : spring-jdbc-tutorial
*/
public class CategoryDaoImpl extends AbstractDao implements CategoryDao {

private final CategoryRowMapper rowMapper = new CategoryRowMapper();

public CategoryDaoImpl(JdbcTemplate jdbcTemplate) {
super(jdbcTemplate);
}

@Override
public List<Category> findPage(int page, int size) {
return getJdbcTemplate().query(
"SELECT id, category_name FROM categories ORDER BY id LIMIT ? OFFSET ?",
rowMapper, size, (long) page * size);
}

@Override
public long countAll() {
return count("SELECT COUNT(*) FROM categories");
}

@Override
public Optional<Category> findById(Long id) {
return findOne("SELECT id, category_name FROM categories WHERE id = ?", rowMapper, id);
}

@Override
public boolean existsByCategoryName(String categoryName) {
return count("SELECT COUNT(*) FROM categories WHERE category_name = ?", categoryName) > 0;
}

@Override
public Category insert(Category category) {
long id = insertReturningId("INSERT INTO categories (category_name) VALUES (?) RETURNING id",
category.getCategoryName());
category.setId(id);
return category;
}

@Override
public void update(Category category) {
getJdbcTemplate().update("UPDATE categories SET category_name = ? WHERE id = ?",
category.getCategoryName(), category.getId());
}

@Override
public void delete(Long id) {
getJdbcTemplate().update("DELETE FROM categories WHERE id = ?", id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.edgareldy.springjdbctutorial.core.catalog.dao.impl;

import com.edgareldy.springjdbctutorial.core.catalog.dao.ProductDao;
import com.edgareldy.springjdbctutorial.core.catalog.entity.Product;
import com.edgareldy.springjdbctutorial.core.catalog.mapper.ProductRowMapper;
import com.edgareldy.springjdbctutorial.core.common.dao.support.AbstractDao;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.List;
import java.util.Optional;

/**
* JdbcTemplate implementation of ProductDao. Declared by @Bean in DaoConfig, no stereotype.
* <p>
* Created edgar.muhamyangabo on 9/20/26
* Author : edgar.muhamyangabo
* Date : 9/20/26
* Project : spring-jdbc-tutorial
*/
public class ProductDaoImpl extends AbstractDao implements ProductDao {

private static final String COLUMNS = "SELECT id, category_id, product_name, unit_price FROM products";

private final ProductRowMapper rowMapper = new ProductRowMapper();

public ProductDaoImpl(JdbcTemplate jdbcTemplate) {
super(jdbcTemplate);
}

@Override
public List<Product> findPage(int page, int size, Long categoryId) {
long offset = (long) page * size;
if (categoryId == null) {
return getJdbcTemplate().query(COLUMNS + " ORDER BY id LIMIT ? OFFSET ?", rowMapper, size, offset);
}
return getJdbcTemplate().query(COLUMNS + " WHERE category_id = ? ORDER BY id LIMIT ? OFFSET ?",
rowMapper, categoryId, size, offset);
}

@Override
public long count(Long categoryId) {
if (categoryId == null) {
return count("SELECT COUNT(*) FROM products");
}
return countByCategoryId(categoryId);
}

@Override
public Optional<Product> findById(Long id) {
return findOne(COLUMNS + " WHERE id = ?", rowMapper, id);
}

@Override
public Product insert(Product product) {
long id = insertReturningId(
"INSERT INTO products (category_id, product_name, unit_price) VALUES (?, ?, ?) RETURNING id",
product.getCategoryId(), product.getProductName(), product.getUnitPrice());
product.setId(id);
return product;
}

@Override
public void update(Product product) {
getJdbcTemplate().update(
"UPDATE products SET category_id = ?, product_name = ?, unit_price = ? WHERE id = ?",
product.getCategoryId(), product.getProductName(), product.getUnitPrice(), product.getId());
}

@Override
public void delete(Long id) {
getJdbcTemplate().update("DELETE FROM products WHERE id = ?", id);
}

@Override
public long countByCategoryId(Long categoryId) {
return count("SELECT COUNT(*) FROM products WHERE category_id = ?", categoryId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.edgareldy.springjdbctutorial.core.catalog.dto;

import java.util.Objects;

/**
* Business shape of a category.
* <p>
* Created edgar.muhamyangabo on 9/20/26
* Author : edgar.muhamyangabo
* Date : 9/20/26
* Project : spring-jdbc-tutorial
*/
public class CategoryDto {

private Long id;

private String categoryName;

public CategoryDto() {
}

public CategoryDto(Long id, String categoryName) {
this.id = id;
this.categoryName = categoryName;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getCategoryName() {
return categoryName;
}

public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CategoryDto that = (CategoryDto) o;
return Objects.equals(id, that.id) && Objects.equals(categoryName, that.categoryName);
}

@Override
public int hashCode() {
return Objects.hash(id, categoryName);
}

@Override
public String toString() {
return "CategoryDto{" +
"id=" + id +
", categoryName=" + categoryName +
"}";
}
}
Loading
Loading