move and refactor city and product services to core

This commit is contained in:
Savosin Denis
2025-05-06 14:09:01 +07:00
parent b855aba506
commit f92ba7795d
18 changed files with 264 additions and 202 deletions

View File

@@ -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
}

View File

@@ -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(),
)
)
}
}

View File

@@ -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?)
}

View File

@@ -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),
)
}

View File

@@ -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)

View File

@@ -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))
}
}

View File

@@ -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)
}
}