Merge branch 'inventory' into 'master'

Inventory

See merge request color-recipes-explorer/backend!17
This commit is contained in:
William Nolin 2021-03-20 02:41:44 +00:00
commit 91927eca84
11 changed files with 120 additions and 247 deletions

View File

@ -309,6 +309,16 @@ private enum class ControllerAuthorizations(
val antMatcher: String,
val permissions: Map<HttpMethod, EmployeePermission>
) {
INVENTORY_ADD(
"/api/material/inventory/add", mapOf(
HttpMethod.PUT to EmployeePermission.EDIT_MATERIAL
)
),
INVENTORY_DEDUCT(
"/api/material/inventory/deduct", mapOf(
HttpMethod.PUT to EmployeePermission.VIEW_MATERIAL
)
),
MATERIALS(
"/api/material/**", mapOf(
HttpMethod.GET to EmployeePermission.VIEW_MATERIAL,

View File

@ -15,6 +15,7 @@ import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
import javax.validation.constraints.Size
import kotlin.jvm.Transient
private const val EMPLOYEE_ID_NULL_MESSAGE = "Un numéro d'employé est requis"
@ -136,14 +137,7 @@ data class EmployeeGroup(
@Column(name = "permission")
@Fetch(FetchMode.SUBSELECT)
val permissions: MutableSet<EmployeePermission> = mutableSetOf(),
@OneToMany(mappedBy = "group")
@field:JsonIgnore
val employees: MutableSet<Employee> = mutableSetOf()
) : NamedModel {
@JsonProperty("employeeCount")
fun getEmployeeCount() = employees.size - 1 // -1 removes the default employee
}
) : NamedModel
open class EmployeeGroupSaveDto(
@field:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
@ -327,9 +321,8 @@ fun employeeGroup(
id: Long? = null,
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
employees: MutableSet<Employee> = mutableSetOf(),
op: EmployeeGroup.() -> Unit = {}
) = EmployeeGroup(id, name, permissions, employees).apply(op)
) = EmployeeGroup(id, name, permissions).apply(op)
fun employeeGroupSaveDto(
name: String = "name",

View File

@ -107,18 +107,20 @@ class GroupsController(groupService: EmployeeGroupServiceImpl) :
@PutMapping("{groupId}/{employeeId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun addEmployeeToGroup(@PathVariable groupId: Long, @PathVariable employeeId: Long): ResponseEntity<Void> {
service.addEmployeeToGroup(groupId, employeeId)
return ResponseEntity
.noContent()
.build()
// service.addEmployeeToGroup(groupId, employeeId)
// return ResponseEntity
// .noContent()
// .build()
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build() // TODO Go to employee controller
}
@DeleteMapping("{groupId}/{employeeId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun removeEmployeeFromGroup(@PathVariable groupId: Long, @PathVariable employeeId: Long): ResponseEntity<Void> {
service.removeEmployeeFromGroup(groupId, employeeId)
return ResponseEntity
.noContent()
.build()
// service.removeEmployeeFromGroup(groupId, employeeId)
// return ResponseEntity
// .noContent()
// .build()
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build() // TODO Go to employee controller
}
}

View File

@ -97,15 +97,13 @@ class InventoryController(
private val inventoryService: InventoryService
) {
@PutMapping("add")
fun add(@RequestBody quantities: Collection<MaterialQuantityDto>): ResponseEntity<Void> {
inventoryService.add(quantities)
return ResponseEntity.ok().build()
fun add(@RequestBody quantities: Collection<MaterialQuantityDto>): ResponseEntity<Collection<MaterialQuantityDto>> {
return ResponseEntity.ok(inventoryService.add(quantities))
}
@PutMapping("deduct")
fun deduct(@RequestBody quantities: Collection<MaterialQuantityDto>): ResponseEntity<Void> {
inventoryService.deduct(quantities)
return ResponseEntity.ok().build()
fun deduct(@RequestBody quantities: Collection<MaterialQuantityDto>): ResponseEntity<Collection<MaterialQuantityDto>> {
return ResponseEntity.ok(inventoryService.deduct(quantities))
}
}

View File

@ -66,23 +66,6 @@ interface EmployeeGroupService :
/** Sets the default group cookie for the given HTTP [response]. */
fun setResponseDefaultGroup(groupId: Long, response: HttpServletResponse)
/** Adds the employee with the given [employeeId] to the group with the given [groupId]. */
fun addEmployeeToGroup(groupId: Long, employeeId: Long)
/**
* Adds a given [employee] to a given [group].
*
* If the [employee] is already in the [group], nothing will be done.
* If the [employee] is already in a group, it will be removed from it.
*/
fun addEmployeeToGroup(group: EmployeeGroup, employee: Employee)
/** Removes the employee with the given [employeeId] from the group with the given [groupId]. */
fun removeEmployeeFromGroup(groupId: Long, employeeId: Long)
/** Removes a given [employee] from the given [group]. */
fun removeEmployeeFromGroup(group: EmployeeGroup, employee: Employee)
}
interface EmployeeUserDetailsService : UserDetailsService {
@ -240,7 +223,7 @@ class EmployeeGroupServiceImpl(
EmployeeGroupService {
override fun existsByName(name: String): Boolean = repository.existsByName(name)
override fun getEmployeesForGroup(id: Long): Collection<Employee> =
getById(id).employees
employeeService.getByGroup(getById(id))
@Transactional
override fun save(entity: EmployeeGroup): EmployeeGroup {
@ -255,8 +238,7 @@ class EmployeeGroupServiceImpl(
EmployeeGroup(
entity.id,
if (name.isNotBlank()) entity.name else persistedGroup.name,
if (permissions.isNotEmpty()) entity.permissions else persistedGroup.permissions,
persistedGroup.employees
if (permissions.isNotEmpty()) entity.permissions else persistedGroup.permissions
)
})
}
@ -286,34 +268,6 @@ class EmployeeGroupServiceImpl(
"$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=${defaultGroupCookieMaxAge}; Path=/api; HttpOnly; Secure; SameSite=strict"
)
}
override fun addEmployeeToGroup(groupId: Long, employeeId: Long) {
addEmployeeToGroup(getById(groupId), employeeService.getById(employeeId))
}
@Transactional
override fun addEmployeeToGroup(group: EmployeeGroup, employee: Employee) {
if (employee.group == group) return
if (employee.group != null) removeEmployeeFromGroup(employee.group!!, employee)
group.employees.add(employee)
employee.group = group
update(group)
employeeService.update(employee)
}
override fun removeEmployeeFromGroup(groupId: Long, employeeId: Long) =
removeEmployeeFromGroup(getById(groupId), employeeService.getById(employeeId))
@Transactional
override fun removeEmployeeFromGroup(group: EmployeeGroup, employee: Employee) {
if (employee.group == null || employee.group != group) return
group.employees.removeIf { it.id == employee.id }
employee.group = null
update(group)
employeeService.update(employee)
}
}
@Service

View File

@ -3,22 +3,23 @@ package dev.fyloz.trial.colorrecipesexplorer.service
import dev.fyloz.trial.colorrecipesexplorer.exception.LowQuantitiesException
import dev.fyloz.trial.colorrecipesexplorer.exception.LowQuantityException
import dev.fyloz.trial.colorrecipesexplorer.model.MaterialQuantityDto
import dev.fyloz.trial.colorrecipesexplorer.service.utils.filterThrows
import dev.fyloz.trial.colorrecipesexplorer.model.materialQuantityDto
import dev.fyloz.trial.colorrecipesexplorer.service.utils.mapMayThrow
import org.springframework.stereotype.Service
import javax.transaction.Transactional
interface InventoryService {
/** Adds each given [MaterialQuantityDto] to the inventory. */
fun add(materialQuantities: Collection<MaterialQuantityDto>)
/** Adds each given [MaterialQuantityDto] to the inventory and returns the updated quantities. */
fun add(materialQuantities: Collection<MaterialQuantityDto>): Collection<MaterialQuantityDto>
/** Adds a given quantity to the given [Material]'s inventory quantity according to the given [materialQuantity]. */
fun add(materialQuantity: MaterialQuantityDto)
/** Adds a given quantity to the given [Material]'s inventory quantity according to the given [materialQuantity] and returns the updated quantity. */
fun add(materialQuantity: MaterialQuantityDto): Float
/** Deducts the inventory quantity of each given [MaterialQuantityDto]. */
fun deduct(materialQuantities: Collection<MaterialQuantityDto>)
/** Deducts the inventory quantity of each given [MaterialQuantityDto] and returns the updated quantities. */
fun deduct(materialQuantities: Collection<MaterialQuantityDto>): Collection<MaterialQuantityDto>
/** Deducts the inventory quantity of a given [Material] by a given quantity according to the given [materialQuantity]. */
fun deduct(materialQuantity: MaterialQuantityDto)
/** Deducts the inventory quantity of a given [Material] by a given quantity according to the given [materialQuantity] and returns the updated quantity. */
fun deduct(materialQuantity: MaterialQuantityDto): Float
}
@Service
@ -26,37 +27,39 @@ class InventoryServiceImpl(
private val materialService: MaterialService
) : InventoryService {
@Transactional
override fun add(materialQuantities: Collection<MaterialQuantityDto>) {
materialQuantities.forEach(::add)
}
override fun add(materialQuantities: Collection<MaterialQuantityDto>) =
materialQuantities.map {
materialQuantityDto(materialId = it.material, quantity = add(it))
}
override fun add(materialQuantity: MaterialQuantityDto) {
override fun add(materialQuantity: MaterialQuantityDto) =
materialService.updateQuantity(
materialService.getById(materialQuantity.material),
materialQuantity.quantity
)
}
@Transactional
override fun deduct(materialQuantities: Collection<MaterialQuantityDto>) {
with(materialQuantities.filterThrows<MaterialQuantityDto, LowQuantityException> {
deduct(it)
}) {
if (this.isNotEmpty()) {
throw LowQuantitiesException(this)
override fun deduct(materialQuantities: Collection<MaterialQuantityDto>): Collection<MaterialQuantityDto> {
val thrown = mutableListOf<MaterialQuantityDto>()
val updatedQuantities =
materialQuantities.mapMayThrow<MaterialQuantityDto, MaterialQuantityDto, LowQuantityException>(
{ thrown.add(it.materialQuantity) }
) {
materialQuantityDto(materialId = it.material, quantity = deduct(it))
}
if (thrown.isNotEmpty()) {
throw LowQuantitiesException(thrown)
}
return updatedQuantities
}
override fun deduct(materialQuantity: MaterialQuantityDto) {
val material = materialService.getById(materialQuantity.material)
if (material.inventoryQuantity >= materialQuantity.quantity) {
materialService.updateQuantity(
material,
-materialQuantity.quantity
)
} else {
throw LowQuantityException(materialQuantity)
override fun deduct(materialQuantity: MaterialQuantityDto): Float =
with(materialService.getById(materialQuantity.material)) {
if (this.inventoryQuantity >= materialQuantity.quantity) {
materialService.updateQuantity(this, -materialQuantity.quantity)
} else {
throw LowQuantityException(materialQuantity)
}
}
}
}

View File

@ -30,8 +30,8 @@ interface MaterialService :
/** Gets the identifier of materials for which a SIMDUT exists. */
fun getAllIdsWithSimdut(): Collection<Long>
/** Updates the quantity of the given [material] with the given [factor]. */
fun updateQuantity(material: Material, factor: Float)
/** Updates the quantity of the given [material] with the given [factor] and returns the updated quantity. */
fun updateQuantity(material: Material, factor: Float): Float
}
@Service
@ -80,7 +80,9 @@ class MaterialServiceImpl(
}
override fun updateQuantity(material: Material, factor: Float) = with(material) {
repository.updateInventoryQuantityById(this.id!!, this.inventoryQuantity + factor)
val updatedQuantity = this.inventoryQuantity + factor
repository.updateInventoryQuantityById(this.id!!, updatedQuantity)
updatedQuantity
}
override fun getAllForMixCreation(recipeId: Long): Collection<Material> {

View File

@ -1,11 +1,18 @@
package dev.fyloz.trial.colorrecipesexplorer.service.utils
/** Returns a list containing only the elements which causes the given [consumer] to throw the given throwable [E]. */
inline fun <T, reified E : Throwable> Iterable<T>.filterThrows(consumer: (T) -> Unit): List<T> = this.filter {
/** Returns a list containing the result of the given [transform] applied to each item of the [Iterable]. If the given [transform] throws, the [Throwable] will be passed to the given [throwableConsumer]. */
inline fun <T, R, reified E : Throwable> Iterable<T>.mapMayThrow(
throwableConsumer: (E) -> Unit = {},
transform: (T) -> R
): List<R> = this.mapNotNull {
try {
consumer(it)
false
transform(it)
} catch (th: Throwable) {
th is E
if (th is E) {
throwableConsumer(th)
null
} else {
throw th
}
}
}

View File

@ -222,9 +222,10 @@ class EmployeeGroupServiceTest :
@Test
fun `getEmployeesForGroup() returns all employees in the given group`() {
val group = employeeGroup(id = 1L, employees = mutableSetOf(groupEmployee))
val group = employeeGroup(id = 1L)
doReturn(group).whenever(service).getById(group.id!!)
whenever(employeeService.getByGroup(group)).doReturn(listOf(groupEmployee))
val found = service.getEmployeesForGroup(group.id!!)
@ -286,122 +287,6 @@ class EmployeeGroupServiceTest :
assertTrue(found.secure)
}
// addEmployeeToGroup()
@Test
fun `addEmployeeToGroup() calls addEmployeeToGroup() with the group of the given groupId and the employee of the given employeeId`() {
whenever(employeeService.getById(groupEmployeeId)).doReturn(groupEmployee)
doReturn(entity).whenever(service).getById(entity.id!!)
doAnswer { }.whenever(service).addEmployeeToGroup(entity, groupEmployee)
service.addEmployeeToGroup(entity.id!!, groupEmployeeId)
verify(service).addEmployeeToGroup(entity, groupEmployee)
}
@Test
fun `addEmployeeToGroup() calls update() and employeeService_update() with the updated entities`() {
val group = employeeGroup()
val employee = employee()
whenever(employeeService.update(employee)).doReturn(employee)
doReturn(group).whenever(service).update(group)
service.addEmployeeToGroup(group, employee)
verify(service).update(group)
verify(employeeService).update(employee)
assertTrue(group.employees.any { it.id == employee.id })
assertEquals(group, employee.group)
}
@Test
fun `addEmployeeToGroup() do nothing when the given employee is already in the given group`() {
val group = employeeGroup()
val employee = employee(group = group)
service.addEmployeeToGroup(group, employee)
verify(service, times(0)).update(group)
verify(employeeService, times(0)).update(employee)
}
@Test
fun `addEmployeeToGroup() remove previous group from the given employee and add it the the given group`() {
val group = employeeGroup(id = 0L)
val previousGroup = employeeGroup(id = 1L)
val employee = employee(group = previousGroup)
whenever(employeeService.update(employee)).doReturn(employee)
doReturn(group).whenever(service).update(group)
doReturn(group).whenever(service).update(previousGroup)
service.addEmployeeToGroup(group, employee)
verify(service).removeEmployeeFromGroup(previousGroup, employee)
verify(service).update(group)
verify(employeeService, times(2)).update(employee)
assertTrue(group.employees.any { it.id == employee.id })
assertEquals(group, employee.group)
}
// removeEmployeeFromGroup()
@Test
fun `removeEmployeeFromGroup() calls removeEmployeeFromGroup() with the group of the given group id and the employee of the given employee id`() {
whenever(employeeService.getById(groupEmployeeId)).doReturn(groupEmployee)
doReturn(entity).whenever(service).getById(entity.id!!)
doAnswer { it.arguments[0] }.whenever(service).update(any<EmployeeGroup>())
service.removeEmployeeFromGroup(entity.id!!, groupEmployeeId)
verify(service).removeEmployeeFromGroup(entity, groupEmployee)
}
@Test
fun `removeEmployeeFromGroup() calls update() and employeeService_update() with the updated entities`() {
val employee = employee()
val group = employeeGroup(employees = mutableSetOf(employee))
employee.group = group
whenever(employeeService.update(any<Employee>())).doReturn(employee)
doReturn(group).whenever(service).update(group)
service.removeEmployeeFromGroup(group, employee)
verify(service).update(group)
verify(employeeService).update(argThat<Employee> { this.group == null })
assertFalse(group.employees.contains(employee))
assertNull(employee.group)
}
@Test
fun `removeEmployeeFromGroup() do nothing when the given employee is not in the given group`() {
val employee = employee()
val group = employeeGroup(id = 0L)
val anotherGroup = employeeGroup(id = 1L, employees = mutableSetOf(employee))
employee.group = anotherGroup
service.removeEmployeeFromGroup(group, employee)
verify(service, times(0)).update(anotherGroup)
verify(employeeService, times(0)).update(employee)
}
@Test
fun `removeEmployeeFromGroup() do nothing when the given employee is not in a group`() {
val employee = employee()
val group = employeeGroup()
service.removeEmployeeFromGroup(group, employee)
verify(service, times(0)).update(group)
verify(employeeService, times(0)).update(employee)
}
// save()
@Test

View File

@ -31,23 +31,32 @@ class InventoryServiceTest {
materialQuantityDto(materialId = 3, quantity = 3456f),
materialQuantityDto(materialId = 4, quantity = 4567f)
)
val storedQuantity = 2000f
service.add(materialQuantities)
doAnswer { storedQuantity + (it.arguments[0] as MaterialQuantityDto).quantity }.whenever(service)
.add(any<MaterialQuantityDto>())
val found = service.add(materialQuantities)
materialQuantities.forEach {
verify(service).add(it)
assertTrue { found.any { updated -> updated.material == it.material && updated.quantity == storedQuantity + it.quantity } }
}
}
@Test
fun `add(materialQuantity) updates material's quantity`() {
withGivenQuantities(0f, 1000f) {
service.add(it)
val updatedQuantity = it + this.quantity
whenever(materialService.updateQuantity(any(), eq(this.quantity))).doReturn(updatedQuantity)
val found = service.add(this)
verify(materialService).updateQuantity(
argThat { this.id == it.material },
eq(it.quantity)
argThat { this.id == this@withGivenQuantities.material },
eq(this.quantity)
)
assertEquals(updatedQuantity, found)
}
}
@ -61,11 +70,16 @@ class InventoryServiceTest {
materialQuantityDto(materialId = 3, quantity = 3456f),
materialQuantityDto(materialId = 4, quantity = 4567f)
)
val storedQuantity = 5000f
service.deduct(materialQuantities)
doAnswer { storedQuantity - (it.arguments[0] as MaterialQuantityDto).quantity }.whenever(service)
.deduct(any<MaterialQuantityDto>())
val found = service.deduct(materialQuantities)
materialQuantities.forEach {
verify(service).deduct(it)
assertTrue { found.any { updated -> updated.material == it.material && updated.quantity == storedQuantity - it.quantity } }
}
}
@ -91,43 +105,47 @@ class InventoryServiceTest {
@Test
fun `deduct(materialQuantity) updates material's quantity`() {
withGivenQuantities(5000f, 1000f) {
service.deduct(it)
val updatedQuantity = it - this.quantity
whenever(materialService.updateQuantity(any(), eq(-this.quantity))).doReturn(updatedQuantity)
val found = service.deduct(this)
verify(materialService).updateQuantity(
argThat { this.id == it.material },
eq(-it.quantity)
argThat { this.id == this@withGivenQuantities.material },
eq(-this.quantity)
)
assertEquals(updatedQuantity, found)
}
}
@Test
fun `deduct(materialQuantity) throws LowQuantityException when there is not enough inventory of the given material`() {
withGivenQuantities(0f, 1000f) {
val exception = assertThrows<LowQuantityException> { service.deduct(it) }
assertEquals(it, exception.materialQuantity)
val exception = assertThrows<LowQuantityException> { service.deduct(this) }
assertEquals(this, exception.materialQuantity)
}
}
private fun withGivenQuantities(
inventory: Float,
deductedQuantity: Float,
stored: Float,
quantity: Float,
materialId: Long = 0L,
test: (MaterialQuantityDto) -> Unit = {}
test: MaterialQuantityDto.(Float) -> Unit = {}
) {
val materialQuantity = materialQuantityDto(materialId = materialId, quantity = deductedQuantity)
val materialQuantity = materialQuantityDto(materialId = materialId, quantity = quantity)
withGivenQuantities(inventory, materialQuantity, test)
withGivenQuantities(stored, materialQuantity, test)
}
private fun withGivenQuantities(
inventory: Float,
stored: Float,
materialQuantity: MaterialQuantityDto,
test: (MaterialQuantityDto) -> Unit = {}
test: MaterialQuantityDto.(Float) -> Unit = {}
) {
val material = material(id = materialQuantity.material, inventoryQuantity = inventory)
val material = material(id = materialQuantity.material, inventoryQuantity = stored)
whenever(materialService.getById(material.id!!)).doReturn(material)
test(materialQuantity)
materialQuantity.test(stored)
}
}

View File

@ -171,9 +171,10 @@ class MaterialServiceTest :
val quantity = 1234f
val totalQuantity = material.inventoryQuantity + quantity
service.updateQuantity(material, quantity)
val found = service.updateQuantity(material, quantity)
verify(repository).updateInventoryQuantityById(material.id!!, totalQuantity)
assertEquals(totalQuantity, found)
}
// getAllForMixCreation()