Skip to content

Commit

Permalink
[PPANTT-36] feat: Add update maintenance API (#310)
Browse files Browse the repository at this point in the history
* [PPANTT-36] added update maintenance API

* [PPANTT-36] added javadoc

* [PPANTT-36] added unit tests

* [PPANTT-36] updated openapi

* [PPANTT-36] feat: add openapi.json, PaStazionePaRepositoryTest

---------

Co-authored-by: Alessio Cialini <63233981+alessio-cialini@users.noreply.github.com>
Co-authored-by: Alessio Cialini <alessio.cialini@emeal.nttdata.com>
  • Loading branch information
3 people committed Jul 29, 2024
1 parent 9329a1f commit 5c416b6
Show file tree
Hide file tree
Showing 9 changed files with 14,432 additions and 14,699 deletions.
28,070 changes: 13,413 additions & 14,657 deletions openapi/openapi.json

Large diffs are not rendered by default.

416 changes: 391 additions & 25 deletions openapi/swagger.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import it.gov.pagopa.apiconfig.core.model.ProblemJson;
import it.gov.pagopa.apiconfig.core.model.stationmaintenance.CreateStationMaintenance;
import it.gov.pagopa.apiconfig.core.model.stationmaintenance.StationMaintenanceResource;
import it.gov.pagopa.apiconfig.core.model.stationmaintenance.UpdateStationMaintenance;
import it.gov.pagopa.apiconfig.core.service.StationMaintenanceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
Expand All @@ -19,6 +20,7 @@
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Expand Down Expand Up @@ -64,4 +66,32 @@ public ResponseEntity<StationMaintenanceResource> createStationMaintenance(
.status(HttpStatus.CREATED)
.body(this.stationMaintenanceService.createStationMaintenance(brokerCode, createStationMaintenance));
}

@Operation(summary = "Update a maintenance for the specified station",
security = {@SecurityRequirement(name = "ApiKey"), @SecurityRequirement(name = "Authorization")})
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "Created",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = StationMaintenanceResource.class))),
@ApiResponse(responseCode = "400", description = "Bad Request",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ProblemJson.class))),
@ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(schema = @Schema())),
@ApiResponse(responseCode = "403", description = "Forbidden", content = @Content(schema = @Schema())),
@ApiResponse(responseCode = "409", description = "Conflict",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ProblemJson.class))),
@ApiResponse(responseCode = "429", description = "Too many requests", content = @Content(schema = @Schema())),
@ApiResponse(responseCode = "500", description = "Service unavailable",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ProblemJson.class)))
})
@PutMapping(value = "/{brokercode}/station-maintenances/{maintenanceid}", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<StationMaintenanceResource> updateStationMaintenance(
@Parameter(description = "Broker's tax code") @PathVariable("brokercode") String brokerCode,
@Parameter(description = "Maintenance's id") @PathVariable("maintenanceid") Long maintenanceId,
@RequestBody @Valid @NotNull UpdateStationMaintenance updateStationMaintenance
) {
return ResponseEntity.ok(
this.stationMaintenanceService.
updateStationMaintenance(brokerCode, maintenanceId, updateStationMaintenance)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,11 @@ public enum AppError {
CBILL_BAD_REQUEST(HttpStatus.BAD_REQUEST, "CBILL massive loading bad request", "File is not valid: %s"),

// Station Maintenance errors
MAINTENANCE_START_DATE_TIME_NOT_VALID(HttpStatus.BAD_REQUEST, "Invalid station maintenance start date time", "A new maintenance must start after 72h from the creation's date time"),
MAINTENANCE_START_DATE_TIME_NOT_VALID(HttpStatus.BAD_REQUEST, "Invalid station maintenance start date time", "A maintenance must start after 72h from the current date time"),
MAINTENANCE_DATE_TIME_INTERVAL_NOT_VALID(HttpStatus.BAD_REQUEST, "Invalid station maintenance date time interval", "The provided maintenance interval is not valid: %s"),
MAINTENANCE_DATE_TIME_INTERVAL_HAS_OVERLAPPING(HttpStatus.CONFLICT, "Conflict on station maintenance date time interval", "There is an overlapping maintenance for the provided date time interval"),
MAINTENANCE_SUMMARY_NOT_FOUND(HttpStatus.NOT_FOUND, "Maintenance summary not found", "The maintenance summary for the provided broker %s and year %s was not found"),
MAINTENANCE_NOT_FOUND(HttpStatus.NOT_FOUND, "Maintenance not found", "The maintenance with the provided id %s was not found"),


UNKNOWN(null, null, null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package it.gov.pagopa.apiconfig.core.model.stationmaintenance;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.ser.OffsetDateTimeSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import it.gov.pagopa.apiconfig.core.util.Constants;
import it.gov.pagopa.apiconfig.core.util.OffsetDateTimeDeserializer;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.format.annotation.DateTimeFormat;

import javax.validation.constraints.NotNull;
import java.time.OffsetDateTime;

/**
* Model class that define the input field for updating a station's maintenance
*/
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UpdateStationMaintenance {

@JsonProperty("start_date_time")
@JsonFormat(pattern = Constants.DateTimeFormat.DATE_TIME_FORMAT)
@JsonSerialize(using = OffsetDateTimeSerializer.class)
@JsonDeserialize(using = OffsetDateTimeDeserializer.class)
@Schema(
example = "2024-04-01T13:00:00.000Z",
description = "The start date time of the station maintenance")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private OffsetDateTime startDateTime;

@JsonProperty("end_date_time")
@NotNull
@JsonFormat(pattern = Constants.DateTimeFormat.DATE_TIME_FORMAT)
@JsonSerialize(using = OffsetDateTimeSerializer.class)
@JsonDeserialize(using = OffsetDateTimeDeserializer.class)
@Schema(
example = "2024-04-01T13:00:00.000Z",
required = true,
description = "The end date time of the station maintenance")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private OffsetDateTime endDateTime;

@JsonProperty("stand_in")
@Schema(description = "StandIn flag")
private Boolean standIn;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import it.gov.pagopa.apiconfig.core.exception.AppException;
import it.gov.pagopa.apiconfig.core.model.stationmaintenance.CreateStationMaintenance;
import it.gov.pagopa.apiconfig.core.model.stationmaintenance.StationMaintenanceResource;
import it.gov.pagopa.apiconfig.core.model.stationmaintenance.UpdateStationMaintenance;
import it.gov.pagopa.apiconfig.core.repository.ExtendedStationMaintenanceRepository;
import it.gov.pagopa.apiconfig.starter.entity.StationMaintenance;
import it.gov.pagopa.apiconfig.starter.entity.StationMaintenanceSummaryId;
Expand Down Expand Up @@ -66,16 +67,16 @@ public StationMaintenanceResource createStationMaintenance(
OffsetDateTime startDateTime = createStationMaintenance.getStartDateTime().truncatedTo(ChronoUnit.MINUTES);
OffsetDateTime endDateTime = createStationMaintenance.getEndDateTime().truncatedTo(ChronoUnit.MINUTES);

if (computeDateDifferenceInHours(now, startDateTime) < 72) {
throw new AppException(AppError.MAINTENANCE_START_DATE_TIME_NOT_VALID);
}
if (isNotRoundedTo15Minutes(startDateTime) || isNotRoundedTo15Minutes(endDateTime)) {
throw new AppException(AppError.MAINTENANCE_DATE_TIME_INTERVAL_NOT_VALID, "Date time are not rounded to 15 minutes");
}
if (startDateTime.isAfter(endDateTime) || startDateTime.isEqual(endDateTime)) {
if (computeDateDifferenceInHours(now, startDateTime) < 72) {
throw new AppException(AppError.MAINTENANCE_START_DATE_TIME_NOT_VALID);
}
if (!endDateTime.isAfter(startDateTime)) {
throw new AppException(AppError.MAINTENANCE_DATE_TIME_INTERVAL_NOT_VALID, "Start date time must be before end date time");
}
if (hasOverlappingMaintenance(createStationMaintenance, startDateTime, endDateTime)) {
if (hasOverlappingMaintenance(createStationMaintenance.getStationCode(), startDateTime, endDateTime, null)) {
throw new AppException(AppError.MAINTENANCE_DATE_TIME_INTERVAL_HAS_OVERLAPPING);
}

Expand All @@ -92,6 +93,134 @@ public StationMaintenanceResource createStationMaintenance(
.build();
}

/**
* Update the station's maintenance with the specified maintenance id with the provided data.
* <p>
* If the maintenance is already in progress, only the endDateTime field can be
* updated otherwise startDateTime, endDateTime and standIn fields can be updated.
* Before the update of the maintenance checks if all the requirements are matched, if it is an in progress maintenance
* perform the following checks:
* <ul>
* <li> endDateTime is valid only if it is rounded to a 15-minute interval (seconds and milliseconds are truncated)
* <li> current timestamp < endDateTime
* <li> there are no overlapping maintenance for the same station excluded the current maintenance
* </ul>
* Otherwise, perform the following checks:
* <ul>
* <li> the startDateTime is after 72h from the creation date time
* <li> startDateTime and endDateTime are valid only if they are rounded to a 15-minute interval
* (seconds and milliseconds are truncated)
* <li> startDateTime < endDateTime
* <li> there are no overlapping maintenance for the same station excluded the current maintenance
* </ul>
* Additionally, in case of scheduled maintenance it retrieves the count of maintenance hours for the specified broker,
* and if the annual limit has been reached, the StandIn flag is set to true.
*
* @param brokerCode broker's tax code
* @param maintenanceId maintenance's id
* @param updateStationMaintenance update info of the maintenance
* @return the updated maintenance
*/
public StationMaintenanceResource updateStationMaintenance(
String brokerCode,
Long maintenanceId,
UpdateStationMaintenance updateStationMaintenance
) {
StationMaintenance stationMaintenance = this.stationMaintenanceRepository.findById(maintenanceId)
.orElseThrow(() -> new AppException(AppError.MAINTENANCE_NOT_FOUND, maintenanceId));
OffsetDateTime now = OffsetDateTime.now().truncatedTo(ChronoUnit.MINUTES);

StationMaintenance saved;
boolean isMaintenanceInProgress = stationMaintenance.getStartDateTime().isBefore(now.plusMinutes(15));
if (isMaintenanceInProgress) {
saved = updateInProgressStationMaintenance(now, updateStationMaintenance, stationMaintenance);
} else {
saved = updateScheduledStationMaintenance(brokerCode, now, updateStationMaintenance, stationMaintenance);
}

return StationMaintenanceResource.builder()
.maintenanceId(saved.getObjId())
.brokerCode(brokerCode)
.startDateTime(saved.getStartDateTime())
.endDateTime(saved.getEndDateTime())
.standIn(saved.getStandIn())
.stationCode(stationMaintenance.getStation().getIdStazione())
.build();
}

private StationMaintenance updateScheduledStationMaintenance(
String brokerCode,
OffsetDateTime now,
UpdateStationMaintenance updateStationMaintenance,
StationMaintenance oldStationMaintenance
) {
if (updateStationMaintenance.getStartDateTime() == null) {
throw new AppException(AppError.MAINTENANCE_START_DATE_TIME_NOT_VALID);
}

OffsetDateTime newStartDateTime = updateStationMaintenance.getStartDateTime().truncatedTo(ChronoUnit.MINUTES);
OffsetDateTime newEndDateTime = updateStationMaintenance.getEndDateTime().truncatedTo(ChronoUnit.MINUTES);

if (isNotRoundedTo15Minutes(newStartDateTime) || isNotRoundedTo15Minutes(newEndDateTime)) {
throw new AppException(AppError.MAINTENANCE_DATE_TIME_INTERVAL_NOT_VALID, "Date time are not rounded to 15 minutes");
}
if (computeDateDifferenceInHours(now, newStartDateTime) < 72) {
throw new AppException(AppError.MAINTENANCE_START_DATE_TIME_NOT_VALID);
}
if (!newEndDateTime.isAfter(newStartDateTime)) {
throw new AppException(AppError.MAINTENANCE_DATE_TIME_INTERVAL_NOT_VALID, "Start date time must be before end date time");
}
if (hasOverlappingMaintenance(
oldStationMaintenance.getStation().getIdStazione(),
newStartDateTime,
newEndDateTime,
oldStationMaintenance.getObjId()
)) {
throw new AppException(AppError.MAINTENANCE_DATE_TIME_INTERVAL_HAS_OVERLAPPING);
}

boolean standIn = updateStationMaintenance.getStandIn() != null
? updateStationMaintenance.getStandIn()
: oldStationMaintenance.getStandIn();
double oldScheduledHours = computeDateDifferenceInHours(oldStationMaintenance.getStartDateTime(), oldStationMaintenance.getEndDateTime());
// force standIn flag to true when the used has already consumed all the available hours for this year
if (isAnnualHoursLimitExceededForUser(brokerCode, now, newStartDateTime, newEndDateTime, oldScheduledHours)) {
standIn = true;
}

oldStationMaintenance.setStartDateTime(newStartDateTime);
oldStationMaintenance.setEndDateTime(newEndDateTime);
oldStationMaintenance.setStandIn(standIn);
return this.stationMaintenanceRepository.save(oldStationMaintenance);
}

private StationMaintenance updateInProgressStationMaintenance(
OffsetDateTime now,
UpdateStationMaintenance updateStationMaintenance,
StationMaintenance oldStationMaintenance
) {
OffsetDateTime newEndDateTime = updateStationMaintenance.getEndDateTime().truncatedTo(ChronoUnit.MINUTES);

if (isNotRoundedTo15Minutes(newEndDateTime)) {
throw new AppException(AppError.MAINTENANCE_DATE_TIME_INTERVAL_NOT_VALID, "End date time is not rounded to 15 minutes");
}
if (!newEndDateTime.isAfter(now)) {
throw new AppException(AppError.MAINTENANCE_DATE_TIME_INTERVAL_NOT_VALID,
"End date time of an in progress maintenance can not be before current timestamp");
}
if (hasOverlappingMaintenance(
oldStationMaintenance.getStation().getIdStazione(),
oldStationMaintenance.getStartDateTime(),
newEndDateTime,
oldStationMaintenance.getObjId()
)) {
throw new AppException(AppError.MAINTENANCE_DATE_TIME_INTERVAL_HAS_OVERLAPPING);
}

oldStationMaintenance.setEndDateTime(newEndDateTime);
return this.stationMaintenanceRepository.save(oldStationMaintenance);
}

private StationMaintenance buildStationMaintenance(
String brokerCode,
CreateStationMaintenance createStationMaintenance,
Expand All @@ -101,7 +230,7 @@ private StationMaintenance buildStationMaintenance(
) {
boolean standIn = createStationMaintenance.getStandIn();
// force standIn flag to true when the used has already consumed all the available hours for this year
if (isAnnualHoursLimitExceededForUser(brokerCode, now, startDateTime, endDateTime)) {
if (isAnnualHoursLimitExceededForUser(brokerCode, now, startDateTime, endDateTime, 0)) {
standIn = true;
}

Expand All @@ -126,7 +255,8 @@ private boolean isAnnualHoursLimitExceededForUser(
String brokerCode,
OffsetDateTime now,
OffsetDateTime startDateTime,
OffsetDateTime endDateTime
OffsetDateTime endDateTime,
double oldScheduledHours
) {
StationMaintenanceSummaryView maintenanceSummary = this.summaryViewRepository.findById(
StationMaintenanceSummaryId.builder()
Expand All @@ -137,17 +267,21 @@ private boolean isAnnualHoursLimitExceededForUser(
double consumedHours = maintenanceSummary.getUsedHours() + maintenanceSummary.getScheduledHours();
double newHoursToBeScheduled = computeDateDifferenceInHours(startDateTime, endDateTime);

return (consumedHours + newHoursToBeScheduled) > 36;
return (consumedHours - oldScheduledHours + newHoursToBeScheduled) > 36;

}

private boolean hasOverlappingMaintenance(
CreateStationMaintenance createStationMaintenance,
String stationCode,
OffsetDateTime startDateTime,
OffsetDateTime endDateTime
OffsetDateTime endDateTime,
Long excludedMaintenance
) {
return !this.stationMaintenanceRepository
.findOverlappingMaintenance(createStationMaintenance.getStationCode(), startDateTime, endDateTime)
.findOverlappingMaintenance(stationCode, startDateTime, endDateTime)
.parallelStream()
.filter(maintenance -> excludedMaintenance == null || !maintenance.getObjId().equals(excludedMaintenance))
.toList()
.isEmpty();
}

Expand Down
Loading

0 comments on commit 5c416b6

Please sign in to comment.