From 77e279fca23bf577c46a9467bff734044f896d39 Mon Sep 17 00:00:00 2001 From: LuisDev18 Date: Sun, 3 Aug 2025 12:35:24 -0500 Subject: [PATCH 1/4] feat: Query hint implement --- Dockerfile | 4 ++-- .../pe/edu/utp/repository/ArticuloRepository.java | 11 +++++++++++ .../pe/edu/utp/security/SecurityConfiguration.java | 4 ---- src/main/resources/application.properties | 3 ++- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index e045243..42a044d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ -FROM eclipse-temurin:17 +FROM eclipse-temurin:21 RUN apt-get update && apt-get -y install \ - openjdk-17-jdk \ + openjdk-21-jdk \ --no-install-recommends \ && rm -rf /var/lib/apt/lists/* diff --git a/src/main/java/pe/edu/utp/repository/ArticuloRepository.java b/src/main/java/pe/edu/utp/repository/ArticuloRepository.java index 4ceca81..93473b3 100644 --- a/src/main/java/pe/edu/utp/repository/ArticuloRepository.java +++ b/src/main/java/pe/edu/utp/repository/ArticuloRepository.java @@ -2,9 +2,11 @@ import java.util.List; +import jakarta.persistence.QueryHint; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.QueryHints; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @@ -21,6 +23,15 @@ public interface ArticuloRepository extends JpaRepository { AND (:precioMax IS NULL OR a.precio <= :precioMax) """ ) + @QueryHints( + { + @QueryHint(name = "org.hibernate.readOnly", value = "true"), + @QueryHint(name = "org.hibernate.fetchSize", value = "50"), + @QueryHint(name = "org.hibernate.cacheable", value = "true"), + @QueryHint(name = "jakarta.persistence.cache.retrieveMode", value = "USE"), + @QueryHint(name = "jakarta.persistence.cache.storeMode", value = "USE"), + } + ) List findByCategoriaAndMarcaAndPrecioBetween( @Param("categoria") String categoria, @Param("marca") String marca, diff --git a/src/main/java/pe/edu/utp/security/SecurityConfiguration.java b/src/main/java/pe/edu/utp/security/SecurityConfiguration.java index 2765bc9..8a3be9c 100644 --- a/src/main/java/pe/edu/utp/security/SecurityConfiguration.java +++ b/src/main/java/pe/edu/utp/security/SecurityConfiguration.java @@ -2,7 +2,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; @@ -26,9 +25,6 @@ public class SecurityConfiguration { private final JwtAuthenticationFilter jwtAuthenticationFilter; - @Value("${spring.security.disabled:false}") - private boolean disabledSecurity; - /* * Sprign security detecta un bean UserDetailsService y un PasswordEncoder, * y automaticamente va a configurar una instancia de DaoAuthenticationProvider diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index db172be..a8219f1 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -12,6 +12,8 @@ spring.datasource.url=jdbc:mysql://${DATASOURCE_HOSTNAME}:${DATASOURCE_PORT}/${D spring.datasource.username=${DATASOURCE_USERNAME} spring.datasource.password=${DATASOURCE_PASSWORD} spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect +#Util para ver info relacionado a tiempos q tarda una consulta +#spring.jpa.properties.hibernate.generate_statistics=true spring.jpa.hibernate.ddl-auto=none logging.level.root=INFO @@ -19,7 +21,6 @@ logging.level.org.hibernate.type=INFO logging.level.web=INFO spring.flyway.baseline-on-migrate=true -spring.security.disabled=${SECURITY_DISABLED:false} From 03df1c999573494abdb7b1a6d663325a8c019f4e Mon Sep 17 00:00:00 2001 From: LuisDev18 Date: Sun, 3 Aug 2025 12:41:39 -0500 Subject: [PATCH 2/4] feat:Add convertUilt by log info --- .../edu/utp/controller/ArticuloController.java | 2 ++ src/main/java/pe/edu/utp/util/ConvertUtil.java | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 src/main/java/pe/edu/utp/util/ConvertUtil.java diff --git a/src/main/java/pe/edu/utp/controller/ArticuloController.java b/src/main/java/pe/edu/utp/controller/ArticuloController.java index d31aa28..15b53aa 100644 --- a/src/main/java/pe/edu/utp/controller/ArticuloController.java +++ b/src/main/java/pe/edu/utp/controller/ArticuloController.java @@ -26,6 +26,7 @@ import pe.edu.utp.exception.NoDataFoundException; import pe.edu.utp.service.ArticuloService; import pe.edu.utp.util.ApiResponse; +import pe.edu.utp.util.ConvertUtil; @RestController @RequestMapping("/articulos") @@ -79,6 +80,7 @@ public ResponseEntity update( @PathVariable("id") Integer id, @Valid @RequestBody ArticuloDto articuloDto ) { + log.info("Article update object: {}",ConvertUtil.jsonAsString(articuloDto)); Articulo articuloUpdate = articuloService.update(articuloDto, id); return ResponseEntity.ok(articuloUpdate); } diff --git a/src/main/java/pe/edu/utp/util/ConvertUtil.java b/src/main/java/pe/edu/utp/util/ConvertUtil.java new file mode 100644 index 0000000..f356b8e --- /dev/null +++ b/src/main/java/pe/edu/utp/util/ConvertUtil.java @@ -0,0 +1,18 @@ +package pe.edu.utp.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class ConvertUtil { + + private ConvertUtil() {} + + public static String jsonAsString(Object obj) { + try { + return new ObjectMapper().writeValueAsString(obj); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + return null; + } +} From 155aaf71d7f5d424ee648e960972e66265f1676d Mon Sep 17 00:00:00 2001 From: LuisDev18 Date: Sun, 3 Aug 2025 12:44:05 -0500 Subject: [PATCH 3/4] fix: spotless format error solution --- src/main/java/pe/edu/utp/controller/ArticuloController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/pe/edu/utp/controller/ArticuloController.java b/src/main/java/pe/edu/utp/controller/ArticuloController.java index 15b53aa..707c16c 100644 --- a/src/main/java/pe/edu/utp/controller/ArticuloController.java +++ b/src/main/java/pe/edu/utp/controller/ArticuloController.java @@ -80,7 +80,7 @@ public ResponseEntity update( @PathVariable("id") Integer id, @Valid @RequestBody ArticuloDto articuloDto ) { - log.info("Article update object: {}",ConvertUtil.jsonAsString(articuloDto)); + log.info("Article update object: {}", ConvertUtil.jsonAsString(articuloDto)); Articulo articuloUpdate = articuloService.update(articuloDto, id); return ResponseEntity.ok(articuloUpdate); } From c6bb7eb7ea8fa538a1ddac2fc9faa0f604adc3a0 Mon Sep 17 00:00:00 2001 From: LuisDev18 Date: Sun, 10 Aug 2025 11:45:33 -0500 Subject: [PATCH 4/4] feat: implement stock update and total price calculation, extract email from jwt, and add user association --- Dockerfile | 56 ++++++++++++++++--- build.gradle | 23 +++++--- buildspec.yml | 36 ++++++++++++ docker-compose.yml | 2 +- .../utp/controller/ArticuloController.java | 26 ++++++++- src/main/java/pe/edu/utp/entity/Articulo.java | 17 ++++++ src/main/java/pe/edu/utp/entity/Usuario.java | 2 + .../utp/repository/ArticuloRepository.java | 7 +++ .../java/pe/edu/utp/security/JwtService.java | 48 +++++++++++----- .../pe/edu/utp/service/ArticuloService.java | 4 +- .../edu/utp/service/ArticuloServiceImpl.java | 20 ++++++- src/main/resources/application.properties | 1 + .../V20250805110000__create_sp_articles.sql | 11 ++++ ...150000__create_function_update_article.sql | 12 ++++ .../V20250806224500__add_country_column.sql | 2 + .../V20250809183000__add_user_fk.sql | 5 ++ .../edu/utp/ArticulosapiApplicationTests.java | 8 +-- 17 files changed, 236 insertions(+), 44 deletions(-) create mode 100644 buildspec.yml create mode 100644 src/main/resources/db/migration/V20250805110000__create_sp_articles.sql create mode 100644 src/main/resources/db/migration/V20250805150000__create_function_update_article.sql create mode 100644 src/main/resources/db/migration/V20250806224500__add_country_column.sql create mode 100644 src/main/resources/db/migration/V20250809183000__add_user_fk.sql diff --git a/Dockerfile b/Dockerfile index 42a044d..a8b7e29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,54 @@ -FROM eclipse-temurin:21 +# ----------------------------------------------------------- +# Etapa 1: Builder - Usamos el JDK para compilar la aplicación con Gradle +# ----------------------------------------------------------- +FROM eclipse-temurin:21-jdk-jammy AS builder -RUN apt-get update && apt-get -y install \ - openjdk-21-jdk \ - --no-install-recommends \ - && rm -rf /var/lib/apt/lists/* +# Establece el directorio de trabajo +WORKDIR /app + +# Copia los archivos del build de Gradle +# Copia el wrapper de Gradle +COPY gradle ./gradle +# Copia los archivos de configuración de Gradle +COPY gradlew build.gradle settings.gradle ./ + +# Copia los archivos de código fuente +COPY src ./src + +# Otorga permisos de ejecución al wrapper de Gradle +RUN chmod +x ./gradlew + +# Compila y empaqueta la aplicación usando el wrapper de Gradle +# El task 'bootJar' es el estándar de Spring Boot para crear el JAR ejecutable +RUN ./gradlew bootJar +# ----------------------------------------------------------- +# Etapa 2: Desarrollo - Usamos el JDK para la ejecución +# ----------------------------------------------------------- +FROM eclipse-temurin:21-jdk-jammy AS development + +ARG APP_VERSION +# Metadatos OCI +LABEL org.opencontainers.image.title="articulos-api" \ + org.opencontainers.image.description="API de artículos para desarrollo en Java 21" \ + org.opencontainers.image.source="https://github.com/LuisDev18/SecurityTask" \ + org.opencontainers.image.version="${APP_VERSION}" + +# Crea un usuario no-root por seguridad +RUN groupadd -g 10001 app && useradd -u 10000 -g app -s /usr/sbin/nologin -m app + +# Establece el directorio de trabajo para la aplicación WORKDIR /app -COPY build/libs/*.jar /app/*.jar +# Copia el archivo JAR compilado desde la etapa 'builder' +# El JAR se encuentra en build/libs por defecto +COPY --from=builder /app/build/libs/*.jar /app/app.jar + +# Expone el puerto por defecto de Spring Boot +EXPOSE 8080 + +# Cambia a usuario no-root para ejecutar la aplicación +USER app -ENTRYPOINT ["java", "-jar", "articulosapi.jar"] +# Comando para ejecutar la aplicación +ENTRYPOINT ["java", "-jar", "/app/app.jar"] diff --git a/build.gradle b/build.gradle index d3a1b22..86094db 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.5.3' // Actualizado a 3.5.3 id 'io.spring.dependency-management' version '1.1.0' - id 'com.diffplug.spotless' version '7.1.0' // Generalmente compatible con Spring Boot 3.x + id 'com.diffplug.spotless' version '7.1.0' } group = 'pe.edu.utp' @@ -15,6 +15,7 @@ configurations { } } + spotless { java { // Configuración de Prettier para Java @@ -44,22 +45,26 @@ repositories { } dependencies { + // Update the MySQL connector to the latest version + implementation 'com.mysql:mysql-connector-j:8.0.33' + runtimeOnly 'com.mysql:mysql-connector-j:8.0.33' + + // Keep your existing Flyway dependencies + implementation 'org.flywaydb:flyway-core:9.16.1' + implementation 'org.flywaydb:flyway-mysql:9.16.1' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'mysql:mysql-connector-java:8.0.30' // Actualizado a una versión más reciente implementation 'org.springframework.boot:spring-boot-starter-websocket' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' - implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8' - implementation 'org.springframework.boot:spring-boot-starter-cache' - - //Dependencia para migraciones de base de datos - implementation 'org.flywaydb:flyway-core:9.16.1' // Actualizado a una versión más reciente - implementation 'org.flywaydb:flyway-mysql:9.16.1' // Actualizado a una versión más reciente - + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' + + // Spring Batch dependencies + implementation 'org.springframework.boot:spring-boot-starter-batch' // Dependencias de Lombok y MapStruct para el procesamiento de anotaciones compileOnly 'org.projectlombok:lombok' diff --git a/buildspec.yml b/buildspec.yml new file mode 100644 index 0000000..9f78f1d --- /dev/null +++ b/buildspec.yml @@ -0,0 +1,36 @@ +version: 0.2 + +phases: + pre_build: + commands: + - echo Running Gradle build... + - ./gradlew clean build + + - echo Logging in to Amazon ECR... + - aws --version + - REPOSITORY_URI=235763262026.dkr.ecr.us-east-1.amazonaws.com/palcos-images-repository + - aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $REPOSITORY_URI + - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7) + - IMAGE_TAG=build-$(echo $CODEBUILD_BUILD_ID | awk -F":" '{print $2}') + build: + commands: + - echo Build started on `date` + - echo Building the Docker image... + - docker build --build-arg APP_VERSION=$IMAGE_VERSION -t $IMAGE_TAG . + post_build: + commands: + - echo Build completed on `date` + - echo Pushing the Docker images... + - docker push $REPOSITORY_URI:$IMAGE_TAG + - echo Writing image definitions file... + # This part is for CodeDeploy to know which container to update. + - DOCKER_CONTAINER_NAME=palcos-images-repository + - printf '[{"name":"%s","imageUri":"%s"}]' $DOCKER_CONTAINER_NAME $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json + - echo printing imagedefinitions.json + - cat imagedefinitions.json + +artifacts: + files: + - imagedefinitions.json + # The path to your JAR file needs to be updated for Gradle. + - build/libs/articulosapi.jar \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 8cefac9..fa36699 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: - DATASOURCE_USERNAME=root - DATASOURCE_PASSWORD=admin env_file: - - .env.keypair + - .env.development depends_on: - mysqldb diff --git a/src/main/java/pe/edu/utp/controller/ArticuloController.java b/src/main/java/pe/edu/utp/controller/ArticuloController.java index 707c16c..aba5ae1 100644 --- a/src/main/java/pe/edu/utp/controller/ArticuloController.java +++ b/src/main/java/pe/edu/utp/controller/ArticuloController.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Map; +import io.swagger.v3.oas.annotations.Parameter; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -16,6 +17,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -24,6 +26,7 @@ import pe.edu.utp.dto.ArticuloResponseDto; import pe.edu.utp.entity.Articulo; import pe.edu.utp.exception.NoDataFoundException; +import pe.edu.utp.security.JwtService; import pe.edu.utp.service.ArticuloService; import pe.edu.utp.util.ApiResponse; import pe.edu.utp.util.ConvertUtil; @@ -35,6 +38,7 @@ public class ArticuloController { private final ArticuloService articuloService; + private final JwtService jwtService; @GetMapping public ResponseEntity>> getAll( @@ -70,8 +74,12 @@ public ResponseEntity> findById(@PathVariable(" } @PostMapping - public ResponseEntity> create(@Valid @RequestBody ArticuloDto articuloDto) { - Articulo registro = articuloService.save(articuloDto); + public ResponseEntity> create( + @Valid @RequestBody ArticuloDto articuloDto, + @Parameter(hidden = true) @RequestHeader(value = "Authorization") String bearerToken + ) { + var email = jwtService.extractUsername(bearerToken); + Articulo registro = articuloService.save(articuloDto, email); return ApiResponse.created("Articulo creado exitosamente", registro).toResponseEntity(); } @@ -100,4 +108,18 @@ public ResponseEntity delete(@PathVariable("id") Integer id) articuloService.delete(id); return ResponseEntity.ok(null); } + + @PutMapping(value = "/new-stock/{productId}") + public ResponseEntity updateStokSp( + @PathVariable("productId") Integer productId, + @RequestParam Integer quantity + ) { + var articuloUpdate = articuloService.discountStoock(productId, quantity); + return ResponseEntity.ok(articuloUpdate); + } + + @GetMapping("/total-price/{productId}") + public Double getTotalPrice(@PathVariable("productId") Integer productId) { + return articuloService.calculateTotalPrice(productId); + } } diff --git a/src/main/java/pe/edu/utp/entity/Articulo.java b/src/main/java/pe/edu/utp/entity/Articulo.java index 594ff1e..f41d221 100644 --- a/src/main/java/pe/edu/utp/entity/Articulo.java +++ b/src/main/java/pe/edu/utp/entity/Articulo.java @@ -8,6 +8,11 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.NamedStoredProcedureQuery; +import jakarta.persistence.ParameterMode; +import jakarta.persistence.StoredProcedureParameter; import jakarta.persistence.Table; import jakarta.persistence.Temporal; import jakarta.persistence.TemporalType; @@ -20,6 +25,14 @@ import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +@NamedStoredProcedureQuery( + name = "updateStockProcedure", + procedureName = "update_stock", + parameters = { + @StoredProcedureParameter(mode = ParameterMode.IN, name = "productId", type = Integer.class), + @StoredProcedureParameter(mode = ParameterMode.IN, name = "quantity", type = Integer.class), + } +) @Entity @Table(name = "articulos") @Getter @@ -45,6 +58,10 @@ public class Articulo { private Integer stock; + @ManyToOne + @JoinColumn(name = "usuario_id") + private Usuario usuario; + @Column(name = "create_at", nullable = false, updatable = false) @Temporal(TemporalType.TIMESTAMP) @CreatedDate diff --git a/src/main/java/pe/edu/utp/entity/Usuario.java b/src/main/java/pe/edu/utp/entity/Usuario.java index bbe826c..74e9598 100644 --- a/src/main/java/pe/edu/utp/entity/Usuario.java +++ b/src/main/java/pe/edu/utp/entity/Usuario.java @@ -45,6 +45,8 @@ public class Usuario implements UserDetails { @Column(name = "rol", length = 20, nullable = false) private Rol rol; + private String country; + @Override public Collection getAuthorities() { List authorities = new ArrayList<>(); diff --git a/src/main/java/pe/edu/utp/repository/ArticuloRepository.java b/src/main/java/pe/edu/utp/repository/ArticuloRepository.java index 93473b3..1b554f5 100644 --- a/src/main/java/pe/edu/utp/repository/ArticuloRepository.java +++ b/src/main/java/pe/edu/utp/repository/ArticuloRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.QueryHints; +import org.springframework.data.jpa.repository.query.Procedure; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @@ -39,4 +40,10 @@ List findByCategoriaAndMarcaAndPrecioBetween( @Param("precioMax") Double precioMax, Pageable pageable ); + + @Procedure(name = "updateStockProcedure") + void updateStook(@Param("productId") Integer productId, @Param("quantity") Integer quantity); + + @Query(value = "Select get_total_price(:productId)", nativeQuery = true) + Double getTotalPrice(Integer productId); } diff --git a/src/main/java/pe/edu/utp/security/JwtService.java b/src/main/java/pe/edu/utp/security/JwtService.java index 1d43152..1f28f55 100644 --- a/src/main/java/pe/edu/utp/security/JwtService.java +++ b/src/main/java/pe/edu/utp/security/JwtService.java @@ -25,16 +25,17 @@ public class JwtService { private static final String TOKEN_HEADER = "Authorization"; private static final String TOKEN_PREFIX = "Bearer "; - public String extractUsername(String token) { - return extractClaim(token, claims -> claims.get("username", String.class)); + public String extractUsername(String tokenOrHeader) { + return extractClaim(tokenOrHeader, claims -> claims.get("username", String.class)); } - public T extractClaim(String token, Function claimsResolver) { - final Claims claims = extractAllClaims(token); + public T extractClaim(String tokenOrHeader, Function claimsResolver) { + final Claims claims = extractAllClaims(tokenOrHeader); return claimsResolver.apply(claims); } - private Claims extractAllClaims(String token) { + private Claims extractAllClaims(String tokenOrHeader) { + String token = sanitizeToken(tokenOrHeader); Claims claims = Jwts .parserBuilder() .setSigningKey(pemReader.getPublicKey()) @@ -45,6 +46,25 @@ private Claims extractAllClaims(String token) { return claims; } + // Quita "Bearer " (con espacio) usando substring y maneja espacios extra + private String sanitizeToken(String tokenOrHeader) { + if (tokenOrHeader == null) { + throw new IllegalArgumentException("JWT no puede ser null"); + } + String t = tokenOrHeader.trim(); + // Case-insensitive por si viene "bearer ..." + if ( + t.length() >= TOKEN_PREFIX.length() && + t.regionMatches(true, 0, TOKEN_PREFIX, 0, TOKEN_PREFIX.length()) + ) { + t = t.substring(TOKEN_PREFIX.length()).trim(); + } + if (t.isEmpty()) { + throw new IllegalArgumentException("JWT está vacío después de quitar el prefijo Bearer"); + } + return t; + } + public String generateToken(UserDetails userDetails) { return generateToken(new HashMap<>(), userDetails); } @@ -52,7 +72,7 @@ public String generateToken(UserDetails userDetails) { public String extractToken(HttpServletRequest request) { String bearerToken = request.getHeader(TOKEN_HEADER); if (bearerToken != null && bearerToken.startsWith(TOKEN_PREFIX)) { - return bearerToken.substring(TOKEN_PREFIX.length()); + return bearerToken.substring(TOKEN_PREFIX.length()).trim(); } return null; } @@ -77,7 +97,7 @@ public String generateToken(Map extraClaims, UserDetails userDet Claims claims = Jwts.claims(); claims.put("username", userDetails.getUsername()); claims.put("roles", userDetails.getAuthorities().toString()); - claims.putAll(extraClaims); // Agregar campos adicionales + claims.putAll(extraClaims); return Jwts .builder() @@ -89,20 +109,20 @@ public String generateToken(Map extraClaims, UserDetails userDet .compact(); } - public boolean isTokenValid(String token, UserDetails userDetails) { - final String username = extractUsername(token); - return (username.equals(userDetails.getUsername())) && !isTokenExpired(token); + public boolean isTokenValid(String tokenOrHeader, UserDetails userDetails) { + final String username = extractUsername(tokenOrHeader); + return (username.equals(userDetails.getUsername())) && !isTokenExpired(tokenOrHeader); } public boolean validateClaims(Claims claims) { return claims.getExpiration().after(new Date()); } - private boolean isTokenExpired(String token) { - return extractExpiration(token).before(new Date()); + private boolean isTokenExpired(String tokenOrHeader) { + return extractExpiration(tokenOrHeader).before(new Date()); } - private Date extractExpiration(String token) { - return extractClaim(token, Claims::getExpiration); + private Date extractExpiration(String tokenOrHeader) { + return extractClaim(tokenOrHeader, Claims::getExpiration); } } diff --git a/src/main/java/pe/edu/utp/service/ArticuloService.java b/src/main/java/pe/edu/utp/service/ArticuloService.java index 9788eeb..523f8ed 100644 --- a/src/main/java/pe/edu/utp/service/ArticuloService.java +++ b/src/main/java/pe/edu/utp/service/ArticuloService.java @@ -19,8 +19,10 @@ List findByCategoriaAndMarcaAndPrecio( Pageable pageable ); ArticuloResponseDto findById(Integer id); - Articulo save(ArticuloDto articulo); + Articulo save(ArticuloDto articulo, String email); Articulo update(ArticuloDto articulo, Integer id); Articulo partialUpdate(Integer id, Map fields); void delete(Integer id); + Articulo discountStoock(Integer id, Integer stook); + Double calculateTotalPrice(Integer productId); } diff --git a/src/main/java/pe/edu/utp/service/ArticuloServiceImpl.java b/src/main/java/pe/edu/utp/service/ArticuloServiceImpl.java index 722039d..2b7c5b9 100644 --- a/src/main/java/pe/edu/utp/service/ArticuloServiceImpl.java +++ b/src/main/java/pe/edu/utp/service/ArticuloServiceImpl.java @@ -17,6 +17,7 @@ import pe.edu.utp.entity.Articulo; import pe.edu.utp.exception.NoDataFoundException; import pe.edu.utp.repository.ArticuloRepository; +import pe.edu.utp.repository.UsuarioRepository; @Service @Slf4j @@ -26,6 +27,7 @@ public class ArticuloServiceImpl implements ArticuloService { private final ArticuloRepository articuloRepository; private final ArticuloConverter articuloConverter; private final ArticuloResponseConverter articuloResponseConverter; + private final UsuarioRepository usuarioRepository; @Override public List findAll(Pageable page) { @@ -41,7 +43,7 @@ public List findByCategoriaAndMarcaAndPrecio( Double precioMax, Pageable pageable ) { - var result = articuloResponseConverter.fromEntity( + return articuloResponseConverter.fromEntity( articuloRepository.findByCategoriaAndMarcaAndPrecioBetween( categoria, marca, @@ -50,7 +52,6 @@ public List findByCategoriaAndMarcaAndPrecio( pageable ) ); - return result; } @Override @@ -60,8 +61,10 @@ public ArticuloResponseDto findById(Integer id) { } @Override - public Articulo save(ArticuloDto articulo) { + public Articulo save(ArticuloDto articulo, String email) { + var user = usuarioRepository.findByEmail(email); var entity = articuloConverter.fromDto(articulo); + entity.setUsuario(user.get()); return articuloRepository.save(entity); } @@ -97,4 +100,15 @@ public Articulo partialUpdate(Integer id, Map fields) { }); return articuloRepository.save(articuloDb); } + + @Override + public Articulo discountStoock(Integer id, Integer stook) { + articuloRepository.updateStook(id, stook); + return findByArticleId(id); + } + + @Override + public Double calculateTotalPrice(Integer productId) { + return articuloRepository.getTotalPrice(productId); + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a8219f1..843be81 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -26,3 +26,4 @@ spring.flyway.baseline-on-migrate=true + diff --git a/src/main/resources/db/migration/V20250805110000__create_sp_articles.sql b/src/main/resources/db/migration/V20250805110000__create_sp_articles.sql new file mode 100644 index 0000000..ab4d6ed --- /dev/null +++ b/src/main/resources/db/migration/V20250805110000__create_sp_articles.sql @@ -0,0 +1,11 @@ +DELIMITER $$ +CREATE PROCEDURE update_stock( + IN productId INT, + IN quantity INT +) +BEGIN + UPDATE articulos + SET stock = stock - quantity + WHERE id = productId; +END$$ +DELIMITER ; \ No newline at end of file diff --git a/src/main/resources/db/migration/V20250805150000__create_function_update_article.sql b/src/main/resources/db/migration/V20250805150000__create_function_update_article.sql new file mode 100644 index 0000000..29b1b77 --- /dev/null +++ b/src/main/resources/db/migration/V20250805150000__create_function_update_article.sql @@ -0,0 +1,12 @@ +DELIMITER $$ +CREATE FUNCTION get_total_price(productId INT) +RETURNS DECIMAL(10,2) +DETERMINISTIC +READS SQL DATA +BEGIN + DECLARE total DECIMAL(10,2); + SELECT SUM(precio*stock) INTO total FROM articulos + WHERE id = productId; + RETURN total; +END$$ +DELIMITER ; \ No newline at end of file diff --git a/src/main/resources/db/migration/V20250806224500__add_country_column.sql b/src/main/resources/db/migration/V20250806224500__add_country_column.sql new file mode 100644 index 0000000..ccabf5a --- /dev/null +++ b/src/main/resources/db/migration/V20250806224500__add_country_column.sql @@ -0,0 +1,2 @@ +ALTER TABLE usuarios + ADD country VARCHAR(255) NULL; \ No newline at end of file diff --git a/src/main/resources/db/migration/V20250809183000__add_user_fk.sql b/src/main/resources/db/migration/V20250809183000__add_user_fk.sql new file mode 100644 index 0000000..6d5120a --- /dev/null +++ b/src/main/resources/db/migration/V20250809183000__add_user_fk.sql @@ -0,0 +1,5 @@ +ALTER TABLE articulos + ADD usuario_id INT NULL; + +ALTER TABLE articulos + ADD CONSTRAINT FK_ARTICULOS_ON_USUARIO FOREIGN KEY (usuario_id) REFERENCES usuarios (id); \ No newline at end of file diff --git a/src/test/java/pe/edu/utp/ArticulosapiApplicationTests.java b/src/test/java/pe/edu/utp/ArticulosapiApplicationTests.java index 499a04d..1c6cff9 100644 --- a/src/test/java/pe/edu/utp/ArticulosapiApplicationTests.java +++ b/src/test/java/pe/edu/utp/ArticulosapiApplicationTests.java @@ -1,17 +1,11 @@ package pe.edu.utp; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class ArticulosapiApplicationTests { - @Value("${spring.datasource.url}") - private String dataSourceUrl; - @Test - void contextLoads() { - System.out.println("DataSource URL: " + dataSourceUrl); - } + void contextLoads() {} }