profileShare

rasmusjy / tasteprint

Read-only snapshot

No repository description.

main default branch 169 files Expires Sep 13, 2026, 9:06 AM
TripService.java 6,885 bytes
1 package com.tasteprint.journey;
2
3 import java.time.Clock;
4 import java.time.LocalDate;
5 import java.util.ArrayList;
6 import java.util.Comparator;
7 import java.util.HashSet;
8 import java.util.List;
9 import java.util.Locale;
10 import java.util.Optional;
11 import java.util.Set;
12 import java.util.UUID;
13
14 import org.springframework.stereotype.Service;
15 import org.springframework.transaction.annotation.Transactional;
16
17 import com.tasteprint.catalog.CatalogService;
18 import com.tasteprint.catalog.DestinationDetails;
19 import com.tasteprint.catalog.DishCategory;
20 import com.tasteprint.catalog.DishView;
21 import com.tasteprint.shared.NotFoundException;
22 import com.tasteprint.tasting.TastingService;
23
24 @Service
25 public class TripService {
26
27 private static final int MISSION_SIZE = 5;
28
29 private final TripRepository trips;
30 private final TripMissionItemRepository missionItems;
31 private final CatalogService catalog;
32 private final TastingService tastings;
33 private final Clock clock;
34
35 TripService(TripRepository trips, TripMissionItemRepository missionItems,
36 CatalogService catalog, TastingService tastings, Clock clock) {
37 this.trips = trips;
38 this.missionItems = missionItems;
39 this.catalog = catalog;
40 this.tastings = tastings;
41 this.clock = clock;
42 }
43
44 @Transactional
45 public TripView create(UUID userId, SaveTripRequest request) {
46 String destinationCode = normalizeCountryCode(request.destinationCode());
47 DestinationDetails destination = catalog.destination(destinationCode);
48 Trip trip = trips.save(new Trip(
49 UUID.randomUUID(), userId, destinationCode, request, clock.instant()
50 ));
51
52 List<DishView> mission = chooseMission(destination.dishes(), tastings.triedDishSlugs(userId));
53 for (int index = 0; index < mission.size(); index++) {
54 missionItems.save(new TripMissionItem(UUID.randomUUID(), trip.id(), mission.get(index).slug(), index + 1));
55 }
56 return view(trip);
57 }
58
59 @Transactional
60 public TripView update(UUID userId, UUID tripId, SaveTripRequest request) {
61 Trip trip = requiredOwned(tripId, userId);
62 String requestedCode = normalizeCountryCode(request.destinationCode());
63 if (!trip.destinationCode().equals(requestedCode)) {
64 throw new IllegalArgumentException("Create a new trip to change the destination.");
65 }
66 trip.update(request, clock.instant());
67 return view(trip);
68 }
69
70 @Transactional
71 public void delete(UUID userId, UUID tripId) {
72 trips.delete(requiredOwned(tripId, userId));
73 }
74
75 @Transactional(readOnly = true)
76 public List<TripView> list(UUID userId) {
77 return trips.findByUserIdOrderByStartsOnAsc(userId).stream()
78 .map(this::view)
79 .sorted(Comparator.comparing((TripView trip) -> statusOrder(trip.status()))
80 .thenComparing(TripView::startsOn))
81 .toList();
82 }
83
84 @Transactional(readOnly = true)
85 public TripView get(UUID userId, UUID tripId) {
86 return view(requiredOwned(tripId, userId));
87 }
88
89 @Transactional(readOnly = true)
90 public Optional<TripView> next(UUID userId) {
91 return list(userId).stream()
92 .filter(trip -> trip.status() != TripStatus.COMPLETED)
93 .findFirst();
94 }
95
96 @Transactional(readOnly = true)
97 public boolean hasAny(UUID userId) {
98 return trips.existsByUserId(userId);
99 }
100
101 private TripView view(Trip trip) {
102 DestinationDetails destination = catalog.destination(trip.destinationCode());
103 Set<String> completedDuringTrip = tastings.triedDishSlugsDuring(
104 trip.userId(), trip.destinationCode(), trip.startsOn(), trip.endsOn()
105 );
106 List<MissionItemView> mission = missionItems.findByTripIdOrderByPositionAsc(trip.id()).stream()
107 .map(item -> new MissionItemView(
108 item.position(), catalog.dish(item.dishSlug()), completedDuringTrip.contains(item.dishSlug())
109 ))
110 .toList();
111 int coverage = weightedCoverage(destination.dishes(), completedDuringTrip);
112 int completed = (int) mission.stream().filter(MissionItemView::completed).count();
113 return new TripView(
114 trip.id(), destination.destination(), trip.city(), trip.startsOn(), trip.endsOn(),
115 status(trip), coverage, completed, mission
116 );
117 }
118
119 private List<DishView> chooseMission(List<DishView> dishes, Set<String> alreadyTried) {
120 List<DishView> sorted = dishes.stream()
121 .sorted(Comparator
122 .comparing((DishView dish) -> alreadyTried.contains(dish.slug()))
123 .thenComparing(DishView::importance, Comparator.reverseOrder()))
124 .toList();
125 List<DishView> selected = new ArrayList<>();
126 Set<DishCategory> usedCategories = new HashSet<>();
127 for (DishView dish : sorted) {
128 if (usedCategories.add(dish.category())) {
129 selected.add(dish);
130 }
131 if (selected.size() == MISSION_SIZE) {
132 break;
133 }
134 }
135 if (selected.size() < MISSION_SIZE) {
136 for (DishView dish : sorted) {
137 if (!selected.contains(dish)) {
138 selected.add(dish);
139 }
140 if (selected.size() == MISSION_SIZE) {
141 break;
142 }
143 }
144 }
145 return selected;
146 }
147
148 private int weightedCoverage(List<DishView> dishes, Set<String> completed) {
149 int totalWeight = dishes.stream().mapToInt(DishView::importance).sum();
150 int completedWeight = dishes.stream()
151 .filter(dish -> completed.contains(dish.slug()))
152 .mapToInt(DishView::importance)
153 .sum();
154 return totalWeight == 0 ? 0 : (int) Math.round(completedWeight * 100.0 / totalWeight);
155 }
156
157 private TripStatus status(Trip trip) {
158 LocalDate today = LocalDate.now(clock);
159 if (today.isBefore(trip.startsOn())) {
160 return TripStatus.UPCOMING;
161 }
162 if (today.isAfter(trip.endsOn())) {
163 return TripStatus.COMPLETED;
164 }
165 return TripStatus.ACTIVE;
166 }
167
168 private int statusOrder(TripStatus status) {
169 return switch (status) {
170 case ACTIVE -> 0;
171 case UPCOMING -> 1;
172 case COMPLETED -> 2;
173 };
174 }
175
176 private Trip requiredOwned(UUID tripId, UUID userId) {
177 return trips.findByIdAndUserId(tripId, userId)
178 .orElseThrow(() -> new NotFoundException("Trip was not found."));
179 }
180
181 private String normalizeCountryCode(String countryCode) {
182 return countryCode.trim().toUpperCase(Locale.ROOT);
183 }
184 }
185