Skip to content

Commit 045694f

Browse files
authored
feature/conluz-122 to main (#123)
* [conluz-122] Initialized claude file * [conluz-122] Implemented new endpoint to get a user by id
1 parent 9b983bd commit 045694f

File tree

12 files changed

+386
-7
lines changed

12 files changed

+386
-7
lines changed

CLAUDE.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
Conluz is an energy community management application built with Spring Boot 3. It manages community members, supply points, consumption data, production metrics from energy plants, and electricity prices. The application is API-driven with JWT authentication, uses PostgreSQL for relational data and InfluxDB for time-series data.
8+
9+
## Development Commands
10+
11+
### Build and Run
12+
```bash
13+
./gradlew build # Build the project
14+
./gradlew bootRun # Run the application (accessible at https://localhost:8443)
15+
./gradlew clean build --info # Clean build with detailed output
16+
```
17+
18+
### Testing
19+
```bash
20+
./gradlew test # Run all tests (uses JUnit 5)
21+
./gradlew test --tests ClassName # Run a specific test class
22+
```
23+
24+
Tests use Testcontainers for PostgreSQL and InfluxDB integration tests.
25+
26+
### Docker Deployment
27+
```bash
28+
# From project root:
29+
docker build -t conluz:1.0 -f Dockerfile .
30+
cd deploy
31+
docker compose up -d # Start all services
32+
docker compose up -d postgres # Start only PostgreSQL
33+
docker compose up -d influxdb # Start only InfluxDB
34+
docker stop conluz # Stop the app
35+
```
36+
37+
## Architecture
38+
39+
### Package Structure
40+
41+
The codebase follows **Hexagonal Architecture** (Ports and Adapters):
42+
43+
- **`domain/`**: Core business logic, pure Java classes
44+
- `admin/`: User, supply point, and plant management
45+
- `consumption/`: Consumption data from Datadis and other sources
46+
- `production/`: Production data from Huawei inverters and other sources
47+
- `price/`: Electricity price data management
48+
- `shared/`: Domain-level shared utilities
49+
50+
- **`infrastructure/`**: Adapters for external systems
51+
- Controllers (REST endpoints)
52+
- Repositories (JPA/InfluxDB implementations)
53+
- External integrations (Datadis, Huawei, Shelly)
54+
- `shared/`: Infrastructure-level shared components (security, DB config, jobs, i18n, etc.)
55+
56+
### Key Components
57+
58+
- **Authentication**: JWT-based with HMAC-SHA256, tokens contain user ID, role, expiration
59+
- **Controllers**: REST endpoints in `infrastructure/*/` packages, documented with OpenAPI/Swagger
60+
- **Services**: Business logic in `domain/*/` packages (e.g., `*Service.java`)
61+
- **Repositories**: Interfaces in `domain/`, implementations in `infrastructure/`
62+
- **Database Migrations**: Liquibase changesets in `src/main/resources/db/liquibase/`
63+
- **Scheduled Jobs**: Quartz-based scheduled tasks enabled via `@EnableScheduling`
64+
65+
### Data Storage
66+
67+
1. **PostgreSQL**: Users, supplies, configuration (managed via Liquibase migrations)
68+
2. **InfluxDB**: Time-series data for consumption, production, and prices with retention policies (1 month, 1 year, forever)
69+
70+
## Configuration
71+
72+
### Required Environment Variables
73+
74+
- `CONLUZ_JWT_SECRET_KEY`: JWT secret key (≥256 bits, HMAC-SHA compatible). Generate using `org.lucoenergia.conluz.infrastructure.shared.security.JwtSecretKeyGenerator`
75+
- `SPRING_DATASOURCE_URL`: PostgreSQL connection (default: `jdbc:postgresql://localhost:5432/conluz_db`)
76+
77+
### Database Setup
78+
79+
For new installations, use `deploy/docker-compose.yaml`. For existing databases:
80+
81+
**PostgreSQL:**
82+
```sql
83+
CREATE DATABASE conluz_db;
84+
CREATE DATABASE conluz_db_test;
85+
CREATE USER luz WITH PASSWORD 'blank';
86+
GRANT ALL PRIVILEGES ON DATABASE conluz_db TO luz;
87+
GRANT ALL PRIVILEGES ON DATABASE conluz_db_test TO luz;
88+
```
89+
90+
**InfluxDB:**
91+
```sql
92+
CREATE DATABASE conluz_db
93+
CREATE USER luz WITH PASSWORD 'blank'
94+
GRANT ALL ON conluz_db TO luz
95+
CREATE RETENTION POLICY one_month ON conluz_db DURATION 30d REPLICATION 1
96+
CREATE RETENTION POLICY one_year ON conluz_db DURATION 365d REPLICATION 1
97+
CREATE RETENTION POLICY forever ON conluz_db DURATION INF REPLICATION 1 DEFAULT
98+
```
99+
100+
## API Documentation
101+
102+
With the app running:
103+
- OpenAPI spec: https://localhost:8443/api-docs
104+
- Swagger UI: https://localhost:8443/api-docs/swagger-ui/index.html
105+
106+
## Git Workflow
107+
108+
- Main branch: `main`
109+
- Feature branches: `feature/conluz-XXX` (where XXX is the issue number)
110+
- Commit format: `[conluz-XXX] Your commit message`
111+
- Merge strategy: Squash and merge to main
112+
- Direct pushes to `main` are not allowed
113+
114+
## Code Standards
115+
116+
- Follow Clean Code principles
117+
- Code and comments must be in English
118+
- Code should be self-explanatory with comments when additional explanation is needed
119+
- All new code must have automated tests
120+
- Architecture tests are enforced via ArchUnit (see `src/test/java/org/lucoenergia/conluz/architecture/`)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package org.lucoenergia.conluz.domain.admin.user.get;
22

33
import org.lucoenergia.conluz.domain.admin.user.User;
4+
import org.lucoenergia.conluz.domain.shared.UserId;
45
import org.lucoenergia.conluz.domain.shared.pagination.PagedRequest;
56
import org.lucoenergia.conluz.domain.shared.pagination.PagedResult;
67

78
public interface GetUserService {
89

910
PagedResult<User> findAll(PagedRequest pagedRequest);
11+
12+
User findById(UserId id);
1013
}

src/main/java/org/lucoenergia/conluz/infrastructure/admin/supply/create/CreateSupplyBody.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.swagger.v3.oas.annotations.media.Schema;
44
import jakarta.validation.constraints.NotEmpty;
5+
import jakarta.validation.constraints.NotNull;
56
import jakarta.validation.constraints.Positive;
67
import org.lucoenergia.conluz.domain.admin.supply.Supply;
78
import org.lucoenergia.conluz.domain.admin.user.User;
@@ -19,6 +20,7 @@ public class CreateSupplyBody {
1920
private String address;
2021
@NotEmpty
2122
private String addressRef;
23+
@NotNull
2224
@Positive
2325
private Float partitionCoefficient;
2426
private String name;

src/main/java/org/lucoenergia/conluz/infrastructure/admin/user/UserExceptionHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,6 @@ public ResponseEntity<RestError> handleException(UserNotFoundException e) {
5555
List.of(e.getId()).toArray(),
5656
LocaleContextHolder.getLocale()
5757
);
58-
return errorBuilder.build(message, HttpStatus.BAD_REQUEST);
58+
return errorBuilder.build(message, HttpStatus.NOT_FOUND);
5959
}
6060
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.lucoenergia.conluz.infrastructure.admin.user.get;
2+
3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
5+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
6+
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
7+
import org.lucoenergia.conluz.domain.admin.user.User;
8+
import org.lucoenergia.conluz.domain.admin.user.get.GetUserService;
9+
import org.lucoenergia.conluz.domain.shared.UserId;
10+
import org.lucoenergia.conluz.infrastructure.admin.user.UserResponse;
11+
import org.lucoenergia.conluz.infrastructure.shared.web.apidocs.ApiTag;
12+
import org.lucoenergia.conluz.infrastructure.shared.web.apidocs.response.BadRequestErrorResponse;
13+
import org.lucoenergia.conluz.infrastructure.shared.web.apidocs.response.ForbiddenErrorResponse;
14+
import org.lucoenergia.conluz.infrastructure.shared.web.apidocs.response.InternalServerErrorResponse;
15+
import org.lucoenergia.conluz.infrastructure.shared.web.apidocs.response.NotFoundErrorResponse;
16+
import org.lucoenergia.conluz.infrastructure.shared.web.apidocs.response.UnauthorizedErrorResponse;
17+
import org.springframework.security.access.prepost.PreAuthorize;
18+
import org.springframework.web.bind.annotation.GetMapping;
19+
import org.springframework.web.bind.annotation.PathVariable;
20+
import org.springframework.web.bind.annotation.RequestMapping;
21+
import org.springframework.web.bind.annotation.RestController;
22+
23+
import java.util.UUID;
24+
25+
/**
26+
* Get user by ID
27+
*/
28+
@RestController
29+
@RequestMapping(value = "/api/v1/users")
30+
public class GetUserByIdController {
31+
32+
private final GetUserService service;
33+
34+
public GetUserByIdController(GetUserService service) {
35+
this.service = service;
36+
}
37+
38+
@GetMapping("/{id}")
39+
@Operation(
40+
summary = "Retrieves a single user by ID",
41+
description = """
42+
This endpoint retrieves detailed information about a specific user by their unique identifier.
43+
44+
**Required Role: ADMIN**
45+
46+
Authentication is required using a Bearer token.
47+
""",
48+
tags = ApiTag.USERS,
49+
operationId = "getUserById",
50+
security = @SecurityRequirement(name = "bearerToken", scopes = {"ADMIN"})
51+
)
52+
@ApiResponses(value = {
53+
@ApiResponse(
54+
responseCode = "200",
55+
description = "User found and returned successfully",
56+
useReturnTypeSchema = true
57+
)
58+
})
59+
@ForbiddenErrorResponse
60+
@UnauthorizedErrorResponse
61+
@BadRequestErrorResponse
62+
@NotFoundErrorResponse
63+
@InternalServerErrorResponse
64+
@PreAuthorize("hasRole('ADMIN')")
65+
public UserResponse getUserById(@PathVariable("id") UUID userId) {
66+
User user = service.findById(UserId.of(userId));
67+
return new UserResponse(user);
68+
}
69+
}

src/main/java/org/lucoenergia/conluz/infrastructure/admin/user/get/GetUserServiceImpl.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package org.lucoenergia.conluz.infrastructure.admin.user.get;
22

33
import org.lucoenergia.conluz.domain.admin.user.User;
4+
import org.lucoenergia.conluz.domain.admin.user.UserNotFoundException;
45
import org.lucoenergia.conluz.domain.admin.user.get.GetUserRepository;
56
import org.lucoenergia.conluz.domain.admin.user.get.GetUserService;
7+
import org.lucoenergia.conluz.domain.shared.UserId;
68
import org.lucoenergia.conluz.domain.shared.pagination.Direction;
79
import org.lucoenergia.conluz.domain.shared.pagination.Order;
810
import org.lucoenergia.conluz.domain.shared.pagination.PagedRequest;
@@ -20,6 +22,7 @@ public GetUserServiceImpl(GetUserRepository getUserRepository) {
2022
this.getUserRepository = getUserRepository;
2123
}
2224

25+
@Override
2326
public PagedResult<User> findAll(PagedRequest pagedRequest) {
2427

2528
// If not sorting is provided, sort by descendant order by default
@@ -30,4 +33,10 @@ public PagedResult<User> findAll(PagedRequest pagedRequest) {
3033

3134
return getUserRepository.findAll(pagedRequest);
3235
}
36+
37+
@Override
38+
public User findById(UserId id) {
39+
return getUserRepository.findById(id)
40+
.orElseThrow(() -> new UserNotFoundException(id));
41+
}
3342
}

src/test/java/org/lucoenergia/conluz/infrastructure/admin/supply/create/CreateSupplyControllerTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ void testWithDuplicatedSupply() throws Exception {
166166
@MethodSource("getBodyWithMissingRequiredFields")
167167
void testMissingRequiredFields(String body) throws Exception {
168168

169+
User user = UserMother.randomUser();
170+
user.setPersonalId("54889216G");
171+
createUserRepository.create(user);
172+
169173
String authHeader = loginAsDefaultAdmin();
170174

171175
mockMvc.perform(post(URL)

src/test/java/org/lucoenergia/conluz/infrastructure/admin/user/delete/DeleteUserControllerTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ void testWithUnknownUser() throws Exception {
5959
.header(HttpHeaders.AUTHORIZATION, authHeader)
6060
.contentType(MediaType.APPLICATION_JSON))
6161
.andDo(print())
62-
.andExpect(status().isBadRequest())
62+
.andExpect(status().isNotFound())
6363
.andExpect(jsonPath("$.timestamp").isNotEmpty())
64-
.andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value()))
64+
.andExpect(jsonPath("$.status").value(HttpStatus.NOT_FOUND.value()))
6565
.andExpect(jsonPath("$.message").isNotEmpty())
6666
.andExpect(jsonPath("$.traceId").isNotEmpty());
6767
}

src/test/java/org/lucoenergia/conluz/infrastructure/admin/user/disable/DisableUserControllerTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ void testWithUnknownUser() throws Exception {
6060
.header(HttpHeaders.AUTHORIZATION, authHeader)
6161
.contentType(MediaType.APPLICATION_JSON))
6262
.andDo(print())
63-
.andExpect(status().isBadRequest())
63+
.andExpect(status().isNotFound())
6464
.andExpect(jsonPath("$.timestamp").isNotEmpty())
65-
.andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value()))
65+
.andExpect(jsonPath("$.status").value(HttpStatus.NOT_FOUND.value()))
6666
.andExpect(jsonPath("$.message").isNotEmpty())
6767
.andExpect(jsonPath("$.traceId").isNotEmpty());
6868
}

src/test/java/org/lucoenergia/conluz/infrastructure/admin/user/enable/EnableUserControllerTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ void testWithUnknownUser() throws Exception {
5959
.header(HttpHeaders.AUTHORIZATION, authHeader)
6060
.contentType(MediaType.APPLICATION_JSON))
6161
.andDo(print())
62-
.andExpect(status().isBadRequest())
62+
.andExpect(status().isNotFound())
6363
.andExpect(jsonPath("$.timestamp").isNotEmpty())
64-
.andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value()))
64+
.andExpect(jsonPath("$.status").value(HttpStatus.NOT_FOUND.value()))
6565
.andExpect(jsonPath("$.message").isNotEmpty())
6666
.andExpect(jsonPath("$.traceId").isNotEmpty());
6767
}

0 commit comments

Comments
 (0)