add send method to product service

This commit is contained in:
Savosin Denis
2025-06-03 11:34:28 +07:00
parent 1bda2e1d21
commit a8639f8d15
8 changed files with 80 additions and 19 deletions

View File

@@ -3,11 +3,10 @@ version = "single-version"
dependencies { dependencies {
implementation(project(":db")) implementation(project(":db"))
implementation(project(":edge-producing"))
implementation(rootProject.libs.spring.boot.starter.actuator) implementation(rootProject.libs.spring.boot.starter.actuator)
implementation(rootProject.libs.spring.boot.starter.jdbc) implementation(rootProject.libs.spring.boot.starter.jdbc)
implementation(rootProject.libs.spring.boot.starter.validation)
implementation(rootProject.libs.json.schema.validator)
testImplementation(rootProject.libs.spring.boot.starter.actuatorAutoconfigure) testImplementation(rootProject.libs.spring.boot.starter.actuatorAutoconfigure)
} }

View File

@@ -0,0 +1,6 @@
package com.github.dannecron.demo.core.exceptions
class InvalidDataException(
override val message: String,
override val cause: Throwable?,
) : RuntimeException(message, cause)

View File

@@ -1,3 +1,3 @@
package com.github.dannecron.demo.core.exceptions package com.github.dannecron.demo.core.exceptions
class ProductNotFoundException: ModelNotFoundException("product") class ProductNotFoundException: ModelNotFoundException("json-schemas/kafka/product")

View File

@@ -2,6 +2,7 @@ package com.github.dannecron.demo.core.services.product
import com.github.dannecron.demo.core.dto.Product import com.github.dannecron.demo.core.dto.Product
import com.github.dannecron.demo.core.exceptions.AlreadyDeletedException import com.github.dannecron.demo.core.exceptions.AlreadyDeletedException
import com.github.dannecron.demo.core.exceptions.InvalidDataException
import com.github.dannecron.demo.core.exceptions.ProductNotFoundException import com.github.dannecron.demo.core.exceptions.ProductNotFoundException
import org.springframework.data.domain.Page import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable import org.springframework.data.domain.Pageable
@@ -16,4 +17,7 @@ interface ProductService {
@Throws(ProductNotFoundException::class, AlreadyDeletedException::class) @Throws(ProductNotFoundException::class, AlreadyDeletedException::class)
fun delete(guid: UUID): Product fun delete(guid: UUID): Product
@Throws(ProductNotFoundException::class, InvalidDataException::class)
fun send(guid: UUID, topic: String?)
} }

View File

@@ -2,21 +2,26 @@ package com.github.dannecron.demo.core.services.product
import com.github.dannecron.demo.core.dto.Product import com.github.dannecron.demo.core.dto.Product
import com.github.dannecron.demo.core.exceptions.AlreadyDeletedException import com.github.dannecron.demo.core.exceptions.AlreadyDeletedException
import com.github.dannecron.demo.core.exceptions.InvalidDataException
import com.github.dannecron.demo.core.exceptions.ProductNotFoundException import com.github.dannecron.demo.core.exceptions.ProductNotFoundException
import com.github.dannecron.demo.core.services.generation.CommonGenerator import com.github.dannecron.demo.core.services.generation.CommonGenerator
import com.github.dannecron.demo.core.utils.LoggerDelegate import com.github.dannecron.demo.core.utils.LoggerDelegate
import com.github.dannecron.demo.db.entity.ProductEntity import com.github.dannecron.demo.db.entity.ProductEntity
import com.github.dannecron.demo.db.repository.ProductRepository import com.github.dannecron.demo.db.repository.ProductRepository
import com.github.dannecron.demo.edgeproducing.dto.ProductDto
import com.github.dannecron.demo.edgeproducing.producer.ProductProducer
import net.logstash.logback.marker.Markers import net.logstash.logback.marker.Markers
import org.springframework.data.domain.Page import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.time.format.DateTimeFormatter
import java.util.UUID import java.util.UUID
@Service @Service
class ProductServiceImpl( class ProductServiceImpl(
private val productRepository: ProductRepository, private val productRepository: ProductRepository,
private val commonGenerator: CommonGenerator, private val commonGenerator: CommonGenerator,
private val productProducer: ProductProducer,
): ProductService { ): ProductService {
private val logger by LoggerDelegate() private val logger by LoggerDelegate()
@@ -58,6 +63,13 @@ class ProductServiceImpl(
.toCore() .toCore()
} }
@Throws(ProductNotFoundException::class, InvalidDataException::class)
override fun send(guid: UUID, topic: String?) {
val product = findByGuid(guid) ?: throw ProductNotFoundException()
productProducer.produceProductSync(product.toProducingDto())
}
private fun ProductEntity.toCore() = Product( private fun ProductEntity.toCore() = Product(
id = id!!, id = id!!,
guid = guid, guid = guid,
@@ -79,4 +91,15 @@ class ProductServiceImpl(
updatedAt = updatedAt, updatedAt = updatedAt,
deletedAt = deletedAt, deletedAt = deletedAt,
) )
private fun Product.toProducingDto() = ProductDto(
id = 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

@@ -6,6 +6,8 @@ import com.github.dannecron.demo.core.exceptions.ProductNotFoundException
import com.github.dannecron.demo.core.services.generation.CommonGenerator import com.github.dannecron.demo.core.services.generation.CommonGenerator
import com.github.dannecron.demo.db.entity.ProductEntity import com.github.dannecron.demo.db.entity.ProductEntity
import com.github.dannecron.demo.db.repository.ProductRepository import com.github.dannecron.demo.db.repository.ProductRepository
import com.github.dannecron.demo.edgeproducing.dto.ProductDto
import com.github.dannecron.demo.edgeproducing.producer.ProductProducer
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 org.mockito.kotlin.any import org.mockito.kotlin.any
@@ -14,8 +16,10 @@ import org.mockito.kotlin.mock
import org.mockito.kotlin.never import org.mockito.kotlin.never
import org.mockito.kotlin.times import org.mockito.kotlin.times
import org.mockito.kotlin.verify import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.whenever import org.mockito.kotlin.whenever
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.format.DateTimeFormatter
import java.util.UUID import java.util.UUID
import kotlin.test.assertEquals import kotlin.test.assertEquals
@@ -24,6 +28,7 @@ class ProductServiceImplTest {
private val mockCurrentTime = OffsetDateTime.now() private val mockCurrentTime = OffsetDateTime.now()
private val productRepository: ProductRepository = mock() private val productRepository: ProductRepository = mock()
private val productProducer: ProductProducer = mock()
private val commonGenerator: CommonGenerator = mock { private val commonGenerator: CommonGenerator = mock {
on { generateUUID() } doReturn mockGuid on { generateUUID() } doReturn mockGuid
on { generateCurrentTime() } doReturn mockCurrentTime on { generateCurrentTime() } doReturn mockCurrentTime
@@ -32,6 +37,7 @@ class ProductServiceImplTest {
private val productService = ProductServiceImpl( private val productService = ProductServiceImpl(
productRepository = productRepository, productRepository = productRepository,
commonGenerator = commonGenerator, commonGenerator = commonGenerator,
productProducer = productProducer,
) )
private val guid = UUID.randomUUID() private val guid = UUID.randomUUID()
@@ -55,6 +61,16 @@ class ProductServiceImplTest {
updatedAt = mockCurrentTime.minusHours(2), updatedAt = mockCurrentTime.minusHours(2),
deletedAt = null, deletedAt = null,
) )
private val producingProductDto = ProductDto(
id = 123,
guid = guid.toString(),
name = "name",
description = "description",
price = 10050,
createdAt = mockCurrentTime.minusDays(1).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
updatedAt = mockCurrentTime.minusHours(2).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
deletedAt = null,
)
@Test @Test
fun create() { fun create() {
@@ -127,4 +143,30 @@ class ProductServiceImplTest {
verify(productRepository, times(1)).findByGuid(guid) verify(productRepository, times(1)).findByGuid(guid)
verify(productRepository, never()).save(any()) verify(productRepository, never()).save(any())
} }
@Test
fun `send - success`() {
val topic = "some-topic"
whenever(productRepository.findByGuid(any())).thenReturn(productEntity)
productService.send(guid, topic)
verify(productRepository, times(1)).findByGuid(guid)
verify(productProducer, times(1)).produceProductSync(producingProductDto)
}
@Test
fun `send - not found`() {
val topic = "some-topic"
whenever(productRepository.findByGuid(any())).thenReturn(null)
assertThrows<ProductNotFoundException> {
productService.send(guid, topic)
}
verify(productRepository, times(1)).findByGuid(guid)
verifyNoInteractions(productProducer)
}
} }

View File

@@ -2,6 +2,7 @@ package com.github.dannecron.demo.http.controllers
import com.github.dannecron.demo.core.dto.Product import com.github.dannecron.demo.core.dto.Product
import com.github.dannecron.demo.core.exceptions.AlreadyDeletedException import com.github.dannecron.demo.core.exceptions.AlreadyDeletedException
import com.github.dannecron.demo.core.exceptions.InvalidDataException
import com.github.dannecron.demo.core.exceptions.ProductNotFoundException import com.github.dannecron.demo.core.exceptions.ProductNotFoundException
import com.github.dannecron.demo.core.services.product.ProductService import com.github.dannecron.demo.core.services.product.ProductService
import com.github.dannecron.demo.http.exceptions.NotFoundException import com.github.dannecron.demo.http.exceptions.NotFoundException
@@ -10,8 +11,6 @@ import com.github.dannecron.demo.http.requests.CreateProductRequest
import com.github.dannecron.demo.http.responses.NotFoundResponse import com.github.dannecron.demo.http.responses.NotFoundResponse
import com.github.dannecron.demo.http.responses.makeOkResponse import com.github.dannecron.demo.http.responses.makeOkResponse
import com.github.dannecron.demo.http.responses.page.PageResponse import com.github.dannecron.demo.http.responses.page.PageResponse
import com.github.dannecron.demo.services.ProductSyncService
import com.github.dannecron.demo.services.kafka.exceptions.InvalidArgumentException
import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.responses.ApiResponse
@@ -29,7 +28,6 @@ import java.util.*
@RequestMapping(value = ["/api/product"], produces = [MediaType.APPLICATION_JSON_VALUE]) @RequestMapping(value = ["/api/product"], produces = [MediaType.APPLICATION_JSON_VALUE])
class ProductController( class ProductController(
private val productService: ProductService, private val productService: ProductService,
private val productSyncService: ProductSyncService,
) { ) {
@GetMapping("/{guid}") @GetMapping("/{guid}")
@Throws(NotFoundException::class) @Throws(NotFoundException::class)
@@ -73,8 +71,8 @@ class ProductController(
@RequestParam(required = false) topic: String? @RequestParam(required = false) topic: String?
): ResponseEntity<Any> { ): ResponseEntity<Any> {
try { try {
productSyncService.syncToKafka(guid, topic) productService.send(guid, topic)
} catch (_: InvalidArgumentException) { } catch (_: InvalidDataException) {
throw UnprocessableException("cannot sync product to kafka") throw UnprocessableException("cannot sync product to kafka")
} }

View File

@@ -4,7 +4,6 @@ import com.github.dannecron.demo.BaseUnitTest
import com.github.dannecron.demo.core.dto.Product import com.github.dannecron.demo.core.dto.Product
import com.github.dannecron.demo.core.services.product.ProductService import com.github.dannecron.demo.core.services.product.ProductService
import com.github.dannecron.demo.http.responses.ResponseStatus import com.github.dannecron.demo.http.responses.ResponseStatus
import com.github.dannecron.demo.services.ProductSyncService
import org.hamcrest.Matchers.contains import org.hamcrest.Matchers.contains
import org.hamcrest.Matchers.nullValue import org.hamcrest.Matchers.nullValue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -39,9 +38,6 @@ class ProductControllerTest: BaseUnitTest() {
@MockBean @MockBean
private lateinit var productService: ProductService private lateinit var productService: ProductService
@MockBean
private lateinit var productSyncService: ProductSyncService
private val guid = UUID.randomUUID() private val guid = UUID.randomUUID()
private val now = OffsetDateTime.now() private val now = OffsetDateTime.now()
private val productId = 12L private val productId = 12L
@@ -72,7 +68,6 @@ class ProductControllerTest: BaseUnitTest() {
.andExpect { jsonPath("\$.updatedAt") { value(nullValue()) } } .andExpect { jsonPath("\$.updatedAt") { value(nullValue()) } }
verify(productService, times(1)).findByGuid(guid) verify(productService, times(1)).findByGuid(guid)
verifyNoInteractions(productSyncService)
} }
@Test @Test
@@ -85,7 +80,6 @@ class ProductControllerTest: BaseUnitTest() {
.andExpect { jsonPath("\$.status") { value(ResponseStatus.NOT_FOUND.status) } } .andExpect { jsonPath("\$.status") { value(ResponseStatus.NOT_FOUND.status) } }
verify(productService, times(1)).findByGuid(guid) verify(productService, times(1)).findByGuid(guid)
verifyNoInteractions(productSyncService)
} }
@Test @Test
@@ -109,7 +103,6 @@ class ProductControllerTest: BaseUnitTest() {
.andExpect { jsonPath("\$.data[0].isDeleted") { value(false) } } .andExpect { jsonPath("\$.data[0].isDeleted") { value(false) } }
verify(productService, times(1)).findAll(pageRequest) verify(productService, times(1)).findAll(pageRequest)
verifyNoInteractions(productSyncService)
} }
@Test @Test
@@ -127,7 +120,6 @@ class ProductControllerTest: BaseUnitTest() {
.andExpect { jsonPath("\$.id") { value(productId) } } .andExpect { jsonPath("\$.id") { value(productId) } }
verify(productService, times(1)).create(productName, productPrice, null) verify(productService, times(1)).create(productName, productPrice, null)
verifyNoInteractions(productSyncService)
} }
@Test @Test
@@ -144,7 +136,6 @@ class ProductControllerTest: BaseUnitTest() {
.andExpect { jsonPath("\$.cause") { contains("name") } } .andExpect { jsonPath("\$.cause") { contains("name") } }
verifyNoInteractions(productService) verifyNoInteractions(productService)
verifyNoInteractions(productSyncService)
} }
@Test @Test
@@ -161,7 +152,6 @@ class ProductControllerTest: BaseUnitTest() {
.andExpect { jsonPath("\$.cause") { value(MethodArgumentNotValidException::class.qualifiedName) } } .andExpect { jsonPath("\$.cause") { value(MethodArgumentNotValidException::class.qualifiedName) } }
verifyNoInteractions(productService) verifyNoInteractions(productService)
verifyNoInteractions(productSyncService)
} }
@Test @Test
@@ -176,6 +166,5 @@ class ProductControllerTest: BaseUnitTest() {
.andExpect { jsonPath("\$.status") { value(ResponseStatus.OK.status) } } .andExpect { jsonPath("\$.status") { value(ResponseStatus.OK.status) } }
verify(productService, times(1)).delete(guid) verify(productService, times(1)).delete(guid)
verifyNoInteractions(productSyncService)
} }
} }