profileShare

rasmusjy / tasteprint

Read-only snapshot

No repository description.

main default branch 169 files Expires Sep 13, 2026, 9:06 AM
ProgressService.java 6,915 bytes
1 package com.tasteprint.progress;
2
3 import java.util.ArrayList;
4 import java.util.Comparator;
5 import java.util.HashMap;
6 import java.util.HashSet;
7 import java.util.LinkedHashSet;
8 import java.util.List;
9 import java.util.Map;
10 import java.util.Optional;
11 import java.util.Set;
12 import java.util.UUID;
13 import java.util.function.Function;
14 import java.util.stream.Collectors;
15
16 import org.springframework.stereotype.Service;
17 import org.springframework.transaction.annotation.Transactional;
18
19 import com.tasteprint.account.AccountService;
20 import com.tasteprint.catalog.CatalogService;
21 import com.tasteprint.catalog.DestinationDetails;
22 import com.tasteprint.catalog.DestinationSummary;
23 import com.tasteprint.catalog.DishView;
24 import com.tasteprint.journey.TripService;
25 import com.tasteprint.journey.TripView;
26 import com.tasteprint.tasting.TastingService;
27
28 @Service
29 public class ProgressService {
30
31 private static final int MISSING_LIMIT = 6;
32
33 private final AccountService accounts;
34 private final CatalogService catalog;
35 private final TastingService tastings;
36 private final TripService trips;
37
38 ProgressService(AccountService accounts, CatalogService catalog,
39 TastingService tastings, TripService trips) {
40 this.accounts = accounts;
41 this.catalog = catalog;
42 this.tastings = tastings;
43 this.trips = trips;
44 }
45
46 @Transactional(readOnly = true)
47 public DashboardView dashboard(UUID userId) {
48 Optional<TripView> nextTrip = trips.next(userId);
49 return new DashboardView(
50 accounts.get(userId), snapshot(userId, nextTrip.map(trip -> trip.destination().code()).orElse(null)),
51 nextTrip.orElse(null)
52 );
53 }
54
55 @Transactional(readOnly = true)
56 public TasteSnapshot snapshot(UUID userId) {
57 return snapshot(userId, null);
58 }
59
60 @Transactional(readOnly = true)
61 public DestinationProgressDetails destination(UUID userId, String code) {
62 DestinationDetails details = catalog.destination(code);
63 Set<String> tried = tastings.triedDishSlugs(userId);
64 DestinationProgress progress = progress(details.destination(), details.dishes(), tried);
65 return new DestinationProgressDetails(
66 progress,
67 details.dishes().stream()
68 .map(dish -> new DishProgress(dish, tried.contains(dish.slug())))
69 .toList()
70 );
71 }
72
73 private TasteSnapshot snapshot(UUID userId, String priorityDestination) {
74 Set<String> tried = tastings.triedDishSlugs(userId);
75 List<DishView> allDishes = catalog.allDishes();
76 Map<String, List<DishView>> dishesByDestination = allDishes.stream()
77 .collect(Collectors.groupingBy(DishView::destinationCode));
78
79 List<DestinationProgress> destinationProgress = catalog.destinations().stream()
80 .map(destination -> progress(
81 destination,
82 dishesByDestination.getOrDefault(destination.code(), List.of()),
83 tried
84 ))
85 .toList();
86
87 Set<String> tastedDestinations = allDishes.stream()
88 .filter(dish -> tried.contains(dish.slug()))
89 .map(DishView::destinationCode)
90 .collect(Collectors.toSet());
91 Map<String, DestinationSummary> destinationsByCode = catalog.destinations().stream()
92 .collect(Collectors.toMap(DestinationSummary::code, Function.identity()));
93 int regions = tastedDestinations.stream()
94 .map(destinationsByCode::get)
95 .filter(java.util.Objects::nonNull)
96 .map(DestinationSummary::regionName)
97 .collect(Collectors.toSet())
98 .size();
99
100 int totalWeight = allDishes.stream().mapToInt(DishView::importance).sum();
101 int triedWeight = allDishes.stream()
102 .filter(dish -> tried.contains(dish.slug()))
103 .mapToInt(DishView::importance)
104 .sum();
105 TasteStats stats = new TasteStats(
106 tastings.count(userId), tried.size(), tastedDestinations.size(), regions,
107 percentage(triedWeight, totalWeight)
108 );
109
110 return new TasteSnapshot(
111 stats,
112 destinationProgress,
113 importantMissing(allDishes, tried, tastedDestinations, priorityDestination),
114 tastings.recent(userId)
115 );
116 }
117
118 private DestinationProgress progress(DestinationSummary destination, List<DishView> dishes,
119 Set<String> tried) {
120 int totalWeight = dishes.stream().mapToInt(DishView::importance).sum();
121 int triedWeight = dishes.stream()
122 .filter(dish -> tried.contains(dish.slug()))
123 .mapToInt(DishView::importance)
124 .sum();
125 int triedCount = (int) dishes.stream().filter(dish -> tried.contains(dish.slug())).count();
126 return new DestinationProgress(
127 destination, percentage(triedWeight, totalWeight), triedCount, dishes.size()
128 );
129 }
130
131 private List<DishView> importantMissing(List<DishView> allDishes, Set<String> tried,
132 Set<String> tastedDestinations, String priorityDestination) {
133 List<DishView> candidates = allDishes.stream()
134 .filter(dish -> !tried.contains(dish.slug()))
135 .sorted(Comparator
136 .comparingInt((DishView dish) -> missingScore(dish, tastedDestinations, priorityDestination))
137 .reversed()
138 .thenComparing(DishView::name))
139 .toList();
140
141 List<DishView> selected = new ArrayList<>();
142 Set<String> usedDestinations = new LinkedHashSet<>();
143 for (DishView dish : candidates) {
144 if (usedDestinations.add(dish.destinationCode())) {
145 selected.add(dish);
146 }
147 if (selected.size() == MISSING_LIMIT) {
148 return selected;
149 }
150 }
151 for (DishView dish : candidates) {
152 if (!selected.contains(dish)) {
153 selected.add(dish);
154 }
155 if (selected.size() == MISSING_LIMIT) {
156 break;
157 }
158 }
159 return selected;
160 }
161
162 private int missingScore(DishView dish, Set<String> tastedDestinations, String priorityDestination) {
163 int score = dish.importance() * 10;
164 if (tastedDestinations.contains(dish.destinationCode())) {
165 score += 100;
166 }
167 if (dish.destinationCode().equals(priorityDestination)) {
168 score += 200;
169 }
170 return score;
171 }
172
173 private int percentage(int part, int whole) {
174 return whole == 0 ? 0 : (int) Math.round(part * 100.0 / whole);
175 }
176 }
177