From 9b4a93aa6a3f27d4c96fbc1dad339b02cd1f82a7 Mon Sep 17 00:00:00 2001 From: Angelo Padron Date: Tue, 5 Nov 2024 12:18:42 -0300 Subject: [PATCH 1/5] add product service and other minor tweaks --- pom.xml | 6 ++ .../backend/service/BookmarkService.kt | 19 +--- .../meliapp/backend/service/ProductService.kt | 32 +++++++ .../service/BookmarkServiceUnitTest.kt | 79 ++++------------ .../backend/service/ProductServiceUnitTest.kt | 89 +++++++++++++++++++ 5 files changed, 145 insertions(+), 80 deletions(-) create mode 100644 src/main/kotlin/org/meliapp/backend/service/ProductService.kt create mode 100644 src/test/kotlin/org/meliapp/backend/service/ProductServiceUnitTest.kt diff --git a/pom.xml b/pom.xml index 9a8d9a8..66c7954 100644 --- a/pom.xml +++ b/pom.xml @@ -108,6 +108,12 @@ kotlin-test-junit5 test + + org.mockito.kotlin + mockito-kotlin + 5.4.0 + test + org.postgresql postgresql diff --git a/src/main/kotlin/org/meliapp/backend/service/BookmarkService.kt b/src/main/kotlin/org/meliapp/backend/service/BookmarkService.kt index 149a15d..c0b5f81 100644 --- a/src/main/kotlin/org/meliapp/backend/service/BookmarkService.kt +++ b/src/main/kotlin/org/meliapp/backend/service/BookmarkService.kt @@ -6,17 +6,14 @@ import org.meliapp.backend.dto.bookmark.BookmarkRequestBody import org.meliapp.backend.dto.bookmark.BookmarkSummary import org.meliapp.backend.exception.apc.BookmarkNotFoundException import org.meliapp.backend.model.Bookmark -import org.meliapp.backend.model.Product import org.meliapp.backend.repository.BookmarkRepository -import org.meliapp.backend.repository.ProductRepository import org.springframework.stereotype.Service @Service class BookmarkService( - private val meliSearchService: MeliSearchService, private val authService: AuthService, private val bookmarkRepository: BookmarkRepository, - private val productRepository: ProductRepository, + private val productService: ProductService, ) { fun getUserBookmarks(): List { @@ -47,18 +44,8 @@ class BookmarkService( @Transactional fun bookmarkProduct(request: BookmarkRequestBody): BookmarkDetails { val currentUser = authService.getUserAuthenticated() - val productResponse = meliSearchService.findById(request.meliId) - - val savedProduct = productRepository - .findByMeliId(productResponse.id) - .orElseGet { - productRepository.save(Product().apply { - meliId = productResponse.id - title = productResponse.title - thumbnail = productResponse.thumbnail - price = productResponse.price - }) - } + + val savedProduct = productService.findByMeliId(request.meliId) val bookmark = Bookmark().apply { product = savedProduct diff --git a/src/main/kotlin/org/meliapp/backend/service/ProductService.kt b/src/main/kotlin/org/meliapp/backend/service/ProductService.kt new file mode 100644 index 0000000..e91c7f2 --- /dev/null +++ b/src/main/kotlin/org/meliapp/backend/service/ProductService.kt @@ -0,0 +1,32 @@ +package org.meliapp.backend.service + +import org.meliapp.backend.model.Product +import org.meliapp.backend.repository.ProductRepository +import org.springframework.stereotype.Service +import java.util.* + +@Service +class ProductService( + private val productRepository: ProductRepository, + private val meliSearchService: MeliSearchService +) { + + fun findByMeliId(id: String): Product { + return productRepository.findByMeliId(id) + .orElseGet { + meliSearchService.findById(id).let { + productRepository.save(Product().apply { + title = it.title + price = it.price + thumbnail = it.thumbnail + meliId = id + }) + } + } + } + + fun findByMeliIdOld(meliId: String): Optional { + return productRepository.findByMeliId(meliId) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/org/meliapp/backend/service/BookmarkServiceUnitTest.kt b/src/test/kotlin/org/meliapp/backend/service/BookmarkServiceUnitTest.kt index 3d4a8c2..19cc35a 100644 --- a/src/test/kotlin/org/meliapp/backend/service/BookmarkServiceUnitTest.kt +++ b/src/test/kotlin/org/meliapp/backend/service/BookmarkServiceUnitTest.kt @@ -6,30 +6,24 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith import org.meliapp.backend.dto.bookmark.BookmarkRequestBody -import org.meliapp.backend.dto.product.ProductResponse import org.meliapp.backend.exception.apc.BookmarkNotFoundException import org.meliapp.backend.exception.apc.ProductNotFoundException import org.meliapp.backend.model.Bookmark import org.meliapp.backend.model.Product import org.meliapp.backend.model.User import org.meliapp.backend.repository.BookmarkRepository -import org.meliapp.backend.repository.ProductRepository -import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.ArgumentMatchers.anyString import org.mockito.InjectMocks import org.mockito.Mock -import org.mockito.Mockito.* +import org.mockito.kotlin.* import org.springframework.test.context.junit.jupiter.SpringExtension -import java.math.BigDecimal import java.util.* -import org.mockito.Mockito.`when` as whenever @ExtendWith(SpringExtension::class) class BookmarkServiceUnitTest { - @Mock - private lateinit var meliSearchService: MeliSearchService - @Mock private lateinit var authService: AuthService @@ -37,7 +31,7 @@ class BookmarkServiceUnitTest { private lateinit var bookmarkRepository: BookmarkRepository @Mock - private lateinit var productRepository: ProductRepository + private lateinit var productService: ProductService @InjectMocks private lateinit var bookmarkService: BookmarkService @@ -47,24 +41,15 @@ class BookmarkServiceUnitTest { // Arrange val bookmarkRequest = BookmarkRequestBody("meli_id", 5, "comment") - val productResponse = ProductResponse( - id = bookmarkRequest.meliId, - price = BigDecimal(1), - title = "product_title", - availableQuantity = 1, - thumbnail = "product_thumbnail", - ) - - val mockUser = mock(User::class.java) - val mockProduct = mock(Product::class.java) + val mockUser: User = mock() + val mockProduct: Product = mock() whenever(mockUser.id).thenReturn(1L) whenever(mockProduct.meliId).thenReturn("meli_id") whenever(mockProduct.title).thenReturn("product_title") whenever(mockProduct.thumbnail).thenReturn("product_thumbnail") - whenever(meliSearchService.findById(bookmarkRequest.meliId)).thenReturn(productResponse) - whenever(productRepository.findByMeliId(bookmarkRequest.meliId)).thenReturn(Optional.of(mockProduct)) + whenever(productService.findByMeliId(bookmarkRequest.meliId)).thenReturn(mockProduct) whenever(authService.getUserAuthenticated()).thenReturn(mockUser) // Act @@ -77,52 +62,18 @@ class BookmarkServiceUnitTest { // Verify verify(bookmarkRepository, times(1)).save(any()) - verify(productRepository, times(0)).save(any()) - - } - - @Test - fun `should not save an already persisted product`() { - // Arrange - val bookmarkRequest = BookmarkRequestBody("meli_id", 5, "comment") - - val productResponse = ProductResponse( - id = bookmarkRequest.meliId, - price = BigDecimal(1), - title = "product_title", - availableQuantity = 1, - thumbnail = "product_thumbnail", - ) - - val mockUser = mock(User::class.java) - val mockProduct = mock(Product::class.java) - whenever(mockUser.id).thenReturn(1L) - whenever(mockProduct.meliId).thenReturn("meli_id") - whenever(mockProduct.title).thenReturn("product_title") - whenever(mockProduct.thumbnail).thenReturn("product_thumbnail") - - whenever(meliSearchService.findById(bookmarkRequest.meliId)).thenReturn(productResponse) - whenever(productRepository.findByMeliId(bookmarkRequest.meliId)).thenReturn(Optional.empty()) - whenever(productRepository.save(any())).thenReturn(mockProduct) - whenever(authService.getUserAuthenticated()).thenReturn(mockUser) - - // Act - bookmarkService.bookmarkProduct(bookmarkRequest) - - // Verify - verify(productRepository, times(1)).save(any()) } @Test fun `should throw exception when bookmarking with wrong meli id`() { // Arrange val bookmarkRequest = BookmarkRequestBody("meli_id", 5, "comment") - val mockUser = mock(User::class.java) + val mockUser: User = mock() whenever(mockUser.id).thenReturn(1L) - whenever(meliSearchService.findById(bookmarkRequest.meliId)).thenThrow(ProductNotFoundException::class.java) whenever(authService.getUserAuthenticated()).thenReturn(mockUser) + whenever(productService.findByMeliId(anyString())).thenThrow(ProductNotFoundException::class.java) // Act and Assert assertThrows { bookmarkService.bookmarkProduct(bookmarkRequest) } @@ -131,7 +82,7 @@ class BookmarkServiceUnitTest { @Test fun `should return a list of bookmarks from user`() { // Arrange - val mockUser = mock(User::class.java) + val mockUser: User = mock() whenever(mockUser.id).thenReturn(1L) whenever(authService.getUserAuthenticated()).thenReturn(mockUser) @@ -147,8 +98,8 @@ class BookmarkServiceUnitTest { // Arrange val bookmarkId = 1L val userId = 1L - val bookmark = mock(Bookmark::class.java) - val user = mock(User::class.java) + val bookmark: Bookmark = mock() + val user: User = mock() whenever(user.id).thenReturn(userId) whenever(authService.getUserAuthenticated()).thenReturn(user) @@ -167,7 +118,7 @@ class BookmarkServiceUnitTest { // Arrange val bookmarkId = 1L val userId = 1L - val user = mock(User::class.java) + val user: User = mock() whenever(bookmarkRepository.findByIdAndUserId(userId, bookmarkId)).thenReturn(Optional.empty()) whenever(user.id).thenReturn(userId) @@ -181,8 +132,8 @@ class BookmarkServiceUnitTest { fun `should edit bookmark`() { // Arrange val request = BookmarkRequestBody("meli_id", 4, "updated comment") - val mockUser = mock(User::class.java) - val mockProduct = mock(Product::class.java) + val mockUser: User = mock() + val mockProduct: Product = mock() val bookmark = Bookmark().apply { id = 1L diff --git a/src/test/kotlin/org/meliapp/backend/service/ProductServiceUnitTest.kt b/src/test/kotlin/org/meliapp/backend/service/ProductServiceUnitTest.kt new file mode 100644 index 0000000..79f528b --- /dev/null +++ b/src/test/kotlin/org/meliapp/backend/service/ProductServiceUnitTest.kt @@ -0,0 +1,89 @@ +package org.meliapp.backend.service + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import org.meliapp.backend.dto.product.ProductResponse +import org.meliapp.backend.exception.apc.ProductNotFoundException +import org.meliapp.backend.model.Product +import org.meliapp.backend.repository.ProductRepository +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.kotlin.* +import org.springframework.test.context.junit.jupiter.SpringExtension +import java.math.BigDecimal +import java.util.* + +@ExtendWith(SpringExtension::class) +class ProductServiceUnitTest { + + @Mock + private lateinit var meliSearchService: MeliSearchService + + @Mock + private lateinit var productRepository: ProductRepository + + @InjectMocks + private lateinit var productService: ProductService + + @Test + fun `can find a product using it's meli id locally`() { + // Arrange + val product: Product = mock() + val meliId = "MELI_ID" + + whenever(product.meliId).thenReturn(meliId) + whenever(productRepository.findByMeliId(meliId)).thenReturn(Optional.of(product)) + + // Act + val result = productService.findByMeliIdOld(meliId) + + // Assert + assertTrue(result.isPresent) + assertEquals(meliId, result.get().meliId) + + } + + @Test + fun `should use search service when cannot find product locally`() { + val meliId = "MELI_ID" + + val product = Product().apply { + title = "title" + thumbnail = "thumbnail" + price = BigDecimal(999) + this.meliId = meliId + } + + val productResponse = ProductResponse( + id = meliId, + thumbnail = product.thumbnail, + price = product.price, + title = product.title, + availableQuantity = 1 + ) + + whenever(productRepository.findByMeliId(any())).thenReturn(Optional.empty()) + whenever(meliSearchService.findById(any())).thenReturn(productResponse) + whenever(productRepository.save(any())).thenReturn(product) + + val result = productService.findByMeliId(meliId) + + assertEquals(product.id, result.id) + verify(productRepository, times(1)).save(any()) + } + + @Test + fun `should throw an exception when the cannot find the product with the given id`() { + val meliId = "MELI_ID" + + whenever(productRepository.findByMeliId(meliId)).thenReturn(Optional.empty()) + whenever(meliSearchService.findById(meliId)).thenThrow(ProductNotFoundException::class.java) + + assertThrows { productService.findByMeliId(meliId) } + } + + +} \ No newline at end of file From 12d22c3d72ff3cb9a1009207d0fdf1ce4844fb06 Mon Sep 17 00:00:00 2001 From: Angelo Padron Date: Wed, 6 Nov 2024 19:10:02 -0300 Subject: [PATCH 2/5] add purchase feature and completed purchases listing --- .../backend/controller/PurchaseController.kt | 26 +++++++ .../dto/product/ProductPurchaseResponse.kt | 12 +++ .../backend/dto/purchase/PurchaseRequest.kt | 9 +++ .../backend/dto/purchase/PurchaseResponse.kt | 15 ++++ .../org/meliapp/backend/model/Purchase.kt | 22 ++++++ .../backend/repository/PurchaseRepository.kt | 11 +++ .../backend/service/PurchaseService.kt | 59 ++++++++++++++ .../service/PurchaseServiceUnitTest.kt | 78 +++++++++++++++++++ 8 files changed, 232 insertions(+) create mode 100644 src/main/kotlin/org/meliapp/backend/controller/PurchaseController.kt create mode 100644 src/main/kotlin/org/meliapp/backend/dto/product/ProductPurchaseResponse.kt create mode 100644 src/main/kotlin/org/meliapp/backend/dto/purchase/PurchaseRequest.kt create mode 100644 src/main/kotlin/org/meliapp/backend/dto/purchase/PurchaseResponse.kt create mode 100644 src/main/kotlin/org/meliapp/backend/model/Purchase.kt create mode 100644 src/main/kotlin/org/meliapp/backend/repository/PurchaseRepository.kt create mode 100644 src/main/kotlin/org/meliapp/backend/service/PurchaseService.kt create mode 100644 src/test/kotlin/org/meliapp/backend/service/PurchaseServiceUnitTest.kt diff --git a/src/main/kotlin/org/meliapp/backend/controller/PurchaseController.kt b/src/main/kotlin/org/meliapp/backend/controller/PurchaseController.kt new file mode 100644 index 0000000..807dbce --- /dev/null +++ b/src/main/kotlin/org/meliapp/backend/controller/PurchaseController.kt @@ -0,0 +1,26 @@ +package org.meliapp.backend.controller + +import org.meliapp.backend.dto.ApiResponse +import org.meliapp.backend.dto.purchase.PurchaseRequest +import org.meliapp.backend.dto.purchase.PurchaseResponse +import org.meliapp.backend.service.PurchaseService +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/api/purchases") +class PurchaseController( + private val purchaseService: PurchaseService +) { + + @PostMapping + fun buy(@RequestBody purchaseRequest: PurchaseRequest): ResponseEntity> { + return ResponseEntity.ok(ApiResponse(purchaseService.buy(purchaseRequest))) + } + + @GetMapping + fun purchases(): ResponseEntity>> { + return ResponseEntity.ok(ApiResponse(purchaseService.purchases())) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/org/meliapp/backend/dto/product/ProductPurchaseResponse.kt b/src/main/kotlin/org/meliapp/backend/dto/product/ProductPurchaseResponse.kt new file mode 100644 index 0000000..cad96e4 --- /dev/null +++ b/src/main/kotlin/org/meliapp/backend/dto/product/ProductPurchaseResponse.kt @@ -0,0 +1,12 @@ +package org.meliapp.backend.dto.product + +import com.fasterxml.jackson.annotation.JsonProperty +import java.math.BigDecimal + +data class ProductPurchaseResponse( + @JsonProperty("meli_id") + val meliId: String, + val title: String, + val thumbnail: String, + val price: BigDecimal, +) diff --git a/src/main/kotlin/org/meliapp/backend/dto/purchase/PurchaseRequest.kt b/src/main/kotlin/org/meliapp/backend/dto/purchase/PurchaseRequest.kt new file mode 100644 index 0000000..bb55ea1 --- /dev/null +++ b/src/main/kotlin/org/meliapp/backend/dto/purchase/PurchaseRequest.kt @@ -0,0 +1,9 @@ +package org.meliapp.backend.dto.purchase + +import com.fasterxml.jackson.annotation.JsonProperty + +data class PurchaseRequest( + @JsonProperty(value = "meli_id") + val meliId: String, + val quantity: Int +) diff --git a/src/main/kotlin/org/meliapp/backend/dto/purchase/PurchaseResponse.kt b/src/main/kotlin/org/meliapp/backend/dto/purchase/PurchaseResponse.kt new file mode 100644 index 0000000..5875764 --- /dev/null +++ b/src/main/kotlin/org/meliapp/backend/dto/purchase/PurchaseResponse.kt @@ -0,0 +1,15 @@ +package org.meliapp.backend.dto.purchase + +import com.fasterxml.jackson.annotation.JsonProperty +import org.meliapp.backend.dto.product.ProductPurchaseResponse +import java.math.BigDecimal +import java.time.LocalDateTime + +data class PurchaseResponse( + val id: Long, + val quantity: Int, + val product: ProductPurchaseResponse, + @JsonProperty("created_at") + val createdAt: LocalDateTime, + val total: BigDecimal, +) diff --git a/src/main/kotlin/org/meliapp/backend/model/Purchase.kt b/src/main/kotlin/org/meliapp/backend/model/Purchase.kt new file mode 100644 index 0000000..8b99b87 --- /dev/null +++ b/src/main/kotlin/org/meliapp/backend/model/Purchase.kt @@ -0,0 +1,22 @@ +package org.meliapp.backend.model + +import jakarta.persistence.* +import org.hibernate.annotations.CreationTimestamp +import java.math.BigDecimal +import java.time.LocalDateTime + +@Entity +class Purchase { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long = 0 + @ManyToOne + lateinit var user: User + @ManyToOne + lateinit var product: Product + var quantity: Int = 0 + @CreationTimestamp + var purchaseDate: LocalDateTime = LocalDateTime.now() + var totalPrice: BigDecimal = BigDecimal.ZERO +} \ No newline at end of file diff --git a/src/main/kotlin/org/meliapp/backend/repository/PurchaseRepository.kt b/src/main/kotlin/org/meliapp/backend/repository/PurchaseRepository.kt new file mode 100644 index 0000000..1fe2399 --- /dev/null +++ b/src/main/kotlin/org/meliapp/backend/repository/PurchaseRepository.kt @@ -0,0 +1,11 @@ +package org.meliapp.backend.repository + +import org.meliapp.backend.model.Purchase +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param + +interface PurchaseRepository : JpaRepository { + @Query("SELECT p FROM Purchase p WHERE p.user.id = :id") + fun findByUserId(@Param("id") id: Long): List +} \ No newline at end of file diff --git a/src/main/kotlin/org/meliapp/backend/service/PurchaseService.kt b/src/main/kotlin/org/meliapp/backend/service/PurchaseService.kt new file mode 100644 index 0000000..4957015 --- /dev/null +++ b/src/main/kotlin/org/meliapp/backend/service/PurchaseService.kt @@ -0,0 +1,59 @@ +package org.meliapp.backend.service + +import org.meliapp.backend.dto.product.ProductPurchaseResponse +import org.meliapp.backend.dto.purchase.PurchaseRequest +import org.meliapp.backend.dto.purchase.PurchaseResponse +import org.meliapp.backend.model.Product +import org.meliapp.backend.model.Purchase +import org.meliapp.backend.repository.PurchaseRepository +import org.springframework.stereotype.Service +import java.math.BigDecimal + +@Service +class PurchaseService( + private val purchaseRepository: PurchaseRepository, + private val authService: AuthService, + private val productService: ProductService +) { + + fun buy(purchaseRequest: PurchaseRequest): PurchaseResponse { + val user = authService.getUserAuthenticated() + val product = productService.findByMeliId(purchaseRequest.meliId) + + val purchase = Purchase() + purchase.user = user + purchase.quantity = purchaseRequest.quantity + purchase.product = product + purchase.totalPrice = product.price * BigDecimal(purchaseRequest.quantity) + + purchaseRepository.save(purchase) + + return toPurchaseResponse(purchase) + + } + + fun purchases(): List { + val user = authService.getUserAuthenticated() + return purchaseRepository.findByUserId(user.id).map { toPurchaseResponse(it) } + } + + private fun toPurchaseResponse(purchase: Purchase): PurchaseResponse { + return PurchaseResponse( + purchase.id, + purchase.quantity, + toProductPurchaseResponse(purchase.product), + purchase.purchaseDate, + purchase.totalPrice + ) + } + + private fun toProductPurchaseResponse(product: Product): ProductPurchaseResponse { + return ProductPurchaseResponse( + product.meliId, + product.title, + product.thumbnail, + product.price) + } + + +} \ No newline at end of file diff --git a/src/test/kotlin/org/meliapp/backend/service/PurchaseServiceUnitTest.kt b/src/test/kotlin/org/meliapp/backend/service/PurchaseServiceUnitTest.kt new file mode 100644 index 0000000..3a1864d --- /dev/null +++ b/src/test/kotlin/org/meliapp/backend/service/PurchaseServiceUnitTest.kt @@ -0,0 +1,78 @@ +package org.meliapp.backend.service + +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.meliapp.backend.dto.purchase.PurchaseRequest +import org.meliapp.backend.model.Product +import org.meliapp.backend.model.Purchase +import org.meliapp.backend.model.User +import org.meliapp.backend.repository.PurchaseRepository +import org.mockito.InjectMocks +import org.mockito.Mock +import org.mockito.kotlin.* +import org.springframework.test.context.junit.jupiter.SpringExtension +import java.math.BigDecimal +import kotlin.test.assertEquals + +@ExtendWith(SpringExtension::class) +class PurchaseServiceUnitTest { + + @Mock + lateinit var purchaseRepository: PurchaseRepository + + @Mock + lateinit var productService: ProductService + + @Mock + lateinit var authService: AuthService + + @InjectMocks + lateinit var purchaseService: PurchaseService + + @BeforeEach + fun setup() { + val user: User = mock() + whenever(user.id).thenReturn(1) + whenever(authService.getUserAuthenticated()).thenReturn(user) + } + + @AfterEach + fun cleanup() { + reset(purchaseRepository, productService, authService) + } + + @Test + fun `can buy an existing product`() { + val meliId = "MELI_ID" + val productTitle = "TITLE" + val product: Product = mock() + + whenever(product.meliId).thenReturn(meliId) + whenever(product.price).thenReturn(BigDecimal.ZERO) + whenever(product.title).thenReturn(productTitle) + whenever(productService.findByMeliId(meliId)).thenReturn(product) + + val result = purchaseService.buy(PurchaseRequest(meliId, 1)) + + assertEquals(meliId, result.product.meliId) + assertEquals(productTitle, result.product.title) + assertEquals(BigDecimal.ZERO, result.product.price) + assertEquals(1, result.quantity) + assertEquals(BigDecimal.ZERO, result.total) + + verify(purchaseRepository, times(1)).save(any()) + + } + + @Test + fun `can get all the purchases from current user`() { + whenever(purchaseRepository.findByUserId(any())).thenReturn(listOf()) + + val result = purchaseService.purchases() + + assertEquals(0, result.size) + } + +} \ No newline at end of file From 2577a007032e7514c064602db3d3c8003daa87df Mon Sep 17 00:00:00 2001 From: Angelo Padron Date: Wed, 6 Nov 2024 19:17:03 -0300 Subject: [PATCH 3/5] fix test --- .../org/meliapp/backend/service/PurchaseServiceUnitTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/kotlin/org/meliapp/backend/service/PurchaseServiceUnitTest.kt b/src/test/kotlin/org/meliapp/backend/service/PurchaseServiceUnitTest.kt index 3a1864d..3cde813 100644 --- a/src/test/kotlin/org/meliapp/backend/service/PurchaseServiceUnitTest.kt +++ b/src/test/kotlin/org/meliapp/backend/service/PurchaseServiceUnitTest.kt @@ -47,17 +47,20 @@ class PurchaseServiceUnitTest { fun `can buy an existing product`() { val meliId = "MELI_ID" val productTitle = "TITLE" + val thumbnail = "THUMBNAIL" val product: Product = mock() whenever(product.meliId).thenReturn(meliId) whenever(product.price).thenReturn(BigDecimal.ZERO) whenever(product.title).thenReturn(productTitle) + whenever(product.thumbnail).thenReturn(thumbnail) whenever(productService.findByMeliId(meliId)).thenReturn(product) val result = purchaseService.buy(PurchaseRequest(meliId, 1)) assertEquals(meliId, result.product.meliId) assertEquals(productTitle, result.product.title) + assertEquals(thumbnail, result.product.thumbnail) assertEquals(BigDecimal.ZERO, result.product.price) assertEquals(1, result.quantity) assertEquals(BigDecimal.ZERO, result.total) From 3707d8c7a37c2c6eee432a0beb037346ddd9b876 Mon Sep 17 00:00:00 2001 From: Angelo Padron Date: Thu, 7 Nov 2024 17:48:17 -0300 Subject: [PATCH 4/5] separate product response into overview and details DTOs --- .../controller/MeliSearchController.kt | 4 +-- .../backend/dto/meli/MeliSearchResponse.kt | 4 +-- .../dto/product/ProductDetailsResponse.kt | 25 +++++++++++++++++++ ...ductResponse.kt => ProductListResponse.kt} | 2 +- .../backend/service/MeliSearchService.kt | 16 +++++++++--- .../service/MeliSearchServiceUnitTest.kt | 18 ++++++++----- .../backend/service/ProductServiceUnitTest.kt | 10 +++++--- 7 files changed, 61 insertions(+), 18 deletions(-) create mode 100644 src/main/kotlin/org/meliapp/backend/dto/product/ProductDetailsResponse.kt rename src/main/kotlin/org/meliapp/backend/dto/product/{ProductResponse.kt => ProductListResponse.kt} (89%) diff --git a/src/main/kotlin/org/meliapp/backend/controller/MeliSearchController.kt b/src/main/kotlin/org/meliapp/backend/controller/MeliSearchController.kt index 70831cf..25aba8b 100644 --- a/src/main/kotlin/org/meliapp/backend/controller/MeliSearchController.kt +++ b/src/main/kotlin/org/meliapp/backend/controller/MeliSearchController.kt @@ -2,7 +2,7 @@ package org.meliapp.backend.controller import org.meliapp.backend.dto.ApiResponse import org.meliapp.backend.dto.meli.MeliSearchResponse -import org.meliapp.backend.dto.product.ProductResponse +import org.meliapp.backend.dto.product.ProductDetailsResponse import org.meliapp.backend.service.MeliSearchService import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* @@ -17,7 +17,7 @@ class MeliSearchController(private val meliSearchService: MeliSearchService) { } @GetMapping("/{id}") - fun findById(@PathVariable id: String): ResponseEntity> { + fun findById(@PathVariable id: String): ResponseEntity> { return ResponseEntity.ok(ApiResponse(meliSearchService.findById(id))) } diff --git a/src/main/kotlin/org/meliapp/backend/dto/meli/MeliSearchResponse.kt b/src/main/kotlin/org/meliapp/backend/dto/meli/MeliSearchResponse.kt index c9a4a67..0f636ea 100644 --- a/src/main/kotlin/org/meliapp/backend/dto/meli/MeliSearchResponse.kt +++ b/src/main/kotlin/org/meliapp/backend/dto/meli/MeliSearchResponse.kt @@ -2,10 +2,10 @@ package org.meliapp.backend.dto.meli import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty -import org.meliapp.backend.dto.product.ProductResponse +import org.meliapp.backend.dto.product.ProductListResponse data class MeliSearchResponse @JsonCreator constructor( - val results: List = emptyList(), + val results: List = emptyList(), val filters: List = emptyList(), @JsonProperty(value = "available_filters") val availableFilters: List = emptyList(), diff --git a/src/main/kotlin/org/meliapp/backend/dto/product/ProductDetailsResponse.kt b/src/main/kotlin/org/meliapp/backend/dto/product/ProductDetailsResponse.kt new file mode 100644 index 0000000..beddeaf --- /dev/null +++ b/src/main/kotlin/org/meliapp/backend/dto/product/ProductDetailsResponse.kt @@ -0,0 +1,25 @@ +package org.meliapp.backend.dto.product + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty +import java.math.BigDecimal + +data class ProductDetailsResponse( + @JsonProperty("id") + val meliId: String, + val title: String, + val price: BigDecimal, + val thumbnail: String, + val pictures: List, + var description: String?, + + ) + +data class DescriptionResponse( + @JsonProperty("plain_text") + val description: String +) + +data class PicturesListResponse @JsonCreator constructor( + val url: String, +) \ No newline at end of file diff --git a/src/main/kotlin/org/meliapp/backend/dto/product/ProductResponse.kt b/src/main/kotlin/org/meliapp/backend/dto/product/ProductListResponse.kt similarity index 89% rename from src/main/kotlin/org/meliapp/backend/dto/product/ProductResponse.kt rename to src/main/kotlin/org/meliapp/backend/dto/product/ProductListResponse.kt index 8a1acd0..8771e03 100644 --- a/src/main/kotlin/org/meliapp/backend/dto/product/ProductResponse.kt +++ b/src/main/kotlin/org/meliapp/backend/dto/product/ProductListResponse.kt @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty import java.math.BigDecimal -data class ProductResponse @JsonCreator constructor( +data class ProductListResponse @JsonCreator constructor( @JsonProperty("id") val id: String = "", @JsonProperty("title") diff --git a/src/main/kotlin/org/meliapp/backend/service/MeliSearchService.kt b/src/main/kotlin/org/meliapp/backend/service/MeliSearchService.kt index 27ed1eb..e84966f 100644 --- a/src/main/kotlin/org/meliapp/backend/service/MeliSearchService.kt +++ b/src/main/kotlin/org/meliapp/backend/service/MeliSearchService.kt @@ -1,7 +1,8 @@ package org.meliapp.backend.service import org.meliapp.backend.dto.meli.MeliSearchResponse -import org.meliapp.backend.dto.product.ProductResponse +import org.meliapp.backend.dto.product.DescriptionResponse +import org.meliapp.backend.dto.product.ProductDetailsResponse import org.meliapp.backend.exception.apc.ProductNotFoundException import org.springframework.beans.factory.annotation.Value import org.springframework.core.ParameterizedTypeReference @@ -36,7 +37,7 @@ class MeliSearchService( } - fun findById(id: String): ProductResponse { + fun findById(id: String): ProductDetailsResponse { val queryString = "/items/${id}" return restClient @@ -45,7 +46,16 @@ class MeliSearchService( .accept(MediaType.APPLICATION_JSON) .retrieve() .onStatus({ it.is4xxClientError }) { _, _ -> throw ProductNotFoundException(id) } - .body(object : ParameterizedTypeReference() {})!! + .body(object : ParameterizedTypeReference() {})!! + .also { + it.description = restClient + .get() + .uri("$queryString/description") + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .body(object : ParameterizedTypeReference() {}) + ?.description + } } diff --git a/src/test/kotlin/org/meliapp/backend/service/MeliSearchServiceUnitTest.kt b/src/test/kotlin/org/meliapp/backend/service/MeliSearchServiceUnitTest.kt index 1426029..2f131be 100644 --- a/src/test/kotlin/org/meliapp/backend/service/MeliSearchServiceUnitTest.kt +++ b/src/test/kotlin/org/meliapp/backend/service/MeliSearchServiceUnitTest.kt @@ -5,7 +5,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith import org.meliapp.backend.dto.meli.MeliSearchResponse -import org.meliapp.backend.dto.product.ProductResponse import org.meliapp.backend.exception.apc.ProductNotFoundException import org.mockito.MockitoAnnotations import org.springframework.beans.factory.annotation.Autowired @@ -21,7 +20,6 @@ import org.springframework.test.web.client.match.MockRestRequestMatchers.request import org.springframework.test.web.client.response.MockRestResponseCreators.withResourceNotFound import org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess import org.springframework.web.client.RestClient -import java.math.BigDecimal import kotlin.test.assertEquals @@ -75,21 +73,29 @@ class MeliSearchServiceUnitTest { fun `find by id should return a product response`() { val id = "MLA123" val expectedUri = "/items/$id" - - val mockResponse = ProductResponse(id, "", BigDecimal.valueOf(0), "", 0) + val expectedDescriptionUri = "$expectedUri/description" server.expect(requestTo(expectedUri)) .andExpect(method(HttpMethod.GET)) .andRespond( withSuccess( - "{ \"id\": \"$id\", \"title\": \"\", \"price\": 0, \"thumbnail\": \"\", \"available_quantity\": 0 }", + "{ \"id\": \"$id\", \"title\": \"\", \"price\": 0, \"thumbnail\": \"\", \"available_quantity\": 0, \"pictures\": [] }", + MediaType.APPLICATION_JSON + ) + ) + + server.expect(requestTo(expectedDescriptionUri)) + .andExpect(method(HttpMethod.GET)) + .andRespond( + withSuccess( + "{ \"plain_text\": \"description\"}", MediaType.APPLICATION_JSON ) ) val response = meliSearchService.findById(id) - assertEquals(mockResponse, response) + assertEquals(id, response.meliId) } @Test diff --git a/src/test/kotlin/org/meliapp/backend/service/ProductServiceUnitTest.kt b/src/test/kotlin/org/meliapp/backend/service/ProductServiceUnitTest.kt index 79f528b..c0c504a 100644 --- a/src/test/kotlin/org/meliapp/backend/service/ProductServiceUnitTest.kt +++ b/src/test/kotlin/org/meliapp/backend/service/ProductServiceUnitTest.kt @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.ExtendWith -import org.meliapp.backend.dto.product.ProductResponse +import org.meliapp.backend.dto.product.ProductDetailsResponse import org.meliapp.backend.exception.apc.ProductNotFoundException import org.meliapp.backend.model.Product import org.meliapp.backend.repository.ProductRepository @@ -57,12 +57,14 @@ class ProductServiceUnitTest { this.meliId = meliId } - val productResponse = ProductResponse( - id = meliId, + val productResponse = ProductDetailsResponse( + meliId = meliId, thumbnail = product.thumbnail, price = product.price, title = product.title, - availableQuantity = 1 + pictures = listOf(), + description = "" + ) whenever(productRepository.findByMeliId(any())).thenReturn(Optional.empty()) From 41b953578215dcc097ac2fc0de4f86ce6f926ad2 Mon Sep 17 00:00:00 2001 From: Angelo Padron Date: Fri, 8 Nov 2024 22:22:07 -0300 Subject: [PATCH 5/5] add minor tweaks --- .../backend/controller/PurchaseController.kt | 12 ++--- .../org/meliapp/backend/model/Purchase.kt | 1 - .../backend/service/PurchaseService.kt | 47 +++++++++---------- .../service/PurchaseServiceUnitTest.kt | 1 + 4 files changed, 28 insertions(+), 33 deletions(-) diff --git a/src/main/kotlin/org/meliapp/backend/controller/PurchaseController.kt b/src/main/kotlin/org/meliapp/backend/controller/PurchaseController.kt index 807dbce..2e8270e 100644 --- a/src/main/kotlin/org/meliapp/backend/controller/PurchaseController.kt +++ b/src/main/kotlin/org/meliapp/backend/controller/PurchaseController.kt @@ -14,13 +14,13 @@ class PurchaseController( ) { @PostMapping - fun buy(@RequestBody purchaseRequest: PurchaseRequest): ResponseEntity> { - return ResponseEntity.ok(ApiResponse(purchaseService.buy(purchaseRequest))) - } + fun buy(@RequestBody purchaseRequest: PurchaseRequest): ResponseEntity> = + ResponseEntity.ok(ApiResponse(purchaseService.buy(purchaseRequest))) + @GetMapping - fun purchases(): ResponseEntity>> { - return ResponseEntity.ok(ApiResponse(purchaseService.purchases())) - } + fun purchases(): ResponseEntity>> = + ResponseEntity.ok(ApiResponse(purchaseService.purchases())) + } \ No newline at end of file diff --git a/src/main/kotlin/org/meliapp/backend/model/Purchase.kt b/src/main/kotlin/org/meliapp/backend/model/Purchase.kt index 8b99b87..f121ba6 100644 --- a/src/main/kotlin/org/meliapp/backend/model/Purchase.kt +++ b/src/main/kotlin/org/meliapp/backend/model/Purchase.kt @@ -7,7 +7,6 @@ import java.time.LocalDateTime @Entity class Purchase { - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) var id: Long = 0 diff --git a/src/main/kotlin/org/meliapp/backend/service/PurchaseService.kt b/src/main/kotlin/org/meliapp/backend/service/PurchaseService.kt index 4957015..4f31a21 100644 --- a/src/main/kotlin/org/meliapp/backend/service/PurchaseService.kt +++ b/src/main/kotlin/org/meliapp/backend/service/PurchaseService.kt @@ -16,44 +16,39 @@ class PurchaseService( private val productService: ProductService ) { - fun buy(purchaseRequest: PurchaseRequest): PurchaseResponse { - val user = authService.getUserAuthenticated() - val product = productService.findByMeliId(purchaseRequest.meliId) - - val purchase = Purchase() - purchase.user = user - purchase.quantity = purchaseRequest.quantity - purchase.product = product - purchase.totalPrice = product.price * BigDecimal(purchaseRequest.quantity) - - purchaseRepository.save(purchase) - - return toPurchaseResponse(purchase) - - } + fun buy(purchaseRequest: PurchaseRequest): PurchaseResponse = + toPurchaseResponse( + purchaseRepository.save( + Purchase().apply { + this.user = authService.getUserAuthenticated() + this.product = productService.findByMeliId(purchaseRequest.meliId) + quantity = purchaseRequest.quantity + totalPrice = product.price * BigDecimal(quantity) + } + ) + ) - fun purchases(): List { - val user = authService.getUserAuthenticated() - return purchaseRepository.findByUserId(user.id).map { toPurchaseResponse(it) } - } + fun purchases(): List = + purchaseRepository + .findByUserId(authService.getUserAuthenticated().id) + .map { toPurchaseResponse(it) } - private fun toPurchaseResponse(purchase: Purchase): PurchaseResponse { - return PurchaseResponse( + private fun toPurchaseResponse(purchase: Purchase): PurchaseResponse = + PurchaseResponse( purchase.id, purchase.quantity, toProductPurchaseResponse(purchase.product), purchase.purchaseDate, purchase.totalPrice ) - } - private fun toProductPurchaseResponse(product: Product): ProductPurchaseResponse { - return ProductPurchaseResponse( + private fun toProductPurchaseResponse(product: Product): ProductPurchaseResponse = + ProductPurchaseResponse( product.meliId, product.title, product.thumbnail, - product.price) - } + product.price + ) } \ No newline at end of file diff --git a/src/test/kotlin/org/meliapp/backend/service/PurchaseServiceUnitTest.kt b/src/test/kotlin/org/meliapp/backend/service/PurchaseServiceUnitTest.kt index 3cde813..41e7a21 100644 --- a/src/test/kotlin/org/meliapp/backend/service/PurchaseServiceUnitTest.kt +++ b/src/test/kotlin/org/meliapp/backend/service/PurchaseServiceUnitTest.kt @@ -55,6 +55,7 @@ class PurchaseServiceUnitTest { whenever(product.title).thenReturn(productTitle) whenever(product.thumbnail).thenReturn(thumbnail) whenever(productService.findByMeliId(meliId)).thenReturn(product) + whenever(purchaseRepository.save(any())).thenAnswer { it.getArgument(0) } val result = purchaseService.buy(PurchaseRequest(meliId, 1))