diff --git a/build.gradle.kts b/build.gradle.kts index cdbd8e5..c62f37d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id("org.springframework.boot") version "3.2.10" id("io.spring.dependency-management") version "1.1.6" + id("org.springframework.boot") version "3.2.10" jacoco diff --git a/src/main/kotlin/com/github/dannecron/demo/models/CustomerLocal.kt b/src/main/kotlin/com/github/dannecron/demo/models/CustomerLocal.kt index 78e6273..1d8140b 100644 --- a/src/main/kotlin/com/github/dannecron/demo/models/CustomerLocal.kt +++ b/src/main/kotlin/com/github/dannecron/demo/models/CustomerLocal.kt @@ -1,6 +1,6 @@ package com.github.dannecron.demo.models -data class CustomerLocal(val name: String, val city: City, val orders: List) { +data class CustomerLocal(val name: String, val city: City, val orders: List) { /** * Return the most expensive product among all delivered products */ diff --git a/src/main/kotlin/com/github/dannecron/demo/models/Order.kt b/src/main/kotlin/com/github/dannecron/demo/models/Order.kt index 6e34fd4..05a153a 100644 --- a/src/main/kotlin/com/github/dannecron/demo/models/Order.kt +++ b/src/main/kotlin/com/github/dannecron/demo/models/Order.kt @@ -1,3 +1,31 @@ package com.github.dannecron.demo.models -data class Order(val products: List, val isDelivered: Boolean) +import com.github.dannecron.demo.services.serializables.OffsetDateTimeSerialization +import com.github.dannecron.demo.services.serializables.UuidSerialization +import kotlinx.serialization.Serializable +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Column +import org.springframework.data.relational.core.mapping.Table +import java.time.OffsetDateTime +import java.util.* + +@Table(value = "order") +@Serializable +data class Order( + @Id + val id: Long?, + @Serializable(with = UuidSerialization::class) + val guid: UUID, + val customerId: Long, + @Serializable(with = OffsetDateTimeSerialization::class) + @Column(value = "delivered_at") + val deliveredAt: OffsetDateTime?, + @Serializable(with = OffsetDateTimeSerialization::class) + @Column(value = "created_at") + val createdAt: OffsetDateTime, + @Serializable(with = OffsetDateTimeSerialization::class) + @Column(value = "updated_at") + val updatedAt: OffsetDateTime? +) { + fun isDelivered(): Boolean = deliveredAt != null +} diff --git a/src/main/kotlin/com/github/dannecron/demo/models/OrderLocal.kt b/src/main/kotlin/com/github/dannecron/demo/models/OrderLocal.kt new file mode 100644 index 0000000..164fd93 --- /dev/null +++ b/src/main/kotlin/com/github/dannecron/demo/models/OrderLocal.kt @@ -0,0 +1,3 @@ +package com.github.dannecron.demo.models + +data class OrderLocal(val products: List, val isDelivered: Boolean) diff --git a/src/main/kotlin/com/github/dannecron/demo/models/OrderProduct.kt b/src/main/kotlin/com/github/dannecron/demo/models/OrderProduct.kt new file mode 100644 index 0000000..c5b3ee3 --- /dev/null +++ b/src/main/kotlin/com/github/dannecron/demo/models/OrderProduct.kt @@ -0,0 +1,41 @@ +package com.github.dannecron.demo.models + +import com.github.dannecron.demo.services.serializables.OffsetDateTimeSerialization +import com.github.dannecron.demo.services.serializables.UuidSerialization +import kotlinx.serialization.Serializable +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Transient +import org.springframework.data.domain.Persistable +import org.springframework.data.relational.core.mapping.Column +import org.springframework.data.relational.core.mapping.Table +import java.time.OffsetDateTime +import java.util.* + +@Table(value = "order_product") +@Serializable +data class OrderProduct( + @Id + @Serializable(with = UuidSerialization::class) + val guid: UUID, + @Column(value = "order_id") + val orderId: Long, + @Column(value = "product_id") + val productId: Long, + @Serializable(with = OffsetDateTimeSerialization::class) + @Column(value = "created_at") + val createdAt: OffsetDateTime, + @Serializable(with = OffsetDateTimeSerialization::class) + @Column(value = "updated_at") + val updatedAt: OffsetDateTime?, +): Persistable { + @Transient + var isNewInstance: Boolean? = null + + override fun getId(): UUID { + return guid + } + + override fun isNew(): Boolean { + return isNewInstance ?: true + } +} diff --git a/src/main/kotlin/com/github/dannecron/demo/providers/MockedShopProvider.kt b/src/main/kotlin/com/github/dannecron/demo/providers/MockedShopProvider.kt index b122901..54de1ab 100644 --- a/src/main/kotlin/com/github/dannecron/demo/providers/MockedShopProvider.kt +++ b/src/main/kotlin/com/github/dannecron/demo/providers/MockedShopProvider.kt @@ -16,17 +16,17 @@ class MockedShopProvider: com.github.dannecron.demo.providers.ShopProvider { name = "Foo-1", city = makeCity(id = 1, name = "Foo"), orders = listOf( - Order(products = listOf(productOne, productTwo), isDelivered = true), - Order(products = listOf(productThree), isDelivered = false), + OrderLocal(products = listOf(productOne, productTwo), isDelivered = true), + OrderLocal(products = listOf(productThree), isDelivered = false), ) ), CustomerLocal( name = "Foo-2", city = makeCity(id = 2, name = "Bar"), orders = listOf( - Order(products = listOf(productOne), isDelivered = false), - Order(products = listOf(productTwo), isDelivered = true), - Order(products = listOf(productFour), isDelivered = true), + OrderLocal(products = listOf(productOne), isDelivered = false), + OrderLocal(products = listOf(productTwo), isDelivered = true), + OrderLocal(products = listOf(productFour), isDelivered = true), ) ), )) diff --git a/src/main/kotlin/com/github/dannecron/demo/providers/OrderProductRepository.kt b/src/main/kotlin/com/github/dannecron/demo/providers/OrderProductRepository.kt new file mode 100644 index 0000000..9b64987 --- /dev/null +++ b/src/main/kotlin/com/github/dannecron/demo/providers/OrderProductRepository.kt @@ -0,0 +1,7 @@ +package com.github.dannecron.demo.providers + +import com.github.dannecron.demo.models.OrderProduct +import org.springframework.data.repository.CrudRepository +import java.util.* + +interface OrderProductRepository: CrudRepository diff --git a/src/main/kotlin/com/github/dannecron/demo/providers/OrderRepository.kt b/src/main/kotlin/com/github/dannecron/demo/providers/OrderRepository.kt new file mode 100644 index 0000000..2006b18 --- /dev/null +++ b/src/main/kotlin/com/github/dannecron/demo/providers/OrderRepository.kt @@ -0,0 +1,8 @@ +package com.github.dannecron.demo.providers + +import com.github.dannecron.demo.models.Order +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository + +@Repository +interface OrderRepository: CrudRepository diff --git a/src/main/kotlin/com/github/dannecron/demo/services/database/order/OrderServiceImpl.kt b/src/main/kotlin/com/github/dannecron/demo/services/database/order/OrderServiceImpl.kt new file mode 100644 index 0000000..83190be --- /dev/null +++ b/src/main/kotlin/com/github/dannecron/demo/services/database/order/OrderServiceImpl.kt @@ -0,0 +1,42 @@ +package com.github.dannecron.demo.services.database.order + +import com.github.dannecron.demo.models.Customer +import com.github.dannecron.demo.models.Order +import com.github.dannecron.demo.models.OrderProduct +import com.github.dannecron.demo.models.Product +import com.github.dannecron.demo.providers.OrderProductRepository +import com.github.dannecron.demo.providers.OrderRepository +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.OffsetDateTime +import java.util.* + +@Service +class OrderServiceImpl( + @Autowired val orderRepository: OrderRepository, + @Autowired val orderProductRepository: OrderProductRepository, +) { + @Transactional + fun createOrder(customer: Customer, products: Set): Order { + val order = Order( + id = null, + guid = UUID.randomUUID(), + customerId = customer.id!!, + deliveredAt = null, + createdAt = OffsetDateTime.now(), + updatedAt = null, + ) + return orderRepository.save(order).also { + savedOrder -> products.toList().forEach { + product -> orderProductRepository.save(OrderProduct( + guid = UUID.randomUUID(), + orderId = savedOrder.id!!, + productId = product.id!!, + createdAt = OffsetDateTime.now(), + updatedAt = null + )) + } + } + } +} diff --git a/src/main/resources/db/migration/structure/V6__create_order_table.sql b/src/main/resources/db/migration/structure/V6__create_order_table.sql new file mode 100644 index 0000000..06f12ac --- /dev/null +++ b/src/main/resources/db/migration/structure/V6__create_order_table.sql @@ -0,0 +1,14 @@ +create table "order" ( + id bigserial primary key, + guid uuid not null, + customer_id bigint not null, + delivered_at timestamptz, + created_at timestamptz not null, + updated_at timestamptz, + CONSTRAINT order_customer_foreign + FOREIGN KEY(customer_id) + REFERENCES customer(id) + ON DELETE CASCADE +); + +create unique index order_guid_idx ON "order" (guid); diff --git a/src/main/resources/db/migration/structure/V7__create_order_product_table.sql b/src/main/resources/db/migration/structure/V7__create_order_product_table.sql new file mode 100644 index 0000000..73352c2 --- /dev/null +++ b/src/main/resources/db/migration/structure/V7__create_order_product_table.sql @@ -0,0 +1,15 @@ +create table order_product ( + guid uuid primary key, + order_id bigint not null, + product_id bigint not null, + created_at timestamptz not null, + updated_at timestamptz, + CONSTRAINT order_product_order_foreign + FOREIGN KEY(order_id) + REFERENCES "order"(id) + ON DELETE CASCADE, + CONSTRAINT order_product_product_foreign + FOREIGN KEY(product_id) + REFERENCES product(id) + ON DELETE CASCADE +); diff --git a/src/test/kotlin/com/github/dannecron/demo/http/controllers/CustomerControllerTest.kt b/src/test/kotlin/com/github/dannecron/demo/http/controllers/CustomerControllerTest.kt index 1ad05c2..83eb0a6 100644 --- a/src/test/kotlin/com/github/dannecron/demo/http/controllers/CustomerControllerTest.kt +++ b/src/test/kotlin/com/github/dannecron/demo/http/controllers/CustomerControllerTest.kt @@ -28,8 +28,8 @@ class CustomerControllerTest( @Test fun getCustomer_successWithCity() { - val customerId = 22.toLong() - val cityId = 11.toLong() + val customerId = 22L + val cityId = 11L val customerGuid = UUID.randomUUID() val customerExtended = CustomerExtended( customer = Customer( @@ -64,7 +64,7 @@ class CustomerControllerTest( @Test fun getCustomer_successNoCity() { - val customerId = 22.toLong() + val customerId = 22L val customerGuid = UUID.randomUUID() val customerExtended = CustomerExtended( customer = Customer( diff --git a/src/test/kotlin/com/github/dannecron/demo/http/controllers/ProductControllerTest.kt b/src/test/kotlin/com/github/dannecron/demo/http/controllers/ProductControllerTest.kt index 4a5ee03..1d9065b 100644 --- a/src/test/kotlin/com/github/dannecron/demo/http/controllers/ProductControllerTest.kt +++ b/src/test/kotlin/com/github/dannecron/demo/http/controllers/ProductControllerTest.kt @@ -109,10 +109,10 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() { @Test fun createProduct_success() { - val productId = 13.toLong() + val productId = 13L val name = "new-product" val description = null - val price = 20000.toLong() + val price = 20000L val reqBody = """{"name":"$name","description":null,"price":$price}""" @@ -142,7 +142,7 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() { @Test fun createProduct_badRequest_noNameParam() { - val price = 20000.toLong() + val price = 20000L val reqBody = """{"description":null,"price":$price}""" @@ -160,7 +160,7 @@ class ProductControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() { @Test fun createProduct_badRequest_emptyName() { - val price = 20000.toLong() + val price = 20000L val reqBody = """{"name":"","description":null,"price":$price}""" diff --git a/src/test/kotlin/com/github/dannecron/demo/http/controllers/ShopControllerTest.kt b/src/test/kotlin/com/github/dannecron/demo/http/controllers/ShopControllerTest.kt index 05d1ad8..051f24e 100644 --- a/src/test/kotlin/com/github/dannecron/demo/http/controllers/ShopControllerTest.kt +++ b/src/test/kotlin/com/github/dannecron/demo/http/controllers/ShopControllerTest.kt @@ -32,18 +32,18 @@ class ShopControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() { name = "cus-one", city = makeCity(id = 1, name = "city-one"), orders = listOf( - Order(products = listOf(productOne), isDelivered = false), - Order(products = listOf(productTwo), isDelivered = false), - Order(products = listOf(productThree), isDelivered = true), + OrderLocal(products = listOf(productOne), isDelivered = false), + OrderLocal(products = listOf(productTwo), isDelivered = false), + OrderLocal(products = listOf(productThree), isDelivered = true), ) ), CustomerLocal( name = "cus-two", city = makeCity(id = 2, name = "city-two"), orders = listOf( - Order(products = listOf(productOne), isDelivered = false), - Order(products = listOf(productTwo), isDelivered = true), - Order(products = listOf(productFour), isDelivered = true), + OrderLocal(products = listOf(productOne), isDelivered = false), + OrderLocal(products = listOf(productTwo), isDelivered = true), + OrderLocal(products = listOf(productFour), isDelivered = true), ) ), )) diff --git a/src/test/kotlin/com/github/dannecron/demo/services/database/order/OrderServiceImplTest.kt b/src/test/kotlin/com/github/dannecron/demo/services/database/order/OrderServiceImplTest.kt new file mode 100644 index 0000000..850d1bc --- /dev/null +++ b/src/test/kotlin/com/github/dannecron/demo/services/database/order/OrderServiceImplTest.kt @@ -0,0 +1,69 @@ +package com.github.dannecron.demo.services.database.order + +import com.github.dannecron.demo.BaseDbTest +import com.github.dannecron.demo.models.Customer +import com.github.dannecron.demo.models.Order +import com.github.dannecron.demo.models.Product +import com.github.dannecron.demo.providers.CustomerRepository +import com.github.dannecron.demo.providers.OrderRepository +import com.github.dannecron.demo.providers.ProductRepository +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.context.ContextConfiguration +import java.time.OffsetDateTime +import java.util.* +import kotlin.test.Test +import kotlin.test.assertNotNull + +@ContextConfiguration(classes = [OrderServiceImpl::class]) +class OrderServiceImplTest: BaseDbTest() { + @Autowired + private lateinit var orderRepository: OrderRepository + @Autowired + private lateinit var productRepository: ProductRepository + @Autowired + private lateinit var customerRepository: CustomerRepository + @Autowired + private lateinit var orderServiceImpl: OrderServiceImpl + + @Test + fun create_success() { + var productOne: Product? = null + var productTwo: Product? = null + var customer: Customer? = null + var order: Order? = null + + try { + productOne = makeProduct().let { productRepository.save(it) } + productTwo = makeProduct().let { productRepository.save(it) } + customer = makeCustomer().let { customerRepository.save(it) } + + order = orderServiceImpl.createOrder(customer, setOf(productOne, productTwo)) + assertNotNull(order.id) + } finally { + order?.id?.also { orderRepository.deleteById(it) } + customer?.id?.also { customerRepository.deleteById(it) } + productOne?.id?.also { productRepository.deleteById(it) } + productTwo?.id?.also { productRepository.deleteById(it) } + } + } + + private fun makeProduct(): Product = Product( + id = null, + guid = UUID.randomUUID(), + name = "name" + UUID.randomUUID(), + description = null, + price = 10000, + createdAt = OffsetDateTime.now(), + updatedAt = null, + deletedAt = null, + ) + + private fun makeCustomer(): Customer = Customer( + id = null, + guid = UUID.randomUUID(), + name = "client", + cityId = null, + createdAt =OffsetDateTime.now(), + updatedAt = null, + ) +} diff --git a/src/test/kotlin/com/github/dannecron/demo/services/database/product/ProductServiceImplDbTest.kt b/src/test/kotlin/com/github/dannecron/demo/services/database/product/ProductServiceImplDbTest.kt index eed0ffc..b0cf373 100644 --- a/src/test/kotlin/com/github/dannecron/demo/services/database/product/ProductServiceImplDbTest.kt +++ b/src/test/kotlin/com/github/dannecron/demo/services/database/product/ProductServiceImplDbTest.kt @@ -29,7 +29,7 @@ class ProductServiceImplDbTest: BaseDbTest() { @Test fun createFindDelete_success() { val name = "new-product-name" - val price = 33333.toLong() + val price = 33333L val description = "some-description" var product: Product? = null