mirror of
https://github.com/Dannecron/spring-boot-demo.git
synced 2025-12-26 00:32:34 +03:00
move and refactor city and product services to core
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
package com.github.dannecron.demo.services.database.city
|
||||
|
||||
import com.github.dannecron.demo.db.entity.City
|
||||
import com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
|
||||
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
|
||||
import com.github.dannecron.demo.services.kafka.dto.CityCreateDto
|
||||
import org.springframework.stereotype.Service
|
||||
import java.util.*
|
||||
|
||||
@Service
|
||||
interface CityService {
|
||||
fun findByGuid(guid: UUID): City?
|
||||
|
||||
fun create(name: String): City
|
||||
fun create(kafkaCityDto: CityCreateDto): City
|
||||
|
||||
@Throws(CityNotFoundException::class, AlreadyDeletedException::class)
|
||||
fun delete(guid: UUID): City
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package com.github.dannecron.demo.services.database.city
|
||||
|
||||
import com.github.dannecron.demo.core.services.generation.CommonGenerator
|
||||
import com.github.dannecron.demo.db.entity.City
|
||||
import com.github.dannecron.demo.db.repository.CityRepository
|
||||
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
|
||||
import com.github.dannecron.demo.services.database.exceptions.CityNotFoundException
|
||||
import com.github.dannecron.demo.services.kafka.dto.CityCreateDto
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.UUID
|
||||
|
||||
@Service
|
||||
class CityServiceImpl(
|
||||
private val cityRepository: CityRepository,
|
||||
private val commonGenerator: CommonGenerator,
|
||||
): CityService {
|
||||
override fun findByGuid(guid: UUID): City? = cityRepository.findByGuid(guid)
|
||||
|
||||
override fun create(name: String): City = City(
|
||||
id = null,
|
||||
guid = commonGenerator.generateUUID(),
|
||||
name = name,
|
||||
createdAt = commonGenerator.generateCurrentTime(),
|
||||
updatedAt = null,
|
||||
deletedAt = null,
|
||||
).let {
|
||||
cityRepository.save(it)
|
||||
}
|
||||
|
||||
override fun create(kafkaCityDto: CityCreateDto): City = City(
|
||||
id = null,
|
||||
guid = UUID.fromString(kafkaCityDto.guid),
|
||||
name = kafkaCityDto.name,
|
||||
createdAt = OffsetDateTime.parse(kafkaCityDto.createdAt, DateTimeFormatter.ISO_OFFSET_DATE_TIME),
|
||||
updatedAt = kafkaCityDto.deletedAt?.let {
|
||||
OffsetDateTime.parse(it, DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||
},
|
||||
deletedAt = kafkaCityDto.deletedAt?.let {
|
||||
OffsetDateTime.parse(it, DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||
},
|
||||
).let {
|
||||
cityRepository.save(it)
|
||||
}
|
||||
|
||||
override fun delete(guid: UUID): City {
|
||||
val city = findByGuid(guid) ?: throw CityNotFoundException()
|
||||
|
||||
if (city.isDeleted()) {
|
||||
throw AlreadyDeletedException()
|
||||
}
|
||||
|
||||
return cityRepository.save(
|
||||
city.copy(
|
||||
deletedAt = OffsetDateTime.now(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.github.dannecron.demo.services.database.product
|
||||
|
||||
import com.github.dannecron.demo.db.entity.Product
|
||||
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
|
||||
import com.github.dannecron.demo.services.database.exceptions.ProductNotFoundException
|
||||
import com.github.dannecron.demo.services.kafka.exceptions.InvalidArgumentException
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.stereotype.Service
|
||||
import java.util.UUID
|
||||
|
||||
@Service
|
||||
interface ProductService {
|
||||
fun findByGuid(guid: UUID): Product?
|
||||
|
||||
fun findAll(pageable: Pageable): Page<Product>
|
||||
|
||||
fun create(name: String, price: Long, description: String?): Product
|
||||
|
||||
@Throws(ProductNotFoundException::class, AlreadyDeletedException::class)
|
||||
fun delete(guid: UUID): Product
|
||||
|
||||
@Throws(ProductNotFoundException::class, InvalidArgumentException::class)
|
||||
fun syncToKafka(guid: UUID, topic: String?)
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package com.github.dannecron.demo.services.database.product
|
||||
|
||||
import com.github.dannecron.demo.core.services.generation.CommonGenerator
|
||||
import com.github.dannecron.demo.db.entity.Product
|
||||
import com.github.dannecron.demo.db.repository.ProductRepository
|
||||
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
|
||||
import com.github.dannecron.demo.services.database.exceptions.ProductNotFoundException
|
||||
import com.github.dannecron.demo.services.kafka.Producer
|
||||
import com.github.dannecron.demo.services.kafka.dto.ProductDto
|
||||
import com.github.dannecron.demo.services.kafka.exceptions.InvalidArgumentException
|
||||
import com.github.dannecron.demo.utils.LoggerDelegate
|
||||
import net.logstash.logback.marker.Markers
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.UUID
|
||||
|
||||
@Service
|
||||
class ProductServiceImpl(
|
||||
private val productRepository: ProductRepository,
|
||||
private val producer: Producer,
|
||||
private val commonGenerator: CommonGenerator,
|
||||
): ProductService {
|
||||
private val logger by LoggerDelegate()
|
||||
|
||||
override fun findByGuid(guid: UUID): Product? = productRepository.findByGuid(guid)
|
||||
.also {
|
||||
logger.debug(
|
||||
Markers.appendEntries(mapOf("guid" to guid, "idResult" to it?.id)),
|
||||
"find product by guid",
|
||||
)
|
||||
}
|
||||
|
||||
override fun findAll(pageable: Pageable): Page<Product> = productRepository.findAll(pageable)
|
||||
|
||||
override fun create(name: String, price: Long, description: String?): Product {
|
||||
val product = Product(
|
||||
id = null,
|
||||
guid = commonGenerator.generateUUID(),
|
||||
name = name,
|
||||
description = description,
|
||||
price = price,
|
||||
createdAt = commonGenerator.generateCurrentTime(),
|
||||
updatedAt = null,
|
||||
deletedAt = null,
|
||||
)
|
||||
|
||||
return productRepository.save(product)
|
||||
}
|
||||
|
||||
override fun delete(guid: UUID): Product {
|
||||
val product = findByGuid(guid) ?: throw ProductNotFoundException()
|
||||
|
||||
if (product.isDeleted()) {
|
||||
throw AlreadyDeletedException()
|
||||
}
|
||||
|
||||
val deletedProduct = product.copy(
|
||||
deletedAt = commonGenerator.generateCurrentTime(),
|
||||
)
|
||||
|
||||
return productRepository.save(deletedProduct)
|
||||
}
|
||||
|
||||
override fun syncToKafka(guid: UUID, topic: String?) {
|
||||
val product = findByGuid(guid) ?: throw ProductNotFoundException()
|
||||
|
||||
producer.produceProductSync(product.toKafkaDto())
|
||||
}
|
||||
|
||||
private fun Product.toKafkaDto() = ProductDto(
|
||||
id = id ?: throw InvalidArgumentException("product.id"),
|
||||
guid = guid.toString(),
|
||||
name = name,
|
||||
description = description,
|
||||
price = price,
|
||||
createdAt = createdAt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
|
||||
updatedAt = updatedAt?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
|
||||
deletedAt = deletedAt?.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
|
||||
)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.github.dannecron.demo.utils
|
||||
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.reflect.full.companionObject
|
||||
|
||||
/**
|
||||
* usage: `private val logger by LoggerDelegate()`
|
||||
*/
|
||||
class LoggerDelegate<in R : Any> : ReadOnlyProperty<R, Logger> {
|
||||
override fun getValue(thisRef: R, property: KProperty<*>)
|
||||
= getLogger(getClassForLogging(thisRef.javaClass))
|
||||
|
||||
private fun <T : Any> getClassForLogging(javaClass: Class<T>): Class<*> {
|
||||
return javaClass.enclosingClass?.takeIf {
|
||||
it.kotlin.companionObject?.java == javaClass
|
||||
} ?: javaClass
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getLogger(forClass: Class<*>): Logger = LoggerFactory.getLogger(forClass)
|
||||
@@ -1,70 +0,0 @@
|
||||
package com.github.dannecron.demo.services.database.city
|
||||
|
||||
import com.github.dannecron.demo.core.services.generation.CommonGenerator
|
||||
import com.github.dannecron.demo.db.entity.City
|
||||
import com.github.dannecron.demo.db.repository.CityRepository
|
||||
import com.github.dannecron.demo.services.kafka.dto.CityCreateDto
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.times
|
||||
import org.mockito.kotlin.verify
|
||||
import org.mockito.kotlin.whenever
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.UUID
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class CityServiceImplTest {
|
||||
private val mockGuid = UUID.randomUUID()
|
||||
private val mockCurrentTime = OffsetDateTime.now()
|
||||
|
||||
private val commonGenerator: CommonGenerator = mock {
|
||||
on { generateUUID() } doReturn mockGuid
|
||||
on { generateCurrentTime() } doReturn mockCurrentTime
|
||||
}
|
||||
private val cityRepository: CityRepository = mock()
|
||||
private val cityServiceImpl = CityServiceImpl(cityRepository, commonGenerator)
|
||||
|
||||
private val city = City(
|
||||
id = 1000,
|
||||
guid = mockGuid,
|
||||
name = "name",
|
||||
createdAt = mockCurrentTime,
|
||||
updatedAt = null,
|
||||
deletedAt = null,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `create - by name`() {
|
||||
whenever(cityRepository.save(any<City>())).thenReturn(city)
|
||||
|
||||
val result = cityServiceImpl.create("name")
|
||||
assertEquals(city, result)
|
||||
|
||||
verify(cityRepository, times(1)).save(city.copy(id = null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create - by dto`() {
|
||||
val cityGuid = UUID.randomUUID()
|
||||
val createdAt = OffsetDateTime.now()
|
||||
val cityCreate = CityCreateDto(
|
||||
guid = cityGuid.toString(),
|
||||
name = "name",
|
||||
createdAt = createdAt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
|
||||
updatedAt = null,
|
||||
deletedAt = null,
|
||||
)
|
||||
|
||||
val expectedCity = city.copy(guid = cityGuid, createdAt = createdAt)
|
||||
|
||||
whenever(cityRepository.save(any<City>())).thenReturn(expectedCity)
|
||||
|
||||
val result = cityServiceImpl.create(cityCreate)
|
||||
assertEquals(expectedCity, result)
|
||||
|
||||
verify(cityRepository, times(1)).save(expectedCity.copy(id = null))
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package com.github.dannecron.demo.services.database.product
|
||||
|
||||
import com.github.dannecron.demo.core.services.generation.CommonGenerator
|
||||
import com.github.dannecron.demo.db.entity.Product
|
||||
import com.github.dannecron.demo.db.repository.ProductRepository
|
||||
import com.github.dannecron.demo.services.database.exceptions.AlreadyDeletedException
|
||||
import com.github.dannecron.demo.services.database.exceptions.ProductNotFoundException
|
||||
import com.github.dannecron.demo.services.kafka.Producer
|
||||
import com.github.dannecron.demo.services.kafka.dto.ProductDto
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.never
|
||||
import org.mockito.kotlin.times
|
||||
import org.mockito.kotlin.verify
|
||||
import org.mockito.kotlin.verifyNoInteractions
|
||||
import org.mockito.kotlin.whenever
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.UUID
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ProductServiceImplTest {
|
||||
private val mockGuid = UUID.randomUUID()
|
||||
private val mockCurrentTime = OffsetDateTime.now()
|
||||
|
||||
private val productRepository: ProductRepository = mock()
|
||||
private val producer: Producer = mock()
|
||||
private val commonGenerator: CommonGenerator = mock {
|
||||
on { generateUUID() } doReturn mockGuid
|
||||
on { generateCurrentTime() } doReturn mockCurrentTime
|
||||
}
|
||||
|
||||
private val productService = ProductServiceImpl(
|
||||
productRepository = productRepository,
|
||||
producer = producer,
|
||||
commonGenerator = commonGenerator,
|
||||
)
|
||||
|
||||
private val guid = UUID.randomUUID()
|
||||
private val product = Product(
|
||||
id = 123,
|
||||
guid = guid,
|
||||
name = "name",
|
||||
description = "description",
|
||||
price = 10050,
|
||||
createdAt = OffsetDateTime.now().minusDays(1),
|
||||
updatedAt = OffsetDateTime.now().minusHours(2),
|
||||
deletedAt = null,
|
||||
)
|
||||
private val kafkaProductDto = ProductDto(
|
||||
id = 123,
|
||||
guid = guid.toString(),
|
||||
name = "name",
|
||||
description = "description",
|
||||
price = 10050,
|
||||
createdAt = product.createdAt.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
|
||||
updatedAt = product.updatedAt!!.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
|
||||
deletedAt = null,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun create() {
|
||||
val expectedProductForCreation = product.copy(
|
||||
id = null,
|
||||
guid = mockGuid,
|
||||
createdAt = mockCurrentTime,
|
||||
updatedAt = null,
|
||||
)
|
||||
val expectedCreatedProduct = expectedProductForCreation.copy(id = 1)
|
||||
|
||||
whenever(productRepository.save<Product>(any())).thenReturn(expectedCreatedProduct)
|
||||
|
||||
val result = productService.create(
|
||||
name = "name",
|
||||
price = 10050,
|
||||
description = "description",
|
||||
)
|
||||
assertEquals(expectedCreatedProduct, result)
|
||||
|
||||
verify(productRepository, times(1)).save(expectedProductForCreation)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete - success`() {
|
||||
val deletedProduct = product.copy(
|
||||
deletedAt = mockCurrentTime,
|
||||
)
|
||||
whenever(productRepository.findByGuid(any())).thenReturn(product)
|
||||
whenever(productRepository.save<Product>(any())).thenReturn(deletedProduct)
|
||||
|
||||
val result = productService.delete(guid)
|
||||
assertEquals(deletedProduct, result)
|
||||
|
||||
verify(productRepository, times(1)).findByGuid(guid)
|
||||
verify(productRepository, times(1)).save(deletedProduct)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete - fail - already deleted`() {
|
||||
val deletedProduct = product.copy(
|
||||
deletedAt = mockCurrentTime,
|
||||
)
|
||||
whenever(productRepository.findByGuid(any())).thenReturn(deletedProduct)
|
||||
|
||||
assertThrows<AlreadyDeletedException> {
|
||||
productService.delete(guid)
|
||||
}
|
||||
|
||||
verify(productRepository, times(1)).findByGuid(guid)
|
||||
verify(productRepository, never()).save(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete - fail - not found`() {
|
||||
whenever(productRepository.findByGuid(any())).thenReturn(null)
|
||||
|
||||
assertThrows<ProductNotFoundException> {
|
||||
productService.delete(guid)
|
||||
}
|
||||
|
||||
verify(productRepository, times(1)).findByGuid(guid)
|
||||
verify(productRepository, never()).save(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncToKafka - success`() {
|
||||
whenever(productRepository.findByGuid(any())) doReturn product
|
||||
|
||||
productService.syncToKafka(guid, null)
|
||||
|
||||
verify(productRepository, times(1)).findByGuid(guid)
|
||||
verify(producer, times(1)).produceProductSync(kafkaProductDto)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `syncToKafka - not found`() {
|
||||
whenever(productRepository.findByGuid(any())) doReturn null
|
||||
|
||||
assertThrows<ProductNotFoundException> {
|
||||
productService.syncToKafka(guid, null)
|
||||
}
|
||||
|
||||
verify(productRepository, times(1)).findByGuid(guid)
|
||||
verifyNoInteractions(producer)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user