diff --git a/src/main/kotlin/org/gitanimals/shop/app/BuyBackgroundFacade.kt b/src/main/kotlin/org/gitanimals/shop/app/BuyBackgroundFacade.kt new file mode 100644 index 0000000..2bd1b41 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/shop/app/BuyBackgroundFacade.kt @@ -0,0 +1,72 @@ +package org.gitanimals.shop.app + +import org.gitanimals.shop.domain.SaleService +import org.gitanimals.shop.domain.SaleType +import org.rooftop.netx.api.Orchestrator +import org.rooftop.netx.api.OrchestratorFactory +import org.springframework.stereotype.Service +import java.util.* + +@Service +class BuyBackgroundFacade( + orchestratorFactory: OrchestratorFactory, + identityApi: IdentityApi, + renderApi: RenderApi, + + private val saleService: SaleService, +) { + + private lateinit var backgroundBuyOrchestrator: Orchestrator + + fun buyBackground(token: String, item: String) { + backgroundBuyOrchestrator.sagaSync( + item, + mapOf("token" to token, "idempotencyKey" to UUID.randomUUID().toString()) + ).decodeResultOrThrow(Unit::class) + } + + init { + backgroundBuyOrchestrator = orchestratorFactory.create("buy background facade") + .start({ + val sale = saleService.getByTypeAndItem(SaleType.BACKGROUND, it) + + require(sale.getCount() > 0) { + "Cannot buy item : \"${sale.type}\" cause its count : \"${sale.getCount()}\" == 0" + } + + sale + }) + .joinWithContext( + contextOrchestrate = { context, sale -> + val token = context.decodeContext("token", String::class) + val idempotencyKey = context.decodeContext("idempotencyKey", String::class) + identityApi.decreasePoint(token, idempotencyKey, sale.price.toString()) + + sale + }, + contextRollback = { context, sale -> + val token = context.decodeContext("token", String::class) + val idempotencyKey = context.decodeContext("idempotencyKey", String::class) + identityApi.increasePoint(token, idempotencyKey, sale.price.toString()) + } + ) + .joinWithContext( + contextOrchestrate = { context, sale -> + val token = context.decodeContext("token", String::class) + val idempotencyKey = context.decodeContext("idempotencyKey", String::class) + + renderApi.addBackground(token, idempotencyKey, sale.item) + sale + }, + contextRollback = { context, sale -> + val token = context.decodeContext("token", String::class) + val idempotencyKey = context.decodeContext("idempotencyKey", String::class) + + renderApi.deleteBackground(token, idempotencyKey, sale.item) + } + ) + .commit { sale -> + saleService.buyBySaleTypeAndItem(sale.type, sale.item) + } + } +} diff --git a/src/main/kotlin/org/gitanimals/shop/app/RenderApi.kt b/src/main/kotlin/org/gitanimals/shop/app/RenderApi.kt index e006d4e..9b2ac9c 100644 --- a/src/main/kotlin/org/gitanimals/shop/app/RenderApi.kt +++ b/src/main/kotlin/org/gitanimals/shop/app/RenderApi.kt @@ -4,6 +4,10 @@ interface RenderApi { fun getPersonaById(token: String, personaId: Long): PersonaResponse + fun addBackground(token: String, idempotencyKey: String, backgroundName: String) + + fun deleteBackground(token: String, idempotencyKey: String, backgroundName: String) + fun deletePersonaById(token: String, personaId: Long) fun addPersona( diff --git a/src/main/kotlin/org/gitanimals/shop/controller/BuySaleController.kt b/src/main/kotlin/org/gitanimals/shop/controller/BuySaleController.kt new file mode 100644 index 0000000..9e11ab6 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/shop/controller/BuySaleController.kt @@ -0,0 +1,29 @@ +package org.gitanimals.shop.controller + +import org.gitanimals.shop.app.BuyBackgroundFacade +import org.gitanimals.shop.controller.request.BuyBackgroundRequest +import org.gitanimals.shop.controller.response.BackgroundResponse +import org.gitanimals.shop.domain.SaleService +import org.gitanimals.shop.domain.SaleType +import org.springframework.http.HttpHeaders +import org.springframework.web.bind.annotation.* + +@RestController +class BuySaleController( + private val saleService: SaleService, + private val buyBackgroundFacade: BuyBackgroundFacade, +) { + + @GetMapping("/shops/backgrounds") + fun getBackgrounds(): BackgroundResponse = + BackgroundResponse.from(saleService.findAllByType(SaleType.BACKGROUND)) + + + @PostMapping("/shops/backgrounds") + fun buyBackground( + @RequestHeader(HttpHeaders.AUTHORIZATION) token: String, + @RequestBody buyBackgroundRequest: BuyBackgroundRequest + ) { + buyBackgroundFacade.buyBackground(token, buyBackgroundRequest.type) + } +} diff --git a/src/main/kotlin/org/gitanimals/shop/controller/request/BuyBackgroundRequest.kt b/src/main/kotlin/org/gitanimals/shop/controller/request/BuyBackgroundRequest.kt new file mode 100644 index 0000000..b3d942d --- /dev/null +++ b/src/main/kotlin/org/gitanimals/shop/controller/request/BuyBackgroundRequest.kt @@ -0,0 +1,5 @@ +package org.gitanimals.shop.controller.request + +data class BuyBackgroundRequest( + val type: String, +) diff --git a/src/main/kotlin/org/gitanimals/shop/controller/response/BackgroundResponse.kt b/src/main/kotlin/org/gitanimals/shop/controller/response/BackgroundResponse.kt new file mode 100644 index 0000000..b2b73dd --- /dev/null +++ b/src/main/kotlin/org/gitanimals/shop/controller/response/BackgroundResponse.kt @@ -0,0 +1,26 @@ +package org.gitanimals.shop.controller.response + +import org.gitanimals.shop.domain.Sale + +data class BackgroundResponse( + val backgrounds: List, +) { + + data class Background( + val type: String, + val price: String, + ) + + companion object { + fun from(sales: List): BackgroundResponse { + return BackgroundResponse( + sales.map { + Background( + type = it.item, + price = it.price.toString(), + ) + }.toList() + ) + } + } +} diff --git a/src/main/kotlin/org/gitanimals/shop/domain/Sale.kt b/src/main/kotlin/org/gitanimals/shop/domain/Sale.kt new file mode 100644 index 0000000..d8c84df --- /dev/null +++ b/src/main/kotlin/org/gitanimals/shop/domain/Sale.kt @@ -0,0 +1,44 @@ +package org.gitanimals.shop.domain + +import jakarta.persistence.* +import org.gitanimals.shop.core.AggregateRoot + +@AggregateRoot +@Entity(name = "sale") +@Table( + name = "sale", indexes = [ + Index(name = "sale_idx_type", columnList = "type", unique = true) + ] +) +class Sale( + @Id + @Column(name = "id") + val id: Long, + + @Enumerated + @Column(name = "type", nullable = false, unique = true) + val type: SaleType, + + @Column(name = "item", nullable = false) + val item: String, + + @Column(name = "price", nullable = false) + val price: Long, + + @Column(name = "count", nullable = false) + private var count: Long, + + @Version + private var version: Long? = null, +) { + + fun getCount(): Long = this.count + + fun buy() { + require(this.count > 0) { + "Cannot buy item : \"$type\" cause its count : \"$count\" == 0" + } + + this.count -= 1 + } +} diff --git a/src/main/kotlin/org/gitanimals/shop/domain/SaleRepository.kt b/src/main/kotlin/org/gitanimals/shop/domain/SaleRepository.kt new file mode 100644 index 0000000..eba7439 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/shop/domain/SaleRepository.kt @@ -0,0 +1,9 @@ +package org.gitanimals.shop.domain + +import org.springframework.data.jpa.repository.JpaRepository + +interface SaleRepository : JpaRepository { + fun getByItem(item: String): Sale + + fun findAllByType(saleType: SaleType): List +} diff --git a/src/main/kotlin/org/gitanimals/shop/domain/SaleService.kt b/src/main/kotlin/org/gitanimals/shop/domain/SaleService.kt new file mode 100644 index 0000000..043f567 --- /dev/null +++ b/src/main/kotlin/org/gitanimals/shop/domain/SaleService.kt @@ -0,0 +1,33 @@ +package org.gitanimals.shop.domain + +import org.springframework.orm.ObjectOptimisticLockingFailureException +import org.springframework.retry.annotation.Retryable +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class SaleService( + private val saleRepository: SaleRepository, +) { + + fun findAllByType(saleType: SaleType): List = saleRepository.findAllByType(saleType) + + @Transactional + @Retryable(ObjectOptimisticLockingFailureException::class) + fun buyBySaleTypeAndItem(saleType: SaleType, item: String) { + val sale = getByTypeAndItem(saleType, item) + + sale.buy() + } + + fun getByTypeAndItem(saleType: SaleType, item: String): Sale { + val sale = saleRepository.getByItem(item) + + require(sale.type == saleType) { + "Cannot find sale by type: \"$saleType\" and item: \"$item\"" + } + + return sale + } +} diff --git a/src/main/kotlin/org/gitanimals/shop/domain/SaleType.kt b/src/main/kotlin/org/gitanimals/shop/domain/SaleType.kt new file mode 100644 index 0000000..75ef2da --- /dev/null +++ b/src/main/kotlin/org/gitanimals/shop/domain/SaleType.kt @@ -0,0 +1,7 @@ +package org.gitanimals.shop.domain + +enum class SaleType { + + BACKGROUND, + ; +} diff --git a/src/main/kotlin/org/gitanimals/shop/infra/RestRenderApi.kt b/src/main/kotlin/org/gitanimals/shop/infra/RestRenderApi.kt index 80dd1df..c3a3205 100644 --- a/src/main/kotlin/org/gitanimals/shop/infra/RestRenderApi.kt +++ b/src/main/kotlin/org/gitanimals/shop/infra/RestRenderApi.kt @@ -42,6 +42,26 @@ class RestRenderApi( } } + override fun addBackground(token: String, idempotencyKey: String, backgroundName: String) { + return restClient.post() + .uri("/internals/backgrounds?name=$backgroundName") + .header(HttpHeaders.AUTHORIZATION, token) + .header("Internal-Secret", internalSecret) + .exchange { _, response -> + require(response.statusCode.is2xxSuccessful) { "Cannot add background by backgroundName: \"$backgroundName\"" } + } + } + + override fun deleteBackground(token: String, idempotencyKey: String, backgroundName: String) { + return restClient.delete() + .uri("/internals/backgrounds?name=$backgroundName") + .header(HttpHeaders.AUTHORIZATION, token) + .header("Internal-Secret", internalSecret) + .exchange { _, response -> + require(response.statusCode.is2xxSuccessful) { "Cannot delete background by backgroundName: \"$backgroundName\"" } + } + } + override fun addPersona( token: String, idempotencyKey: String,