From f12839a15fc42a26b1a0bacf51a866a17a08e82d Mon Sep 17 00:00:00 2001 From: Denis Savosin Date: Mon, 30 Sep 2024 12:40:47 +0700 Subject: [PATCH] add service layer with ProductService, add deleted_at and unique index to product table --- build.gradle.kts | 5 +- .../demo/exceptions/UnprocessableException.kt | 3 ++ .../kotlin/com/example/demo/models/Product.kt | 5 ++ .../demo/provider/ProductRepository.kt | 8 +++- .../example/demo/services/ProductService.kt | 17 +++++++ .../demo/services/ProductServiceImpl.kt | 48 +++++++++++++++++++ .../V2__add_deteled_at_to_product_table.sql | 1 + .../V3__add_unique_index_to_product_guid.sql | 1 + 8 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/com/example/demo/exceptions/UnprocessableException.kt create mode 100644 src/main/kotlin/com/example/demo/services/ProductService.kt create mode 100644 src/main/kotlin/com/example/demo/services/ProductServiceImpl.kt create mode 100644 src/main/resources/db/migration/structure/V2__add_deteled_at_to_product_table.sql create mode 100644 src/main/resources/db/migration/structure/V3__add_unique_index_to_product_guid.sql diff --git a/build.gradle.kts b/build.gradle.kts index 995f84c..dea97c8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,15 +28,12 @@ dependencies { implementation("org.flywaydb:flyway-core") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json") - implementation("org.springframework.boot:spring-boot-starter-data-jpa") - implementation("org.springframework.boot:spring-boot-starter-jdbc") + implementation("org.postgresql:postgresql") implementation("org.springframework.boot:spring-boot-starter-mustache") implementation("org.springframework.boot:spring-boot-starter-web") developmentOnly("org.springframework.boot:spring-boot-devtools") - runtimeOnly("org.postgresql:postgresql") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0") testImplementation("org.springframework.boot:spring-boot-starter-test") diff --git a/src/main/kotlin/com/example/demo/exceptions/UnprocessableException.kt b/src/main/kotlin/com/example/demo/exceptions/UnprocessableException.kt new file mode 100644 index 0000000..76d6a4d --- /dev/null +++ b/src/main/kotlin/com/example/demo/exceptions/UnprocessableException.kt @@ -0,0 +1,3 @@ +package com.example.demo.exceptions + +class UnprocessableException(override val message: String): RuntimeException(message) \ No newline at end of file diff --git a/src/main/kotlin/com/example/demo/models/Product.kt b/src/main/kotlin/com/example/demo/models/Product.kt index 006d593..7219888 100644 --- a/src/main/kotlin/com/example/demo/models/Product.kt +++ b/src/main/kotlin/com/example/demo/models/Product.kt @@ -27,6 +27,11 @@ data class Product( @Serializable(with = OffsetDateTimeSerialization::class) @Column(value = "updated_at") val updatedAt: OffsetDateTime?, + @Serializable(with = OffsetDateTimeSerialization::class) + @Column(value = "deleted_at") + val deletedAt: OffsetDateTime?, ) { fun getPriceDouble(): Double = (price.toDouble() / 100).roundTo(2) + + fun isDeleted(): Boolean = deletedAt != null } \ No newline at end of file diff --git a/src/main/kotlin/com/example/demo/provider/ProductRepository.kt b/src/main/kotlin/com/example/demo/provider/ProductRepository.kt index 992ef6a..632d135 100644 --- a/src/main/kotlin/com/example/demo/provider/ProductRepository.kt +++ b/src/main/kotlin/com/example/demo/provider/ProductRepository.kt @@ -1,13 +1,17 @@ package com.example.demo.provider import com.example.demo.models.Product -import org.springframework.data.jpa.repository.Query +import org.springframework.data.jdbc.repository.query.Query import org.springframework.data.repository.CrudRepository +import org.springframework.data.repository.query.Param import org.springframework.stereotype.Repository +import java.time.OffsetDateTime import java.util.* @Repository interface ProductRepository: CrudRepository { - @Query(value = "SELECT * FROM Product WHERE guid = :guid") fun findByGuid(guid: UUID): Product? + + @Query(value = "UPDATE Product SET deleted_at = :deletedAt WHERE guid = :guid RETURNING *") + fun softDelete(@Param("guid") guid: UUID, @Param("deletedAt") deletedAt: OffsetDateTime): Product? } \ No newline at end of file diff --git a/src/main/kotlin/com/example/demo/services/ProductService.kt b/src/main/kotlin/com/example/demo/services/ProductService.kt new file mode 100644 index 0000000..6b2e931 --- /dev/null +++ b/src/main/kotlin/com/example/demo/services/ProductService.kt @@ -0,0 +1,17 @@ +package com.example.demo.services + +import com.example.demo.exceptions.NotFoundException +import com.example.demo.exceptions.UnprocessableException +import com.example.demo.models.Product +import org.springframework.stereotype.Service +import java.util.* + +@Service +interface ProductService { + fun findByGuid(guid: UUID): Product? + + fun create(name: String, price: Long, description: String?): Product + + @Throws(NotFoundException::class, UnprocessableException::class) + fun delete(guid: UUID): Product? +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/demo/services/ProductServiceImpl.kt b/src/main/kotlin/com/example/demo/services/ProductServiceImpl.kt new file mode 100644 index 0000000..0e1b6df --- /dev/null +++ b/src/main/kotlin/com/example/demo/services/ProductServiceImpl.kt @@ -0,0 +1,48 @@ +package com.example.demo.services + +import com.example.demo.exceptions.NotFoundException +import com.example.demo.exceptions.UnprocessableException +import com.example.demo.models.Product +import com.example.demo.provider.ProductRepository +import java.time.OffsetDateTime +import java.util.* + +class ProductServiceImpl(private val productRepository: ProductRepository): ProductService { + override fun findByGuid(guid: UUID): Product? = productRepository.findByGuid(guid) + + override fun create(name: String, price: Long, description: String?): Product { + val product = Product( + id = null, + guid = UUID.randomUUID(), + name = name, + description = description, + price = price, + createdAt = OffsetDateTime.now(), + updatedAt = null, + deletedAt = null, + ) + + return productRepository.save(product) + } + + override fun delete(guid: UUID): Product? { + val product = findByGuid(guid) ?: throw NotFoundException() + + if (product.isDeleted()) { + throw UnprocessableException("product already deleted") + } + + val deletedProduct = product.copy( + id = product.id!!, + guid = product.guid, + name = product.name, + description = product.description, + price = product.price, + createdAt = product.createdAt, + updatedAt = product.updatedAt, + deletedAt = OffsetDateTime.now(), + ) + + return productRepository.save(deletedProduct) + } +} \ No newline at end of file diff --git a/src/main/resources/db/migration/structure/V2__add_deteled_at_to_product_table.sql b/src/main/resources/db/migration/structure/V2__add_deteled_at_to_product_table.sql new file mode 100644 index 0000000..5a09973 --- /dev/null +++ b/src/main/resources/db/migration/structure/V2__add_deteled_at_to_product_table.sql @@ -0,0 +1 @@ +alter table product add deleted_at timestamptz default null; \ No newline at end of file diff --git a/src/main/resources/db/migration/structure/V3__add_unique_index_to_product_guid.sql b/src/main/resources/db/migration/structure/V3__add_unique_index_to_product_guid.sql new file mode 100644 index 0000000..7ea5dc7 --- /dev/null +++ b/src/main/resources/db/migration/structure/V3__add_unique_index_to_product_guid.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX product_guid_idx ON product (guid);