refactor(dtos): convert all DTOs to Java records for native-mode Jackson
CI / Build Native (push) Successful in 6m23s

En Quarkus native-image, Jackson no introspecta public fields de clases
con normales (deja de refleccionar field metadata). Salia error:

  No serializer found for class com.l2.shots.auth.AuthMeResponse and
  no properties discovered to create BeanSerializer

Los records tienen auto-accessors (id(), username(), etc) que Jackson
serializa nativamente, sin necesidad de reflection o
@RegisterForReflection.

Convertidos:
- auth: AuthMeResponse, AdminUserSummary, Credentials,
         ChangePasswordRequest, AdminResetPasswordRequest, ErrorBody,
         MustChangeBody
- history: RunSummary, RunDetails, RunItem, RunSnapshot, RunIn,
           HistoryStats
- state: AppState (con Insumos y FormulaDto anidados)

Callers actualizados para usar accessors en vez de field access:
- AuthService.listUsersForAdmin -> AdminUserSummary.from
- AuthService.saveRun -> RunSummary.from / RunDetails.of
- AuthResource.register/login -> creds.username() / creds.password()
- HistoryResource.saveRun -> input.items() etc

Notas:
- RunDetails NO puede extender RunSummary en records (JLS no permite
  extends entre records). Va como record independiente con todos los
  campos. El JSON que produce matchea la interface RunDetails del
  frontend (que extendia RunSummary).
- HistoryStats ahora se construye de una sola vez al final de
  computeStats en lugar de ir mutando campos.
- MustChangeBody quedo con un constructor no-canonico no-arg para
  mantener el call site original (new MustChangeBody()).
This commit is contained in:
2026-08-15 00:46:19 -04:00
parent 89a224ee0c
commit 017c58a2b2
18 changed files with 173 additions and 244 deletions
@@ -1,6 +1,4 @@
package com.l2.shots.auth;
public class AdminResetPasswordRequest {
public String username;
public String newPassword;
public record AdminResetPasswordRequest(String username, String newPassword) {
}
@@ -3,22 +3,21 @@ package com.l2.shots.auth;
import java.time.Instant;
import java.util.UUID;
public class AdminUserSummary {
public UUID id;
public String username;
public Instant createdAt;
public Instant lastLoginAt;
public boolean isAdmin;
public boolean mustChangePassword;
public record AdminUserSummary(
UUID id,
String username,
Instant createdAt,
Instant lastLoginAt,
boolean isAdmin,
boolean mustChangePassword) {
public AdminUserSummary() {}
public AdminUserSummary(User u) {
this.id = u.id;
this.username = u.username;
this.createdAt = u.createdAt;
this.lastLoginAt = u.lastLoginAt;
this.isAdmin = u.isAdmin;
this.mustChangePassword = u.mustChangePassword;
public static AdminUserSummary from(User u) {
return new AdminUserSummary(
u.id,
u.username,
u.createdAt,
u.lastLoginAt,
u.isAdmin,
u.mustChangePassword);
}
}
@@ -3,20 +3,10 @@ package com.l2.shots.auth;
import java.time.Instant;
import java.util.UUID;
public class AuthMeResponse {
public UUID id;
public String username;
public Instant createdAt;
public boolean mustChangePassword;
public boolean isAdmin;
public AuthMeResponse() {}
public AuthMeResponse(UUID id, String username, Instant createdAt, boolean mustChangePassword, boolean isAdmin) {
this.id = id;
this.username = username;
this.createdAt = createdAt;
this.mustChangePassword = mustChangePassword;
this.isAdmin = isAdmin;
}
public record AuthMeResponse(
UUID id,
String username,
Instant createdAt,
boolean mustChangePassword,
boolean isAdmin) {
}
@@ -42,7 +42,7 @@ public class AuthResource {
@POST
@Path("/register")
public Response register(Credentials creds) {
Optional<User> result = authService.register(creds.username, creds.password);
Optional<User> result = authService.register(creds.username(), creds.password());
if (result.isEmpty()) {
return Response.status(409)
.entity(new ErrorBody("username no disponible o datos inválidos"))
@@ -58,7 +58,7 @@ public class AuthResource {
@POST
@Path("/login")
public Response login(Credentials creds) {
Optional<User> result = authService.authenticate(creds.username, creds.password);
Optional<User> result = authService.authenticate(creds.username(), creds.password());
if (result.isEmpty()) {
return Response.status(401)
.entity(new ErrorBody("credenciales inválidas"))
@@ -83,7 +83,7 @@ public class AuthResource {
@Path("/change-password")
@Authenticated
public Response changePassword(@Context HttpHeaders headers, ChangePasswordRequest body) {
if (body == null || body.currentPassword == null || body.newPassword == null) {
if (body == null || body.currentPassword() == null || body.newPassword() == null) {
return Response.status(400).entity(new ErrorBody("Faltan campos requeridos")).build();
}
Optional<JsonWebToken> jwt = jwtCookieAuth.extractToken(headers);
@@ -91,7 +91,7 @@ public class AuthResource {
Optional<User> user = authService.getEntityFromToken(jwt.get());
if (user.isEmpty()) return Response.status(401).build();
AuthService.ChangePasswordResult result = authService.changePassword(
user.get().id, body.currentPassword, body.newPassword);
user.get().id, body.currentPassword(), body.newPassword());
switch (result) {
case OK:
User refreshed = User.findById(user.get().id);
@@ -116,7 +116,7 @@ public class AuthResource {
@Path("/admin/reset-password")
@RolesAllowed("admin")
public Response adminResetPassword(AdminResetPasswordRequest body) {
if (body == null || body.username == null || body.newPassword == null) {
if (body == null || body.username() == null || body.newPassword() == null) {
return Response.status(400).entity(new ErrorBody("Faltan campos requeridos")).build();
}
UUID requesterId;
@@ -125,7 +125,7 @@ public class AuthResource {
} catch (Exception e) {
return Response.status(401).build();
}
boolean ok = authService.resetPasswordAsAdmin(requesterId, body.username, body.newPassword);
boolean ok = authService.resetPasswordAsAdmin(requesterId, body.username(), body.newPassword());
if (!ok) {
return Response.status(400).entity(new ErrorBody("Usuario no encontrado o contraseña inválida")).build();
}
@@ -180,9 +180,6 @@ public class AuthResource {
.build();
}
public static class ErrorBody {
public String error;
public ErrorBody() {}
public ErrorBody(String error) { this.error = error; }
public record ErrorBody(String error) {
}
}
@@ -77,7 +77,7 @@ public class AuthService {
public java.util.List<AdminUserSummary> listUsersForAdmin() {
return User.<User>listAll().stream()
.map(AdminUserSummary::new)
.map(AdminUserSummary::from)
.collect(java.util.stream.Collectors.toList());
}
@@ -1,6 +1,4 @@
package com.l2.shots.auth;
public class ChangePasswordRequest {
public String currentPassword;
public String newPassword;
public record ChangePasswordRequest(String currentPassword, String newPassword) {
}
@@ -1,6 +1,4 @@
package com.l2.shots.auth;
public class Credentials {
public String username;
public String password;
public record Credentials(String username, String password) {
}
@@ -60,8 +60,9 @@ public class MustChangePasswordFilter implements ContainerRequestFilter {
.build());
}
public static class MustChangeBody {
public String error = "Debe cambiar la contraseña antes de continuar";
public boolean mustChangePassword = true;
public record MustChangeBody(String error, boolean mustChangePassword) {
public MustChangeBody() {
this("Debe cambiar la contraseña antes de continuar", true);
}
}
}
@@ -36,16 +36,16 @@ public class HistoryResource {
Optional<UUID> userId = extractUserId(headers);
if (userId.isEmpty()) return Response.status(401).build();
if (input == null || input.items == null || input.snapshot == null) {
if (input == null || input.items() == null || input.snapshot() == null) {
return Response.status(400).entity("{\"error\":\"payload inválido\"}").build();
}
if (input.items.size() > 100) {
if (input.items().size() > 100) {
return Response.status(400).entity("{\"error\":\"demasiados items\"}").build();
}
if (input.label != null && input.label.length() > 100) {
if (input.label() != null && input.label().length() > 100) {
return Response.status(400).entity("{\"error\":\"label demasiado largo\"}").build();
}
if (input.totalCristalesUsed <= 0) {
if (input.totalCristalesUsed() <= 0) {
return Response.status(400).entity("{\"error\":\"no hay cristales usados\"}").build();
}
@@ -18,7 +18,7 @@ public class HistoryService {
public List<RunSummary> listForUser(UUID userId) {
return ProductionRun.<ProductionRun>list("userId = ?1 ORDER BY createdAt DESC", userId)
.stream()
.map(RunSummary::new)
.map(RunSummary::from)
.toList();
}
@@ -29,7 +29,7 @@ public class HistoryService {
List<RunItem> items = mapper.readValue(entity.itemsJson, mapper.getTypeFactory()
.constructCollectionType(List.class, RunItem.class));
RunSnapshot snapshot = mapper.readValue(entity.snapshotJson, RunSnapshot.class);
return Optional.of(new RunDetails(entity, items, snapshot));
return Optional.of(RunDetails.of(entity, items, snapshot));
} catch (JsonProcessingException e) {
return Optional.empty();
}
@@ -41,21 +41,21 @@ public class HistoryService {
entity.id = UUID.randomUUID();
entity.userId = userId;
entity.createdAt = Instant.now();
entity.label = input.label;
entity.totalCost = input.totalCost;
entity.totalSale = input.totalSale;
entity.totalProfit = input.totalProfit;
entity.totalShots = input.totalShots;
entity.totalCristalesUsed = input.totalCristalesUsed;
entity.totalOreUsed = input.totalOreUsed;
entity.label = input.label();
entity.totalCost = input.totalCost();
entity.totalSale = input.totalSale();
entity.totalProfit = input.totalProfit();
entity.totalShots = input.totalShots();
entity.totalCristalesUsed = input.totalCristalesUsed();
entity.totalOreUsed = input.totalOreUsed();
try {
entity.itemsJson = mapper.writeValueAsString(input.items);
entity.snapshotJson = mapper.writeValueAsString(input.snapshot);
entity.itemsJson = mapper.writeValueAsString(input.items());
entity.snapshotJson = mapper.writeValueAsString(input.snapshot());
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize run payload", e);
}
entity.persist();
return new RunSummary(entity);
return RunSummary.from(entity);
}
@Transactional
@@ -67,22 +67,8 @@ public class HistoryService {
List<ProductionRun> runs = ProductionRun.list(
"userId = ?1 ORDER BY createdAt DESC", userId);
HistoryStats stats = new HistoryStats();
stats.totalRuns = runs.size();
if (runs.isEmpty()) {
stats.totalCost = 0;
stats.totalSale = 0;
stats.totalProfit = 0;
stats.totalShots = 0;
stats.avgProfit = 0;
stats.avgCost = 0;
stats.avgSale = 0;
stats.bestRun = null;
stats.worstRun = null;
stats.last5Avg = 0;
stats.last10Avg = 0;
return stats;
return new HistoryStats(0, 0, 0, 0, 0, 0, 0, 0, null, null, 0, 0);
}
long totalCost = 0;
@@ -101,27 +87,28 @@ public class HistoryService {
if (r.totalProfit < worst.totalProfit) worst = r;
}
stats.totalCost = totalCost;
stats.totalSale = totalSale;
stats.totalProfit = totalProfit;
stats.totalShots = totalShots;
stats.avgProfit = totalProfit / runs.size();
stats.avgCost = totalCost / runs.size();
stats.avgSale = totalSale / runs.size();
stats.bestRun = new RunSummary(best);
stats.worstRun = new RunSummary(worst);
int n5 = Math.min(5, runs.size());
int n10 = Math.min(10, runs.size());
int n = runs.size();
int n5 = Math.min(5, n);
int n10 = Math.min(10, n);
long sum5 = 0;
long sum10 = 0;
for (int i = 0; i < n10; i++) {
sum10 += runs.get(i).totalProfit;
if (i < n5) sum5 += runs.get(i).totalProfit;
}
stats.last5Avg = n5 > 0 ? sum5 / n5 : 0;
stats.last10Avg = n10 > 0 ? sum10 / n10 : 0;
return stats;
return new HistoryStats(
n,
totalCost,
totalSale,
totalProfit,
totalShots,
totalProfit / n,
totalCost / n,
totalSale / n,
RunSummary.from(best),
RunSummary.from(worst),
n5 > 0 ? sum5 / n5 : 0,
n10 > 0 ? sum10 / n10 : 0);
}
}
@@ -1,16 +1,16 @@
package com.l2.shots.history;
public class HistoryStats {
public int totalRuns;
public long totalCost;
public long totalSale;
public long totalProfit;
public long totalShots;
public long avgProfit;
public long avgCost;
public long avgSale;
public RunSummary bestRun;
public RunSummary worstRun;
public long last5Avg;
public long last10Avg;
public record HistoryStats(
int totalRuns,
long totalCost,
long totalSale,
long totalProfit,
long totalShots,
long avgProfit,
long avgCost,
long avgSale,
RunSummary bestRun,
RunSummary worstRun,
long last5Avg,
long last10Avg) {
}
@@ -1,16 +1,34 @@
package com.l2.shots.history;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
public class RunDetails extends RunSummary {
public List<RunItem> items;
public RunSnapshot snapshot;
public record RunDetails(
UUID id,
Instant createdAt,
String label,
long totalCost,
long totalSale,
long totalProfit,
long totalShots,
long totalCristalesUsed,
long totalOreUsed,
List<RunItem> items,
RunSnapshot snapshot) {
public RunDetails() {}
public RunDetails(ProductionRun r, List<RunItem> items, RunSnapshot snapshot) {
super(r);
this.items = items;
this.snapshot = snapshot;
public static RunDetails of(ProductionRun r, List<RunItem> items, RunSnapshot snapshot) {
return new RunDetails(
r.id,
r.createdAt,
r.label,
r.totalCost,
r.totalSale,
r.totalProfit,
r.totalShots,
r.totalCristalesUsed,
r.totalOreUsed,
items,
snapshot);
}
}
+10 -10
View File
@@ -2,14 +2,14 @@ package com.l2.shots.history;
import java.util.List;
public class RunIn {
public String label;
public long totalCost;
public long totalSale;
public long totalProfit;
public long totalShots;
public long totalCristalesUsed;
public long totalOreUsed;
public List<RunItem> items;
public RunSnapshot snapshot;
public record RunIn(
String label,
long totalCost,
long totalSale,
long totalProfit,
long totalShots,
long totalCristalesUsed,
long totalOreUsed,
List<RunItem> items,
RunSnapshot snapshot) {
}
+11 -28
View File
@@ -1,31 +1,14 @@
package com.l2.shots.history;
public class RunItem {
public String tipo;
public String grado;
public int cristalesDisponibles;
public int cristalesUsados;
public int oreNecesario;
public int crafteosPosibles;
public int shotsObtenidos;
public long costoTotal;
public long valorVenta;
public long ganancia;
public RunItem() {}
public RunItem(String tipo, String grado, int cristalesDisponibles, int cristalesUsados,
int oreNecesario, int crafteosPosibles, int shotsObtenidos,
long costoTotal, long valorVenta, long ganancia) {
this.tipo = tipo;
this.grado = grado;
this.cristalesDisponibles = cristalesDisponibles;
this.cristalesUsados = cristalesUsados;
this.oreNecesario = oreNecesario;
this.crafteosPosibles = crafteosPosibles;
this.shotsObtenidos = shotsObtenidos;
this.costoTotal = costoTotal;
this.valorVenta = valorVenta;
this.ganancia = ganancia;
}
public record RunItem(
String tipo,
String grado,
int cristalesDisponibles,
int cristalesUsados,
int oreNecesario,
int crafteosPosibles,
int shotsObtenidos,
long costoTotal,
long valorVenta,
long ganancia) {
}
@@ -3,14 +3,7 @@ package com.l2.shots.history;
import java.util.List;
import java.util.Map;
public class RunSnapshot {
public Map<String, Object> insumos;
public List<Map<String, Object>> formulas;
public RunSnapshot() {}
public RunSnapshot(Map<String, Object> insumos, List<Map<String, Object>> formulas) {
this.insumos = insumos;
this.formulas = formulas;
}
public record RunSnapshot(
Map<String, Object> insumos,
List<Map<String, Object>> formulas) {
}
@@ -3,28 +3,27 @@ package com.l2.shots.history;
import java.time.Instant;
import java.util.UUID;
public class RunSummary {
public UUID id;
public Instant createdAt;
public String label;
public long totalCost;
public long totalSale;
public long totalProfit;
public long totalShots;
public long totalCristalesUsed;
public long totalOreUsed;
public record RunSummary(
UUID id,
Instant createdAt,
String label,
long totalCost,
long totalSale,
long totalProfit,
long totalShots,
long totalCristalesUsed,
long totalOreUsed) {
public RunSummary() {}
public RunSummary(ProductionRun r) {
this.id = r.id;
this.createdAt = r.createdAt;
this.label = r.label;
this.totalCost = r.totalCost;
this.totalSale = r.totalSale;
this.totalProfit = r.totalProfit;
this.totalShots = r.totalShots;
this.totalCristalesUsed = r.totalCristalesUsed;
this.totalOreUsed = r.totalOreUsed;
public static RunSummary from(ProductionRun r) {
return new RunSummary(
r.id,
r.createdAt,
r.label,
r.totalCost,
r.totalSale,
r.totalProfit,
r.totalShots,
r.totalCristalesUsed,
r.totalOreUsed);
}
}
+16 -48
View File
@@ -3,57 +3,25 @@ package com.l2.shots.state;
import java.util.List;
import java.util.Map;
public class AppState {
public record AppState(
Insumos insumos,
List<FormulaDto> formulas,
Map<String, Map<String, Integer>> disponibles) {
public Insumos insumos;
public List<FormulaDto> formulas;
public Map<String, Map<String, Integer>> disponibles;
public AppState() {}
public AppState(Insumos insumos, List<FormulaDto> formulas, Map<String, Map<String, Integer>> disponibles) {
this.insumos = insumos;
this.formulas = formulas;
this.disponibles = disponibles;
}
public static class Insumos {
public Map<String, Integer> cristales;
public int soulOre;
public int spiritOre;
public Map<String, Map<String, Integer>> venta;
public Insumos() {}
public Insumos(Map<String, Integer> cristales, int soulOre, int spiritOre,
public record Insumos(
Map<String, Integer> cristales,
int soulOre,
int spiritOre,
Map<String, Map<String, Integer>> venta) {
this.cristales = cristales;
this.soulOre = soulOre;
this.spiritOre = spiritOre;
this.venta = venta;
}
}
public static class FormulaDto {
public String id;
public String tipo;
public String grado;
public int cristalesReq;
public Integer soulOreReq;
public Integer spiritOreReq;
public int shotsObtenidos;
public FormulaDto() {}
public FormulaDto(String id, String tipo, String grado, int cristalesReq,
Integer soulOreReq, Integer spiritOreReq, int shotsObtenidos) {
this.id = id;
this.tipo = tipo;
this.grado = grado;
this.cristalesReq = cristalesReq;
this.soulOreReq = soulOreReq;
this.spiritOreReq = spiritOreReq;
this.shotsObtenidos = shotsObtenidos;
}
public record FormulaDto(
String id,
String tipo,
String grado,
int cristalesReq,
Integer soulOreReq,
Integer spiritOreReq,
int shotsObtenidos) {
}
}
@@ -43,10 +43,10 @@ public class StateResource {
Optional<UUID> userId = extractUserId(headers);
if (userId.isEmpty()) return Response.status(401).build();
if (state == null || state.insumos == null || state.formulas == null || state.disponibles == null) {
if (state == null || state.insumos() == null || state.formulas() == null || state.disponibles() == null) {
return Response.status(400).entity("{\"error\":\"estado inválido\"}").build();
}
if (state.formulas.size() > 200) {
if (state.formulas().size() > 200) {
return Response.status(400).entity("{\"error\":\"estado demasiado grande\"}").build();
}
stateService.saveForUser(userId.get(), state);