Skip to content

Commit

Permalink
Feature/expand project types (#162)
Browse files Browse the repository at this point in the history
Add project types attending to billing

---------

Co-authored-by: adrianavillar <[email protected]>
  • Loading branch information
RubenGonzalezDePablo and adrianavillar authored May 31, 2024
1 parent 6c9d59a commit 2704b05
Show file tree
Hide file tree
Showing 58 changed files with 386 additions and 175 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class ActivityRequestBodyConverter() {
subcontractingActivityRequestBody.description,
projectRole,
user.id,
true,
subcontractingActivityRequestBody.billable,
user.departmentId,
insertDate,
false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ class ActivityResponseConverter(
id = activity.id!!,
projectRoleId = activity.projectRole.id,
month = YearMonth.of(activity.timeInterval.start.year,activity.timeInterval.start.month),
userId = activity.userId
userId = activity.userId,
billable = activity.billable
)

fun mapActivityToActivityResponse(activity: Activity) = ActivityResponse(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.autentia.tnt.binnacle.converters

import com.autentia.tnt.binnacle.entities.Project
import com.autentia.tnt.binnacle.entities.ProjectBillingTypes
import com.autentia.tnt.binnacle.entities.dto.ProjectResponseDTO
import jakarta.inject.Singleton

Expand All @@ -11,7 +12,7 @@ class ProjectResponseConverter {
project.id,
project.name,
project.open,
project.billable,
ProjectBillingTypes().getProjectBillingType(project.billingType),
project.organization.id,
project.startDate,
project.blockDate,
Expand All @@ -23,7 +24,7 @@ class ProjectResponseConverter {
project.id,
project.name,
project.open,
project.billable,
project.projectBillingType,
project.organization.id,
project.startDate,
project.blockDate,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package com.autentia.tnt.binnacle.core.domain

import com.autentia.tnt.binnacle.entities.ProjectBillingType
import java.time.LocalDate

data class Project(
val id: Long,
val name: String,
val open: Boolean,
val billable: Boolean,
val projectBillingType: ProjectBillingType,
val startDate: LocalDate,
val blockDate: LocalDate? = null,
val blockedByUser: Long? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ data class Project(

val name: String,
val open: Boolean,
val billable: Boolean,
val startDate: LocalDate,
var blockDate: LocalDate? = null,
var blockedByUser: Long? = null,
Expand All @@ -25,13 +24,18 @@ data class Project(
@OneToMany(mappedBy = "project", fetch = FetchType.LAZY)
@Fetch(FetchMode.SUBSELECT)
@JsonIgnore
val projectRoles: List<ProjectRole>
val projectRoles: List<ProjectRole>,


val billingType: String = ""


) {
fun toDomain() = com.autentia.tnt.binnacle.core.domain.Project(
id,
name,
open,
billable,
ProjectBillingTypes().getProjectBillingType(billingType),
startDate,
blockDate,
blockedByUser,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.autentia.tnt.binnacle.entities

class ProjectBillingTypes{
private val noBillingDefault = ProjectBillingType("NO_BILLABLE", Billable.NEVER, false)
private val closedPrice = ProjectBillingType("CLOSED_PRICE", Billable.ALWAYS, true)
private val timeMaterials = ProjectBillingType("TIME_AND_MATERIALS", Billable.OPTIONAL, true)
private val timeMaterialsLimited = ProjectBillingType("TIME_AND_MATERIALS_LIMITED", Billable.OPTIONAL, true)
private val bundleOfHours = ProjectBillingType("BUNDLE_OF_HOURS", Billable.OPTIONAL, true)
fun getProjectBillingType(type: String):ProjectBillingType{
when (type){
"NO_BILLABLE" -> return noBillingDefault
"CLOSED_PRICE" ->return closedPrice
"TIME_AND_MATERIALS" -> return timeMaterials
"TIME_AND_MATERIALS_LIMITED" ->return timeMaterialsLimited
"BUNDLE_OF_HOURS" ->return bundleOfHours
else -> throw IllegalArgumentException(type + " is not a valid project billing type")
}
}
}

data class ProjectBillingType(
val name: String,
val type: Billable,
val billableByDefault: Boolean
)
enum class Billable{
ALWAYS, NEVER, OPTIONAL
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package com.autentia.tnt.binnacle.entities.dto

import com.autentia.tnt.binnacle.entities.ProjectBillingType
import java.time.LocalDate

data class ProjectResponseDTO(
val id: Long,
val name: String,
val open: Boolean,
val billable: Boolean,
val projectBillingType: ProjectBillingType,
val organizationId: Long,
val startDate: LocalDate,
val blockDate: LocalDate? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ data class SubcontractedActivityRequestDTO (
val month: YearMonth,
val duration: Int,
val description: String,
val billable: Boolean,
val projectRoleId: Long,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import java.time.YearMonth
data class SubcontractedActivityResponseDTO (
val duration: Int,
val description: String,
val billable: Boolean,
val id: Long,
val projectRoleId: Long,
val month: YearMonth,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.autentia.tnt.binnacle.exception

class ActivityBillableIncoherenceException(message: String) : BinnacleException(message) {
constructor() : this("The billing field of the activity is incoherent with the project billing type")
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.autentia.tnt.binnacle.core.domain.Activity
import com.autentia.tnt.binnacle.core.domain.Calendar
import com.autentia.tnt.binnacle.core.domain.TimeInterval
import com.autentia.tnt.binnacle.entities.ApprovalState
import com.autentia.tnt.binnacle.entities.Billable
import com.autentia.tnt.binnacle.entities.Project
import com.autentia.tnt.binnacle.entities.TimeUnit
import com.autentia.tnt.binnacle.services.ActivityCalendarService
Expand Down Expand Up @@ -159,4 +160,12 @@ internal abstract class AbstractActivityValidator(
val activities = activityService.findOverlappedActivities(activity.getStart(), activity.getEnd(), userId)
return activities.size > 1 || activities.size == 1 && activities[0].id != activity.id
}

protected fun isActivityBillableCoherenceWithProjectBillingType(activity: Activity):Boolean{
when(activity.projectRole.project.projectBillingType.type){
Billable.NEVER -> return !activity.billable
Billable.ALWAYS -> return activity.billable
Billable.OPTIONAL -> return true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ internal class ActivityValidator(
isProjectBlocked(project, activityToCreate) -> throw ProjectBlockedException(project.blockDate!!)
isBeforeProjectCreationDate(activityToCreate, project) -> throw ActivityBeforeProjectCreationDateException()
isOverlappingAnotherActivityTime(activityToCreate, user.id) -> throw OverlapsAnotherTimeException()
!isActivityBillableCoherenceWithProjectBillingType(activityToCreate) -> throw ActivityBillableIncoherenceException()
user.isBeforeHiringDate(activityToCreate.timeInterval.start.toLocalDate()) ->
throw ActivityBeforeHiringDateException()

Expand Down Expand Up @@ -95,6 +96,7 @@ internal class ActivityValidator(
!activityToUpdate.projectRole.project.open -> throw ProjectClosedException()
!isOpenPeriod(activityToUpdate.timeInterval.start) -> throw ActivityPeriodClosedException()
isOverlappingAnotherActivityTime(activityToUpdate, user.id) -> throw OverlapsAnotherTimeException()
!isActivityBillableCoherenceWithProjectBillingType(activityToUpdate) -> throw ActivityBillableIncoherenceException()
user.isBeforeHiringDate(activityToUpdate.timeInterval.start.toLocalDate()) ->
throw ActivityBeforeHiringDateException()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ internal class SubcontractedActivityValidator(
isProjectBlocked(project, activityToCreate) -> throw ProjectBlockedException(project.blockDate!!)
isBeforeProjectCreationDate(activityToCreate, project) -> throw ActivityBeforeProjectCreationDateException()
!isPositiveDuration(activityToCreate) -> throw InvalidDurationFormatException()

!isActivityBillableCoherenceWithProjectBillingType(activityToCreate) -> throw ActivityBillableIncoherenceException()
}
}

Expand Down Expand Up @@ -64,8 +64,7 @@ internal class SubcontractedActivityValidator(
!activityToUpdate.projectRole.project.open -> throw ProjectClosedException()
!isOpenPeriod(activityToUpdate.timeInterval.start) -> throw ActivityPeriodClosedException()
!isPositiveDuration(activityToUpdate) -> throw InvalidDurationFormatException()


!isActivityBillableCoherenceWithProjectBillingType(activityToUpdate) -> throw ActivityBillableIncoherenceException()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,24 +149,24 @@ internal fun createProject(id: Long = 1L) = Project(
id = id,
name = "Dummy Project",
open = true,
billable = false,
LocalDate.now(),
null,
null,
projectRoles = listOf(),
organization = createOrganization()
organization = createOrganization(),
billingType = "TIME_AND_MATERIALS"
)

internal fun createBlockedProject(id: Long = 1L) = Project(
id = id,
name = "Dummy Project",
open = true,
billable = false,
LocalDate.now(),
blockDate = LocalDate.of(2000, 1, 1),
null,
projectRoles = listOf(),
organization = createOrganization()
organization = createOrganization(),
billingType = "NO_BILLABLE"
)

internal fun createProjectRoleTimeInfo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@ package com.autentia.tnt.binnacle.converters

import com.autentia.tnt.binnacle.config.createUser
import com.autentia.tnt.binnacle.core.domain.ActivityRequestBody
import com.autentia.tnt.binnacle.entities.Activity
import com.autentia.tnt.binnacle.entities.ApprovalState
import com.autentia.tnt.binnacle.entities.Organization
import com.autentia.tnt.binnacle.entities.Project
import com.autentia.tnt.binnacle.entities.ProjectRole
import com.autentia.tnt.binnacle.entities.RequireEvidence
import com.autentia.tnt.binnacle.entities.TimeUnit
import com.autentia.tnt.binnacle.entities.*
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.time.LocalDate
Expand Down Expand Up @@ -44,7 +38,7 @@ internal class ActivityRequestBodyConverterTest {

val DUMMY_ORGANIZATION = Organization(1L, "Dummy Organization", 1, listOf())

val DUMMY_PROJECT = Project(1L, "Dummy Project", open = true, billable = false, LocalDate.now(), null, null, projectRoles = listOf(), organization = DUMMY_ORGANIZATION)
val DUMMY_PROJECT = Project(1L, "Dummy Project", open = true, LocalDate.now(), null, null, projectRoles = listOf(), organization = DUMMY_ORGANIZATION, billingType = "NO_BILLABLE")

val DUMMY_PROJECT_ROLE =
ProjectRole(10L, "Dummy Project role", RequireEvidence.NO, DUMMY_PROJECT, 0, 0, true, false, TimeUnit.MINUTES)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.autentia.tnt.binnacle.converters
import com.autentia.tnt.binnacle.config.createOrganization
import com.autentia.tnt.binnacle.entities.Organization
import com.autentia.tnt.binnacle.entities.Project
import com.autentia.tnt.binnacle.entities.ProjectBillingTypes
import com.autentia.tnt.binnacle.entities.dto.ProjectResponseDTO
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
Expand All @@ -24,20 +25,19 @@ internal class ProjectResponseConverterTest {
id = 1,
name = "Dummy Project",
open = false,
billable = false,
LocalDate.now(),
null,
null,
projectRoles = listOf(),
organization = Mockito.mock(Organization::class.java)
organization = Mockito.mock(Organization::class.java),
billingType = "NO_BILLABLE"
)

val projectResponseDTO = sut.toProjectResponseDTO(project)

assertEquals(project.id, projectResponseDTO.id)
assertEquals(project.name, projectResponseDTO.name)
assertEquals(project.open, projectResponseDTO.open)
assertEquals(project.billable, projectResponseDTO.billable)
}

@Test
Expand All @@ -50,23 +50,23 @@ internal class ProjectResponseConverterTest {
id = 1,
name = "First Project",
open = false,
billable = false,
startDate,
null,
null,
projectRoles = listOf(),
organization = organization
organization = organization,
billingType = "NO_BILLABLE"
),
Project(
id = 2,
name = "Second Project",
open = false,
billable = true,
startDate,
null,
null,
projectRoles = listOf(),
organization = organization
organization = organization,
billingType = "CLOSED_PRICE"
),
)

Expand All @@ -79,15 +79,16 @@ internal class ProjectResponseConverterTest {
id = 1,
name = "First Project",
open = false,
billable = false,
ProjectBillingTypes().getProjectBillingType("NO_BILLABLE"),
1L,
startDate,

),
ProjectResponseDTO(
id = 2,
name = "Second Project",
open = false,
billable = true,
ProjectBillingTypes().getProjectBillingType("CLOSED_PRICE"),
1L,
startDate,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ internal class ProjectRoleUserConverterTest {
}

private companion object {
val project = Project(1, "Dummy project", false, false, LocalDate.now(), null, null, Organization(2, "Organzation", 1, listOf()), listOf())
val project = Project(1, "Dummy project", false, LocalDate.now(), null, null, Organization(2, "Organzation", 1, listOf()), listOf(),"NO_BILLABLE")
val role = ProjectRole(1, "First Role", RequireEvidence.NO, project, 0, 0, true, false, TimeUnit.MINUTES)
val timeInfo =TimeInfo(MaxTimeAllowed(250, 0), TimeUnit.MINUTES)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import com.autentia.tnt.binnacle.entities.ApprovalState
import com.autentia.tnt.binnacle.entities.Project
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import jakarta.inject.Inject
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import java.lang.IllegalStateException
import java.time.LocalDate
import java.time.LocalTime

Expand Down Expand Up @@ -172,20 +172,20 @@ internal class ActivityDaoIT {

projectRepository.update(
Project(
project.id, project.name, false, project.billable, LocalDate.now(), null, null, project.organization, project.projectRoles
project.id, project.name, false, LocalDate.now(), null, null, project.organization, project.projectRoles, project.billingType
)
)
projectRepository.update(
Project(
openedProject.id,
openedProject.name,
true,
openedProject.billable,
openedProject.startDate,
openedProject.blockDate,
openedProject.blockedByUser,
openedProject.organization,
openedProject.projectRoles
openedProject.projectRoles,
openedProject.billingType
)
)

Expand Down
Loading

0 comments on commit 2704b05

Please sign in to comment.