Skip to content

Commit 2af54d6

Browse files
committed
Created endpoint to return production data by supply hourly
1 parent 139c3ed commit 2af54d6

File tree

3 files changed

+336
-1
lines changed

3 files changed

+336
-1
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.lucoenergia.conluz.infrastructure.admin.supply.production;
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 org.lucoenergia.conluz.domain.production.ProductionByTime;
7+
import org.lucoenergia.conluz.domain.production.get.GetProductionService;
8+
import org.lucoenergia.conluz.domain.shared.SupplyId;
9+
import org.lucoenergia.conluz.infrastructure.shared.web.apidocs.ApiTag;
10+
import org.lucoenergia.conluz.infrastructure.shared.web.apidocs.response.BadRequestErrorResponse;
11+
import org.lucoenergia.conluz.infrastructure.shared.web.apidocs.response.ForbiddenErrorResponse;
12+
import org.lucoenergia.conluz.infrastructure.shared.web.apidocs.response.InternalServerErrorResponse;
13+
import org.lucoenergia.conluz.infrastructure.shared.web.apidocs.response.NotFoundErrorResponse;
14+
import org.lucoenergia.conluz.infrastructure.shared.web.apidocs.response.UnauthorizedErrorResponse;
15+
import org.springframework.format.annotation.DateTimeFormat;
16+
import org.springframework.security.access.prepost.PreAuthorize;
17+
import org.springframework.web.bind.annotation.GetMapping;
18+
import org.springframework.web.bind.annotation.PathVariable;
19+
import org.springframework.web.bind.annotation.RequestMapping;
20+
import org.springframework.web.bind.annotation.RequestParam;
21+
import org.springframework.web.bind.annotation.RestController;
22+
23+
import java.time.OffsetDateTime;
24+
import java.util.List;
25+
import java.util.UUID;
26+
27+
/**
28+
* Controller for retrieving hourly production data for a specific supply
29+
*/
30+
@RestController
31+
@RequestMapping("/api/v1/supplies/{id}/production/hourly")
32+
public class GetSupplyHourlyProductionController {
33+
34+
private final GetProductionService getProductionService;
35+
36+
public GetSupplyHourlyProductionController(GetProductionService getProductionService) {
37+
this.getProductionService = getProductionService;
38+
}
39+
40+
@GetMapping
41+
@Operation(
42+
summary = "Retrieves hourly production data assigned to a specific supply",
43+
description = "This endpoint retrieves hourly energy production data assigned to a specific supply point within a given date interval. The production values are calculated by multiplying the total production by the supply's partition coefficient. This endpoint is useful for tracking the energy production allocated to individual supply points in the energy community.",
44+
tags = ApiTag.SUPPLIES,
45+
operationId = "getSupplyHourlyProduction"
46+
)
47+
@ApiResponses(value = {
48+
@ApiResponse(
49+
responseCode = "200",
50+
description = "Query executed successfully",
51+
useReturnTypeSchema = true
52+
)
53+
})
54+
@BadRequestErrorResponse
55+
@InternalServerErrorResponse
56+
@UnauthorizedErrorResponse
57+
@ForbiddenErrorResponse
58+
@NotFoundErrorResponse
59+
@PreAuthorize("isAuthenticated()")
60+
public List<ProductionByTime> getSupplyHourlyProduction(
61+
@PathVariable("id") UUID id,
62+
@RequestParam("startDate") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime startDate,
63+
@RequestParam("endDate") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime endDate) {
64+
65+
return getProductionService.getHourlyProductionByRangeOfDatesAndSupply(startDate, endDate, SupplyId.of(id));
66+
}
67+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package org.lucoenergia.conluz.infrastructure.admin.supply.production;
2+
3+
import org.junit.jupiter.api.AfterEach;
4+
import org.junit.jupiter.api.BeforeEach;
5+
import org.junit.jupiter.api.Test;
6+
import org.lucoenergia.conluz.domain.admin.supply.Supply;
7+
import org.lucoenergia.conluz.domain.admin.supply.SupplyMother;
8+
import org.lucoenergia.conluz.domain.admin.supply.create.CreateSupplyRepository;
9+
import org.lucoenergia.conluz.domain.admin.user.User;
10+
import org.lucoenergia.conluz.domain.admin.user.UserMother;
11+
import org.lucoenergia.conluz.domain.admin.user.create.CreateUserRepository;
12+
import org.lucoenergia.conluz.domain.shared.UserId;
13+
import org.lucoenergia.conluz.infrastructure.production.EnergyProductionInfluxLoader;
14+
import org.lucoenergia.conluz.infrastructure.shared.BaseControllerTest;
15+
import org.lucoenergia.conluz.infrastructure.shared.security.auth.JwtAuthenticationFilter;
16+
import org.springframework.beans.factory.annotation.Autowired;
17+
import org.springframework.http.HttpHeaders;
18+
import org.springframework.http.HttpStatus;
19+
import org.springframework.transaction.annotation.Transactional;
20+
21+
import java.nio.charset.StandardCharsets;
22+
import java.util.UUID;
23+
24+
import static org.hamcrest.Matchers.containsString;
25+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
26+
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
27+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
28+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
29+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
30+
31+
@Transactional
32+
class GetSupplyHourlyProductionControllerTest extends BaseControllerTest {
33+
34+
private static final String URL = "/api/v1/supplies";
35+
private static final String START_DATE = "2023-09-01T00:00:00.000+02:00";
36+
private static final String END_DATE = "2023-09-01T23:00:00.000+02:00";
37+
38+
@Autowired
39+
private CreateUserRepository createUserRepository;
40+
@Autowired
41+
private CreateSupplyRepository createSupplyRepository;
42+
@Autowired
43+
private EnergyProductionInfluxLoader energyProductionInfluxLoader;
44+
45+
@BeforeEach
46+
void beforeEach() {
47+
energyProductionInfluxLoader.loadData();
48+
}
49+
50+
@AfterEach
51+
void afterEach() {
52+
energyProductionInfluxLoader.clearData();
53+
}
54+
55+
@Test
56+
void testGetSupplyHourlyProductionSuccess() throws Exception {
57+
String authHeader = loginAsDefaultAdmin();
58+
59+
User user = createUserRepository.create(UserMother.randomUser());
60+
Supply supply = createSupplyRepository.create(SupplyMother.random(user).build(), UserId.of(user.getId()));
61+
62+
mockMvc.perform(get(URL + "/" + supply.getId() + "/production/hourly")
63+
.header(HttpHeaders.AUTHORIZATION, authHeader)
64+
.queryParam("startDate", START_DATE)
65+
.queryParam("endDate", END_DATE))
66+
.andExpect(status().isOk())
67+
.andExpect(content().string(containsString("power")));
68+
}
69+
70+
@Test
71+
void testGetSupplyHourlyProductionWithMissingStartDate() throws Exception {
72+
String authHeader = loginAsDefaultAdmin();
73+
74+
User user = createUserRepository.create(UserMother.randomUser());
75+
Supply supply = createSupplyRepository.create(SupplyMother.random(user).build(), UserId.of(user.getId()));
76+
77+
mockMvc.perform(get(URL + "/" + supply.getId() + "/production/hourly")
78+
.header(HttpHeaders.AUTHORIZATION, authHeader)
79+
.queryParam("endDate", END_DATE))
80+
.andDo(print())
81+
.andExpect(status().isBadRequest())
82+
.andExpect(content().string(containsString("\"traceId\":")))
83+
.andExpect(content().string(containsString("\"timestamp\":")))
84+
.andExpect(content().string(containsString("\"status\":400")))
85+
.andExpect(content().string(containsString("\"message\":\"El parámetro con nombre 'startDate' es obligatorio.\"")));
86+
}
87+
88+
@Test
89+
void testGetSupplyHourlyProductionWithMissingEndDate() throws Exception {
90+
String authHeader = loginAsDefaultAdmin();
91+
92+
User user = createUserRepository.create(UserMother.randomUser());
93+
Supply supply = createSupplyRepository.create(SupplyMother.random(user).build(), UserId.of(user.getId()));
94+
95+
mockMvc.perform(get(URL + "/" + supply.getId() + "/production/hourly")
96+
.header(HttpHeaders.AUTHORIZATION, authHeader)
97+
.queryParam("startDate", START_DATE))
98+
.andDo(print())
99+
.andExpect(status().isBadRequest())
100+
.andExpect(content().string(containsString("\"traceId\":")))
101+
.andExpect(content().string(containsString("\"timestamp\":")))
102+
.andExpect(content().string(containsString("\"status\":400")))
103+
.andExpect(content().encoding(StandardCharsets.UTF_8))
104+
.andExpect(content().string(containsString("\"message\":\"El parámetro con nombre 'endDate' es obligatorio.\"")));
105+
}
106+
107+
@Test
108+
void testGetSupplyHourlyProductionWithUnknownSupply() throws Exception {
109+
String authHeader = loginAsDefaultAdmin();
110+
UUID supplyId = UUID.randomUUID();
111+
112+
mockMvc.perform(get(URL + "/" + supplyId + "/production/hourly")
113+
.header(HttpHeaders.AUTHORIZATION, authHeader)
114+
.queryParam("startDate", START_DATE)
115+
.queryParam("endDate", END_DATE))
116+
.andExpect(status().isNotFound())
117+
.andExpect(content().string(containsString("\"traceId\":")))
118+
.andExpect(content().string(containsString("\"timestamp\":")))
119+
.andExpect(content().string(containsString("\"status\":404")))
120+
.andExpect(content().string(containsString(String.format("\"message\":\"El punto de suministro con identificador '%s' no ha sido encontrado. Revise que el identificador sea correcto.\"", supplyId))));
121+
}
122+
123+
@Test
124+
void testWithMissingToken() throws Exception {
125+
UUID randomId = UUID.randomUUID();
126+
127+
mockMvc.perform(get(URL + "/" + randomId + "/production/hourly")
128+
.queryParam("startDate", START_DATE)
129+
.queryParam("endDate", END_DATE))
130+
.andDo(print())
131+
.andExpect(status().isUnauthorized())
132+
.andExpect(jsonPath("$.timestamp").isNotEmpty())
133+
.andExpect(jsonPath("$.status").value(HttpStatus.UNAUTHORIZED.value()))
134+
.andExpect(jsonPath("$.message").isNotEmpty())
135+
.andExpect(jsonPath("$.traceId").isNotEmpty());
136+
}
137+
138+
@Test
139+
void testWithWrongToken() throws Exception {
140+
UUID randomId = UUID.randomUUID();
141+
final String wrongToken = JwtAuthenticationFilter.AUTHORIZATION_HEADER_PREFIX + "wrong";
142+
143+
mockMvc.perform(get(URL + "/" + randomId + "/production/hourly")
144+
.queryParam("startDate", START_DATE)
145+
.queryParam("endDate", END_DATE)
146+
.header(HttpHeaders.AUTHORIZATION, wrongToken))
147+
.andExpect(status().isUnauthorized())
148+
.andExpect(jsonPath("$.timestamp").isNotEmpty())
149+
.andExpect(jsonPath("$.status").value(HttpStatus.UNAUTHORIZED.value()))
150+
.andExpect(jsonPath("$.message").isNotEmpty())
151+
.andExpect(jsonPath("$.traceId").isNotEmpty());
152+
}
153+
154+
@Test
155+
void testWithExpiredToken() throws Exception {
156+
UUID randomId = UUID.randomUUID();
157+
final String expiredToken = JwtAuthenticationFilter.AUTHORIZATION_HEADER_PREFIX +
158+
"eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiQURNSU4iLCJzdWIiOiJiMTFlMTgxNS1mNzE0LTRmNGEtOGZjMS0yNjQxM2FmM2YzYmIiLCJpYXQiOjE3MDQyNzkzNzIsImV4cCI6MTcwNDI4MTE3Mn0.jO3pgdDj4mg9TnRzL7f8RUL1ytJS7057jAg6zaCcwn0";
159+
160+
mockMvc.perform(get(URL + "/" + randomId + "/production/hourly")
161+
.queryParam("startDate", START_DATE)
162+
.queryParam("endDate", END_DATE)
163+
.header(HttpHeaders.AUTHORIZATION, expiredToken))
164+
.andDo(print())
165+
.andExpect(status().isUnauthorized())
166+
.andExpect(jsonPath("$.timestamp").isNotEmpty())
167+
.andExpect(jsonPath("$.status").value(HttpStatus.UNAUTHORIZED.value()))
168+
.andExpect(jsonPath("$.message").isNotEmpty())
169+
.andExpect(jsonPath("$.traceId").isNotEmpty());
170+
}
171+
}
Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,120 @@
11
package org.lucoenergia.conluz.infrastructure.production.get;
22

3+
import org.junit.jupiter.api.AfterEach;
34
import org.junit.jupiter.api.Assertions;
5+
import org.junit.jupiter.api.BeforeEach;
46
import org.junit.jupiter.api.Test;
5-
import org.lucoenergia.conluz.infrastructure.production.get.GetProductionRepositoryInflux;
7+
import org.lucoenergia.conluz.domain.production.ProductionByTime;
8+
import org.lucoenergia.conluz.infrastructure.production.EnergyProductionInfluxLoader;
69
import org.lucoenergia.conluz.infrastructure.shared.BaseIntegrationTest;
710
import org.springframework.beans.factory.annotation.Autowired;
811
import org.springframework.boot.test.context.SpringBootTest;
912

13+
import java.time.OffsetDateTime;
14+
import java.util.List;
15+
16+
import static org.junit.jupiter.api.Assertions.assertEquals;
17+
import static org.junit.jupiter.api.Assertions.assertFalse;
18+
import static org.junit.jupiter.api.Assertions.assertNotNull;
19+
import static org.junit.jupiter.api.Assertions.assertTrue;
20+
1021
@SpringBootTest
1122
class GetProductionRepositoryInfluxTest extends BaseIntegrationTest {
1223

1324
@Autowired
1425
private GetProductionRepositoryInflux repository;
1526

27+
@Autowired
28+
private EnergyProductionInfluxLoader energyProductionInfluxLoader;
29+
30+
@BeforeEach
31+
void beforeEach() {
32+
energyProductionInfluxLoader.loadData();
33+
}
34+
35+
@AfterEach
36+
void afterEach() {
37+
energyProductionInfluxLoader.clearData();
38+
}
39+
1640
@Test
1741
void testGetInstantProduction() {
1842

1943
Double result = repository.getInstantProduction().getPower();
2044

2145
Assertions.assertNotNull(result);
2246
}
47+
48+
@Test
49+
void testGetHourlyProductionByRangeOfDates() {
50+
OffsetDateTime startDate = OffsetDateTime.parse("2023-09-01T00:00:00.000+02:00");
51+
OffsetDateTime endDate = OffsetDateTime.parse("2023-09-01T23:00:00.000+02:00");
52+
Float partitionCoefficient = 1.0f;
53+
54+
List<ProductionByTime> result = repository.getHourlyProductionByRangeOfDates(startDate, endDate, partitionCoefficient);
55+
56+
assertNotNull(result);
57+
assertFalse(result.isEmpty());
58+
59+
// The loader loads 24 hourly data points for September 1, 2023
60+
assertEquals(24, result.size());
61+
62+
// Verify first hour (00:00 - 01:00) - no production at night
63+
ProductionByTime hour1 = result.get(0);
64+
assertNotNull(hour1);
65+
assertNotNull(hour1.getPower());
66+
assertEquals(0.0d, hour1.getPower(), 0.01d, "First hour should have no production");
67+
assertNotNull(hour1.getTime());
68+
69+
// Verify peak production hour (14:00 - 15:00 = hour 15)
70+
ProductionByTime peakHour = result.get(14);
71+
assertNotNull(peakHour);
72+
assertNotNull(peakHour.getPower());
73+
assertEquals(31.1d, peakHour.getPower(), 0.01d, "Peak hour should have 31.1 kW production");
74+
assertTrue(peakHour.getPower() > 0, "Peak hour should have positive production");
75+
76+
// Verify hour 12 (11:00 - 12:00) - good solar production
77+
ProductionByTime hour12 = result.get(11);
78+
assertNotNull(hour12);
79+
assertEquals(25.76d, hour12.getPower(), 0.01d, "Hour 12 should have 25.76 kW production");
80+
81+
// Verify last hour (23:00 - 00:00) - no production at night
82+
ProductionByTime lastHour = result.get(23);
83+
assertNotNull(lastHour);
84+
assertEquals(0.0d, lastHour.getPower(), 0.01d, "Last hour should have no production");
85+
86+
// Verify total production is greater than 0
87+
double totalProduction = result.stream()
88+
.mapToDouble(ProductionByTime::getPower)
89+
.sum();
90+
assertTrue(totalProduction > 0, "Total production should be greater than 0");
91+
92+
// Expected total: sum of all 24 hourly values
93+
// 0+0+0+0+0+0+0+0.13+1.32+5.45+15.97+25.76+27.79+25.29+31.1+26.87+30.95+28.86+10.48+5.37+0.81+0+0+0 = 236.15
94+
assertEquals(236.15d, totalProduction, 0.01d, "Total production should match sum of hourly values");
95+
}
96+
97+
@Test
98+
void testGetHourlyProductionByRangeOfDatesWithPartitionCoefficient() {
99+
OffsetDateTime startDate = OffsetDateTime.parse("2023-09-01T00:00:00.000+02:00");
100+
OffsetDateTime endDate = OffsetDateTime.parse("2023-09-01T23:00:00.000+02:00");
101+
Float partitionCoefficient = 0.5f; // 50% partition
102+
103+
List<ProductionByTime> result = repository.getHourlyProductionByRangeOfDates(startDate, endDate, partitionCoefficient);
104+
105+
assertNotNull(result);
106+
assertFalse(result.isEmpty());
107+
assertEquals(24, result.size());
108+
109+
// Verify peak hour with partition coefficient
110+
ProductionByTime peakHour = result.get(14);
111+
assertNotNull(peakHour);
112+
assertEquals(31.1d * 0.5d, peakHour.getPower(), 0.01d, "Production should be multiplied by partition coefficient");
113+
114+
// Verify total production is half of the original
115+
double totalProduction = result.stream()
116+
.mapToDouble(ProductionByTime::getPower)
117+
.sum();
118+
assertEquals(236.15d * 0.5d, totalProduction, 0.01d, "Total production should be halved with 0.5 partition coefficient");
119+
}
23120
}

0 commit comments

Comments
 (0)