From efac09a76bb079761a3c9a365973668e481a640a Mon Sep 17 00:00:00 2001 From: FyloZ Date: Sat, 19 Mar 2022 21:26:01 -0400 Subject: [PATCH] #25 Migrate mixes to new logic --- .../fyloz/colorrecipesexplorer/Constants.kt | 1 + .../config/initializers/MixInitializer.kt | 14 +- .../config/initializers/RecipeInitializer.kt | 4 +- .../colorrecipesexplorer/dtos/MaterialDto.kt | 7 + .../fyloz/colorrecipesexplorer/dtos/MixDto.kt | 46 ++++ .../logic/InventoryLogic.kt | 113 ++++---- .../colorrecipesexplorer/logic/MixLogic.kt | 124 ++++----- .../logic/MixMaterialLogic.kt | 22 +- .../logic/MixTypeLogic.kt | 51 ++-- .../colorrecipesexplorer/logic/RecipeLogic.kt | 6 +- .../colorrecipesexplorer/model/Material.kt | 21 +- .../fyloz/colorrecipesexplorer/model/Mix.kt | 116 +------- .../colorrecipesexplorer/model/Recipe.kt | 4 +- .../repository/MixRepository.kt | 16 +- .../repository/MixTypeRepository.kt | 10 +- .../rest/InventoryController.kt | 4 +- .../rest/MixController.kt | 42 +++ .../rest/RecipeController.kt | 32 --- .../service/MixService.kt | 42 +++ .../service/MixTypeService.kt | 7 +- .../colorrecipesexplorer/utils/Collections.kt | 36 ++- .../resources/application-debug.properties | 1 + .../logic/DefaultInventoryLogicTest.kt | 247 ++++++++++++++++++ .../logic/DefaultMaterialLogicTest.kt | 21 +- .../logic/DefaultMixLogicTest.kt | 180 +++++++++++++ .../logic/DefaultMixMaterialLogicTest.kt | 3 +- .../logic/DefaultMixTypeLogicTest.kt | 100 ++++--- .../logic/InventoryLogicTest.kt | 182 ------------- .../logic/MixLogicTest.kt | 245 ----------------- .../logic/RecipeLogicTest.kt | 5 +- 30 files changed, 861 insertions(+), 841 deletions(-) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MixController.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt create mode 100644 src/main/resources/application-debug.properties create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt index 18ff917..4cb51dc 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt @@ -5,6 +5,7 @@ object Constants { const val FILE = "/api/file" const val MATERIAL = "/api/material" const val MATERIAL_TYPE = "/api/materialtype" + const val MIX = "/api/recipe/mix" } object FilePaths { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt index 1781762..bc48f94 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt @@ -1,9 +1,9 @@ package dev.fyloz.colorrecipesexplorer.config.initializers 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.model.Mix -import dev.fyloz.colorrecipesexplorer.model.MixMaterial import dev.fyloz.colorrecipesexplorer.utils.merge import mu.KotlinLogging import org.springframework.context.annotation.Configuration @@ -31,12 +31,12 @@ class MixInitializer( logger.debug("Mix materials positions are valid!") } - private fun fixMixPositions(mix: Mix) { + private fun fixMixPositions(mix: MixDto) { val maxPosition = mix.mixMaterials.maxOf { it.position } logger.warn("Mix ${mix.id} (${mix.mixType.name}, ${mix.recipe.name}) has invalid positions:") - val invalidMixMaterials: Collection = with(mix.mixMaterials.filter { it.position == 0 }) { + val invalidMixMaterials: Collection = with(mix.mixMaterials.filter { it.position == 0 }) { if (maxPosition == 0 && this.size > 1) { orderMixMaterials(this) } else { @@ -52,16 +52,16 @@ class MixInitializer( } } - private fun increaseMixMaterialsPosition(mixMaterials: Iterable, firstPosition: Int) = + private fun increaseMixMaterialsPosition(mixMaterials: Iterable, firstPosition: Int) = mixMaterials .mapIndexed { index, mixMaterial -> mixMaterial.copy(position = firstPosition + index) } .onEach { logger.info("\tPosition of material ${it.material.id} (${it.material.name}) has been set to ${it.position}") } - private fun orderMixMaterials(mixMaterials: Collection) = + private fun orderMixMaterials(mixMaterials: Collection) = 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 val pop = this.pop() this.add(pop) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt index 1491d76..5890b57 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt @@ -36,7 +36,7 @@ class RecipeInitializer( .filter { groupInfo -> groupInfo.steps!!.any { it.position == 0 } } .map { fixGroupInformationPositions(recipe, it) } - val updatedGroupInformation = recipe.groupsInformation.merge(fixedGroupInformation) + val updatedGroupInformation = recipe.groupsInformation.merge(fixedGroupInformation) { it.id } with(recipe.copy(groupsInformation = updatedGroupInformation.toMutableSet())) { recipeLogic.update(this) @@ -54,7 +54,7 @@ class RecipeInitializer( val invalidRecipeSteps = steps.filter { it.position == 0 } 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()) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt index b5367bc..f45aa91 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MaterialDto.kt @@ -32,3 +32,10 @@ data class MaterialSaveDto( val simdutFile: MultipartFile? ) : EntityDto + +data class MaterialQuantityDto( + val materialId: Long, + + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) + val quantity: Float +) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt new file mode 100644 index 0000000..957a59f --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt @@ -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 +) : EntityDto + +data class MixSaveDto( + val id: Long = 0L, + + @field:NotBlank + val name: String, + + val recipeId: Long = 0L, + + val materialTypeId: Long, + + val mixMaterials: Set +) + +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? +) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt index f43f1e6..d938cde 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogic.kt @@ -2,8 +2,11 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase 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.model.* +import dev.fyloz.colorrecipesexplorer.model.Material import dev.fyloz.colorrecipesexplorer.utils.mapMayThrow import org.springframework.http.HttpStatus import org.springframework.stereotype.Service @@ -34,83 +37,87 @@ class DefaultInventoryLogic( ) : InventoryLogic { @Transactional override fun add(materialQuantities: Collection) = - materialQuantities.map { - materialQuantityDto(materialId = it.material, quantity = add(it)) - } + materialQuantities.map { MaterialQuantityDto(it.materialId, add(it)) } override fun add(materialQuantity: MaterialQuantityDto) = - materialLogic.updateQuantity( - materialLogic.getById(materialQuantity.material), - materialQuantity.quantity - ) + materialLogic.updateQuantity( + materialLogic.getById(materialQuantity.materialId), + materialQuantity.quantity + ) @Transactional override fun deductMix(mixRatio: MixDeductDto): Collection { val mix = mixLogic.getById(mixRatio.id) - val firstMixMaterial = mix.mixMaterials.first() - val adjustedFirstMaterialQuantity = firstMixMaterial.quantity * mixRatio.ratio - fun adjustQuantity(mixMaterial: MixMaterial): Float = - 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) - ) - }) + return deduct(getMaterialsWithAdjustedQuantities(mix.mixMaterials, mixRatio)) } @Transactional override fun deduct(materialQuantities: Collection): Collection { val thrown = mutableListOf() + val updatedQuantities = - materialQuantities.mapMayThrow( - { thrown.add(it) } - ) { - materialQuantityDto(materialId = it.material, quantity = deduct(it)) - } + materialQuantities.mapMayThrow( + { thrown.add(it) } + ) { + MaterialQuantityDto(it.materialId, deduct(it)) + } if (thrown.isNotEmpty()) { throw MultiplesNotEnoughInventoryException(thrown) } + return updatedQuantities } override fun deduct(materialQuantity: MaterialQuantityDto): Float = - with(materialLogic.getById(materialQuantity.material)) { - if (this.inventoryQuantity >= materialQuantity.quantity) { - materialLogic.updateQuantity(this, -materialQuantity.quantity) - } else { - throw NotEnoughInventoryException(materialQuantity.quantity, this) - } - } + with(materialLogic.getById(materialQuantity.materialId)) { + if (this.inventoryQuantity >= materialQuantity.quantity) { + materialLogic.updateQuantity(this, -materialQuantity.quantity) + } else { + throw NotEnoughInventoryException(materialQuantity.quantity, this) + } + } + + private fun getMaterialsWithAdjustedQuantities( + mixMaterials: Collection, + mixRatio: MixDeductDto + ): Collection { + 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) : - RestException( - "notenoughinventory", - "Not enough inventory", - HttpStatus.BAD_REQUEST, - "Cannot deduct ${quantity}mL of ${material.name} because there is only ${material.inventoryQuantity}mL in inventory", - mapOf( - "material" to material.name, - "materialId" to material.id.toString(), - "requestQuantity" to quantity, - "availableQuantity" to material.inventoryQuantity + RestException( + "notenoughinventory", + "Not enough inventory", + HttpStatus.BAD_REQUEST, + "Cannot deduct ${quantity}mL of ${material.name} because there is only ${material.inventoryQuantity}mL in inventory", + mapOf( + "material" to material.name, + "materialId" to material.id.toString(), + "requestQuantity" to quantity, + "availableQuantity" to material.inventoryQuantity + ) ) - ) class MultiplesNotEnoughInventoryException(exceptions: List) : - RestException( - "notenoughinventory-multiple", - "Not enough inventory", - HttpStatus.BAD_REQUEST, - "Cannot deduct requested quantities because there is no enough of them in inventory", - mapOf( - "lowQuantities" to exceptions.map { it.extensions } + RestException( + "notenoughinventory-multiple", + "Not enough inventory", + HttpStatus.BAD_REQUEST, + "Cannot deduct requested quantities because there is no enough of them in inventory", + mapOf( + "lowQuantities" to exceptions.map { it.extensions } + ) ) - ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt index 9de3733..f719422 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt @@ -1,102 +1,66 @@ package dev.fyloz.colorrecipesexplorer.logic -import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.repository.MixRepository -import dev.fyloz.colorrecipesexplorer.utils.setAll +import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent +import dev.fyloz.colorrecipesexplorer.dtos.MixDto +import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto +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.stereotype.Service -import javax.transaction.Transactional +import org.springframework.transaction.annotation.Transactional -interface MixLogic : ExternalModelService { - /** Gets all mixes with the given [mixType]. */ - fun getAllByMixType(mixType: MixType): Collection +interface MixLogic : Logic { + /** Saves the given [dto]. */ + fun save(dto: MixSaveDto): MixDto - /** Checks if a [MixType] is shared by several [Mix]es or not. */ - fun mixTypeIsShared(mixType: MixType): Boolean + /** Updates the given [dto]. */ + 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) - - /** Updates the location of a given [Mix] to the given [MixLocationDto]. */ - fun updateLocation(updatedLocation: MixLocationDto) } -@Service -@RequireDatabase +@LogicComponent class DefaultMixLogic( - mixRepository: MixRepository, - @Lazy val recipeLogic: RecipeLogic, - @Lazy val materialTypeLogic: MaterialTypeLogic, - val mixMaterialLogic: MixMaterialLogic, - val mixTypeLogic: MixTypeLogic -) : AbstractExternalModelService(mixRepository), - MixLogic { - override fun idNotFoundException(id: Long) = mixIdNotFoundException(id) - override fun idAlreadyExistsException(id: Long) = mixIdAlreadyExistsException(id) - - override fun getAllByMixType(mixType: MixType): Collection = 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() - ) - + service: MixService, + @Lazy private val recipeLogic: RecipeLogic, + @Lazy private val materialTypeLogic: MaterialTypeLogic, + private val mixTypeLogic: MixTypeLogic, + private val mixMaterialLogic: MixMaterialLogic +) : BaseLogic(service, Mix::class.simpleName!!), MixLogic { @Transactional - override fun save(entity: MixSaveDto): Mix { - val recipe = recipeLogic.getById(entity.recipeId) - val materialType = materialTypeLogic.getById(entity.materialTypeId) - val mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(entity.name, materialType) + override fun save(dto: MixSaveDto): MixDto { + val recipe = recipeLogic.getById(dto.recipeId) + val materialType = materialTypeLogic.getById(dto.materialTypeId) - val mixMaterials = - if (entity.mixMaterials != null) mixMaterialLogic.saveAll(entity.mixMaterials).toSet() else setOf() - mixMaterialLogic.validateMixMaterials(mixMaterials) + val mix = MixDto( + recipe = recipe, + 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()) - mix = save(mix) - - recipeLogic.addMix(recipe, mix) - - return mix + return save(mix) } @Transactional - override fun update(entity: MixUpdateDto): Mix { - val mix = getById(entity.id) - if (entity.name != null || entity.materialTypeId != null) { - val name = entity.name ?: mix.mixType.name - val materialType = if (entity.materialTypeId != null) - materialType(materialTypeLogic.getById(entity.materialTypeId)) - else - mix.mixType.material.materialType!! + override fun update(dto: MixSaveDto): MixDto { + val materialType = materialTypeLogic.getById(dto.materialTypeId) + val mix = getById(dto.id) - mix.mixType = if (mixTypeIsShared(mix.mixType)) { - mixType(mixTypeLogic.saveForNameAndMaterialType(name, materialTypeDto(materialType))) - } else { - mixType(mixTypeLogic.updateForNameAndMaterialType(mixTypeDto(mix.mixType), name, materialTypeDto(materialType))) - } - } - if (entity.mixMaterials != null) { - mix.mixMaterials.setAll(mixMaterialLogic.saveAll(entity.mixMaterials!!).map(::mixMaterial).toMutableSet()) - } - return update(mix) + return update( + MixDto( + id = dto.id, + recipe = recipeLogic.getById(dto.recipeId), + mixType = mixTypeLogic.updateOrCreateForNameAndMaterialType(mix.mixType, dto.name, materialType), + mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials).toSet() + ) + ) } - override fun updateLocations(updatedLocations: Collection) { + override fun updateLocations(updatedLocations: Collection) = updatedLocations.forEach(::updateLocation) - } - override fun updateLocation(updatedLocation: MixLocationDto) { - repository.updateLocationById(updatedLocation.mixId, updatedLocation.location) + private fun updateLocation(updatedLocation: MixLocationDto) { + 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) - } -} +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt index 2cedbe8..b6ddf38 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt @@ -2,12 +2,14 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto +import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialSaveDto import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.model.MixMaterial import dev.fyloz.colorrecipesexplorer.service.MixMaterialService import dev.fyloz.colorrecipesexplorer.utils.PositionUtils +import org.springframework.context.annotation.Lazy import org.springframework.http.HttpStatus interface MixMaterialLogic : Logic { @@ -17,10 +19,13 @@ interface MixMaterialLogic : Logic { * If any of those criteria are not met, an [InvalidGroupStepsPositionsException] will be thrown. */ fun validateMixMaterials(mixMaterials: Set) + + /** Validates the given mix materials [dtos] and save them. */ + fun validateAndSaveAll(dtos: Collection): Collection } @LogicComponent -class DefaultMixMaterialLogic(service: MixMaterialService) : +class DefaultMixMaterialLogic(service: MixMaterialService, @Lazy private val materialLogic: MaterialLogic) : BaseLogic(service, MixMaterial::class.simpleName!!), MixMaterialLogic { override fun validateMixMaterials(mixMaterials: Set) { if (mixMaterials.isEmpty()) return @@ -37,6 +42,21 @@ class DefaultMixMaterialLogic(service: MixMaterialService) : throw InvalidFirstMixMaterialException(sortedMixMaterials[0]) } } + + override fun validateAndSaveAll(dtos: Collection): Collection { + 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 diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt index 79eb5f9..3226392 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixTypeLogic.kt @@ -6,27 +6,50 @@ import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto import dev.fyloz.colorrecipesexplorer.model.MixType import dev.fyloz.colorrecipesexplorer.service.MixTypeService +import org.springframework.context.annotation.Lazy import org.springframework.transaction.annotation.Transactional interface MixTypeLogic : Logic { - /** 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 - /** Returns a new and persisted [MixType] with the given [name] and [materialType]. */ - fun saveForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto - - /** Returns the given [mixType] updated with the given [name] and [materialType]. */ - fun updateForNameAndMaterialType(mixType: MixTypeDto, name: String, materialType: MaterialTypeDto): MixTypeDto + /** Updates the [mixType] with the given [name] and [materialType], or create a new one if it is shared with other mixes. */ + fun updateOrCreateForNameAndMaterialType( + mixType: MixTypeDto, + name: String, + materialType: MaterialTypeDto + ): MixTypeDto } @LogicComponent -class DefaultMixTypeLogic(service: MixTypeService, private val materialLogic: MaterialLogic) : +class DefaultMixTypeLogic( + service: MixTypeService, + @Lazy private val materialLogic: MaterialLogic +) : BaseLogic(service, MixType::class.simpleName!!), MixTypeLogic { + @Transactional override fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialTypeDto) = service.getByNameAndMaterialType(name, materialType.id) ?: saveForNameAndMaterialType(name, materialType) - @Transactional - override fun saveForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto { + override fun updateOrCreateForNameAndMaterialType( + 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( MaterialDto( name = name, @@ -39,7 +62,7 @@ class DefaultMixTypeLogic(service: MixTypeService, private val materialLogic: Ma return save(MixTypeDto(name = name, material = material)) } - override fun updateForNameAndMaterialType( + private fun updateForNameAndMaterialType( mixType: MixTypeDto, name: String, 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)) 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) - } } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt index 775b64f..19315e3 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt @@ -72,11 +72,7 @@ class DefaultRecipeLogic( isApprobationExpired(this), this.remark, this.company, - this.mixes.map { - with(mixLogic) { - it.toOutput() - } - }.toSet(), + this.mixes.map { mix(it) }.toSet(), this.groupsInformation, recipeImageLogic.getAllImages(this) .map { this.imageUrl(configService.getContent(ConfigurationType.INSTANCE_URL), it) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt index 23fd00c..024e651 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt @@ -2,13 +2,7 @@ package dev.fyloz.colorrecipesexplorer.model import dev.fyloz.colorrecipesexplorer.Constants 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.validation.constraints.Min - -const val SIMDUT_FILES_PATH = "pdf/simdut" @Entity @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 === fun material( @@ -71,10 +58,4 @@ fun material( @Deprecated("Temporary DSL for transition") fun materialDto( entity: Material -) = 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) \ No newline at end of file +) = MaterialDto(entity.id!!, entity.name, entity.inventoryQuantity, entity.isMixType, materialTypeDto(entity.materialType!!)) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt index 2670501..6931638 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt @@ -1,15 +1,11 @@ package dev.fyloz.colorrecipesexplorer.model -import com.fasterxml.jackson.annotation.JsonIgnore -import dev.fyloz.colorrecipesexplorer.Constants +import dev.fyloz.colorrecipesexplorer.dtos.MixDto import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import javax.persistence.* -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank - @Entity @Table(name = "mix") @@ -20,7 +16,6 @@ data class Mix( var location: String?, - @JsonIgnore @ManyToOne @JoinColumn(name = "recipe_id") val recipe: Recipe, @@ -31,53 +26,9 @@ data class Mix( @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @JoinColumn(name = "mix_id") - var mixMaterials: MutableSet, + var mixMaterials: Set, ) : ModelEntity -open class MixSaveDto( - @field:NotBlank - val name: String, - - val recipeId: Long, - - val materialTypeId: Long, - - val mixMaterials: Set? -) : EntityDto - -open class MixUpdateDto( - val id: Long, - - @field:NotBlank - val name: String?, - - val materialTypeId: Long?, - - var mixMaterials: Set? -) : EntityDto - -data class MixOutputDto( - val id: Long, - val location: String?, - val mixType: MixType, - val mixMaterials: Set -) - -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 ==== fun mix( id: Long? = null, @@ -88,59 +39,12 @@ fun mix( op: Mix.() -> Unit = {} ) = Mix(id, location, recipe, mixType, mixMaterials).apply(op) -fun mixSaveDto( - name: String = "name", - recipeId: Long = 0L, - materialTypeId: Long = 0L, - mixMaterials: Set? = setOf(), - op: MixSaveDto.() -> Unit = {} -) = MixSaveDto(name, recipeId, materialTypeId, mixMaterials).apply(op) +@Deprecated("Temporary DSL for transition") +fun mix( + dto: MixDto +) = Mix(dto.id, dto.location, dto.recipe, mixType(dto.mixType), dto.mixMaterials.map(::mixMaterial).toSet()) -fun mixUpdateDto( - id: Long = 0L, - name: String? = "name", - materialTypeId: Long? = 0L, - mixMaterials: Set? = 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" - ) +@Deprecated("Temporary DSL for transition") +fun mix( + entity: Mix +) = MixDto(entity.id!!, entity.location, entity.recipe, mixTypeDto(entity.mixType), entity.mixMaterials.map(::mixMaterialDto).toSet()) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt index af8142c..3fc6873 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt @@ -2,6 +2,8 @@ package dev.fyloz.colorrecipesexplorer.model import com.fasterxml.jackson.annotation.JsonIgnore 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.NotFoundException import dev.fyloz.colorrecipesexplorer.model.account.Group @@ -150,7 +152,7 @@ data class RecipeOutputDto( val approbationExpired: Boolean?, val remark: String?, val company: Company, - val mixes: Set, + val mixes: Set, val groupsInformation: Set, var imagesUrls: Set ) : ModelEntity diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepository.kt index 51c39a9..a363199 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepository.kt @@ -7,21 +7,11 @@ import org.springframework.data.jpa.repository.Modifying import org.springframework.data.jpa.repository.Query interface MixRepository : JpaRepository { - /** Finds all mixes with the given [mixType]. */ - fun findAllByMixType(mixType: MixType): Collection + /** Finds all mixes with the mix type with the given [mixTypeId]. */ + fun findAllByMixTypeId(mixTypeId: Long): Collection /** Updates the [location] of the [Mix] with the given [id]. */ @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?) - - @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 } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt index ac9e643..9e1c7f8 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt @@ -1,6 +1,5 @@ package dev.fyloz.colorrecipesexplorer.repository -import dev.fyloz.colorrecipesexplorer.model.MaterialType import dev.fyloz.colorrecipesexplorer.model.MixType import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.Query @@ -24,4 +23,13 @@ interface MixTypeRepository : JpaRepository { """ ) 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 } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt index 47d0b96..5f5cbc5 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/InventoryController.kt @@ -1,8 +1,8 @@ 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.model.MaterialQuantityDto -import dev.fyloz.colorrecipesexplorer.model.MixDeductDto import org.springframework.context.annotation.Profile import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.PutMapping diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MixController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MixController.kt new file mode 100644 index 0000000..7392f80 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MixController.kt @@ -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(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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt index 90abb8a..875a879 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt @@ -16,7 +16,6 @@ import javax.validation.Valid private const val RECIPE_CONTROLLER_PATH = "api/recipe" -private const val MIX_CONTROLLER_PATH = "api/recipe/mix" @RestController @RequestMapping(RECIPE_CONTROLLER_PATH) @@ -83,34 +82,3 @@ class RecipeController( 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_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) - } -} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt new file mode 100644 index 0000000..5905923 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt @@ -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 { + /** Gets all mixes with the mix type with the given [mixTypeId]. */ + fun getAllByMixTypeId(mixTypeId: Long): Collection + + /** 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(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() + ) +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt index 6d196f6..e942fc7 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt @@ -14,6 +14,9 @@ interface MixTypeService : Service { /** Checks if a mix depends on the mix type with the given [id]. */ 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 @@ -25,8 +28,8 @@ class DefaultMixTypeService(repository: MixTypeRepository, val materialService: override fun getByNameAndMaterialType(name: String, materialTypeId: Long) = repository.findByNameAndMaterialType(name, materialTypeId)?.let(::toDto) - override fun isUsedByMixes(id: Long) = - repository.isUsedByMixes(id) + override fun isUsedByMixes(id: Long) = repository.isUsedByMixes(id) + override fun isShared(id: Long) = repository.isShared(id) override fun toDto(entity: MixType) = MixTypeDto(entity.id!!, entity.name, materialService.toDto(entity.material)) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt index 2d525d1..8c4d50b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt @@ -1,6 +1,6 @@ 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]. */ inline fun Iterable.mapMayThrow( @@ -19,26 +19,12 @@ inline fun Iterable.mapMayThrow( } } -/** Find duplicated keys in the given [Iterable], using keys obtained from the given [keySelector]. */ -inline fun Iterable.findDuplicated(keySelector: (T) -> K) = - this.groupBy(keySelector) - .filter { it.value.count() > 1 } - .map { it.key } - /** Find duplicated elements in the given [Iterable]. */ fun Iterable.findDuplicated() = this.groupBy { it } .filter { it.value.count() > 1 } .map { it.key } -/** Check if the given [Iterable] has gaps between each element, using keys obtained from the given [keySelector]. */ -inline fun Iterable.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. */ fun Iterable.hasGaps() = this.sorted() @@ -58,8 +44,20 @@ inline fun MutableCollection.excludeAll(predicate: (T) -> Boolean): Itera return matching } -/** Merge to [ModelEntity] [Iterable]s and prevent id duplication. */ -fun Iterable.merge(other: Iterable) = - this - .filter { model -> other.all { it.id != model.id } } +/** + * Merge two [EntityDto] [Iterable]s and prevent duplication of their ids. + * In case of collision, the items from the [other] iterable will be taken. + */ +@JvmName("mergeDto") +fun Iterable.merge(other: Iterable) = + 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 Iterable.merge(other: Iterable, keyMapper: (T) -> K) = + this.associateBy { keyMapper(it) } + .filter { pair -> other.all { keyMapper(it) != pair.key } } + .map { it.value } .plus(other) diff --git a/src/main/resources/application-debug.properties b/src/main/resources/application-debug.properties new file mode 100644 index 0000000..72fc330 --- /dev/null +++ b/src/main/resources/application-debug.properties @@ -0,0 +1 @@ +spring.jpa.show-sql=true \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt new file mode 100644 index 0000000..d5496d7 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt @@ -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() + private val mixLogicMock = mockk() + + 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()) } 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()) } 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() + 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>()) } 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>()) } 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()) } answers { + defaultInventoryQuantity - firstArg().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()) } answers { + defaultInventoryQuantity - firstArg().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()) } throws NotEnoughInventoryException(1000f, material) + + // Act + // Assert + assertThrows { 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 { inventoryLogic.deduct(quantity) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt index 20670b5..1238efe 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt @@ -1,12 +1,13 @@ package dev.fyloz.colorrecipesexplorer.logic -import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto -import dev.fyloz.colorrecipesexplorer.dtos.MaterialSaveDto -import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto +import dev.fyloz.colorrecipesexplorer.dtos.* import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException 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 io.mockk.* import org.junit.jupiter.api.AfterEach @@ -50,10 +51,10 @@ class DefaultMaterialLogicTest { mutableListOf(), setOf() ) - private val mix = Mix( - 1L, "location", recipe, mixType = MixType(1L, "Unit test mix type", material(materialMixType)), mutableSetOf() + private val mix = MixDto( + 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( "Unit test SIMDUT", @@ -62,7 +63,7 @@ class DefaultMaterialLogicTest { private val materialSaveDto = MaterialSaveDto(1L, "Unit test material", 1000f, materialType.id, simdutFileMock) init { - recipe.mixes.addAll(listOf(mix, mix2)) + recipe.mixes.addAll(listOf(mix(mix), mix(mix2))) } @AfterEach @@ -139,7 +140,7 @@ class DefaultMaterialLogicTest { every { mixLogicMock.getById(any()) } returns mix // Act - val materials = materialLogic.getAllForMixUpdate(mix.id!!) + val materials = materialLogic.getAllForMixUpdate(mix.id) // Assert assertContains(materials, material) @@ -152,7 +153,7 @@ class DefaultMaterialLogicTest { every { mixLogicMock.getById(any()) } returns mix // Act - val materials = materialLogic.getAllForMixUpdate(mix.id!!) + val materials = materialLogic.getAllForMixUpdate(mix.id) // Assert assertContains(materials, materialMixType2) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt new file mode 100644 index 0000000..1579f71 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt @@ -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() + private val recipeLogicMock = mockk() + private val materialTypeLogicMock = mockk() + private val mixTypeLogicMock = mockk() + private val mixMaterialLogicMock = mockk() + + 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()) } 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()) } 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) + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt index c27434c..f442552 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixMaterialLogicTest.kt @@ -14,8 +14,9 @@ import org.junit.jupiter.api.assertThrows class DefaultMixMaterialLogicTest { private val mixMaterialServiceMock = mockk() + private val materialLogicMock = mockk() - private val mixMaterialLogic = DefaultMixMaterialLogic(mixMaterialServiceMock) + private val mixMaterialLogic = DefaultMixMaterialLogic(mixMaterialServiceMock, materialLogicMock) @AfterEach internal fun afterEach() { diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixTypeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixTypeLogicTest.kt index 4c90086..cfe4b83 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixTypeLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixTypeLogicTest.kt @@ -9,6 +9,7 @@ import io.mockk.* import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import kotlin.math.exp import kotlin.test.assertEquals class DefaultMixTypeLogicTest { @@ -39,84 +40,105 @@ class DefaultMixTypeLogicTest { } @Test - fun getOrCreateForNameAndMaterialType_notFound_returnsFromSaveForNameAndMaterialType() { + fun getOrCreateForNameAndMaterialType_notFound_callsSave() { // Arrange every { mixTypeServiceMock.getByNameAndMaterialType(any(), any()) } returns null - every { mixTypeLogic.saveForNameAndMaterialType(any(), any()) } returns mixType + every { materialLogicMock.save(any()) } 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()) } returns material + every { mixTypeLogic.save(any()) } returnsArgument 0 + + val expectedMixType = mixType.copy(id = 0L) // Act val actualMixType = mixTypeLogic.getOrCreateForNameAndMaterialType(mixType.name, materialType) // Assert - assertEquals(mixType, actualMixType) + assertEquals(expectedMixType, actualMixType) } @Test - fun saveForNameAndMaterialType_normalBehavior_callsSavesInMaterialLogic() { + fun updateOrCreateForNameAndMaterialType_mixTypeShared_callsSave() { // Arrange - every { materialLogicMock.save(any()) } returnsArgument 0 + every { mixTypeServiceMock.isShared(any()) } returns true + every { materialLogicMock.save(any()) } returns material every { mixTypeLogic.save(any()) } returnsArgument 0 + val expectedMixType = mixType.copy(id = 0L, name = "${mixType.name} updated") + // Act - mixTypeLogic.saveForNameAndMaterialType(mixType.name, materialType) + mixTypeLogic.updateOrCreateForNameAndMaterialType(mixType, expectedMixType.name, materialType) // Assert verify { - materialLogicMock.save(match { it.name == mixType.name && it.materialType == materialType }) + mixTypeLogic.save(expectedMixType) } - confirmVerified(materialLogicMock) } @Test - fun saveForNameAndMaterialType_normalBehavior_callsSave() { + fun updateOrCreateForNameAndMaterialType_mixTypeShared_returnsFromSave() { // Arrange - every { materialLogicMock.save(any()) } returnsArgument 0 + every { mixTypeServiceMock.isShared(any()) } returns true + every { materialLogicMock.save(any()) } returns material every { mixTypeLogic.save(any()) } returnsArgument 0 + val expectedMixType = mixType.copy(id = 0L, name = "${mixType.name} updated") + // 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()) } returns material + every { mixTypeLogic.update(any()) } returnsArgument 0 + + val expectedMixType = mixType.copy(name = "${mixType.name} updated") + + // Act + mixTypeLogic.updateOrCreateForNameAndMaterialType(mixType, expectedMixType.name, materialType) // Assert verify { - mixTypeLogic.save(match { it.name == mixType.name && it.material.name == mixType.name && it.material.materialType == materialType }) + mixTypeLogic.update(expectedMixType) } } @Test - fun updateForNameAndMaterialType_normalBehavior_callsSavesInMaterialLogic() { + fun updateOrCreateForNameAndMaterialType_mixTypeNotShared_returnsFromUpdate() { // Arrange - val updatedName = mixType.name + " updated" - val updatedMaterialType = materialType.copy(id = 2L) - - every { materialLogicMock.update(any()) } returnsArgument 0 + every { mixTypeServiceMock.isShared(any()) } returns false + every { materialLogicMock.update(any()) } returns material every { mixTypeLogic.update(any()) } returnsArgument 0 - // Act - mixTypeLogic.updateForNameAndMaterialType(mixType, updatedName, updatedMaterialType) - - // Assert - verify { - materialLogicMock.update(match { 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()) } returnsArgument 0 - every { mixTypeLogic.update(any()) } returnsArgument 0 + val expectedMixType = mixType.copy(name = "${mixType.name} updated") // Act - mixTypeLogic.updateForNameAndMaterialType(mixType, updatedName, updatedMaterialType) + val actualMixType = mixTypeLogic.updateOrCreateForNameAndMaterialType(mixType, expectedMixType.name, materialType) // Assert - verify { - mixTypeLogic.update(match { it.name == updatedName && it.material.name == updatedName && it.material.materialType == updatedMaterialType }) - } + assertEquals(expectedMixType, actualMixType) } @Test diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt deleted file mode 100644 index 96a416c..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/InventoryLogicTest.kt +++ /dev/null @@ -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()) - - 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).map { materialQuantity -> - materialQuantityDto(materialId = materialQuantity.material, quantity = 0f) - } - }.whenever(logic).deduct(any>()) - - val found = logic.deductMix(mixRatio) - - verify(logic).deduct(argThat> { - 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()) - - 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 { 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 { 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) - } -} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt deleted file mode 100644 index d76215b..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogicTest.kt +++ /dev/null @@ -1,245 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.logic - -//@TestInstance(TestInstance.Lifecycle.PER_CLASS) -//class MixLogicTest : AbstractExternalModelServiceTest() { -// 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())) -// 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()) -//// -//// val found = logic.save(entitySaveDto) -//// -//// verify(logic).save(argThat { 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()) -//// -//// 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>())).doAnswer { -//// (it.arguments[0] as Set).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() -// ) -// ) -//) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt index 5be796c..53c34be 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt @@ -1,6 +1,7 @@ package dev.fyloz.colorrecipesexplorer.logic import com.nhaarman.mockitokotlin2.* +import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic 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`() { val publicData = recipePublicDataDto( mixesLocation = setOf( - mixLocationDto(mixId = 0L, location = "Loc 1"), - mixLocationDto(mixId = 1L, location = "Loc 2") + MixLocationDto(mixId = 0L, location = "Loc 1"), + MixLocationDto(mixId = 1L, location = "Loc 2") ) )