feature/#25-dtos #28
|
@ -1,7 +1,7 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic.files;
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto;
|
||||
import dev.fyloz.colorrecipesexplorer.logic.RecipeLogic;
|
||||
import dev.fyloz.colorrecipesexplorer.model.Recipe;
|
||||
import dev.fyloz.colorrecipesexplorer.xlsx.XlsxExporter;
|
||||
import mu.KotlinLogging;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -32,7 +32,7 @@ public class XlsService {
|
|||
* @param recipe La recette
|
||||
* @return Le fichier XLS de la recette
|
||||
*/
|
||||
public byte[] generate(Recipe recipe) {
|
||||
public byte[] generate(RecipeDto recipe) {
|
||||
return new XlsxExporter(logger).generate(recipe);
|
||||
}
|
||||
|
||||
|
@ -55,10 +55,10 @@ public class XlsService {
|
|||
logger.info("Exportation de toutes les couleurs en XLS");
|
||||
|
||||
byte[] zipContent;
|
||||
Collection<Recipe> recipes = recipeService.getAll();
|
||||
Collection<RecipeDto> recipes = recipeService.getAll();
|
||||
|
||||
try (ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(); ZipOutputStream zipOutput = new ZipOutputStream(byteOutput)) {
|
||||
for (Recipe recipe : recipes) {
|
||||
for (RecipeDto recipe : recipes) {
|
||||
byte[] recipeXLS = generate(recipe);
|
||||
zipOutput.putNextEntry(new ZipEntry(String.format("%s_%s.xlsx", recipe.getCompany().getName(), recipe.getName())));
|
||||
zipOutput.write(recipeXLS, 0, recipeXLS.length);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package dev.fyloz.colorrecipesexplorer.xlsx;
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.Mix;
|
||||
import dev.fyloz.colorrecipesexplorer.model.MixMaterial;
|
||||
import dev.fyloz.colorrecipesexplorer.model.Recipe;
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MixDto;
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto;
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto;
|
||||
import dev.fyloz.colorrecipesexplorer.xlsx.component.Document;
|
||||
import dev.fyloz.colorrecipesexplorer.xlsx.component.Sheet;
|
||||
import dev.fyloz.colorrecipesexplorer.xlsx.component.Table;
|
||||
|
@ -23,7 +23,7 @@ public class XlsxExporter {
|
|||
this.logger = logger;
|
||||
}
|
||||
|
||||
public byte[] generate(Recipe recipe) {
|
||||
public byte[] generate(RecipeDto recipe) {
|
||||
logger.info(String.format("Génération du XLS de la couleur %s (%s)", recipe.getName(), recipe.getId()));
|
||||
|
||||
Document document = new Document(recipe.getName(), logger);
|
||||
|
@ -44,7 +44,7 @@ public class XlsxExporter {
|
|||
return output;
|
||||
}
|
||||
|
||||
private void registerCells(Recipe recipe, Sheet sheet) {
|
||||
private void registerCells(RecipeDto recipe, Sheet sheet) {
|
||||
// Header
|
||||
sheet.registerCell(new TitleCell(recipe.getName()));
|
||||
sheet.registerCell(new DescriptionCell(DescriptionCell.DescriptionCellType.NAME, "Bannière"));
|
||||
|
@ -59,17 +59,17 @@ public class XlsxExporter {
|
|||
sheet.registerCell(new DescriptionCell(DescriptionCell.DescriptionCellType.VALUE_STR, recipe.getRemark()));
|
||||
|
||||
// Mélanges
|
||||
Collection<Mix> recipeMixes = recipe.getMixes();
|
||||
Collection<MixDto> recipeMixes = recipe.getMixes();
|
||||
if (recipeMixes.size() > 0) {
|
||||
sheet.registerCell(new SectionTitleCell("Recette"));
|
||||
|
||||
for (Mix mix : recipeMixes) {
|
||||
for (MixDto mix : recipeMixes) {
|
||||
Table mixTable = new Table(4, mix.getMixMaterials().size() + 1, mix.getMixType().getName());
|
||||
mixTable.setColumnName(0, "Quantité");
|
||||
mixTable.setColumnName(2, "Unités");
|
||||
|
||||
int row = 0;
|
||||
for (MixMaterial mixMaterial : mix.getMixMaterials()) {
|
||||
for (MixMaterialDto mixMaterial : mix.getMixMaterials()) {
|
||||
mixTable.setRowName(row, mixMaterial.getMaterial().getName());
|
||||
mixTable.setContent(new Position(1, row + 1), mixMaterial.getQuantity());
|
||||
mixTable.setContent(new Position(3, row + 1), mixMaterial.getMaterial().getMaterialType().getUsePercentages() ? "%" : "mL");
|
||||
|
|
|
@ -6,12 +6,15 @@ object Constants {
|
|||
const val MATERIAL = "/api/material"
|
||||
const val MATERIAL_TYPE = "/api/materialtype"
|
||||
const val MIX = "/api/recipe/mix"
|
||||
const val RECIPE = "/api/recipe"
|
||||
}
|
||||
|
||||
object FilePaths {
|
||||
const val PDF = "pdf"
|
||||
private const val PDF = "pdf"
|
||||
private const val IMAGES = "images"
|
||||
|
||||
const val SIMDUT = "$PDF/simdut"
|
||||
const val RECIPE_IMAGES = "$IMAGES/recipes"
|
||||
}
|
||||
|
||||
object ValidationMessages {
|
||||
|
@ -19,4 +22,8 @@ object Constants {
|
|||
const val SIZE_GREATER_OR_EQUALS_ONE = "Must be greater or equals to 1"
|
||||
const val RANGE_OUTSIDE_PERCENTS = "Must be between 0 and 100"
|
||||
}
|
||||
|
||||
object ValidationRegexes {
|
||||
const val VALIDATION_COLOR_PATTERN = "^#([0-9a-f]{6})$"
|
||||
}
|
||||
}
|
|
@ -34,7 +34,7 @@ class MixInitializer(
|
|||
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:")
|
||||
logger.warn("Mix ${mix.id} (mix name: ${mix.mixType.name}, recipe id: ${mix.recipeId}) has invalid positions:")
|
||||
|
||||
val invalidMixMaterials: Collection<MixMaterialDto> = with(mix.mixMaterials.filter { it.position == 0 }) {
|
||||
if (maxPosition == 0 && this.size > 1) {
|
||||
|
@ -47,7 +47,7 @@ class MixInitializer(
|
|||
val fixedMixMaterials = increaseMixMaterialsPosition(invalidMixMaterials, maxPosition + 1)
|
||||
val updatedMixMaterials = mix.mixMaterials.merge(fixedMixMaterials)
|
||||
|
||||
with(mix.copy(mixMaterials = updatedMixMaterials.toMutableSet())) {
|
||||
with(mix.copy(mixMaterials = updatedMixMaterials)) {
|
||||
mixLogic.update(this)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config.initializers
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto
|
||||
import dev.fyloz.colorrecipesexplorer.logic.RecipeLogic
|
||||
import dev.fyloz.colorrecipesexplorer.model.Recipe
|
||||
import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation
|
||||
import dev.fyloz.colorrecipesexplorer.model.RecipeStep
|
||||
import dev.fyloz.colorrecipesexplorer.utils.merge
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
@ -24,30 +24,29 @@ class RecipeInitializer(
|
|||
private fun fixAllPositions() {
|
||||
logger.debug("Validating recipes steps positions...")
|
||||
|
||||
recipeLogic.getAll()
|
||||
recipeLogic.getAllWithMixesAndGroupsInformation()
|
||||
.forEach(this::fixRecipePositions)
|
||||
|
||||
logger.debug("Recipes steps positions are valid!")
|
||||
}
|
||||
|
||||
private fun fixRecipePositions(recipe: Recipe) {
|
||||
private fun fixRecipePositions(recipe: RecipeDto) {
|
||||
val fixedGroupInformation = recipe.groupsInformation
|
||||
.filter { it.steps != null }
|
||||
.filter { groupInfo -> groupInfo.steps!!.any { it.position == 0 } }
|
||||
.filter { groupInfo -> groupInfo.steps.any { it.position == 0 } }
|
||||
.map { fixGroupInformationPositions(recipe, it) }
|
||||
|
||||
val updatedGroupInformation = recipe.groupsInformation.merge(fixedGroupInformation) { it.id }
|
||||
|
||||
with(recipe.copy(groupsInformation = updatedGroupInformation.toMutableSet())) {
|
||||
with(recipe.copy(groupsInformation = updatedGroupInformation)) {
|
||||
recipeLogic.update(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fixGroupInformationPositions(
|
||||
recipe: Recipe,
|
||||
groupInformation: RecipeGroupInformation
|
||||
): RecipeGroupInformation {
|
||||
val steps = groupInformation.steps!!
|
||||
recipe: RecipeDto,
|
||||
groupInformation: RecipeGroupInformationDto
|
||||
): RecipeGroupInformationDto {
|
||||
val steps = groupInformation.steps
|
||||
val maxPosition = steps.maxOf { it.position }
|
||||
|
||||
logger.warn("Recipe ${recipe.id} (${recipe.name}) has invalid positions:")
|
||||
|
@ -56,12 +55,12 @@ class RecipeInitializer(
|
|||
val fixedRecipeSteps = increaseRecipeStepsPosition(groupInformation, invalidRecipeSteps, maxPosition + 1)
|
||||
val updatedRecipeSteps = steps.merge(fixedRecipeSteps) { it.id }
|
||||
|
||||
return groupInformation.copy(steps = updatedRecipeSteps.toMutableSet())
|
||||
return groupInformation.copy(steps = updatedRecipeSteps)
|
||||
}
|
||||
|
||||
private fun increaseRecipeStepsPosition(
|
||||
groupInformation: RecipeGroupInformation,
|
||||
recipeSteps: Iterable<RecipeStep>,
|
||||
groupInformation: RecipeGroupInformationDto,
|
||||
recipeSteps: Iterable<RecipeStepDto>,
|
||||
firstPosition: Int
|
||||
) =
|
||||
recipeSteps
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config.properties
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.MaterialType
|
||||
import dev.fyloz.colorrecipesexplorer.model.materialType
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.util.Assert
|
||||
|
|
|
@ -2,7 +2,6 @@ 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
|
||||
|
||||
|
@ -12,11 +11,11 @@ data class MixDto(
|
|||
val location: String? = null,
|
||||
|
||||
@JsonIgnore
|
||||
val recipe: Recipe, // TODO change to dto
|
||||
val recipeId: Long,
|
||||
|
||||
val mixType: MixTypeDto,
|
||||
|
||||
val mixMaterials: Set<MixMaterialDto>
|
||||
val mixMaterials: List<MixMaterialDto>
|
||||
) : EntityDto
|
||||
|
||||
data class MixSaveDto(
|
||||
|
@ -29,7 +28,7 @@ data class MixSaveDto(
|
|||
|
||||
val materialTypeId: Long,
|
||||
|
||||
val mixMaterials: Set<MixMaterialSaveDto>
|
||||
val mixMaterials: List<MixMaterialSaveDto>
|
||||
)
|
||||
|
||||
data class MixDeductDto(
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
package dev.fyloz.colorrecipesexplorer.dtos
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Group
|
||||
import java.time.LocalDate
|
||||
import javax.validation.constraints.Max
|
||||
import javax.validation.constraints.Min
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.Pattern
|
||||
|
||||
data class RecipeDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
val name: String,
|
||||
|
||||
val description: String,
|
||||
|
||||
val color: String,
|
||||
|
||||
val gloss: Byte,
|
||||
|
||||
val sample: Int?,
|
||||
|
||||
val approbationDate: LocalDate?,
|
||||
|
||||
val approbationExpired: Boolean,
|
||||
|
||||
val remark: String,
|
||||
|
||||
val company: CompanyDto,
|
||||
|
||||
val mixes: List<MixDto>,
|
||||
|
||||
val groupsInformation: List<RecipeGroupInformationDto>
|
||||
) : EntityDto {
|
||||
val mixTypes: Collection<MixTypeDto>
|
||||
@JsonIgnore
|
||||
get() = mixes.map { it.mixType }
|
||||
}
|
||||
|
||||
data class RecipeSaveDto(
|
||||
@field:NotBlank
|
||||
val name: String,
|
||||
|
||||
@field:NotBlank
|
||||
val description: String,
|
||||
|
||||
@field:NotBlank
|
||||
@field:Pattern(regexp = Constants.ValidationRegexes.VALIDATION_COLOR_PATTERN)
|
||||
val color: String,
|
||||
|
||||
@field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS)
|
||||
@field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS)
|
||||
val gloss: Byte,
|
||||
|
||||
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
|
||||
val sample: Int?,
|
||||
|
||||
val approbationDate: LocalDate?,
|
||||
|
||||
val remark: String?,
|
||||
|
||||
val companyId: Long
|
||||
)
|
||||
|
||||
data class RecipeUpdateDto(
|
||||
val id: Long,
|
||||
|
||||
@field:NotBlank
|
||||
val name: String,
|
||||
|
||||
@field:NotBlank
|
||||
val description: String,
|
||||
|
||||
@field:NotBlank
|
||||
@field:Pattern(regexp = Constants.ValidationRegexes.VALIDATION_COLOR_PATTERN)
|
||||
val color: String,
|
||||
|
||||
@field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS)
|
||||
@field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS)
|
||||
val gloss: Byte,
|
||||
|
||||
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
|
||||
val sample: Int?,
|
||||
|
||||
val approbationDate: LocalDate?,
|
||||
|
||||
val remark: String?,
|
||||
|
||||
val steps: List<RecipeGroupStepsDto>
|
||||
)
|
||||
|
||||
data class RecipeGroupInformationDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
val group: Group,
|
||||
|
||||
val note: String? = null,
|
||||
|
||||
val steps: List<RecipeStepDto> = listOf()
|
||||
) : EntityDto
|
||||
|
||||
data class RecipeGroupStepsDto(
|
||||
val groupId: Long,
|
||||
|
||||
val steps: List<RecipeStepDto>
|
||||
)
|
||||
|
||||
data class RecipeGroupNoteDto(
|
||||
val groupId: Long,
|
||||
|
||||
val content: String?
|
||||
)
|
||||
|
||||
data class RecipePublicDataDto(
|
||||
val recipeId: Long,
|
||||
|
||||
val notes: List<RecipeGroupNoteDto>,
|
||||
|
||||
val mixesLocation: List<MixLocationDto>
|
||||
)
|
|
@ -5,6 +5,8 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
|||
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.service.Service
|
||||
import dev.fyloz.colorrecipesexplorer.utils.collections.LazyMapList
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
/**
|
||||
* Represents the logic for a DTO type.
|
||||
|
@ -92,6 +94,17 @@ abstract class BaseLogic<D : EntityDto, S : Service<D, *, *>>(
|
|||
details
|
||||
)
|
||||
|
||||
private fun loadRelations(dto: D, relationSelectors: Collection<(D) -> Iterable<*>>) {
|
||||
relationSelectors.map { it(dto) }
|
||||
.forEach {
|
||||
if (it is LazyMapList<*, *>) {
|
||||
it.initialize()
|
||||
} else {
|
||||
println("Can't load :(")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ID_IDENTIFIER_NAME = "id"
|
||||
const val NAME_IDENTIFIER_NAME = "name"
|
||||
|
|
|
@ -56,9 +56,9 @@ class DefaultMaterialLogic(
|
|||
|
||||
override fun getAllForMixUpdate(mixId: Long): Collection<MaterialDto> {
|
||||
val mix = mixLogic.getById(mixId)
|
||||
val recipesMixTypes = mix.recipe.mixTypes
|
||||
val recipe = recipeLogic.getById(mix.recipeId)
|
||||
|
||||
return getAll().filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } }
|
||||
return getAll().filter { !it.isMixType || recipe.mixTypes.any { mixType -> mixType.material.id == it.id } }
|
||||
.filter { it.id != mix.mixType.material.id }
|
||||
}
|
||||
|
||||
|
|
|
@ -34,9 +34,9 @@ class DefaultMixLogic(
|
|||
val materialType = materialTypeLogic.getById(dto.materialTypeId)
|
||||
|
||||
val mix = MixDto(
|
||||
recipe = recipe,
|
||||
recipeId = recipe.id,
|
||||
mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(dto.name, materialType),
|
||||
mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials).toSet()
|
||||
mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials)
|
||||
)
|
||||
|
||||
return save(mix)
|
||||
|
@ -50,9 +50,9 @@ class DefaultMixLogic(
|
|||
return update(
|
||||
MixDto(
|
||||
id = dto.id,
|
||||
recipe = recipeLogic.getById(dto.recipeId),
|
||||
recipeId = dto.recipeId,
|
||||
mixType = mixTypeLogic.updateOrCreateForNameAndMaterialType(mix.mixType, dto.name, materialType),
|
||||
mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials).toSet()
|
||||
mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ interface MixMaterialLogic : Logic<MixMaterialDto, MixMaterialService> {
|
|||
fun validateMixMaterials(mixMaterials: Set<MixMaterialDto>)
|
||||
|
||||
/** Validates the given mix materials [dtos] and save them. */
|
||||
fun validateAndSaveAll(dtos: Collection<MixMaterialSaveDto>): Collection<MixMaterialDto>
|
||||
fun validateAndSaveAll(dtos: List<MixMaterialSaveDto>): List<MixMaterialDto>
|
||||
}
|
||||
|
||||
@LogicComponent
|
||||
|
@ -43,7 +43,7 @@ class DefaultMixMaterialLogic(service: MixMaterialService, @Lazy private val mat
|
|||
}
|
||||
}
|
||||
|
||||
override fun validateAndSaveAll(dtos: Collection<MixMaterialSaveDto>): Collection<MixMaterialDto> {
|
||||
override fun validateAndSaveAll(dtos: List<MixMaterialSaveDto>): List<MixMaterialDto> {
|
||||
val dtosWithMaterials = dtos.map {
|
||||
MixMaterialDto(
|
||||
id = it.id,
|
||||
|
|
|
@ -1,256 +1,185 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
|
||||
import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.*
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Group
|
||||
import dev.fyloz.colorrecipesexplorer.model.validation.or
|
||||
import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository
|
||||
import dev.fyloz.colorrecipesexplorer.utils.setAll
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.stereotype.Service
|
||||
import dev.fyloz.colorrecipesexplorer.model.Recipe
|
||||
import dev.fyloz.colorrecipesexplorer.service.RecipeService
|
||||
import dev.fyloz.colorrecipesexplorer.utils.collections.LazyMapList
|
||||
import dev.fyloz.colorrecipesexplorer.utils.merge
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.time.LocalDate
|
||||
import java.time.Period
|
||||
import javax.transaction.Transactional
|
||||
|
||||
interface RecipeLogic :
|
||||
ExternalModelService<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeOutputDto, RecipeRepository> {
|
||||
/** Checks if one or more recipes have the given [company]. */
|
||||
fun existsByCompany(company: Company): Boolean
|
||||
|
||||
/** Checks if a recipe exists with the given [name] and [company]. */
|
||||
fun existsByNameAndCompany(name: String, company: Company): Boolean
|
||||
|
||||
/** Checks if the approbation date of the given [recipe] is expired. */
|
||||
fun isApprobationExpired(recipe: Recipe): Boolean?
|
||||
interface RecipeLogic : Logic<RecipeDto, RecipeService> {
|
||||
/** Gets all recipes and load their mixes and groupsInformation, to prevent LazyInitializationExceptions */
|
||||
fun getAllWithMixesAndGroupsInformation(): Collection<RecipeDto>
|
||||
|
||||
/** Gets all recipes with the given [name]. */
|
||||
fun getAllByName(name: String): Collection<Recipe>
|
||||
fun getAllByName(name: String): Collection<RecipeDto>
|
||||
|
||||
/** Gets all recipes with the given [company]. */
|
||||
fun getAllByCompany(company: Company): Collection<Recipe>
|
||||
/** Saves the given [dto]. */
|
||||
fun save(dto: RecipeSaveDto): RecipeDto
|
||||
|
||||
/** Updates the given [dto]. */
|
||||
fun update(dto: RecipeUpdateDto): RecipeDto
|
||||
|
||||
/** Updates the public data of a recipe with the given [publicDataDto]. */
|
||||
fun updatePublicData(publicDataDto: RecipePublicDataDto)
|
||||
|
||||
/** Adds the given [mix] to the given [recipe]. */
|
||||
fun addMix(recipe: Recipe, mix: Mix): Recipe
|
||||
|
||||
/** Removes the given [mix] from its recipe. */
|
||||
fun removeMix(mix: Mix): Recipe
|
||||
}
|
||||
|
||||
@Service
|
||||
@RequireDatabase
|
||||
@LogicComponent
|
||||
class DefaultRecipeLogic(
|
||||
recipeRepository: RecipeRepository,
|
||||
val companyLogic: CompanyLogic,
|
||||
val mixLogic: MixLogic,
|
||||
val recipeStepLogic: RecipeStepLogic,
|
||||
@Lazy val groupLogic: GroupLogic,
|
||||
val recipeImageLogic: RecipeImageLogic,
|
||||
val configService: ConfigurationLogic
|
||||
) :
|
||||
AbstractExternalModelService<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeOutputDto, RecipeRepository>(
|
||||
recipeRepository
|
||||
),
|
||||
RecipeLogic {
|
||||
override fun idNotFoundException(id: Long) = recipeIdNotFoundException(id)
|
||||
override fun idAlreadyExistsException(id: Long) = recipeIdAlreadyExistsException(id)
|
||||
service: RecipeService,
|
||||
private val companyLogic: CompanyLogic,
|
||||
private val recipeStepLogic: RecipeStepLogic,
|
||||
private val mixLogic: MixLogic,
|
||||
private val groupLogic: GroupLogic
|
||||
) : BaseLogic<RecipeDto, RecipeService>(service, Recipe::class.simpleName!!), RecipeLogic {
|
||||
@Transactional
|
||||
override fun getAllWithMixesAndGroupsInformation() =
|
||||
getAll().onEach { (it.mixes as LazyMapList<*, *>).initialize() }
|
||||
.onEach { (it.groupsInformation as LazyMapList<*, *>).initialize() }
|
||||
|
||||
override fun Recipe.toOutput() = RecipeOutputDto(
|
||||
this.id!!,
|
||||
this.name,
|
||||
this.description,
|
||||
this.color,
|
||||
this.gloss,
|
||||
this.sample,
|
||||
this.approbationDate,
|
||||
isApprobationExpired(this),
|
||||
this.remark,
|
||||
this.company,
|
||||
this.mixes.map { mix(it) }.toSet(),
|
||||
this.groupsInformation,
|
||||
recipeImageLogic.getAllImages(this)
|
||||
.map { this.imageUrl(configService.getContent(ConfigurationType.INSTANCE_URL), it) }
|
||||
.toSet()
|
||||
override fun getAllByName(name: String) = service.getAllByName(name)
|
||||
|
||||
override fun save(dto: RecipeSaveDto) = save(
|
||||
RecipeDto(
|
||||
name = dto.name,
|
||||
description = dto.description,
|
||||
color = dto.color,
|
||||
gloss = dto.gloss,
|
||||
sample = dto.sample,
|
||||
approbationDate = dto.approbationDate,
|
||||
approbationExpired = false,
|
||||
remark = dto.remark ?: "",
|
||||
company = companyLogic.getById(dto.companyId),
|
||||
mixes = listOf(),
|
||||
groupsInformation = listOf()
|
||||
)
|
||||
)
|
||||
|
||||
override fun existsByCompany(company: Company): Boolean = repository.existsByCompany(company)
|
||||
override fun existsByNameAndCompany(name: String, company: Company) =
|
||||
repository.existsByNameAndCompany(name, company)
|
||||
override fun save(dto: RecipeDto): RecipeDto {
|
||||
throwIfNameAndCompanyAlreadyExists(dto.name, dto.company.id)
|
||||
|
||||
override fun isApprobationExpired(recipe: Recipe): Boolean? =
|
||||
with(Period.parse(configService.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION))) {
|
||||
recipe.approbationDate?.plus(this)?.isBefore(LocalDate.now())
|
||||
}
|
||||
|
||||
override fun getAllByName(name: String) = repository.findAllByName(name)
|
||||
override fun getAllByCompany(company: Company) = repository.findAllByCompany(company)
|
||||
|
||||
override fun save(entity: RecipeSaveDto): Recipe {
|
||||
val company = company(companyLogic.getById(entity.companyId))
|
||||
|
||||
if (existsByNameAndCompany(entity.name, company)) {
|
||||
throw recipeNameAlreadyExistsForCompanyException(entity.name, company)
|
||||
}
|
||||
|
||||
return save(with(entity) {
|
||||
recipe(
|
||||
name = name,
|
||||
description = description,
|
||||
color = color,
|
||||
gloss = gloss,
|
||||
sample = sample,
|
||||
approbationDate = approbationDate,
|
||||
remark = remark ?: "",
|
||||
company = company
|
||||
)
|
||||
})
|
||||
return super.save(dto)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun update(entity: RecipeUpdateDto): Recipe {
|
||||
val persistedRecipe = getById(entity.id)
|
||||
val name = entity.name
|
||||
val company = persistedRecipe.company
|
||||
override fun update(dto: RecipeUpdateDto): RecipeDto {
|
||||
val recipe = getById(dto.id)
|
||||
|
||||
if (name != null &&
|
||||
name != persistedRecipe.name &&
|
||||
existsByNameAndCompany(name, company)
|
||||
) {
|
||||
throw recipeNameAlreadyExistsForCompanyException(name, company)
|
||||
}
|
||||
|
||||
return update(with(entity) {
|
||||
recipe(
|
||||
id = id,
|
||||
name = name or persistedRecipe.name,
|
||||
description = description or persistedRecipe.description,
|
||||
color = color or persistedRecipe.color,
|
||||
gloss = gloss ?: persistedRecipe.gloss,
|
||||
sample = sample ?: persistedRecipe.sample,
|
||||
approbationDate = approbationDate ?: persistedRecipe.approbationDate,
|
||||
remark = remark or persistedRecipe.remark,
|
||||
company = company,
|
||||
mixes = persistedRecipe.mixes,
|
||||
groupsInformation = updateGroupsInformation(persistedRecipe, entity)
|
||||
return update(
|
||||
RecipeDto(
|
||||
id = dto.id,
|
||||
name = dto.name,
|
||||
description = dto.description,
|
||||
color = dto.color,
|
||||
gloss = dto.gloss,
|
||||
sample = dto.sample,
|
||||
approbationDate = dto.approbationDate,
|
||||
approbationExpired = false,
|
||||
remark = dto.remark ?: "",
|
||||
company = recipe.company,
|
||||
mixes = recipe.mixes,
|
||||
groupsInformation = updateGroupsInformationSteps(recipe, dto)
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateGroupsInformation(recipe: Recipe, updateDto: RecipeUpdateDto): Set<RecipeGroupInformation> {
|
||||
val steps = updateDto.steps ?: return recipe.groupsInformation
|
||||
override fun update(dto: RecipeDto): RecipeDto {
|
||||
throwIfNameAndCompanyAlreadyExists(dto.name, dto.company.id, dto.id)
|
||||
|
||||
val updatedGroupsInformation = mutableSetOf<RecipeGroupInformation>()
|
||||
steps.forEach {
|
||||
with(recipe.groupInformationForGroup(it.groupId)) {
|
||||
// Set steps for the existing RecipeGroupInformation or create a new one
|
||||
val updatedGroupInformation = this?.apply {
|
||||
if (this.steps != null) {
|
||||
this.steps!!.setAll(it.steps)
|
||||
} else {
|
||||
this.steps = it.steps.toMutableSet()
|
||||
}
|
||||
} ?: recipeGroupInformation(
|
||||
group = groupLogic.getById(it.groupId),
|
||||
steps = it.steps.toMutableSet()
|
||||
)
|
||||
|
||||
updatedGroupsInformation.add(updatedGroupInformation)
|
||||
recipeStepLogic.validateGroupInformationSteps(updatedGroupInformation)
|
||||
}
|
||||
}
|
||||
|
||||
return updatedGroupsInformation
|
||||
return super.update(dto)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun updatePublicData(publicDataDto: RecipePublicDataDto) {
|
||||
if (publicDataDto.notes != null) {
|
||||
// Update notes
|
||||
if (publicDataDto.notes.isNotEmpty()) {
|
||||
val recipe = getById(publicDataDto.recipeId)
|
||||
|
||||
fun noteForGroup(group: Group) =
|
||||
publicDataDto.notes.firstOrNull { it.groupId == group.id }?.content
|
||||
|
||||
// Notes
|
||||
recipe.groupsInformation.map {
|
||||
val updatedNote = noteForGroup(it.group)
|
||||
it.apply {
|
||||
note = updatedNote
|
||||
}
|
||||
}
|
||||
|
||||
update(recipe)
|
||||
update(recipe.copy(groupsInformation = updateGroupsInformationNotes(recipe, publicDataDto.notes)))
|
||||
}
|
||||
|
||||
if (publicDataDto.mixesLocation != null) {
|
||||
// Update mixes locations
|
||||
if (publicDataDto.mixesLocation.isNotEmpty()) {
|
||||
mixLogic.updateLocations(publicDataDto.mixesLocation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addMix(recipe: Recipe, mix: Mix) =
|
||||
update(recipe.apply { mixes.add(mix) })
|
||||
private fun updateGroupsInformationSteps(recipe: RecipeDto, dto: RecipeUpdateDto): List<RecipeGroupInformationDto> {
|
||||
val updatedGroupsInformation = dto.steps.map { updateGroupInformationSteps(recipe, it) }
|
||||
return recipe.groupsInformation.merge(updatedGroupsInformation)
|
||||
}
|
||||
|
||||
override fun removeMix(mix: Mix): Recipe =
|
||||
update(mix.recipe.apply { mixes.remove(mix) })
|
||||
private fun updateGroupInformationSteps(recipe: RecipeDto, groupSteps: RecipeGroupStepsDto) =
|
||||
getOrCreateGroupInformation(recipe, groupSteps.groupId).copy(steps = groupSteps.steps).also {
|
||||
recipeStepLogic.validateGroupInformationSteps(it)
|
||||
}
|
||||
|
||||
private fun updateGroupsInformationNotes(
|
||||
recipe: RecipeDto, notes: List<RecipeGroupNoteDto>
|
||||
): List<RecipeGroupInformationDto> {
|
||||
val updatedGroupsInformation = notes.map { updateGroupInformationNote(recipe, it) }
|
||||
return recipe.groupsInformation.merge(updatedGroupsInformation)
|
||||
}
|
||||
|
||||
private fun updateGroupInformationNote(recipe: RecipeDto, groupNote: RecipeGroupNoteDto) =
|
||||
getOrCreateGroupInformation(recipe, groupNote.groupId).copy(note = groupNote.content)
|
||||
|
||||
private fun getOrCreateGroupInformation(recipe: RecipeDto, groupId: Long) =
|
||||
recipe.groupsInformation.firstOrNull { it.group.id == groupId }
|
||||
?: RecipeGroupInformationDto(group = groupLogic.getById(groupId))
|
||||
|
||||
private fun throwIfNameAndCompanyAlreadyExists(name: String, companyId: Long, id: Long? = null) {
|
||||
if (service.existsByNameAndCompany(name, companyId, id)) {
|
||||
throw AlreadyExistsException(
|
||||
"$typeNameLowerCase-company",
|
||||
"$typeName already exists",
|
||||
"A recipe with the name '$name' already exists for the company with the id '$companyId'",
|
||||
name,
|
||||
NAME_IDENTIFIER_NAME,
|
||||
mutableMapOf(
|
||||
"companyId" to companyId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface RecipeImageLogic {
|
||||
/** Gets the name of every images associated to the recipe with the given [recipe]. */
|
||||
fun getAllImages(recipe: Recipe): Set<String>
|
||||
/** Gets the id of every image associated to the recipe with the given [recipeId]. */
|
||||
fun getAllImages(recipeId: Long): List<String>
|
||||
|
||||
/** Saves the given [image] and associate it to the recipe with the given [recipe]. Returns the name of the saved image. */
|
||||
fun download(image: MultipartFile, recipe: Recipe): String
|
||||
/** Saves the given [image] and associate it to the recipe with the given [recipeId]. Returns the id of the saved image. */
|
||||
fun download(image: MultipartFile, recipeId: Long): String
|
||||
|
||||
/** Deletes the image with the given [name] for the given [recipe]. */
|
||||
fun delete(recipe: Recipe, name: String)
|
||||
/** Deletes the image with the given [path] for the given [recipeId]. */
|
||||
fun delete(recipeId: Long, path: String)
|
||||
}
|
||||
|
||||
const val RECIPE_IMAGE_ID_DELIMITER = "_"
|
||||
const val RECIPE_IMAGE_EXTENSION = ".jpg"
|
||||
@LogicComponent
|
||||
class DefaultRecipeImageLogic(val fileLogic: WriteableFileLogic) : RecipeImageLogic {
|
||||
override fun getAllImages(recipeId: Long) =
|
||||
fileLogic.listDirectoryFiles(getRecipeImagesDirectory(recipeId)).map { it.name }
|
||||
|
||||
@Service
|
||||
@RequireDatabase
|
||||
class DefaultRecipeImageLogic(
|
||||
val fileService: WriteableFileLogic
|
||||
) : RecipeImageLogic {
|
||||
override fun getAllImages(recipe: Recipe) =
|
||||
fileService.listDirectoryFiles(recipe.imagesDirectoryPath)
|
||||
.map { it.name }
|
||||
.toSet()
|
||||
override fun download(image: MultipartFile, recipeId: Long): String {
|
||||
/** Gets the next id available for a new image for the given [recipeId]. */
|
||||
fun getNextAvailableId(): String = with(getAllImages(recipeId)) {
|
||||
(if (isEmpty()) 0 else maxOf { it.toLong() } + 1L).toString()
|
||||
}
|
||||
|
||||
override fun download(image: MultipartFile, recipe: Recipe): String {
|
||||
/** Gets the next id available for a new image for the given [recipe]. */
|
||||
fun getNextAvailableId(): Long =
|
||||
with(getAllImages(recipe)) {
|
||||
if (isEmpty())
|
||||
0
|
||||
else
|
||||
maxOf {
|
||||
it.split(RECIPE_IMAGE_ID_DELIMITER)
|
||||
.last()
|
||||
.replace(RECIPE_IMAGE_EXTENSION, "")
|
||||
.toLong()
|
||||
} + 1L
|
||||
}
|
||||
|
||||
return getImageFileName(recipe, getNextAvailableId()).also {
|
||||
with(getImagePath(recipe, it)) {
|
||||
fileService.writeToDirectory(image, this, recipe.imagesDirectoryPath, true)
|
||||
}
|
||||
return getNextAvailableId().also {
|
||||
val imagePath = getImagePath(recipeId, it)
|
||||
fileLogic.writeToDirectory(image, imagePath, getRecipeImagesDirectory(recipeId), true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(recipe: Recipe, name: String) =
|
||||
fileService.deleteFromDirectory(getImagePath(recipe, name), recipe.imagesDirectoryPath)
|
||||
override fun delete(recipeId: Long, path: String) =
|
||||
fileLogic.deleteFromDirectory(path, getRecipeImagesDirectory(recipeId))
|
||||
|
||||
private fun getImageFileName(recipe: Recipe, id: Long) =
|
||||
"${recipe.name}$RECIPE_IMAGE_ID_DELIMITER$id"
|
||||
private fun getImagePath(recipeId: Long, id: String) = "${getRecipeImagesDirectory(recipeId)}/$id"
|
||||
|
||||
private fun getImagePath(recipe: Recipe, name: String) =
|
||||
"${recipe.imagesDirectoryPath}/$name$RECIPE_IMAGE_EXTENSION"
|
||||
private fun getRecipeImagesDirectory(recipeId: Long) = "${Constants.FilePaths.RECIPE_IMAGES}/$recipeId"
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError
|
||||
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation
|
||||
import dev.fyloz.colorrecipesexplorer.model.RecipeStep
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Group
|
||||
import dev.fyloz.colorrecipesexplorer.service.RecipeStepService
|
||||
|
@ -14,17 +14,15 @@ import org.springframework.http.HttpStatus
|
|||
|
||||
interface RecipeStepLogic : Logic<RecipeStepDto, RecipeStepService> {
|
||||
/** Validates the steps of the given [groupInformation], according to the criteria of [PositionUtils.validate]. */
|
||||
fun validateGroupInformationSteps(groupInformation: RecipeGroupInformation)
|
||||
fun validateGroupInformationSteps(groupInformation: RecipeGroupInformationDto)
|
||||
}
|
||||
|
||||
@LogicComponent
|
||||
class DefaultRecipeStepLogic(recipeStepService: RecipeStepService) :
|
||||
BaseLogic<RecipeStepDto, RecipeStepService>(recipeStepService, RecipeStep::class.simpleName!!), RecipeStepLogic {
|
||||
override fun validateGroupInformationSteps(groupInformation: RecipeGroupInformation) {
|
||||
if (groupInformation.steps == null) return
|
||||
|
||||
override fun validateGroupInformationSteps(groupInformation: RecipeGroupInformationDto) {
|
||||
try {
|
||||
PositionUtils.validate(groupInformation.steps!!.map { it.position }.toList())
|
||||
PositionUtils.validate(groupInformation.steps.map { it.position }.toList())
|
||||
} catch (ex: InvalidPositionsException) {
|
||||
throw InvalidGroupStepsPositionsException(groupInformation.group, ex)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto
|
||||
import javax.persistence.*
|
||||
|
||||
@Entity
|
||||
|
@ -12,21 +11,4 @@ data class Company(
|
|||
|
||||
@Column(unique = true)
|
||||
val name: String
|
||||
) : ModelEntity
|
||||
|
||||
// ==== DSL ====
|
||||
fun company(
|
||||
id: Long? = null,
|
||||
name: String = "name",
|
||||
op: Company.() -> Unit = {}
|
||||
) = Company(id, name).apply(op)
|
||||
|
||||
@Deprecated("Temporary DSL for transition")
|
||||
fun company(
|
||||
dto: CompanyDto
|
||||
) = Company(dto.id, dto.name)
|
||||
|
||||
@Deprecated("Temporary DSL for transition")
|
||||
fun companyDto(
|
||||
entity: Company
|
||||
) = CompanyDto(entity.id!!, entity.name)
|
||||
) : ModelEntity
|
|
@ -1,7 +1,6 @@
|
|||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto
|
||||
import javax.persistence.*
|
||||
|
||||
@Entity
|
||||
|
@ -28,34 +27,4 @@ data class Material(
|
|||
fun getSimdutFilePath(name: String) =
|
||||
"${Constants.FilePaths.SIMDUT}/$name.pdf"
|
||||
}
|
||||
}
|
||||
|
||||
// === DSL ===
|
||||
|
||||
fun material(
|
||||
id: Long? = null,
|
||||
name: String = "name",
|
||||
inventoryQuantity: Float = 0f,
|
||||
isMixType: Boolean = false,
|
||||
materialType: MaterialType? = materialType(),
|
||||
op: Material.() -> Unit = {}
|
||||
) = Material(id, name, inventoryQuantity, isMixType, materialType).apply(op)
|
||||
|
||||
fun material(
|
||||
material: Material,
|
||||
id: Long? = null,
|
||||
name: String? = null,
|
||||
) = Material(
|
||||
id ?: material.id, name
|
||||
?: material.name, material.inventoryQuantity, material.isMixType, material.materialType
|
||||
)
|
||||
|
||||
@Deprecated("Temporary DSL for transition")
|
||||
fun material(
|
||||
dto: MaterialDto
|
||||
) = Material(dto.id, dto.name, dto.inventoryQuantity, dto.isMixType, materialType(dto.materialType))
|
||||
|
||||
@Deprecated("Temporary DSL for transition")
|
||||
fun materialDto(
|
||||
entity: Material
|
||||
) = MaterialDto(entity.id!!, entity.name, entity.inventoryQuantity, entity.isMixType, materialTypeDto(entity.materialType!!))
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto
|
||||
import org.hibernate.annotations.ColumnDefault
|
||||
import javax.persistence.*
|
||||
|
||||
|
@ -24,39 +23,4 @@ data class MaterialType(
|
|||
@Column(name = "system_type")
|
||||
@ColumnDefault("false")
|
||||
val systemType: Boolean = false
|
||||
) : ModelEntity
|
||||
|
||||
// ==== DSL ====
|
||||
fun materialType(
|
||||
id: Long? = null,
|
||||
name: String = "name",
|
||||
prefix: String = "PRE",
|
||||
usePercentages: Boolean = false,
|
||||
systemType: Boolean = false,
|
||||
op: MaterialType.() -> Unit = {}
|
||||
) = MaterialType(id, name, prefix, usePercentages, systemType).apply(op)
|
||||
|
||||
fun materialType(
|
||||
materialType: MaterialType,
|
||||
newId: Long? = null,
|
||||
newName: String? = null,
|
||||
newSystemType: Boolean? = null
|
||||
) = with(materialType) {
|
||||
MaterialType(
|
||||
newId ?: id,
|
||||
newName ?: name,
|
||||
prefix,
|
||||
usePercentages,
|
||||
newSystemType ?: systemType
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("Temporary DSL for transition")
|
||||
fun materialType(
|
||||
dto: MaterialTypeDto
|
||||
) = MaterialType(dto.id, dto.name, dto.prefix, dto.usePercentages, dto.systemType)
|
||||
|
||||
@Deprecated("Temporary DSL for transition")
|
||||
fun materialTypeDto(
|
||||
entity: MaterialType
|
||||
) = MaterialTypeDto(entity.id!!, entity.name, entity.prefix, entity.usePercentages, entity.systemType)
|
||||
) : ModelEntity
|
|
@ -1,10 +1,5 @@
|
|||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
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.*
|
||||
|
||||
@Entity
|
||||
|
@ -16,9 +11,8 @@ data class Mix(
|
|||
|
||||
var location: String?,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "recipe_id")
|
||||
val recipe: Recipe,
|
||||
@Column(name = "recipe_id")
|
||||
val recipeId: Long,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "mix_type_id")
|
||||
|
@ -26,25 +20,5 @@ data class Mix(
|
|||
|
||||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
@JoinColumn(name = "mix_id")
|
||||
var mixMaterials: Set<MixMaterial>,
|
||||
) : ModelEntity
|
||||
|
||||
// ==== DSL ====
|
||||
fun mix(
|
||||
id: Long? = null,
|
||||
location: String? = "location",
|
||||
recipe: Recipe = recipe(),
|
||||
mixType: MixType = mixType(),
|
||||
mixMaterials: MutableSet<MixMaterial> = mutableSetOf(),
|
||||
op: Mix.() -> Unit = {}
|
||||
) = Mix(id, location, recipe, mixType, 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())
|
||||
|
||||
@Deprecated("Temporary DSL for transition")
|
||||
fun mix(
|
||||
entity: Mix
|
||||
) = MixDto(entity.id!!, entity.location, entity.recipe, mixTypeDto(entity.mixType), entity.mixMaterials.map(::mixMaterialDto).toSet())
|
||||
var mixMaterials: List<MixMaterial>,
|
||||
) : ModelEntity
|
|
@ -1,6 +1,5 @@
|
|||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto
|
||||
import javax.persistence.*
|
||||
|
||||
@Entity
|
||||
|
@ -17,23 +16,4 @@ data class MixMaterial(
|
|||
var quantity: Float,
|
||||
|
||||
var position: Int
|
||||
) : ModelEntity
|
||||
|
||||
// ==== DSL ====
|
||||
fun mixMaterial(
|
||||
id: Long? = null,
|
||||
material: Material = material(),
|
||||
quantity: Float = 0f,
|
||||
position: Int = 0,
|
||||
op: MixMaterial.() -> Unit = {}
|
||||
) = MixMaterial(id, material, quantity, position).apply(op)
|
||||
|
||||
@Deprecated("Temporary DSL for transition")
|
||||
fun mixMaterialDto(
|
||||
entity: MixMaterial
|
||||
) = MixMaterialDto(entity.id!!, materialDto(entity.material), entity.quantity, entity.position)
|
||||
|
||||
@Deprecated("Temporary DSL for transition")
|
||||
fun mixMaterial(
|
||||
dto: MixMaterialDto
|
||||
) = MixMaterial(dto.id, material(dto.material), dto.quantity, dto.position)
|
||||
) : ModelEntity
|
|
@ -1,11 +1,5 @@
|
|||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import org.springframework.http.HttpStatus
|
||||
import javax.persistence.*
|
||||
|
||||
@Entity
|
||||
|
@ -21,32 +15,4 @@ data class MixType(
|
|||
@OneToOne(cascade = [CascadeType.ALL])
|
||||
@JoinColumn(name = "material_id")
|
||||
var material: Material
|
||||
) : ModelEntity
|
||||
|
||||
// ==== DSL ====
|
||||
fun mixType(
|
||||
id: Long? = null,
|
||||
name: String = "name",
|
||||
material: Material = material(),
|
||||
op: MixType.() -> Unit = {}
|
||||
) = MixType(id, name, material).apply(op)
|
||||
|
||||
fun mixType(
|
||||
name: String = "name",
|
||||
materialType: MaterialType = materialType(),
|
||||
op: MixType.() -> Unit = {}
|
||||
) = mixType(
|
||||
id = null,
|
||||
name,
|
||||
material = material(name = name, inventoryQuantity = 0f, isMixType = true, materialType = materialType)
|
||||
).apply(op)
|
||||
|
||||
@Deprecated("Temporary DSL for transition")
|
||||
fun mixTypeDto(
|
||||
entity: MixType
|
||||
) = MixTypeDto(entity.id!!, entity.name, materialDto(entity.material))
|
||||
|
||||
@Deprecated("Temporary DSL for transition")
|
||||
fun mixType(
|
||||
dto: MixTypeDto
|
||||
) = MixType(dto.id, dto.name, material(dto.material))
|
||||
) : ModelEntity
|
|
@ -1,25 +1,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
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.group
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.LocalDate
|
||||
import javax.persistence.*
|
||||
import javax.validation.constraints.Max
|
||||
import javax.validation.constraints.Min
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.Pattern
|
||||
|
||||
private const val VALIDATION_COLOR_PATTERN = "^#([0-9a-f]{6})$"
|
||||
|
||||
const val RECIPE_IMAGES_DIRECTORY = "images/recipes"
|
||||
|
||||
@Entity
|
||||
@Table(name = "recipe")
|
||||
|
@ -51,110 +34,12 @@ data class Recipe(
|
|||
@JoinColumn(name = "company_id")
|
||||
val company: Company,
|
||||
|
||||
@OneToMany(cascade = [CascadeType.ALL], mappedBy = "recipe")
|
||||
val mixes: MutableList<Mix>,
|
||||
@OneToMany(cascade = [CascadeType.ALL], mappedBy = "recipeId")
|
||||
val mixes: List<Mix>,
|
||||
|
||||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
@OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true)
|
||||
@JoinColumn(name = "recipe_id")
|
||||
val groupsInformation: Set<RecipeGroupInformation>
|
||||
) : ModelEntity {
|
||||
/** The mix types contained in this recipe. */
|
||||
val mixTypes: Collection<MixType>
|
||||
@JsonIgnore
|
||||
get() = mixes.map { it.mixType }
|
||||
|
||||
val imagesDirectoryPath
|
||||
@JsonIgnore
|
||||
@Transient
|
||||
get() = "$RECIPE_IMAGES_DIRECTORY/$id"
|
||||
|
||||
fun groupInformationForGroup(groupId: Long) =
|
||||
groupsInformation.firstOrNull { it.group.id == groupId }
|
||||
|
||||
fun imageUrl(deploymentUrl: String, name: String) =
|
||||
"$deploymentUrl${Constants.ControllerPaths.FILE}?path=${
|
||||
URLEncoder.encode(
|
||||
"${this.imagesDirectoryPath}/$name",
|
||||
StandardCharsets.UTF_8
|
||||
)
|
||||
}"
|
||||
}
|
||||
|
||||
open class RecipeSaveDto(
|
||||
@field:NotBlank
|
||||
val name: String,
|
||||
|
||||
@field:NotBlank
|
||||
val description: String,
|
||||
|
||||
@field:NotBlank
|
||||
@field:Pattern(regexp = VALIDATION_COLOR_PATTERN)
|
||||
val color: String,
|
||||
|
||||
@field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS)
|
||||
@field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS)
|
||||
val gloss: Byte,
|
||||
|
||||
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
|
||||
val sample: Int?,
|
||||
|
||||
val approbationDate: LocalDate?,
|
||||
|
||||
val remark: String?,
|
||||
|
||||
val companyId: Long
|
||||
) : EntityDto<Recipe> {
|
||||
override fun toEntity(): Recipe = recipe(
|
||||
name = name,
|
||||
description = description,
|
||||
sample = sample,
|
||||
approbationDate = approbationDate,
|
||||
remark = remark ?: "",
|
||||
company = company(id = companyId)
|
||||
)
|
||||
}
|
||||
|
||||
open class RecipeUpdateDto(
|
||||
val id: Long,
|
||||
|
||||
@field:NotBlank
|
||||
val name: String?,
|
||||
|
||||
@field:NotBlank
|
||||
val description: String?,
|
||||
|
||||
@field:NotBlank
|
||||
@field:Pattern(regexp = VALIDATION_COLOR_PATTERN)
|
||||
val color: String?,
|
||||
|
||||
@field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS)
|
||||
@field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS)
|
||||
val gloss: Byte?,
|
||||
|
||||
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
|
||||
val sample: Int?,
|
||||
|
||||
val approbationDate: LocalDate?,
|
||||
|
||||
val remark: String?,
|
||||
|
||||
val steps: Set<RecipeStepsDto>?
|
||||
) : EntityDto<Recipe>
|
||||
|
||||
data class RecipeOutputDto(
|
||||
override val id: Long,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val color: String,
|
||||
val gloss: Byte,
|
||||
val sample: Int?,
|
||||
val approbationDate: LocalDate?,
|
||||
val approbationExpired: Boolean?,
|
||||
val remark: String?,
|
||||
val company: Company,
|
||||
val mixes: Set<MixDto>,
|
||||
val groupsInformation: Set<RecipeGroupInformation>,
|
||||
var imagesUrls: Set<String>
|
||||
val groupsInformation: List<RecipeGroupInformation>
|
||||
) : ModelEntity
|
||||
|
||||
@Entity
|
||||
|
@ -172,133 +57,5 @@ data class RecipeGroupInformation(
|
|||
|
||||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
@JoinColumn(name = "recipe_group_information_id")
|
||||
var steps: MutableSet<RecipeStep>?
|
||||
) : ModelEntity
|
||||
|
||||
data class RecipeStepsDto(
|
||||
val groupId: Long,
|
||||
|
||||
val steps: Set<RecipeStep>
|
||||
)
|
||||
|
||||
data class RecipePublicDataDto(
|
||||
val recipeId: Long,
|
||||
|
||||
val notes: Set<NoteDto>?,
|
||||
|
||||
val mixesLocation: Set<MixLocationDto>?
|
||||
)
|
||||
|
||||
data class NoteDto(
|
||||
val groupId: Long,
|
||||
|
||||
val content: String?
|
||||
)
|
||||
|
||||
// ==== DSL ====
|
||||
fun recipe(
|
||||
id: Long? = null,
|
||||
name: String = "name",
|
||||
description: String = "description",
|
||||
color: String = "ffffff",
|
||||
gloss: Byte = 0,
|
||||
sample: Int? = -1,
|
||||
approbationDate: LocalDate? = LocalDate.MIN,
|
||||
remark: String = "remark",
|
||||
company: Company = company(),
|
||||
mixes: MutableList<Mix> = mutableListOf(),
|
||||
groupsInformation: Set<RecipeGroupInformation> = setOf(),
|
||||
op: Recipe.() -> Unit = {}
|
||||
) = Recipe(
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
color,
|
||||
gloss,
|
||||
sample,
|
||||
approbationDate,
|
||||
remark,
|
||||
company,
|
||||
mixes,
|
||||
groupsInformation
|
||||
).apply(op)
|
||||
|
||||
fun recipeSaveDto(
|
||||
name: String = "name",
|
||||
description: String = "description",
|
||||
color: String = "ffffff",
|
||||
gloss: Byte = 0,
|
||||
sample: Int? = -1,
|
||||
approbationDate: LocalDate? = LocalDate.MIN,
|
||||
remark: String = "remark",
|
||||
companyId: Long = 0L,
|
||||
op: RecipeSaveDto.() -> Unit = {}
|
||||
) = RecipeSaveDto(name, description, color, gloss, sample, approbationDate, remark, companyId).apply(op)
|
||||
|
||||
fun recipeUpdateDto(
|
||||
id: Long = 0L,
|
||||
name: String? = "name",
|
||||
description: String? = "description",
|
||||
color: String? = "ffffff",
|
||||
gloss: Byte? = 0,
|
||||
sample: Int? = -1,
|
||||
approbationDate: LocalDate? = LocalDate.MIN,
|
||||
remark: String? = "remark",
|
||||
steps: Set<RecipeStepsDto>? = setOf(),
|
||||
op: RecipeUpdateDto.() -> Unit = {}
|
||||
) = RecipeUpdateDto(id, name, description, color, gloss, sample, approbationDate, remark, steps).apply(op)
|
||||
|
||||
fun recipeGroupInformation(
|
||||
id: Long? = null,
|
||||
group: Group = group(),
|
||||
note: String? = null,
|
||||
steps: MutableSet<RecipeStep>? = mutableSetOf(),
|
||||
op: RecipeGroupInformation.() -> Unit = {}
|
||||
) = RecipeGroupInformation(id, group, note, steps).apply(op)
|
||||
|
||||
fun recipePublicDataDto(
|
||||
recipeId: Long = 0L,
|
||||
notes: Set<NoteDto>? = null,
|
||||
mixesLocation: Set<MixLocationDto>? = null,
|
||||
op: RecipePublicDataDto.() -> Unit = {}
|
||||
) = RecipePublicDataDto(recipeId, notes, mixesLocation).apply(op)
|
||||
|
||||
fun noteDto(
|
||||
groupId: Long = 0L,
|
||||
content: String? = "note",
|
||||
op: NoteDto.() -> Unit = {}
|
||||
) = NoteDto(groupId, content).apply(op)
|
||||
|
||||
// ==== Exceptions ====
|
||||
private const val RECIPE_NOT_FOUND_EXCEPTION_TITLE = "Recipe not found"
|
||||
private const val RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Recipe already exists"
|
||||
private const val RECIPE_EXCEPTION_ERROR_CODE = "recipe"
|
||||
|
||||
fun recipeIdNotFoundException(id: Long) =
|
||||
NotFoundException(
|
||||
RECIPE_EXCEPTION_ERROR_CODE,
|
||||
RECIPE_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A recipe with the id $id could not be found",
|
||||
id
|
||||
)
|
||||
|
||||
fun recipeIdAlreadyExistsException(id: Long) =
|
||||
AlreadyExistsException(
|
||||
RECIPE_EXCEPTION_ERROR_CODE,
|
||||
RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A recipe with the id $id already exists",
|
||||
id
|
||||
)
|
||||
|
||||
fun recipeNameAlreadyExistsForCompanyException(name: String, company: Company) =
|
||||
AlreadyExistsException(
|
||||
"${RECIPE_EXCEPTION_ERROR_CODE}-company",
|
||||
RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A recipe with the name $name already exists for the company ${company.name}",
|
||||
name,
|
||||
"name",
|
||||
mutableMapOf(
|
||||
"company" to company.name,
|
||||
"companyId" to company.id!!
|
||||
)
|
||||
)
|
||||
var steps: List<RecipeStep>?
|
||||
) : ModelEntity
|
|
@ -1,6 +1,5 @@
|
|||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto
|
||||
import javax.persistence.*
|
||||
|
||||
@Entity
|
||||
|
@ -13,9 +12,4 @@ data class RecipeStep(
|
|||
val position: Int,
|
||||
|
||||
val message: String
|
||||
) : ModelEntity
|
||||
|
||||
@Deprecated("Temporary DSL for transition")
|
||||
fun recipeStepDto(
|
||||
entity: RecipeStep
|
||||
) = RecipeStepDto(entity.id!!, entity.position, entity.message)
|
||||
) : ModelEntity
|
|
@ -12,6 +12,6 @@ interface MixRepository : JpaRepository<Mix, Long> {
|
|||
|
||||
/** 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?)
|
||||
}
|
||||
|
|
|
@ -8,11 +8,16 @@ import org.springframework.stereotype.Repository
|
|||
@Repository
|
||||
interface MixTypeRepository : JpaRepository<MixType, Long> {
|
||||
/** Checks if a mix type with the given [name], [materialTypeId] and a different [id] exists. */
|
||||
@Query("select case when(count(m) > 0) then true else false end from MixType m where m.name = :name and m.material.materialType.id = :materialTypeId and m.id <> :id")
|
||||
@Query(
|
||||
"""
|
||||
SELECT CASE WHEN(COUNT(m) > 0) THEN TRUE ELSE FALSE END
|
||||
FROM MixType m WHERE m.name = :name AND m.material.materialType.id = :materialTypeId AND m.id <> :id
|
||||
"""
|
||||
)
|
||||
fun existsByNameAndMaterialType(name: String, materialTypeId: Long, id: Long): Boolean
|
||||
|
||||
/** Finds the mix type with the given [name] and [materialTypeId]. */
|
||||
@Query("select m from MixType m where m.name = :name and m.material.materialType.id = :materialTypeId")
|
||||
@Query("SELECT m FROM MixType m WHERE m.name = :name AND m.material.materialType.id = :materialTypeId")
|
||||
fun findByNameAndMaterialType(name: String, materialTypeId: Long): MixType?
|
||||
|
||||
/** Checks if a mix depends on the mix type with the given [id]. */
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
package dev.fyloz.colorrecipesexplorer.repository
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.Company
|
||||
import dev.fyloz.colorrecipesexplorer.model.Recipe
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
|
||||
interface RecipeRepository : JpaRepository<Recipe, Long> {
|
||||
/** Checks if one or more recipes have the given [company]. */
|
||||
fun existsByCompany(company: Company): Boolean
|
||||
|
||||
/** Checks if a recipe exists with the given [name] and [company]. */
|
||||
fun existsByNameAndCompany(name: String, company: Company): Boolean
|
||||
/** Checks if a recipe with the given [name], [companyId] and a different [id] exists. */
|
||||
@Query(
|
||||
"""
|
||||
SELECT CASE WHEN(COUNT(r) > 0) THEN TRUE ELSE FALSE END
|
||||
FROM Recipe r WHERE r.name = :name AND r.company.id = :companyId AND r.id <> :id
|
||||
"""
|
||||
)
|
||||
fun existsByNameAndCompanyAndIdNot(name: String, companyId: Long, id: Long): Boolean
|
||||
|
||||
/** Gets all recipes with the given [name]. */
|
||||
fun findAllByName(name: String): Collection<Recipe>
|
||||
|
||||
/** Gets all recipes with the given [company]. */
|
||||
fun findAllByCompany(company: Company): Collection<Recipe>
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
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.logic.MixLogic
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipePublicDataDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeSaveDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeUpdateDto
|
||||
import dev.fyloz.colorrecipesexplorer.logic.RecipeImageLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.RecipeLogic
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import org.springframework.context.annotation.Profile
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
|
@ -14,71 +17,63 @@ import org.springframework.web.bind.annotation.*
|
|||
import org.springframework.web.multipart.MultipartFile
|
||||
import javax.validation.Valid
|
||||
|
||||
|
||||
private const val RECIPE_CONTROLLER_PATH = "api/recipe"
|
||||
|
||||
@RestController
|
||||
@RequestMapping(RECIPE_CONTROLLER_PATH)
|
||||
@RequestMapping(Constants.ControllerPaths.RECIPE)
|
||||
@Profile("!emergency")
|
||||
@PreAuthorizeViewRecipes
|
||||
class RecipeController(
|
||||
private val recipeLogic: RecipeLogic,
|
||||
private val recipeImageLogic: RecipeImageLogic
|
||||
) {
|
||||
class RecipeController(private val recipeLogic: RecipeLogic, private val recipeImageLogic: RecipeImageLogic) {
|
||||
@GetMapping
|
||||
fun getAll(@RequestParam(required = false) name: String?) =
|
||||
if (name == null)
|
||||
ok(recipeLogic.getAllForOutput())
|
||||
else
|
||||
ok(with(recipeLogic) {
|
||||
getAllByName(name).map { it.toOutput() }
|
||||
})
|
||||
fun getAll(@RequestParam(required = false) name: String?) = ok(
|
||||
if (name == null) {
|
||||
recipeLogic.getAll()
|
||||
} else {
|
||||
recipeLogic.getAllByName(name)
|
||||
}
|
||||
)
|
||||
|
||||
@GetMapping("{id}")
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(recipeLogic.getByIdForOutput(id))
|
||||
fun getById(@PathVariable id: Long) = ok(recipeLogic.getById(id))
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorizeEditRecipes
|
||||
fun save(@Valid @RequestBody recipe: RecipeSaveDto) =
|
||||
created<RecipeOutputDto>(RECIPE_CONTROLLER_PATH) {
|
||||
with(recipeLogic) {
|
||||
save(recipe).toOutput()
|
||||
}
|
||||
created<RecipeDto>(Constants.ControllerPaths.RECIPE) {
|
||||
recipeLogic.save(recipe)
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorizeEditRecipes
|
||||
fun update(@Valid @RequestBody recipe: RecipeUpdateDto) =
|
||||
noContent {
|
||||
recipeLogic.update(recipe)
|
||||
}
|
||||
fun update(@Valid @RequestBody recipe: RecipeUpdateDto) = noContent {
|
||||
recipeLogic.update(recipe)
|
||||
}
|
||||
|
||||
@PutMapping("public")
|
||||
@PreAuthorize("hasAuthority('EDIT_RECIPES_PUBLIC_DATA')")
|
||||
fun updatePublicData(@Valid @RequestBody publicDataDto: RecipePublicDataDto) =
|
||||
noContent {
|
||||
recipeLogic.updatePublicData(publicDataDto)
|
||||
}
|
||||
fun updatePublicData(@Valid @RequestBody publicDataDto: RecipePublicDataDto) = noContent {
|
||||
recipeLogic.updatePublicData(publicDataDto)
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
@PreAuthorizeEditRecipes
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
noContent {
|
||||
recipeLogic.deleteById(id)
|
||||
}
|
||||
fun deleteById(@PathVariable id: Long) = noContent {
|
||||
recipeLogic.deleteById(id)
|
||||
}
|
||||
|
||||
@GetMapping("{recipeId}/image")
|
||||
fun getAllImages(@PathVariable recipeId: Long) = ok {
|
||||
recipeImageLogic.getAllImages(recipeId)
|
||||
}
|
||||
|
||||
@PutMapping("{recipeId}/image", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||
@PreAuthorizeEditRecipes
|
||||
fun downloadImage(@PathVariable recipeId: Long, image: MultipartFile): ResponseEntity<RecipeOutputDto> {
|
||||
recipeImageLogic.download(image, recipeLogic.getById(recipeId))
|
||||
fun downloadImage(@PathVariable recipeId: Long, image: MultipartFile): ResponseEntity<RecipeDto> {
|
||||
recipeImageLogic.download(image, recipeId)
|
||||
return getById(recipeId)
|
||||
}
|
||||
|
||||
@DeleteMapping("{recipeId}/image/{name}")
|
||||
@DeleteMapping("{recipeId}/image/{path}")
|
||||
@PreAuthorizeEditRecipes
|
||||
fun deleteImage(@PathVariable recipeId: Long, @PathVariable name: String) =
|
||||
noContent {
|
||||
recipeImageLogic.delete(recipeLogic.getById(recipeId), name)
|
||||
}
|
||||
fun deleteImage(@PathVariable recipeId: Long, @PathVariable path: String) = noContent {
|
||||
recipeImageLogic.delete(recipeId, path)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,17 +26,17 @@ class DefaultMixService(
|
|||
MixDto(
|
||||
entity.id!!,
|
||||
entity.location,
|
||||
entity.recipe,
|
||||
entity.recipeId,
|
||||
mixTypeService.toDto(entity.mixType),
|
||||
entity.mixMaterials.map(mixMaterialService::toDto).toSet()
|
||||
entity.mixMaterials.map(mixMaterialService::toDto)
|
||||
)
|
||||
|
||||
override fun toEntity(dto: MixDto) =
|
||||
Mix(
|
||||
dto.id,
|
||||
dto.location,
|
||||
dto.recipe,
|
||||
dto.recipeId,
|
||||
mixTypeService.toEntity(dto.mixType),
|
||||
dto.mixMaterials.map(mixMaterialService::toEntity).toSet()
|
||||
dto.mixMaterials.map(mixMaterialService::toEntity)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto
|
||||
import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic
|
||||
import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
|
||||
import dev.fyloz.colorrecipesexplorer.model.Recipe
|
||||
import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation
|
||||
import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository
|
||||
import dev.fyloz.colorrecipesexplorer.utils.collections.lazyMap
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.LocalDate
|
||||
import java.time.Period
|
||||
|
||||
interface RecipeService : Service<RecipeDto, Recipe, RecipeRepository> {
|
||||
/** Checks if a recipe with the given [name], [companyId] and a different [id] exists. */
|
||||
fun existsByNameAndCompany(name: String, companyId: Long, id: Long?): Boolean
|
||||
|
||||
/** Gets all recipes with the given [name]. */
|
||||
fun getAllByName(name: String): Collection<RecipeDto>
|
||||
}
|
||||
|
||||
@ServiceComponent
|
||||
class DefaultRecipeService(
|
||||
repository: RecipeRepository,
|
||||
private val companyService: CompanyService,
|
||||
private val mixService: MixService,
|
||||
private val recipeStepService: RecipeStepService,
|
||||
private val configLogic: ConfigurationLogic
|
||||
) :
|
||||
BaseService<RecipeDto, Recipe, RecipeRepository>(repository), RecipeService {
|
||||
override fun existsByNameAndCompany(name: String, companyId: Long, id: Long?) =
|
||||
repository.existsByNameAndCompanyAndIdNot(name, companyId, id ?: 0L)
|
||||
|
||||
override fun getAllByName(name: String) =
|
||||
repository.findAllByName(name).map(::toDto)
|
||||
|
||||
@Transactional
|
||||
override fun toDto(entity: Recipe) =
|
||||
RecipeDto(
|
||||
entity.id!!,
|
||||
entity.name,
|
||||
entity.description,
|
||||
entity.color,
|
||||
entity.gloss,
|
||||
entity.sample,
|
||||
entity.approbationDate,
|
||||
isApprobationExpired(entity) ?: false,
|
||||
entity.remark,
|
||||
companyService.toDto(entity.company),
|
||||
entity.mixes.lazyMap(mixService::toDto),
|
||||
entity.groupsInformation.lazyMap(::groupInformationToDto)
|
||||
)
|
||||
|
||||
private fun groupInformationToDto(entity: RecipeGroupInformation) =
|
||||
RecipeGroupInformationDto(
|
||||
entity.id!!,
|
||||
entity.group,
|
||||
entity.note,
|
||||
entity.steps?.lazyMap(recipeStepService::toDto) ?: listOf()
|
||||
)
|
||||
|
||||
override fun toEntity(dto: RecipeDto) =
|
||||
Recipe(
|
||||
dto.id,
|
||||
dto.name,
|
||||
dto.description,
|
||||
dto.color,
|
||||
dto.gloss,
|
||||
dto.sample,
|
||||
dto.approbationDate,
|
||||
dto.remark,
|
||||
companyService.toEntity(dto.company),
|
||||
dto.mixes.map(mixService::toEntity),
|
||||
dto.groupsInformation.map(::groupInformationToEntity)
|
||||
)
|
||||
|
||||
private fun groupInformationToEntity(dto: RecipeGroupInformationDto) =
|
||||
RecipeGroupInformation(dto.id, dto.group, dto.note, dto.steps.map(recipeStepService::toEntity))
|
||||
|
||||
private fun isApprobationExpired(recipe: Recipe): Boolean? =
|
||||
with(Period.parse(configLogic.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION))) {
|
||||
recipe.approbationDate?.plus(this)?.isBefore(LocalDate.now())
|
||||
}
|
||||
}
|
|
@ -60,4 +60,4 @@ 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)
|
|
@ -0,0 +1,26 @@
|
|||
package dev.fyloz.colorrecipesexplorer.utils.collections
|
||||
|
||||
class LazyMapList<T, R>(private val sourceList: List<T>, private val transform: (T) -> R) : List<R> {
|
||||
private val list by lazy { sourceList.map(transform) }
|
||||
|
||||
fun initialize() {
|
||||
// Call a property so the list is initialized
|
||||
size
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = list.size
|
||||
|
||||
override fun contains(element: R) = list.contains(element)
|
||||
override fun containsAll(elements: Collection<R>) = list.containsAll(elements)
|
||||
override fun get(index: Int) = list[index]
|
||||
override fun indexOf(element: R) = list.indexOf(element)
|
||||
override fun isEmpty() = list.isEmpty()
|
||||
override fun iterator() = list.iterator()
|
||||
override fun lastIndexOf(element: R) = list.lastIndexOf(element)
|
||||
override fun listIterator() = list.listIterator()
|
||||
override fun listIterator(index: Int) = list.listIterator(index)
|
||||
override fun subList(fromIndex: Int, toIndex: Int) = list.subList(fromIndex, toIndex)
|
||||
}
|
||||
|
||||
fun <T, R> List<T>.lazyMap(transform: (T) -> R) = LazyMapList(this, transform)
|
|
@ -1,8 +1,6 @@
|
|||
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
|
||||
|
@ -87,7 +85,7 @@ class DefaultInventoryLogicTest {
|
|||
fun deductMix_normalBehavior_callsDeductWithMixMaterials() {
|
||||
// Arrange
|
||||
val company = CompanyDto(1L, "Unit test company")
|
||||
val recipe = Recipe(
|
||||
val recipe = RecipeDto(
|
||||
1L,
|
||||
"Unit test recipe",
|
||||
"Unit test recipe",
|
||||
|
@ -95,14 +93,15 @@ class DefaultInventoryLogicTest {
|
|||
0xf,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
"Remark",
|
||||
company(company),
|
||||
company,
|
||||
mutableListOf(),
|
||||
setOf()
|
||||
listOf()
|
||||
)
|
||||
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 mix = MixDto(1L, null, recipe.id, mixType, listOf(mixMaterial))
|
||||
|
||||
val dto = MixDeductDto(mix.id, 2f)
|
||||
val expectedQuantities = listOf(MaterialQuantityDto(material.id, mixMaterial.quantity * dto.ratio))
|
||||
|
@ -123,7 +122,7 @@ class DefaultInventoryLogicTest {
|
|||
fun deductMix_normalBehavior_returnsFromDeduct() {
|
||||
// Arrange
|
||||
val company = CompanyDto(1L, "Unit test company")
|
||||
val recipe = Recipe(
|
||||
val recipe = RecipeDto(
|
||||
1L,
|
||||
"Unit test recipe",
|
||||
"Unit test recipe",
|
||||
|
@ -131,14 +130,15 @@ class DefaultInventoryLogicTest {
|
|||
0xf,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
"Remark",
|
||||
company(company),
|
||||
company,
|
||||
mutableListOf(),
|
||||
setOf()
|
||||
listOf()
|
||||
)
|
||||
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 mix = MixDto(1L, null, recipe.id, mixType, listOf(mixMaterial))
|
||||
|
||||
val dto = MixDeductDto(mix.id, 2f)
|
||||
val expectedQuantities = listOf(MaterialQuantityDto(material.id, mixMaterial.quantity * dto.ratio))
|
||||
|
|
|
@ -4,10 +4,7 @@ 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.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
|
||||
|
@ -37,8 +34,8 @@ class DefaultMaterialLogicTest {
|
|||
private val material = MaterialDto(1L, "Unit test material", 1000f, false, materialType)
|
||||
private val materialMixType = material.copy(id = 2L, isMixType = true)
|
||||
private val materialMixType2 = material.copy(id = 3L, isMixType = true)
|
||||
private val company = Company(1L, "Unit test company")
|
||||
private val recipe = Recipe(
|
||||
private val company = CompanyDto(1L, "Unit test company")
|
||||
private val recipe = RecipeDto(
|
||||
1L,
|
||||
"Unit test recipe",
|
||||
"Unit test recipe",
|
||||
|
@ -46,13 +43,14 @@ class DefaultMaterialLogicTest {
|
|||
0,
|
||||
123,
|
||||
null,
|
||||
false,
|
||||
"A remark",
|
||||
company,
|
||||
mutableListOf(),
|
||||
setOf()
|
||||
listOf()
|
||||
)
|
||||
private val mix = MixDto(
|
||||
1L, "location", recipe, mixType = MixTypeDto(1L, "Unit test mix type", materialMixType), mutableSetOf()
|
||||
1L, "location", recipe.id, mixType = MixTypeDto(1L, "Unit test mix type", materialMixType), listOf()
|
||||
)
|
||||
private val mix2 = mix.copy(id = 2L, mixType = mix.mixType.copy(id = 2L, material = materialMixType2))
|
||||
|
||||
|
@ -62,10 +60,6 @@ class DefaultMaterialLogicTest {
|
|||
) // Put some content in the mock file, so it is not ignored
|
||||
private val materialSaveDto = MaterialSaveDto(1L, "Unit test material", 1000f, materialType.id, simdutFileMock)
|
||||
|
||||
init {
|
||||
recipe.mixes.addAll(listOf(mix(mix), mix(mix2)))
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
internal fun afterEach() {
|
||||
clearAllMocks()
|
||||
|
@ -114,7 +108,7 @@ class DefaultMaterialLogicTest {
|
|||
every { recipeLogicMock.getById(any()) } returns recipe
|
||||
|
||||
// Act
|
||||
val materials = materialLogic.getAllForMixCreation(recipe.id!!)
|
||||
val materials = materialLogic.getAllForMixCreation(recipe.id)
|
||||
|
||||
// Assert
|
||||
assertContains(materials, material)
|
||||
|
@ -123,11 +117,13 @@ class DefaultMaterialLogicTest {
|
|||
@Test
|
||||
fun getAllForMixCreation_normalBehavior_returnsRecipeMixTypesMaterials() {
|
||||
// Arrange
|
||||
val recipe = recipe.copy(mixes = listOf(mix, mix2))
|
||||
|
||||
every { materialLogic.getAll() } returns listOf(material, materialMixType2)
|
||||
every { recipeLogicMock.getById(any()) } returns recipe
|
||||
|
||||
// Act
|
||||
val materials = materialLogic.getAllForMixCreation(recipe.id!!)
|
||||
val materials = materialLogic.getAllForMixCreation(recipe.id)
|
||||
|
||||
// Assert
|
||||
assertContains(materials, materialMixType2)
|
||||
|
@ -137,6 +133,7 @@ class DefaultMaterialLogicTest {
|
|||
fun getAllForMixUpdate_normalBehavior_returnsNonMixTypeMaterials() {
|
||||
// Arrange
|
||||
every { materialLogic.getAll() } returns listOf(material, materialMixType, materialMixType2)
|
||||
every { recipeLogicMock.getById(any()) } returns recipe
|
||||
every { mixLogicMock.getById(any()) } returns mix
|
||||
|
||||
// Act
|
||||
|
@ -149,7 +146,11 @@ class DefaultMaterialLogicTest {
|
|||
@Test
|
||||
fun getAllForMixUpdate_normalBehavior_returnsRecipeMixTypesMaterials() {
|
||||
// Arrange
|
||||
val recipe = recipe.copy(mixes = listOf(mix, mix2))
|
||||
val mix = mix.copy(recipeId = recipe.id)
|
||||
|
||||
every { materialLogic.getAll() } returns listOf(material, materialMixType, materialMixType2)
|
||||
every { recipeLogicMock.getById(any()) } returns recipe
|
||||
every { mixLogicMock.getById(any()) } returns mix
|
||||
|
||||
// Act
|
||||
|
@ -163,10 +164,11 @@ class DefaultMaterialLogicTest {
|
|||
fun getAllForMixUpdate_normalBehavior_excludesGivenMixTypeMaterial() {
|
||||
// Arrange
|
||||
every { materialLogic.getAll() } returns listOf(material, materialMixType, materialMixType2)
|
||||
every { recipeLogicMock.getById(any()) } returns recipe
|
||||
every { mixLogicMock.getById(any()) } returns mix
|
||||
|
||||
// Act
|
||||
val materials = materialLogic.getAllForMixUpdate(mix.id!!)
|
||||
val materials = materialLogic.getAllForMixUpdate(mix.id)
|
||||
|
||||
// Assert
|
||||
assertFalse { materialMixType in materials }
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
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
|
||||
|
@ -25,8 +23,8 @@ class DefaultMixLogicTest {
|
|||
)
|
||||
)
|
||||
|
||||
private val company = Company(1L, "Unit test company")
|
||||
private val recipe = Recipe(
|
||||
private val company = CompanyDto(1L, "Unit test company")
|
||||
private val recipe = RecipeDto(
|
||||
1L,
|
||||
"Unit test recipe",
|
||||
"Unit test recipe",
|
||||
|
@ -34,17 +32,18 @@ class DefaultMixLogicTest {
|
|||
0xf,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
"A remark",
|
||||
company,
|
||||
mutableListOf(),
|
||||
setOf()
|
||||
listOf()
|
||||
)
|
||||
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))
|
||||
private val mix = MixDto(recipeId = recipe.id, mixType = mixType, mixMaterials = listOf(mixMaterial))
|
||||
|
||||
@AfterEach
|
||||
internal fun afterEach() {
|
||||
|
@ -56,7 +55,6 @@ class DefaultMixLogicTest {
|
|||
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
|
||||
}
|
||||
|
||||
|
@ -76,7 +74,7 @@ class DefaultMixLogicTest {
|
|||
|
||||
val mixMaterialDto =
|
||||
MixMaterialSaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position)
|
||||
val saveDto = MixSaveDto(0L, mixType.name, recipe.id!!, materialType.id, setOf(mixMaterialDto))
|
||||
val saveDto = MixSaveDto(0L, mixType.name, recipe.id, materialType.id, listOf(mixMaterialDto))
|
||||
|
||||
// Act
|
||||
mixLogic.save(saveDto)
|
||||
|
@ -93,7 +91,7 @@ class DefaultMixLogicTest {
|
|||
setup_save_normalBehavior()
|
||||
|
||||
val mixMaterialDtos =
|
||||
setOf(
|
||||
listOf(
|
||||
MixMaterialSaveDto(
|
||||
mixMaterial.id,
|
||||
mixMaterial.material.id,
|
||||
|
@ -101,7 +99,7 @@ class DefaultMixLogicTest {
|
|||
mixMaterial.position
|
||||
)
|
||||
)
|
||||
val saveDto = MixSaveDto(0L, mixType.name, recipe.id!!, materialType.id, mixMaterialDtos)
|
||||
val saveDto = MixSaveDto(0L, mixType.name, recipe.id, materialType.id, mixMaterialDtos)
|
||||
|
||||
// Act
|
||||
mixLogic.save(saveDto)
|
||||
|
@ -120,7 +118,7 @@ class DefaultMixLogicTest {
|
|||
|
||||
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))
|
||||
val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id, materialType.id, listOf(mixMaterialDto))
|
||||
|
||||
// Act
|
||||
mixLogic.update(saveDto)
|
||||
|
@ -136,7 +134,7 @@ class DefaultMixLogicTest {
|
|||
// Arrange
|
||||
setup_update_normalBehavior()
|
||||
|
||||
val mixMaterialDtos = setOf(
|
||||
val mixMaterialDtos = listOf(
|
||||
MixMaterialSaveDto(
|
||||
mixMaterial.id,
|
||||
mixMaterial.material.id,
|
||||
|
@ -144,7 +142,7 @@ class DefaultMixLogicTest {
|
|||
mixMaterial.position
|
||||
)
|
||||
)
|
||||
val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id!!, materialType.id, mixMaterialDtos)
|
||||
val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id, materialType.id, mixMaterialDtos)
|
||||
|
||||
// Act
|
||||
mixLogic.update(saveDto)
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.Constants
|
||||
import dev.fyloz.colorrecipesexplorer.logic.files.CachedFile
|
||||
import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic
|
||||
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.mock.web.MockMultipartFile
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class DefaultRecipeImageLogicTest {
|
||||
private val fileLogicMock = mockk<WriteableFileLogic>()
|
||||
|
||||
private val recipeImageLogic = spyk(DefaultRecipeImageLogic(fileLogicMock))
|
||||
|
||||
private val recipeId = 1L
|
||||
|
||||
@AfterEach
|
||||
internal fun afterEach() {
|
||||
clearAllMocks()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getAllImages_normalBehavior_returnsAllRecipeImages() {
|
||||
// Arrange
|
||||
val filePath = FilePath("${Constants.FilePaths.RECIPE_IMAGES}/$recipeId")
|
||||
val files = listOf(
|
||||
CachedFile("0", filePath, true),
|
||||
CachedFile("1", filePath, true)
|
||||
)
|
||||
val expectedImages = files.map { it.name }
|
||||
|
||||
every { fileLogicMock.listDirectoryFiles(any()) } returns files
|
||||
|
||||
// Act
|
||||
val actualImages = recipeImageLogic.getAllImages(recipeId)
|
||||
|
||||
// Assert
|
||||
assertEquals(expectedImages, actualImages)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun download_normalBehavior_callsWriteToDirectoryInFileLogic() {
|
||||
// Arrange
|
||||
val previousImageId = 0L
|
||||
|
||||
every { recipeImageLogic.getAllImages(recipeId) } returns listOf(previousImageId.toString())
|
||||
every { fileLogicMock.writeToDirectory(any(), any(), any(), any()) } just runs
|
||||
|
||||
val file = MockMultipartFile("Unit test name", byteArrayOf())
|
||||
|
||||
val expectedFilePath = "${Constants.FilePaths.RECIPE_IMAGES}/$recipeId"
|
||||
val expectedImageId = previousImageId + 1
|
||||
|
||||
// Act
|
||||
recipeImageLogic.download(file, recipeId)
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
fileLogicMock.writeToDirectory(file, "$expectedFilePath/$expectedImageId", expectedFilePath, true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun download_normalBehavior_returnsImageId() {
|
||||
// Arrange
|
||||
val previousImageId = 0L
|
||||
|
||||
every { recipeImageLogic.getAllImages(recipeId) } returns listOf(previousImageId.toString())
|
||||
every { fileLogicMock.writeToDirectory(any(), any(), any(), any()) } just runs
|
||||
|
||||
val file = MockMultipartFile("Unit test name", byteArrayOf())
|
||||
|
||||
val expectedImageId = previousImageId + 1
|
||||
|
||||
// Act
|
||||
val downloadedImageId = recipeImageLogic.download(file, recipeId)
|
||||
|
||||
// Assert
|
||||
assertEquals(expectedImageId.toString(), downloadedImageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun delete_normalBehavior_callsDeleteFromDirectoryInFileLogic() {
|
||||
// Arrange
|
||||
every { fileLogicMock.deleteFromDirectory(any(), any()) } just runs
|
||||
|
||||
val recipeImagesDirectoryPath = "${Constants.FilePaths.RECIPE_IMAGES}/$recipeId"
|
||||
val imagePath = "$recipeImagesDirectoryPath/1"
|
||||
|
||||
// Act
|
||||
recipeImageLogic.delete(recipeId, imagePath)
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
fileLogicMock.deleteFromDirectory(imagePath, recipeImagesDirectoryPath)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.*
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Group
|
||||
import dev.fyloz.colorrecipesexplorer.service.RecipeService
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import kotlin.test.assertContains
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class DefaultRecipeLogicTest {
|
||||
private val recipeServiceMock = mockk<RecipeService>()
|
||||
private val companyLogicMock = mockk<CompanyLogic>()
|
||||
private val recipeStepLogicMock = mockk<RecipeStepLogic>()
|
||||
private val mixLogicMock = mockk<MixLogic>()
|
||||
private val groupLogicMock = mockk<GroupLogic>()
|
||||
|
||||
private val recipeLogic =
|
||||
spyk(DefaultRecipeLogic(recipeServiceMock, companyLogicMock, recipeStepLogicMock, mixLogicMock, groupLogicMock))
|
||||
|
||||
private val company = CompanyDto(1L, "Unit test company")
|
||||
private val group = Group(1L, "Unit test group")
|
||||
private val recipe = RecipeDto(
|
||||
1L,
|
||||
"Unit test recipe",
|
||||
"Unit test recipe",
|
||||
"FFFFFF",
|
||||
0xf,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
"Remark",
|
||||
company,
|
||||
listOf(),
|
||||
listOf()
|
||||
)
|
||||
|
||||
@AfterEach
|
||||
internal fun afterEach() {
|
||||
clearAllMocks()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getAllByName_normalBehavior_returnsFromService() {
|
||||
// Arrange
|
||||
val expectedRecipes = listOf(recipe)
|
||||
|
||||
every { recipeServiceMock.getAllByName(any()) } returns expectedRecipes
|
||||
|
||||
// Act
|
||||
val actualRecipes = recipeLogic.getAllByName(recipe.name)
|
||||
|
||||
// Assert
|
||||
assertEquals(actualRecipes, expectedRecipes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun save_dto_normalBehavior_returnsFromSave() {
|
||||
// Arrange
|
||||
every { recipeServiceMock.existsByNameAndCompany(any(), any(), any()) } returns false
|
||||
every { companyLogicMock.getById(any()) } returns company
|
||||
every { recipeLogic.save(any<RecipeDto>()) } returns recipe
|
||||
|
||||
val dto = RecipeSaveDto(
|
||||
recipe.name,
|
||||
recipe.description,
|
||||
recipe.color,
|
||||
recipe.gloss,
|
||||
recipe.sample,
|
||||
recipe.approbationDate,
|
||||
recipe.remark,
|
||||
company.id
|
||||
)
|
||||
|
||||
// Act
|
||||
val savedRecipe = recipeLogic.save(dto)
|
||||
|
||||
// Assert
|
||||
assertEquals(recipe, savedRecipe)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun save_nameAndCompanyExists_throwsAlreadyExistsException() {
|
||||
// Arrange
|
||||
every { recipeServiceMock.existsByNameAndCompany(any(), any(), any()) } returns true
|
||||
|
||||
// Act
|
||||
// Assert
|
||||
assertThrows<AlreadyExistsException> { recipeLogic.save(recipe) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun update_dto_normalBehavior_returnsFromSave() {
|
||||
// Arrange
|
||||
every { recipeServiceMock.existsByNameAndCompany(any(), any(), any()) } returns false
|
||||
every { recipeServiceMock.getById(any()) } returns recipe
|
||||
every { companyLogicMock.getById(any()) } returns company
|
||||
every { recipeLogic.update(any<RecipeDto>()) } returns recipe
|
||||
|
||||
val dto = RecipeUpdateDto(
|
||||
recipe.id,
|
||||
recipe.name,
|
||||
recipe.description,
|
||||
recipe.color,
|
||||
recipe.gloss,
|
||||
recipe.sample,
|
||||
recipe.approbationDate,
|
||||
recipe.remark,
|
||||
listOf()
|
||||
)
|
||||
|
||||
// Act
|
||||
val updatedRecipe = recipeLogic.update(dto)
|
||||
|
||||
// Assert
|
||||
assertEquals(recipe, updatedRecipe)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun update_nameAndCompanyExists_throwsAlreadyExistsException() {
|
||||
// Arrange
|
||||
every { recipeServiceMock.existsByNameAndCompany(any(), any(), any()) } returns true
|
||||
|
||||
// Act
|
||||
// Assert
|
||||
assertThrows<AlreadyExistsException> { recipeLogic.update(recipe) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updatePublicData_normalBehavior_callsUpdate() {
|
||||
// Arrange
|
||||
every { recipeLogic.getById(any()) } returns recipe
|
||||
every { recipeLogic.update(any<RecipeDto>()) } returnsArgument 0
|
||||
every { groupLogicMock.getById(any()) } returns group
|
||||
|
||||
val groupNote = RecipeGroupNoteDto(1L, "Unit test note")
|
||||
val dto = RecipePublicDataDto(recipe.id, listOf(groupNote), listOf())
|
||||
|
||||
// Act
|
||||
recipeLogic.updatePublicData(dto)
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
recipeLogic.update(any<RecipeDto>())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updatePublicData_normalBehavior_updatesRecipeGroupsInformation() {
|
||||
// Arrange
|
||||
var updatedRecipe = recipe
|
||||
|
||||
every { recipeLogic.getById(any()) } returns recipe
|
||||
every { recipeLogic.update(any<RecipeDto>()) } answers { firstArg<RecipeDto>().also { updatedRecipe = it } }
|
||||
every { groupLogicMock.getById(any()) } returns group
|
||||
|
||||
val expectedGroupInformation = RecipeGroupInformationDto(0L, group, "Unit test note", listOf())
|
||||
|
||||
val groupNote = RecipeGroupNoteDto(group.id!!, expectedGroupInformation.note)
|
||||
val dto = RecipePublicDataDto(recipe.id, listOf(groupNote), listOf())
|
||||
|
||||
// Act
|
||||
recipeLogic.updatePublicData(dto)
|
||||
|
||||
// Assert
|
||||
assertContains(updatedRecipe.groupsInformation, expectedGroupInformation)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updatePublicData_emptyNotes_doesNothing() {
|
||||
// Arrange
|
||||
val dto = RecipePublicDataDto(recipe.id, listOf(), listOf())
|
||||
|
||||
// Act
|
||||
recipeLogic.updatePublicData(dto)
|
||||
|
||||
// Assert
|
||||
verify(exactly = 0) {
|
||||
recipeLogic.update(any<RecipeDto>())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updatePublicData_normalBehavior_callsUpdateLocationsInMixLogic() {
|
||||
// Arrange
|
||||
every { mixLogicMock.updateLocations(any()) } just runs
|
||||
|
||||
val mixesLocation = listOf(MixLocationDto(group.id!!, "location"))
|
||||
val dto = RecipePublicDataDto(recipe.id, listOf(), mixesLocation)
|
||||
|
||||
// Act
|
||||
recipeLogic.updatePublicData(dto)
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
mixLogicMock.updateLocations(mixesLocation)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updatePublicData_emptyMixesLocation_doesNothing() {
|
||||
// Arrange
|
||||
val dto = RecipePublicDataDto(recipe.id, listOf(), listOf())
|
||||
|
||||
// Act
|
||||
recipeLogic.updatePublicData(dto)
|
||||
|
||||
// Assert
|
||||
verify(exactly = 0) {
|
||||
mixLogicMock.updateLocations(any())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError
|
||||
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException
|
||||
import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation
|
||||
import dev.fyloz.colorrecipesexplorer.model.RecipeStep
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.Group
|
||||
import dev.fyloz.colorrecipesexplorer.service.RecipeStepService
|
||||
import dev.fyloz.colorrecipesexplorer.utils.PositionUtils
|
||||
|
@ -29,8 +29,8 @@ class DefaultRecipeStepLogicTest {
|
|||
every { PositionUtils.validate(any()) } just runs
|
||||
|
||||
val group = Group(1L, "Unit test group")
|
||||
val steps = mutableSetOf(RecipeStep(1L, 1, "A message"))
|
||||
val groupInfo = RecipeGroupInformation(1L, group, "A note", steps)
|
||||
val steps = listOf(RecipeStepDto(1L, 1, "A message"))
|
||||
val groupInfo = RecipeGroupInformationDto(1L, group, "A note", steps)
|
||||
|
||||
// Act
|
||||
recipeStepLogic.validateGroupInformationSteps(groupInfo)
|
||||
|
@ -41,24 +41,6 @@ class DefaultRecipeStepLogicTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateGroupInformationSteps_stepSetIsNull_doesNothing() {
|
||||
// Arrange
|
||||
mockkObject(PositionUtils)
|
||||
every { PositionUtils.validate(any()) } just runs
|
||||
|
||||
val group = Group(1L, "Unit test group")
|
||||
val groupInfo = RecipeGroupInformation(1L, group, "A note", null)
|
||||
|
||||
// Act
|
||||
recipeStepLogic.validateGroupInformationSteps(groupInfo)
|
||||
|
||||
// Assert
|
||||
verify(exactly = 0) {
|
||||
PositionUtils.validate(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateGroupInformationSteps_invalidSteps_throwsInvalidGroupStepsPositionsException() {
|
||||
// Arrange
|
||||
|
@ -68,8 +50,8 @@ class DefaultRecipeStepLogicTest {
|
|||
every { PositionUtils.validate(any()) } throws InvalidPositionsException(errors)
|
||||
|
||||
val group = Group(1L, "Unit test group")
|
||||
val steps = mutableSetOf(RecipeStep(1L, 1, "A message"))
|
||||
val groupInfo = RecipeGroupInformation(1L, group, "A note", steps)
|
||||
val steps = listOf(RecipeStepDto(1L, 1, "A message"))
|
||||
val groupInfo = RecipeGroupInformationDto(1L, group, "A note", steps)
|
||||
|
||||
// Act
|
||||
// Assert
|
||||
|
|
|
@ -1,371 +0,0 @@
|
|||
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
|
||||
import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic
|
||||
import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.model.account.group
|
||||
import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository
|
||||
import dev.fyloz.colorrecipesexplorer.utils.FilePath
|
||||
import io.mockk.*
|
||||
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 org.springframework.mock.web.MockMultipartFile
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.time.LocalDate
|
||||
import java.time.Period
|
||||
import kotlin.test.*
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class RecipeLogicTest :
|
||||
AbstractExternalModelServiceTest<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeLogic, RecipeRepository>() {
|
||||
override val repository: RecipeRepository = mock()
|
||||
private val companyLogic: CompanyLogic = mock()
|
||||
private val mixService: MixLogic = mock()
|
||||
private val groupService: GroupLogic = mock()
|
||||
private val recipeStepService: RecipeStepLogic = mock()
|
||||
private val configService: ConfigurationLogic = mock()
|
||||
override val logic: RecipeLogic =
|
||||
spy(
|
||||
DefaultRecipeLogic(
|
||||
repository,
|
||||
companyLogic,
|
||||
mixService,
|
||||
recipeStepService,
|
||||
groupService,
|
||||
mock(),
|
||||
configService
|
||||
)
|
||||
)
|
||||
|
||||
private val company: Company = company(id = 0L)
|
||||
override val entity: Recipe = recipe(id = 0L, name = "recipe", company = company)
|
||||
override val anotherEntity: Recipe = recipe(id = 1L, name = "another recipe", company = company)
|
||||
override val entitySaveDto: RecipeSaveDto = spy(recipeSaveDto(name = entity.name, companyId = entity.company.id!!))
|
||||
override val entityUpdateDto: RecipeUpdateDto = spy(recipeUpdateDto(id = entity.id!!, name = entity.name))
|
||||
|
||||
@AfterEach
|
||||
override fun afterEach() {
|
||||
reset(companyLogic, mixService)
|
||||
super.afterEach()
|
||||
}
|
||||
|
||||
// existsByCompany()
|
||||
|
||||
@Test
|
||||
fun `existsByCompany() returns true when at least one recipe exists for the given company`() {
|
||||
whenever(repository.existsByCompany(company)).doReturn(true)
|
||||
|
||||
val found = logic.existsByCompany(company)
|
||||
|
||||
assertTrue(found)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `existsByCompany() returns false when no recipe exists for the given company`() {
|
||||
whenever(repository.existsByCompany(company)).doReturn(false)
|
||||
|
||||
val found = logic.existsByCompany(company)
|
||||
|
||||
assertFalse(found)
|
||||
}
|
||||
|
||||
// existsByNameAndCompany()
|
||||
|
||||
@Test
|
||||
fun `existsByNameAndCompany() returns if a recipe exists for the given name and company in the repository`() {
|
||||
setOf(true, false).forEach {
|
||||
whenever(repository.existsByNameAndCompany(entity.name, company)).doReturn(it)
|
||||
|
||||
val exists = logic.existsByNameAndCompany(entity.name, company)
|
||||
|
||||
assertEquals(it, exists)
|
||||
}
|
||||
}
|
||||
|
||||
// isApprobationExpired()
|
||||
|
||||
@Test
|
||||
fun `isApprobationExpired() returns false when the approbation date of the given recipe is within the configured period`() {
|
||||
val period = Period.ofMonths(4)
|
||||
val recipe = recipe(approbationDate = LocalDate.now())
|
||||
|
||||
whenever(configService.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION)).doReturn(period.toString())
|
||||
|
||||
val approbationExpired = logic.isApprobationExpired(recipe)
|
||||
|
||||
assertNotNull(approbationExpired)
|
||||
assertFalse(approbationExpired)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isApprobationExpired() returns true when the approbation date of the given recipe is outside the configured period`() {
|
||||
val period = Period.ofMonths(4)
|
||||
val recipe = recipe(approbationDate = LocalDate.now().minus(period).minusMonths(1))
|
||||
|
||||
whenever(configService.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION)).doReturn(period.toString())
|
||||
|
||||
val approbationExpired = logic.isApprobationExpired(recipe)
|
||||
|
||||
assertNotNull(approbationExpired)
|
||||
assertTrue(approbationExpired)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isApprobationExpired() returns null when the given recipe as no approbation date`() {
|
||||
val period = Period.ofMonths(4)
|
||||
val recipe = recipe(approbationDate = null)
|
||||
|
||||
whenever(configService.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION)).doReturn(period.toString())
|
||||
|
||||
val approbationExpired = logic.isApprobationExpired(recipe)
|
||||
|
||||
assertNull(approbationExpired)
|
||||
}
|
||||
|
||||
// getAllByName()
|
||||
|
||||
@Test
|
||||
fun `getAllByName() returns the recipes with the given name`() {
|
||||
val recipes = listOf(entity, anotherEntity)
|
||||
|
||||
whenever(repository.findAllByName(entity.name)).doReturn(recipes)
|
||||
|
||||
val found = logic.getAllByName(entity.name)
|
||||
|
||||
assertEquals(recipes, found)
|
||||
}
|
||||
|
||||
// getAllByCompany()
|
||||
|
||||
@Test
|
||||
fun `getAllByCompany() returns the recipes with the given company`() {
|
||||
val companies = listOf(entity, anotherEntity)
|
||||
whenever(repository.findAllByCompany(company)).doReturn(companies)
|
||||
|
||||
val found = logic.getAllByCompany(company)
|
||||
|
||||
assertEquals(companies, found)
|
||||
}
|
||||
|
||||
// save()
|
||||
|
||||
@Test
|
||||
override fun `save(dto) calls and returns save() with the created entity`() {
|
||||
whenever(companyLogic.getById(company.id!!)).doReturn(companyDto(company))
|
||||
doReturn(false).whenever(logic).existsByNameAndCompany(entity.name, company)
|
||||
withBaseSaveDtoTest(entity, entitySaveDto, logic, { argThat { this.id == null && this.color == color } })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `save(dto) throw AlreadyExistsException when a recipe with the given name and company exists in the repository`() {
|
||||
whenever(companyLogic.getById(company.id!!)).doReturn(companyDto(company))
|
||||
doReturn(true).whenever(logic).existsByNameAndCompany(entity.name, company)
|
||||
|
||||
with(assertThrows<AlreadyExistsException> { logic.save(entitySaveDto) }) {
|
||||
this.assertErrorCode("company-name")
|
||||
}
|
||||
}
|
||||
|
||||
// update()
|
||||
|
||||
@Test
|
||||
override fun `update(dto) calls and returns update() with the created entity`() {
|
||||
doReturn(false).whenever(logic).existsByNameAndCompany(entity.name, company)
|
||||
withBaseUpdateDtoTest(entity, entityUpdateDto, logic, { any() })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `update(dto) throws AlreadyExistsException when a recipe exists for the given name and company`() {
|
||||
val name = "another recipe"
|
||||
|
||||
doReturn(entity).whenever(logic).getById(entity.id!!)
|
||||
doReturn(true).whenever(logic).existsByNameAndCompany(name, company)
|
||||
doReturn(name).whenever(entityUpdateDto).name
|
||||
|
||||
with(assertThrows<AlreadyExistsException> { logic.update(entityUpdateDto) }) {
|
||||
this.assertErrorCode("company-name")
|
||||
}
|
||||
}
|
||||
|
||||
// updatePublicData()
|
||||
|
||||
@Test
|
||||
fun `updatePublicData() updates the notes of a recipe groups information according to the RecipePublicDataDto`() {
|
||||
val recipe = recipe(
|
||||
id = 0L, groupsInformation = setOf(
|
||||
recipeGroupInformation(id = 0L, group = group(id = 1L), note = "Old note"),
|
||||
recipeGroupInformation(id = 1L, group = group(id = 2L), note = "Another note"),
|
||||
recipeGroupInformation(id = 2L, group = group(id = 3L), note = "Up to date note")
|
||||
)
|
||||
)
|
||||
val notes = setOf(
|
||||
noteDto(groupId = 1, content = "Note 1"),
|
||||
noteDto(groupId = 2, content = null)
|
||||
)
|
||||
val publicData = recipePublicDataDto(recipeId = recipe.id!!, notes = notes)
|
||||
|
||||
doReturn(recipe).whenever(logic).getById(recipe.id!!)
|
||||
doAnswer { it.arguments[0] }.whenever(logic).update(any<Recipe>())
|
||||
|
||||
logic.updatePublicData(publicData)
|
||||
|
||||
verify(logic).update(argThat<Recipe> {
|
||||
assertTrue { this.groupsInformation.first { it.group.id == 1L }.note == notes.first { it.groupId == 1L }.content }
|
||||
assertTrue { this.groupsInformation.first { it.group.id == 2L }.note == notes.first { it.groupId == 2L }.content }
|
||||
assertTrue { this.groupsInformation.any { it.group.id == 3L } && this.groupsInformation.first { it.group.id == 3L }.note == null }
|
||||
true
|
||||
})
|
||||
verify(mixService, times(0)).updateLocations(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
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")
|
||||
)
|
||||
)
|
||||
|
||||
logic.updatePublicData(publicData)
|
||||
|
||||
verify(mixService).updateLocations(publicData.mixesLocation!!)
|
||||
verify(logic, times(0)).update(any<Recipe>())
|
||||
}
|
||||
|
||||
// addMix()
|
||||
|
||||
@Test
|
||||
fun `addMix() adds the given mix to the given recipe and updates it`() {
|
||||
val mix = mix(id = 0L)
|
||||
val recipe = recipe(id = 0L, mixes = mutableListOf())
|
||||
|
||||
doAnswer { it.arguments[0] }.whenever(logic).update(any<Recipe>())
|
||||
|
||||
val found = logic.addMix(recipe, mix)
|
||||
|
||||
verify(logic).update(any<Recipe>())
|
||||
|
||||
assertEquals(recipe.id, found.id)
|
||||
assertTrue(found.mixes.contains(mix))
|
||||
}
|
||||
|
||||
// removeMix()
|
||||
|
||||
@Test
|
||||
fun `removeMix() removes the given mix from its recipe and updates it`() {
|
||||
val recipe = recipe(id = 0L, mixes = mutableListOf())
|
||||
val mix = mix(id = 0L, recipe = recipe)
|
||||
recipe.mixes.add(mix)
|
||||
|
||||
doAnswer { it.arguments[0] }.whenever(logic).update(any<Recipe>())
|
||||
|
||||
val found = logic.removeMix(mix)
|
||||
|
||||
verify(logic).update(any<Recipe>())
|
||||
|
||||
assertEquals(recipe.id, found.id)
|
||||
assertFalse(found.mixes.contains(mix))
|
||||
}
|
||||
}
|
||||
|
||||
private class RecipeImageServiceTestContext {
|
||||
val fileService = mockk<WriteableFileLogic> {
|
||||
every { write(any<MultipartFile>(), any(), any()) } just Runs
|
||||
every { delete(any()) } just Runs
|
||||
}
|
||||
val recipeImageService = spyk(DefaultRecipeImageLogic(fileService))
|
||||
val recipe = spyk(recipe())
|
||||
val recipeImagesIds = setOf(1L, 10L, 21L)
|
||||
val recipeImagesNames = recipeImagesIds.map { it.imageName }.toSet()
|
||||
val recipeImagesFiles = recipeImagesNames.map { CachedFile(it, FilePath(it), true) }
|
||||
|
||||
val Long.imageName
|
||||
get() = "${recipe.name}$RECIPE_IMAGE_ID_DELIMITER$this"
|
||||
|
||||
val String.imagePath
|
||||
get() = "${recipe.imagesDirectoryPath}/$this$RECIPE_IMAGE_EXTENSION"
|
||||
}
|
||||
|
||||
class RecipeImageLogicTest {
|
||||
@AfterEach
|
||||
internal fun afterEach() {
|
||||
clearAllMocks()
|
||||
}
|
||||
|
||||
private fun test(test: RecipeImageServiceTestContext.() -> Unit) {
|
||||
RecipeImageServiceTestContext().test()
|
||||
}
|
||||
|
||||
// getAllImages()
|
||||
|
||||
@Test
|
||||
fun `getAllImages() returns a Set containing the name of every files in the recipe's directory`() {
|
||||
test {
|
||||
every { fileService.listDirectoryFiles(any()) } returns recipeImagesFiles
|
||||
|
||||
val foundImagesNames = recipeImageService.getAllImages(recipe)
|
||||
|
||||
assertEquals(recipeImagesNames, foundImagesNames)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAllImages() returns an empty Set when the recipe's directory does not exists`() {
|
||||
test {
|
||||
every { fileService.listDirectoryFiles(any()) } returns emptySet()
|
||||
|
||||
assertTrue {
|
||||
recipeImageService.getAllImages(recipe).isEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// download()
|
||||
|
||||
@Test
|
||||
fun `download() writes the given image to the FileService and returns its name`() {
|
||||
test {
|
||||
val mockImage = MockMultipartFile("image.jpg", byteArrayOf(*"Random data".encodeToByteArray()))
|
||||
val expectedImageId = recipeImagesIds.maxOrNull()!! + 1L
|
||||
val expectedImageName = expectedImageId.imageName
|
||||
val expectedImagePath = expectedImageName.imagePath
|
||||
|
||||
every { fileService.listDirectoryFiles(any()) } returns recipeImagesFiles
|
||||
every { fileService.writeToDirectory(any(), any(), any(), any()) } just runs
|
||||
|
||||
val foundImageName = recipeImageService.download(mockImage, recipe)
|
||||
|
||||
assertEquals(expectedImageName, foundImageName)
|
||||
|
||||
verify {
|
||||
fileService.writeToDirectory(mockImage, expectedImagePath, any(), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delete()
|
||||
|
||||
@Test
|
||||
fun `delete() deletes the image with the given name in the FileService`() {
|
||||
test {
|
||||
val imageName = recipeImagesIds.first().imageName
|
||||
val imagePath = imageName.imagePath
|
||||
|
||||
every { fileService.deleteFromDirectory(any(), any()) } just runs
|
||||
|
||||
recipeImageService.delete(recipe, imageName)
|
||||
|
||||
verify {
|
||||
fileService.deleteFromDirectory(imagePath, any())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package dev.fyloz.colorrecipesexplorer.repository
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.material
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@DataJpaTest(excludeAutoConfiguration = [LiquibaseAutoConfiguration::class])
|
||||
class MaterialRepositoryTest @Autowired constructor(
|
||||
private val materialRepository: MaterialRepository,
|
||||
private val entityManager: TestEntityManager
|
||||
) {
|
||||
// updateInventoryQuantityById()
|
||||
|
||||
@Test
|
||||
fun `updateInventoryQuantityById() updates the quantity of the material with the given identifier`() {
|
||||
var material = entityManager.persist(material(inventoryQuantity = 1000f, materialType = null))
|
||||
val updatedQuantity = 1235f
|
||||
|
||||
materialRepository.updateInventoryQuantityById(material.id!!, updatedQuantity)
|
||||
|
||||
material = entityManager.refresh(material)
|
||||
|
||||
assertEquals(updatedQuantity, material.inventoryQuantity)
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package dev.fyloz.colorrecipesexplorer.repository
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@DataJpaTest(excludeAutoConfiguration = [LiquibaseAutoConfiguration::class])
|
||||
class MixRepositoryTest @Autowired constructor(
|
||||
private val mixRepository: MixRepository,
|
||||
private val entityManager: TestEntityManager
|
||||
) {
|
||||
// updateLocationById()
|
||||
|
||||
@Test
|
||||
fun `updateLocationById() updates the location of the mix with the given identifier`() {
|
||||
withMixLocation(null) { mix ->
|
||||
val updatedLocation = "new location"
|
||||
|
||||
mixRepository.updateLocationById(mix.id!!, updatedLocation)
|
||||
|
||||
val updated = entityManager.refresh(mix)
|
||||
|
||||
assertEquals(updatedLocation, updated.location)
|
||||
}
|
||||
}
|
||||
|
||||
private fun withMixLocation(location: String?, test: (Mix) -> Unit) {
|
||||
val materialType = entityManager.persist(materialType())
|
||||
val mixType = entityManager.persist(mixType(materialType = materialType))
|
||||
|
||||
val company = entityManager.persist(company())
|
||||
val recipe = entityManager.persist(recipe(company = company))
|
||||
|
||||
val mix = mix(id = null, location = location, recipe = recipe, mixType = mixType)
|
||||
test(entityManager.persist(mix))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue