#25 Separate mix materials and mix types

This commit is contained in:
FyloZ 2022-04-15 14:03:41 -04:00
parent eb4bb6b52a
commit 129fc4dcb9
Signed by: william
GPG Key ID: 835378AE9AF4AE97
51 changed files with 895 additions and 767 deletions

View File

@ -2,17 +2,17 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
group = "dev.fyloz.colorrecipesexplorer" group = "dev.fyloz.colorrecipesexplorer"
val kotlinVersion = "1.6.0" val kotlinVersion = "1.6.20"
val springBootVersion = "2.6.1" val springBootVersion = "2.6.1"
plugins { plugins {
// Outer scope variables can't be accessed in the plugins section, so we have to redefine them here // Outer scope variables can't be accessed in the plugins section, so we have to redefine them here
val kotlinVersion = "1.6.0" val kotlinVersion = "1.6.20"
val springBootVersion = "2.6.1" val springBootVersion = "2.6.1"
id("java") id("java")
id("org.jetbrains.kotlin.jvm") version kotlinVersion id("org.jetbrains.kotlin.jvm") version kotlinVersion
id("org.jetbrains.dokka") version "1.4.32" id("org.jetbrains.dokka") version "1.6.10"
id("org.springframework.boot") version springBootVersion id("org.springframework.boot") version springBootVersion
id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion
id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion
@ -30,7 +30,7 @@ dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom:${kotlinVersion}")) implementation(platform("org.jetbrains.kotlin:kotlin-bom:${kotlinVersion}"))
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
implementation("dev.fyloz.colorrecipesexplorer:database-manager:5.2.1") implementation("dev.fyloz.colorrecipesexplorer:database-manager:6.2")
implementation("dev.fyloz:memorycache:1.0") implementation("dev.fyloz:memorycache:1.0")
implementation("io.github.microutils:kotlin-logging-jvm:2.1.21") implementation("io.github.microutils:kotlin-logging-jvm:2.1.21")
implementation("io.jsonwebtoken:jjwt-api:0.11.2") implementation("io.jsonwebtoken:jjwt-api:0.11.2")

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -1,7 +1,7 @@
package dev.fyloz.colorrecipesexplorer.xlsx; package dev.fyloz.colorrecipesexplorer.xlsx;
import dev.fyloz.colorrecipesexplorer.dtos.MixDto; import dev.fyloz.colorrecipesexplorer.dtos.MixDto;
import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto; import dev.fyloz.colorrecipesexplorer.dtos.MixQuantityOutputDto;
import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto; import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Document; import dev.fyloz.colorrecipesexplorer.xlsx.component.Document;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Sheet; import dev.fyloz.colorrecipesexplorer.xlsx.component.Sheet;
@ -64,15 +64,15 @@ public class XlsxExporter {
sheet.registerCell(new SectionTitleCell("Recette")); sheet.registerCell(new SectionTitleCell("Recette"));
for (MixDto mix : recipeMixes) { for (MixDto mix : recipeMixes) {
Table mixTable = new Table(4, mix.getMixMaterials().size() + 1, mix.getMixType().getName()); Table mixTable = new Table(4, mix.getMixQuantities().getAll().size() + 1, mix.getMixType().getName());
mixTable.setColumnName(0, "Quantité"); mixTable.setColumnName(0, "Quantité");
mixTable.setColumnName(2, "Unités"); mixTable.setColumnName(2, "Unités");
int row = 0; int row = 0;
for (MixMaterialDto mixMaterial : mix.getMixMaterials()) { for (MixQuantityOutputDto mixQuantity : mix.getMixQuantitiesOutput()) {
mixTable.setRowName(row, mixMaterial.getMaterial().getName()); mixTable.setRowName(row, mixQuantity.getMaterial().getName());
mixTable.setContent(new Position(1, row + 1), mixMaterial.getQuantity()); mixTable.setContent(new Position(1, row + 1), mixQuantity.getQuantity());
mixTable.setContent(new Position(3, row + 1), mixMaterial.getMaterial().getMaterialType().getUsePercentages() ? "%" : "mL"); mixTable.setContent(new Position(3, row + 1), mixQuantity.getMaterial().getMaterialType().getUsePercentages() ? "%" : "mL");
row++; row++;
} }

View File

@ -2,11 +2,13 @@ package dev.fyloz.colorrecipesexplorer
object Constants { object Constants {
object ControllerPaths { object ControllerPaths {
const val COMPANY = "/api/company"
const val FILE = "/api/file" const val FILE = "/api/file"
const val MATERIAL = "/api/material" const val MATERIAL = "/api/material"
const val MATERIAL_TYPE = "/api/materialtype" const val MATERIAL_TYPE = "/api/materialtype"
const val MIX = "/api/recipe/mix" const val MIX = "/api/recipe/mix"
const val RECIPE = "/api/recipe" const val RECIPE = "/api/recipe"
const val TOUCH_UP_KIT = "/api/touchupkit"
} }
object FilePaths { object FilePaths {
@ -14,9 +16,22 @@ object Constants {
private const val IMAGES = "images" private const val IMAGES = "images"
const val SIMDUT = "$PDF/simdut" const val SIMDUT = "$PDF/simdut"
const val TOUCH_UP_KITS = "$PDF/touchupkits"
const val RECIPE_IMAGES = "$IMAGES/recipes" const val RECIPE_IMAGES = "$IMAGES/recipes"
} }
object ModelNames {
const val COMPANY = "Company"
const val MATERIAL = "Material"
const val MATERIAL_TYPE = "MaterialType"
const val MIX = "Mix"
const val MIX_MATERIAL = "MixMaterial"
const val MIX_TYPE = "MixType"
const val RECIPE = "Recipe"
const val RECIPE_STEP = "RecipeStep"
const val TOUCH_UP_KIT = "TouchUpKit"
}
object ValidationMessages { object ValidationMessages {
const val SIZE_GREATER_OR_EQUALS_ZERO = "Must be greater or equals to 0" const val SIZE_GREATER_OR_EQUALS_ZERO = "Must be greater or equals to 0"
const val SIZE_GREATER_OR_EQUALS_ONE = "Must be greater or equals to 1" const val SIZE_GREATER_OR_EQUALS_ONE = "Must be greater or equals to 1"

View File

@ -15,7 +15,7 @@ import org.springframework.core.env.ConfigurableEnvironment
import javax.sql.DataSource import javax.sql.DataSource
import org.springframework.context.annotation.Configuration as SpringConfiguration import org.springframework.context.annotation.Configuration as SpringConfiguration
const val SUPPORTED_DATABASE_VERSION = 5 const val SUPPORTED_DATABASE_VERSION = 6
const val ENV_VAR_ENABLE_DATABASE_UPDATE_NAME = "CRE_ENABLE_DB_UPDATE" const val ENV_VAR_ENABLE_DATABASE_UPDATE_NAME = "CRE_ENABLE_DB_UPDATE"
val DATABASE_NAME_REGEX = Regex("(\\w+)$") val DATABASE_NAME_REGEX = Regex("(\\w+)$")

View File

@ -1,8 +1,7 @@
package dev.fyloz.colorrecipesexplorer.config.initializers package dev.fyloz.colorrecipesexplorer.config.initializers
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
import dev.fyloz.colorrecipesexplorer.dtos.MixDto import dev.fyloz.colorrecipesexplorer.dtos.*
import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto
import dev.fyloz.colorrecipesexplorer.logic.MixLogic import dev.fyloz.colorrecipesexplorer.logic.MixLogic
import dev.fyloz.colorrecipesexplorer.utils.merge import dev.fyloz.colorrecipesexplorer.utils.merge
import mu.KotlinLogging import mu.KotlinLogging
@ -25,18 +24,19 @@ class MixInitializer(
logger.debug("Validating mix materials positions...") logger.debug("Validating mix materials positions...")
mixLogic.getAll() mixLogic.getAll()
.filter { mix -> mix.mixMaterials.any { it.position == 0 } } .filter { it.mixQuantities.all.any { mq -> mq.position == 0 } }
.forEach(this::fixMixPositions) .forEach(this::fixMixPositions)
logger.debug("Mix materials positions are valid!") logger.debug("Mix materials positions are valid!")
} }
private fun fixMixPositions(mix: MixDto) { private fun fixMixPositions(mix: MixDto) {
val maxPosition = mix.mixMaterials.maxOf { it.position } val mixQuantities = mix.mixQuantitiesOutput
val maxPosition = mixQuantities.maxOf { it.position }
logger.warn("Mix ${mix.id} (mix name: ${mix.mixType.name}, recipe id: ${mix.recipeId}) 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 }) { val invalidMixQuantities: Collection<MixQuantityOutputDto> = with(mixQuantities.filter { it.position == 0 }) {
if (maxPosition == 0 && this.size > 1) { if (maxPosition == 0 && this.size > 1) {
orderMixMaterials(this) orderMixMaterials(this)
} else { } else {
@ -44,28 +44,37 @@ class MixInitializer(
} }
} }
val fixedMixMaterials = increaseMixMaterialsPosition(invalidMixMaterials, maxPosition + 1) val fixedMixQuantities = increaseMixMaterialsPosition(invalidMixQuantities, maxPosition + 1)
val updatedMixMaterials = mix.mixMaterials.merge(fixedMixMaterials) val updatedMixQuantities =
mixQuantities.map { MixQuantitySaveDto(it.id, it.material.id, it.quantity, it.position, it.isMixType) }
.merge(fixedMixQuantities)
with(mix.copy(mixMaterials = updatedMixMaterials)) { val updatedMix = MixSaveDto(mix.id, mix.mixType.name, mix.recipeId, mix.mixType.materialType.id, updatedMixQuantities)
mixLogic.update(this) mixLogic.update(updatedMix)
}
} }
private fun increaseMixMaterialsPosition(mixMaterials: Iterable<MixMaterialDto>, firstPosition: Int) = private fun increaseMixMaterialsPosition(mixQuantities: Iterable<MixQuantityOutputDto>, firstPosition: Int) =
mixMaterials mixQuantities
.mapIndexed { index, mixMaterial -> mixMaterial.copy(position = firstPosition + index) } .mapIndexed { index, mixQuantity ->
MixQuantitySaveDto(
mixQuantity.id,
mixQuantity.material.id,
mixQuantity.quantity,
firstPosition + index,
mixQuantity.isMixType
)
}
.onEach { .onEach {
logger.info("\tPosition of material ${it.material.id} (${it.material.name}) has been set to ${it.position}") logger.info("\tPosition of material ${it.id} (mixType: ${it.isMixType}) has been set to ${it.position}")
} }
private fun orderMixMaterials(mixMaterials: Collection<MixMaterialDto>) = private fun orderMixMaterials(mixQuantities: Collection<MixQuantityOutputDto>) =
LinkedList(mixMaterials).apply { LinkedList(mixQuantities).apply {
while (this.peek().material.materialType.usePercentages) { while (this.peek().material.materialType.usePercentages) {
// The first mix material can't use percents, so move it to the end of the queue // The first mix material can't use percents, so move it to the end of the queue
val pop = this.pop() val pop = this.pop()
this.add(pop) this.add(pop)
logger.debug("\tMaterial ${pop.material.id} (${pop.material.name}) uses percents, moving to the end of the queue") logger.debug("\tMaterial ${pop.id} (mixType: ${pop.isMixType}) uses percents, moving to the end of the queue")
} }
} }
} }

View File

@ -16,7 +16,7 @@ data class MaterialDto(
val materialType: MaterialTypeDto, val materialType: MaterialTypeDto,
val simdutUrl: String? = null val hasSimdut: Boolean = false
) : EntityDto ) : EntityDto
data class MaterialSaveDto( data class MaterialSaveDto(

View File

@ -1,6 +1,7 @@
package dev.fyloz.colorrecipesexplorer.dtos package dev.fyloz.colorrecipesexplorer.dtos
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.Constants
import javax.validation.constraints.Min import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank import javax.validation.constraints.NotBlank
@ -15,8 +16,39 @@ data class MixDto(
val mixType: MixTypeDto, val mixType: MixTypeDto,
val mixMaterials: List<MixMaterialDto> @JsonIgnore
) : EntityDto val mixQuantities: MixQuantitiesDto,
) : EntityDto {
@Suppress("unused")
@get:JsonProperty("mixQuantities")
val mixQuantitiesOutput by lazy {
mixQuantities.materials.map {
MixQuantityOutputDto(it.id, it.material, it.quantity, it.position, false)
} + mixQuantities.mixTypes.map {
MixQuantityOutputDto(it.id, it.mixType.asMaterial(), it.quantity, it.position, true)
}
}
}
data class MixQuantitiesDto(
val materials: List<MixMaterialDto> = listOf(),
val mixTypes: List<MixMixTypeDto> = listOf()
) {
val all get() = materials + mixTypes
}
data class MixQuantityOutputDto(
val id: Long,
val material: MaterialDto,
val quantity: Float,
val position: Int,
val isMixType: Boolean
)
data class MixSaveDto( data class MixSaveDto(
val id: Long = 0L, val id: Long = 0L,
@ -28,7 +60,7 @@ data class MixSaveDto(
val materialTypeId: Long, val materialTypeId: Long,
val mixMaterials: List<MixMaterialSaveDto> val mixQuantities: List<MixQuantitySaveDto>
) )
data class MixDeductDto( data class MixDeductDto(

View File

@ -3,17 +3,47 @@ package dev.fyloz.colorrecipesexplorer.dtos
import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.Constants
import javax.validation.constraints.Min import javax.validation.constraints.Min
sealed interface MixQuantityDto : EntityDto {
val quantity: Float
val position: Int
val materialType: MaterialTypeDto
val name: String
}
data class MixMaterialDto( data class MixMaterialDto(
override val id: Long = 0L, override val id: Long = 0L,
val material: MaterialDto, val material: MaterialDto,
val quantity: Float, override val quantity: Float,
val position: Int override val position: Int
) : EntityDto ) : MixQuantityDto {
override val materialType: MaterialTypeDto
get() = material.materialType
data class MixMaterialSaveDto( override val name: String
get() = material.name
}
data class MixMixTypeDto(
override val id: Long,
val mixType: MixTypeDto,
override val quantity: Float,
override val position: Int
) : MixQuantityDto {
override val materialType: MaterialTypeDto
get() = mixType.materialType
override val name: String
get() = mixType.name
}
data class MixQuantitySaveDto(
override val id: Long = 0L, override val id: Long = 0L,
val materialId: Long, val materialId: Long,
@ -21,5 +51,7 @@ data class MixMaterialSaveDto(
@field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO)
val quantity: Float, val quantity: Float,
val position: Int val position: Int,
val isMixType: Boolean
) : EntityDto ) : EntityDto

View File

@ -5,5 +5,10 @@ data class MixTypeDto(
val name: String, val name: String,
val material: MaterialDto val materialType: MaterialTypeDto,
) : EntityDto
val material: MaterialDto? = null
) : EntityDto {
fun asMaterial() =
MaterialDto(id, name, 0f, true, materialType)
}

View File

@ -0,0 +1,52 @@
package dev.fyloz.colorrecipesexplorer.dtos
import dev.fyloz.colorrecipesexplorer.Constants
import java.time.LocalDate
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotEmpty
data class TouchUpKitDto(
override val id: Long = 0L,
@field:NotBlank
val project: String,
@field:NotBlank
val buggy: String,
@field:NotBlank
val company: String,
@field:Min(1, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ONE)
val quantity: Int,
val shippingDate: LocalDate,
val completionDate: LocalDate?,
val completed: Boolean = false,
val expired: Boolean = false,
@field:NotEmpty
val finish: List<String>,
@field:NotEmpty
val material: List<String>,
@field:NotEmpty
val content: List<TouchUpKitProductDto>
) : EntityDto
data class TouchUpKitProductDto(
override val id: Long = 0L,
val name: String,
val description: String?,
val quantity: Float,
val ready: Boolean
) : EntityDto

View File

@ -1,15 +1,15 @@
package dev.fyloz.colorrecipesexplorer.logic package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto
import dev.fyloz.colorrecipesexplorer.model.Company
import dev.fyloz.colorrecipesexplorer.service.CompanyService import dev.fyloz.colorrecipesexplorer.service.CompanyService
interface CompanyLogic : Logic<CompanyDto, CompanyService> interface CompanyLogic : Logic<CompanyDto, CompanyService>
@LogicComponent @LogicComponent
class DefaultCompanyLogic(service: CompanyService) : class DefaultCompanyLogic(service: CompanyService) :
BaseLogic<CompanyDto, CompanyService>(service, Company::class.simpleName!!), CompanyLogic { BaseLogic<CompanyDto, CompanyService>(service, Constants.ModelNames.COMPANY), CompanyLogic {
override fun save(dto: CompanyDto): CompanyDto { override fun save(dto: CompanyDto): CompanyDto {
throwIfNameAlreadyExists(dto.name) throwIfNameAlreadyExists(dto.name)

View File

@ -48,8 +48,10 @@ class DefaultInventoryLogic(
@Transactional @Transactional
override fun deductMix(mixRatio: MixDeductDto): Collection<MaterialQuantityDto> { override fun deductMix(mixRatio: MixDeductDto): Collection<MaterialQuantityDto> {
val mix = mixLogic.getById(mixRatio.id) val mix = mixLogic.getById(mixRatio.id)
val mixMaterials = mix.mixQuantities.materials
return deduct(getMaterialsWithAdjustedQuantities(mix.mixMaterials, mixRatio)) if (mixMaterials.isEmpty()) return listOf()
return deduct(getMaterialsWithAdjustedQuantities(mixMaterials, mixRatio))
} }
@Transactional @Transactional

View File

@ -1,8 +1,10 @@
package dev.fyloz.colorrecipesexplorer.logic package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto
import dev.fyloz.colorrecipesexplorer.dtos.MaterialSaveDto import dev.fyloz.colorrecipesexplorer.dtos.MaterialSaveDto
import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto
import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic
import dev.fyloz.colorrecipesexplorer.model.Material import dev.fyloz.colorrecipesexplorer.model.Material
import dev.fyloz.colorrecipesexplorer.service.MaterialService import dev.fyloz.colorrecipesexplorer.service.MaterialService
@ -11,21 +13,18 @@ interface MaterialLogic : Logic<MaterialDto, MaterialService> {
/** Checks if a material with the given [name] exists. */ /** Checks if a material with the given [name] exists. */
fun existsByName(name: String): Boolean fun existsByName(name: String): Boolean
/** Gets all materials that are not a mix type. */ /**
fun getAllNotMixType(): Collection<MaterialDto> * Returns every material available in the context of the recipe with the given [recipeId].
* The materials included contains every non mix type material, and the materials generated for the recipe mix types.
*/
fun getAllForRecipe(recipeId: Long): Collection<MaterialDto>
/** /**
* Gets all materials available for the creation of a mix for the recipe with the given [recipeId], * Returns every material available in the context of the mix with the given [mixId].
* including normal materials and materials from mix types included in the said recipe. * The materials included contains every non mix type material, and the materials generated for
* the mix's recipe mix types, excluding the mix's mix type.
*/ */
fun getAllForMixCreation(recipeId: Long): Collection<MaterialDto> fun getAllForMix(mixId: Long): Collection<MaterialDto>
/**
* Gets all materials available for updating the mix with the given [mixId],
* including normal materials and materials from mix types included in the mix recipe
* and excluding the material of the mix type of the said mix.
*/
fun getAllForMixUpdate(mixId: Long): Collection<MaterialDto>
/** Saves the given [dto]. */ /** Saves the given [dto]. */
fun save(dto: MaterialSaveDto): MaterialDto fun save(dto: MaterialSaveDto): MaterialDto
@ -44,24 +43,26 @@ class DefaultMaterialLogic(
val mixLogic: MixLogic, val mixLogic: MixLogic,
val materialTypeLogic: MaterialTypeLogic, val materialTypeLogic: MaterialTypeLogic,
val fileLogic: WriteableFileLogic val fileLogic: WriteableFileLogic
) : BaseLogic<MaterialDto, MaterialService>(service, Material::class.simpleName!!), MaterialLogic { ) : BaseLogic<MaterialDto, MaterialService>(service, Constants.ModelNames.MATERIAL), MaterialLogic {
override fun existsByName(name: String) = service.existsByName(name, null) override fun existsByName(name: String) = service.existsByName(name, null)
override fun getAllNotMixType() = service.getAllNotMixType()
override fun getAllForMixCreation(recipeId: Long): Collection<MaterialDto> { override fun getAllForRecipe(recipeId: Long): Collection<MaterialDto> {
val recipesMixTypes = recipeLogic.getById(recipeId).mixTypes val recipe = recipeLogic.getById(recipeId)
return getAll().filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } } return getAllWithMixTypesMaterials(recipe.mixTypes)
} }
override fun getAllForMixUpdate(mixId: Long): Collection<MaterialDto> { override fun getAllForMix(mixId: Long): Collection<MaterialDto> {
val mix = mixLogic.getById(mixId) val mix = mixLogic.getById(mixId)
val recipe = recipeLogic.getById(mix.recipeId) val recipe = recipeLogic.getById(mix.recipeId)
return getAll().filter { !it.isMixType || recipe.mixTypes.any { mixType -> mixType.material.id == it.id } } val availableMixTypes = recipe.mixTypes.filter { it != mix.mixType }
.filter { it.id != mix.mixType.material.id } return getAllWithMixTypesMaterials(availableMixTypes)
} }
private fun getAllWithMixTypesMaterials(mixTypes: Collection<MixTypeDto>) =
getAll() + mixTypes.map { it.asMaterial() }
override fun save(dto: MaterialSaveDto) = save(saveDtoToDto(dto, false)).also { saveSimdutFile(dto, false) } override fun save(dto: MaterialSaveDto) = save(saveDtoToDto(dto, false)).also { saveSimdutFile(dto, false) }
override fun save(dto: MaterialDto): MaterialDto { override fun save(dto: MaterialDto): MaterialDto {
throwIfNameAlreadyExists(dto.name) throwIfNameAlreadyExists(dto.name)
@ -101,7 +102,7 @@ class DefaultMaterialLogic(
val isMixType = !updating || getById(saveDto.id).isMixType val isMixType = !updating || getById(saveDto.id).isMixType
val materialType = materialTypeLogic.getById(saveDto.materialTypeId) val materialType = materialTypeLogic.getById(saveDto.materialTypeId)
return MaterialDto(saveDto.id, saveDto.name, saveDto.inventoryQuantity, isMixType, materialType, null) return MaterialDto(saveDto.id, saveDto.name, saveDto.inventoryQuantity, isMixType, materialType)
} }
private fun saveSimdutFile(dto: MaterialSaveDto, updating: Boolean) { private fun saveSimdutFile(dto: MaterialSaveDto, updating: Boolean) {

View File

@ -1,9 +1,9 @@
package dev.fyloz.colorrecipesexplorer.logic package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto
import dev.fyloz.colorrecipesexplorer.exception.CannotUpdateException import dev.fyloz.colorrecipesexplorer.exception.CannotUpdateException
import dev.fyloz.colorrecipesexplorer.model.MaterialType
import dev.fyloz.colorrecipesexplorer.service.MaterialTypeService import dev.fyloz.colorrecipesexplorer.service.MaterialTypeService
interface MaterialTypeLogic : Logic<MaterialTypeDto, MaterialTypeService> { interface MaterialTypeLogic : Logic<MaterialTypeDto, MaterialTypeService> {
@ -19,7 +19,7 @@ interface MaterialTypeLogic : Logic<MaterialTypeDto, MaterialTypeService> {
@LogicComponent @LogicComponent
class DefaultMaterialTypeLogic(service: MaterialTypeService) : class DefaultMaterialTypeLogic(service: MaterialTypeService) :
BaseLogic<MaterialTypeDto, MaterialTypeService>(service, MaterialType::class.simpleName!!), MaterialTypeLogic { BaseLogic<MaterialTypeDto, MaterialTypeService>(service, Constants.ModelNames.MATERIAL_TYPE), MaterialTypeLogic {
override fun getAll(systemType: Boolean) = service.getAll(systemType) override fun getAll(systemType: Boolean) = service.getAll(systemType)
override fun getByName(name: String) = service.getByName(name) override fun getByName(name: String) = service.getByName(name)

View File

@ -1,10 +1,10 @@
package dev.fyloz.colorrecipesexplorer.logic package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.dtos.MixDto import dev.fyloz.colorrecipesexplorer.dtos.MixDto
import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto
import dev.fyloz.colorrecipesexplorer.dtos.MixSaveDto import dev.fyloz.colorrecipesexplorer.dtos.MixSaveDto
import dev.fyloz.colorrecipesexplorer.model.Mix
import dev.fyloz.colorrecipesexplorer.service.MixService import dev.fyloz.colorrecipesexplorer.service.MixService
import org.springframework.context.annotation.Lazy import org.springframework.context.annotation.Lazy
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
@ -26,8 +26,8 @@ class DefaultMixLogic(
@Lazy private val recipeLogic: RecipeLogic, @Lazy private val recipeLogic: RecipeLogic,
@Lazy private val materialTypeLogic: MaterialTypeLogic, @Lazy private val materialTypeLogic: MaterialTypeLogic,
private val mixTypeLogic: MixTypeLogic, private val mixTypeLogic: MixTypeLogic,
private val mixMaterialLogic: MixMaterialLogic private val mixQuantityLogic: MixQuantityLogic
) : BaseLogic<MixDto, MixService>(service, Mix::class.simpleName!!), MixLogic { ) : BaseLogic<MixDto, MixService>(service, Constants.ModelNames.MIX), MixLogic {
@Transactional @Transactional
override fun save(dto: MixSaveDto): MixDto { override fun save(dto: MixSaveDto): MixDto {
val recipe = recipeLogic.getById(dto.recipeId) val recipe = recipeLogic.getById(dto.recipeId)
@ -36,7 +36,7 @@ class DefaultMixLogic(
val mix = MixDto( val mix = MixDto(
recipeId = recipe.id, recipeId = recipe.id,
mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(dto.name, materialType), mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(dto.name, materialType),
mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials) mixQuantities = mixQuantityLogic.validateAndPrepareForMix(dto.mixQuantities)
) )
return save(mix) return save(mix)
@ -47,12 +47,19 @@ class DefaultMixLogic(
val materialType = materialTypeLogic.getById(dto.materialTypeId) val materialType = materialTypeLogic.getById(dto.materialTypeId)
val mix = getById(dto.id) val mix = getById(dto.id)
// Update the mix type if it has been changed
val mixType = if (mix.mixType.name != dto.name || mix.mixType.materialType.id != dto.materialTypeId) {
mixTypeLogic.updateOrCreateForNameAndMaterialType(mix.mixType, dto.name, materialType)
} else {
mix.mixType
}
return update( return update(
MixDto( MixDto(
id = dto.id, id = dto.id,
recipeId = dto.recipeId, recipeId = mix.recipeId,
mixType = mixTypeLogic.updateOrCreateForNameAndMaterialType(mix.mixType, dto.name, materialType), mixType = mixType,
mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials) mixQuantities = mixQuantityLogic.validateAndPrepareForMix(dto.mixQuantities)
) )
) )
} }

View File

@ -1,33 +1,32 @@
package dev.fyloz.colorrecipesexplorer.logic package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto import dev.fyloz.colorrecipesexplorer.dtos.*
import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialSaveDto
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException
import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.MixMaterial
import dev.fyloz.colorrecipesexplorer.service.MixMaterialService
import dev.fyloz.colorrecipesexplorer.utils.PositionUtils import dev.fyloz.colorrecipesexplorer.utils.PositionUtils
import org.springframework.context.annotation.Lazy import org.springframework.context.annotation.Lazy
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
interface MixMaterialLogic : Logic<MixMaterialDto, MixMaterialService> { interface MixQuantityLogic {
/** /**
* Validates if the given [mixMaterials]. To be valid, the position of each mix material must be greater or equals to 1 and unique in the set. * Validates if the given [mixMaterials]. To be valid, the position of each mix material must be greater or equals to 1 and unique in the set.
* There must also be no gap between the positions. Also, the quantity of the first mix material in the set must not be expressed in percentages. * There must also be no gap between the positions. Also, the quantity of the first mix material in the set must not be expressed in percentages.
* If any of those criteria are not met, an [InvalidGroupStepsPositionsException] will be thrown. * If any of those criteria are not met, an [InvalidGroupStepsPositionsException] will be thrown.
*/ */
fun validateMixMaterials(mixMaterials: Set<MixMaterialDto>) fun validateMixQuantities(mixMaterials: List<MixQuantityDto>)
/** Validates the given mix materials [dtos] and save them. */ /** Validates the given mix quantities [dtos] and put them in [MixQuantitiesDto] to be consumed by a mix. */
fun validateAndSaveAll(dtos: List<MixMaterialSaveDto>): List<MixMaterialDto> fun validateAndPrepareForMix(dtos: List<MixQuantitySaveDto>): MixQuantitiesDto
} }
@LogicComponent @LogicComponent
class DefaultMixMaterialLogic(service: MixMaterialService, @Lazy private val materialLogic: MaterialLogic) : class DefaultMixQuantityLogic(
BaseLogic<MixMaterialDto, MixMaterialService>(service, MixMaterial::class.simpleName!!), MixMaterialLogic { @Lazy private val materialLogic: MaterialLogic,
override fun validateMixMaterials(mixMaterials: Set<MixMaterialDto>) { private val mixTypeLogic: MixTypeLogic
) : MixQuantityLogic {
override fun validateMixQuantities(mixMaterials: List<MixQuantityDto>) {
if (mixMaterials.isEmpty()) return if (mixMaterials.isEmpty()) return
val sortedMixMaterials = mixMaterials.sortedBy { it.position } val sortedMixMaterials = mixMaterials.sortedBy { it.position }
@ -38,24 +37,35 @@ class DefaultMixMaterialLogic(service: MixMaterialService, @Lazy private val mat
throw InvalidMixMaterialsPositionsException(ex.errors) throw InvalidMixMaterialsPositionsException(ex.errors)
} }
if (sortedMixMaterials[0].material.materialType.usePercentages) { val firstMixMaterial = sortedMixMaterials[0]
if (firstMixMaterial is MixMaterialDto) {
if (firstMixMaterial.material.materialType.usePercentages) {
throw InvalidFirstMixMaterialException(sortedMixMaterials[0]) throw InvalidFirstMixMaterialException(sortedMixMaterials[0])
} }
} }
}
override fun validateAndSaveAll(dtos: List<MixMaterialSaveDto>): List<MixMaterialDto> { override fun validateAndPrepareForMix(dtos: List<MixQuantitySaveDto>): MixQuantitiesDto {
val dtosWithMaterials = dtos.map { val mixMixTypes = dtos.filter { it.isMixType }.map {
MixMixTypeDto(
id = it.id,
mixType = mixTypeLogic.getById(it.materialId),
quantity = it.quantity,
position = it.position
)
}
val mixMaterials = dtos.filter { !it.isMixType }.map {
MixMaterialDto( MixMaterialDto(
id = it.id, id = it.id,
material = materialLogic.getById(it.materialId), material = materialLogic.getById(it.materialId),
quantity = it.quantity, quantity = it.quantity,
position = it.position position = it.position
) )
}.toSet() }
validateMixMaterials(dtosWithMaterials) validateMixQuantities(mixMixTypes + mixMaterials)
return MixQuantitiesDto(mixMaterials, mixMixTypes)
return dtosWithMaterials.map(::save)
} }
} }
@ -73,7 +83,7 @@ class InvalidMixMaterialsPositionsException(
) )
class InvalidFirstMixMaterialException( class InvalidFirstMixMaterialException(
val mixMaterial: MixMaterialDto val mixMaterial: MixQuantityDto
) : RestException( ) : RestException(
"invalid-mixmaterial-first", "invalid-mixmaterial-first",
"Invalid first mix material", "Invalid first mix material",

View File

@ -1,12 +1,10 @@
package dev.fyloz.colorrecipesexplorer.logic package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto
import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto
import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto
import dev.fyloz.colorrecipesexplorer.model.MixType
import dev.fyloz.colorrecipesexplorer.service.MixTypeService import dev.fyloz.colorrecipesexplorer.service.MixTypeService
import org.springframework.context.annotation.Lazy
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
interface MixTypeLogic : Logic<MixTypeDto, MixTypeService> { interface MixTypeLogic : Logic<MixTypeDto, MixTypeService> {
@ -22,11 +20,7 @@ interface MixTypeLogic : Logic<MixTypeDto, MixTypeService> {
} }
@LogicComponent @LogicComponent
class DefaultMixTypeLogic( class DefaultMixTypeLogic(service: MixTypeService) : BaseLogic<MixTypeDto, MixTypeService>(service, Constants.ModelNames.MIX_TYPE), MixTypeLogic {
service: MixTypeService,
@Lazy private val materialLogic: MaterialLogic
) :
BaseLogic<MixTypeDto, MixTypeService>(service, MixType::class.simpleName!!), MixTypeLogic {
@Transactional @Transactional
override fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialTypeDto) = override fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialTypeDto) =
service.getByNameAndMaterialType(name, materialType.id) ?: saveForNameAndMaterialType(name, materialType) service.getByNameAndMaterialType(name, materialType.id) ?: saveForNameAndMaterialType(name, materialType)
@ -35,7 +29,9 @@ class DefaultMixTypeLogic(
mixType: MixTypeDto, mixType: MixTypeDto,
name: String, name: String,
materialType: MaterialTypeDto materialType: MaterialTypeDto
) = if (service.isShared(mixType.id)) { ) = if (service.existsByNameAndMaterialType(name, materialType.id, mixType.id)) {
service.getByNameAndMaterialType(name, materialType.id)!!
} else if (service.isShared(mixType.id)) {
saveForNameAndMaterialType(name, materialType) saveForNameAndMaterialType(name, materialType)
} else { } else {
updateForNameAndMaterialType(mixType, name, materialType) updateForNameAndMaterialType(mixType, name, materialType)
@ -50,16 +46,7 @@ class DefaultMixTypeLogic(
} }
private fun saveForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto { private fun saveForNameAndMaterialType(name: String, materialType: MaterialTypeDto): MixTypeDto {
val material = materialLogic.save( return save(MixTypeDto(name = name, materialType = materialType))
MaterialDto(
name = name,
inventoryQuantity = Float.MIN_VALUE,
isMixType = true,
materialType = materialType
)
)
return save(MixTypeDto(name = name, material = material))
} }
private fun updateForNameAndMaterialType( private fun updateForNameAndMaterialType(
@ -67,7 +54,6 @@ class DefaultMixTypeLogic(
name: String, name: String,
materialType: MaterialTypeDto materialType: MaterialTypeDto
): MixTypeDto { ): MixTypeDto {
val material = materialLogic.update(mixType.material.copy(name = name, materialType = materialType)) return update(mixType.copy(name = name, materialType = materialType, material = mixType.material))
return update(mixType.copy(name = name, material = material))
} }
} }

View File

@ -6,7 +6,6 @@ import dev.fyloz.colorrecipesexplorer.dtos.*
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic
import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic
import dev.fyloz.colorrecipesexplorer.model.Recipe
import dev.fyloz.colorrecipesexplorer.service.RecipeService import dev.fyloz.colorrecipesexplorer.service.RecipeService
import dev.fyloz.colorrecipesexplorer.utils.collections.LazyMapList import dev.fyloz.colorrecipesexplorer.utils.collections.LazyMapList
import dev.fyloz.colorrecipesexplorer.utils.merge import dev.fyloz.colorrecipesexplorer.utils.merge
@ -37,7 +36,7 @@ class DefaultRecipeLogic(
private val recipeStepLogic: RecipeStepLogic, private val recipeStepLogic: RecipeStepLogic,
private val mixLogic: MixLogic, private val mixLogic: MixLogic,
private val groupLogic: GroupLogic private val groupLogic: GroupLogic
) : BaseLogic<RecipeDto, RecipeService>(service, Recipe::class.simpleName!!), RecipeLogic { ) : BaseLogic<RecipeDto, RecipeService>(service, Constants.ModelNames.RECIPE), RecipeLogic {
@Transactional @Transactional
override fun getAllWithMixesAndGroupsInformation() = override fun getAllWithMixesAndGroupsInformation() =
getAll().onEach { (it.mixes as LazyMapList<*, *>).initialize() } getAll().onEach { (it.mixes as LazyMapList<*, *>).initialize() }
@ -155,8 +154,8 @@ interface RecipeImageLogic {
/** Saves the given [image] and associate it to the recipe with the given [recipeId]. Returns the id of the saved image. */ /** 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 fun download(image: MultipartFile, recipeId: Long): String
/** Deletes the image with the given [path] for the given [recipeId]. */ /** Deletes the image with the given [id] for the given [recipeId]. */
fun delete(recipeId: Long, path: String) fun delete(recipeId: Long, id: String)
} }
@LogicComponent @LogicComponent
@ -167,7 +166,13 @@ class DefaultRecipeImageLogic(val fileLogic: WriteableFileLogic) : RecipeImageLo
override fun download(image: MultipartFile, recipeId: Long): String { override fun download(image: MultipartFile, recipeId: Long): String {
/** Gets the next id available for a new image for the given [recipeId]. */ /** Gets the next id available for a new image for the given [recipeId]. */
fun getNextAvailableId(): String = with(getAllImages(recipeId)) { fun getNextAvailableId(): String = with(getAllImages(recipeId)) {
(if (isEmpty()) 0 else maxOf { it.toLong() } + 1L).toString() val currentIds = mapNotNull { it.toLongOrNull() }
if (currentIds.isEmpty()) {
return 0.toString()
}
val nextId = currentIds.maxOf { it } + 1L
return nextId.toString()
} }
return getNextAvailableId().also { return getNextAvailableId().also {
@ -176,8 +181,8 @@ class DefaultRecipeImageLogic(val fileLogic: WriteableFileLogic) : RecipeImageLo
} }
} }
override fun delete(recipeId: Long, path: String) = override fun delete(recipeId: Long, id: String) =
fileLogic.deleteFromDirectory(path, getRecipeImagesDirectory(recipeId)) fileLogic.deleteFromDirectory(getImagePath(recipeId, id), getRecipeImagesDirectory(recipeId))
private fun getImagePath(recipeId: Long, id: String) = "${getRecipeImagesDirectory(recipeId)}/$id" private fun getImagePath(recipeId: Long, id: String) = "${getRecipeImagesDirectory(recipeId)}/$id"

View File

@ -1,12 +1,12 @@
package dev.fyloz.colorrecipesexplorer.logic package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto
import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException
import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.RecipeStep
import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.model.account.Group
import dev.fyloz.colorrecipesexplorer.service.RecipeStepService import dev.fyloz.colorrecipesexplorer.service.RecipeStepService
import dev.fyloz.colorrecipesexplorer.utils.PositionUtils import dev.fyloz.colorrecipesexplorer.utils.PositionUtils
@ -19,7 +19,7 @@ interface RecipeStepLogic : Logic<RecipeStepDto, RecipeStepService> {
@LogicComponent @LogicComponent
class DefaultRecipeStepLogic(recipeStepService: RecipeStepService) : class DefaultRecipeStepLogic(recipeStepService: RecipeStepService) :
BaseLogic<RecipeStepDto, RecipeStepService>(recipeStepService, RecipeStep::class.simpleName!!), RecipeStepLogic { BaseLogic<RecipeStepDto, RecipeStepService>(recipeStepService, Constants.ModelNames.RECIPE_STEP), RecipeStepLogic {
override fun validateGroupInformationSteps(groupInformation: RecipeGroupInformationDto) { override fun validateGroupInformationSteps(groupInformation: RecipeGroupInformationDto) {
try { try {
PositionUtils.validate(groupInformation.steps.map { it.position }.toList()) PositionUtils.validate(groupInformation.steps.map { it.position }.toList())

View File

@ -1,33 +1,21 @@
package dev.fyloz.colorrecipesexplorer.logic package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
import dev.fyloz.colorrecipesexplorer.dtos.TouchUpKitDto
import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic
import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic
import dev.fyloz.colorrecipesexplorer.model.ConfigurationType import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
import dev.fyloz.colorrecipesexplorer.model.touchupkit.* import dev.fyloz.colorrecipesexplorer.service.TouchUpKitService
import dev.fyloz.colorrecipesexplorer.repository.TouchUpKitRepository
import dev.fyloz.colorrecipesexplorer.rest.TOUCH_UP_KIT_CONTROLLER_PATH
import dev.fyloz.colorrecipesexplorer.utils.* import dev.fyloz.colorrecipesexplorer.utils.*
import org.springframework.core.io.ByteArrayResource import org.springframework.core.io.ByteArrayResource
import org.springframework.core.io.Resource import org.springframework.core.io.Resource
import org.springframework.stereotype.Service
import java.time.LocalDate import java.time.LocalDate
import java.time.Period
private const val TOUCH_UP_KIT_FILES_PATH = "pdf/touchupkits"
const val TOUCH_UP_TEXT_FR = "KIT DE RETOUCHE"
const val TOUCH_UP_TEXT_EN = "TOUCH UP KIT"
interface TouchUpKitLogic :
ExternalModelService<TouchUpKit, TouchUpKitSaveDto, TouchUpKitUpdateDto, TouchUpKitOutputDto, TouchUpKitRepository> {
fun isExpired(touchUpKit: TouchUpKit): Boolean
interface TouchUpKitLogic : Logic<TouchUpKitDto, TouchUpKitService> {
/** Sets the touch up kit with the given [id] as complete. */
fun complete(id: Long) fun complete(id: Long)
/** Generates and returns a [PdfDocument] for the given [job]. */
fun generateJobPdf(job: String): PdfDocument
/** /**
* Generates and returns a [PdfDocument] for the given [job] as a [ByteArrayResource]. * Generates and returns a [PdfDocument] for the given [job] as a [ByteArrayResource].
* *
@ -36,68 +24,37 @@ interface TouchUpKitLogic :
*/ */
fun generateJobPdfResource(job: String): Resource fun generateJobPdfResource(job: String): Resource
/** Writes the given [document] to the [FileService] if TOUCH_UP_KIT_CACHE_PDF is enabled. */ /** Generates and returns a [PdfDocument] for the given [job]. */
fun String.cachePdfDocument(document: PdfDocument) fun generateJobPdf(job: String): PdfDocument
/** Writes the given [pdf] to the disk if TOUCH_UP_KIT_CACHE_PDF is enabled. */
fun cacheJobPdf(job: String, pdf: PdfDocument)
} }
@Service @LogicComponent
@RequireDatabase
class DefaultTouchUpKitLogic( class DefaultTouchUpKitLogic(
private val fileService: WriteableFileLogic, service: TouchUpKitService,
private val configService: ConfigurationLogic, private val fileLogic: WriteableFileLogic,
touchUpKitRepository: TouchUpKitRepository private val configLogic: ConfigurationLogic
) : AbstractExternalModelService<TouchUpKit, TouchUpKitSaveDto, TouchUpKitUpdateDto, TouchUpKitOutputDto, TouchUpKitRepository>( ) : BaseLogic<TouchUpKitDto, TouchUpKitService>(service, Constants.ModelNames.TOUCH_UP_KIT), TouchUpKitLogic {
touchUpKitRepository
), TouchUpKitLogic {
private val cacheGeneratedFiles by lazy { private val cacheGeneratedFiles by lazy {
configService.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) == true.toString() configLogic.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) == true.toString()
} }
override fun idNotFoundException(id: Long) = touchUpKitIdNotFoundException(id) override fun complete(id: Long) = service.updateCompletionDateById(id, LocalDate.now())
override fun idAlreadyExistsException(id: Long) = touchUpKitIdAlreadyExistsException(id)
override fun TouchUpKit.toOutput() = TouchUpKitOutputDto( override fun generateJobPdfResource(job: String): Resource {
this.id!!, if (cacheGeneratedFiles) {
this.project, val pdfPath = jobPdfPath(job)
this.buggy, if (fileLogic.exists(pdfPath)) {
this.company, return fileLogic.read(pdfPath)
this.quantity, }
this.shippingDate,
this.completionDate != null,
this.completionDate,
isExpired(this),
this.finish,
this.material,
this.content,
this.pdfUrl()
)
override fun update(entity: TouchUpKitUpdateDto): TouchUpKit {
val persistedKit by lazy { getById(entity.id) }
return super.update(with(entity) {
touchUpKit(
id = id,
project = project ?: persistedKit.project,
buggy = buggy ?: persistedKit.buggy,
company = company ?: persistedKit.company,
quantity = quantity ?: persistedKit.quantity,
shippingDate = shippingDate ?: persistedKit.shippingDate,
completionDate = completionDate ?: persistedKit.completionDate,
finish = finish ?: persistedKit.finish,
material = material ?: persistedKit.material,
content = content?.map { touchUpKitProduct(it) }?.toSet() ?: persistedKit.content
)
})
} }
override fun isExpired(touchUpKit: TouchUpKit) = val pdf = generateJobPdf(job)
with(Period.parse(configService.getContent(ConfigurationType.TOUCH_UP_KIT_EXPIRATION))) { cacheJobPdf(job, pdf)
touchUpKit.completed && touchUpKit.completionDate!!.plus(this) < LocalDate.now()
}
override fun complete(id: Long) { return pdf.toByteArrayResource()
update(touchUpKitUpdateDto(id = id, completionDate = LocalDate.now()))
} }
override fun generateJobPdf(job: String) = pdf { override fun generateJobPdf(job: String) = pdf {
@ -122,29 +79,17 @@ class DefaultTouchUpKitLogic(
} }
} }
override fun generateJobPdfResource(job: String): Resource { override fun cacheJobPdf(job: String, pdf: PdfDocument) {
if (cacheGeneratedFiles) {
with(job.pdfDocumentPath()) {
if (fileService.exists(this)) {
return fileService.read(this)
}
}
}
return generateJobPdf(job).apply {
job.cachePdfDocument(this)
}.toByteArrayResource()
}
override fun String.cachePdfDocument(document: PdfDocument) {
if (!cacheGeneratedFiles) return if (!cacheGeneratedFiles) return
fileService.write(document.toByteArrayResource(), this.pdfDocumentPath(), true) fileLogic.write(pdf.toByteArrayResource(), jobPdfPath(job), true)
} }
private fun String.pdfDocumentPath() = private fun jobPdfPath(job: String) =
"$TOUCH_UP_KIT_FILES_PATH/$this.pdf" "${Constants.FilePaths.TOUCH_UP_KITS}/$job.pdf"
private fun TouchUpKit.pdfUrl() = companion object {
"${configService.getContent(ConfigurationType.INSTANCE_URL)}$TOUCH_UP_KIT_CONTROLLER_PATH/pdf?job=$project" const val TOUCH_UP_TEXT_FR = "KIT DE RETOUCHE"
const val TOUCH_UP_TEXT_EN = "TOUCH UP KIT"
}
} }

View File

@ -20,10 +20,10 @@ class TouchUpKitRemover(
} }
private fun removeExpiredKits() { private fun removeExpiredKits() {
with(touchUpKitLogic.getAll().filter(touchUpKitLogic::isExpired)) { with(touchUpKitLogic.getAll().filter { it.expired }) {
this.forEach { this.forEach {
logger.debug("Removed expired touch up kit ${it.id} (${it.project} ${it.buggy})") logger.debug("Removed expired touch up kit ${it.id} (${it.project} ${it.buggy})")
touchUpKitLogic.delete(it) touchUpKitLogic.deleteById(it.id)
} }
logger.info("Removed ${this.size} expired touch up kits") logger.info("Removed ${this.size} expired touch up kits")
} }

View File

@ -4,7 +4,6 @@ import dev.fyloz.colorrecipesexplorer.config.security.blacklistedJwtTokens
import dev.fyloz.colorrecipesexplorer.logic.AbstractExternalModelService import dev.fyloz.colorrecipesexplorer.logic.AbstractExternalModelService
import dev.fyloz.colorrecipesexplorer.logic.ExternalModelService import dev.fyloz.colorrecipesexplorer.logic.ExternalModelService
import dev.fyloz.colorrecipesexplorer.model.account.* import dev.fyloz.colorrecipesexplorer.model.account.*
import dev.fyloz.colorrecipesexplorer.model.validation.or
import dev.fyloz.colorrecipesexplorer.repository.UserRepository import dev.fyloz.colorrecipesexplorer.repository.UserRepository
import org.springframework.context.annotation.Lazy import org.springframework.context.annotation.Lazy
import org.springframework.context.annotation.Profile import org.springframework.context.annotation.Profile
@ -136,8 +135,8 @@ class DefaultUserLogic(
return update(with(entity) { return update(with(entity) {
User( User(
id = id, id = id,
firstName = firstName or persistedUser.firstName, firstName = firstName ?: persistedUser.firstName,
lastName = lastName or persistedUser.lastName, lastName = lastName ?: persistedUser.lastName,
password = persistedUser.password, password = persistedUser.password,
isDefaultGroupUser = false, isDefaultGroupUser = false,
isSystemUser = false, isSystemUser = false,

View File

@ -16,9 +16,9 @@ data class Mix(
@ManyToOne @ManyToOne
@JoinColumn(name = "mix_type_id") @JoinColumn(name = "mix_type_id")
var mixType: MixType, val mixType: MixType,
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "mix_id") @JoinColumn(name = "mix_id")
var mixMaterials: List<MixMaterial>, val mixMaterials: List<MixMaterial>
) : ModelEntity ) : ModelEntity

View File

@ -0,0 +1,19 @@
package dev.fyloz.colorrecipesexplorer.model
import javax.persistence.*
@Entity
@Table(name = "mix_mix_type")
data class MixMixType(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long,
@ManyToOne
@JoinColumn(name = "mix_type_id")
val mixType: MixType,
val quantity: Float,
val position: Int
) : ModelEntity

View File

@ -9,10 +9,13 @@ data class MixType(
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?, override val id: Long?,
@Column(unique = true)
val name: String, val name: String,
@ManyToOne
@JoinColumn(name = "material_type_id")
val materialType: MaterialType,
@OneToOne(cascade = [CascadeType.ALL]) @OneToOne(cascade = [CascadeType.ALL])
@JoinColumn(name = "material_id") @JoinColumn(name = "material_id")
var material: Material val material: Material?
) : ModelEntity ) : ModelEntity

View File

@ -1,17 +1,8 @@
package dev.fyloz.colorrecipesexplorer.model.touchupkit package dev.fyloz.colorrecipesexplorer.model.touchupkit
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.EntityDto
import dev.fyloz.colorrecipesexplorer.model.ModelEntity import dev.fyloz.colorrecipesexplorer.model.ModelEntity
import java.time.LocalDate import java.time.LocalDate
import javax.persistence.* import javax.persistence.*
import javax.validation.constraints.Min
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotEmpty
const val TOUCH_UP_KIT_DELIMITER = ';'
@Entity @Entity
@Table(name = "touch_up_kit") @Table(name = "touch_up_kit")
@ -35,24 +26,15 @@ data class TouchUpKit(
val completionDate: LocalDate?, val completionDate: LocalDate?,
@Column(name = "finish") @Column(name = "finish")
private val finishConcatenated: String, val finish: String,
@Column(name = "material") @Column(name = "material")
private val materialConcatenated: String, val material: String,
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "touch_up_kit_id") @JoinColumn(name = "touch_up_kit_id")
val content: Set<TouchUpKitProduct> val content: List<TouchUpKitProduct>
) : ModelEntity { ) : ModelEntity
val finish
get() = finishConcatenated.split(TOUCH_UP_KIT_DELIMITER)
val material
get() = materialConcatenated.split(TOUCH_UP_KIT_DELIMITER)
val completed
get() = completionDate != null
}
@Entity @Entity
@Table(name = "touch_up_kit_product") @Table(name = "touch_up_kit_product")
@ -69,174 +51,3 @@ data class TouchUpKitProduct(
val ready: Boolean val ready: Boolean
) : ModelEntity ) : ModelEntity
data class TouchUpKitSaveDto(
@field:NotBlank
val project: String,
@field:NotBlank
val buggy: String,
@field:NotBlank
val company: String,
@field:Min(1, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ONE)
val quantity: Int,
val shippingDate: LocalDate,
@field:NotEmpty
val finish: List<String>,
@field:NotEmpty
val material: List<String>,
@field:NotEmpty
val content: Set<TouchUpKitProductDto>
) : EntityDto<TouchUpKit> {
override fun toEntity() = touchUpKit(this)
}
data class TouchUpKitUpdateDto(
val id: Long,
@field:NotBlank
val project: String?,
@field:NotBlank
val buggy: String?,
@field:NotBlank
val company: String?,
@field:Min(1, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ONE)
val quantity: Int?,
val shippingDate: LocalDate?,
val completionDate: LocalDate?,
@field:NotEmpty
val finish: List<String>?,
@field:NotEmpty
val material: List<String>?,
@field:NotEmpty
val content: Set<TouchUpKitProductDto>?
) : EntityDto<TouchUpKit>
data class TouchUpKitOutputDto(
override val id: Long,
val project: String,
val buggy: String,
val company: String,
val quantity: Int,
val shippingDate: LocalDate,
val completed: Boolean,
val completionDate: LocalDate?,
val expired: Boolean,
val finish: List<String>,
val material: List<String>,
val content: Set<TouchUpKitProduct>,
val pdfUrl: String
) : ModelEntity
data class TouchUpKitProductDto(
val name: String,
val description: String?,
val quantity: Float,
val ready: Boolean
)
// ==== DSL ====
fun touchUpKit(
id: Long? = null,
project: String = "project",
buggy: String = "buggy",
company: String = "company",
quantity: Int = 1,
shippingDate: LocalDate = LocalDate.now(),
completionDate: LocalDate? = null,
finish: List<String>,
material: List<String>,
content: Set<TouchUpKitProduct>,
op: TouchUpKit.() -> Unit = {}
) = TouchUpKit(
id,
project,
buggy,
company,
quantity,
shippingDate,
completionDate,
finish.reduce { acc, f -> "$acc$TOUCH_UP_KIT_DELIMITER$f" },
material.reduce { acc, f -> "$acc$TOUCH_UP_KIT_DELIMITER$f" },
content
).apply(op)
fun touchUpKit(touchUpKitSaveDto: TouchUpKitSaveDto) =
with(touchUpKitSaveDto) {
touchUpKit(
project = project,
buggy = buggy,
company = company,
quantity = quantity,
shippingDate = shippingDate,
finish = finish,
material = material,
content = content.map { touchUpKitProduct(it) }.toSet()
)
}
fun touchUpKitProduct(
id: Long? = null,
name: String = "product",
description: String? = "description",
quantity: Float = 1f,
ready: Boolean = false,
op: TouchUpKitProduct.() -> Unit = {}
) = TouchUpKitProduct(id, name, description, quantity, ready)
.apply(op)
fun touchUpKitUpdateDto(
id: Long = 0L,
project: String? = null,
buggy: String? = null,
company: String? = null,
quantity: Int? = null,
shippingDate: LocalDate? = null,
completionDate: LocalDate? = null,
finish: List<String>? = null,
material: List<String>? = null,
content: Set<TouchUpKitProductDto>? = null
) = TouchUpKitUpdateDto(id, project, buggy, company, quantity, shippingDate, completionDate, finish, material, content)
fun touchUpKitProduct(touchUpKitProductDto: TouchUpKitProductDto) =
touchUpKitProduct(
name = touchUpKitProductDto.name,
description = touchUpKitProductDto.description,
quantity = touchUpKitProductDto.quantity,
ready = touchUpKitProductDto.ready
)
// ==== Exceptions ====
private const val TOUCH_UP_KIT_NOT_FOUND_EXCEPTION_TITLE = "Touch up kit not found"
private const val TOUCH_UP_KIT_ALREADY_EXISTS_EXCEPTION_TITLE = "Touch up kit already exists"
private const val TOUCH_UP_KIT_EXCEPTION_ERROR_CODE = "touchupkit"
fun touchUpKitIdNotFoundException(id: Long) =
NotFoundException(
TOUCH_UP_KIT_EXCEPTION_ERROR_CODE,
TOUCH_UP_KIT_NOT_FOUND_EXCEPTION_TITLE,
"A touch up kit with the id $id could not be found",
id
)
fun touchUpKitIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
TOUCH_UP_KIT_EXCEPTION_ERROR_CODE,
TOUCH_UP_KIT_ALREADY_EXISTS_EXCEPTION_TITLE,
"A touch up kit with the id $id already exists",
id
)

View File

@ -1,45 +0,0 @@
package dev.fyloz.colorrecipesexplorer.model.validation
import javax.validation.Constraint
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext
import javax.validation.Payload
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.reflect.KClass
private const val MESSAGE = "must be null or not blank"
@Target(AnnotationTarget.FIELD)
@MustBeDocumented
@Constraint(validatedBy = [NullOrNotBlankValidator::class])
annotation class NullOrNotBlank(
val message: String = MESSAGE,
val groups: Array<KClass<*>> = [],
@Suppress("unused") val payload: Array<KClass<out Payload>> = []
)
class NullOrNotBlankValidator : ConstraintValidator<NullOrNotBlank, String> {
var message = MESSAGE
override fun initialize(constraintAnnotation: NullOrNotBlank) {
message = constraintAnnotation.message
}
override fun isValid(value: String?, context: ConstraintValidatorContext): Boolean {
return value.isNullOrNotBlank().apply {
if (!this) context.buildConstraintViolationWithTemplate(message)
}
}
}
fun String?.isNullOrNotBlank(): Boolean = this == null || isNotBlank()
/** Checks if the given string [value] is not null and not blank. */
@ExperimentalContracts
fun isNotNullAndNotBlank(value: String?): Boolean {
contract { returns(true) implies (value != null) }
return value != null && value.isNotBlank()
}
infix fun String?.or(alternative: String): String = if (isNotNullAndNotBlank(this)) this else alternative

View File

@ -1,46 +0,0 @@
package dev.fyloz.colorrecipesexplorer.model.validation
import javax.validation.Constraint
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext
import javax.validation.Payload
import kotlin.reflect.KClass
private const val MIN_SIZE = Long.MIN_VALUE
private const val MAX_SIZE = Long.MAX_VALUE
private const val MESSAGE = "must be null or have a correct length"
@Target(AnnotationTarget.FIELD)
@MustBeDocumented
@Constraint(validatedBy = [NullOrSizeValidator::class])
annotation class NullOrSize(
val min: Long = MIN_SIZE,
val max: Long = MAX_SIZE,
val message: String = MESSAGE,
val groups: Array<KClass<*>> = [],
@Suppress("unused") val payload: Array<KClass<out Payload>> = []
)
class NullOrSizeValidator : ConstraintValidator<NullOrSize, Any> {
var min = MIN_SIZE
var max = MAX_SIZE
var message = MESSAGE
override fun initialize(constraintAnnotation: NullOrSize) {
min = constraintAnnotation.min
max = constraintAnnotation.max
message = constraintAnnotation.message
}
override fun isValid(value: Any?, context: ConstraintValidatorContext): Boolean {
if (value == null) return true
return when (value) {
is Number -> value.toLong() in min..max
is String -> value.length in min..max
is Collection<*> -> value.size in min..max
else -> throw IllegalStateException("Cannot use @NullOrSize on type ${value::class}")
}.apply {
if (!this) context.buildConstraintViolationWithTemplate(message)
}
}
}

View File

@ -2,6 +2,7 @@ package dev.fyloz.colorrecipesexplorer.repository
import dev.fyloz.colorrecipesexplorer.model.MixMaterial import dev.fyloz.colorrecipesexplorer.model.MixMaterial
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@Repository @Repository

View File

@ -0,0 +1,34 @@
package dev.fyloz.colorrecipesexplorer.repository
import dev.fyloz.colorrecipesexplorer.model.MixMixType
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository
@Repository
interface MixMixTypeRepository : JpaRepository<MixMixType, Long> {
@Query(
nativeQuery = true, value = """
SELECT * FROM mix_mix_type mmt WHERE mmt.mix_id = :mixId
"""
)
fun findAllByMixId(mixId: Long): List<MixMixType>
@Modifying
@Query(
nativeQuery = true, value = """
INSERT INTO mix_mix_type (id, mix_type_id, mix_id, quantity, position)
VALUES (:id, :mixTypeId, :mixId, :quantity, :position)
"""
)
fun saveForMixId(id: Long?, mixTypeId: Long, mixId: Long, quantity: Float, position: Int)
@Modifying
@Query(
nativeQuery = true, value = """
DELETE FROM mix_mix_type mmt WHERE mmt.mix_id = :mixId
"""
)
fun deleteAllByMixId(mixId: Long)
}

View File

@ -10,11 +10,12 @@ interface MixTypeRepository : JpaRepository<MixType, Long> {
/** Checks if a mix type with the given [name], [materialTypeId] and a different [id] exists. */ /** Checks if a mix type with the given [name], [materialTypeId] and a different [id] exists. */
@Query( @Query(
""" """
SELECT CASE WHEN(COUNT(m) > 0) THEN TRUE ELSE FALSE END SELECT CASE WHEN(COUNT(mt.id)) > 1 THEN TRUE ELSE FALSE END
FROM MixType m WHERE m.name = :name AND m.material.materialType.id = :materialTypeId AND m.id <> :id FROM MixType mt
WHERE mt.name = :name AND mt.materialType.id = :materialTypeId AND mt.id <> :id
""" """
) )
fun existsByNameAndMaterialType(name: String, materialTypeId: Long, id: Long): Boolean fun existsByNameAndMaterialTypeAndIdNot(name: String, materialTypeId: Long, id: Long): Boolean
/** Finds the mix type with the given [name] and [materialTypeId]. */ /** 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")

View File

@ -2,5 +2,13 @@ package dev.fyloz.colorrecipesexplorer.repository
import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKit import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKit
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import java.time.LocalDate
interface TouchUpKitRepository : JpaRepository<TouchUpKit, Long> interface TouchUpKitRepository : JpaRepository<TouchUpKit, Long> {
/** Updates the [completionDate] of the touch up kit with the given [id]. */
@Modifying
@Query("UPDATE TouchUpKit t SET t.completionDate = :completionDate WHERE t.id = :id")
fun updateCompletionDateById(id: Long, completionDate: LocalDate)
}

View File

@ -1,5 +1,6 @@
package dev.fyloz.colorrecipesexplorer.rest package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto
@ -8,10 +9,8 @@ import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import javax.validation.Valid import javax.validation.Valid
private const val COMPANY_CONTROLLER_PATH = "api/company"
@RestController @RestController
@RequestMapping(COMPANY_CONTROLLER_PATH) @RequestMapping(Constants.ControllerPaths.COMPANY)
@RequireDatabase @RequireDatabase
@PreAuthorizeViewCatalog @PreAuthorizeViewCatalog
class CompanyController(private val companyLogic: CompanyLogic) { class CompanyController(private val companyLogic: CompanyLogic) {
@ -26,7 +25,7 @@ class CompanyController(private val companyLogic: CompanyLogic) {
@PostMapping @PostMapping
@PreAuthorize("hasAuthority('EDIT_COMPANIES')") @PreAuthorize("hasAuthority('EDIT_COMPANIES')")
fun save(@Valid @RequestBody company: CompanyDto) = fun save(@Valid @RequestBody company: CompanyDto) =
created<CompanyDto>(COMPANY_CONTROLLER_PATH) { created<CompanyDto>(Constants.ControllerPaths.COMPANY) {
companyLogic.save(company) companyLogic.save(company)
} }

View File

@ -23,10 +23,6 @@ class MaterialController(
fun getAll() = fun getAll() =
ok(materialLogic.getAll()) ok(materialLogic.getAll())
@GetMapping("notmixtype")
fun getAllNotMixType() =
ok(materialLogic.getAllNotMixType())
@GetMapping("{id}") @GetMapping("{id}")
fun getById(@PathVariable id: Long) = fun getById(@PathVariable id: Long) =
ok(materialLogic.getById(id)) ok(materialLogic.getById(id))
@ -54,10 +50,10 @@ class MaterialController(
@GetMapping("mix/create/{recipeId}") @GetMapping("mix/create/{recipeId}")
fun getAllForMixCreation(@PathVariable recipeId: Long) = fun getAllForMixCreation(@PathVariable recipeId: Long) =
ok(materialLogic.getAllForMixCreation(recipeId)) ok(materialLogic.getAllForRecipe(recipeId))
@GetMapping("mix/update/{mixId}") @GetMapping("mix/update/{mixId}")
fun getAllForMixUpdate(@PathVariable mixId: Long) = fun getAllForMixUpdate(@PathVariable mixId: Long) =
ok(materialLogic.getAllForMixUpdate(mixId)) ok(materialLogic.getAllForMix(mixId))
} }

View File

@ -11,7 +11,6 @@ import dev.fyloz.colorrecipesexplorer.logic.RecipeImageLogic
import dev.fyloz.colorrecipesexplorer.logic.RecipeLogic import dev.fyloz.colorrecipesexplorer.logic.RecipeLogic
import org.springframework.context.annotation.Profile import org.springframework.context.annotation.Profile
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile import org.springframework.web.multipart.MultipartFile
@ -60,20 +59,19 @@ class RecipeController(private val recipeLogic: RecipeLogic, private val recipeI
} }
@GetMapping("{recipeId}/image") @GetMapping("{recipeId}/image")
fun getAllImages(@PathVariable recipeId: Long) = ok { fun getAllImages(@PathVariable recipeId: Long) =
recipeImageLogic.getAllImages(recipeId) ok(recipeImageLogic.getAllImages(recipeId))
}
@PutMapping("{recipeId}/image", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) @PutMapping("{recipeId}/image", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
@PreAuthorizeEditRecipes @PreAuthorizeEditRecipes
fun downloadImage(@PathVariable recipeId: Long, image: MultipartFile): ResponseEntity<RecipeDto> { fun downloadImage(@PathVariable recipeId: Long, image: MultipartFile) =
fileCreated("images/recipes/$recipeId") {
recipeImageLogic.download(image, recipeId) recipeImageLogic.download(image, recipeId)
return getById(recipeId)
} }
@DeleteMapping("{recipeId}/image/{path}") @DeleteMapping("{recipeId}/image/{id}")
@PreAuthorizeEditRecipes @PreAuthorizeEditRecipes
fun deleteImage(@PathVariable recipeId: Long, @PathVariable path: String) = noContent { fun deleteImage(@PathVariable recipeId: Long, @PathVariable id: String) = noContent {
recipeImageLogic.delete(recipeId, path) recipeImageLogic.delete(recipeId, id)
} }
} }

View File

@ -1,5 +1,6 @@
package dev.fyloz.colorrecipesexplorer.rest package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
import dev.fyloz.colorrecipesexplorer.dtos.EntityDto import dev.fyloz.colorrecipesexplorer.dtos.EntityDto
import dev.fyloz.colorrecipesexplorer.model.ModelEntity import dev.fyloz.colorrecipesexplorer.model.ModelEntity
@ -35,6 +36,14 @@ fun okFile(file: Resource, mediaType: String? = null): ResponseEntity<Resource>
.contentType(MediaType.parseMediaType(mediaType ?: DEFAULT_MEDIA_TYPE)) .contentType(MediaType.parseMediaType(mediaType ?: DEFAULT_MEDIA_TYPE))
.body(file) .body(file)
/** Creates a HTTP CREATED [ResponseEntity] for the file created by the given [producer]. */
fun fileCreated(basePath: String, producer: () -> String): ResponseEntity<String> {
val fileName = producer()
val path = "${Constants.ControllerPaths.FILE}?path=$basePath/$fileName"
return ResponseEntity.created(URI.create(path)).body(fileName)
}
/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */ /** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */
fun <T : ModelEntity> created(controllerPath: String, body: T): ResponseEntity<T> = fun <T : ModelEntity> created(controllerPath: String, body: T): ResponseEntity<T> =
created(controllerPath, body, body.id!!) created(controllerPath, body, body.id!!)

View File

@ -1,9 +1,8 @@
package dev.fyloz.colorrecipesexplorer.rest package dev.fyloz.colorrecipesexplorer.rest
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.dtos.TouchUpKitDto
import dev.fyloz.colorrecipesexplorer.logic.TouchUpKitLogic import dev.fyloz.colorrecipesexplorer.logic.TouchUpKitLogic
import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitOutputDto
import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitSaveDto
import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitUpdateDto
import org.springframework.context.annotation.Profile import org.springframework.context.annotation.Profile
import org.springframework.core.io.Resource import org.springframework.core.io.Resource
import org.springframework.http.MediaType import org.springframework.http.MediaType
@ -12,35 +11,29 @@ import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import javax.validation.Valid import javax.validation.Valid
const val TOUCH_UP_KIT_CONTROLLER_PATH = "/api/touchupkit"
@RestController @RestController
@RequestMapping(TOUCH_UP_KIT_CONTROLLER_PATH) @RequestMapping(Constants.ControllerPaths.TOUCH_UP_KIT)
@Profile("!emergency") @Profile("!emergency")
@PreAuthorize("hasAuthority('VIEW_TOUCH_UP_KITS')") @PreAuthorize("hasAuthority('VIEW_TOUCH_UP_KITS')")
class TouchUpKitController( class TouchUpKitController(
private val touchUpKitLogic: TouchUpKitLogic private val touchUpKitLogic: TouchUpKitLogic
) { ) {
@GetMapping @GetMapping
fun getAll() = fun getAll() = ok(touchUpKitLogic.getAll())
ok(touchUpKitLogic.getAllForOutput())
@GetMapping("{id}") @GetMapping("{id}")
fun getById(@PathVariable id: Long) = fun getById(@PathVariable id: Long) = ok(touchUpKitLogic.getById(id))
ok(touchUpKitLogic.getByIdForOutput(id))
@PostMapping @PostMapping
@PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')") @PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')")
fun save(@Valid @RequestBody touchUpKit: TouchUpKitSaveDto) = fun save(@Valid @RequestBody touchUpKit: TouchUpKitDto) =
created<TouchUpKitOutputDto>(TOUCH_UP_KIT_CONTROLLER_PATH) { created<TouchUpKitDto>(Constants.ControllerPaths.TOUCH_UP_KIT) {
with(touchUpKitLogic) { touchUpKitLogic.save(touchUpKit)
save(touchUpKit).toOutput()
}
} }
@PutMapping @PutMapping
@PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')") @PreAuthorize("hasAuthority('EDIT_TOUCH_UP_KITS')")
fun update(@Valid @RequestBody touchUpKit: TouchUpKitUpdateDto) = noContent { fun update(@Valid @RequestBody touchUpKit: TouchUpKitDto) = noContent {
touchUpKitLogic.update(touchUpKit) touchUpKitLogic.update(touchUpKit)
} }

View File

@ -7,16 +7,11 @@ import dev.fyloz.colorrecipesexplorer.logic.files.FileLogic
import dev.fyloz.colorrecipesexplorer.model.Material import dev.fyloz.colorrecipesexplorer.model.Material
import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository
import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Qualifier
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
interface MaterialService : Service<MaterialDto, Material, MaterialRepository> { interface MaterialService : Service<MaterialDto, Material, MaterialRepository> {
/** Checks if a material with the given [name] and a different [id] exists. */ /** Checks if a material with the given [name] and a different [id] exists. */
fun existsByName(name: String, id: Long?): Boolean fun existsByName(name: String, id: Long?): Boolean
/** Gets all non mix type materials. */
fun getAllNotMixType(): Collection<MaterialDto>
/** Updates the [inventoryQuantity] of the [Material] with the given [id]. */ /** Updates the [inventoryQuantity] of the [Material] with the given [id]. */
fun updateInventoryQuantityById(id: Long, inventoryQuantity: Float) fun updateInventoryQuantityById(id: Long, inventoryQuantity: Float)
@ -32,7 +27,7 @@ class DefaultMaterialService(
) : ) :
BaseService<MaterialDto, Material, MaterialRepository>(repository), MaterialService { BaseService<MaterialDto, Material, MaterialRepository>(repository), MaterialService {
override fun existsByName(name: String, id: Long?) = repository.existsByNameAndIdNot(name, id ?: 0) override fun existsByName(name: String, id: Long?) = repository.existsByNameAndIdNot(name, id ?: 0)
override fun getAllNotMixType() = repository.findAllByIsMixTypeIsFalse().map(::toDto) override fun getAll() = repository.findAllByIsMixTypeIsFalse().map(::toDto)
override fun updateInventoryQuantityById(id: Long, inventoryQuantity: Float) = override fun updateInventoryQuantityById(id: Long, inventoryQuantity: Float) =
repository.updateInventoryQuantityById(id, inventoryQuantity) repository.updateInventoryQuantityById(id, inventoryQuantity)
@ -45,20 +40,12 @@ class DefaultMaterialService(
entity.inventoryQuantity, entity.inventoryQuantity,
entity.isMixType, entity.isMixType,
materialTypeService.toDto(entity.materialType!!), materialTypeService.toDto(entity.materialType!!),
getSimdutUrl(entity) hasSimdut(entity)
) )
override fun toEntity(dto: MaterialDto) = override fun toEntity(dto: MaterialDto) =
Material(dto.id, dto.name, dto.inventoryQuantity, dto.isMixType, materialTypeService.toEntity(dto.materialType)) Material(dto.id, dto.name, dto.inventoryQuantity, dto.isMixType, materialTypeService.toEntity(dto.materialType))
private fun getSimdutUrl(material: Material): String? { private fun hasSimdut(material: Material) =
val filePath = "${Constants.FilePaths.SIMDUT}/${material.name}.pdf" fileLogic.exists("${Constants.FilePaths.SIMDUT}/${material.name}.pdf")
if (!fileLogic.exists(filePath)) {
return null
}
val encodedPath = URLEncoder.encode(filePath, StandardCharsets.UTF_8)
return "${Constants.ControllerPaths.FILE}?path=$encodedPath"
}
} }

View File

@ -0,0 +1,34 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent
import dev.fyloz.colorrecipesexplorer.dtos.MixMixTypeDto
import dev.fyloz.colorrecipesexplorer.model.MixMixType
import dev.fyloz.colorrecipesexplorer.repository.MixMixTypeRepository
interface MixMixTypeService : Service<MixMixTypeDto, MixMixType, MixMixTypeRepository> {
fun getAllByMixId(mixId: Long): List<MixMixTypeDto>
fun saveAllForMixId(mixMixTypes: List<MixMixTypeDto>, mixId: Long): List<MixMixTypeDto>
}
@ServiceComponent
class DefaultMixMixTypeService(repository: MixMixTypeRepository, private val mixTypeService: MixTypeService) :
BaseService<MixMixTypeDto, MixMixType, MixMixTypeRepository>(repository), MixMixTypeService {
override fun getAllByMixId(mixId: Long) = repository.findAllByMixId(mixId).map(::toDto)
override fun saveAllForMixId(mixMixTypes: List<MixMixTypeDto>, mixId: Long): List<MixMixTypeDto> {
repository.deleteAllByMixId(mixId)
mixMixTypes.forEach { saveForMixId(it, mixId) }
return getAllByMixId(mixId)
}
fun saveForMixId(mixMixType: MixMixTypeDto, mixId: Long) =
repository.saveForMixId(mixMixType.id, mixMixType.mixType.id, mixId, mixMixType.quantity, mixMixType.position)
override fun toDto(entity: MixMixType) =
MixMixTypeDto(entity.id, mixTypeService.toDto(entity.mixType), entity.quantity, entity.position)
override fun toEntity(dto: MixMixTypeDto) =
MixMixType(dto.id, mixTypeService.toEntity(dto.mixType), dto.quantity, dto.position)
}

View File

@ -2,6 +2,7 @@ package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent
import dev.fyloz.colorrecipesexplorer.dtos.MixDto import dev.fyloz.colorrecipesexplorer.dtos.MixDto
import dev.fyloz.colorrecipesexplorer.dtos.MixQuantitiesDto
import dev.fyloz.colorrecipesexplorer.model.Mix import dev.fyloz.colorrecipesexplorer.model.Mix
import dev.fyloz.colorrecipesexplorer.repository.MixRepository import dev.fyloz.colorrecipesexplorer.repository.MixRepository
@ -17,18 +18,29 @@ interface MixService : Service<MixDto, Mix, MixRepository> {
class DefaultMixService( class DefaultMixService(
repository: MixRepository, repository: MixRepository,
private val mixTypeService: MixTypeService, private val mixTypeService: MixTypeService,
private val mixMaterialService: MixMaterialService private val mixMaterialService: MixMaterialService,
private val mixMixTypeService: MixMixTypeService
) : BaseService<MixDto, Mix, MixRepository>(repository), MixService { ) : BaseService<MixDto, Mix, MixRepository>(repository), MixService {
override fun getAllByMixTypeId(mixTypeId: Long) = repository.findAllByMixTypeId(mixTypeId).map(::toDto) override fun getAllByMixTypeId(mixTypeId: Long) = repository.findAllByMixTypeId(mixTypeId).map(::toDto)
override fun updateLocationById(id: Long, location: String?) = repository.updateLocationById(id, location) override fun updateLocationById(id: Long, location: String?) = repository.updateLocationById(id, location)
override fun save(dto: MixDto): MixDto {
val savedMix = super.save(dto)
val savedMixMixTypes = mixMixTypeService.saveAllForMixId(dto.mixQuantities.mixTypes, savedMix.id)
return savedMix.copy(mixQuantities = savedMix.mixQuantities.copy(mixTypes = savedMixMixTypes))
}
override fun toDto(entity: Mix) = override fun toDto(entity: Mix) =
MixDto( MixDto(
entity.id!!, entity.id!!,
entity.location, entity.location,
entity.recipeId, entity.recipeId,
mixTypeService.toDto(entity.mixType), mixTypeService.toDto(entity.mixType),
entity.mixMaterials.map(mixMaterialService::toDto) MixQuantitiesDto(
entity.mixMaterials.filter { !it.material.isMixType }.map(mixMaterialService::toDto),
mixMixTypeService.getAllByMixId(entity.id)
)
) )
override fun toEntity(dto: MixDto) = override fun toEntity(dto: MixDto) =
@ -37,6 +49,6 @@ class DefaultMixService(
dto.location, dto.location,
dto.recipeId, dto.recipeId,
mixTypeService.toEntity(dto.mixType), mixTypeService.toEntity(dto.mixType),
dto.mixMaterials.map(mixMaterialService::toEntity) dto.mixQuantities.materials.map(mixMaterialService::toEntity)
) )
} }

View File

@ -20,10 +20,14 @@ interface MixTypeService : Service<MixTypeDto, MixType, MixTypeRepository> {
} }
@ServiceComponent @ServiceComponent
class DefaultMixTypeService(repository: MixTypeRepository, val materialService: MaterialService) : class DefaultMixTypeService(
repository: MixTypeRepository,
val materialService: MaterialService,
val materialTypeService: MaterialTypeService
) :
BaseService<MixTypeDto, MixType, MixTypeRepository>(repository), MixTypeService { BaseService<MixTypeDto, MixType, MixTypeRepository>(repository), MixTypeService {
override fun existsByNameAndMaterialType(name: String, materialTypeId: Long, id: Long?) = override fun existsByNameAndMaterialType(name: String, materialTypeId: Long, id: Long?) =
repository.existsByNameAndMaterialType(name, materialTypeId, id ?: 0L) repository.existsByNameAndMaterialTypeAndIdNot(name, materialTypeId, id ?: 0L)
override fun getByNameAndMaterialType(name: String, materialTypeId: Long) = override fun getByNameAndMaterialType(name: String, materialTypeId: Long) =
repository.findByNameAndMaterialType(name, materialTypeId)?.let(::toDto) repository.findByNameAndMaterialType(name, materialTypeId)?.let(::toDto)
@ -32,8 +36,17 @@ class DefaultMixTypeService(repository: MixTypeRepository, val materialService:
override fun isShared(id: Long) = repository.isShared(id) override fun isShared(id: Long) = repository.isShared(id)
override fun toDto(entity: MixType) = override fun toDto(entity: MixType) =
MixTypeDto(entity.id!!, entity.name, materialService.toDto(entity.material)) MixTypeDto(
entity.id!!,
entity.name,
materialTypeService.toDto(entity.materialType),
if (entity.material != null) materialService.toDto(entity.material) else null
)
override fun toEntity(dto: MixTypeDto) = override fun toEntity(dto: MixTypeDto) =
MixType(dto.id, dto.name, materialService.toEntity(dto.material)) MixType(
dto.id, dto.name,
materialTypeService.toEntity(dto.materialType),
if (dto.material != null) materialService.toEntity(dto.material) else null
)
} }

View File

@ -0,0 +1,69 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent
import dev.fyloz.colorrecipesexplorer.dtos.TouchUpKitDto
import dev.fyloz.colorrecipesexplorer.dtos.TouchUpKitProductDto
import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic
import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKit
import dev.fyloz.colorrecipesexplorer.model.touchupkit.TouchUpKitProduct
import dev.fyloz.colorrecipesexplorer.repository.TouchUpKitRepository
import java.time.LocalDate
import java.time.Period
interface TouchUpKitService : Service<TouchUpKitDto, TouchUpKit, TouchUpKitRepository> {
/** Updates the [completionDate] of the touch up kit with the given [id]. */
fun updateCompletionDateById(id: Long, completionDate: LocalDate)
}
@ServiceComponent
class DefaultTouchUpKitService(repository: TouchUpKitRepository, private val configurationLogic: ConfigurationLogic) :
BaseService<TouchUpKitDto, TouchUpKit, TouchUpKitRepository>(repository), TouchUpKitService {
override fun updateCompletionDateById(id: Long, completionDate: LocalDate) =
repository.updateCompletionDateById(id, completionDate)
override fun toDto(entity: TouchUpKit) =
TouchUpKitDto(
entity.id!!,
entity.project,
entity.buggy,
entity.company,
entity.quantity,
entity.shippingDate,
entity.completionDate,
entity.completionDate != null,
isExpired(entity),
entity.finish.split(LIST_DELIMITER),
entity.material.split(LIST_DELIMITER),
entity.content.map(::touchUpKitProductToDto)
)
private fun touchUpKitProductToDto(entity: TouchUpKitProduct) =
TouchUpKitProductDto(entity.id!!, entity.name, entity.description, entity.quantity, entity.ready)
override fun toEntity(dto: TouchUpKitDto) =
TouchUpKit(
dto.id,
dto.project,
dto.buggy,
dto.company,
dto.quantity,
dto.shippingDate,
dto.completionDate,
dto.finish.joinToString(LIST_DELIMITER),
dto.material.joinToString(LIST_DELIMITER),
dto.content.map(::touchUpKitProductToEntity)
)
private fun touchUpKitProductToEntity(dto: TouchUpKitProductDto) =
TouchUpKitProduct(dto.id, dto.name, dto.description, dto.quantity, dto.ready)
private fun isExpired(touchUpKit: TouchUpKit) =
with(Period.parse(configurationLogic.getContent(ConfigurationType.TOUCH_UP_KIT_EXPIRATION))) {
touchUpKit.completionDate != null && touchUpKit.completionDate.plus(this) < LocalDate.now()
}
companion object {
private const val LIST_DELIMITER = ";"
}
}

View File

@ -99,9 +99,9 @@ class DefaultInventoryLogicTest {
mutableListOf(), mutableListOf(),
listOf() listOf()
) )
val mixType = MixTypeDto(1L, "Unit test mix type", material) val mixType = MixTypeDto(1L, "Unit test mix type", materialType)
val mixMaterial = MixMaterialDto(1L, material, 1000f, 1) val mixMaterial = MixMaterialDto(1L, material, 1000f, 1)
val mix = MixDto(1L, null, recipe.id, mixType, listOf(mixMaterial)) val mix = MixDto(1L, null, recipe.id, mixType, MixQuantitiesDto(listOf(mixMaterial), listOf()))
val dto = MixDeductDto(mix.id, 2f) val dto = MixDeductDto(mix.id, 2f)
val expectedQuantities = listOf(MaterialQuantityDto(material.id, mixMaterial.quantity * dto.ratio)) val expectedQuantities = listOf(MaterialQuantityDto(material.id, mixMaterial.quantity * dto.ratio))
@ -136,9 +136,9 @@ class DefaultInventoryLogicTest {
mutableListOf(), mutableListOf(),
listOf() listOf()
) )
val mixType = MixTypeDto(1L, "Unit test mix type", material) val mixType = MixTypeDto(1L, "Unit test mix type", materialType)
val mixMaterial = MixMaterialDto(1L, material, 1000f, 1) val mixMaterial = MixMaterialDto(1L, material, 1000f, 1)
val mix = MixDto(1L, null, recipe.id, mixType, listOf(mixMaterial)) val mix = MixDto(1L, null, recipe.id, mixType, MixQuantitiesDto(listOf(mixMaterial), listOf()))
val dto = MixDeductDto(mix.id, 2f) val dto = MixDeductDto(mix.id, 2f)
val expectedQuantities = listOf(MaterialQuantityDto(material.id, mixMaterial.quantity * dto.ratio)) val expectedQuantities = listOf(MaterialQuantityDto(material.id, mixMaterial.quantity * dto.ratio))

View File

@ -50,7 +50,7 @@ class DefaultMaterialLogicTest {
listOf() listOf()
) )
private val mix = MixDto( private val mix = MixDto(
1L, "location", recipe.id, mixType = MixTypeDto(1L, "Unit test mix type", materialMixType), listOf() 1L, "location", recipe.id, mixType = MixTypeDto(1L, "Unit test mix type", materialType), MixQuantitiesDto()
) )
private val mix2 = mix.copy(id = 2L, mixType = mix.mixType.copy(id = 2L, material = materialMixType2)) private val mix2 = mix.copy(id = 2L, mixType = mix.mixType.copy(id = 2L, material = materialMixType2))
@ -89,18 +89,6 @@ class DefaultMaterialLogicTest {
assertFalse(exists) assertFalse(exists)
} }
@Test
fun getAllNotMixType_normalBehavior_returnsMaterialsFromService() {
// Arrange
every { materialServiceMock.getAllNotMixType() } returns listOf(material)
// Act
val materials = materialLogic.getAllNotMixType()
// Assert
assertContains(materials, material)
}
@Test @Test
fun getAllForMixCreation_normalBehavior_returnsNonMixTypeMaterials() { fun getAllForMixCreation_normalBehavior_returnsNonMixTypeMaterials() {
// Arrange // Arrange
@ -108,7 +96,7 @@ class DefaultMaterialLogicTest {
every { recipeLogicMock.getById(any()) } returns recipe every { recipeLogicMock.getById(any()) } returns recipe
// Act // Act
val materials = materialLogic.getAllForMixCreation(recipe.id) val materials = materialLogic.getAllForRecipe(recipe.id)
// Assert // Assert
assertContains(materials, material) assertContains(materials, material)
@ -123,7 +111,7 @@ class DefaultMaterialLogicTest {
every { recipeLogicMock.getById(any()) } returns recipe every { recipeLogicMock.getById(any()) } returns recipe
// Act // Act
val materials = materialLogic.getAllForMixCreation(recipe.id) val materials = materialLogic.getAllForRecipe(recipe.id)
// Assert // Assert
assertContains(materials, materialMixType2) assertContains(materials, materialMixType2)
@ -137,7 +125,7 @@ class DefaultMaterialLogicTest {
every { mixLogicMock.getById(any()) } returns mix every { mixLogicMock.getById(any()) } returns mix
// Act // Act
val materials = materialLogic.getAllForMixUpdate(mix.id) val materials = materialLogic.getAllForMix(mix.id)
// Assert // Assert
assertContains(materials, material) assertContains(materials, material)
@ -154,26 +142,12 @@ class DefaultMaterialLogicTest {
every { mixLogicMock.getById(any()) } returns mix every { mixLogicMock.getById(any()) } returns mix
// Act // Act
val materials = materialLogic.getAllForMixUpdate(mix.id) val materials = materialLogic.getAllForMix(mix.id)
// Assert // Assert
assertContains(materials, materialMixType2) assertContains(materials, materialMixType2)
} }
@Test
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)
// Assert
assertFalse { materialMixType in materials }
}
@Test @Test
fun save_materialSaveDto_normalBehavior_callsSave() { fun save_materialSaveDto_normalBehavior_callsSave() {
// Arrange // Arrange

View File

@ -11,7 +11,7 @@ class DefaultMixLogicTest {
private val recipeLogicMock = mockk<RecipeLogic>() private val recipeLogicMock = mockk<RecipeLogic>()
private val materialTypeLogicMock = mockk<MaterialTypeLogic>() private val materialTypeLogicMock = mockk<MaterialTypeLogic>()
private val mixTypeLogicMock = mockk<MixTypeLogic>() private val mixTypeLogicMock = mockk<MixTypeLogic>()
private val mixMaterialLogicMock = mockk<MixMaterialLogic>() private val mixQuantityLogicMock = mockk<MixQuantityLogic>()
private val mixLogic = spyk( private val mixLogic = spyk(
DefaultMixLogic( DefaultMixLogic(
@ -19,7 +19,7 @@ class DefaultMixLogicTest {
recipeLogicMock, recipeLogicMock,
materialTypeLogicMock, materialTypeLogicMock,
mixTypeLogicMock, mixTypeLogicMock,
mixMaterialLogicMock mixQuantityLogicMock
) )
) )
@ -40,10 +40,11 @@ class DefaultMixLogicTest {
) )
private val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false) private val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false)
private val mixType = private val mixType =
MixTypeDto(1L, "Unit test mix type", MaterialDto(1L, "Unit test mix type material", 1000f, true, materialType)) MixTypeDto(1L, "Unit test mix type", materialType)
private val mixMaterial = private val mixMaterial =
MixMaterialDto(1L, MaterialDto(2L, "Unit test material", 1000f, false, materialType), 50f, 1) MixMaterialDto(1L, MaterialDto(2L, "Unit test material", 1000f, false, materialType), 50f, 1)
private val mix = MixDto(recipeId = recipe.id, mixType = mixType, mixMaterials = listOf(mixMaterial)) private val mixQuantities = MixQuantitiesDto(listOf(mixMaterial))
private val mix = MixDto(recipeId = recipe.id, mixType = mixType, mixQuantities = mixQuantities)
@AfterEach @AfterEach
internal fun afterEach() { internal fun afterEach() {
@ -54,7 +55,7 @@ class DefaultMixLogicTest {
every { recipeLogicMock.getById(any()) } returns recipe every { recipeLogicMock.getById(any()) } returns recipe
every { materialTypeLogicMock.getById(any()) } returns materialType every { materialTypeLogicMock.getById(any()) } returns materialType
every { mixTypeLogicMock.getOrCreateForNameAndMaterialType(any(), any()) } returns mixType every { mixTypeLogicMock.getOrCreateForNameAndMaterialType(any(), any()) } returns mixType
every { mixMaterialLogicMock.validateAndSaveAll(any()) } returns listOf(mixMaterial) every { mixQuantityLogicMock.validateAndPrepareForMix(any()) } returns mixQuantities
every { mixLogic.save(any<MixDto>()) } returnsArgument 0 every { mixLogic.save(any<MixDto>()) } returnsArgument 0
} }
@ -62,7 +63,7 @@ class DefaultMixLogicTest {
every { recipeLogicMock.getById(any()) } returns recipe every { recipeLogicMock.getById(any()) } returns recipe
every { materialTypeLogicMock.getById(any()) } returns materialType every { materialTypeLogicMock.getById(any()) } returns materialType
every { mixTypeLogicMock.updateOrCreateForNameAndMaterialType(any(), any(), any()) } returns mixType every { mixTypeLogicMock.updateOrCreateForNameAndMaterialType(any(), any(), any()) } returns mixType
every { mixMaterialLogicMock.validateAndSaveAll(any()) } returns listOf(mixMaterial) every { mixQuantityLogicMock.validateAndPrepareForMix(any()) } returns mixQuantities
every { mixLogic.getById(any()) } returns mix every { mixLogic.getById(any()) } returns mix
every { mixLogic.update(any<MixDto>()) } returnsArgument 0 every { mixLogic.update(any<MixDto>()) } returnsArgument 0
} }
@ -73,7 +74,7 @@ class DefaultMixLogicTest {
setup_save_normalBehavior() setup_save_normalBehavior()
val mixMaterialDto = val mixMaterialDto =
MixMaterialSaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position) MixQuantitySaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position, false)
val saveDto = MixSaveDto(0L, mixType.name, recipe.id, materialType.id, listOf(mixMaterialDto)) val saveDto = MixSaveDto(0L, mixType.name, recipe.id, materialType.id, listOf(mixMaterialDto))
// Act // Act
@ -92,11 +93,12 @@ class DefaultMixLogicTest {
val mixMaterialDtos = val mixMaterialDtos =
listOf( listOf(
MixMaterialSaveDto( MixQuantitySaveDto(
mixMaterial.id, mixMaterial.id,
mixMaterial.material.id, mixMaterial.material.id,
mixMaterial.quantity, mixMaterial.quantity,
mixMaterial.position mixMaterial.position,
false
) )
) )
val saveDto = MixSaveDto(0L, mixType.name, recipe.id, materialType.id, mixMaterialDtos) val saveDto = MixSaveDto(0L, mixType.name, recipe.id, materialType.id, mixMaterialDtos)
@ -106,9 +108,9 @@ class DefaultMixLogicTest {
// Assert // Assert
verify { verify {
mixMaterialLogicMock.validateAndSaveAll(mixMaterialDtos) mixQuantityLogicMock.validateAndPrepareForMix(mixMaterialDtos)
} }
confirmVerified(mixMaterialLogicMock) confirmVerified(mixQuantityLogicMock)
} }
@Test @Test
@ -117,7 +119,7 @@ class DefaultMixLogicTest {
setup_update_normalBehavior() setup_update_normalBehavior()
val mixMaterialDto = val mixMaterialDto =
MixMaterialSaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position) MixQuantitySaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position, false)
val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id, materialType.id, listOf(mixMaterialDto)) val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id, materialType.id, listOf(mixMaterialDto))
// Act // Act
@ -135,11 +137,12 @@ class DefaultMixLogicTest {
setup_update_normalBehavior() setup_update_normalBehavior()
val mixMaterialDtos = listOf( val mixMaterialDtos = listOf(
MixMaterialSaveDto( MixQuantitySaveDto(
mixMaterial.id, mixMaterial.id,
mixMaterial.material.id, mixMaterial.material.id,
mixMaterial.quantity, mixMaterial.quantity,
mixMaterial.position mixMaterial.position,
false
) )
) )
val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id, materialType.id, mixMaterialDtos) val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id, materialType.id, mixMaterialDtos)
@ -149,9 +152,9 @@ class DefaultMixLogicTest {
// Assert // Assert
verify { verify {
mixMaterialLogicMock.validateAndSaveAll(mixMaterialDtos) mixQuantityLogicMock.validateAndPrepareForMix(mixMaterialDtos)
} }
confirmVerified(mixMaterialLogicMock) confirmVerified(mixQuantityLogicMock)
} }
@Test @Test

View File

@ -5,18 +5,17 @@ import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto
import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError
import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException
import dev.fyloz.colorrecipesexplorer.service.MixMaterialService
import dev.fyloz.colorrecipesexplorer.utils.PositionUtils import dev.fyloz.colorrecipesexplorer.utils.PositionUtils
import io.mockk.* import io.mockk.*
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
class DefaultMixMaterialLogicTest { class DefaultMixQuantityLogicTest {
private val mixMaterialServiceMock = mockk<MixMaterialService>()
private val materialLogicMock = mockk<MaterialLogic>() private val materialLogicMock = mockk<MaterialLogic>()
private val mixTypeLogicMock = mockk<MixTypeLogic>()
private val mixMaterialLogic = DefaultMixMaterialLogic(mixMaterialServiceMock, materialLogicMock) private val mixMaterialLogic = DefaultMixQuantityLogic(materialLogicMock, mixTypeLogicMock)
@AfterEach @AfterEach
internal fun afterEach() { internal fun afterEach() {
@ -35,7 +34,7 @@ class DefaultMixMaterialLogicTest {
// Act // Act
// Assert // Assert
mixMaterialLogic.validateMixMaterials(setOf(mixMaterial)) mixMaterialLogic.validateMixQuantities(listOf(mixMaterial))
} }
@Test @Test
@ -43,7 +42,7 @@ class DefaultMixMaterialLogicTest {
// Arrange // Arrange
// Act // Act
// Assert // Assert
mixMaterialLogic.validateMixMaterials(setOf()) mixMaterialLogic.validateMixQuantities(listOf())
} }
@Test @Test
@ -58,7 +57,7 @@ class DefaultMixMaterialLogicTest {
// Act // Act
// Assert // Assert
assertThrows<InvalidFirstMixMaterialException> { mixMaterialLogic.validateMixMaterials(setOf(mixMaterial)) } assertThrows<InvalidFirstMixMaterialException> { mixMaterialLogic.validateMixQuantities(listOf(mixMaterial)) }
} }
@Test @Test
@ -75,6 +74,6 @@ class DefaultMixMaterialLogicTest {
// Act // Act
// Assert // Assert
assertThrows<InvalidMixMaterialsPositionsException> { mixMaterialLogic.validateMixMaterials(setOf(mixMaterial)) } assertThrows<InvalidMixMaterialsPositionsException> { mixMaterialLogic.validateMixQuantities(listOf(mixMaterial)) }
} }
} }

View File

@ -1,6 +1,5 @@
package dev.fyloz.colorrecipesexplorer.logic package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto
import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto
import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
@ -9,18 +8,15 @@ import io.mockk.*
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import kotlin.math.exp
import kotlin.test.assertEquals import kotlin.test.assertEquals
class DefaultMixTypeLogicTest { class DefaultMixTypeLogicTest {
private val mixTypeServiceMock = mockk<MixTypeService>() private val mixTypeServiceMock = mockk<MixTypeService>()
private val materialLogicMock = mockk<MaterialLogic>()
private val mixTypeLogic = spyk(DefaultMixTypeLogic(mixTypeServiceMock, materialLogicMock)) private val mixTypeLogic = spyk(DefaultMixTypeLogic(mixTypeServiceMock))
private val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false) private val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false)
private val material = MaterialDto(1L, "Unit test material", 1000f, true, materialType) private val mixType = MixTypeDto(id = 1L, name = "Unit test mix type", materialType)
private val mixType = MixTypeDto(id = 1L, name = "Unit test mix type", material)
@AfterEach @AfterEach
fun afterEach() { fun afterEach() {
@ -43,7 +39,6 @@ class DefaultMixTypeLogicTest {
fun getOrCreateForNameAndMaterialType_notFound_callsSave() { fun getOrCreateForNameAndMaterialType_notFound_callsSave() {
// Arrange // Arrange
every { mixTypeServiceMock.getByNameAndMaterialType(any(), any()) } returns null every { mixTypeServiceMock.getByNameAndMaterialType(any(), any()) } returns null
every { materialLogicMock.save(any<MaterialDto>()) } returns material
every { mixTypeLogic.save(any()) } returnsArgument 0 every { mixTypeLogic.save(any()) } returnsArgument 0
val expectedMixType = mixType.copy(id = 0L) val expectedMixType = mixType.copy(id = 0L)
@ -61,7 +56,6 @@ class DefaultMixTypeLogicTest {
fun getOrCreateForNameAndMaterialType_notFound_returnsFromSave() { fun getOrCreateForNameAndMaterialType_notFound_returnsFromSave() {
// Arrange // Arrange
every { mixTypeServiceMock.getByNameAndMaterialType(any(), any()) } returns null every { mixTypeServiceMock.getByNameAndMaterialType(any(), any()) } returns null
every { materialLogicMock.save(any<MaterialDto>()) } returns material
every { mixTypeLogic.save(any()) } returnsArgument 0 every { mixTypeLogic.save(any()) } returnsArgument 0
val expectedMixType = mixType.copy(id = 0L) val expectedMixType = mixType.copy(id = 0L)
@ -73,11 +67,26 @@ class DefaultMixTypeLogicTest {
assertEquals(expectedMixType, actualMixType) assertEquals(expectedMixType, actualMixType)
} }
@Test
fun updateOrCreateForNameAndMaterialType_alreadyExists_returnsFromgetByNameAndMaterialType() {
// Arrange
every { mixTypeServiceMock.existsByNameAndMaterialType(any(), any(), any()) } returns true
every { mixTypeServiceMock.isShared(any()) } returns true
every { mixTypeServiceMock.getByNameAndMaterialType(any(), any()) } returns mixType
every { mixTypeLogic.save(any()) } returnsArgument 0
// Act
val actualMixType = mixTypeLogic.updateOrCreateForNameAndMaterialType(mixType, mixType.name, materialType)
// Assert
assertEquals(mixType, actualMixType)
}
@Test @Test
fun updateOrCreateForNameAndMaterialType_mixTypeShared_callsSave() { fun updateOrCreateForNameAndMaterialType_mixTypeShared_callsSave() {
// Arrange // Arrange
every { mixTypeServiceMock.existsByNameAndMaterialType(any(), any(), any()) } returns false
every { mixTypeServiceMock.isShared(any()) } returns true every { mixTypeServiceMock.isShared(any()) } returns true
every { materialLogicMock.save(any<MaterialDto>()) } returns material
every { mixTypeLogic.save(any()) } returnsArgument 0 every { mixTypeLogic.save(any()) } returnsArgument 0
val expectedMixType = mixType.copy(id = 0L, name = "${mixType.name} updated") val expectedMixType = mixType.copy(id = 0L, name = "${mixType.name} updated")
@ -94,8 +103,8 @@ class DefaultMixTypeLogicTest {
@Test @Test
fun updateOrCreateForNameAndMaterialType_mixTypeShared_returnsFromSave() { fun updateOrCreateForNameAndMaterialType_mixTypeShared_returnsFromSave() {
// Arrange // Arrange
every { mixTypeServiceMock.existsByNameAndMaterialType(any(), any(), any()) } returns false
every { mixTypeServiceMock.isShared(any()) } returns true every { mixTypeServiceMock.isShared(any()) } returns true
every { materialLogicMock.save(any<MaterialDto>()) } returns material
every { mixTypeLogic.save(any()) } returnsArgument 0 every { mixTypeLogic.save(any()) } returnsArgument 0
val expectedMixType = mixType.copy(id = 0L, name = "${mixType.name} updated") val expectedMixType = mixType.copy(id = 0L, name = "${mixType.name} updated")
@ -110,8 +119,8 @@ class DefaultMixTypeLogicTest {
@Test @Test
fun updateOrCreateForNameAndMaterialType_mixTypeNotShared_callsUpdate() { fun updateOrCreateForNameAndMaterialType_mixTypeNotShared_callsUpdate() {
// Arrange // Arrange
every { mixTypeServiceMock.existsByNameAndMaterialType(any(), any(), any()) } returns false
every { mixTypeServiceMock.isShared(any()) } returns false every { mixTypeServiceMock.isShared(any()) } returns false
every { materialLogicMock.update(any<MaterialDto>()) } returns material
every { mixTypeLogic.update(any()) } returnsArgument 0 every { mixTypeLogic.update(any()) } returnsArgument 0
val expectedMixType = mixType.copy(name = "${mixType.name} updated") val expectedMixType = mixType.copy(name = "${mixType.name} updated")
@ -128,8 +137,8 @@ class DefaultMixTypeLogicTest {
@Test @Test
fun updateOrCreateForNameAndMaterialType_mixTypeNotShared_returnsFromUpdate() { fun updateOrCreateForNameAndMaterialType_mixTypeNotShared_returnsFromUpdate() {
// Arrange // Arrange
every { mixTypeServiceMock.existsByNameAndMaterialType(any(), any(), any()) } returns false
every { mixTypeServiceMock.isShared(any()) } returns false every { mixTypeServiceMock.isShared(any()) } returns false
every { materialLogicMock.update(any<MaterialDto>()) } returns material
every { mixTypeLogic.update(any()) } returnsArgument 0 every { mixTypeLogic.update(any()) } returnsArgument 0
val expectedMixType = mixType.copy(name = "${mixType.name} updated") val expectedMixType = mixType.copy(name = "${mixType.name} updated")

View File

@ -88,10 +88,11 @@ class DefaultRecipeImageLogicTest {
every { fileLogicMock.deleteFromDirectory(any(), any()) } just runs every { fileLogicMock.deleteFromDirectory(any(), any()) } just runs
val recipeImagesDirectoryPath = "${Constants.FilePaths.RECIPE_IMAGES}/$recipeId" val recipeImagesDirectoryPath = "${Constants.FilePaths.RECIPE_IMAGES}/$recipeId"
val imagePath = "$recipeImagesDirectoryPath/1" val imageId = 1.toString()
val imagePath = "$recipeImagesDirectoryPath/$imageId"
// Act // Act
recipeImageLogic.delete(recipeId, imagePath) recipeImageLogic.delete(recipeId, imageId)
// Assert // Assert
verify { verify {

View File

@ -0,0 +1,146 @@
package dev.fyloz.colorrecipesexplorer.logic
import dev.fyloz.colorrecipesexplorer.Constants
import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic
import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic
import dev.fyloz.colorrecipesexplorer.model.ConfigurationType
import dev.fyloz.colorrecipesexplorer.service.TouchUpKitService
import dev.fyloz.colorrecipesexplorer.utils.PdfDocument
import dev.fyloz.colorrecipesexplorer.utils.toByteArrayResource
import io.mockk.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.springframework.core.io.ByteArrayResource
import kotlin.test.assertEquals
class DefaultTouchUpKitLogicTest {
private val touchUpKitServiceMock = mockk<TouchUpKitService>()
private val fileLogicMock = mockk<WriteableFileLogic>()
private val configLogicMock = mockk<ConfigurationLogic>()
private val touchUpKitLogic = spyk(DefaultTouchUpKitLogic(touchUpKitServiceMock, fileLogicMock, configLogicMock))
private val pdfMockData = mockk<ByteArrayResource>()
private val pdfMock = mockk<PdfDocument> {
mockkStatic(PdfDocument::toByteArrayResource)
mockkStatic(PdfDocument::toByteArrayResource)
every { toByteArrayResource() } returns pdfMockData
}
@AfterEach
internal fun afterEach() {
clearAllMocks()
}
@Test
fun complete_normalBehavior_callsUpdateCompletionDateByIdInService() {
// Arrange
every { touchUpKitServiceMock.updateCompletionDateById(any(), any()) } just runs
val touchUpKitId = 1L
// Act
touchUpKitLogic.complete(touchUpKitId)
// Assert
verify {
touchUpKitServiceMock.updateCompletionDateById(touchUpKitId, any())
}
}
@Test
fun generateJobPdfResource_normalBehavior_returnsGeneratedPdf() {
// Arrange
every { touchUpKitLogic.generateJobPdf(any()) } returns pdfMock
every { configLogicMock.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns false.toString()
// Act
val generatedPdfData = touchUpKitLogic.generateJobPdfResource("Unit test job")
// Assert
assertEquals(pdfMockData, generatedPdfData)
}
@Test
fun generateJobPdfResource_normalBehavior_callsCacheJobPdf() {
// Arrange
every { touchUpKitLogic.generateJobPdf(any()) } returns pdfMock
every { configLogicMock.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns false.toString()
val job = "Unit test job"
// Act
touchUpKitLogic.generateJobPdfResource(job)
// Assert
verify {
touchUpKitLogic.cacheJobPdf(job, pdfMock)
}
}
@Test
fun generateJobPdfResource_cacheEnabledAndPdfExists_returnsCachedJobPdf() {
// Arrange
every { configLogicMock.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns true.toString()
every { fileLogicMock.exists(any()) } returns true
every { fileLogicMock.read(any()) } returns pdfMockData
// Act
val pdfData = touchUpKitLogic.generateJobPdfResource("Unit test job")
// Assert
assertEquals(pdfMockData, pdfData)
}
@Test
fun generateJobPdfResource_cacheEnabledAndPdfNotExists_generatesPdf() {
// Arrange
every { touchUpKitLogic.generateJobPdf(any()) } returns pdfMock
every { touchUpKitLogic.cacheJobPdf(any(), any()) } just runs
every { configLogicMock.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns true.toString()
every { fileLogicMock.exists(any()) } returns false
// Act
touchUpKitLogic.generateJobPdfResource("Unit test job")
// Assert
verify {
touchUpKitLogic.cacheJobPdf(any(), any())
}
}
@Test
fun cacheJobPdf_normalBehavior_callsWriteInFileLogic() {
// Arrange
every { configLogicMock.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns true.toString()
every { fileLogicMock.write(any<ByteArrayResource>(), any(), any()) } just runs
val job = "Unit test job"
val pdfPath = "${Constants.FilePaths.TOUCH_UP_KITS}/$job.pdf"
// Act
touchUpKitLogic.cacheJobPdf(job, pdfMock)
// Assert
verify {
fileLogicMock.write(pdfMockData, pdfPath, true)
}
confirmVerified(fileLogicMock)
}
@Test
fun cacheJobPdf_cacheDisabled_doNothing() {
// Arrange
every { configLogicMock.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns false.toString()
every { fileLogicMock.write(any<ByteArrayResource>(), any(), any()) } just runs
// Act
touchUpKitLogic.cacheJobPdf("Unit test job", pdfMock)
// Assert
verify(exactly = 0) {
fileLogicMock.write(any<ByteArrayResource>(), any(), any())
}
confirmVerified(fileLogicMock)
}
}

View File

@ -13,126 +13,126 @@ import org.junit.jupiter.api.Test
import org.springframework.core.io.ByteArrayResource import org.springframework.core.io.ByteArrayResource
import kotlin.test.assertEquals import kotlin.test.assertEquals
private class TouchUpKitServiceTestContext { //private class TouchUpKitServiceTestContext {
val touchUpKitRepository = mockk<TouchUpKitRepository>() // val touchUpKitRepository = mockk<TouchUpKitRepository>()
val fileService = mockk<WriteableFileLogic> { // val fileService = mockk<WriteableFileLogic> {
every { write(any<ByteArrayResource>(), any(), any()) } just Runs // every { write(any<ByteArrayResource>(), any(), any()) } just Runs
} // }
val configService = mockk<ConfigurationLogic>(relaxed = true) // val configService = mockk<ConfigurationLogic>(relaxed = true)
val touchUpKitService = spyk(DefaultTouchUpKitLogic(fileService, configService, touchUpKitRepository)) // val touchUpKitService = spyk(DefaultTouchUpKitLogic(fileService, configService, touchUpKitRepository))
val pdfDocumentData = mockk<ByteArrayResource>() // val pdfDocumentData = mockk<ByteArrayResource>()
val pdfDocument = mockk<PdfDocument> { // val pdfDocument = mockk<PdfDocument> {
mockkStatic(PdfDocument::toByteArrayResource) // mockkStatic(PdfDocument::toByteArrayResource)
mockkStatic(PdfDocument::toByteArrayResource) // mockkStatic(PdfDocument::toByteArrayResource)
every { toByteArrayResource() } returns pdfDocumentData // every { toByteArrayResource() } returns pdfDocumentData
} // }
} //}
class TouchUpKitLogicTest { class TouchUpKitLogicTest {
private val job = "job" // private val job = "job"
//
@AfterEach // @AfterEach
internal fun afterEach() { // internal fun afterEach() {
clearAllMocks() // clearAllMocks()
} // }
//
// generateJobPdf() // // generateJobPdf()
//
@Test // @Test
fun `generateJobPdf() generates a valid PdfDocument for the given job`() { // fun `generateJobPdf() generates a valid PdfDocument for the given job`() {
test { // test {
val generatedPdfDocument = touchUpKitService.generateJobPdf(job) // val generatedPdfDocument = touchUpKitService.generateJobPdf(job)
//
setOf(0, 1).forEach { // setOf(0, 1).forEach {
assertEquals(TOUCH_UP_TEXT_FR, generatedPdfDocument.containers[it].texts[0].text) // assertEquals(TOUCH_UP_TEXT_FR, generatedPdfDocument.containers[it].texts[0].text)
assertEquals(TOUCH_UP_TEXT_EN, generatedPdfDocument.containers[it].texts[1].text) // assertEquals(TOUCH_UP_TEXT_EN, generatedPdfDocument.containers[it].texts[1].text)
assertEquals(job, generatedPdfDocument.containers[it].texts[2].text) // assertEquals(job, generatedPdfDocument.containers[it].texts[2].text)
} // }
} // }
} // }
//
// generateJobPdfResource() // // generateJobPdfResource()
//
@Test // @Test
fun `generateJobPdfResource() generates and returns a ByteArrayResource for the given job then cache it`() { // fun `generateJobPdfResource() generates and returns a ByteArrayResource for the given job then cache it`() {
test { // test {
every { touchUpKitService.generateJobPdf(any()) } returns pdfDocument // every { touchUpKitService.generateJobPdf(any()) } returns pdfDocument
with(touchUpKitService) { // with(touchUpKitService) {
every { job.cachePdfDocument(pdfDocument) } just Runs // every { job.cachePdfDocument(pdfDocument) } just Runs
} // }
//
val generatedResource = touchUpKitService.generateJobPdfResource(job) // val generatedResource = touchUpKitService.generateJobPdfResource(job)
//
assertEquals(pdfDocumentData, generatedResource) // assertEquals(pdfDocumentData, generatedResource)
//
verify { // verify {
with(touchUpKitService) { // with(touchUpKitService) {
job.cachePdfDocument(pdfDocument) // job.cachePdfDocument(pdfDocument)
} // }
} // }
} // }
} // }
//
@Test // @Test
fun `generateJobPdfResource() returns a cached ByteArrayResource from the FileService when caching is enabled and a cached file eixsts for the given job`() { // fun `generateJobPdfResource() returns a cached ByteArrayResource from the FileService when caching is enabled and a cached file eixsts for the given job`() {
test { // test {
enableCachePdf() // enableCachePdf()
every { fileService.exists(any()) } returns true // every { fileService.exists(any()) } returns true
every { fileService.read(any()) } returns pdfDocumentData // every { fileService.read(any()) } returns pdfDocumentData
every { configService.get(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns configuration( // every { configService.get(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns configuration(
ConfigurationType.TOUCH_UP_KIT_CACHE_PDF, // ConfigurationType.TOUCH_UP_KIT_CACHE_PDF,
"true" // "true"
) // )
//
val redResource = touchUpKitService.generateJobPdfResource(job) // val redResource = touchUpKitService.generateJobPdfResource(job)
//
assertEquals(pdfDocumentData, redResource) // assertEquals(pdfDocumentData, redResource)
} // }
} // }
//
// String.cachePdfDocument() // // String.cachePdfDocument()
//
@Test // @Test
fun `cachePdfDocument() does nothing when caching is disabled`() { // fun `cachePdfDocument() does nothing when caching is disabled`() {
test { // test {
disableCachePdf() // disableCachePdf()
//
with(touchUpKitService) { // with(touchUpKitService) {
job.cachePdfDocument(pdfDocument) // job.cachePdfDocument(pdfDocument)
} // }
//
verify(exactly = 0) { // verify(exactly = 0) {
fileService.write(any<ByteArrayResource>(), any(), any()) // fileService.write(any<ByteArrayResource>(), any(), any())
} // }
} // }
} // }
//
@Test // @Test
fun `cachePdfDocument() writes the given document to the FileService when cache is enabled`() { // fun `cachePdfDocument() writes the given document to the FileService when cache is enabled`() {
test { // test {
enableCachePdf() // enableCachePdf()
//
with(touchUpKitService) { // with(touchUpKitService) {
job.cachePdfDocument(pdfDocument) // job.cachePdfDocument(pdfDocument)
} // }
//
verify { // verify {
fileService.write(pdfDocumentData, any(), true) // fileService.write(pdfDocumentData, any(), true)
} // }
} // }
} // }
//
private fun TouchUpKitServiceTestContext.enableCachePdf() = // private fun TouchUpKitServiceTestContext.enableCachePdf() =
this.setCachePdf(true) // this.setCachePdf(true)
//
private fun TouchUpKitServiceTestContext.disableCachePdf() = // private fun TouchUpKitServiceTestContext.disableCachePdf() =
this.setCachePdf(false) // this.setCachePdf(false)
//
private fun TouchUpKitServiceTestContext.setCachePdf(enabled: Boolean) { // private fun TouchUpKitServiceTestContext.setCachePdf(enabled: Boolean) {
every { configService.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns enabled.toString() // every { configService.getContent(ConfigurationType.TOUCH_UP_KIT_CACHE_PDF) } returns enabled.toString()
} // }
//
private fun test(test: TouchUpKitServiceTestContext.() -> Unit) { // private fun test(test: TouchUpKitServiceTestContext.() -> Unit) {
TouchUpKitServiceTestContext().test() // TouchUpKitServiceTestContext().test()
} // }
} }