Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
fc9a236
feat(order): add the Order entity and OrderDto
EdgarEldy Sep 19, 2026
1b26545
feat(order): add the order RowMapper and entity mapper
EdgarEldy Sep 19, 2026
8a626fc
feat(order): add the order DAO with optional filters
EdgarEldy Sep 19, 2026
c1e3e2a
feat(order): implement the order service on top of the catalog and cu…
EdgarEldy Sep 19, 2026
927bac4
feat(order): wire the order DAO and service explicitly
EdgarEldy Sep 19, 2026
f7e97bd
feat(ws): add the order payloads and converter
EdgarEldy Sep 19, 2026
9a1a4d8
feat(ws): expose the order endpoints
EdgarEldy Sep 19, 2026
bd27d9a
feat(ws): import the order module into the shared context
EdgarEldy Sep 19, 2026
c5fb3cf
build(order): add Testcontainers test dependencies to the order module
EdgarEldy Sep 19, 2026
1b33ba1
test(order): cover the order DAO against PostgreSQL
EdgarEldy Sep 19, 2026
97ade20
test(order): test the order RowMapper and entity mapper
EdgarEldy Sep 19, 2026
fe066f3
test(order): unit test the order service with mocked customer and pro…
EdgarEldy Sep 19, 2026
205e770
test(ws): test the order controller with MockMvc
EdgarEldy Sep 19, 2026
6df2919
test(ws): check the permission matrix of the order endpoints
EdgarEldy Sep 19, 2026
eaf6676
test(ws): run the order flows on the real stack
EdgarEldy Sep 19, 2026
79f133c
fix(ws): refuse a decimal where an integer is expected
EdgarEldy Sep 19, 2026
f8cd73f
docs(readme): tick the feature/order tasks and correct the order depe…
EdgarEldy Sep 19, 2026
87694e4
refactor(order): drop an unreachable second total check
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
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ spring-jdbc-tutorial/
│ ├── dto/ (OrderDto.java)
│ ├── mapper/ (OrderRowMapper.java, OrderMapper.java)
│ ├── dao/ (OrderDao.java + impl/)
│ ├── service/ (OrderService.java + impl/, depends on CategoryService/ProductService/CustomerService)
│ ├── service/ (OrderService.java + impl/, depends on ProductService/CustomerService only: the category is never needed)
│ └── config/ (DaoConfig.java, ServiceConfig.java)
└── ws/
├── pom.xml (depends on core/auth, core/catalog, core/customer, core/order)
Expand Down Expand Up @@ -438,11 +438,11 @@ Full CRUD for users, roles, and permissions, still inside `core/auth`. Assignmen

### Tasks

- [ ] `core/order`: `Order` entity (`@Table("orders")`) and `OrderDto`, `OrderRowMapper` (entity) plus `OrderMapper` (entity ↔ dto), `OrderDao` + impl, `OrderService` + impl (depends on `catalog`'s `ProductService` and `customer`'s `CustomerService` - their service interfaces, never their DAOs directly): computes `total = quantity * product.unitPrice`, checks the referenced customer/product exist
- [ ] `core/order`'s `DaoConfig`/`ServiceConfig` (the latter injecting `ProductService`/`CustomerService` beans from the other modules' contexts)
- [ ] `ws`: `OrderController`, `OrderConverter`
- [ ] `WebMvcConfig` updated to import `core/order`'s config
- [ ] Tests at all five layers, `OrderDao` with its own fixture dataset, the total computation, and the not-found cases for a bad `customerId`/`productId`
- [x] `core/order`: `Order` entity (`@Table("orders")`) and `OrderDto`, `OrderRowMapper` (entity) plus `OrderMapper` (entity ↔ dto), `OrderDao` + impl, `OrderService` + impl (depends on `catalog`'s `ProductService` and `customer`'s `CustomerService` - their service interfaces, never their DAOs directly): computes `total = quantity * product.unitPrice`, checks the referenced customer/product exist
- [x] `core/order`'s `DaoConfig`/`ServiceConfig` (the latter injecting `ProductService`/`CustomerService` beans from the other modules' contexts)
- [x] `ws`: `OrderController`, `OrderConverter`
- [x] `WebMvcConfig` updated to import `core/order`'s config
- [x] Tests at all five layers, `OrderDao` with its own fixture dataset, the total computation, and the not-found cases for a bad `customerId`/`productId`

## Order of work

Expand Down
17 changes: 17 additions & 0 deletions core/order/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,22 @@
<groupId>com.edgareldy.springjdbctutorial</groupId>
<artifactId>customer</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,24 @@
package com.edgareldy.springjdbctutorial.core.order.config;

import com.edgareldy.springjdbctutorial.core.order.dao.OrderDao;
import com.edgareldy.springjdbctutorial.core.order.dao.impl.OrderDaoImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

/**
* Declares the DAO beans of the order 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 OrderDao orderDao(JdbcTemplate jdbcTemplate) {
return new OrderDaoImpl(jdbcTemplate);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.edgareldy.springjdbctutorial.core.order.config;

import com.edgareldy.springjdbctutorial.core.catalog.service.ProductService;
import com.edgareldy.springjdbctutorial.core.customer.service.CustomerService;
import com.edgareldy.springjdbctutorial.core.order.dao.OrderDao;
import com.edgareldy.springjdbctutorial.core.order.service.OrderService;
import com.edgareldy.springjdbctutorial.core.order.service.impl.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
* Declares the service beans of the order module. Imports its DaoConfig and the catalog and customer
* ServiceConfig (fully qualified, every module has a class of that name) 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,
com.edgareldy.springjdbctutorial.core.catalog.config.ServiceConfig.class,
com.edgareldy.springjdbctutorial.core.customer.config.ServiceConfig.class})
public class ServiceConfig {

@Bean
public OrderService orderService(OrderDao orderDao, ProductService productService, CustomerService customerService) {
return new OrderServiceImpl(orderDao, productService, customerService);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.edgareldy.springjdbctutorial.core.order.dao;

import com.edgareldy.springjdbctutorial.core.order.entity.Order;

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

/**
* Data access for orders. Entity types only. Orders are immutable: there is no update and no delete.
* <p>
* Created edgar.muhamyangabo on 9/20/26
* Author : edgar.muhamyangabo
* Date : 9/20/26
* Project : spring-jdbc-tutorial
*/
public interface OrderDao {

/** One page of orders ordered by id, each filter is applied only when not null. */
List<Order> findPage(int page, int size, Long customerId, Long productId);

/** Number of orders matching the same optional filters. */
long count(Long customerId, Long productId);

Optional<Order> findById(Long id);

/** Inserts the order and returns it with its generated id. */
Order insert(Order order);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.edgareldy.springjdbctutorial.core.order.dao.impl;

import com.edgareldy.springjdbctutorial.core.common.dao.support.AbstractDao;
import com.edgareldy.springjdbctutorial.core.order.dao.OrderDao;
import com.edgareldy.springjdbctutorial.core.order.entity.Order;
import com.edgareldy.springjdbctutorial.core.order.mapper.OrderRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;

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

/**
* JdbcTemplate implementation of OrderDao. 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 OrderDaoImpl extends AbstractDao implements OrderDao {

private static final String SELECT = "SELECT id, customer_id, product_id, quantity, total FROM orders";

private final OrderRowMapper rowMapper = new OrderRowMapper();

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

@Override
public List<Order> findPage(int page, int size, Long customerId, Long productId) {
List<Object> args = new ArrayList<>();
String where = where(customerId, productId, args);
args.add(size);
args.add((long) page * size);
return getJdbcTemplate().query(SELECT + where + " ORDER BY id LIMIT ? OFFSET ?", rowMapper, args.toArray());
}

@Override
public long count(Long customerId, Long productId) {
List<Object> args = new ArrayList<>();
String where = where(customerId, productId, args);
Long total = getJdbcTemplate().queryForObject("SELECT COUNT(*) FROM orders" + where, Long.class, args.toArray());
return total == null ? 0L : total;
}

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

@Override
public Order insert(Order order) {
long id = insertReturningId(
"INSERT INTO orders (customer_id, product_id, quantity, total) VALUES (?, ?, ?, ?) RETURNING id",
order.getCustomerId(), order.getProductId(), order.getQuantity(), order.getTotal());
order.setId(id);
return order;
}

/**
* Builds the WHERE clause from the non-null filters. Only fixed column names are concatenated,
* the values are always bound as parameters and appended to args in placeholder order.
*/
private static String where(Long customerId, Long productId, List<Object> args) {
StringBuilder where = new StringBuilder();
if (customerId != null) {
where.append(" WHERE customer_id = ?");
args.add(customerId);
}
if (productId != null) {
where.append(customerId == null ? " WHERE" : " AND").append(" product_id = ?");
args.add(productId);
}
return where.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.edgareldy.springjdbctutorial.core.order.dto;

import java.math.BigDecimal;
import java.util.Objects;

/**
* Business shape of an order. The total is computed by the service, never taken from the caller.
* <p>
* Created edgar.muhamyangabo on 9/20/26
* Author : edgar.muhamyangabo
* Date : 9/20/26
* Project : spring-jdbc-tutorial
*/
public class OrderDto {

private Long id;

private Long customerId;

private Long productId;

private int quantity;

private BigDecimal total;

public OrderDto() {
}

public OrderDto(Long id, Long customerId, Long productId, int quantity, BigDecimal total) {
this.id = id;
this.customerId = customerId;
this.productId = productId;
this.quantity = quantity;
this.total = total;
}

public Long getId() {
return id;
}

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

public Long getCustomerId() {
return customerId;
}

public void setCustomerId(Long customerId) {
this.customerId = customerId;
}

public Long getProductId() {
return productId;
}

public void setProductId(Long productId) {
this.productId = productId;
}

public int getQuantity() {
return quantity;
}

public void setQuantity(int quantity) {
this.quantity = quantity;
}

public BigDecimal getTotal() {
return total;
}

public void setTotal(BigDecimal total) {
this.total = total;
}

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

@Override
public int hashCode() {
return Objects.hash(id, customerId, productId, quantity, total);
}

@Override
public String toString() {
return "OrderDto{" +
"id=" + id +
", customerId=" + customerId +
", productId=" + productId +
", quantity=" + quantity +
", total=" + total +
"}";
}
}
Loading
Loading