Skip to content

Commit cb50ac2

Browse files
committed
ui 2 simulation
1 parent 8760436 commit cb50ac2

21 files changed

Lines changed: 1011 additions & 153 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.axoniq.demo.orderfulfillment.api;
2+
3+
import org.axonframework.messaging.eventhandling.annotation.Event;
4+
5+
@Event
6+
public record OrderDelivered(
7+
String orderId,
8+
String trackingNumber
9+
) {
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.axoniq.demo.orderfulfillment.api;
2+
3+
import org.axonframework.messaging.eventhandling.annotation.Event;
4+
5+
@Event
6+
public record OrderFailed(
7+
String orderId,
8+
String reason
9+
) {
10+
}

order-fulfillment-workflow/src/main/java/io/axoniq/demo/orderfulfillment/api/OrderPlaced.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ public record OrderPlaced(
77
String orderId,
88
String customerId,
99
String email,
10-
double amount
10+
double amount,
11+
String originCity,
12+
double originLat,
13+
double originLng,
14+
String destinationCity,
15+
double destinationLat,
16+
double destinationLng,
17+
String scenario
1118
) {
1219
}

order-fulfillment-workflow/src/main/java/io/axoniq/demo/orderfulfillment/api/ShipOrderCompleted.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,16 @@
22

33
/**
44
* Emitted automatically by the workflow engine when the {@code shipOrder} step completes.
5-
* The Completed event's payload is whatever the action returned — here {@code orderId} and
6-
* {@code trackingNumber}. The workflow does not publish this event itself.
5+
* The Completed event's payload is whatever the action returned — here {@code orderId},
6+
* {@code trackingNumber} and the route coordinates so downstream simulators can animate
7+
* truck movement without re-reading the projection.
78
*/
8-
public record ShipOrderCompleted(String orderId, String trackingNumber) {
9+
public record ShipOrderCompleted(
10+
String orderId,
11+
String trackingNumber,
12+
double originLat,
13+
double originLng,
14+
double destinationLat,
15+
double destinationLng
16+
) {
917
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.axoniq.demo.orderfulfillment.api;
2+
3+
import org.axonframework.messaging.eventhandling.annotation.Event;
4+
5+
/**
6+
* Published by the truck-movement simulator while a shipment is in transit. Not part of the
7+
* workflow — purely a visualization signal driving the live map.
8+
*/
9+
@Event
10+
public record TruckLocationUpdated(
11+
String orderId,
12+
double lat,
13+
double lng,
14+
double progress
15+
) {
16+
}

order-fulfillment-workflow/src/main/java/io/axoniq/demo/orderfulfillment/controller/OrderController.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io.axoniq.demo.orderfulfillment.projection.OrderEventStream;
66
import io.axoniq.demo.orderfulfillment.projection.OrderStatus;
77
import io.axoniq.demo.orderfulfillment.projection.OrderStatusProjection;
8+
import io.axoniq.demo.orderfulfillment.simulator.Cities;
89
import org.axonframework.messaging.eventhandling.gateway.EventGateway;
910
import org.slf4j.Logger;
1011
import org.slf4j.LoggerFactory;
@@ -41,10 +42,17 @@ public OrderController(EventGateway eventGateway,
4142
@PostMapping
4243
public String placeOrder(@RequestParam("customerId") String customerId,
4344
@RequestParam("email") String email,
44-
@RequestParam("amount") double amount) {
45+
@RequestParam("amount") double amount,
46+
@RequestParam(value = "scenario", required = false) String scenario) {
4547
var orderId = UUID.randomUUID().toString();
46-
logger.info("Publishing OrderPlaced for order {}.", orderId);
47-
eventGateway.publish(null, new OrderPlaced(orderId, customerId, email, amount));
48+
var route = Cities.randomPair();
49+
logger.info("Publishing OrderPlaced for order {} ({} → {}).",
50+
orderId, route[0].name(), route[1].name());
51+
eventGateway.publish(null, new OrderPlaced(
52+
orderId, customerId, email, amount,
53+
route[0].name(), route[0].lat(), route[0].lng(),
54+
route[1].name(), route[1].lat(), route[1].lng(),
55+
scenario == null ? "happy" : scenario));
4856
return orderId;
4957
}
5058

order-fulfillment-workflow/src/main/java/io/axoniq/demo/orderfulfillment/projection/OrderEventStream.java

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package io.axoniq.demo.orderfulfillment.projection;
22

33
import io.axoniq.demo.orderfulfillment.api.InitiatingPaymentForCustomerStarted;
4+
import io.axoniq.demo.orderfulfillment.api.OrderDelivered;
5+
import io.axoniq.demo.orderfulfillment.api.OrderFailed;
46
import io.axoniq.demo.orderfulfillment.api.OrderPlaced;
57
import io.axoniq.demo.orderfulfillment.api.ShipOrderCompleted;
8+
import io.axoniq.demo.orderfulfillment.api.TruckLocationUpdated;
69
import org.axonframework.messaging.eventhandling.annotation.EventHandler;
710
import org.slf4j.Logger;
811
import org.slf4j.LoggerFactory;
@@ -11,46 +14,64 @@
1114

1215
import java.io.IOException;
1316
import java.time.Instant;
17+
import java.util.HashMap;
1418
import java.util.List;
1519
import java.util.Map;
1620
import java.util.concurrent.CopyOnWriteArrayList;
1721

1822
/**
19-
* Pushes workflow Started/Completed updates to subscribed SSE clients. Uses the same workflow
20-
* events the {@link OrderStatusProjection} listens to, so the live view reflects the durable
21-
* projection state.
23+
* Pushes workflow Started/Completed updates plus simulator location ticks to subscribed SSE
24+
* clients. New subscribers receive a snapshot of all known orders so the map can render existing
25+
* shipments immediately.
2226
*/
2327
@Component
2428
public class OrderEventStream {
2529

2630
private static final Logger logger = LoggerFactory.getLogger(OrderEventStream.class);
2731

2832
private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
33+
private final OrderStatusProjection projection;
34+
35+
public OrderEventStream(OrderStatusProjection projection) {
36+
this.projection = projection;
37+
}
2938

3039
public SseEmitter subscribe() {
3140
var emitter = new SseEmitter(0L);
3241
emitters.add(emitter);
3342
emitter.onCompletion(() -> emitters.remove(emitter));
3443
emitter.onTimeout(() -> emitters.remove(emitter));
3544
emitter.onError(e -> emitters.remove(emitter));
45+
try {
46+
emitter.send(SseEmitter.event().name("snapshot").data(projection.findAll()));
47+
} catch (IOException e) {
48+
emitters.remove(emitter);
49+
}
3650
return emitter;
3751
}
3852

3953
@EventHandler
4054
public void on(OrderPlaced event) {
41-
broadcast(Map.of(
42-
"type", "PLACED",
43-
"orderId", event.orderId(),
44-
"customerId", event.customerId(),
45-
"email", event.email(),
46-
"amount", event.amount(),
47-
"timestamp", Instant.now().toString()
48-
));
55+
var payload = new HashMap<String, Object>();
56+
payload.put("type", "PLACED");
57+
payload.put("orderId", event.orderId());
58+
payload.put("customerId", event.customerId());
59+
payload.put("email", event.email());
60+
payload.put("amount", event.amount());
61+
payload.put("originCity", event.originCity());
62+
payload.put("originLat", event.originLat());
63+
payload.put("originLng", event.originLng());
64+
payload.put("destinationCity", event.destinationCity());
65+
payload.put("destinationLat", event.destinationLat());
66+
payload.put("destinationLng", event.destinationLng());
67+
payload.put("scenario", event.scenario());
68+
payload.put("timestamp", Instant.now().toString());
69+
broadcast("order", payload);
4970
}
5071

5172
@EventHandler
5273
public void on(InitiatingPaymentForCustomerStarted event) {
53-
broadcast(Map.of(
74+
broadcast("order", Map.of(
5475
"type", "AWAITING_PAYMENT",
5576
"orderId", event.orderId(),
5677
"timestamp", Instant.now().toString()
@@ -59,18 +80,47 @@ public void on(InitiatingPaymentForCustomerStarted event) {
5980

6081
@EventHandler
6182
public void on(ShipOrderCompleted event) {
62-
broadcast(Map.of(
63-
"type", "SHIPPED",
83+
broadcast("order", Map.of(
84+
"type", "IN_TRANSIT",
6485
"orderId", event.orderId(),
6586
"trackingNumber", event.trackingNumber(),
6687
"timestamp", Instant.now().toString()
6788
));
6889
}
6990

70-
private void broadcast(Map<String, Object> payload) {
91+
@EventHandler
92+
public void on(TruckLocationUpdated event) {
93+
broadcast("location", Map.of(
94+
"orderId", event.orderId(),
95+
"lat", event.lat(),
96+
"lng", event.lng(),
97+
"progress", event.progress()
98+
));
99+
}
100+
101+
@EventHandler
102+
public void on(OrderDelivered event) {
103+
broadcast("order", Map.of(
104+
"type", "DELIVERED",
105+
"orderId", event.orderId(),
106+
"timestamp", Instant.now().toString()
107+
));
108+
}
109+
110+
@EventHandler
111+
public void on(OrderFailed event) {
112+
broadcast("order", Map.of(
113+
"type", "FAILED",
114+
"orderId", event.orderId(),
115+
"reason", event.reason(),
116+
"timestamp", Instant.now().toString()
117+
));
118+
}
119+
120+
private void broadcast(String name, Object payload) {
71121
for (SseEmitter emitter : emitters) {
72122
try {
73-
emitter.send(SseEmitter.event().name("order").data(payload));
123+
emitter.send(SseEmitter.event().name(name).data(payload));
74124
} catch (IOException | IllegalStateException e) {
75125
logger.debug("Dropping dead SSE emitter: {}", e.getMessage());
76126
emitters.remove(emitter);

order-fulfillment-workflow/src/main/java/io/axoniq/demo/orderfulfillment/projection/OrderStatus.java

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,70 @@ public record OrderStatus(
66
String email,
77
double amount,
88
Status status,
9-
String trackingNumber
9+
String trackingNumber,
10+
String originCity,
11+
double originLat,
12+
double originLng,
13+
String destinationCity,
14+
double destinationLat,
15+
double destinationLng,
16+
double currentLat,
17+
double currentLng,
18+
double progress,
19+
String scenario,
20+
String failureReason
1021
) {
1122

1223
public enum Status {
1324
PLACED,
1425
AWAITING_PAYMENT,
15-
SHIPPED
26+
IN_TRANSIT,
27+
DELIVERED,
28+
FAILED
1629
}
1730

1831
public OrderStatus awaitingPayment() {
19-
return new OrderStatus(orderId, customerId, email, amount, Status.AWAITING_PAYMENT, trackingNumber);
32+
return new OrderStatus(orderId, customerId, email, amount, Status.AWAITING_PAYMENT,
33+
trackingNumber,
34+
originCity, originLat, originLng,
35+
destinationCity, destinationLat, destinationLng,
36+
currentLat, currentLng, progress,
37+
scenario, failureReason);
2038
}
2139

22-
public OrderStatus shipped(String trackingNumber) {
23-
return new OrderStatus(orderId, customerId, email, amount, Status.SHIPPED, trackingNumber);
40+
public OrderStatus dispatched(String trackingNumber) {
41+
return new OrderStatus(orderId, customerId, email, amount, Status.IN_TRANSIT,
42+
trackingNumber,
43+
originCity, originLat, originLng,
44+
destinationCity, destinationLat, destinationLng,
45+
originLat, originLng, 0.0,
46+
scenario, failureReason);
47+
}
48+
49+
public OrderStatus moved(double lat, double lng, double progress) {
50+
return new OrderStatus(orderId, customerId, email, amount, status,
51+
trackingNumber,
52+
originCity, originLat, originLng,
53+
destinationCity, destinationLat, destinationLng,
54+
lat, lng, progress,
55+
scenario, failureReason);
56+
}
57+
58+
public OrderStatus delivered() {
59+
return new OrderStatus(orderId, customerId, email, amount, Status.DELIVERED,
60+
trackingNumber,
61+
originCity, originLat, originLng,
62+
destinationCity, destinationLat, destinationLng,
63+
destinationLat, destinationLng, 1.0,
64+
scenario, failureReason);
65+
}
66+
67+
public OrderStatus failed(String reason) {
68+
return new OrderStatus(orderId, customerId, email, amount, Status.FAILED,
69+
trackingNumber,
70+
originCity, originLat, originLng,
71+
destinationCity, destinationLat, destinationLng,
72+
currentLat, currentLng, progress,
73+
scenario, reason);
2474
}
2575
}

order-fulfillment-workflow/src/main/java/io/axoniq/demo/orderfulfillment/projection/OrderStatusProjection.java

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
package io.axoniq.demo.orderfulfillment.projection;
22

33
import io.axoniq.demo.orderfulfillment.api.InitiatingPaymentForCustomerStarted;
4+
import io.axoniq.demo.orderfulfillment.api.OrderDelivered;
5+
import io.axoniq.demo.orderfulfillment.api.OrderFailed;
46
import io.axoniq.demo.orderfulfillment.api.OrderPlaced;
57
import io.axoniq.demo.orderfulfillment.api.ShipOrderCompleted;
8+
import io.axoniq.demo.orderfulfillment.api.TruckLocationUpdated;
69
import org.axonframework.messaging.eventhandling.annotation.EventHandler;
710
import org.springframework.stereotype.Component;
811

12+
import java.util.Collection;
913
import java.util.Map;
1014
import java.util.Optional;
1115
import java.util.concurrent.ConcurrentHashMap;
1216

1317

1418
/**
15-
* Builds order status by listening to the externally-published {@link OrderPlaced} and to the
16-
* workflow engine's auto-emitted {@link InitiatingPaymentForCustomerStarted} (the Started event
17-
* of the {@code initiatePayment} step) and {@link ShipOrderCompleted} (the Completed event of
18-
* the {@code shipOrder} step).
19+
* Builds order status from workflow Started/Completed events plus the simulator's truck-movement
20+
* events. The projection is the system of record for the live UI — every status change here is
21+
* what the SSE stream broadcasts.
1922
*/
2023
@Component
2124
public class OrderStatusProjection {
@@ -30,6 +33,17 @@ public void on(OrderPlaced event) {
3033
event.email(),
3134
event.amount(),
3235
OrderStatus.Status.PLACED,
36+
null,
37+
event.originCity(),
38+
event.originLat(),
39+
event.originLng(),
40+
event.destinationCity(),
41+
event.destinationLat(),
42+
event.destinationLng(),
43+
event.originLat(),
44+
event.originLng(),
45+
0.0,
46+
event.scenario(),
3347
null));
3448
}
3549

@@ -41,10 +55,30 @@ public void on(InitiatingPaymentForCustomerStarted event) {
4155
@EventHandler
4256
public void on(ShipOrderCompleted event) {
4357
orders.computeIfPresent(event.orderId(),
44-
(id, current) -> current.shipped(event.trackingNumber()));
58+
(id, current) -> current.dispatched(event.trackingNumber()));
59+
}
60+
61+
@EventHandler
62+
public void on(TruckLocationUpdated event) {
63+
orders.computeIfPresent(event.orderId(),
64+
(id, current) -> current.moved(event.lat(), event.lng(), event.progress()));
65+
}
66+
67+
@EventHandler
68+
public void on(OrderDelivered event) {
69+
orders.computeIfPresent(event.orderId(), (id, current) -> current.delivered());
70+
}
71+
72+
@EventHandler
73+
public void on(OrderFailed event) {
74+
orders.computeIfPresent(event.orderId(), (id, current) -> current.failed(event.reason()));
4575
}
4676

4777
public Optional<OrderStatus> findById(String orderId) {
4878
return Optional.ofNullable(orders.get(orderId));
4979
}
80+
81+
public Collection<OrderStatus> findAll() {
82+
return orders.values();
83+
}
5084
}

0 commit comments

Comments
 (0)