From fecbee8b289561803043cbfba9a1cd0e25b6da2f Mon Sep 17 00:00:00 2001 From: Denis Savosin Date: Mon, 14 Oct 2024 10:38:04 +0700 Subject: [PATCH] add customer table, dto, repository, service --- .../com/example/demo/config/AppConfig.kt | 13 +++- .../com/example/demo/models/Customer.kt | 36 +++++---- .../com/example/demo/models/CustomerLocal.kt | 16 ++++ .../kotlin/com/example/demo/models/Shop.kt | 16 ++-- .../demo/providers/CustomerRepository.kt | 11 +++ .../demo/providers/MockedShopProvider.kt | 6 +- .../database/customer/CustomerService.kt | 12 +++ .../database/customer/CustomerServiceImpl.kt | 32 ++++++++ .../structure/V5__create_customer_table.sql | 14 ++++ .../http/controllers/ShopControllerTest.kt | 6 +- .../database/city/CityServiceImplDbTest.kt | 4 +- .../customer/CustomerServiceImplDbTest.kt | 73 +++++++++++++++++++ 12 files changed, 205 insertions(+), 34 deletions(-) create mode 100644 src/main/kotlin/com/example/demo/models/CustomerLocal.kt create mode 100644 src/main/kotlin/com/example/demo/providers/CustomerRepository.kt create mode 100644 src/main/kotlin/com/example/demo/services/database/customer/CustomerService.kt create mode 100644 src/main/kotlin/com/example/demo/services/database/customer/CustomerServiceImpl.kt create mode 100644 src/main/resources/db/migration/structure/V5__create_customer_table.sql create mode 100644 src/test/kotlin/com/example/demo/services/database/customer/CustomerServiceImplDbTest.kt diff --git a/src/main/kotlin/com/example/demo/config/AppConfig.kt b/src/main/kotlin/com/example/demo/config/AppConfig.kt index 82ecb6d..a3b74b6 100644 --- a/src/main/kotlin/com/example/demo/config/AppConfig.kt +++ b/src/main/kotlin/com/example/demo/config/AppConfig.kt @@ -2,12 +2,11 @@ package com.example.demo.config import com.example.demo.config.properties.KafkaProperties import com.example.demo.config.properties.ValidationProperties -import com.example.demo.providers.CityRepository -import com.example.demo.providers.MockedShopProvider -import com.example.demo.providers.ProductRepository -import com.example.demo.providers.ShopProvider +import com.example.demo.providers.* import com.example.demo.services.database.city.CityService import com.example.demo.services.database.city.CityServiceImpl +import com.example.demo.services.database.customer.CustomerService +import com.example.demo.services.database.customer.CustomerServiceImpl import com.example.demo.services.database.product.ProductService import com.example.demo.services.database.product.ProductServiceImpl import com.example.demo.services.kafka.Producer @@ -55,6 +54,12 @@ class AppConfig( @Bean fun cityService(@Autowired cityRepository: CityRepository): CityService = CityServiceImpl(cityRepository) + @Bean + fun customerService( + @Autowired customerRepository: CustomerRepository, + @Autowired cityRepository: CityRepository, + ): CustomerService = CustomerServiceImpl(customerRepository, cityRepository) + @Bean fun schemaValidator( @Autowired validationProperties: ValidationProperties, diff --git a/src/main/kotlin/com/example/demo/models/Customer.kt b/src/main/kotlin/com/example/demo/models/Customer.kt index ddda4b2..4e21ae2 100644 --- a/src/main/kotlin/com/example/demo/models/Customer.kt +++ b/src/main/kotlin/com/example/demo/models/Customer.kt @@ -1,16 +1,26 @@ package com.example.demo.models -data class Customer(val name: String, val city: City, val orders: List) { - /** - * Return the most expensive product among all delivered products - */ - fun getMostExpensiveDeliveredProduct(): Product? = orders.filter { ord -> ord.isDelivered } - .flatMap { ord -> ord.products } - .maxByOrNull { pr -> pr.price } +import com.example.demo.services.serializables.OffsetDateTimeSerialization +import com.example.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.* - fun getMostExpensiveOrderedProduct(): Product? = orders.flatMap { ord -> ord.products }.maxByOrNull { pr -> pr.price } - - fun getOrderedProducts(): Set = orders.flatMap { order -> order.products }.toSet() - - fun getTotalOrderPrice(): Double = orders.flatMap { ord -> ord.products }.sumOf { pr -> pr.getPriceDouble()} -} \ No newline at end of file +@Table("customer") +data class Customer( + @Id + val id: Long?, + @Serializable(with = UuidSerialization::class) + val guid: UUID, + val name: String, + val cityId: Long?, + @Serializable(with = OffsetDateTimeSerialization::class) + @Column(value = "created_at") + val createdAt: OffsetDateTime, + @Serializable(with = OffsetDateTimeSerialization::class) + @Column(value = "updated_at") + val updatedAt: OffsetDateTime?, +) diff --git a/src/main/kotlin/com/example/demo/models/CustomerLocal.kt b/src/main/kotlin/com/example/demo/models/CustomerLocal.kt new file mode 100644 index 0000000..ff89430 --- /dev/null +++ b/src/main/kotlin/com/example/demo/models/CustomerLocal.kt @@ -0,0 +1,16 @@ +package com.example.demo.models + +data class CustomerLocal(val name: String, val city: City, val orders: List) { + /** + * Return the most expensive product among all delivered products + */ + fun getMostExpensiveDeliveredProduct(): Product? = orders.filter { ord -> ord.isDelivered } + .flatMap { ord -> ord.products } + .maxByOrNull { pr -> pr.price } + + fun getMostExpensiveOrderedProduct(): Product? = orders.flatMap { ord -> ord.products }.maxByOrNull { pr -> pr.price } + + fun getOrderedProducts(): Set = orders.flatMap { order -> order.products }.toSet() + + fun getTotalOrderPrice(): Double = orders.flatMap { ord -> ord.products }.sumOf { pr -> pr.getPriceDouble()} +} \ No newline at end of file diff --git a/src/main/kotlin/com/example/demo/models/Shop.kt b/src/main/kotlin/com/example/demo/models/Shop.kt index f1c99c0..b6118c2 100644 --- a/src/main/kotlin/com/example/demo/models/Shop.kt +++ b/src/main/kotlin/com/example/demo/models/Shop.kt @@ -1,26 +1,26 @@ package com.example.demo.models -data class Shop(val name: String, val customers: List) { +data class Shop(val name: String, val customers: List) { fun checkAllCustomersAreFrom(city: City): Boolean = customers.count { cus -> cus.city == city } == customers.count() fun countCustomersFrom(city: City): Int = customers.count { cus -> cus.city == city } fun getCitiesCustomersAreFrom(): Set = customers.map { cus -> cus.city }.toSet() - fun findAnyCustomerFrom(city: City): Customer? = customers.firstOrNull { cus -> cus.city == city } + fun findAnyCustomerFrom(city: City): CustomerLocal? = customers.firstOrNull { cus -> cus.city == city } fun getAllOrderedProducts(): Set = customers.flatMap { cus -> cus.getOrderedProducts() }.toSet() - fun getCustomersFrom(city: City): List = customers.filter { cus -> cus.city == city } + fun getCustomersFrom(city: City): List = customers.filter { cus -> cus.city == city } - fun getCustomersSortedByNumberOfOrders(): List = customers.sortedBy { cus -> cus.orders.count() } + fun getCustomersSortedByNumberOfOrders(): List = customers.sortedBy { cus -> cus.orders.count() } - fun getCustomerWithMaximumNumberOfOrders(): Customer? = customers.maxByOrNull { cus -> cus.orders.count() } + fun getCustomerWithMaximumNumberOfOrders(): CustomerLocal? = customers.maxByOrNull { cus -> cus.orders.count() } /** * Return customers who have more undelivered orders than delivered */ - fun getCustomersWithMoreUndeliveredOrdersThanDelivered(): Set = customers.partition(predicate = fun (cus): Boolean { + fun getCustomersWithMoreUndeliveredOrdersThanDelivered(): Set = customers.partition(predicate = fun (cus): Boolean { val (del, undel) = cus.orders.partition { ord -> ord.isDelivered } return del.count() < undel.count() @@ -42,7 +42,7 @@ data class Shop(val name: String, val customers: List) { }.toSet() } - fun groupCustomersByCity(): Map> = customers.groupBy { cus -> cus.city } + fun groupCustomersByCity(): Map> = customers.groupBy { cus -> cus.city } fun hasCustomerFrom(city: City): Boolean = customers.any { cus -> cus.city == city } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/example/demo/providers/CustomerRepository.kt b/src/main/kotlin/com/example/demo/providers/CustomerRepository.kt new file mode 100644 index 0000000..17f5622 --- /dev/null +++ b/src/main/kotlin/com/example/demo/providers/CustomerRepository.kt @@ -0,0 +1,11 @@ +package com.example.demo.providers + +import com.example.demo.models.Customer +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository +import java.util.* + +@Repository +interface CustomerRepository: CrudRepository { + fun findByGuid(guid: UUID): Customer? +} diff --git a/src/main/kotlin/com/example/demo/providers/MockedShopProvider.kt b/src/main/kotlin/com/example/demo/providers/MockedShopProvider.kt index d876f80..3deeded 100644 --- a/src/main/kotlin/com/example/demo/providers/MockedShopProvider.kt +++ b/src/main/kotlin/com/example/demo/providers/MockedShopProvider.kt @@ -12,7 +12,7 @@ class MockedShopProvider: ShopProvider { val productFour = makeProduct(id = 4, name = "four", price = 14.2) return Shop(name="shop", customers= listOf( - Customer( + CustomerLocal( name = "Foo-1", city = makeCity(id = 1, name = "Foo"), orders = listOf( @@ -20,7 +20,7 @@ class MockedShopProvider: ShopProvider { Order(products = listOf(productThree), isDelivered = false), ) ), - Customer( + CustomerLocal( name = "Foo-2", city = makeCity(id = 2, name = "Bar"), orders = listOf( @@ -51,4 +51,4 @@ class MockedShopProvider: ShopProvider { updatedAt = null, deletedAt = null, ) -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/example/demo/services/database/customer/CustomerService.kt b/src/main/kotlin/com/example/demo/services/database/customer/CustomerService.kt new file mode 100644 index 0000000..379c860 --- /dev/null +++ b/src/main/kotlin/com/example/demo/services/database/customer/CustomerService.kt @@ -0,0 +1,12 @@ +package com.example.demo.services.database.customer + +import com.example.demo.models.Customer +import com.example.demo.services.database.exceptions.CityNotFoundException +import java.util.* + +interface CustomerService { + fun findByGuid(guid: UUID): Customer? + + @Throws(CityNotFoundException::class) + fun create(name: String, cityGuid: UUID?): Customer +} diff --git a/src/main/kotlin/com/example/demo/services/database/customer/CustomerServiceImpl.kt b/src/main/kotlin/com/example/demo/services/database/customer/CustomerServiceImpl.kt new file mode 100644 index 0000000..dbe78fa --- /dev/null +++ b/src/main/kotlin/com/example/demo/services/database/customer/CustomerServiceImpl.kt @@ -0,0 +1,32 @@ +package com.example.demo.services.database.customer + +import com.example.demo.models.Customer +import com.example.demo.providers.CityRepository +import com.example.demo.providers.CustomerRepository +import com.example.demo.services.database.exceptions.CityNotFoundException +import java.time.OffsetDateTime +import java.util.* + +class CustomerServiceImpl( + private val customerRepository: CustomerRepository, + private val cityRepository: CityRepository +): CustomerService { + override fun findByGuid(guid: UUID): Customer? = customerRepository.findByGuid(guid) + + override fun create(name: String, cityGuid: UUID?): Customer { + val cityId: Long? = cityGuid?.let { + cityRepository.findByGuid(it)?.id ?: throw CityNotFoundException() + } + + val customer = Customer( + id = null, + guid = UUID.randomUUID(), + name = name, + cityId = cityId, + createdAt = OffsetDateTime.now(), + updatedAt = null, + ) + + return customerRepository.save(customer) + } +} diff --git a/src/main/resources/db/migration/structure/V5__create_customer_table.sql b/src/main/resources/db/migration/structure/V5__create_customer_table.sql new file mode 100644 index 0000000..6f3b103 --- /dev/null +++ b/src/main/resources/db/migration/structure/V5__create_customer_table.sql @@ -0,0 +1,14 @@ +create table customer ( + id bigserial primary key, + guid uuid not null, + name varchar(255) not null, + city_id bigint, + created_at timestamptz not null, + updated_at timestamptz, + CONSTRAINT customer_city_foreign + FOREIGN KEY(city_id) + REFERENCES city(id) + ON DELETE SET NULL +); + +create unique index customer_guid_idx ON customer (guid); \ No newline at end of file diff --git a/src/test/kotlin/com/example/demo/http/controllers/ShopControllerTest.kt b/src/test/kotlin/com/example/demo/http/controllers/ShopControllerTest.kt index fbfbeb4..7ec9f10 100644 --- a/src/test/kotlin/com/example/demo/http/controllers/ShopControllerTest.kt +++ b/src/test/kotlin/com/example/demo/http/controllers/ShopControllerTest.kt @@ -28,7 +28,7 @@ class ShopControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() { val productFour = makeProduct(id = 4, name = "four", price = 14.2) val shopMock = Shop(name="shop", customers= listOf( - Customer( + CustomerLocal( name = "cus-one", city = makeCity(id = 1, name = "city-one"), orders = listOf( @@ -37,7 +37,7 @@ class ShopControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() { Order(products = listOf(productThree), isDelivered = true), ) ), - Customer( + CustomerLocal( name = "cus-two", city = makeCity(id = 2, name = "city-two"), orders = listOf( @@ -94,4 +94,4 @@ class ShopControllerTest(@Autowired val mockMvc: MockMvc): BaseUnitTest() { updatedAt = null, deletedAt = null, ) -} \ No newline at end of file +} diff --git a/src/test/kotlin/com/example/demo/services/database/city/CityServiceImplDbTest.kt b/src/test/kotlin/com/example/demo/services/database/city/CityServiceImplDbTest.kt index 783a2e3..53e8ff1 100644 --- a/src/test/kotlin/com/example/demo/services/database/city/CityServiceImplDbTest.kt +++ b/src/test/kotlin/com/example/demo/services/database/city/CityServiceImplDbTest.kt @@ -3,8 +3,8 @@ package com.example.demo.services.database.city import com.example.demo.BaseDbTest import com.example.demo.models.City import com.example.demo.providers.CityRepository -import com.example.demo.services.database.exceptions.CityNotFoundException import com.example.demo.services.database.exceptions.AlreadyDeletedException +import com.example.demo.services.database.exceptions.CityNotFoundException import org.junit.jupiter.api.assertThrows import org.springframework.beans.factory.annotation.Autowired import org.springframework.test.context.ContextConfiguration @@ -25,7 +25,6 @@ class CityServiceImplDbTest: BaseDbTest() { try { city = cityServiceImpl.create(name = name) - assertNotNull(city) assertNotNull(city.id) assertEquals(name, city.name) @@ -35,7 +34,6 @@ class CityServiceImplDbTest: BaseDbTest() { assertFalse(dbCity.isDeleted()) val deletedCity = cityServiceImpl.delete(city.guid) - assertNotNull(deletedCity) assertEquals(city.id, deletedCity.id) assertNotNull(deletedCity.deletedAt) assertTrue(deletedCity.isDeleted()) diff --git a/src/test/kotlin/com/example/demo/services/database/customer/CustomerServiceImplDbTest.kt b/src/test/kotlin/com/example/demo/services/database/customer/CustomerServiceImplDbTest.kt new file mode 100644 index 0000000..486d003 --- /dev/null +++ b/src/test/kotlin/com/example/demo/services/database/customer/CustomerServiceImplDbTest.kt @@ -0,0 +1,73 @@ +package com.example.demo.services.database.customer + +import com.example.demo.BaseDbTest +import com.example.demo.models.City +import com.example.demo.providers.CityRepository +import com.example.demo.providers.CustomerRepository +import com.example.demo.services.database.exceptions.CityNotFoundException +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.test.context.ContextConfiguration +import java.time.OffsetDateTime +import java.util.* +import kotlin.test.* + +@ContextConfiguration(classes = [CustomerRepository::class, CityRepository::class, CustomerServiceImpl::class]) +class CustomerServiceImplDbTest: BaseDbTest() { + @Autowired + private lateinit var customerRepository: CustomerRepository + + @Autowired + private lateinit var cityRepository: CityRepository + + @Autowired + private lateinit var customerServiceImpl: CustomerServiceImpl + + @Test + fun createFind_success() { + val nameOne = "Some Dude-One" + val nameTwo = "Some Dude-Two" + val nameThree = "Some Dude-Three" + var city = City( + id = null, + guid = UUID.randomUUID(), + name = "some city name", + createdAt = OffsetDateTime.now(), + updatedAt = null, + deletedAt = null, + ) + var customerIds = longArrayOf() + + try { + city = cityRepository.save(city) + + customerServiceImpl.create(nameOne, city.guid).let { + customerIds += it.id ?: fail("customerWithCity id is null") + assertEquals(city.id, it.cityId) + assertNotNull(it.createdAt) + assertNull(it.updatedAt) + } + + val customerWithNoCity = customerServiceImpl.create(nameTwo, null) + customerIds += customerWithNoCity.id ?: fail("customerWithNoCity id is null") + assertNull(customerWithNoCity.cityId) + assertNotNull(customerWithNoCity.createdAt) + assertNull(customerWithNoCity.updatedAt) + + val existedCustomer = customerServiceImpl.findByGuid(customerWithNoCity.guid) + assertNotNull(existedCustomer) + assertEquals(customerWithNoCity.id, existedCustomer.id) + + assertThrows { + customerServiceImpl.create(nameThree, UUID.randomUUID()) + } + } finally { + val cityId = city.id + if (cityId != null) { + cityRepository.deleteById(cityId) + } + + customerIds.onEach { customerRepository.deleteById(it) } + } + } +}