Skip to content

Commit a714e04

Browse files
committed
Implemented endpoint to return supply production monthly
1 parent b4c65e6 commit a714e04

File tree

4 files changed

+279
-1
lines changed

4 files changed

+279
-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 monthly production data for a specific supply
29+
*/
30+
@RestController
31+
@RequestMapping("/api/v1/supplies/{id}/production/monthly")
32+
public class GetSupplyMonthlyProductionController {
33+
34+
private final GetProductionService getProductionService;
35+
36+
public GetSupplyMonthlyProductionController(GetProductionService getProductionService) {
37+
this.getProductionService = getProductionService;
38+
}
39+
40+
@GetMapping
41+
@Operation(
42+
summary = "Retrieves monthly production data assigned to a specific supply",
43+
description = "This endpoint retrieves monthly 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 = "getSupplyMonthlyProduction"
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> getSupplyMonthlyProduction(
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.getMonthlyProductionByRangeOfDatesAndSupply(startDate, endDate, SupplyId.of(id));
66+
}
67+
}

src/main/java/org/lucoenergia/conluz/infrastructure/shared/db/influxdb/InfluxDuration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ public class InfluxDuration {
77

88
public static final String HOURLY = "1h";
99
public static final String DAILY = "1d";
10-
public static final String MONTHLY = "1mo";
10+
public static final String MONTHLY = "30d";
1111
public static final String YEARLY = "1y";
1212
}
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 GetSupplyMonthlyProductionControllerTest 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 testGetSupplyMonthlyProductionSuccess() 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/monthly")
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 testGetSupplyMonthlyProductionWithMissingStartDate() 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/monthly")
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 testGetSupplyMonthlyProductionWithMissingEndDate() 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/monthly")
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 testGetSupplyMonthlyProductionWithUnknownSupply() throws Exception {
109+
String authHeader = loginAsDefaultAdmin();
110+
UUID supplyId = UUID.randomUUID();
111+
112+
mockMvc.perform(get(URL + "/" + supplyId + "/production/monthly")
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/monthly")
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/monthly")
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/monthly")
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+
}

src/test/java/org/lucoenergia/conluz/infrastructure/production/get/GetProductionRepositoryInfluxTest.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,44 @@ void testGetHourlyProductionByRangeOfDatesWithPartitionCoefficient() {
117117
.sum();
118118
assertEquals(236.15d * 0.5d, totalProduction, 0.01d, "Total production should be halved with 0.5 partition coefficient");
119119
}
120+
121+
@Test
122+
void testGetMonthlyProductionByRangeOfDates() {
123+
OffsetDateTime startDate = OffsetDateTime.parse("2023-09-01T00:00:00.000+02:00");
124+
OffsetDateTime endDate = OffsetDateTime.parse("2023-09-01T23:00:00.000+02:00");
125+
Float partitionCoefficient = 1.0f;
126+
127+
List<ProductionByTime> result = repository.getMonthlyProductionByRangeOfDates(startDate, endDate, partitionCoefficient);
128+
129+
assertNotNull(result);
130+
assertFalse(result.isEmpty());
131+
132+
// Verify total production matches the sum of hourly values for the day
133+
ProductionByTime monthData = result.get(0);
134+
assertNotNull(monthData);
135+
assertNotNull(monthData.getPower());
136+
assertNotNull(monthData.getTime());
137+
138+
// The monthly aggregation should sum all hourly values for September
139+
// Expected total: sum of all 24 hourly values for Sept 1
140+
// 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
141+
assertEquals(236.15d, monthData.getPower(), 0.01d, "Monthly production should match sum of hourly values");
142+
}
143+
144+
@Test
145+
void testGetMonthlyProductionByRangeOfDatesWithPartitionCoefficient() {
146+
OffsetDateTime startDate = OffsetDateTime.parse("2023-09-01T00:00:00.000+02:00");
147+
OffsetDateTime endDate = OffsetDateTime.parse("2023-09-01T23:00:00.000+02:00");
148+
Float partitionCoefficient = 0.5f; // 50% partition
149+
150+
List<ProductionByTime> result = repository.getMonthlyProductionByRangeOfDates(startDate, endDate, partitionCoefficient);
151+
152+
assertNotNull(result);
153+
assertFalse(result.isEmpty());
154+
155+
// Verify production is multiplied by partition coefficient
156+
ProductionByTime monthData = result.get(0);
157+
assertNotNull(monthData);
158+
assertEquals(236.15d * 0.5d, monthData.getPower(), 0.01d, "Monthly production should be multiplied by partition coefficient");
159+
}
120160
}

0 commit comments

Comments
 (0)