Skip to content

Commit

Permalink
feat: restrict vacations on selected year (#131)
Browse files Browse the repository at this point in the history
* feat: add chargeYear to request object

* feat: refactor tests and refactor create vacation method. Added new exception branch

* feat: create new remaining vacation service with shared calculation logic to validation, create and update vacation

* feat: refactor calendar calculations to RemainingVacationService and refactor tests

* feat: add NoMoreDaysLeftInYearException to Vacation controller

* feat: check vacation on update

* feat: Refactor test

* feat: Include new test cases for NoMoreDaysLeftInYearException exception and refactor

* feat: Post and Put method will return object response instead of lists

Refactor tests

* feat: Fixed test in VacationControllerIT

* feat: Remove validations from VacationService

* feat: Add tests to RemainingVacationService

* refactor: remove todo comment
  • Loading branch information
david-isla authored Sep 14, 2023
1 parent 5097642 commit 606f8dc
Show file tree
Hide file tree
Showing 23 changed files with 680 additions and 595 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class RequestVacationConverter {
id = requestVacationDTO.id,
startDate = requestVacationDTO.startDate,
endDate = requestVacationDTO.endDate,
chargeYear = requestVacationDTO.chargeYear,
description = requestVacationDTO.description
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ data class RequestVacation (
@field:NotNull
val endDate: LocalDate,

@field:NotNull
val chargeYear: Int,

@field:Size(max = 1024, message = "Description must not exceed 1024 characters")
var description: String? = null
){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ data class RequestVacationDTO(
@field:NotNull
val endDate: LocalDate,

@field:NotNull
val chargeYear: Int,

@field:Size(max = 1024, message = "Description must not exceed 1024 characters")
var description: String? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.autentia.tnt.binnacle.exception

class NoMoreDaysLeftInYearException(message: String) : BinnacleException(message) {
constructor() : this("There are no more days left in selected year")
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,10 @@ internal interface VacationDao : CrudRepository<Vacation, Long> {
endYear: LocalDate,
userId: Long
): List<Vacation>

@Query("SELECT h FROM Vacation h WHERE h.userId= :userId AND h.chargeYear = :chargeYear")
fun findByChargeYear(
chargeYear: LocalDate,
userId: Long
): List<Vacation>
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ internal interface VacationRepository {
endYear: LocalDate
): List<Vacation>

fun findByChargeYear(
chargeYear: LocalDate
): List<Vacation>

fun findBetweenChargeYearsWithoutSecurity(
startYear: LocalDate,
endYear: LocalDate,
Expand All @@ -28,6 +32,7 @@ internal interface VacationRepository {


fun findById(vacationId: Long): Vacation?
fun save(vacation: Vacation): Vacation
fun saveAll(vacations: Iterable<Vacation>): Iterable<Vacation>
fun update(vacation: Vacation): Vacation
fun deleteById(vacationId: Long)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ internal class VacationRepositorySecured(
return vacationDao.findBetweenChargeYears(startYear, endYear, authentication.id())
}

override fun findByChargeYear(chargeYear: LocalDate): List<Vacation> {
val authentication = securityService.checkAuthentication()
return vacationDao.findByChargeYear(chargeYear, authentication.id())
}

override fun findBetweenChargeYearsWithoutSecurity(
startYear: LocalDate,
endYear: LocalDate,
Expand All @@ -46,6 +51,12 @@ internal class VacationRepositorySecured(
}
}

override fun save(vacation: Vacation): Vacation {
val authentication = securityService.checkAuthentication()
require(vacation.userId == authentication.id()) { "User cannot save vacation" }
return vacationDao.save(vacation)
}

override fun saveAll(vacations: Iterable<Vacation>): Iterable<Vacation> {
val authentication = securityService.checkAuthentication()
vacations.forEach { require(it.userId == authentication.id()) { "User cannot save vacation" } }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.autentia.tnt.binnacle.services

import com.autentia.tnt.binnacle.converters.VacationConverter
import com.autentia.tnt.binnacle.core.domain.Calendar
import com.autentia.tnt.binnacle.core.domain.CalendarFactory
import com.autentia.tnt.binnacle.core.domain.DateInterval
import com.autentia.tnt.binnacle.core.domain.RequestVacation
import com.autentia.tnt.binnacle.core.domain.Vacation
import com.autentia.tnt.binnacle.entities.User
import com.autentia.tnt.binnacle.repositories.VacationRepository
import jakarta.inject.Singleton
import java.time.LocalDate
import java.time.Month

@Singleton
internal class RemainingVacationService(
private val vacationRepository: VacationRepository,
private val myVacationsDetailService: MyVacationsDetailService,
private val vacationConverter: VacationConverter,
private val calendarFactory: CalendarFactory,
) {

private fun getVacationsWithWorkableDays(vacations: List<com.autentia.tnt.binnacle.entities.Vacation>): List<Vacation> {
return if (vacations.isEmpty()) {
emptyList()
} else {
val start: LocalDate? = vacations.minOfOrNull(com.autentia.tnt.binnacle.entities.Vacation::startDate)
val end: LocalDate? = vacations.maxOfOrNull(com.autentia.tnt.binnacle.entities.Vacation::endDate)
val dateInterval = DateInterval.of(start!!, end!!)
val calendar = calendarFactory.create(dateInterval)
getVacationsWithWorkableDays(calendar, vacations)
}
}

private fun getVacationsWithWorkableDays(calendar: Calendar, vacations: List<com.autentia.tnt.binnacle.entities.Vacation>): List<Vacation> =
vacations.map {
val days = calendar.getWorkableDays(DateInterval.of(it.startDate, it.endDate))
vacationConverter.toVacationDomain(it, days)
}

private fun getVacationsByChargeYear(
chargeYear: LocalDate
): List<Vacation> {
val vacations = vacationRepository.findByChargeYear(chargeYear)
return getVacationsWithWorkableDays(vacations)
}

fun getRemainingVacations(chargeYear: Int, user: User) : Int {
val vacations = getVacationsByChargeYear(LocalDate.of(chargeYear, 1, 1))
return myVacationsDetailService
.getRemainingVacations(chargeYear, vacations, user)
}

fun getRequestedVacationsSelectedYear(
requestVacation: RequestVacation
): List<LocalDate> {
val currentYear = requestVacation.chargeYear
val lastYear = currentYear - 1
val nextYear = currentYear + 1

val lastYearFirstDay = LocalDate.of(lastYear, Month.JANUARY, 1)
val nextYearLastDay = LocalDate.of(nextYear, Month.DECEMBER, 31)

val calendar = calendarFactory.create(DateInterval.of(lastYearFirstDay, nextYearLastDay))
return calendar.getWorkableDays(DateInterval.of(requestVacation.startDate, requestVacation.endDate))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import com.autentia.tnt.binnacle.core.utils.minDate
import com.autentia.tnt.binnacle.entities.User
import com.autentia.tnt.binnacle.entities.Vacation
import com.autentia.tnt.binnacle.entities.VacationState
import com.autentia.tnt.binnacle.exception.MaxNextYearRequestVacationException
import com.autentia.tnt.binnacle.repositories.VacationRepository
import io.micronaut.transaction.annotation.ReadOnly
import jakarta.inject.Singleton
Expand All @@ -19,9 +18,9 @@ import com.autentia.tnt.binnacle.core.domain.Vacation as VacationDomain
@Singleton
internal class VacationService(
private val vacationRepository: VacationRepository,
private val myVacationsDetailService: MyVacationsDetailService,
private val vacationConverter: VacationConverter,
private val calendarFactory: CalendarFactory,
private val remainingVacationService: RemainingVacationService
) {
@Transactional
@ReadOnly
Expand Down Expand Up @@ -77,104 +76,40 @@ internal class VacationService(
}

@Transactional
fun createVacationPeriod(requestVacation: RequestVacation, user: User): MutableList<CreateVacationResponse> {

val currentYear = LocalDate.now().year
val lastYear = currentYear - 1
val nextYear = currentYear + 1

val lastYearFirstDay = LocalDate.of(lastYear, Month.JANUARY, 1)
val nextYearLastDay = LocalDate.of(nextYear, Month.DECEMBER, 31)

val vacationsByYear: Map<Int, List<VacationDomain>> =
getVacationsByYear(lastYearFirstDay, nextYearLastDay)

val lastYearRemainingVacations = myVacationsDetailService
.getRemainingVacations(lastYear, vacationsByYear.getOrElse(lastYear) { listOf() }, user)
val currentYearRemainingVacations = myVacationsDetailService
.getRemainingVacations(currentYear, vacationsByYear.getOrElse(currentYear) { listOf() }, user)
val nextYearRemainingVacations = myVacationsDetailService
.getRemainingVacations(nextYear, vacationsByYear.getOrElse(nextYear) { listOf() }, user)

var selectedDays = getRequestedVacationsSelectedYear(lastYearFirstDay, nextYearLastDay, requestVacation)

val remainingHolidaysLastAndCurrentYear = lastYearRemainingVacations + currentYearRemainingVacations

val vacationPeriods = mutableListOf<CreateVacationResponse>()

when {
remainingHolidaysLastAndCurrentYear >= selectedDays.size -> {
if (lastYearRemainingVacations > 0) {
vacationPeriods += chargeDaysIntoYear(selectedDays, lastYear, lastYearRemainingVacations)
selectedDays = selectedDays.drop(lastYearRemainingVacations)
}

if (currentYearRemainingVacations > 0 && selectedDays.isNotEmpty()) {
vacationPeriods += chargeDaysIntoYear(selectedDays, currentYear, currentYearRemainingVacations)
}
}

else -> {
if (currentYearRemainingVacations > 0) {
vacationPeriods += chargeDaysIntoYear(selectedDays, currentYear, currentYearRemainingVacations)
selectedDays = selectedDays.drop(currentYearRemainingVacations)
}

if (cantRequestPeriodUsingVacationDaysOfNextYear(
selectedDays.size,
nextYearRemainingVacations,
user.getAgreementTermsByYear(nextYear).vacation
)
) {
throw MaxNextYearRequestVacationException("You can't charge more than 5 days of the next year vacations in the current year")
} else if (nextYearRemainingVacations > 0 && selectedDays.isNotEmpty()) {
vacationPeriods += chargeDaysIntoYear(selectedDays, nextYear, nextYearRemainingVacations)
}
}
}
fun createVacationPeriod(requestVacation: RequestVacation, user: User): CreateVacationResponse {

val vacationsToSave = vacationPeriods.map {
Vacation(
id = null,
startDate = it.startDate,
endDate = it.endDate,
description = requestVacation.description.orEmpty(),
state = VacationState.PENDING,
userId = user.id,
departmentId = user.departmentId,
observations = "",
chargeYear = LocalDate.of(it.chargeYear, Month.JANUARY, 1)
)
}
val selectedDays = remainingVacationService.getRequestedVacationsSelectedYear(requestVacation)

vacationRepository.saveAll(vacationsToSave)
val vacationPeriod = CreateVacationResponse(
startDate = selectedDays.first(),
endDate = selectedDays.last(),
days = selectedDays.size,
chargeYear = requestVacation.chargeYear
)

return vacationPeriods
}
val vacationToSave = Vacation(
id = null,
startDate = vacationPeriod.startDate,
endDate = vacationPeriod.endDate,
description = requestVacation.description.orEmpty(),
state = VacationState.PENDING,
userId = user.id,
departmentId = user.departmentId,
observations = "",
chargeYear = LocalDate.of(vacationPeriod.chargeYear, Month.JANUARY, 1)
)

private fun getRequestedVacationsSelectedYear(
lastYearFirstDay: LocalDate,
nextYearLastDay: LocalDate,
requestVacation: RequestVacation
): List<LocalDate> {
val calendar = calendarFactory.create(DateInterval.of(lastYearFirstDay, nextYearLastDay))
return calendar.getWorkableDays(DateInterval.of(requestVacation.startDate, requestVacation.endDate))
}
vacationRepository.save(vacationToSave)

private fun getVacationsByYear(
lastYearFirstDay: LocalDate,
nextYearLastDay: LocalDate
): Map<Int, List<com.autentia.tnt.binnacle.core.domain.Vacation>> {
val vacations = vacationRepository.findBetweenChargeYears(lastYearFirstDay, nextYearLastDay)
return getVacationsWithWorkableDays(vacations).groupBy { it.chargeYear.year }
return vacationPeriod
}

@Transactional
fun updateVacationPeriod(
requestVacation: RequestVacation,
user: User,
vacation: Vacation
): MutableList<CreateVacationResponse> {
): CreateVacationResponse {

val calendar = calendarFactory.create(
DateInterval.of(
Expand All @@ -187,8 +122,6 @@ internal class VacationService(
val newCorrespondingDays =
calendar.getWorkableDays(DateInterval.of(requestVacation.startDate, requestVacation.endDate)).size

var vacationPeriods = mutableListOf<CreateVacationResponse>()

if (oldCorrespondingDays == newCorrespondingDays) {
val newPeriod = vacation.copy(
startDate = requestVacation.startDate,
Expand All @@ -198,57 +131,18 @@ internal class VacationService(

val savedPeriod = vacationRepository.update(newPeriod)

vacationPeriods.plusAssign(
CreateVacationResponse(
return CreateVacationResponse(
startDate = savedPeriod.startDate,
endDate = savedPeriod.endDate,
days = oldCorrespondingDays,
chargeYear = savedPeriod.chargeYear.year
)
)
} else {
// Delete the request period first
vacationRepository.deleteById(vacation.id!!)

vacationPeriods = createVacationPeriod(requestVacation, user)
}

return vacationPeriods
}
// Delete the request period first
vacationRepository.deleteById(vacation.id!!)

fun cantRequestPeriodUsingVacationDaysOfNextYear(
selectedDays: Int,
nextYearRemainingHolidays: Int,
holidaysQuantity: Int
): Boolean {
val maxVacationDaysOfNextYearToCharge = 5
val alreadyRequested5DaysInNextYear =
nextYearRemainingHolidays <= holidaysQuantity - maxVacationDaysOfNextYearToCharge
val days = nextYearRemainingHolidays - (holidaysQuantity - maxVacationDaysOfNextYearToCharge)

return alreadyRequested5DaysInNextYear || selectedDays > days
}

fun chargeDaysIntoYear(
selectedDays: List<LocalDate>,
year: Int,
remainingHolidays: Int
): CreateVacationResponse {
return if (remainingHolidays > selectedDays.size) {
CreateVacationResponse(
startDate = selectedDays[0],
endDate = selectedDays[selectedDays.size - 1],
days = selectedDays.size,
chargeYear = year
)
} else {
CreateVacationResponse(
startDate = selectedDays[0],
endDate = selectedDays[remainingHolidays - 1],
days = remainingHolidays,
chargeYear = year
)
}
return createVacationPeriod(requestVacation, user)
}

}
Loading

0 comments on commit 606f8dc

Please sign in to comment.