Skip to content

Commit

Permalink
release: 0.0.7 (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
devxb authored Apr 29, 2024
2 parents 5c5ffa5 + 5fb7974 commit 9a0726e
Show file tree
Hide file tree
Showing 33 changed files with 1,182 additions and 2 deletions.
49 changes: 49 additions & 0 deletions docs/api/buy_product.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Buy_product

productId에 해당하는 product를 구매합니다.

## Request

### HTTP METHOD : `POST`

### url : `https://api.gitanimals.org/auctions/products/{product-id}`

### RequestHeader

- Authorization: `{token}`

### RequestBody

## Response

200 OK

```json
{
"id": "1",
"sellerId": "1",
"persona": {
"personaId": "1",
"personaType": "PENGUIN",
"personaLevel": 1
},
"price": "1000",
"paymentState": "SOLD_OUT",
"receipt": {
"buyerId": "1",
"soldAt": "2022-04-29T10:15:30Z"
}
}
```

| key | description |
|----------------------|--------------------------------------------------------|
| id | 등록된 상품의 id |
| sellerId | 상품을 판매하는 유저의 id |
| persona.personaId | persona의 id |
| persona.personaType | persona의 type |
| persona.personaLevel | persona의 level |
| price | 등록된 상품의 가격 |
| paymentState | 등록된 상품의 상태 |
| receipt | 결제정보. 등록된 상품이 SOLD_OUT 상태일때만 존재하며, 아니라면, null값이 반환됩니다. |

60 changes: 60 additions & 0 deletions docs/api/register_product.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Register product

shop에 product를 등록한다.

## Request

### HTTP METHOD : `POST`

### url : `https://api.gitanimals.org/auctions/products`

### RequestHeader

- Authorization: `{token}`

### Request body

```json
{
"personaId": "1",
"price": "1000"
}
```

| key | description |
|-----------|---------------------------------------------|
| personaId | 등록할 펫의 id 입니다. |
| price | 등록할 펫의 가격입니다. Long 범위(2^64) 안에서 설정할 수 있습니다. |

# Response

200 OK

```json
{
"id": "1",
"sellerId": "1",
"persona": {
"personaId": "1",
"personaType": "PENGUIN",
"personaLevel": 1
},
"price": "1000",
"paymentState": "SOLD_OUT",
"receipt": {
"buyerId": "1",
"soldAt": "2022-04-29T10:15:30Z"
}
}
```

| key | description |
|---------------------|--------------------------------------------------------|
| id | 등록된 상품의 id |
| sellerId | 상품을 판매하는 유저의 id |
| persona.personaId | persona의 id |
| persona.personaType | persona의 type |
| persona.personaLevel | persona의 level |
| price | 등록된 상품의 가격 |
| paymentState | 등록된 상품의 상태 |
| receipt | 결제정보. 등록된 상품이 SOLD_OUT 상태일때만 존재하며, 아니라면, null값이 반환됩니다. |
79 changes: 79 additions & 0 deletions src/main/kotlin/org/gitanimals/auction/app/BuyProductFacade.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.gitanimals.auction.app

import org.gitanimals.auction.domain.Product
import org.gitanimals.auction.domain.ProductService
import org.rooftop.netx.api.Orchestrator
import org.rooftop.netx.api.OrchestratorFactory
import org.springframework.stereotype.Service
import java.util.*

@Service
class BuyProductFacade(
private val renderApi: RenderApi,
private val identityApi: IdentityApi,
private val productService: ProductService,
orchestratorFactory: OrchestratorFactory,
) {

private lateinit var orchestrator: Orchestrator<Long, Product>

fun buyProduct(token: String, productId: Long): Product {
val buyer = identityApi.getUserByToken(token)
val product = productService.getProductById(productId)

require(product.price <= buyer.points.toLong()) {
"Cannot buy product cause buyer does not have enough point \"${product.price}\" >= \"${buyer.points}\""
}

return orchestrator.sagaSync(
productId,
mapOf(
"token" to token,
"idempotencyKey" to UUID.randomUUID().toString()
)
).decodeResultOrThrow(Product::class)
}

init {
this.orchestrator = orchestratorFactory.create<Long>("buy product orchestrator")
.startWithContext(
contextOrchestrate = { context, productId ->
val token = context.decodeContext("token", String::class)
val buyer = identityApi.getUserByToken(token)

productService.buyProduct(productId, buyer.id.toLong())
},
contextRollback = { _, productId -> productService.rollbackProduct(productId) }
)
.joinWithContext(
contextOrchestrate = { context, product ->
val token = context.decodeContext("token", String::class)
val idempotencyKey = context.decodeContext("idempotencyKey", String::class)

identityApi.decreasePoint(token, idempotencyKey, product.price.toString())

product
},
contextRollback = { context, product ->
val token = context.decodeContext("token", String::class)
val idempotencyKey = context.decodeContext("idempotencyKey", String::class)

identityApi.increasePoint(token, idempotencyKey, product.price.toString())
}
)
.commitWithContext { context, product ->
val token = context.decodeContext("token", String::class)
val idempotencyKey = context.decodeContext("idempotencyKey", String::class)

renderApi.addPersona(
token,
idempotencyKey,
product.persona.personaId,
product.persona.personaLevel,
product.persona.personaType,
)

product
}
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/org/gitanimals/auction/app/IdentityApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.gitanimals.auction.app

interface IdentityApi {

fun getUserByToken(token: String): UserResponse

fun decreasePoint(token: String, idempotencyKey: String, point: String)

fun increasePoint(token: String, idempotencyKey: String, point: String)

data class UserResponse(
val id: String,
val username: String,
val points: String,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.gitanimals.auction.app

import org.gitanimals.auction.domain.Product
import org.gitanimals.auction.domain.ProductService
import org.gitanimals.auction.domain.request.RegisterProductRequest
import org.rooftop.netx.api.Orchestrator
import org.rooftop.netx.api.OrchestratorFactory
import org.springframework.stereotype.Service
import java.util.*

@Service
class RegisterProductFacade(
private val identityApi: IdentityApi,
private val renderApi: RenderApi,
private val productService: ProductService,
orchestratorFactory: OrchestratorFactory,
) {

private lateinit var orchestrator: Orchestrator<RegisterProductRequest, Product>

fun registerProduct(token: String, personaId: Long, price: Long): Product {
val seller = identityApi.getUserByToken(token)
val sellProduct = renderApi.getPersonaById(token, personaId)

val request = RegisterProductRequest(
sellerId = seller.id.toLong(),
personaId = sellProduct.id.toLong(),
personaType = sellProduct.type,
personaLevel = sellProduct.level.toInt(),
price = price,
)

return orchestrator.sagaSync(
request,
mapOf("token" to token, "idempotencyKey" to UUID.randomUUID().toString()),
).decodeResultOrThrow(Product::class)
}

init {
this.orchestrator =
orchestratorFactory.create<RegisterProductRequest>("register product orchestrator")
.startWithContext(
contextOrchestrate = { context, request ->
val token = context.decodeContext("token", String::class)
renderApi.deletePersonaById(token, request.personaId)
request
},
contextRollback = { context, request ->
val token = context.decodeContext("token", String::class)
val idempotencyKey = context.decodeContext("idempotencyKey", String::class)
renderApi.addPersona(
token,
idempotencyKey,
request.personaId,
request.personaLevel,
request.personaType
)
}
)
.commit { productService.registerProduct(it) }
}
}
22 changes: 22 additions & 0 deletions src/main/kotlin/org/gitanimals/auction/app/RenderApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.gitanimals.auction.app

interface RenderApi {

fun getPersonaById(token: String, personaId: Long): PersonaResponse

fun deletePersonaById(token: String, personaId: Long)

fun addPersona(
token: String,
idempotencyKey: String,
personaId: Long,
personaLevel: Int,
personaType: String,
)

data class PersonaResponse(
val id: String,
val type: String,
val level: String,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.gitanimals.auction.controller

import org.gitanimals.auction.app.BuyProductFacade
import org.gitanimals.auction.app.RegisterProductFacade
import org.gitanimals.auction.controller.request.RegisterProductRequest
import org.gitanimals.auction.controller.response.ErrorResponse
import org.gitanimals.auction.controller.response.ProductResponse
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.*

@RestController
class AuctionController(
private val buyProductFacade: BuyProductFacade,
private val registerProductFacade: RegisterProductFacade,
) {

@ResponseStatus(HttpStatus.OK)
@PostMapping("/auctions/products")
fun registerProducts(
@RequestHeader(HttpHeaders.AUTHORIZATION) token: String,
@RequestBody registerProductRequest: RegisterProductRequest,
): ProductResponse {
val product = registerProductFacade.registerProduct(
token,
registerProductRequest.personaId.toLong(),
registerProductRequest.price.toLong(),
)

return ProductResponse.from(product)
}

@ResponseStatus(HttpStatus.OK)
@PostMapping("/auctions/products/{product-id}")
fun buyProducts(
@RequestHeader(HttpHeaders.AUTHORIZATION) token: String,
@PathVariable("product-id") productId: Long,
): ProductResponse {
val product = buyProductFacade.buyProduct(token, productId)

return ProductResponse.from(product)
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException::class)
fun handleIllegalArgumentException(exception: IllegalArgumentException): ErrorResponse =
ErrorResponse.from(exception)

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(IllegalStateException::class)
fun handleIllegalStateException(exception: IllegalStateException): ErrorResponse =
ErrorResponse.from(exception)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.gitanimals.auction.controller.request

data class RegisterProductRequest(
val personaId: String,
val price: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.gitanimals.auction.controller.response

data class ErrorResponse(
val message: String,
) {

companion object {
fun from(exception: Exception): ErrorResponse =
ErrorResponse(exception.message ?: exception.localizedMessage)
}
}
Loading

0 comments on commit 9a0726e

Please sign in to comment.