From f4068f211eec0f606f21a87fce397d61e56f7740 Mon Sep 17 00:00:00 2001 From: Savosin Denis Date: Mon, 12 May 2025 14:13:52 +0700 Subject: [PATCH] move and refactor customer service to core fix core dto serialization --- .../github/dannecron/demo/core/dto/City.kt | 8 + .../dannecron/demo/core/dto/Customer.kt | 20 +++ .../github/dannecron/demo/core/dto/Product.kt | 8 + .../demo/core/dto/view/CustomerExtended.kt | 11 ++ .../core/services/customer/CustomerService.kt | 13 ++ .../services/customer/CustomerServiceImpl.kt | 60 +++++++ .../customer/CustomerServiceImplTest.kt | 146 ++++++++++++++++++ 7 files changed, 266 insertions(+) create mode 100644 core/src/main/kotlin/com/github/dannecron/demo/core/dto/Customer.kt create mode 100644 core/src/main/kotlin/com/github/dannecron/demo/core/dto/view/CustomerExtended.kt create mode 100644 core/src/main/kotlin/com/github/dannecron/demo/core/services/customer/CustomerService.kt create mode 100644 core/src/main/kotlin/com/github/dannecron/demo/core/services/customer/CustomerServiceImpl.kt create mode 100644 core/src/test/kotlin/com/github/dannecron/demo/core/services/customer/CustomerServiceImplTest.kt diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/City.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/City.kt index dcbb55e..6f5c37c 100644 --- a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/City.kt +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/City.kt @@ -1,14 +1,22 @@ package com.github.dannecron.demo.core.dto +import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization +import com.github.dannecron.demo.db.serialialization.UuidSerialization +import kotlinx.serialization.Serializable import java.time.OffsetDateTime import java.util.UUID +@Serializable data class City( val id: Long, + @Serializable(with = UuidSerialization::class) val guid: UUID, val name: String, + @Serializable(with = OffsetDateTimeSerialization::class) val createdAt: OffsetDateTime, + @Serializable(with = OffsetDateTimeSerialization::class) val updatedAt: OffsetDateTime?, + @Serializable(with = OffsetDateTimeSerialization::class) val deletedAt: OffsetDateTime?, ) { fun isDeleted(): Boolean = deletedAt != null diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Customer.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Customer.kt new file mode 100644 index 0000000..9404ac6 --- /dev/null +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Customer.kt @@ -0,0 +1,20 @@ +package com.github.dannecron.demo.core.dto + +import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization +import com.github.dannecron.demo.db.serialialization.UuidSerialization +import kotlinx.serialization.Serializable +import java.time.OffsetDateTime +import java.util.UUID + +@Serializable +data class Customer( + val id: Long, + @Serializable(with = UuidSerialization::class) + val guid: UUID, + val name: String, + val cityId: Long?, + @Serializable(with = OffsetDateTimeSerialization::class) + val createdAt: OffsetDateTime, + @Serializable(with = OffsetDateTimeSerialization::class) + val updatedAt: OffsetDateTime?, +) diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Product.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Product.kt index afb9961..140357a 100644 --- a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Product.kt +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/Product.kt @@ -1,18 +1,26 @@ package com.github.dannecron.demo.core.dto +import com.github.dannecron.demo.db.serialialization.OffsetDateTimeSerialization +import com.github.dannecron.demo.db.serialialization.UuidSerialization +import kotlinx.serialization.Serializable import java.time.OffsetDateTime import java.util.UUID import kotlin.math.pow import kotlin.math.roundToInt +@Serializable data class Product( val id: Long, + @Serializable(with = UuidSerialization::class) val guid: UUID, val name: String, val description: String?, val price: Long, + @Serializable(with = OffsetDateTimeSerialization::class) val createdAt: OffsetDateTime, + @Serializable(with = OffsetDateTimeSerialization::class) val updatedAt: OffsetDateTime?, + @Serializable(with = OffsetDateTimeSerialization::class) val deletedAt: OffsetDateTime?, ) { fun getPriceDouble(): Double = (price.toDouble() / 100).roundTo(2) diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/dto/view/CustomerExtended.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/view/CustomerExtended.kt new file mode 100644 index 0000000..c661bb6 --- /dev/null +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/dto/view/CustomerExtended.kt @@ -0,0 +1,11 @@ +package com.github.dannecron.demo.core.dto.view + +import com.github.dannecron.demo.core.dto.City +import com.github.dannecron.demo.core.dto.Customer +import kotlinx.serialization.Serializable + +@Serializable +data class CustomerExtended( + val customer: Customer, + val city: City?, +) diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/services/customer/CustomerService.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/services/customer/CustomerService.kt new file mode 100644 index 0000000..8f9f113 --- /dev/null +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/services/customer/CustomerService.kt @@ -0,0 +1,13 @@ +package com.github.dannecron.demo.core.services.customer + +import com.github.dannecron.demo.core.dto.Customer +import com.github.dannecron.demo.core.dto.view.CustomerExtended +import com.github.dannecron.demo.core.exceptions.CityNotFoundException +import java.util.UUID + +interface CustomerService { + fun findByGuid(guid: UUID): CustomerExtended? + + @Throws(CityNotFoundException::class) + fun create(name: String, cityGuid: UUID?): Customer +} diff --git a/core/src/main/kotlin/com/github/dannecron/demo/core/services/customer/CustomerServiceImpl.kt b/core/src/main/kotlin/com/github/dannecron/demo/core/services/customer/CustomerServiceImpl.kt new file mode 100644 index 0000000..6657f12 --- /dev/null +++ b/core/src/main/kotlin/com/github/dannecron/demo/core/services/customer/CustomerServiceImpl.kt @@ -0,0 +1,60 @@ +package com.github.dannecron.demo.core.services.customer + +import com.github.dannecron.demo.core.dto.City +import com.github.dannecron.demo.core.dto.Customer +import com.github.dannecron.demo.core.dto.view.CustomerExtended +import com.github.dannecron.demo.core.exceptions.CityNotFoundException +import com.github.dannecron.demo.core.services.generation.CommonGenerator +import com.github.dannecron.demo.db.entity.CityEntity +import com.github.dannecron.demo.db.entity.CustomerEntity +import com.github.dannecron.demo.db.repository.CityRepository +import com.github.dannecron.demo.db.repository.CustomerRepository +import org.springframework.stereotype.Service +import java.util.UUID +import kotlin.jvm.optionals.getOrNull + +@Service +class CustomerServiceImpl( + private val customerRepository: CustomerRepository, + private val cityRepository: CityRepository, + private val commonGenerator: CommonGenerator, +) : CustomerService { + override fun findByGuid(guid: UUID): CustomerExtended? = customerRepository.findByGuid(guid) + ?.let { customer -> + CustomerExtended( + customer = customer.toCore(), + city = customer.cityId?.let { cityId -> cityRepository.findById(cityId).getOrNull()?.toCore() } + ) + } + + @Throws(CityNotFoundException::class) + override fun create(name: String, cityGuid: UUID?): Customer = CustomerEntity( + id = null, + guid = commonGenerator.generateUUID(), + name = name, + cityId = cityGuid?.let { + cityRepository.findByGuid(it)?.id ?: throw CityNotFoundException() + }, + createdAt = commonGenerator.generateCurrentTime(), + updatedAt = null, + ).let(customerRepository::save) + .toCore() + + private fun CustomerEntity.toCore() = Customer( + id = id!!, + guid = guid, + name = name, + cityId = cityId, + createdAt = createdAt, + updatedAt = updatedAt, + ) + + private fun CityEntity.toCore() = City( + id = id!!, + guid = guid, + name = name, + createdAt = createdAt, + updatedAt = updatedAt, + deletedAt = deletedAt, + ) +} diff --git a/core/src/test/kotlin/com/github/dannecron/demo/core/services/customer/CustomerServiceImplTest.kt b/core/src/test/kotlin/com/github/dannecron/demo/core/services/customer/CustomerServiceImplTest.kt new file mode 100644 index 0000000..3e1b02c --- /dev/null +++ b/core/src/test/kotlin/com/github/dannecron/demo/core/services/customer/CustomerServiceImplTest.kt @@ -0,0 +1,146 @@ +package com.github.dannecron.demo.core.services.customer + +import com.github.dannecron.demo.core.dto.City +import com.github.dannecron.demo.core.dto.Customer +import com.github.dannecron.demo.core.dto.view.CustomerExtended +import com.github.dannecron.demo.core.exceptions.CityNotFoundException +import com.github.dannecron.demo.core.services.generation.CommonGenerator +import com.github.dannecron.demo.db.entity.CityEntity +import com.github.dannecron.demo.db.entity.CustomerEntity +import com.github.dannecron.demo.db.repository.CityRepository +import com.github.dannecron.demo.db.repository.CustomerRepository +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.util.Optional +import java.util.UUID +import kotlin.test.assertEquals + +class CustomerServiceImplTest { + 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 customerRepository: CustomerRepository = mock() + private val cityRepository: CityRepository = mock() + private val customerServiceImpl = CustomerServiceImpl( + customerRepository = customerRepository, + cityRepository = cityRepository, + commonGenerator = commonGenerator, + ) + + private val cityId = 123L + private val cityGuid = UUID.randomUUID() + private val createdAt = OffsetDateTime.now() + + private val customerEntity = CustomerEntity( + id = 1, + guid = mockGuid, + name = "name", + cityId = cityId, + createdAt = mockCurrentTime, + updatedAt = null, + ) + private val customer = Customer( + id = 1, + guid = mockGuid, + name = "name", + cityId = cityId, + createdAt = mockCurrentTime, + updatedAt = null, + ) + + private val cityEntity = CityEntity( + id = cityId, + guid = cityGuid, + name = "city", + createdAt = createdAt, + updatedAt = null, + deletedAt = null, + ) + private val city = City( + id = cityId, + guid = cityGuid, + name = "city", + createdAt = createdAt, + updatedAt = null, + deletedAt = null, + ) + + @Test + fun `create - success - with city`() { + whenever(customerRepository.save(any())).thenReturn(customerEntity) + whenever(cityRepository.findByGuid(cityGuid)).thenReturn(cityEntity) + + val result = customerServiceImpl.create("name", cityGuid) + assertEquals(customer, result) + + verify(customerRepository, times(1)).save(customerEntity.copy(id = null)) + verify(cityRepository, times(1)).findByGuid(cityGuid) + } + + @Test + fun `create - success - no city`() { + val customerNoCityEntity = customerEntity.copy(cityId = null) + val customerNoCity = customer.copy(cityId = null) + + whenever(customerRepository.save(any())).thenReturn(customerNoCityEntity) + + val result = customerServiceImpl.create("name", null) + assertEquals(customerNoCity, result) + + verify(customerRepository, times(1)).save(customerNoCityEntity.copy(id = null)) + verifyNoInteractions(cityRepository) + } + + @Test + fun `create - fail - with city`() { + whenever(customerRepository.save(any())).thenReturn(customerEntity) + whenever(cityRepository.findByGuid(cityGuid)).thenReturn(null) + + assertThrows { + customerServiceImpl.create("name", cityGuid) + } + + verify(customerRepository, never()).save(customerEntity.copy(id = null)) + verify(cityRepository, times(1)).findByGuid(cityGuid) + } + + @Test + fun `findByGuid - with city`() { + val customerGuid = mockGuid + whenever(customerRepository.findByGuid(any())).thenReturn(customerEntity) + whenever(cityRepository.findById(any())).thenReturn(Optional.of(cityEntity)) + + val result = customerServiceImpl.findByGuid(customerGuid) + assertEquals(CustomerExtended(customer, city), result) + + verify(customerRepository, times(1)).findByGuid(customerGuid) + verify(cityRepository, times(1)).findById(cityId) + } + + @Test + fun `findByGuid - no city`() { + val customerGuid = mockGuid + whenever(customerRepository.findByGuid(any())).thenReturn(customerEntity) + whenever(cityRepository.findById(any())).thenReturn(Optional.empty()) + + val result = customerServiceImpl.findByGuid(customerGuid) + assertEquals(CustomerExtended(customer, null), result) + + verify(customerRepository, times(1)).findByGuid(customerGuid) + verify(cityRepository, times(1)).findById(cityId) + } +}