From 07a7a1cb3e4cfc1580bc05b0a6010a1e9f7e7508 Mon Sep 17 00:00:00 2001 From: Kostya Date: Sun, 16 Aug 2026 11:55:08 +0000 Subject: Rename Option/Account to Market/Ledger, add Result-based error handling Split GuessMarketContext into a separate CatalogContext for read-only event listing, wrap fallible operations in Result instead of throwing, and wire load/save of events and users through a JSON Wrapper. --- .../main/java/market/guess/api/CatalogContext.java | 12 ++++ .../main/java/market/guess/api/EventStatus.java | 4 +- .../java/market/guess/api/EventSummaryDTO.java | 2 +- .../java/market/guess/api/GuessMarketContext.java | 23 ++----- api/src/main/java/market/guess/api/MarketDTO.java | 3 + .../main/java/market/guess/api/MarketStateDTO.java | 2 +- .../main/java/market/guess/api/OptionStateDTO.java | 3 - api/src/main/java/market/guess/api/Result.java | 49 +++++++++++++++ .../main/java/market/guess/api/TradeRowDTO.java | 2 +- .../market/guess/service/LocalCatalogContext.java | 45 +++++++++++++ .../guess/service/LocalGuessMarketContext.java | 61 +++++++----------- .../market/guess/service/TradingOperation.java | 4 +- .../market/guess/service/TradingOperationImpl.java | 4 +- .../java/market/guess/service/domain/Account.java | 49 --------------- .../java/market/guess/service/domain/Event.java | 73 +++++++++++++++------- .../market/guess/service/domain/LedgerEntry.java | 13 ---- .../java/market/guess/service/domain/Market.java | 32 ++++++++++ .../java/market/guess/service/domain/Option.java | 3 - .../java/market/guess/service/domain/User.java | 7 ++- .../service/infrastructure/MarketContext.java | 34 +++++++++- .../infrastructure/mapper/v1/EventMapperV1.java | 8 +-- .../infrastructure/mapper/v2/EventMapperV2.java | 10 +-- .../repository/InMemoryEventRepository.java | 5 ++ .../repository/InMemoryUserRepository.java | 5 ++ .../infrastructure/repository/Repository.java | 2 + .../java/market/guess/service/ledger/Ledger.java | 35 +++++++++++ .../market/guess/service/ledger/LedgerContext.java | 49 +++++++++++++++ .../market/guess/service/ledger/LedgerEntry.java | 13 ++++ .../main/java/market/guess/ui/console/Menu.java | 2 +- .../guess/ui/console/command/BuyMenuCommand.java | 54 ++++++++-------- .../console/command/EventDetailsMenuCommand.java | 6 -- .../guess/ui/console/command/ExitMenuCommand.java | 5 -- .../ui/console/command/ListEventsMenuCommand.java | 20 +++--- .../ui/console/command/LoadFileMenuCommand.java | 33 +++++----- .../command/LoadMarketStateMenuCommand.java | 26 +++++--- .../guess/ui/console/command/MenuCommand.java | 2 - .../command/SaveMarketStateMenuCommand.java | 6 -- .../ui/console/command/SettleEventMenuCommand.java | 6 -- .../market/guess/ui/console/io/InputProcessor.java | 43 +++++++++++-- 39 files changed, 491 insertions(+), 264 deletions(-) create mode 100644 api/src/main/java/market/guess/api/CatalogContext.java create mode 100644 api/src/main/java/market/guess/api/MarketDTO.java delete mode 100644 api/src/main/java/market/guess/api/OptionStateDTO.java create mode 100644 api/src/main/java/market/guess/api/Result.java create mode 100644 service/src/main/java/market/guess/service/LocalCatalogContext.java delete mode 100644 service/src/main/java/market/guess/service/domain/Account.java delete mode 100644 service/src/main/java/market/guess/service/domain/LedgerEntry.java create mode 100644 service/src/main/java/market/guess/service/domain/Market.java delete mode 100644 service/src/main/java/market/guess/service/domain/Option.java create mode 100644 service/src/main/java/market/guess/service/ledger/Ledger.java create mode 100644 service/src/main/java/market/guess/service/ledger/LedgerContext.java create mode 100644 service/src/main/java/market/guess/service/ledger/LedgerEntry.java diff --git a/api/src/main/java/market/guess/api/CatalogContext.java b/api/src/main/java/market/guess/api/CatalogContext.java new file mode 100644 index 0000000..7e80042 --- /dev/null +++ b/api/src/main/java/market/guess/api/CatalogContext.java @@ -0,0 +1,12 @@ +package market.guess.api; + +import java.nio.file.Path; +import java.util.List; + +public interface CatalogContext { + Result loadEvents(Path path); + + Result> getAllEvents(); + + Result getEvent(String eventKey); +} diff --git a/api/src/main/java/market/guess/api/EventStatus.java b/api/src/main/java/market/guess/api/EventStatus.java index acd7542..6580157 100644 --- a/api/src/main/java/market/guess/api/EventStatus.java +++ b/api/src/main/java/market/guess/api/EventStatus.java @@ -1,7 +1,7 @@ package market.guess.api; public enum EventStatus { - NOT_STARTED, + DRAFT, ACTIVE, - CLOSED + SETTLED } diff --git a/api/src/main/java/market/guess/api/EventSummaryDTO.java b/api/src/main/java/market/guess/api/EventSummaryDTO.java index 361656b..d185103 100644 --- a/api/src/main/java/market/guess/api/EventSummaryDTO.java +++ b/api/src/main/java/market/guess/api/EventSummaryDTO.java @@ -3,7 +3,7 @@ package market.guess.api; import java.util.List; public record EventSummaryDTO( - String eventKey, + String key, int displayId, String name, String description, diff --git a/api/src/main/java/market/guess/api/GuessMarketContext.java b/api/src/main/java/market/guess/api/GuessMarketContext.java index d959de6..4eee37e 100644 --- a/api/src/main/java/market/guess/api/GuessMarketContext.java +++ b/api/src/main/java/market/guess/api/GuessMarketContext.java @@ -1,27 +1,16 @@ package market.guess.api; import java.nio.file.Path; -import java.util.List; -import market.guess.exception.GuessMarketException; public interface GuessMarketContext { - LoadResultDTO loadEvents(Path path); + Result buyShares( + String userName, String eventKey, String optionKey, int quantity); - List listEvents(); + Result closeEvent(String userName, String eventKey, String winningOptionKey); - boolean isLoaded(); + Result account(String userName); - EventDetailDTO getEvent(String eventKey); + Result saveState(Path path); - PurchaseReceiptDTO buyShares(String userName, String eventKey, String optionKey, int quantity) - throws GuessMarketException; - - EventDetailDTO closeEvent(String userName, String eventKey, String winningOptionKey) - throws GuessMarketException; - - AccountDTO account(String userName); - - void saveState(Path path) throws GuessMarketException; - - LoadResultDTO restoreState(Path path); + Result restoreState(Path path); } diff --git a/api/src/main/java/market/guess/api/MarketDTO.java b/api/src/main/java/market/guess/api/MarketDTO.java new file mode 100644 index 0000000..b7c48a2 --- /dev/null +++ b/api/src/main/java/market/guess/api/MarketDTO.java @@ -0,0 +1,3 @@ +package market.guess.api; + +public record MarketDTO(String key, String name, String price, String volume) {} diff --git a/api/src/main/java/market/guess/api/MarketStateDTO.java b/api/src/main/java/market/guess/api/MarketStateDTO.java index f49b2aa..1f7e79b 100644 --- a/api/src/main/java/market/guess/api/MarketStateDTO.java +++ b/api/src/main/java/market/guess/api/MarketStateDTO.java @@ -5,6 +5,6 @@ import java.util.List; public record MarketStateDTO( String eventKey, MechanismType mechanism, - List options, + List markets, String accountBalance, String commissionCollected) {} diff --git a/api/src/main/java/market/guess/api/OptionStateDTO.java b/api/src/main/java/market/guess/api/OptionStateDTO.java deleted file mode 100644 index c002fdd..0000000 --- a/api/src/main/java/market/guess/api/OptionStateDTO.java +++ /dev/null @@ -1,3 +0,0 @@ -package market.guess.api; - -public record OptionStateDTO(String optionKey, String name, String price, long sharesOutstanding) {} diff --git a/api/src/main/java/market/guess/api/Result.java b/api/src/main/java/market/guess/api/Result.java new file mode 100644 index 0000000..15ac5db --- /dev/null +++ b/api/src/main/java/market/guess/api/Result.java @@ -0,0 +1,49 @@ +package market.guess.api; + +import java.time.Instant; + +public final class Result { + private final boolean success; + private final String message; + private final T data; + private final Instant timeStamp; + + private Result(boolean success, String message, T data) { + this.success = success; + this.message = message; + this.data = data; + this.timeStamp = Instant.now(); + } + + public static Result ok() { + return new Result(true, "Operation successful", null); + } + + public static Result ok(T data) { + return new Result<>(true, "Operation successful", data); + } + + public static Result error(String message, T data) { + return new Result<>(false, message, data); + } + + public static Result error(String message) { + return new Result<>(false, message, null); + } + + public boolean isSuccess() { + return success; + } + + public String getMessage() { + return message; + } + + public T getData() { + return data; + } + + public Instant getTimestamp() { + return timeStamp; + } +} diff --git a/api/src/main/java/market/guess/api/TradeRowDTO.java b/api/src/main/java/market/guess/api/TradeRowDTO.java index 7331bb1..68d159a 100644 --- a/api/src/main/java/market/guess/api/TradeRowDTO.java +++ b/api/src/main/java/market/guess/api/TradeRowDTO.java @@ -1,4 +1,4 @@ package market.guess.api; public record TradeRowDTO( - String at, String userName, String optionName, long quantity, String pricePaid) {} + String at, String userName, String optionName, String quantity, String pricePaid) {} diff --git a/service/src/main/java/market/guess/service/LocalCatalogContext.java b/service/src/main/java/market/guess/service/LocalCatalogContext.java new file mode 100644 index 0000000..15a5890 --- /dev/null +++ b/service/src/main/java/market/guess/service/LocalCatalogContext.java @@ -0,0 +1,45 @@ +package market.guess.service; + +import java.nio.file.Path; +import java.util.List; +import market.guess.api.CatalogContext; +import market.guess.api.EventDetailDTO; +import market.guess.api.EventSummaryDTO; +import market.guess.api.LoadResultDTO; +import market.guess.api.Result; +import market.guess.service.domain.Event; +import market.guess.service.infrastructure.MarketContext; +import market.guess.service.infrastructure.provider.Loader; + +public final class LocalCatalogContext implements CatalogContext { + + private final MarketContext context; + private final Loader provider; + + public LocalCatalogContext(Loader provider, MarketContext context) { + super(); + this.provider = provider; + this.context = context; + } + + @Override + public Result loadEvents(Path path) { + return Result.ok(provider.load(path)); + } + + @Override + public Result> getAllEvents() { + return Result.ok(context.getEvents().getAll().stream().map(Event::toEventSummary).toList()); + } + + @Override + public Result getEvent(String eventKey) { + var optional = context.getEvents().get(eventKey); + + if (optional.isEmpty()) return Result.error("Unable to find event."); + var event = optional.get(); + + return Result.ok( + new EventDetailDTO(event.toEventSummary(), event.toMarketState(), event.getHistory(), "")); + } +} diff --git a/service/src/main/java/market/guess/service/LocalGuessMarketContext.java b/service/src/main/java/market/guess/service/LocalGuessMarketContext.java index b405329..8f97432 100644 --- a/service/src/main/java/market/guess/service/LocalGuessMarketContext.java +++ b/service/src/main/java/market/guess/service/LocalGuessMarketContext.java @@ -4,83 +4,66 @@ import java.nio.file.Path; import java.util.List; import market.guess.api.AccountDTO; import market.guess.api.EventDetailDTO; -import market.guess.api.EventSummaryDTO; import market.guess.api.GuessMarketContext; import market.guess.api.LoadResultDTO; +import market.guess.api.ProblemDTO; import market.guess.api.PurchaseReceiptDTO; -import market.guess.exception.GuessMarketException; -import market.guess.service.domain.Event; +import market.guess.api.Result; import market.guess.service.infrastructure.MarketContext; -import market.guess.service.infrastructure.provider.Loader; public final class LocalGuessMarketContext implements GuessMarketContext { private final TradingOperation tradingOperation; private final MarketContext context; - private final Loader provider; - public LocalGuessMarketContext( - Loader provider, MarketContext context, TradingOperation tradingOperation) { + public LocalGuessMarketContext(MarketContext context, TradingOperation tradingOperation) { super(); - this.provider = provider; this.context = context; this.tradingOperation = tradingOperation; } @Override - public LoadResultDTO loadEvents(Path path) { - return provider.load(path); - } - - @Override - public List listEvents() { - return context.getEvents().getAll().stream().map(Event::toEventSummary).toList(); - } - - @Override - public boolean isLoaded() { - throw new UnsupportedOperationException("Unimplemented method 'isLoaded'"); - } - - @Override - public EventDetailDTO getEvent(String eventKey) { - throw new UnsupportedOperationException("Unimplemented method 'getEvent'"); - } - - @Override - public PurchaseReceiptDTO buyShares( - String userName, String eventKey, String optionKey, int quantity) - throws GuessMarketException { + public Result buyShares( + String userName, String eventKey, String optionKey, int quantity) { var optional = context.getEvents().get(eventKey); var userOptional = context.getUsers().get(userName); - if (optional.isEmpty() || userOptional.isEmpty()) throw new GuessMarketException(); + if (optional.isEmpty() || userOptional.isEmpty()) Result.error("Unable to find user/event."); var event = optional.get(); var user = userOptional.get(); var option = event.getOption(optionKey); var trade = tradingOperation.buy(event, user, option, quantity); - return new PurchaseReceiptDTO(); + return Result.ok(new PurchaseReceiptDTO()); } @Override - public EventDetailDTO closeEvent(String userName, String eventKey, String winningOptionKey) - throws GuessMarketException { + public Result closeEvent( + String userName, String eventKey, String winningOptionKey) { throw new UnsupportedOperationException("Unimplemented method 'account'"); } @Override - public AccountDTO account(String userName) { + public Result account(String userName) { throw new UnsupportedOperationException("Unimplemented method 'account'"); } @Override - public void saveState(Path path) throws GuessMarketException { + public Result saveState(Path path) { context.save(); + return Result.ok(); } @Override - public LoadResultDTO restoreState(Path path) { - throw new UnsupportedOperationException("Unimplemented method 'restoreState'"); + public Result restoreState(Path path) { + + try { + context.load(); + } catch (Exception e) { + return Result.error( + "Failed to load.", + LoadResultDTO.failure(path.toString(), List.of(new ProblemDTO("", e.getMessage(), "")))); + } + return Result.ok(LoadResultDTO.success(path.toString(), context.getEvents().getAll().size())); } } diff --git a/service/src/main/java/market/guess/service/TradingOperation.java b/service/src/main/java/market/guess/service/TradingOperation.java index c8153c2..63572c0 100644 --- a/service/src/main/java/market/guess/service/TradingOperation.java +++ b/service/src/main/java/market/guess/service/TradingOperation.java @@ -1,13 +1,13 @@ package market.guess.service; import market.guess.service.domain.Event; -import market.guess.service.domain.Option; +import market.guess.service.domain.Market; import market.guess.service.domain.Settlement; import market.guess.service.domain.Trade; import market.guess.service.domain.User; public interface TradingOperation { - Trade buy(Event event, User user, Option Option, int quantity); + Trade buy(Event event, User user, Market Option, int quantity); Settlement settle(Event event, String winningOption); } diff --git a/service/src/main/java/market/guess/service/TradingOperationImpl.java b/service/src/main/java/market/guess/service/TradingOperationImpl.java index f6c9aa1..641a3a9 100644 --- a/service/src/main/java/market/guess/service/TradingOperationImpl.java +++ b/service/src/main/java/market/guess/service/TradingOperationImpl.java @@ -1,7 +1,7 @@ package market.guess.service; import market.guess.service.domain.Event; -import market.guess.service.domain.Option; +import market.guess.service.domain.Market; import market.guess.service.domain.Settlement; import market.guess.service.domain.Trade; import market.guess.service.domain.User; @@ -9,7 +9,7 @@ import market.guess.service.domain.User; public final class TradingOperationImpl implements TradingOperation { @Override - public Trade buy(Event event, User user, Option Option, int quantity) { + public Trade buy(Event event, User user, Market Option, int quantity) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'buy'"); } diff --git a/service/src/main/java/market/guess/service/domain/Account.java b/service/src/main/java/market/guess/service/domain/Account.java deleted file mode 100644 index 48d5054..0000000 --- a/service/src/main/java/market/guess/service/domain/Account.java +++ /dev/null @@ -1,49 +0,0 @@ -package market.guess.service.domain; - -import java.math.BigDecimal; -import java.time.Clock; -import java.util.ArrayList; -import java.util.List; -import market.guess.api.LedgerType; - -public final class Account { - private final String owner; - private final List entries = new ArrayList<>(); - private BigDecimal balance; - private int runningId; - - private final Clock clock; - - public Account(String owner, BigDecimal initialBalance) { - this(owner, initialBalance, Clock.systemUTC()); - } - - public Account(String owner, BigDecimal initialBalance, Clock clock) { - super(); - this.owner = owner; - this.balance = BigDecimalOptions.toMoney(initialBalance); - this.clock = clock; - } - - public String getOwner() { - return owner; - } - - public BigDecimal getBalance() { - return balance; - } - - public boolean credit(LedgerType type, BigDecimal amount, String note) { - record(type, amount, note); - return true; - } - - public boolean debit(LedgerType type, BigDecimal amount, String note) { - record(type, amount.negate(), note); - return true; - } - - private void record(LedgerType type, BigDecimal amount, String note) { - entries.add(new LedgerEntry(runningId++, clock.instant(), type, amount, balance, note)); - } -} diff --git a/service/src/main/java/market/guess/service/domain/Event.java b/service/src/main/java/market/guess/service/domain/Event.java index ce35e82..5c98908 100644 --- a/service/src/main/java/market/guess/service/domain/Event.java +++ b/service/src/main/java/market/guess/service/domain/Event.java @@ -7,6 +7,9 @@ import java.util.List; import market.guess.api.CommissionTiming; import market.guess.api.EventStatus; import market.guess.api.EventSummaryDTO; +import market.guess.api.MarketStateDTO; +import market.guess.api.TradeRowDTO; +import market.guess.service.ledger.Ledger; import market.guess.service.mechanism.TradingMechanism; public final class Event { @@ -17,13 +20,13 @@ public final class Event { private final int commissionPercent; private final CommissionTiming commissionTiming; private final TradingMechanism mechanism; - private final List