#25 Migrate mixes to new logic

This commit is contained in:
FyloZ 2022-03-19 21:26:01 -04:00
parent 956db504f5
commit efac09a76b
Signed by: william
GPG Key ID: 835378AE9AF4AE97
30 changed files with 861 additions and 841 deletions

View File

@ -5,6 +5,7 @@ object Constants {
const val FILE = "/api/file" const val FILE = "/api/file"
const val MATERIAL = "/api/material" const val MATERIAL = "/api/material"
const val MATERIAL_TYPE = "/api/materialtype" const val MATERIAL_TYPE = "/api/materialtype"
const val MIX = "/api/recipe/mix"
} }
object FilePaths { object FilePaths {

View File

@ -1,9 +1,9 @@
package dev.fyloz.colorrecipesexplorer.config.initializers package dev.fyloz.colorrecipesexplorer.config.initializers
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
import dev.fyloz.colorrecipesexplorer.dtos.MixDto
import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto
import dev.fyloz.colorrecipesexplorer.logic.MixLogic import dev.fyloz.colorrecipesexplorer.logic.MixLogic
import dev.fyloz.colorrecipesexplorer.model.Mix
import dev.fyloz.colorrecipesexplorer.model.MixMaterial
import dev.fyloz.colorrecipesexplorer.utils.merge import dev.fyloz.colorrecipesexplorer.utils.merge
import mu.KotlinLogging import mu.KotlinLogging
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
@ -31,12 +31,12 @@ class MixInitializer(
logger.debug("Mix materials positions are valid!") logger.debug("Mix materials positions are valid!")
} }
private fun fixMixPositions(mix: Mix) { private fun fixMixPositions(mix: MixDto) {
val maxPosition = mix.mixMaterials.maxOf { it.position } val maxPosition = mix.mixMaterials.maxOf { it.position }
logger.warn("Mix ${mix.id} (${mix.mixType.name}, ${mix.recipe.name}) has invalid positions:") logger.warn("Mix ${mix.id} (${mix.mixType.name}, ${mix.recipe.name}) has invalid positions:")
val invalidMixMaterials: Collection<MixMaterial> = with(mix.mixMaterials.filter { it.position == 0 }) { val invalidMixMaterials: Collection<MixMaterialDto> = with(mix.mixMaterials.filter { it.position == 0 }) {
if (maxPosition == 0 && this.size > 1) { if (maxPosition == 0 && this.size > 1) {
orderMixMaterials(this) orderMixMaterials(this)
} else { } else {
@ -52,16 +52,16 @@ class MixInitializer(
} }
} }
private fun increaseMixMaterialsPosition(mixMaterials: Iterable<MixMaterial>, firstPosition: Int) = private fun increaseMixMaterialsPosition(mixMaterials: Iterable<MixMaterialDto>, firstPosition: Int) =
mixMaterials mixMaterials
.mapIndexed { index, mixMaterial -> mixMaterial.copy(position = firstPosition + index) } .mapIndexed { index, mixMaterial -> mixMaterial.copy(position = firstPosition + index) }
.onEach { .onEach {
logger.info("\tPosition of material ${it.material.id} (${it.material.name}) has been set to ${it.position}") logger.info("\tPosition of material ${it.material.id} (${it.material.name}) has been set to ${it.position}")
} }
private fun orderMixMaterials(mixMaterials: Collection<MixMaterial>) = private fun orderMixMaterials(mixMaterials: Collection<MixMaterialDto>) =
LinkedList(mixMaterials).apply { LinkedList(mixMaterials).apply {
while (this.peek().material.materialType?.usePercentages == true) { while (this.peek().material.materialType.usePercentages) {
// The first mix material can't use percents, so move it to the end of the queue // The first mix material can't use percents, so move it to the end of the queue
val pop = this.pop() val pop = this.pop()
this.add(pop) this.add(pop)

View File

@ -36,7 +36,7 @@ class RecipeInitializer(
.filter { groupInfo -> groupInfo.steps!!.any { it.position == 0 } } .filter { groupInfo -> groupInfo.steps!!.any { it.position == 0 } }
.map { fixGroupInformationPositions(recipe, it) } .map { fixGroupInformationPositions(recipe, it) }
val updatedGroupInformation = recipe.groupsInformation.merge(fixedGroupInformation) val updatedGroupInformation = recipe.groupsInformation.merge(fixedGroupInformation) { it.id }
with(recipe.copy(groupsInformation = updatedGroupInformation.toMutableSet())) { with(recipe.copy(groupsInformation = updatedGroupInformation.toMutableSet())) {
recipeLogic.update(this) recipeLogic.update(this)
@ -54,7 +54,7 @@ class RecipeInitializer(
val invalidRecipeSteps = steps.filter { it.position == 0 } val invalidRecipeSteps = steps.filter { it.position == 0 }
val fixedRecipeSteps = increaseRecipeStepsPosition(groupInformation, invalidRecipeSteps, maxPosition + 1) val fixedRecipeSteps = increaseRecipeStepsPosition(groupInformation, invalidRecipeSteps, maxPosition + 1)
val updatedRecipeSteps = steps.merge(fixedRecipeSteps) val updatedRecipeSteps = steps.merge(fixedRecipeSteps) { it.id }
return groupInformation.copy(steps = updatedRecipeSteps.toMutableSet()) return groupInformation.copy(steps = updatedRecipeSteps.toMutableSet())
} }

View File

@ -32,3 +32,10 @@ data class MaterialSaveDto(
val simdutFile: MultipartFile? val simdutFile: MultipartFile?
) : EntityDto ) : EntityDto
data class MaterialQuantityDto(
val materialId: Long,
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
val quantity: Float
)

View File

@ -0,0 +1,46 @@
package dev.fyloz.colorrecipesexplorer.dtos
import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.model.Recipe
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
data class MixDto(
override val id: Long = 0L,
val location: String? = null,
@JsonIgnore
val recipe: Recipe, // TODO change to dto
val mixType: MixTypeDto,
val mixMaterials: Set<MixMaterialDto>
) : EntityDto
data class MixSaveDto(
val id: Long = 0L,
@field:NotBlank
val name: String,
val recipeId: Long = 0L,
val materialTypeId: Long,
val mixMaterials: Set<MixMaterialSaveDto>
)
data class MixDeductDto(
val id: Long,
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
val ratio: Float
)
data class MixLocationDto(
val mixId: Long,
val location: String?
)

View File

@ -2,8 +2,11 @@ package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto
import dev.fyloz.colorrecipesexplorer.dtos.MaterialQuantityDto
import dev.fyloz.colorrecipesexplorer.dtos.MixDeductDto
import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto
import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.Material
import dev.fyloz.colorrecipesexplorer.utils.mapMayThrow import dev.fyloz.colorrecipesexplorer.utils.mapMayThrow
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@ -34,83 +37,87 @@ class DefaultInventoryLogic(
) : InventoryLogic { ) : InventoryLogic {
@Transactional @Transactional
override fun add(materialQuantities: Collection<MaterialQuantityDto>) = override fun add(materialQuantities: Collection<MaterialQuantityDto>) =
materialQuantities.map { materialQuantities.map { MaterialQuantityDto(it.materialId, add(it)) }
materialQuantityDto(materialId = it.material, quantity = add(it))
}
override fun add(materialQuantity: MaterialQuantityDto) = override fun add(materialQuantity: MaterialQuantityDto) =
materialLogic.updateQuantity( materialLogic.updateQuantity(
materialLogic.getById(materialQuantity.material), materialLogic.getById(materialQuantity.materialId),
materialQuantity.quantity materialQuantity.quantity
) )
@Transactional @Transactional
override fun deductMix(mixRatio: MixDeductDto): Collection<MaterialQuantityDto> { override fun deductMix(mixRatio: MixDeductDto): Collection<MaterialQuantityDto> {
val mix = mixLogic.getById(mixRatio.id) val mix = mixLogic.getById(mixRatio.id)
val firstMixMaterial = mix.mixMaterials.first()
val adjustedFirstMaterialQuantity = firstMixMaterial.quantity * mixRatio.ratio
fun adjustQuantity(mixMaterial: MixMaterial): Float = return deduct(getMaterialsWithAdjustedQuantities(mix.mixMaterials, mixRatio))
if (!mixMaterial.material.materialType!!.usePercentages)
mixMaterial.quantity * mixRatio.ratio
else
(mixMaterial.quantity * adjustedFirstMaterialQuantity) / 100f
return deduct(mix.mixMaterials.map {
materialQuantityDto(
materialId = it.material.id!!,
quantity = adjustQuantity(it)
)
})
} }
@Transactional @Transactional
override fun deduct(materialQuantities: Collection<MaterialQuantityDto>): Collection<MaterialQuantityDto> { override fun deduct(materialQuantities: Collection<MaterialQuantityDto>): Collection<MaterialQuantityDto> {
val thrown = mutableListOf<NotEnoughInventoryException>() val thrown = mutableListOf<NotEnoughInventoryException>()
val updatedQuantities = val updatedQuantities =
materialQuantities.mapMayThrow<MaterialQuantityDto, MaterialQuantityDto, NotEnoughInventoryException>( materialQuantities.mapMayThrow<MaterialQuantityDto, MaterialQuantityDto, NotEnoughInventoryException>(
{ thrown.add(it) } { thrown.add(it) }
) { ) {
materialQuantityDto(materialId = it.material, quantity = deduct(it)) MaterialQuantityDto(it.materialId, deduct(it))
} }
if (thrown.isNotEmpty()) { if (thrown.isNotEmpty()) {
throw MultiplesNotEnoughInventoryException(thrown) throw MultiplesNotEnoughInventoryException(thrown)
} }
return updatedQuantities return updatedQuantities
} }
override fun deduct(materialQuantity: MaterialQuantityDto): Float = override fun deduct(materialQuantity: MaterialQuantityDto): Float =
with(materialLogic.getById(materialQuantity.material)) { with(materialLogic.getById(materialQuantity.materialId)) {
if (this.inventoryQuantity >= materialQuantity.quantity) { if (this.inventoryQuantity >= materialQuantity.quantity) {
materialLogic.updateQuantity(this, -materialQuantity.quantity) materialLogic.updateQuantity(this, -materialQuantity.quantity)
} else { } else {
throw NotEnoughInventoryException(materialQuantity.quantity, this) throw NotEnoughInventoryException(materialQuantity.quantity, this)
} }
} }
private fun getMaterialsWithAdjustedQuantities(
mixMaterials: Collection<MixMaterialDto>,
mixRatio: MixDeductDto
): Collection<MaterialQuantityDto> {
val adjustedFirstMaterialQuantity = mixMaterials.first().quantity * mixRatio.ratio
fun getAdjustedQuantity(material: MaterialDto, quantity: Float) =
if (!material.materialType.usePercentages)
quantity * mixRatio.ratio // Simply multiply the quantity by the ratio
else
(quantity * adjustedFirstMaterialQuantity) / 100f // Percents quantities are a ratio of the first material
return mixMaterials.associate { it.material to it.quantity }
.mapValues { getAdjustedQuantity(it.key, it.value) }
.map { MaterialQuantityDto(it.key.id, it.value) }
}
} }
class NotEnoughInventoryException(quantity: Float, material: MaterialDto) : class NotEnoughInventoryException(quantity: Float, material: MaterialDto) :
RestException( RestException(
"notenoughinventory", "notenoughinventory",
"Not enough inventory", "Not enough inventory",
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Cannot deduct ${quantity}mL of ${material.name} because there is only ${material.inventoryQuantity}mL in inventory", "Cannot deduct ${quantity}mL of ${material.name} because there is only ${material.inventoryQuantity}mL in inventory",
mapOf( mapOf(
"material" to material.name, "material" to material.name,
"materialId" to material.id.toString(), "materialId" to material.id.toString(),
"requestQuantity" to quantity, "requestQuantity" to quantity,
"availableQuantity" to material.inventoryQuantity "availableQuantity" to material.inventoryQuantity
)
) )
)
class MultiplesNotEnoughInventoryException(exceptions: List<NotEnoughInventoryException>) : class MultiplesNotEnoughInventoryException(exceptions: List<NotEnoughInventoryException>) :
RestException( RestException(
"notenoughinventory-multiple", "notenoughinventory-multiple",
"Not enough inventory", "Not enough inventory",
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
"Cannot deduct requested quantities because there is no enough of them in inventory", "Cannot deduct requested quantities because there is no enough of them in inventory",
mapOf( mapOf(
"lowQuantities" to exceptions.map { it.extensions } "lowQuantities" to exceptions.map { it.extensions }
)
) )
)

View File

@ -1,102 +1,66 @@
package dev.fyloz.colorrecipesexplorer.logic package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.dtos.MixDto
import dev.fyloz.colorrecipesexplorer.repository.MixRepository import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto
import dev.fyloz.colorrecipesexplorer.utils.setAll import dev.fyloz.colorrecipesexplorer.dtos.MixSaveDto
import dev.fyloz.colorrecipesexplorer.model.Mix
import dev.fyloz.colorrecipesexplorer.service.MixService
import org.springframework.context.annotation.Lazy import org.springframework.context.annotation.Lazy
import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional
import javax.transaction.Transactional
interface MixLogic : ExternalModelService<Mix, MixSaveDto, MixUpdateDto, MixOutputDto, MixRepository> { interface MixLogic : Logic<MixDto, MixService> {
/** Gets all mixes with the given [mixType]. */ /** Saves the given [dto]. */
fun getAllByMixType(mixType: MixType): Collection<Mix> fun save(dto: MixSaveDto): MixDto
/** Checks if a [MixType] is shared by several [Mix]es or not. */ /** Updates the given [dto]. */
fun mixTypeIsShared(mixType: MixType): Boolean fun update(dto: MixSaveDto): MixDto
/** Updates the location of each [Mix] in the given [MixLocationDto]s. */ /** Updates the location of each mix in the given [updatedLocations]. */
fun updateLocations(updatedLocations: Collection<MixLocationDto>) fun updateLocations(updatedLocations: Collection<MixLocationDto>)
/** Updates the location of a given [Mix] to the given [MixLocationDto]. */
fun updateLocation(updatedLocation: MixLocationDto)
} }
@Service @LogicComponent
@RequireDatabase
class DefaultMixLogic( class DefaultMixLogic(
mixRepository: MixRepository, service: MixService,
@Lazy val recipeLogic: RecipeLogic, @Lazy private val recipeLogic: RecipeLogic,
@Lazy val materialTypeLogic: MaterialTypeLogic, @Lazy private val materialTypeLogic: MaterialTypeLogic,
val mixMaterialLogic: MixMaterialLogic, private val mixTypeLogic: MixTypeLogic,
val mixTypeLogic: MixTypeLogic private val mixMaterialLogic: MixMaterialLogic
) : AbstractExternalModelService<Mix, MixSaveDto, MixUpdateDto, MixOutputDto, MixRepository>(mixRepository), ) : BaseLogic<MixDto, MixService>(service, Mix::class.simpleName!!), MixLogic {
MixLogic {
override fun idNotFoundException(id: Long) = mixIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = mixIdAlreadyExistsException(id)
override fun getAllByMixType(mixType: MixType): Collection<Mix> = repository.findAllByMixType(mixType)
override fun mixTypeIsShared(mixType: MixType): Boolean = getAllByMixType(mixType).count() > 1
override fun Mix.toOutput() = MixOutputDto(
this.id!!,
this.location,
this.mixType,
this.mixMaterials.map { mixMaterialDto(it) }.toSet()
)
@Transactional @Transactional
override fun save(entity: MixSaveDto): Mix { override fun save(dto: MixSaveDto): MixDto {
val recipe = recipeLogic.getById(entity.recipeId) val recipe = recipeLogic.getById(dto.recipeId)
val materialType = materialTypeLogic.getById(entity.materialTypeId) val materialType = materialTypeLogic.getById(dto.materialTypeId)
val mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(entity.name, materialType)
val mixMaterials = val mix = MixDto(
if (entity.mixMaterials != null) mixMaterialLogic.saveAll(entity.mixMaterials).toSet() else setOf() recipe = recipe,
mixMaterialLogic.validateMixMaterials(mixMaterials) mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(dto.name, materialType),
mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials).toSet()
)
var mix = mix(recipe = recipe, mixType = mixType(mixType), mixMaterials = mixMaterials.map(::mixMaterial).toMutableSet()) return save(mix)
mix = save(mix)
recipeLogic.addMix(recipe, mix)
return mix
} }
@Transactional @Transactional
override fun update(entity: MixUpdateDto): Mix { override fun update(dto: MixSaveDto): MixDto {
val mix = getById(entity.id) val materialType = materialTypeLogic.getById(dto.materialTypeId)
if (entity.name != null || entity.materialTypeId != null) { val mix = getById(dto.id)
val name = entity.name ?: mix.mixType.name
val materialType = if (entity.materialTypeId != null)
materialType(materialTypeLogic.getById(entity.materialTypeId))
else
mix.mixType.material.materialType!!
mix.mixType = if (mixTypeIsShared(mix.mixType)) { return update(
mixType(mixTypeLogic.saveForNameAndMaterialType(name, materialTypeDto(materialType))) MixDto(
} else { id = dto.id,
mixType(mixTypeLogic.updateForNameAndMaterialType(mixTypeDto(mix.mixType), name, materialTypeDto(materialType))) recipe = recipeLogic.getById(dto.recipeId),
} mixType = mixTypeLogic.updateOrCreateForNameAndMaterialType(mix.mixType, dto.name, materialType),
} mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials).toSet()
if (entity.mixMaterials != null) { )
mix.mixMaterials.setAll(mixMaterialLogic.saveAll(entity.mixMaterials!!).map(::mixMaterial).toMutableSet()) )
}
return update(mix)
} }
override fun updateLocations(updatedLocations: Collection<MixLocationDto>) { override fun updateLocations(updatedLocations: Collection<MixLocationDto>) =
updatedLocations.forEach(::updateLocation) updatedLocations.forEach(::updateLocation)
}
override fun updateLocation(updatedLocation: MixLocationDto) { private fun updateLocation(updatedLocation: MixLocationDto) {
repository.updateLocationById(updatedLocation.mixId, updatedLocation.location) service.updateLocationById(updatedLocation.mixId, updatedLocation.location)
} }
}
@Transactional
override fun delete(entity: Mix) {
if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMixException(entity)
recipeLogic.removeMix(entity)
super.delete(entity)
}
}

View File

@ -2,12 +2,14 @@ package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto
import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialSaveDto
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException
import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.MixMaterial import dev.fyloz.colorrecipesexplorer.model.MixMaterial
import dev.fyloz.colorrecipesexplorer.service.MixMaterialService import dev.fyloz.colorrecipesexplorer.service.MixMaterialService
import dev.fyloz.colorrecipesexplorer.utils.PositionUtils import dev.fyloz.colorrecipesexplorer.utils.PositionUtils
import org.springframework.context.annotation.Lazy
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
interface MixMaterialLogic : Logic<MixMaterialDto, MixMaterialService> { interface MixMaterialLogic : Logic<MixMaterialDto, MixMaterialService> {
@ -17,10 +19,13 @@ interface MixMaterialLogic : Logic<MixMaterialDto, MixMaterialService> {
* If any of those criteria are not met, an [InvalidGroupStepsPositionsException] will be thrown. * If any of those criteria are not met, an [InvalidGroupStepsPositionsException] will be thrown.
*/ */
fun validateMixMaterials(mixMaterials: Set<MixMaterialDto>) fun validateMixMaterials(mixMaterials: Set<MixMaterialDto>)
/** Validates the given mix materials [dtos] and save them. */
fun validateAndSaveAll(dtos: Collection<MixMaterialSaveDto>): Collection<MixMaterialDto>
} }
@LogicComponent @LogicComponent
class DefaultMixMaterialLogic(service: MixMaterialService) : class DefaultMixMaterialLogic(service: MixMaterialService, @Lazy private val materialLogic: MaterialLogic) :
BaseLogic<MixMaterialDto, MixMaterialService>(service, MixMaterial::class.simpleName!!), MixMaterialLogic { BaseLogic<MixMaterialDto, MixMaterialService>(service, MixMaterial::class.simpleName!!), MixMaterialLogic {
override fun validateMixMaterials(mixMaterials: Set<MixMaterialDto>) { override fun validateMixMaterials(mixMaterials: Set<MixMaterialDto>) {
if (mixMaterials.isEmpty()) return if (mixMaterials.isEmpty()) return
@ -37,6 +42,21 @@ class DefaultMixMaterialLogic(service: MixMaterialService) :
throw InvalidFirstMixMaterialException(sortedMixMaterials[0]) throw InvalidFirstMixMaterialException(sortedMixMaterials[0])
} }
} }
override fun validateAndSaveAll(dtos: Collection<MixMaterialSaveDto>): Collection<MixMaterialDto> {
val dtosWithMaterials = dtos.map {
MixMaterialDto(
id = it.id,
material = materialLogic.getById(it.materialId),
quantity = it.quantity,
position = it.position
)
}.toSet()
validateMixMaterials(dtosWithMaterials)
return dtosWithMaterials.map(::save)
}
} }
// TODO check if required // TODO check if required

View File

@ -6,27 +6,50 @@ import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto
import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto
import dev.fyloz.colorrecipesexplorer.model.MixType import dev.fyloz.colorrecipesexplorer.model.MixType
import dev.fyloz.colorrecipesexplorer.service.MixTypeService import dev.fyloz.colorrecipesexplorer.service.MixTypeService
import org.springframework.context.annotation.Lazy
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
interface MixTypeLogic : Logic<MixTypeDto, MixTypeService> { interface MixTypeLogic : Logic<MixTypeDto, MixTypeService> {
/** Returns a [MixType] for the given [name] and [materialType]. If this mix type does not already exist, it will be created. */ /** Returns a mix type for the given [name] and [materialType]. If this mix type does not already exist, it will be created. */
fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto
/** Returns a new and persisted [MixType] with the given [name] and [materialType]. */ /** Updates the [mixType] with the given [name] and [materialType], or create a new one if it is shared with other mixes. */
fun saveForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto fun updateOrCreateForNameAndMaterialType(
mixType: MixTypeDto,
/** Returns the given [mixType] updated with the given [name] and [materialType]. */ name: String,
fun updateForNameAndMaterialType(mixType: MixTypeDto, name: String, materialType: MaterialTypeDto): MixTypeDto materialType: MaterialTypeDto
): MixTypeDto
} }
@LogicComponent @LogicComponent
class DefaultMixTypeLogic(service: MixTypeService, private val materialLogic: MaterialLogic) : class DefaultMixTypeLogic(
service: MixTypeService,
@Lazy private val materialLogic: MaterialLogic
) :
BaseLogic<MixTypeDto, MixTypeService>(service, MixType::class.simpleName!!), MixTypeLogic { BaseLogic<MixTypeDto, MixTypeService>(service, MixType::class.simpleName!!), MixTypeLogic {
@Transactional
override fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialTypeDto) = override fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialTypeDto) =
service.getByNameAndMaterialType(name, materialType.id) ?: saveForNameAndMaterialType(name, materialType) service.getByNameAndMaterialType(name, materialType.id) ?: saveForNameAndMaterialType(name, materialType)
@Transactional override fun updateOrCreateForNameAndMaterialType(
override fun saveForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto { mixType: MixTypeDto,
name: String,
materialType: MaterialTypeDto
) = if (service.isShared(mixType.id)) {
saveForNameAndMaterialType(name, materialType)
} else {
updateForNameAndMaterialType(mixType, name, materialType)
}
override fun deleteById(id: Long) {
if (service.isUsedByMixes(id)) {
throw cannotDeleteException("Cannot delete the mix type with the id '$id' because one or more mixes depends on it")
}
super.deleteById(id)
}
private fun saveForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto {
val material = materialLogic.save( val material = materialLogic.save(
MaterialDto( MaterialDto(
name = name, name = name,
@ -39,7 +62,7 @@ class DefaultMixTypeLogic(service: MixTypeService, private val materialLogic: Ma
return save(MixTypeDto(name = name, material = material)) return save(MixTypeDto(name = name, material = material))
} }
override fun updateForNameAndMaterialType( private fun updateForNameAndMaterialType(
mixType: MixTypeDto, mixType: MixTypeDto,
name: String, name: String,
materialType: MaterialTypeDto materialType: MaterialTypeDto
@ -47,12 +70,4 @@ class DefaultMixTypeLogic(service: MixTypeService, private val materialLogic: Ma
val material = materialLogic.update(mixType.material.copy(name = name, materialType = materialType)) val material = materialLogic.update(mixType.material.copy(name = name, materialType = materialType))
return update(mixType.copy(name = name, material = material)) return update(mixType.copy(name = name, material = material))
} }
override fun deleteById(id: Long) {
if (service.isUsedByMixes(id)) {
throw cannotDeleteException("Cannot delete the mix type with the id '$id' because one or more mixes depends on it")
}
super.deleteById(id)
}
} }

View File

@ -72,11 +72,7 @@ class DefaultRecipeLogic(
isApprobationExpired(this), isApprobationExpired(this),
this.remark, this.remark,
this.company, this.company,
this.mixes.map { this.mixes.map { mix(it) }.toSet(),
with(mixLogic) {
it.toOutput()
}
}.toSet(),
this.groupsInformation, this.groupsInformation,
recipeImageLogic.getAllImages(this) recipeImageLogic.getAllImages(this)
.map { this.imageUrl(configService.getContent(ConfigurationType.INSTANCE_URL), it) } .map { this.imageUrl(configService.getContent(ConfigurationType.INSTANCE_URL), it) }

View File

@ -2,13 +2,7 @@ package dev.fyloz.colorrecipesexplorer.model
import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import javax.persistence.* import javax.persistence.*
import javax.validation.constraints.Min
const val SIMDUT_FILES_PATH = "pdf/simdut"
@Entity @Entity
@Table(name = "material") @Table(name = "material")
@ -36,13 +30,6 @@ data class Material(
} }
} }
data class MaterialQuantityDto(
val material: Long,
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
val quantity: Float
)
// === DSL === // === DSL ===
fun material( fun material(
@ -71,10 +58,4 @@ fun material(
@Deprecated("Temporary DSL for transition") @Deprecated("Temporary DSL for transition")
fun materialDto( fun materialDto(
entity: Material entity: Material
) = MaterialDto(entity.id!!, entity.name, entity.inventoryQuantity, entity.isMixType, materialTypeDto(entity.materialType!!)) ) = MaterialDto(entity.id!!, entity.name, entity.inventoryQuantity, entity.isMixType, materialTypeDto(entity.materialType!!))
fun materialQuantityDto(
materialId: Long,
quantity: Float,
op: MaterialQuantityDto.() -> Unit = {}
) = MaterialQuantityDto(materialId, quantity).apply(op)

View File

@ -1,15 +1,11 @@
package dev.fyloz.colorrecipesexplorer.model package dev.fyloz.colorrecipesexplorer.model
import com.fasterxml.jackson.annotation.JsonIgnore import dev.fyloz.colorrecipesexplorer.dtos.MixDto
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import javax.persistence.* import javax.persistence.*
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
@Entity @Entity
@Table(name = "mix") @Table(name = "mix")
@ -20,7 +16,6 @@ data class Mix(
var location: String?, var location: String?,
@JsonIgnore
@ManyToOne @ManyToOne
@JoinColumn(name = "recipe_id") @JoinColumn(name = "recipe_id")
val recipe: Recipe, val recipe: Recipe,
@ -31,53 +26,9 @@ data class Mix(
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "mix_id") @JoinColumn(name = "mix_id")
var mixMaterials: MutableSet<MixMaterial>, var mixMaterials: Set<MixMaterial>,
) : ModelEntity ) : ModelEntity
open class MixSaveDto(
@field:NotBlank
val name: String,
val recipeId: Long,
val materialTypeId: Long,
val mixMaterials: Set<MixMaterialDto>?
) : EntityDto<Mix>
open class MixUpdateDto(
val id: Long,
@field:NotBlank
val name: String?,
val materialTypeId: Long?,
var mixMaterials: Set<MixMaterialDto>?
) : EntityDto<Mix>
data class MixOutputDto(
val id: Long,
val location: String?,
val mixType: MixType,
val mixMaterials: Set<MixMaterialDto>
)
data class MixDeductDto(
val id: Long,
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
val ratio: Float
)
data class MixLocationDto(
val mixId: Long,
val location: String?
)
//fun Mix.toOutput() =
// ==== DSL ==== // ==== DSL ====
fun mix( fun mix(
id: Long? = null, id: Long? = null,
@ -88,59 +39,12 @@ fun mix(
op: Mix.() -> Unit = {} op: Mix.() -> Unit = {}
) = Mix(id, location, recipe, mixType, mixMaterials).apply(op) ) = Mix(id, location, recipe, mixType, mixMaterials).apply(op)
fun mixSaveDto( @Deprecated("Temporary DSL for transition")
name: String = "name", fun mix(
recipeId: Long = 0L, dto: MixDto
materialTypeId: Long = 0L, ) = Mix(dto.id, dto.location, dto.recipe, mixType(dto.mixType), dto.mixMaterials.map(::mixMaterial).toSet())
mixMaterials: Set<MixMaterialDto>? = setOf(),
op: MixSaveDto.() -> Unit = {}
) = MixSaveDto(name, recipeId, materialTypeId, mixMaterials).apply(op)
fun mixUpdateDto( @Deprecated("Temporary DSL for transition")
id: Long = 0L, fun mix(
name: String? = "name", entity: Mix
materialTypeId: Long? = 0L, ) = MixDto(entity.id!!, entity.location, entity.recipe, mixTypeDto(entity.mixType), entity.mixMaterials.map(::mixMaterialDto).toSet())
mixMaterials: Set<MixMaterialDto>? = setOf(),
op: MixUpdateDto.() -> Unit = {}
) = MixUpdateDto(id, name, materialTypeId, mixMaterials).apply(op)
fun mixRatio(
id: Long = 0L,
ratio: Float = 1f,
op: MixDeductDto.() -> Unit = {}
) = MixDeductDto(id, ratio).apply(op)
fun mixLocationDto(
mixId: Long = 0L,
location: String? = "location",
op: MixLocationDto.() -> Unit = {}
) = MixLocationDto(mixId, location).apply(op)
// ==== Exceptions ====
private const val MIX_NOT_FOUND_EXCEPTION_TITLE = "Mix not found"
private const val MIX_ALREADY_EXISTS_EXCEPTION_TITLE = "Mix already exists"
private const val MIX_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete mix"
private const val MIX_EXCEPTION_ERROR_CODE = "mix"
fun mixIdNotFoundException(id: Long) =
NotFoundException(
MIX_EXCEPTION_ERROR_CODE,
MIX_NOT_FOUND_EXCEPTION_TITLE,
"A mix with the id $id could not be found",
id
)
fun mixIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
MIX_EXCEPTION_ERROR_CODE,
MIX_ALREADY_EXISTS_EXCEPTION_TITLE,
"A mix with the id $id already exists",
id
)
fun cannotDeleteMixException(mix: Mix) =
CannotDeleteException(
MIX_EXCEPTION_ERROR_CODE,
MIX_CANNOT_DELETE_EXCEPTION_TITLE,
"Cannot delete the mix ${mix.mixType.name} because one or more mixes depends on it"
)

View File

@ -2,6 +2,8 @@ package dev.fyloz.colorrecipesexplorer.model
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.dtos.MixDto
import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.model.account.Group
@ -150,7 +152,7 @@ data class RecipeOutputDto(
val approbationExpired: Boolean?, val approbationExpired: Boolean?,
val remark: String?, val remark: String?,
val company: Company, val company: Company,
val mixes: Set<MixOutputDto>, val mixes: Set<MixDto>,
val groupsInformation: Set<RecipeGroupInformation>, val groupsInformation: Set<RecipeGroupInformation>,
var imagesUrls: Set<String> var imagesUrls: Set<String>
) : ModelEntity ) : ModelEntity

View File

@ -7,21 +7,11 @@ import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query import org.springframework.data.jpa.repository.Query
interface MixRepository : JpaRepository<Mix, Long> { interface MixRepository : JpaRepository<Mix, Long> {
/** Finds all mixes with the given [mixType]. */ /** Finds all mixes with the mix type with the given [mixTypeId]. */
fun findAllByMixType(mixType: MixType): Collection<Mix> fun findAllByMixTypeId(mixTypeId: Long): Collection<Mix>
/** Updates the [location] of the [Mix] with the given [id]. */ /** Updates the [location] of the [Mix] with the given [id]. */
@Modifying @Modifying
@Query("UPDATE Mix m SET m.location = :location WHERE m.id = :id") @Query("update Mix m set m.location = :location where m.id = :id")
fun updateLocationById(id: Long, location: String?) fun updateLocationById(id: Long, location: String?)
@Query(
"""
select case when(count(mm.id) > 0) then false else true end
from Mix m
left join MixMaterial mm on m.mixType.material.id = mm.material.id
where m.id = :id
"""
)
fun canBeDeleted(id: Long): Boolean
} }

View File

@ -1,6 +1,5 @@
package dev.fyloz.colorrecipesexplorer.repository package dev.fyloz.colorrecipesexplorer.repository
import dev.fyloz.colorrecipesexplorer.model.MaterialType
import dev.fyloz.colorrecipesexplorer.model.MixType import dev.fyloz.colorrecipesexplorer.model.MixType
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query import org.springframework.data.jpa.repository.Query
@ -24,4 +23,13 @@ interface MixTypeRepository : JpaRepository<MixType, Long> {
""" """
) )
fun isUsedByMixes(id: Long): Boolean fun isUsedByMixes(id: Long): Boolean
/** Checks if the mix type with the given [id] is used by more than one mix. */
@Query(
"""
select case when(count(m.id) > 1) then false else true end
from Mix m where m.mixType.id = :id
"""
)
fun isShared(id: Long): Boolean
} }

View File

@ -1,8 +1,8 @@
package dev.fyloz.colorrecipesexplorer.rest package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.dtos.MaterialQuantityDto
import dev.fyloz.colorrecipesexplorer.dtos.MixDeductDto
import dev.fyloz.colorrecipesexplorer.logic.InventoryLogic import dev.fyloz.colorrecipesexplorer.logic.InventoryLogic
import dev.fyloz.colorrecipesexplorer.model.MaterialQuantityDto
import dev.fyloz.colorrecipesexplorer.model.MixDeductDto
import org.springframework.context.annotation.Profile import org.springframework.context.annotation.Profile
import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.PutMapping

View File

@ -0,0 +1,42 @@
package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditRecipes
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewRecipes
import dev.fyloz.colorrecipesexplorer.dtos.MixDto
import dev.fyloz.colorrecipesexplorer.dtos.MixSaveDto
import dev.fyloz.colorrecipesexplorer.logic.MixLogic
import org.springframework.context.annotation.Profile
import org.springframework.web.bind.annotation.*
import javax.validation.Valid
@RestController
@RequestMapping(Constants.ControllerPaths.MIX)
@Profile("!emergency")
@PreAuthorizeViewRecipes
class MixController(private val mixLogic: MixLogic) {
@GetMapping("{id}")
fun getById(@PathVariable id: Long) =
ok(mixLogic.getById(id))
@PostMapping
@PreAuthorizeEditRecipes
fun save(@Valid @RequestBody mix: MixSaveDto) =
created<MixDto>(Constants.ControllerPaths.MIX) {
mixLogic.save(mix)
}
@PutMapping
@PreAuthorizeEditRecipes
fun update(@Valid @RequestBody mix: MixSaveDto) =
noContent {
mixLogic.update(mix)
}
@DeleteMapping("{id}")
@PreAuthorizeEditRecipes
fun deleteById(@PathVariable id: Long) =
noContent {
mixLogic.deleteById(id)
}
}

View File

@ -16,7 +16,6 @@ import javax.validation.Valid
private const val RECIPE_CONTROLLER_PATH = "api/recipe" private const val RECIPE_CONTROLLER_PATH = "api/recipe"
private const val MIX_CONTROLLER_PATH = "api/recipe/mix"
@RestController @RestController
@RequestMapping(RECIPE_CONTROLLER_PATH) @RequestMapping(RECIPE_CONTROLLER_PATH)
@ -83,34 +82,3 @@ class RecipeController(
recipeImageLogic.delete(recipeLogic.getById(recipeId), name) recipeImageLogic.delete(recipeLogic.getById(recipeId), name)
} }
} }
@RestController
@RequestMapping(MIX_CONTROLLER_PATH)
@Profile("!emergency")
@PreAuthorizeViewRecipes
class MixController(private val mixLogic: MixLogic) {
@GetMapping("{id}")
fun getById(@PathVariable id: Long) =
ok(mixLogic.getByIdForOutput(id))
@PostMapping
@PreAuthorizeEditRecipes
fun save(@Valid @RequestBody mix: MixSaveDto) =
created<Mix>(MIX_CONTROLLER_PATH) {
mixLogic.save(mix)
}
@PutMapping
@PreAuthorizeEditRecipes
fun update(@Valid @RequestBody mix: MixUpdateDto) =
noContent {
mixLogic.update(mix)
}
@DeleteMapping("{id}")
@PreAuthorizeEditRecipes
fun deleteById(@PathVariable id: Long) =
noContent {
mixLogic.deleteById(id)
}
}

View File

@ -0,0 +1,42 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent
import dev.fyloz.colorrecipesexplorer.dtos.MixDto
import dev.fyloz.colorrecipesexplorer.model.Mix
import dev.fyloz.colorrecipesexplorer.repository.MixRepository
interface MixService : Service<MixDto, Mix, MixRepository> {
/** Gets all mixes with the mix type with the given [mixTypeId]. */
fun getAllByMixTypeId(mixTypeId: Long): Collection<MixDto>
/** Updates the [location] of the mix with the given [id]. */
fun updateLocationById(id: Long, location: String?)
}
@ServiceComponent
class DefaultMixService(
repository: MixRepository,
private val mixTypeService: MixTypeService,
private val mixMaterialService: MixMaterialService
) : BaseService<MixDto, Mix, MixRepository>(repository), MixService {
override fun getAllByMixTypeId(mixTypeId: Long) = repository.findAllByMixTypeId(mixTypeId).map(::toDto)
override fun updateLocationById(id: Long, location: String?) = repository.updateLocationById(id, location)
override fun toDto(entity: Mix) =
MixDto(
entity.id!!,
entity.location,
entity.recipe,
mixTypeService.toDto(entity.mixType),
entity.mixMaterials.map(mixMaterialService::toDto).toSet()
)
override fun toEntity(dto: MixDto) =
Mix(
dto.id,
dto.location,
dto.recipe,
mixTypeService.toEntity(dto.mixType),
dto.mixMaterials.map(mixMaterialService::toEntity).toSet()
)
}

View File

@ -14,6 +14,9 @@ interface MixTypeService : Service<MixTypeDto, MixType, MixTypeRepository> {
/** Checks if a mix depends on the mix type with the given [id]. */ /** Checks if a mix depends on the mix type with the given [id]. */
fun isUsedByMixes(id: Long): Boolean fun isUsedByMixes(id: Long): Boolean
/** Checks if the mix type with the given [id] is used by more than one mix. */
fun isShared(id: Long): Boolean
} }
@ServiceComponent @ServiceComponent
@ -25,8 +28,8 @@ class DefaultMixTypeService(repository: MixTypeRepository, val materialService:
override fun getByNameAndMaterialType(name: String, materialTypeId: Long) = override fun getByNameAndMaterialType(name: String, materialTypeId: Long) =
repository.findByNameAndMaterialType(name, materialTypeId)?.let(::toDto) repository.findByNameAndMaterialType(name, materialTypeId)?.let(::toDto)
override fun isUsedByMixes(id: Long) = override fun isUsedByMixes(id: Long) = repository.isUsedByMixes(id)
repository.isUsedByMixes(id) override fun isShared(id: Long) = repository.isShared(id)
override fun toDto(entity: MixType) = override fun toDto(entity: MixType) =
MixTypeDto(entity.id!!, entity.name, materialService.toDto(entity.material)) MixTypeDto(entity.id!!, entity.name, materialService.toDto(entity.material))

View File

@ -1,6 +1,6 @@
package dev.fyloz.colorrecipesexplorer.utils package dev.fyloz.colorrecipesexplorer.utils
import dev.fyloz.colorrecipesexplorer.model.ModelEntity import dev.fyloz.colorrecipesexplorer.dtos.EntityDto
/** 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]. */ /** 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( inline fun <T, R, reified E : Throwable> Iterable<T>.mapMayThrow(
@ -19,26 +19,12 @@ inline fun <T, R, reified E : Throwable> Iterable<T>.mapMayThrow(
} }
} }
/** Find duplicated keys in the given [Iterable], using keys obtained from the given [keySelector]. */
inline fun <T, K> Iterable<T>.findDuplicated(keySelector: (T) -> K) =
this.groupBy(keySelector)
.filter { it.value.count() > 1 }
.map { it.key }
/** Find duplicated elements in the given [Iterable]. */ /** Find duplicated elements in the given [Iterable]. */
fun <T> Iterable<T>.findDuplicated() = fun <T> Iterable<T>.findDuplicated() =
this.groupBy { it } this.groupBy { it }
.filter { it.value.count() > 1 } .filter { it.value.count() > 1 }
.map { it.key } .map { it.key }
/** Check if the given [Iterable] has gaps between each element, using keys obtained from the given [keySelector]. */
inline fun <T> Iterable<T>.hasGaps(keySelector: (T) -> Int) =
this.map(keySelector)
.toIntArray()
.sorted()
.filterIndexed { index, it -> it != index + 1 }
.isNotEmpty()
/** Check if the given [Int] [Iterable] has gaps between each element. */ /** Check if the given [Int] [Iterable] has gaps between each element. */
fun Iterable<Int>.hasGaps() = fun Iterable<Int>.hasGaps() =
this.sorted() this.sorted()
@ -58,8 +44,20 @@ inline fun <T> MutableCollection<T>.excludeAll(predicate: (T) -> Boolean): Itera
return matching return matching
} }
/** Merge to [ModelEntity] [Iterable]s and prevent id duplication. */ /**
fun <T : ModelEntity> Iterable<T>.merge(other: Iterable<T>) = * Merge two [EntityDto] [Iterable]s and prevent duplication of their ids.
this * In case of collision, the items from the [other] iterable will be taken.
.filter { model -> other.all { it.id != model.id } } */
@JvmName("mergeDto")
fun <T : EntityDto> Iterable<T>.merge(other: Iterable<T>) =
this.merge(other) { it.id }
/**
* Merge two [Iterable]s and prevent duplication of the keys determined by the given [keyMapper].
* In case of collision, the items from the [other] iterable will be taken.
*/
fun <T, K> Iterable<T>.merge(other: Iterable<T>, keyMapper: (T) -> K) =
this.associateBy { keyMapper(it) }
.filter { pair -> other.all { keyMapper(it) != pair.key } }
.map { it.value }
.plus(other) .plus(other)

View File

@ -0,0 +1 @@
spring.jpa.show-sql=true

View File

@ -0,0 +1,247 @@
package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.dtos.*
import dev.fyloz.colorrecipesexplorer.model.Recipe
import dev.fyloz.colorrecipesexplorer.model.company
import io.mockk.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals
class DefaultInventoryLogicTest {
private val materialLogicMock = mockk<MaterialLogic>()
private val mixLogicMock = mockk<MixLogic>()
private val inventoryLogic = spyk(DefaultInventoryLogic(materialLogicMock, mixLogicMock))
private val defaultInventoryQuantity = 5000f
private val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false)
private val material = MaterialDto(1L, "Unit test material", defaultInventoryQuantity, false, materialType)
@AfterEach
internal fun afterEach() {
clearAllMocks()
}
@Test
fun add_collection_normalBehavior_callsAddSingleForEachQuantity() {
// Arrange
val quantities = setOf(
MaterialQuantityDto(1L, 1000f),
MaterialQuantityDto(2L, 2000f)
)
val expectedQuantities = quantities.map { it.copy(quantity = it.quantity + defaultInventoryQuantity) }
every { inventoryLogic.add(any<MaterialQuantityDto>()) } answers { expectedQuantities[this.nArgs].quantity }
// Act
inventoryLogic.add(quantities)
// Assert
verify {
quantities.forEach {
inventoryLogic.add(it)
}
}
}
@Test
fun add_collection_normalBehavior_returnsFromAddSingle() {
// Arrange
val quantities = setOf(
MaterialQuantityDto(1L, 1000f),
MaterialQuantityDto(2L, 2000f)
)
val expectedQuantities = quantities.map { it.copy(quantity = it.quantity + defaultInventoryQuantity) }
every { inventoryLogic.add(any<MaterialQuantityDto>()) } returnsMany (expectedQuantities.map { it.quantity })
// Act
val actualQuantities = inventoryLogic.add(quantities)
// Assert
assertEquals(expectedQuantities, actualQuantities)
}
@Test
fun add_single_normalBehavior_callsUpdateQuantityInMaterialLogic() {
// Arrange
every { materialLogicMock.getById(any()) } returns material
every { materialLogicMock.updateQuantity(any(), any()) } answers {
this.secondArg<Float>() + defaultInventoryQuantity
}
val quantity = MaterialQuantityDto(1L, 1000f)
// Act
inventoryLogic.add(quantity)
// Assert
verify {
materialLogicMock.updateQuantity(material, quantity.quantity)
}
}
@Test
fun deductMix_normalBehavior_callsDeductWithMixMaterials() {
// Arrange
val company = CompanyDto(1L, "Unit test company")
val recipe = Recipe(
1L,
"Unit test recipe",
"Unit test recipe",
"FFFFFF",
0xf,
null,
null,
"Remark",
company(company),
mutableListOf(),
setOf()
)
val mixType = MixTypeDto(1L, "Unit test mix type", material)
val mixMaterial = MixMaterialDto(1L, material, 1000f, 1)
val mix = MixDto(1L, null, recipe, mixType, setOf(mixMaterial))
val dto = MixDeductDto(mix.id, 2f)
val expectedQuantities = listOf(MaterialQuantityDto(material.id, mixMaterial.quantity * dto.ratio))
every { mixLogicMock.getById(any()) } returns mix
every { inventoryLogic.deduct(any<Collection<MaterialQuantityDto>>()) } returns listOf()
// Act
inventoryLogic.deductMix(dto)
// Assert
verify {
inventoryLogic.deduct(expectedQuantities)
}
}
@Test
fun deductMix_normalBehavior_returnsFromDeduct() {
// Arrange
val company = CompanyDto(1L, "Unit test company")
val recipe = Recipe(
1L,
"Unit test recipe",
"Unit test recipe",
"FFFFFF",
0xf,
null,
null,
"Remark",
company(company),
mutableListOf(),
setOf()
)
val mixType = MixTypeDto(1L, "Unit test mix type", material)
val mixMaterial = MixMaterialDto(1L, material, 1000f, 1)
val mix = MixDto(1L, null, recipe, mixType, setOf(mixMaterial))
val dto = MixDeductDto(mix.id, 2f)
val expectedQuantities = listOf(MaterialQuantityDto(material.id, mixMaterial.quantity * dto.ratio))
every { mixLogicMock.getById(any()) } returns mix
every { inventoryLogic.deduct(any<Collection<MaterialQuantityDto>>()) } returns expectedQuantities
// Act
val actualQuantities = inventoryLogic.deductMix(dto)
// Assert
assertEquals(expectedQuantities, actualQuantities)
}
@Test
fun deduct_collection_normalBehavior_callsDeductForEachQuantity() {
// Arrange
val quantities = setOf(
MaterialQuantityDto(1L, 1000f),
MaterialQuantityDto(2L, 2000f)
)
every { inventoryLogic.deduct(any<MaterialQuantityDto>()) } answers {
defaultInventoryQuantity - firstArg<MaterialQuantityDto>().quantity
}
// Act
inventoryLogic.deduct(quantities)
// Assert
verify {
quantities.forEach {
inventoryLogic.deduct(it)
}
}
}
@Test
fun deduct_collection_normalBehavior_returnsFromDeduct() {
// Arrange
val quantities = setOf(
MaterialQuantityDto(1L, 1000f),
MaterialQuantityDto(2L, 2000f)
)
every { inventoryLogic.deduct(any<MaterialQuantityDto>()) } answers {
defaultInventoryQuantity - firstArg<MaterialQuantityDto>().quantity
}
val expectedQuantities =
quantities.map { MaterialQuantityDto(it.materialId, defaultInventoryQuantity - it.quantity) }
// Act
val actualQuantities = inventoryLogic.deduct(quantities)
// Assert
assertEquals(expectedQuantities, actualQuantities)
}
@Test
fun deduct_collection_notEnoughInventory_throwsMultiplesNotEnoughInventoryException() {
// Arrange
val quantities = setOf(
MaterialQuantityDto(1L, 1000f),
MaterialQuantityDto(2L, 2000f)
)
every { inventoryLogic.deduct(any<MaterialQuantityDto>()) } throws NotEnoughInventoryException(1000f, material)
// Act
// Assert
assertThrows<MultiplesNotEnoughInventoryException> { inventoryLogic.deduct(quantities) }
}
@Test
fun deduct_single_normalBehavior_callsUpdateQuantityInMaterialLogic() {
// Arrange
val quantity = MaterialQuantityDto(material.id, 1000f)
every { materialLogicMock.getById(any()) } returns material
every { materialLogicMock.updateQuantity(any(), any()) } returns defaultInventoryQuantity - quantity.quantity
// Act
inventoryLogic.deduct(quantity)
// Assert
verify {
materialLogicMock.getById(quantity.materialId)
materialLogicMock.updateQuantity(material, -quantity.quantity)
}
confirmVerified(materialLogicMock)
}
@Test
fun deduct_single_notEnoughInventory_throwsNotEnoughInventoryException() {
// Arrange
val quantity = MaterialQuantityDto(material.id, material.inventoryQuantity + 1000f)
every { materialLogicMock.getById(any()) } returns material
every { materialLogicMock.updateQuantity(any(), any()) } returns defaultInventoryQuantity - quantity.quantity
// Act
// Assert
assertThrows<NotEnoughInventoryException> { inventoryLogic.deduct(quantity) }
}
}

View File

@ -1,12 +1,13 @@
package dev.fyloz.colorrecipesexplorer.logic package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto import dev.fyloz.colorrecipesexplorer.dtos.*
import dev.fyloz.colorrecipesexplorer.dtos.MaterialSaveDto
import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic
import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.Company
import dev.fyloz.colorrecipesexplorer.model.Material
import dev.fyloz.colorrecipesexplorer.model.Recipe
import dev.fyloz.colorrecipesexplorer.model.mix
import dev.fyloz.colorrecipesexplorer.service.MaterialService import dev.fyloz.colorrecipesexplorer.service.MaterialService
import io.mockk.* import io.mockk.*
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
@ -50,10 +51,10 @@ class DefaultMaterialLogicTest {
mutableListOf(), mutableListOf(),
setOf() setOf()
) )
private val mix = Mix( private val mix = MixDto(
1L, "location", recipe, mixType = MixType(1L, "Unit test mix type", material(materialMixType)), mutableSetOf() 1L, "location", recipe, mixType = MixTypeDto(1L, "Unit test mix type", materialMixType), mutableSetOf()
) )
private val mix2 = mix.copy(id = 2L, mixType = mix.mixType.copy(id = 2L, material = material(materialMixType2))) private val mix2 = mix.copy(id = 2L, mixType = mix.mixType.copy(id = 2L, material = materialMixType2))
private val simdutFileMock = MockMultipartFile( private val simdutFileMock = MockMultipartFile(
"Unit test SIMDUT", "Unit test SIMDUT",
@ -62,7 +63,7 @@ class DefaultMaterialLogicTest {
private val materialSaveDto = MaterialSaveDto(1L, "Unit test material", 1000f, materialType.id, simdutFileMock) private val materialSaveDto = MaterialSaveDto(1L, "Unit test material", 1000f, materialType.id, simdutFileMock)
init { init {
recipe.mixes.addAll(listOf(mix, mix2)) recipe.mixes.addAll(listOf(mix(mix), mix(mix2)))
} }
@AfterEach @AfterEach
@ -139,7 +140,7 @@ class DefaultMaterialLogicTest {
every { mixLogicMock.getById(any()) } returns mix every { mixLogicMock.getById(any()) } returns mix
// Act // Act
val materials = materialLogic.getAllForMixUpdate(mix.id!!) val materials = materialLogic.getAllForMixUpdate(mix.id)
// Assert // Assert
assertContains(materials, material) assertContains(materials, material)
@ -152,7 +153,7 @@ class DefaultMaterialLogicTest {
every { mixLogicMock.getById(any()) } returns mix every { mixLogicMock.getById(any()) } returns mix
// Act // Act
val materials = materialLogic.getAllForMixUpdate(mix.id!!) val materials = materialLogic.getAllForMixUpdate(mix.id)
// Assert // Assert
assertContains(materials, materialMixType2) assertContains(materials, materialMixType2)

View File

@ -0,0 +1,180 @@
package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.dtos.*
import dev.fyloz.colorrecipesexplorer.model.Company
import dev.fyloz.colorrecipesexplorer.model.Recipe
import dev.fyloz.colorrecipesexplorer.service.MixService
import io.mockk.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
class DefaultMixLogicTest {
private val mixServiceMock = mockk<MixService>()
private val recipeLogicMock = mockk<RecipeLogic>()
private val materialTypeLogicMock = mockk<MaterialTypeLogic>()
private val mixTypeLogicMock = mockk<MixTypeLogic>()
private val mixMaterialLogicMock = mockk<MixMaterialLogic>()
private val mixLogic = spyk(
DefaultMixLogic(
mixServiceMock,
recipeLogicMock,
materialTypeLogicMock,
mixTypeLogicMock,
mixMaterialLogicMock
)
)
private val company = Company(1L, "Unit test company")
private val recipe = Recipe(
1L,
"Unit test recipe",
"Unit test recipe",
"FFFFFF",
0xf,
null,
null,
"A remark",
company,
mutableListOf(),
setOf()
)
private val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false)
private val mixType =
MixTypeDto(1L, "Unit test mix type", MaterialDto(1L, "Unit test mix type material", 1000f, true, materialType))
private val mixMaterial =
MixMaterialDto(1L, MaterialDto(2L, "Unit test material", 1000f, false, materialType), 50f, 1)
private val mix = MixDto(recipe = recipe, mixType = mixType, mixMaterials = setOf(mixMaterial))
@AfterEach
internal fun afterEach() {
clearAllMocks()
}
private fun setup_save_normalBehavior() {
every { recipeLogicMock.getById(any()) } returns recipe
every { materialTypeLogicMock.getById(any()) } returns materialType
every { mixTypeLogicMock.getOrCreateForNameAndMaterialType(any(), any()) } returns mixType
every { mixMaterialLogicMock.validateAndSaveAll(any()) } returns listOf(mixMaterial)
every { recipeLogicMock.addMix(any(), any()) } returns recipe
every { mixLogic.save(any<MixDto>()) } returnsArgument 0
}
private fun setup_update_normalBehavior() {
every { recipeLogicMock.getById(any()) } returns recipe
every { materialTypeLogicMock.getById(any()) } returns materialType
every { mixTypeLogicMock.updateOrCreateForNameAndMaterialType(any(), any(), any()) } returns mixType
every { mixMaterialLogicMock.validateAndSaveAll(any()) } returns listOf(mixMaterial)
every { mixLogic.getById(any()) } returns mix
every { mixLogic.update(any<MixDto>()) } returnsArgument 0
}
@Test
fun save_dto_normalBehavior_callsSave() {
// Arrange
setup_save_normalBehavior()
val mixMaterialDto =
MixMaterialSaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position)
val saveDto = MixSaveDto(0L, mixType.name, recipe.id!!, materialType.id, setOf(mixMaterialDto))
// Act
mixLogic.save(saveDto)
// Assert
verify {
mixLogic.save(mix)
}
}
@Test
fun save_dto_normalBehavior_callsValidateAndSaveAllInMixMaterialLogic() {
// Arrange
setup_save_normalBehavior()
val mixMaterialDtos =
setOf(
MixMaterialSaveDto(
mixMaterial.id,
mixMaterial.material.id,
mixMaterial.quantity,
mixMaterial.position
)
)
val saveDto = MixSaveDto(0L, mixType.name, recipe.id!!, materialType.id, mixMaterialDtos)
// Act
mixLogic.save(saveDto)
// Assert
verify {
mixMaterialLogicMock.validateAndSaveAll(mixMaterialDtos)
}
confirmVerified(mixMaterialLogicMock)
}
@Test
fun update_dto_normalBehavior_callsUpdate() {
// Arrange
setup_update_normalBehavior()
val mixMaterialDto =
MixMaterialSaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position)
val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id!!, materialType.id, setOf(mixMaterialDto))
// Act
mixLogic.update(saveDto)
// Assert
verify {
mixLogic.update(mix)
}
}
@Test
fun update_dto_normalBehavior_callsValidateAndSaveAllInMixMaterialLogic() {
// Arrange
setup_update_normalBehavior()
val mixMaterialDtos = setOf(
MixMaterialSaveDto(
mixMaterial.id,
mixMaterial.material.id,
mixMaterial.quantity,
mixMaterial.position
)
)
val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id!!, materialType.id, mixMaterialDtos)
// Act
mixLogic.update(saveDto)
// Assert
verify {
mixMaterialLogicMock.validateAndSaveAll(mixMaterialDtos)
}
confirmVerified(mixMaterialLogicMock)
}
@Test
fun updateLocations_normalBehavior_callsUpdateLocationByIdInServiceForEachUpdatedLocation() {
// Arrange
every { mixServiceMock.updateLocationById(any(), any()) } just runs
val updatedLocations = listOf(
MixLocationDto(1L, "A location"),
MixLocationDto(2L, "Another location")
)
// Act
mixLogic.updateLocations(updatedLocations)
// Assert
updatedLocations.forEach {
verify {
mixServiceMock.updateLocationById(it.mixId, it.location)
}
}
confirmVerified(mixServiceMock)
}
}

View File

@ -14,8 +14,9 @@ import org.junit.jupiter.api.assertThrows
class DefaultMixMaterialLogicTest { class DefaultMixMaterialLogicTest {
private val mixMaterialServiceMock = mockk<MixMaterialService>() private val mixMaterialServiceMock = mockk<MixMaterialService>()
private val materialLogicMock = mockk<MaterialLogic>()
private val mixMaterialLogic = DefaultMixMaterialLogic(mixMaterialServiceMock) private val mixMaterialLogic = DefaultMixMaterialLogic(mixMaterialServiceMock, materialLogicMock)
@AfterEach @AfterEach
internal fun afterEach() { internal fun afterEach() {

View File

@ -9,6 +9,7 @@ import io.mockk.*
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import kotlin.math.exp
import kotlin.test.assertEquals import kotlin.test.assertEquals
class DefaultMixTypeLogicTest { class DefaultMixTypeLogicTest {
@ -39,84 +40,105 @@ class DefaultMixTypeLogicTest {
} }
@Test @Test
fun getOrCreateForNameAndMaterialType_notFound_returnsFromSaveForNameAndMaterialType() { fun getOrCreateForNameAndMaterialType_notFound_callsSave() {
// Arrange // Arrange
every { mixTypeServiceMock.getByNameAndMaterialType(any(), any()) } returns null every { mixTypeServiceMock.getByNameAndMaterialType(any(), any()) } returns null
every { mixTypeLogic.saveForNameAndMaterialType(any(), any()) } returns mixType every { materialLogicMock.save(any<MaterialDto>()) } returns material
every { mixTypeLogic.save(any()) } returnsArgument 0
val expectedMixType = mixType.copy(id = 0L)
// Act
mixTypeLogic.getOrCreateForNameAndMaterialType(mixType.name, materialType)
// Assert
verify {
mixTypeLogic.save(expectedMixType)
}
}
@Test
fun getOrCreateForNameAndMaterialType_notFound_returnsFromSave() {
// Arrange
every { mixTypeServiceMock.getByNameAndMaterialType(any(), any()) } returns null
every { materialLogicMock.save(any<MaterialDto>()) } returns material
every { mixTypeLogic.save(any()) } returnsArgument 0
val expectedMixType = mixType.copy(id = 0L)
// Act // Act
val actualMixType = mixTypeLogic.getOrCreateForNameAndMaterialType(mixType.name, materialType) val actualMixType = mixTypeLogic.getOrCreateForNameAndMaterialType(mixType.name, materialType)
// Assert // Assert
assertEquals(mixType, actualMixType) assertEquals(expectedMixType, actualMixType)
} }
@Test @Test
fun saveForNameAndMaterialType_normalBehavior_callsSavesInMaterialLogic() { fun updateOrCreateForNameAndMaterialType_mixTypeShared_callsSave() {
// Arrange // Arrange
every { materialLogicMock.save(any<MaterialDto>()) } returnsArgument 0 every { mixTypeServiceMock.isShared(any()) } returns true
every { materialLogicMock.save(any<MaterialDto>()) } returns material
every { mixTypeLogic.save(any()) } returnsArgument 0 every { mixTypeLogic.save(any()) } returnsArgument 0
val expectedMixType = mixType.copy(id = 0L, name = "${mixType.name} updated")
// Act // Act
mixTypeLogic.saveForNameAndMaterialType(mixType.name, materialType) mixTypeLogic.updateOrCreateForNameAndMaterialType(mixType, expectedMixType.name, materialType)
// Assert // Assert
verify { verify {
materialLogicMock.save(match<MaterialDto> { it.name == mixType.name && it.materialType == materialType }) mixTypeLogic.save(expectedMixType)
} }
confirmVerified(materialLogicMock)
} }
@Test @Test
fun saveForNameAndMaterialType_normalBehavior_callsSave() { fun updateOrCreateForNameAndMaterialType_mixTypeShared_returnsFromSave() {
// Arrange // Arrange
every { materialLogicMock.save(any<MaterialDto>()) } returnsArgument 0 every { mixTypeServiceMock.isShared(any()) } returns true
every { materialLogicMock.save(any<MaterialDto>()) } returns material
every { mixTypeLogic.save(any()) } returnsArgument 0 every { mixTypeLogic.save(any()) } returnsArgument 0
val expectedMixType = mixType.copy(id = 0L, name = "${mixType.name} updated")
// Act // Act
mixTypeLogic.saveForNameAndMaterialType(mixType.name, materialType) val actualMixType = mixTypeLogic.updateOrCreateForNameAndMaterialType(mixType, expectedMixType.name, materialType)
// Assert
assertEquals(expectedMixType, actualMixType)
}
@Test
fun updateOrCreateForNameAndMaterialType_mixTypeNotShared_callsUpdate() {
// Arrange
every { mixTypeServiceMock.isShared(any()) } returns false
every { materialLogicMock.update(any<MaterialDto>()) } returns material
every { mixTypeLogic.update(any()) } returnsArgument 0
val expectedMixType = mixType.copy(name = "${mixType.name} updated")
// Act
mixTypeLogic.updateOrCreateForNameAndMaterialType(mixType, expectedMixType.name, materialType)
// Assert // Assert
verify { verify {
mixTypeLogic.save(match { it.name == mixType.name && it.material.name == mixType.name && it.material.materialType == materialType }) mixTypeLogic.update(expectedMixType)
} }
} }
@Test @Test
fun updateForNameAndMaterialType_normalBehavior_callsSavesInMaterialLogic() { fun updateOrCreateForNameAndMaterialType_mixTypeNotShared_returnsFromUpdate() {
// Arrange // Arrange
val updatedName = mixType.name + " updated" every { mixTypeServiceMock.isShared(any()) } returns false
val updatedMaterialType = materialType.copy(id = 2L) every { materialLogicMock.update(any<MaterialDto>()) } returns material
every { materialLogicMock.update(any<MaterialDto>()) } returnsArgument 0
every { mixTypeLogic.update(any()) } returnsArgument 0 every { mixTypeLogic.update(any()) } returnsArgument 0
// Act val expectedMixType = mixType.copy(name = "${mixType.name} updated")
mixTypeLogic.updateForNameAndMaterialType(mixType, updatedName, updatedMaterialType)
// Assert
verify {
materialLogicMock.update(match<MaterialDto> { it.name == updatedName && it.materialType == updatedMaterialType })
}
confirmVerified(materialLogicMock)
}
@Test
fun updateForNameAndMaterialType_normalBehavior_callsSave() {
// Arrange
val updatedName = mixType.name + " updated"
val updatedMaterialType = materialType.copy(id = 2L)
every { materialLogicMock.update(any<MaterialDto>()) } returnsArgument 0
every { mixTypeLogic.update(any()) } returnsArgument 0
// Act // Act
mixTypeLogic.updateForNameAndMaterialType(mixType, updatedName, updatedMaterialType) val actualMixType = mixTypeLogic.updateOrCreateForNameAndMaterialType(mixType, expectedMixType.name, materialType)
// Assert // Assert
verify { assertEquals(expectedMixType, actualMixType)
mixTypeLogic.update(match { it.name == updatedName && it.material.name == updatedName && it.material.materialType == updatedMaterialType })
}
} }
@Test @Test

View File

@ -1,182 +0,0 @@
package dev.fyloz.colorrecipesexplorer.logic
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.model.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class InventoryLogicTest {
private val materialLogic: MaterialLogic = mock()
private val mixLogic: MixLogic = mock()
private val logic = spy(DefaultInventoryLogic(materialLogic, mixLogic))
@AfterEach
fun afterEach() {
reset(materialLogic, logic)
}
// add()
@Test
fun `add(materialQuantities) calls add() for each MaterialQuantityDto`() {
val materialQuantities = listOf(
materialQuantityDto(materialId = 1, quantity = 1234f),
materialQuantityDto(materialId = 2, quantity = 2345f),
materialQuantityDto(materialId = 3, quantity = 3456f),
materialQuantityDto(materialId = 4, quantity = 4567f)
)
val storedQuantity = 2000f
doAnswer { storedQuantity + (it.arguments[0] as MaterialQuantityDto).quantity }.whenever(logic)
.add(any<MaterialQuantityDto>())
val found = logic.add(materialQuantities)
materialQuantities.forEach {
verify(logic).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) {
val updatedQuantity = it + this.quantity
whenever(materialLogic.updateQuantity(any(), eq(this.quantity))).doReturn(updatedQuantity)
val found = logic.add(this)
verify(materialLogic).updateQuantity(
argThat { this.id == this@withGivenQuantities.material },
eq(this.quantity)
)
assertEquals(updatedQuantity, found)
}
}
// deductMix()
@Test
fun `deductMix() calls deduct() with a collection of MaterialQuantityDto with adjusted quantities`() {
val material = material(id = 0L, materialType = materialType(usePercentages = false))
val materialPercents = material(id = 1L, materialType = materialType(usePercentages = true))
val mixRatio = mixRatio(ratio = 1.5f)
val mix = mix(
id = mixRatio.id,
mixMaterials = mutableSetOf(
mixMaterial(id = 0L, material = material, quantity = 1000f, position = 0),
mixMaterial(id = 1L, material = materialPercents, quantity = 50f, position = 1)
)
)
val expectedQuantities = mapOf(
0L to 1500f,
1L to 750f
)
whenever(mixLogic.getById(mix.id!!)).doReturn(mix)
doAnswer {
(it.arguments[0] as Collection<MaterialQuantityDto>).map { materialQuantity ->
materialQuantityDto(materialId = materialQuantity.material, quantity = 0f)
}
}.whenever(logic).deduct(any<Collection<MaterialQuantityDto>>())
val found = logic.deductMix(mixRatio)
verify(logic).deduct(argThat<Collection<MaterialQuantityDto>> {
this.all { it.quantity == expectedQuantities[it.material] }
})
assertEquals(expectedQuantities.size, found.size)
}
// deduct()
@Test
fun `deduct(materialQuantities) calls deduct() for each MaterialQuantityDto`() {
val materialQuantities = listOf(
materialQuantityDto(materialId = 1, quantity = 1234f),
materialQuantityDto(materialId = 2, quantity = 2345f),
materialQuantityDto(materialId = 3, quantity = 3456f),
materialQuantityDto(materialId = 4, quantity = 4567f)
)
val storedQuantity = 5000f
doAnswer { storedQuantity - (it.arguments[0] as MaterialQuantityDto).quantity }.whenever(logic)
.deduct(any<MaterialQuantityDto>())
val found = logic.deduct(materialQuantities)
materialQuantities.forEach {
verify(logic).deduct(it)
assertTrue { found.any { updated -> updated.material == it.material && updated.quantity == storedQuantity - it.quantity } }
}
}
@Test
fun `deduct(materialQuantities) throws MultiplesNotEnoughInventoryException when there is not enough inventory of a given material`() {
val materialQuantities = listOf(
materialQuantityDto(materialId = 1, quantity = 1234f),
materialQuantityDto(materialId = 2, quantity = 2345f),
materialQuantityDto(materialId = 3, quantity = 3456f),
materialQuantityDto(materialId = 4, quantity = 4567f)
)
val inventoryQuantity = 3000f
materialQuantities.forEach {
withGivenQuantities(inventoryQuantity, it)
}
assertThrows<MultiplesNotEnoughInventoryException> { logic.deduct(materialQuantities) }
}
@Test
fun `deduct(materialQuantity) updates material's quantity`() {
withGivenQuantities(5000f, 1000f) {
val updatedQuantity = it - this.quantity
whenever(materialLogic.updateQuantity(any(), eq(-this.quantity))).doReturn(updatedQuantity)
val found = logic.deduct(this)
verify(materialLogic).updateQuantity(
argThat { this.id == this@withGivenQuantities.material },
eq(-this.quantity)
)
assertEquals(updatedQuantity, found)
}
}
@Test
fun `deduct(materialQuantity) throws NotEnoughInventoryException when there is not enough inventory of the given material`() {
withGivenQuantities(0f, 1000f) {
assertThrows<NotEnoughInventoryException> { logic.deduct(this) }
}
}
private fun withGivenQuantities(
stored: Float,
quantity: Float,
materialId: Long = 0L,
test: MaterialQuantityDto.(Float) -> Unit = {}
) {
val materialQuantity = materialQuantityDto(materialId = materialId, quantity = quantity)
withGivenQuantities(stored, materialQuantity, test)
}
private fun withGivenQuantities(
stored: Float,
materialQuantity: MaterialQuantityDto,
test: MaterialQuantityDto.(Float) -> Unit = {}
) {
val material = material(id = materialQuantity.material, inventoryQuantity = stored)
whenever(materialLogic.getById(material.id!!)).doReturn(materialDto(material))
materialQuantity.test(stored)
}
}

View File

@ -1,245 +0,0 @@
package dev.fyloz.colorrecipesexplorer.logic
//@TestInstance(TestInstance.Lifecycle.PER_CLASS)
//class MixLogicTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpdateDto, MixLogic, MixRepository>() {
// override val repository: MixRepository = mock()
// private val recipeService: RecipeLogic = mock()
// private val materialTypeService: MaterialTypeLogic = mock()
// private val mixMaterialService: MixMaterialLogic = mock()
// private val mixTypeService: MixTypeLogic = mock()
// override val logic: MixLogic =
// spy(DefaultMixLogic(repository, recipeService, materialTypeService, mixMaterialService, mixTypeService))
//
// override val entity: Mix = mix(id = 0L, location = "location")
// override val anotherEntity: Mix = mix(id = 1L)
// override val entitySaveDto: MixSaveDto = spy(mixSaveDto(mixMaterials = setOf<MixMaterialDto>()))
// override val entityUpdateDto: MixUpdateDto = spy(mixUpdateDto(id = entity.id!!))
//
// @AfterEach
// override fun afterEach() {
// super.afterEach()
// reset(recipeService, materialTypeService, mixMaterialService, mixTypeService)
// }
//
// // getAllByMixType()
//
//// @Test
//// fun `getAllByMixType() returns all mixes with the given mix type`() {
//// val mixType = mixType(id = 0L)
////
//// whenever(repository.findAllByMixType(mixType)).doReturn(entityList)
////
//// val found = logic.getAllByMixType(mixType)
////
//// assertEquals(entityList, found)
//// }
////
//// // save()
////
//// @Test
//// override fun `save(dto) calls and returns save() with the created entity`() {
//// val recipe = recipe(id = entitySaveDto.recipeId)
//// val materialType = materialType(id = entitySaveDto.materialTypeId)
//// val material = material(
//// name = entitySaveDto.name,
//// inventoryQuantity = Float.MIN_VALUE,
//// isMixType = true,
//// materialType = materialType
//// )
//// val mixType = mixType(name = entitySaveDto.name, material = material)
//// val mix = mix(recipe = recipe, mixType = mixType)
//// val mixWithId = mix(id = 0L, recipe = recipe, mixType = mixType)
//// val mixMaterials = setOf(mixMaterial(material = material(id = 1L), quantity = 1000f))
////
//// whenever(recipeService.getById(recipe.id!!)).doReturn(recipe)
//// whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialTypeDto(materialType))
//// whenever(mixMaterialService.create(entitySaveDto.mixMaterials!!)).doReturn(mixMaterials)
//// whenever(
//// mixTypeService.getOrCreateForNameAndMaterialType(
//// mixType.name,
//// mixType.material.materialType!!
//// )
//// ).doReturn(mixType)
//// doReturn(true).whenever(logic).existsById(mixWithId.id!!)
//// doReturn(mixWithId).whenever(logic).save(any<Mix>())
////
//// val found = logic.save(entitySaveDto)
////
//// verify(logic).save(argThat<Mix> { this.recipe == mix.recipe })
//// verify(recipeService).addMix(recipe, mixWithId)
////
//// // Verify if this method is called instead of the MixType's constructor, which does not check if the name is already taken by a material.
//// verify(mixTypeService).getOrCreateForNameAndMaterialType(mixType.name, mixType.material.materialType!!)
////
//// assertEquals(mixWithId, found)
//// }
////
//// // update()
////
//// private fun mixUpdateDtoTest(
//// scope: MixUpdateDtoTestScope = MixUpdateDtoTestScope(),
//// sharedMixType: Boolean = false,
//// op: MixUpdateDtoTestScope.() -> Unit
//// ) {
//// with(scope) {
//// doReturn(true).whenever(logic).existsById(mix.id!!)
//// doReturn(mix).whenever(logic).getById(mix.id!!)
//// doReturn(sharedMixType).whenever(logic).mixTypeIsShared(mix.mixType)
//// doAnswer { it.arguments[0] }.whenever(logic).update(any<Mix>())
////
//// if (mixUpdateDto.materialTypeId != null) {
//// whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialTypeDto(materialType))
//// }
////
//// op()
//// }
//// }
////
//// private fun mixUpdateDtoMixTypeTest(sharedMixType: Boolean = false, op: MixUpdateDtoTestScope.() -> Unit) {
//// with(MixUpdateDtoTestScope(mixUpdateDto = mixUpdateDto(id = 0L, name = "name", materialTypeId = 0L))) {
//// mixUpdateDtoTest(this, sharedMixType, op)
//// }
//// }
////
//// @Test
//// override fun `update(dto) calls and returns update() with the created entity`() {
//// val mixUpdateDto = spy(mixUpdateDto(id = 0L, name = null, materialTypeId = null))
////
//// doReturn(entity).whenever(logic).getById(any())
//// doReturn(entity).whenever(logic).update(entity)
////
//// val found = logic.update(mixUpdateDto)
////
//// verify(logic).update(entity)
////
//// assertEquals(entity, found)
//// }
////
//// @Test
//// fun `update(dto) calls MixTypeService saveForNameAndMaterialType() when mix type is shared`() {
//// mixUpdateDtoMixTypeTest(sharedMixType = true) {
//// whenever(mixTypeService.saveForNameAndMaterialType(mixUpdateDto.name!!, materialType))
//// .doReturn(newMixType)
////
//// val found = logic.update(mixUpdateDto)
////
//// verify(mixTypeService).saveForNameAndMaterialType(mixUpdateDto.name!!, materialType)
////
//// assertEquals(newMixType, found.mixType)
//// }
//// }
////
//// @Test
//// fun `update(dto) calls MixTypeService updateForNameAndMaterialType() when mix type is not shared`() {
//// mixUpdateDtoMixTypeTest {
//// whenever(mixTypeService.updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType))
//// .doReturn(newMixType)
////
//// val found = logic.update(mixUpdateDto)
////
//// verify(mixTypeService).updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType)
////
//// assertEquals(newMixType, found.mixType)
//// }
//// }
////
//// @Test
//// fun `update(dto) update, create and delete mix materials according to the given mix materials map`() {
//// mixUpdateDtoTest {
//// val mixMaterials = setOf(
//// mixMaterialDto(materialId = 0L, quantity = 100f, position = 0),
//// mixMaterialDto(materialId = 1L, quantity = 200f, position = 1),
//// mixMaterialDto(materialId = 2L, quantity = 300f, position = 2),
//// mixMaterialDto(materialId = 3L, quantity = 400f, position = 3),
//// )
//// mixUpdateDto.mixMaterials = mixMaterials
////
//// whenever(mixMaterialService.create(any<Set<MixMaterialDto>>())).doAnswer {
//// (it.arguments[0] as Set<MixMaterialDto>).map { dto ->
//// mixMaterial(
//// material = material(id = dto.materialId),
//// quantity = dto.quantity,
//// position = dto.position
//// )
//// }.toSet()
//// }
////
//// val found = logic.update(mixUpdateDto)
////
//// mixMaterials.forEach {
//// assertTrue {
//// found.mixMaterials.any { mixMaterial ->
//// mixMaterial.material.id == it.materialId && mixMaterial.quantity == it.quantity && mixMaterial.position == it.position
//// }
//// }
//// }
//// }
//// }
////
//// // updateLocations()
////
//// @Test
//// fun `updateLocations() calls updateLocation() for each given MixLocationDto`() {
//// val locations = setOf(
//// mixLocationDto(mixId = 0, location = "Loc 0"),
//// mixLocationDto(mixId = 1, location = "Loc 1"),
//// mixLocationDto(mixId = 2, location = "Loc 2"),
//// mixLocationDto(mixId = 3, location = "Loc 3")
//// )
////
//// logic.updateLocations(locations)
////
//// locations.forEach {
//// verify(logic).updateLocation(it)
//// }
//// }
////
//// // updateLocation()
////
//// @Test
//// fun `updateLocation() updates the location of a mix in the repository according to the given MixLocationDto`() {
//// val locationDto = mixLocationDto(mixId = 0L, location = "Location")
////
//// logic.updateLocation(locationDto)
////
//// verify(repository).updateLocationById(locationDto.mixId, locationDto.location)
//// }
////
//// // delete()
////
//// override fun `delete() deletes in the repository`() {
//// whenCanBeDeleted {
//// super.`delete() deletes in the repository`()
//// }
//// }
////
//// // deleteById()
////
//// @Test
//// override fun `deleteById() deletes the entity with the given id in the repository`() {
//// whenCanBeDeleted {
//// super.`deleteById() deletes the entity with the given id in the repository`()
//// }
//// }
////
//// private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) {
//// whenever(repository.canBeDeleted(id)).doReturn(true)
////
//// test()
//// }
//}
//
//data class MixUpdateDtoTestScope(
// val mixType: MixType = mixType(name = "mix type"),
// val newMixType: MixType = mixType(name = "another mix type"),
// val materialType: MaterialType = materialType(id = 0L),
// val mix: Mix = mix(id = 0L, mixType = mixType),
// val mixUpdateDto: MixUpdateDto = spy(
// mixUpdateDto(
// id = 0L,
// name = null,
// materialTypeId = null,
// mixMaterials = setOf()
// )
// )
//)

View File

@ -1,6 +1,7 @@
package dev.fyloz.colorrecipesexplorer.logic package dev.fyloz.colorrecipesexplorer.logic
import com.nhaarman.mockitokotlin2.* import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic
import dev.fyloz.colorrecipesexplorer.logic.files.CachedFile import dev.fyloz.colorrecipesexplorer.logic.files.CachedFile
@ -228,8 +229,8 @@ class RecipeLogicTest :
fun `updatePublicData() update the location of a recipe mixes in the mix logic according to the RecipePublicDataDto`() { fun `updatePublicData() update the location of a recipe mixes in the mix logic according to the RecipePublicDataDto`() {
val publicData = recipePublicDataDto( val publicData = recipePublicDataDto(
mixesLocation = setOf( mixesLocation = setOf(
mixLocationDto(mixId = 0L, location = "Loc 1"), MixLocationDto(mixId = 0L, location = "Loc 1"),
mixLocationDto(mixId = 1L, location = "Loc 2") MixLocationDto(mixId = 1L, location = "Loc 2")
) )
) )