diff --git a/src/main/kotlin/com/example/auction/model/Auction.kt b/src/main/kotlin/com/example/auction/model/Auction.kt index 0d5e553..258b02f 100644 --- a/src/main/kotlin/com/example/auction/model/Auction.kt +++ b/src/main/kotlin/com/example/auction/model/Auction.kt @@ -2,61 +2,52 @@ package com.example.auction.model import com.example.auction.model.AuctionState.closed import com.example.auction.model.AuctionState.open +import com.example.auction.model.AuctionState.settled import com.example.auction.model.MonetaryAmount.Companion.ZERO import com.example.pii.UserId import com.example.settlement.Charge import com.example.settlement.Collection import com.example.settlement.OrderId import com.example.settlement.SettlementInstruction +import dev.forkhandles.result4k.Result4k +import dev.forkhandles.result4k.asFailure +import dev.forkhandles.result4k.asSuccess import java.math.RoundingMode.DOWN import java.math.RoundingMode.UP import java.util.Currency -abstract class Auction( - var seller: UserId, - var description: String, - var currency: Currency, - var reserve: MonetaryAmount, - var commission: MonetaryAmount, - var chargePerBid: MonetaryAmount, - var id: AuctionId, - var state: AuctionState, - var bids: MutableList, - var winner: AuctionWinner? +data class Auction( + val seller: UserId, + val description: String, + val currency: Currency, + val reserve: MonetaryAmount, + val commission: MonetaryAmount, + val chargePerBid: MonetaryAmount, + val id: AuctionId, + val state: AuctionState, + val bids: List, + val winner: AuctionWinner?, + val rules: AuctionRules ) { - abstract val rules: AuctionRules - - fun placeBid(buyer: UserId, bid: Money) { - if (buyer == seller) { - throw BadRequestException("shill bidding detected by $seller") - } - if (bid.currency != currency) { - throw BadRequestException("bid in wrong currency, should be $currency") - } - if (bid.amount == ZERO) { - throw BadRequestException("zero bid") + fun placeBid(buyer: UserId, bid: Money): Result4k { + return when { + buyer == seller -> BadRequestException("shill bidding detected by $seller").asFailure() + bid.currency != currency -> BadRequestException("bid in wrong currency, should be $currency").asFailure() + bid.amount == ZERO -> BadRequestException("zero bid").asFailure() + state != open -> WrongStateException("auction $id is closed").asFailure() + else -> copy(bids = bids + Bid(buyer, bid.amount)).asSuccess() } - if (state != open) { - throw WrongStateException("auction $id is closed") - } - - bids.add(Bid(buyer, bid.amount)) } - fun close() { - state = closed - winner = decideWinner() - } + fun closed(): Auction = + copy(state = closed, winner = rules.decideWinner(this)) - protected abstract fun decideWinner(): AuctionWinner? - - fun settled() { + fun settled() = if (state == open) { - throw WrongStateException("auction $id not closed") + WrongStateException("auction $id not closed").asFailure() + } else { + copy(state = settled).asSuccess() } - - state = AuctionState.settled - } fun settlement(): SettlementInstruction? { val winner = this.winner ?: return null @@ -99,8 +90,4 @@ abstract class Auction( charges = commissionCharges + bidCharges ) } - - override fun toString(): String { - return "${this::class.simpleName}(seller=$seller, description='$description', currency=$currency, reserve=$reserve, commission=$commission, chargePerBid=$chargePerBid, id=$id, state=$state, rules=$rules, winner=$winner)" - } } diff --git a/src/main/kotlin/com/example/auction/model/AuctionError.kt b/src/main/kotlin/com/example/auction/model/AuctionError.kt new file mode 100644 index 0000000..dd61fd6 --- /dev/null +++ b/src/main/kotlin/com/example/auction/model/AuctionError.kt @@ -0,0 +1,4 @@ +package com.example.auction.model + +sealed class AuctionError(message: String, cause: Throwable? = null) + : RuntimeException(message, cause) diff --git a/src/main/kotlin/com/example/auction/model/AuctionRules.kt b/src/main/kotlin/com/example/auction/model/AuctionRules.kt index 02a0573..0a61b5b 100644 --- a/src/main/kotlin/com/example/auction/model/AuctionRules.kt +++ b/src/main/kotlin/com/example/auction/model/AuctionRules.kt @@ -1,5 +1,7 @@ package com.example.auction.model -enum class AuctionRules { - Blind, Vickrey, Reverse +enum class AuctionRules(val decideWinner: (Auction) -> AuctionWinner?) { + Blind(::blindAuctionWinner), + Vickrey(::vickreyAuctionWinner), + Reverse(::reverseAuctionWinner) } diff --git a/src/main/kotlin/com/example/auction/model/BadRequestException.kt b/src/main/kotlin/com/example/auction/model/BadRequestException.kt index c2f400a..1e5ffc4 100644 --- a/src/main/kotlin/com/example/auction/model/BadRequestException.kt +++ b/src/main/kotlin/com/example/auction/model/BadRequestException.kt @@ -1,3 +1,4 @@ package com.example.auction.model -class BadRequestException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) \ No newline at end of file +class BadRequestException(message: String, cause: Throwable? = null) : + AuctionError(message, cause) diff --git a/src/main/kotlin/com/example/auction/model/Bid.kt b/src/main/kotlin/com/example/auction/model/Bid.kt index 5c101a2..f1dad36 100644 --- a/src/main/kotlin/com/example/auction/model/Bid.kt +++ b/src/main/kotlin/com/example/auction/model/Bid.kt @@ -3,9 +3,9 @@ package com.example.auction.model import com.example.pii.UserId class Bid( - var buyer: UserId, - var amount: MonetaryAmount, - var id: BidId = BidId.NONE + val buyer: UserId, + val amount: MonetaryAmount, + val id: BidId = BidId.NONE ) { override fun toString(): String { return "Bid(buyer=$buyer, amount=$amount, id=$id)" diff --git a/src/main/kotlin/com/example/auction/model/BlindAuction.kt b/src/main/kotlin/com/example/auction/model/BlindAuction.kt index 9b8aa42..c03b3af 100644 --- a/src/main/kotlin/com/example/auction/model/BlindAuction.kt +++ b/src/main/kotlin/com/example/auction/model/BlindAuction.kt @@ -5,7 +5,7 @@ import com.example.auction.model.AuctionState.open import com.example.pii.UserId import java.util.Currency -class BlindAuction( +fun BlindAuction( seller: UserId, description: String, currency: Currency, @@ -15,8 +15,8 @@ class BlindAuction( id: AuctionId, bids: MutableList = mutableListOf(), state: AuctionState = open, - winner: AuctionWinner? = null -) : Auction( + winner: AuctionWinner? = null, +) = Auction( seller = seller, description = description, currency = currency, @@ -26,15 +26,14 @@ class BlindAuction( id = id, state = state, bids = bids, - winner = winner -) { - override val rules = Blind - - override fun decideWinner(): AuctionWinner? { - return bids - .associateBy { it.buyer } - .values - .maxByOrNull { it.amount } - ?.takeIf { it.amount >= reserve }?.toWinner() - } -} + winner = winner, + rules = Blind +) + +fun blindAuctionWinner(auction: Auction) = + auction.bids + .associateBy { it.buyer } + .values + .maxByOrNull { it.amount } + ?.takeIf { it.amount >= auction.reserve } + ?.toWinner() diff --git a/src/main/kotlin/com/example/auction/model/NotFoundException.kt b/src/main/kotlin/com/example/auction/model/NotFoundException.kt new file mode 100644 index 0000000..bcdfec3 --- /dev/null +++ b/src/main/kotlin/com/example/auction/model/NotFoundException.kt @@ -0,0 +1,4 @@ +package com.example.auction.model + +class NotFoundException(message: String, cause: Throwable? = null) : + AuctionError(message, cause) diff --git a/src/main/kotlin/com/example/auction/model/ReverseAuction.kt b/src/main/kotlin/com/example/auction/model/ReverseAuction.kt index 3578962..4bab31f 100644 --- a/src/main/kotlin/com/example/auction/model/ReverseAuction.kt +++ b/src/main/kotlin/com/example/auction/model/ReverseAuction.kt @@ -5,7 +5,7 @@ import com.example.auction.model.AuctionState.open import com.example.pii.UserId import java.util.Currency -class ReverseAuction( +fun ReverseAuction( seller: UserId, description: String, currency: Currency, @@ -16,7 +16,7 @@ class ReverseAuction( bids: MutableList = mutableListOf(), state: AuctionState = open, winner: AuctionWinner? = null -) : Auction( +) = Auction( seller = seller, description = description, currency = currency, @@ -26,30 +26,16 @@ class ReverseAuction( id = id, state = state, bids = bids, - winner = winner -) { - override val rules = Reverse - - override fun decideWinner(): AuctionWinner? { - val bidsByAmount = mutableMapOf>() - for (bid in bids) if (bid.amount >= reserve) { - var bidGroup = bidsByAmount[bid.amount] - if (bidGroup == null) { - bidGroup = mutableListOf() - bidsByAmount[bid.amount] = bidGroup - } - bidGroup.add(bid) - } - - var lowestUniqueBid: Bid? = null - - for (bids in bidsByAmount.values) if (bids.size == 1) { - val bid = bids.single() - if (lowestUniqueBid == null || bid.amount < lowestUniqueBid.amount) { - lowestUniqueBid = bid - } - } - - return lowestUniqueBid?.toWinner() - } + winner = winner, + rules = Reverse +) + +fun reverseAuctionWinner(auction: Auction): AuctionWinner? { + return auction.bids.filter { it.amount >= auction.reserve } + .uniqueBy { it.amount } + .minByOrNull { it.amount } + ?.toWinner() } + +private fun List.uniqueBy(f: (T) -> U) = + groupBy(f).values.mapNotNull { it.singleOrNull() } diff --git a/src/main/kotlin/com/example/auction/model/VickreyAuction.kt b/src/main/kotlin/com/example/auction/model/VickreyAuction.kt index dc4e62b..31b2a28 100644 --- a/src/main/kotlin/com/example/auction/model/VickreyAuction.kt +++ b/src/main/kotlin/com/example/auction/model/VickreyAuction.kt @@ -5,7 +5,7 @@ import com.example.auction.model.AuctionState.open import com.example.pii.UserId import java.util.Currency -class VickreyAuction( +fun VickreyAuction( seller: UserId, description: String, currency: Currency, @@ -16,7 +16,7 @@ class VickreyAuction( bids: MutableList = mutableListOf(), state: AuctionState = open, winner: AuctionWinner? = null -) : Auction( +) = Auction( seller = seller, description = description, currency = currency, @@ -26,22 +26,21 @@ class VickreyAuction( id = id, state = state, bids = bids, - winner = winner -) { - override val rules = Vickrey - - override fun decideWinner(): AuctionWinner? { - return bids - .associateBy { it.buyer } - .values - .sortedByDescending { it.amount } - .take(2) - .run { - when { - isEmpty() -> null - last().amount < reserve -> null - else -> AuctionWinner(first().buyer, last().amount) - } + winner = winner, + rules = Vickrey +) + +fun vickreyAuctionWinner(auction: Auction): AuctionWinner? { + return auction.bids + .associateBy { it.buyer } + .values + .sortedByDescending { it.amount } + .take(2) + .run { + when { + isEmpty() -> null + last().amount < auction.reserve -> null + else -> AuctionWinner(first().buyer, last().amount) } - } -} \ No newline at end of file + } +} diff --git a/src/main/kotlin/com/example/auction/model/WrongStateException.kt b/src/main/kotlin/com/example/auction/model/WrongStateException.kt index 8ebd99e..9584409 100644 --- a/src/main/kotlin/com/example/auction/model/WrongStateException.kt +++ b/src/main/kotlin/com/example/auction/model/WrongStateException.kt @@ -2,4 +2,4 @@ package com.example.auction.model class WrongStateException(message: String, cause: Throwable? = null) : - RuntimeException(message, cause) \ No newline at end of file + AuctionError(message, cause) diff --git a/src/main/kotlin/com/example/auction/repository/SpringJdbcAuctionRepository.kt b/src/main/kotlin/com/example/auction/repository/SpringJdbcAuctionRepository.kt index 01832fc..7b4a0b5 100644 --- a/src/main/kotlin/com/example/auction/repository/SpringJdbcAuctionRepository.kt +++ b/src/main/kotlin/com/example/auction/repository/SpringJdbcAuctionRepository.kt @@ -3,17 +3,11 @@ package com.example.auction.repository import com.example.auction.model.Auction import com.example.auction.model.AuctionId import com.example.auction.model.AuctionRules -import com.example.auction.model.AuctionRules.Blind -import com.example.auction.model.AuctionRules.Reverse -import com.example.auction.model.AuctionRules.Vickrey import com.example.auction.model.AuctionState.valueOf import com.example.auction.model.AuctionWinner import com.example.auction.model.Bid import com.example.auction.model.BidId -import com.example.auction.model.BlindAuction import com.example.auction.model.MonetaryAmount -import com.example.auction.model.ReverseAuction -import com.example.auction.model.VickreyAuction import com.example.pii.UserId import org.springframework.jdbc.core.simple.JdbcClient import org.springframework.jdbc.support.GeneratedKeyHolder @@ -219,22 +213,17 @@ private fun ResultSet.toAuction(bids: MutableList): Auction { ) } else null - val constructor = when (rules) { - Blind -> ::BlindAuction - Vickrey -> ::VickreyAuction - Reverse -> ::ReverseAuction - } - - return constructor( - UserId(getString("SELLER")), - getString("DESCRIPTION"), - currency, - MonetaryAmount(getBigDecimal("RESERVE").setScale(currency.defaultFractionDigits)), - MonetaryAmount(getBigDecimal("COMMISSION")), - MonetaryAmount(getBigDecimal("CHARGE_PER_BID")), - AuctionId(getString("ID")), - bids, - valueOf(getString("STATE")), - winner + return Auction( + rules = rules, + seller = UserId(getString("SELLER")), + description = getString("DESCRIPTION"), + currency = currency, + reserve = MonetaryAmount(getBigDecimal("RESERVE").setScale(currency.defaultFractionDigits)), + commission = MonetaryAmount(getBigDecimal("COMMISSION")), + chargePerBid = MonetaryAmount(getBigDecimal("CHARGE_PER_BID")), + id = AuctionId(getString("ID")), + bids = bids, + state = valueOf(getString("STATE")), + winner = winner ) } diff --git a/src/main/kotlin/com/example/auction/service/AuctionService.kt b/src/main/kotlin/com/example/auction/service/AuctionService.kt index 4533131..562aea7 100644 --- a/src/main/kotlin/com/example/auction/service/AuctionService.kt +++ b/src/main/kotlin/com/example/auction/service/AuctionService.kt @@ -1,10 +1,13 @@ package com.example.auction.service +import com.example.auction.model.Auction +import com.example.auction.model.AuctionError import com.example.auction.model.AuctionId +import dev.forkhandles.result4k.Result4k interface AuctionService { fun listAuctions(count: Int, after: AuctionId): List fun createAuction(rq: CreateAuctionRequest): AuctionId - fun placeBid(auctionId: AuctionId, bid: BidRequest) + fun placeBid(auctionId: AuctionId, bid: BidRequest): Result4k fun closeAuction(auctionId: AuctionId): AuctionResult } \ No newline at end of file diff --git a/src/main/kotlin/com/example/auction/service/AuctionServiceImpl.kt b/src/main/kotlin/com/example/auction/service/AuctionServiceImpl.kt index 4f80f2a..deee6f1 100644 --- a/src/main/kotlin/com/example/auction/service/AuctionServiceImpl.kt +++ b/src/main/kotlin/com/example/auction/service/AuctionServiceImpl.kt @@ -7,10 +7,17 @@ import com.example.auction.model.BlindAuction import com.example.auction.model.MonetaryAmount import com.example.auction.model.MonetaryAmount.Companion.ZERO import com.example.auction.model.Money +import com.example.auction.model.NotFoundException import com.example.auction.model.ReverseAuction import com.example.auction.model.VickreyAuction import com.example.auction.repository.AuctionRepository import com.example.pii.UserIdValidator +import dev.forkhandles.result4k.Result4k +import dev.forkhandles.result4k.asFailure +import dev.forkhandles.result4k.asSuccess +import dev.forkhandles.result4k.flatMap +import dev.forkhandles.result4k.orThrow +import dev.forkhandles.result4k.peek import org.springframework.dao.ConcurrencyFailureException import org.springframework.retry.annotation.Backoff import org.springframework.retry.annotation.Retryable @@ -117,23 +124,20 @@ class AuctionServiceImpl( } @ApiTransaction - override fun placeBid(auctionId: AuctionId, bid: BidRequest) { + override fun placeBid(auctionId: AuctionId, bid: BidRequest) = if (!piiVault.isValid(bid.buyer)) { - throw BadRequestException("invalid user id ${bid.buyer}") + BadRequestException("invalid user id ${bid.buyer}").asFailure() + } else { + loadAuctionToo(auctionId) + .flatMap { it.placeBid(bid.buyer, bid.amount) } + .peek { repository.updateAuction(it) } } - - val auction = loadAuction(auctionId) - auction.placeBid(bid.buyer, bid.amount) - - repository.updateAuction(auction) - } @ApiTransaction override fun closeAuction(auctionId: AuctionId): AuctionResult { val auction = loadAuction(auctionId) - - auction.close() - repository.updateAuction(auction) + .closed() + .also { repository.updateAuction(it) } return when (val result = auction.winner) { null -> Passed @@ -145,8 +149,12 @@ class AuctionServiceImpl( } private fun loadAuction(auctionId: AuctionId): Auction { - return repository.getAuction(auctionId) - ?: throw NotFoundException("no auction found with id $auctionId") + return loadAuctionToo(auctionId).orThrow() + } + + private fun loadAuctionToo(auctionId: AuctionId): Result4k { + return repository.getAuction(auctionId)?.asSuccess() + ?: NotFoundException("no auction found with id $auctionId").asFailure() } } diff --git a/src/main/kotlin/com/example/auction/service/AuctionSettlementService.kt b/src/main/kotlin/com/example/auction/service/AuctionSettlementService.kt index 0dd887d..bf1ba6a 100644 --- a/src/main/kotlin/com/example/auction/service/AuctionSettlementService.kt +++ b/src/main/kotlin/com/example/auction/service/AuctionSettlementService.kt @@ -3,6 +3,7 @@ package com.example.auction.service import com.example.auction.model.AuctionId import com.example.auction.repository.AuctionRepository import com.example.settlement.Settlement +import dev.forkhandles.result4k.onFailure import org.springframework.beans.factory.annotation.Value import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component @@ -21,21 +22,24 @@ class AuctionSettlementService( lastAuctionId = settleAuctions(batchSize, after = lastAuctionId) } } - + fun settleAuctions(batchSize: Int, after: AuctionId = AuctionId.NONE): AuctionId? { val batch = repository.listForSettlement(batchSize, after = after) - + for (id in batch) { - val auction = repository.getAuction(id) ?: continue - val instruction = auction.settlement() ?: continue - - settlement.settle(instruction) - auction.settled() - - repository.updateAuction(auction) + settleAuction(id) } - + return batch.lastOrNull() } + + private fun settleAuction(id: AuctionId) { + val auction = repository.getAuction(id) ?: return + val finalState = auction.settled().onFailure { return } + val instruction = auction.settlement() ?: return + + settlement.settle(instruction) + repository.updateAuction(finalState) + } } diff --git a/src/main/kotlin/com/example/auction/service/NotFoundException.kt b/src/main/kotlin/com/example/auction/service/NotFoundException.kt deleted file mode 100644 index 20f56ae..0000000 --- a/src/main/kotlin/com/example/auction/service/NotFoundException.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.example.auction.service - -class NotFoundException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) \ No newline at end of file diff --git a/src/main/kotlin/com/example/auction/web/AuctionController.kt b/src/main/kotlin/com/example/auction/web/AuctionController.kt index b3b9dfd..8ceee14 100644 --- a/src/main/kotlin/com/example/auction/web/AuctionController.kt +++ b/src/main/kotlin/com/example/auction/web/AuctionController.kt @@ -1,14 +1,21 @@ package com.example.auction.web import com.example.auction.model.AuctionId +import com.example.auction.model.BadRequestException +import com.example.auction.model.NotFoundException +import com.example.auction.model.WrongStateException import com.example.auction.service.AuctionResult import com.example.auction.service.AuctionService import com.example.auction.service.AuctionSummary import com.example.auction.service.BidRequest import com.example.auction.service.CreateAuctionRequest import com.example.auction.service.CreateAuctionResponse +import dev.forkhandles.result4k.map +import dev.forkhandles.result4k.recover import org.slf4j.LoggerFactory import org.springframework.http.HttpStatus +import org.springframework.http.ProblemDetail +import org.springframework.http.ResponseEntity import org.springframework.stereotype.Controller import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @@ -52,11 +59,21 @@ class AuctionController( @PostMapping( "{auctionId}/bids", consumes = ["application/json"], - produces = [] + produces = ["application/json"] ) - @ResponseStatus(HttpStatus.NO_CONTENT) - fun recordBid(@PathVariable auctionId: AuctionId, @RequestBody bid: BidRequest) { - auctionService.placeBid(auctionId, bid) + fun recordBid(@PathVariable auctionId: AuctionId, @RequestBody bid: BidRequest): ResponseEntity<*> { + return auctionService.placeBid(auctionId, bid) + .map { ResponseEntity.noContent().build() } + .recover { e -> + val status = when (e) { + is BadRequestException -> HttpStatus.BAD_REQUEST + is NotFoundException -> HttpStatus.NOT_FOUND + is WrongStateException -> HttpStatus.CONFLICT + } + val problem = ProblemDetail.forStatusAndDetail(status, e.message) + + return ResponseEntity(problem, status) + } } @PostMapping( diff --git a/src/main/kotlin/com/example/auction/web/AuctionExceptionHandlers.kt b/src/main/kotlin/com/example/auction/web/AuctionExceptionHandlers.kt index e61dfa7..1b0cc03 100644 --- a/src/main/kotlin/com/example/auction/web/AuctionExceptionHandlers.kt +++ b/src/main/kotlin/com/example/auction/web/AuctionExceptionHandlers.kt @@ -1,8 +1,8 @@ package com.example.auction.web import com.example.auction.model.BadRequestException +import com.example.auction.model.NotFoundException import com.example.auction.model.WrongStateException -import com.example.auction.service.NotFoundException import org.springframework.http.HttpStatus.BAD_REQUEST import org.springframework.http.HttpStatus.CONFLICT import org.springframework.http.HttpStatus.NOT_FOUND diff --git a/src/test/kotlin/com/example/auction/AuctionErrorCasesTest.kt b/src/test/kotlin/com/example/auction/AuctionErrorCasesTest.kt index ed96b24..39a24d3 100644 --- a/src/test/kotlin/com/example/auction/AuctionErrorCasesTest.kt +++ b/src/test/kotlin/com/example/auction/AuctionErrorCasesTest.kt @@ -4,8 +4,10 @@ import com.example.auction.model.BlindAuction import com.example.auction.model.MonetaryAmount import com.example.auction.model.WrongStateException import com.example.pii.UserId +import dev.forkhandles.result4k.failureOrNull import kotlin.test.Test -import kotlin.test.assertFailsWith +import kotlin.test.assertIs + class AuctionErrorCasesTest { @Test @@ -18,8 +20,7 @@ class AuctionErrorCasesTest { reserve = MonetaryAmount("100.00") ) - assertFailsWith { - a.settled() - } + val failure = a.settled().failureOrNull() + assertIs(failure) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/com/example/auction/acceptance/http/HttpAuctionTesting.kt b/src/test/kotlin/com/example/auction/acceptance/http/HttpAuctionTesting.kt index 46f4373..be6b5e2 100644 --- a/src/test/kotlin/com/example/auction/acceptance/http/HttpAuctionTesting.kt +++ b/src/test/kotlin/com/example/auction/acceptance/http/HttpAuctionTesting.kt @@ -4,13 +4,13 @@ import com.example.auction.acceptance.AuctionTesting import com.example.auction.model.AuctionId import com.example.auction.model.BadRequestException import com.example.auction.model.Money +import com.example.auction.model.NotFoundException import com.example.auction.model.WrongStateException import com.example.auction.service.AuctionResult import com.example.auction.service.AuctionSummary import com.example.auction.service.BidRequest import com.example.auction.service.CreateAuctionRequest import com.example.auction.service.CreateAuctionResponse -import com.example.auction.service.NotFoundException import com.example.pii.UserId import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue diff --git a/src/test/kotlin/com/example/auction/acceptance/mem/DomainModelOnlyTesting.kt b/src/test/kotlin/com/example/auction/acceptance/mem/DomainModelOnlyTesting.kt index 961a7b7..423e4da 100644 --- a/src/test/kotlin/com/example/auction/acceptance/mem/DomainModelOnlyTesting.kt +++ b/src/test/kotlin/com/example/auction/acceptance/mem/DomainModelOnlyTesting.kt @@ -15,6 +15,7 @@ import com.example.settlement.SettlementInstruction import com.example.simulators.pii_vault.PiiVaultSimulatorService import com.example.simulators.settlement.SettlementSimulatorService import com.example.simulators.settlement.get +import dev.forkhandles.result4k.orThrow abstract class DomainModelOnlyTesting : AuctionTesting { @@ -45,6 +46,7 @@ abstract class DomainModelOnlyTesting : AuctionTesting { override fun UserId.bid(auction: AuctionId, amount: Money) { service.placeBid(auction, BidRequest(this, amount)) + .orThrow() } override fun UserId.closeAuction(auction: AuctionId) = diff --git a/src/test/kotlin/com/example/auction/repository/AuctionRepositoryContract.kt b/src/test/kotlin/com/example/auction/repository/AuctionRepositoryContract.kt index 9ef0fe6..1b82f8d 100644 --- a/src/test/kotlin/com/example/auction/repository/AuctionRepositoryContract.kt +++ b/src/test/kotlin/com/example/auction/repository/AuctionRepositoryContract.kt @@ -13,6 +13,8 @@ import com.example.auction.newReverseAuction import com.example.auction.newVickreyAuction import com.example.auction.predecessor import com.example.auction.streamPages +import com.example.pii.UserId +import dev.forkhandles.result4k.orThrow import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -46,11 +48,12 @@ interface AuctionRepositoryContract { @Test fun `adding bids`() { val auction = newBlindAuction() - repository.addAuction(auction) + .also { repository.addAuction(it) } - auction.placeBid(alice, 1.EUR) - auction.placeBid(bob, 2.EUR) - repository.updateAuction(auction) + load(auction.id) + .placeBid(alice, 1.EUR).orThrow() + .placeBid(bob, 2.EUR).orThrow() + .also { repository.updateAuction(it) } val loaded = repository.getAuction(auction.id) ?: fail("could not reload auction ${auction.id}") @@ -65,24 +68,22 @@ interface AuctionRepositoryContract { @Test fun `saving and loading the winner`() { val auction = newBlindAuction() - repository.addAuction(auction) + .also { repository.addAuction(it) } + val auctionId = auction.id - auction.placeBid(alice, 1.EUR) - auction.placeBid(bob, 2.EUR) - repository.updateAuction(auction) + load(auction.id) + .placeBid(alice, 1.EUR).orThrow() + .placeBid(bob, 2.EUR).orThrow() + .also { repository.updateAuction(it) } - run { - val loaded = load(auctionId) - loaded.winner = AuctionWinner(winner = bob, owed = MonetaryAmount("2.00")) - repository.updateAuction(loaded) - } + load(auctionId) + .copy(winner = AuctionWinner(winner = bob, owed = MonetaryAmount("2.00"))) + .also { repository.updateAuction(it) } - run { - val loaded = load(auctionId) - assertNotNull(loaded.winner) - assertEquals(AuctionWinner(bob, MonetaryAmount("2.00")), loaded.winner) - } + val loaded = load(auctionId) + assertNotNull(loaded.winner) + assertEquals(AuctionWinner(bob, MonetaryAmount("2.00")), loaded.winner) } @Test @@ -115,7 +116,7 @@ interface AuctionRepositoryContract { auctionIds.forEach { id -> val auction = load(id) - auction.close() + auction.closed() repository.updateAuction(auction) } diff --git a/src/test/kotlin/com/example/auction/repository/InMemoryAuctionRepository.kt b/src/test/kotlin/com/example/auction/repository/InMemoryAuctionRepository.kt index c71e37a..be66c18 100644 --- a/src/test/kotlin/com/example/auction/repository/InMemoryAuctionRepository.kt +++ b/src/test/kotlin/com/example/auction/repository/InMemoryAuctionRepository.kt @@ -18,8 +18,7 @@ class InMemoryAuctionRepository : AuctionRepository { auctions[id] override fun updateAuction(auction: Auction) { - // TODO: ask Nat if I can delete this line. It doesn't seem needed because we mutate the auction - // auctions[auction.id] = auction + auctions[auction.id] = auction } override fun listOpenAuctions(count: Int, after: AuctionId) = diff --git a/src/test/kotlin/com/example/auction/web/ControllerFuzzTests.kt b/src/test/kotlin/com/example/auction/web/ControllerFuzzTests.kt index 3d76341..35b4e32 100644 --- a/src/test/kotlin/com/example/auction/web/ControllerFuzzTests.kt +++ b/src/test/kotlin/com/example/auction/web/ControllerFuzzTests.kt @@ -3,7 +3,10 @@ package com.example.auction.web import com.example.auction.AuctionApplication import com.example.auction.AuctionId import com.example.auction.acceptance.EUR +import com.example.auction.model.Auction +import com.example.auction.model.AuctionError import com.example.auction.model.AuctionId +import com.example.auction.newBlindAuction import com.example.auction.service.AuctionResult import com.example.auction.service.AuctionService import com.example.auction.service.AuctionSummary @@ -15,6 +18,8 @@ import com.example.pii.UserId import com.natpryce.snodge.json.defaultJsonMutagens import com.natpryce.snodge.json.forStrings import com.natpryce.snodge.mutants +import dev.forkhandles.result4k.Result4k +import dev.forkhandles.result4k.asSuccess import org.junit.jupiter.api.extension.ExtensionContext import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments @@ -97,7 +102,8 @@ class DummyAuctionService : AuctionService { return AuctionId(1) } - override fun placeBid(auctionId: AuctionId, bid: BidRequest) { + override fun placeBid(auctionId: AuctionId, bid: BidRequest): Result4k { + return newBlindAuction(1).asSuccess() } override fun closeAuction(auctionId: AuctionId): AuctionResult {