aboutsummaryrefslogtreecommitdiffstats
path: root/service/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'service/src/main/java')
-rw-r--r--service/src/main/java/market/guess/service/LocalGuessMarketContext.java20
-rw-r--r--service/src/main/java/market/guess/service/catalog/infrastructure/MarketContext.java18
-rw-r--r--service/src/main/java/market/guess/service/catalog/infrastructure/mapper/v1/EventMapperV1.java17
-rw-r--r--service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLLoaderV1.java34
-rw-r--r--service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLValidatorV1.java80
5 files changed, 142 insertions, 27 deletions
diff --git a/service/src/main/java/market/guess/service/LocalGuessMarketContext.java b/service/src/main/java/market/guess/service/LocalGuessMarketContext.java
index 909a808..f04324b 100644
--- a/service/src/main/java/market/guess/service/LocalGuessMarketContext.java
+++ b/service/src/main/java/market/guess/service/LocalGuessMarketContext.java
@@ -10,6 +10,7 @@ import market.guess.model.event.EventDetailDTO;
10import market.guess.service.catalog.infrastructure.MarketContext; 10import market.guess.service.catalog.infrastructure.MarketContext;
11import market.guess.service.domain.Order; 11import market.guess.service.domain.Order;
12import market.guess.service.fulfillment.FulfillmentContext; 12import market.guess.service.fulfillment.FulfillmentContext;
13import market.guess.service.helpers.BigDecimalOptions;
13import market.guess.service.helpers.InstantOptions; 14import market.guess.service.helpers.InstantOptions;
14import market.guess.service.matching.MatchingEngine; 15import market.guess.service.matching.MatchingEngine;
15import market.guess.service.risk.RiskEngine; 16import market.guess.service.risk.RiskEngine;
@@ -55,6 +56,20 @@ public final class LocalGuessMarketContext implements GuessMarketContext {
55 56
56 var rawTrades = matching.match(order); 57 var rawTrades = matching.match(order);
57 var trades = fulfillment.fulfill(user, order, rawTrades); 58 var trades = fulfillment.fulfill(user, order, rawTrades);
59
60 var totalSharesCost =
61 trades.stream()
62 .map(t -> t.sharesCost())
63 .reduce(BigDecimalOptions.ZERO_MONEY, BigDecimal::add);
64 var totalCommission =
65 trades.stream()
66 .map(t -> t.commission())
67 .reduce(BigDecimalOptions.ZERO_MONEY, BigDecimal::add);
68 var totalPaid =
69 trades.stream()
70 .map(t -> t.totalPaid())
71 .reduce(BigDecimalOptions.ZERO_MONEY, BigDecimal::add);
72
58 return Result.ok( 73 return Result.ok(
59 new PurchaseReceiptDTO( 74 new PurchaseReceiptDTO(
60 trades.stream() 75 trades.stream()
@@ -65,8 +80,11 @@ public final class LocalGuessMarketContext implements GuessMarketContext {
65 t.buyerUserName(), 80 t.buyerUserName(),
66 t.marketName(), 81 t.marketName(),
67 String.valueOf(t.quantity()), 82 String.valueOf(t.quantity()),
68 t.totalPaid().toPlainString())) 83 t.sharesCost().toPlainString()))
69 .toList(), 84 .toList(),
85 totalSharesCost.toPlainString(),
86 totalCommission.toPlainString(),
87 totalPaid.toPlainString(),
70 event.toMarketState())); 88 event.toMarketState()));
71 } 89 }
72 90
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/MarketContext.java b/service/src/main/java/market/guess/service/catalog/infrastructure/MarketContext.java
index cb2b467..5fc2e24 100644
--- a/service/src/main/java/market/guess/service/catalog/infrastructure/MarketContext.java
+++ b/service/src/main/java/market/guess/service/catalog/infrastructure/MarketContext.java
@@ -67,15 +67,22 @@ public final class MarketContext {
67 GSON.toJson(new Wrapper(events.getAll(), users.getAll())), 67 GSON.toJson(new Wrapper(events.getAll(), users.getAll())),
68 StandardCharsets.UTF_8); 68 StandardCharsets.UTF_8);
69 } catch (Exception e) { 69 } catch (Exception e) {
70 throw new GuessMarketException("Failed to sove the file."); 70 throw new GuessMarketException("Failed to save state to " + target + ": " + e.getMessage());
71 } 71 }
72 } 72 }
73 73
74 public void load(Path path) throws GuessMarketException { 74 public void load(Path path) throws GuessMarketException {
75 var target = withJsonExtension(path);
76 if (!Files.isRegularFile(target)) {
77 throw new GuessMarketException("State file not found: " + target);
78 }
75 try { 79 try {
76 var wrapper = 80 var wrapper =
77 GSON.fromJson( 81 GSON.fromJson(
78 Files.readString(withJsonExtension(path), StandardCharsets.UTF_8), Wrapper.class); 82 Files.readString(target, StandardCharsets.UTF_8), Wrapper.class);
83 if (wrapper == null || wrapper.events() == null) {
84 throw new GuessMarketException("State file contains invalid or empty data.");
85 }
79 events.clear(); 86 events.clear();
80 users.clear(); 87 users.clear();
81 88
@@ -88,14 +95,15 @@ public final class MarketContext {
88 } 95 }
89 96
90 } catch (JsonSyntaxException e) { 97 } catch (JsonSyntaxException e) {
91 throw new GuessMarketException("The JSON file is malformed."); 98 throw new GuessMarketException("The state file is malformed JSON: " + e.getMessage());
92 } catch (IOException e) { 99 } catch (IOException e) {
93 throw new GuessMarketException("Unable the file."); 100 throw new GuessMarketException("Unable to read state file: " + e.getMessage());
94 } 101 }
95 } 102 }
96 103
97 private static Path withJsonExtension(Path path) { 104 private static Path withJsonExtension(Path path) {
105 if (path == null) return null;
98 var name = path.getFileName().toString(); 106 var name = path.getFileName().toString();
99 return name.endsWith(".json") ? path : path.resolveSibling(name + ".json"); 107 return name.toLowerCase().endsWith(".json") ? path : path.resolveSibling(name + ".json");
100 } 108 }
101} 109}
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/mapper/v1/EventMapperV1.java b/service/src/main/java/market/guess/service/catalog/infrastructure/mapper/v1/EventMapperV1.java
index f45eb46..b9c43bb 100644
--- a/service/src/main/java/market/guess/service/catalog/infrastructure/mapper/v1/EventMapperV1.java
+++ b/service/src/main/java/market/guess/service/catalog/infrastructure/mapper/v1/EventMapperV1.java
@@ -19,8 +19,8 @@ public final class EventMapperV1 implements Mapper<GMEvent, Event> {
19 return new Event( 19 return new Event(
20 getEventKey(source), 20 getEventKey(source),
21 source.getId(), 21 source.getId(),
22 joinName(source.getName()), 22 joinName(source.getName()).trim(),
23 source.getDescription(), 23 source.getDescription() != null ? source.getDescription().trim() : "",
24 source.getComision().getValue(), 24 source.getComision().getValue(),
25 getTiming(source.getComision().getType()), 25 getTiming(source.getComision().getType()),
26 new LmsrTradingMechanism(source.getGMMethod().getGMLMSR().getB(), options.size()), 26 new LmsrTradingMechanism(source.getGMMethod().getGMLMSR().getB(), options.size()),
@@ -38,17 +38,20 @@ public final class EventMapperV1 implements Mapper<GMEvent, Event> {
38 } 38 }
39 39
40 private static CommissionTiming getTiming(String type) throws IllegalArgumentException { 40 private static CommissionTiming getTiming(String type) throws IllegalArgumentException {
41 return switch (type) { 41 if (type == null) throw new IllegalArgumentException("Commission type cannot be null");
42 return switch (type.toLowerCase().trim()) {
42 case "on-close" -> CommissionTiming.ON_CLOSE; 43 case "on-close" -> CommissionTiming.ON_CLOSE;
43 case "on-purchase" -> CommissionTiming.ON_PURCHASE; 44 case "on-purchase" -> CommissionTiming.ON_PURCHASE;
44 default -> throw new IllegalArgumentException("Unknown comision type: " + type); 45 default -> throw new IllegalArgumentException("Unknown commission type: " + type);
45 }; 46 };
46 } 47 }
47 48
48 private static List<Market> getOptions(GMEvent x) { 49 private static List<Market> getOptions(GMEvent x) {
49 var ret = new ArrayList<Market>(x.getGMOptions().getGMOption().size()); 50 var raw = x.getGMOptions().getGMOption();
50 for (var i = 0; i < x.getGMOptions().getGMOption().size(); i++) { 51 var ret = new ArrayList<Market>(raw.size());
51 ret.add(new Market(getEventKey(x) + ":" + i, x.getGMOptions().getGMOption().get(i))); 52 for (var i = 0; i < raw.size(); i++) {
53 var opt = raw.get(i);
54 ret.add(new Market(getEventKey(x) + ":" + i, opt != null ? opt.trim() : ""));
52 } 55 }
53 return ret; 56 return ret;
54 } 57 }
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLLoaderV1.java b/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLLoaderV1.java
index bcae787..44e1c7f 100644
--- a/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLLoaderV1.java
+++ b/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLLoaderV1.java
@@ -2,8 +2,10 @@ package market.guess.service.catalog.infrastructure.provider.v1;
2 2
3import jakarta.xml.bind.JAXBContext; 3import jakarta.xml.bind.JAXBContext;
4import jakarta.xml.bind.JAXBException; 4import jakarta.xml.bind.JAXBException;
5import java.math.BigDecimal;
5import java.nio.file.Files; 6import java.nio.file.Files;
6import java.nio.file.Path; 7import java.nio.file.Path;
8import java.util.ArrayList;
7import market.guess.api.CatalogContext; 9import market.guess.api.CatalogContext;
8import market.guess.api.LoadResult; 10import market.guess.api.LoadResult;
9import market.guess.api.Result; 11import market.guess.api.Result;
@@ -12,6 +14,7 @@ import market.guess.service.catalog.infrastructure.provider.Loader;
12import market.guess.service.catalog.infrastructure.repository.EventRepository; 14import market.guess.service.catalog.infrastructure.repository.EventRepository;
13import market.guess.service.catalog.infrastructure.repository.UserRepository; 15import market.guess.service.catalog.infrastructure.repository.UserRepository;
14import market.guess.service.catalog.model.v1.GuessMarket; 16import market.guess.service.catalog.model.v1.GuessMarket;
17import market.guess.service.domain.Event;
15import market.guess.service.domain.User; 18import market.guess.service.domain.User;
16 19
17public final class XMLLoaderV1 implements Loader { 20public final class XMLLoaderV1 implements Loader {
@@ -35,19 +38,23 @@ public final class XMLLoaderV1 implements Loader {
35 try { 38 try {
36 context = JAXBContext.newInstance(GuessMarket.class); 39 context = JAXBContext.newInstance(GuessMarket.class);
37 } catch (JAXBException e) { 40 } catch (JAXBException e) {
38 throw new IllegalStateException(); 41 throw new IllegalStateException("Failed to initialize JAXB context: " + e.getMessage(), e);
39 } 42 }
40 } 43 }
41 44
42 @Override 45 @Override
43 public Result<LoadResult> load(Path path) { 46 public Result<LoadResult> load(Path path) {
47 if (path == null) {
48 return Result.error("INVALID_PATH", "File path cannot be null.");
49 }
50
44 if (!Files.isRegularFile(path)) { 51 if (!Files.isRegularFile(path)) {
45 return Result.error("FILE_NOT_FOUND", "No such file."); 52 return Result.error("FILE_NOT_FOUND", "No such file: " + path);
46 } 53 }
47 54
48 if (path.getFileName() == null 55 if (path.getFileName() == null
49 || !path.getFileName().toString().toLowerCase().endsWith(".xml")) { 56 || !path.getFileName().toString().toLowerCase().endsWith(".xml")) {
50 return Result.error("NOT_XML", "The file must be an XML."); 57 return Result.error("NOT_XML", "The file must have a .xml extension.");
51 } 58 }
52 59
53 try (var stream = Files.newInputStream(path)) { 60 try (var stream = Files.newInputStream(path)) {
@@ -59,21 +66,34 @@ public final class XMLLoaderV1 implements Loader {
59 return Result.error(validation.getMessage(), validation.getDetails()); 66 return Result.error(validation.getMessage(), validation.getDetails());
60 } 67 }
61 68
69 var domainEvents = new ArrayList<Event>();
62 for (var event : seed.getGMEvents().getGMEvent()) { 70 for (var event : seed.getGMEvents().getGMEvent()) {
63 eventRepository.add(mapper.toDomain(event)); 71 domainEvents.add(mapper.toDomain(event));
72 }
73
74 eventRepository.clear();
75 userRepository.clear();
76
77 for (var domainEvent : domainEvents) {
78 eventRepository.add(domainEvent);
64 } 79 }
65 80
66 userRepository.add(new User("Tester", 500)); 81 userRepository.add(new User("Tester", new BigDecimal("500.00")));
67 82
68 return Result.ok(new LoadResult(path.toString(), eventRepository.getAll().size())); 83 return Result.ok(new LoadResult(path.toString(), eventRepository.getAll().size()));
84 } catch (JAXBException e) {
85 var cause =
86 e.getLinkedException() != null ? e.getLinkedException().getMessage() : e.getMessage();
87 return Result.error(
88 "XML_PARSE_ERROR",
89 "The XML file is malformed or invalid: " + (cause != null ? cause : e.getMessage()));
69 } catch (Exception e) { 90 } catch (Exception e) {
70 return Result.error("UNREADABLE", "The file could not be read."); 91 return Result.error("UNREADABLE", "The file could not be read: " + e.getMessage());
71 } 92 }
72 } 93 }
73 94
74 @Override 95 @Override
75 public Result<LoadResult> seed(CatalogContext context) { 96 public Result<LoadResult> seed(CatalogContext context) {
76 // TODO Auto-generated method stub
77 throw new UnsupportedOperationException("Unimplemented method 'seed'"); 97 throw new UnsupportedOperationException("Unimplemented method 'seed'");
78 } 98 }
79} 99}
diff --git a/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLValidatorV1.java b/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLValidatorV1.java
index 567558a..5a4e5ea 100644
--- a/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLValidatorV1.java
+++ b/service/src/main/java/market/guess/service/catalog/infrastructure/provider/v1/XMLValidatorV1.java
@@ -9,27 +9,93 @@ public final class XMLValidatorV1 {
9 private static final int MAX_COMMISSION = 90; 9 private static final int MAX_COMMISSION = 90;
10 10
11 public Result<Void> validate(GuessMarket seed) { 11 public Result<Void> validate(GuessMarket seed) {
12 if (seed == null || seed.getGMEvents() == null || seed.getGMEvents().getGMEvent() == null) {
13 return Result.error("INVALID_XML", "The XML file contains no GM-events element.");
14 }
12 15
13 var seenIds = new HashSet<Integer>(); 16 var events = seed.getGMEvents().getGMEvent();
14 var duplicated = new HashSet<Integer>(); 17 if (events.isEmpty()) {
18 return Result.error("EMPTY_EVENTS", "The XML file does not contain any events.");
19 }
15 20
16 for (var e : seed.getGMEvents().getGMEvent()) { 21 var seenIds = new HashSet<Integer>();
17 22
18 if (!seenIds.add(e.getId()) && duplicated.add(e.getId())) { 23 for (var e : events) {
24 // 1. Event ID must be positive integer (> 0)
25 if (e.getId() <= 0) {
26 return Result.error(
27 "INVALID_ID",
28 "Event ID must be a positive integer (> 0), found: " + e.getId());
29 }
30 if (!seenIds.add(e.getId())) {
19 return Result.error( 31 return Result.error(
20 "DUPLICATE_ID", 32 "DUPLICATE_ID",
21 "Event id " + e.getId() + " appears more than once, every event id must be unique."); 33 "Event ID " + e.getId() + " appears more than once. Every event ID must be unique.");
22 } 34 }
23 35
36 // 2. Event name must not be empty
37 if (e.getName() == null || e.getName().isEmpty() || String.join(" ", e.getName()).isBlank()) {
38 return Result.error("MISSING_NAME", "Event ID " + e.getId() + " has an empty name.");
39 }
40
41 // 3. Event description must not be empty
42 if (e.getDescription() == null || e.getDescription().isBlank()) {
43 return Result.error("MISSING_DESCRIPTION", "Event ID " + e.getId() + " has an empty description.");
44 }
45
46 // 4. Commission validation
47 if (e.getComision() == null) {
48 return Result.error("MISSING_COMMISSION", "Event ID " + e.getId() + " is missing commission.");
49 }
24 var pct = e.getComision().getValue(); 50 var pct = e.getComision().getValue();
25 if (pct < MIN_COMMISSION || pct > MAX_COMMISSION) { 51 if (pct < MIN_COMMISSION || pct > MAX_COMMISSION) {
26 return Result.error( 52 return Result.error(
27 "COMMISSION_OUT_OF_RANGE", 53 "COMMISSION_OUT_OF_RANGE",
28 "Commission is %d%%, it must be between %d%% and %d%%." 54 "Event ID " + e.getId() + " commission is " + pct + "%. It must be between " + MIN_COMMISSION + "% and " + MAX_COMMISSION + "%.");
29 .formatted(pct, MIN_COMMISSION, MAX_COMMISSION)); 55 }
56
57 var type = e.getComision().getType();
58 if (type == null || (!type.equalsIgnoreCase("on-close") && !type.equalsIgnoreCase("on-purchase"))) {
59 return Result.error(
60 "INVALID_COMMISSION_TYPE",
61 "Event ID " + e.getId() + " has invalid commission type '" + type + "'. Must be 'on-close' or 'on-purchase'.");
62 }
63
64 // 5. GM-options validation (exactly 2 options for Exercise 1)
65 if (e.getGMOptions() == null || e.getGMOptions().getGMOption() == null) {
66 return Result.error("MISSING_OPTIONS", "Event ID " + e.getId() + " is missing GM-options.");
67 }
68 var options = e.getGMOptions().getGMOption();
69 if (options.size() != 2) {
70 return Result.error(
71 "INVALID_OPTIONS_COUNT",
72 "Event ID " + e.getId() + " must have exactly 2 options for Exercise 1, but found " + options.size() + ".");
73 }
74 var opt0 = options.get(0) == null ? "" : options.get(0).trim();
75 var opt1 = options.get(1) == null ? "" : options.get(1).trim();
76 if (opt0.isEmpty() || opt1.isEmpty()) {
77 return Result.error("EMPTY_OPTION", "Event ID " + e.getId() + " has an empty option name.");
78 }
79 if (opt0.equalsIgnoreCase(opt1)) {
80 return Result.error("DUPLICATE_OPTION", "Event ID " + e.getId() + " has duplicate option names: '" + opt0 + "'.");
81 }
82
83 // 6. GM-method and LMSR validation
84 if (e.getGMMethod() == null) {
85 return Result.error("MISSING_METHOD", "Event ID " + e.getId() + " is missing GM-method.");
86 }
87 if (e.getGMMethod().getGMLMSR() == null) {
88 return Result.error("INVALID_METHOD", "Event ID " + e.getId() + " must use LMSR trading method in Exercise 1.");
89 }
90 var b = e.getGMMethod().getGMLMSR().getB();
91 if (b <= 0) {
92 return Result.error(
93 "INVALID_LIQUIDITY",
94 "Event ID " + e.getId() + " has invalid liquidity b=" + b + ". b must be a positive integer (> 0).");
30 } 95 }
31 } 96 }
32 97
33 return Result.ok(); 98 return Result.ok();
34 } 99 }
35} 100}
101