mirror of
https://git.imnavajas.es/jjimenez/erp-imprimelibros.git
synced 2026-01-13 00:48:49 +00:00
Compare commits
94 Commits
59f7d315d9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 562dc2b231 | |||
| 9a49ccf6b8 | |||
| b2026f1cab | |||
| a5b6bf3a25 | |||
| 9a67c2e78f | |||
| 8263d97bf7 | |||
| 292aebcf65 | |||
| e50153205a | |||
| 5ecb38f474 | |||
| d7a85d9bfb | |||
| 4343997eb1 | |||
| 6bfc60d158 | |||
| aa8ecdf75c | |||
| dc529ff055 | |||
| 4a535ab644 | |||
| 400251ac3d | |||
| 6bea279066 | |||
| bf823281a5 | |||
| 9d4320db9a | |||
| d7b5dedb38 | |||
| 089641b601 | |||
| 98a5fcaa0b | |||
| 13a38fcdd5 | |||
| 839301cf94 | |||
| 1ba1b28793 | |||
| 47866ddead | |||
| 982423d766 | |||
| 5b5ce7ccd7 | |||
| 61be8d6d3b | |||
| 3a00702bb1 | |||
| b94a099e01 | |||
| d4120bb486 | |||
| 4cc47b4249 | |||
| cf73801dbe | |||
| 3b9f446195 | |||
| c6e2322132 | |||
| 58fd4815c6 | |||
| 9baf880022 | |||
| 25a7bcf0b8 | |||
| 58f0eee5d9 | |||
| 997741c3c9 | |||
| 6ff5250d1b | |||
| 73676f60b9 | |||
| 18a43ea121 | |||
| d19cd1923c | |||
| 84a822db22 | |||
| 69f27df98b | |||
| 6bd36dbe8c | |||
| 3086a6de41 | |||
| 4f1b3f2bb6 | |||
| d31a6e9e8e | |||
| 51ea3b4120 | |||
| 32416d10f3 | |||
| 69a4076e28 | |||
| b52f5e7d4f | |||
| 4ceb4fb8e4 | |||
| cc696d7a99 | |||
| 032e44b9c5 | |||
| 7254c8f11b | |||
| 90239be31e | |||
| 3ea1496861 | |||
| f13eeb940c | |||
| 62396eb7a7 | |||
| c11c34011e | |||
| a4443763d8 | |||
| ed32f773a4 | |||
| dc64e40e38 | |||
| 7516e9e91e | |||
| f528809c07 | |||
| 725cff9b51 | |||
| 88650fc5e8 | |||
| dc8b67b937 | |||
| 4d451cc85e | |||
| 51d22515e8 | |||
| 7f2db075a8 | |||
| 6afa78df68 | |||
| a01d74aeb2 | |||
| 40dc719e89 | |||
| 90c191d8f8 | |||
| 167c136dca | |||
| feff9ee94a | |||
| ae2904aa71 | |||
| 5e9631073e | |||
| c272fd7b9b | |||
| f770bd07d6 | |||
| cecc0c0ea0 | |||
| 9dad59fe16 | |||
| f6a683de81 | |||
| de7a392e07 | |||
| 8590235709 | |||
| 8e011e7fca | |||
| 2ed032d7c6 | |||
| 3517918afe | |||
| 1526600ee1 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -31,3 +31,6 @@ build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Logs ###
|
||||
erp-*.log
|
||||
|
||||
45
docker-compose.plesk.yml
Normal file
45
docker-compose.plesk.yml
Normal file
@ -0,0 +1,45 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
imprimelibros-db:
|
||||
image: mysql:8.0
|
||||
container_name: imprimelibros-db
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: NrXz6DK6UoN
|
||||
MYSQL_DATABASE: imprimelibros
|
||||
MYSQL_USER: imprimelibros_user
|
||||
MYSQL_PASSWORD: om91irrDctd
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
networks:
|
||||
- imprimelibros-network
|
||||
restart: always
|
||||
ports:
|
||||
- "3309:3306" # host:container
|
||||
|
||||
imprimelibros-app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: imprimelibros-app:latest
|
||||
container_name: imprimelibros-app
|
||||
depends_on:
|
||||
- imprimelibros-db
|
||||
environment:
|
||||
SPRING_DATASOURCE_URL: jdbc:mysql://imprimelibros-db:3306/imprimelibros?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Europe/Madrid
|
||||
SPRING_DATASOURCE_USERNAME: imprimelibros_user
|
||||
SPRING_DATASOURCE_PASSWORD: om91irrDctd
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
volumes:
|
||||
- ./logs:/var/log/imprimelibros
|
||||
restart: always
|
||||
networks:
|
||||
- imprimelibros-network
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
|
||||
networks:
|
||||
imprimelibros-network:
|
||||
driver: bridge
|
||||
@ -31,6 +31,8 @@ services:
|
||||
SPRING_DATASOURCE_PASSWORD: om91irrDctd
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./logs:/var/log/imprimelibros
|
||||
restart: always
|
||||
networks:
|
||||
- imprimelibros-network
|
||||
|
||||
12303
logs/erp.log
Normal file
12303
logs/erp.log
Normal file
File diff suppressed because one or more lines are too long
48
pom.xml
48
pom.xml
@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.6</version>
|
||||
<version>3.5.9</version>
|
||||
<relativePath /> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.imprimelibros</groupId>
|
||||
@ -32,6 +32,12 @@
|
||||
<liquibase.version>4.29.2</liquibase.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
@ -151,6 +157,41 @@
|
||||
<version>${liquibase.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Redsys -->
|
||||
<dependency>
|
||||
<groupId>sis.redsys</groupId>
|
||||
<artifactId>apiSha256</artifactId>
|
||||
<version>1.0</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/src/main/resources/lib/apiSha256.jar</systemPath>
|
||||
</dependency>
|
||||
|
||||
<!-- Dependencias locales incluidas en el ZIP -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
<version>1.47</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/src/main/resources/lib/bcprov-jdk15on-1.4.7.jar</systemPath>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>1.3</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/src/main/resources/lib/commons-codec-1.3.jar</systemPath>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
<version>1.0</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${project.basedir}/src/main/resources/lib/org.json.jar</systemPath>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
@ -158,6 +199,11 @@
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<!-- IMPORTANTE: incluir dependencias con scope=system en el fat-jar -->
|
||||
<configuration>
|
||||
<!-- Esto hace que meta las dependencias con scope=system en BOOT-INF/lib -->
|
||||
<includeSystemScope>true</includeSystemScope>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- (Migraciones) Plugin Maven para generar/ejecutar changelogs -->
|
||||
|
||||
@ -3,8 +3,10 @@ package com.imprimelibros.erp;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
@ConfigurationPropertiesScan(basePackages = "com.imprimelibros.erp")
|
||||
public class ErpApplication {
|
||||
|
||||
|
||||
@ -1,18 +1,23 @@
|
||||
package com.imprimelibros.erp.cart;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Table(name = "carts",
|
||||
uniqueConstraints = @UniqueConstraint(name="uq_carts_user_active", columnNames={"user_id","status"}))
|
||||
@Table(name = "carts", uniqueConstraints = @UniqueConstraint(name = "uq_carts_user_active", columnNames = { "user_id",
|
||||
"status" }))
|
||||
public class Cart {
|
||||
|
||||
public enum Status { ACTIVE, LOCKED, ABANDONED }
|
||||
public enum Status {
|
||||
ACTIVE, LOCKED, ABANDONED
|
||||
}
|
||||
|
||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
@ -25,6 +30,9 @@ public class Cart {
|
||||
@Column(nullable = false, length = 3)
|
||||
private String currency = "EUR";
|
||||
|
||||
@Column(name = "only_one_shipment", nullable = false)
|
||||
private Boolean onlyOneShipment = true;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt = LocalDateTime.now();
|
||||
|
||||
@ -34,23 +42,93 @@ public class Cart {
|
||||
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
private List<CartItem> items = new ArrayList<>();
|
||||
|
||||
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
private List<CartDireccion> direcciones = new ArrayList<>();
|
||||
|
||||
@Column(name = "total", nullable = false)
|
||||
private BigDecimal total = BigDecimal.ZERO;
|
||||
|
||||
@PreUpdate
|
||||
public void preUpdate() { this.updatedAt = LocalDateTime.now(); }
|
||||
public void preUpdate() {
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
// Getters & Setters
|
||||
public Long getId() { return id; }
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Status getStatus() { return status; }
|
||||
public void setStatus(Status status) { this.status = status; }
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public String getCurrency() { return currency; }
|
||||
public void setCurrency(String currency) { this.currency = currency; }
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public List<CartItem> getItems() { return items; }
|
||||
public void setItems(List<CartItem> items) { this.items = items; }
|
||||
public void setStatus(Status status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
public void setCurrency(String currency) {
|
||||
this.currency = currency;
|
||||
}
|
||||
|
||||
public Boolean getOnlyOneShipment() {
|
||||
return onlyOneShipment;
|
||||
}
|
||||
|
||||
public void setOnlyOneShipment(Boolean onlyOneShipment) {
|
||||
this.onlyOneShipment = onlyOneShipment;
|
||||
}
|
||||
|
||||
public BigDecimal getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
public void setTotal(BigDecimal total) {
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public List<CartItem> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public void setItems(List<CartItem> items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
public List<CartDireccion> getDirecciones() {
|
||||
return direcciones;
|
||||
}
|
||||
|
||||
public void setDirecciones(List<CartDireccion> direcciones) {
|
||||
this.direcciones = direcciones;
|
||||
}
|
||||
|
||||
public void addDireccion(CartDireccion d) {
|
||||
direcciones.add(d);
|
||||
d.setCart(this);
|
||||
}
|
||||
|
||||
public void removeDireccion(CartDireccion d) {
|
||||
direcciones.remove(d);
|
||||
d.setCart(null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
package com.imprimelibros.erp.cart;
|
||||
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
public class CartCleanupService {
|
||||
|
||||
private final CartRepository cartRepository;
|
||||
|
||||
public CartCleanupService(CartRepository cartRepository) {
|
||||
this.cartRepository = cartRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ejecuta cada noche a las 2:00 AM
|
||||
*/
|
||||
@Transactional
|
||||
@Scheduled(cron = "0 0 2 * * *") // cada día a las 02:00
|
||||
public void markAbandonedCarts() {
|
||||
LocalDateTime limite = LocalDateTime.now().minusDays(7);
|
||||
int updated = cartRepository.markOldCartsAsAbandoned(limite);
|
||||
System.out.println("Carritos abandonados marcados: " + updated);
|
||||
}
|
||||
}
|
||||
@ -3,71 +3,105 @@ package com.imprimelibros.erp.cart;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import com.imprimelibros.erp.users.UserDetailsImpl;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import com.imprimelibros.erp.users.User;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.direcciones.Direccion;
|
||||
import com.imprimelibros.erp.direcciones.DireccionService;
|
||||
import com.imprimelibros.erp.i18n.TranslationService;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import com.imprimelibros.erp.cart.dto.UpdateCartRequest;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/cart")
|
||||
public class CartController {
|
||||
|
||||
private final CartService service;
|
||||
protected final CartService service;
|
||||
protected DireccionService direccionService;
|
||||
protected MessageSource messageSource;
|
||||
protected TranslationService translationService;
|
||||
|
||||
public CartController(CartService service) {
|
||||
public CartController(CartService service, DireccionService direccionService, MessageSource messageSource,
|
||||
TranslationService translationService) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el ID de usuario desde tu seguridad.
|
||||
* Adáptalo a tu UserDetails (e.g., SecurityContext con getId())
|
||||
*/
|
||||
private Long currentUserId(Principal principal) {
|
||||
if (principal == null) {
|
||||
throw new IllegalStateException("Usuario no autenticado");
|
||||
}
|
||||
|
||||
if (principal instanceof Authentication auth) {
|
||||
Object principalObj = auth.getPrincipal();
|
||||
|
||||
if (principalObj instanceof UserDetailsImpl udi) {
|
||||
return udi.getId();
|
||||
} else if (principalObj instanceof User u && u.getId() != null) {
|
||||
return u.getId();
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException("No se pudo obtener el ID del usuario actual");
|
||||
this.direccionService = direccionService;
|
||||
this.messageSource = messageSource;
|
||||
this.translationService = translationService;
|
||||
}
|
||||
|
||||
/** Vista del carrito */
|
||||
@GetMapping
|
||||
public String viewCart(Model model, Principal principal, Locale locale) {
|
||||
var items = service.listItems(currentUserId(principal), locale);
|
||||
|
||||
List<String> keys = List.of(
|
||||
"app.cancelar",
|
||||
"app.seleccionar",
|
||||
"cart.shipping.add.title",
|
||||
"cart.shipping.select-placeholder",
|
||||
"cart.shipping.new-address",
|
||||
"cart.shipping.errors.noAddressSelected",
|
||||
"cart.shipping.enter-units",
|
||||
"cart.shipping.units-label",
|
||||
"cart.shipping.errors.units-error",
|
||||
"cart.shipping.ud",
|
||||
"cart.shipping.uds",
|
||||
"cart.shipping.send-in-palets",
|
||||
"cart.shipping.send-in-palets.info",
|
||||
"cart.shipping.tipo-envio",
|
||||
"cart.pass-to.customer.error",
|
||||
"cart.pass-to.customer.error-move",
|
||||
"app.yes",
|
||||
"app.aceptar",
|
||||
"app.cancelar");
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
|
||||
Long userId = Utils.currentUserId(principal);
|
||||
Cart cart = service.getOrCreateActiveCart(userId);
|
||||
|
||||
var items = service.listItems(userId, locale);
|
||||
model.addAttribute("items", items);
|
||||
|
||||
Map<String, Object> direcciones = service.getCartDirecciones(cart.getId(), locale);
|
||||
if (direcciones != null && direcciones.containsKey("mainDir"))
|
||||
model.addAttribute("mainDir", direcciones.get("mainDir"));
|
||||
else if (direcciones != null && direcciones.containsKey("direcciones"))
|
||||
model.addAttribute("direcciones", direcciones.get("direcciones"));
|
||||
|
||||
var summary = service.getCartSummary(cart, locale);
|
||||
model.addAttribute("cartSummary", summary);
|
||||
if (summary.get("errorShipmentCost") != null && (Boolean) summary.get("errorShipmentCost"))
|
||||
model.addAttribute("errorEnvio", true);
|
||||
else
|
||||
model.addAttribute("errorEnvio", false);
|
||||
|
||||
model.addAttribute("cart", cart);
|
||||
return "imprimelibros/cart/cart"; // crea esta vista si quieres (tabla simple)
|
||||
}
|
||||
|
||||
/** Añadir presupuesto via POST form */
|
||||
@PostMapping("/add")
|
||||
public String add(@PathVariable(name = "presupuestoId", required = true) Long presupuestoId, Principal principal) {
|
||||
service.addPresupuesto(currentUserId(principal), presupuestoId);
|
||||
service.addPresupuesto(Utils.currentUserId(principal), presupuestoId);
|
||||
return "redirect:/cart";
|
||||
}
|
||||
|
||||
/** Añadir presupuesto con ruta REST (opcional) */
|
||||
@PostMapping("/add/{presupuestoId}")
|
||||
public Object addPath(@PathVariable Long presupuestoId, Principal principal, HttpServletRequest request) {
|
||||
service.addPresupuesto(currentUserId(principal), presupuestoId);
|
||||
service.addPresupuesto(Utils.currentUserId(principal), presupuestoId);
|
||||
boolean isAjax = "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
|
||||
if (isAjax) {
|
||||
// Responder 200 con la URL a la que quieres ir
|
||||
@ -83,13 +117,13 @@ public class CartController {
|
||||
public long getCount(Principal principal) {
|
||||
if (principal == null)
|
||||
return 0;
|
||||
return service.countItems(currentUserId(principal));
|
||||
return service.countItems(Utils.currentUserId(principal));
|
||||
}
|
||||
|
||||
/** Eliminar línea por ID de item */
|
||||
@DeleteMapping("/{itemId}/remove")
|
||||
public String remove(@PathVariable Long itemId, Principal principal) {
|
||||
service.removeItem(currentUserId(principal), itemId);
|
||||
service.removeItem(Utils.currentUserId(principal), itemId);
|
||||
return "redirect:/cart";
|
||||
}
|
||||
|
||||
@ -97,14 +131,72 @@ public class CartController {
|
||||
@DeleteMapping("/delete/item/{presupuestoId}")
|
||||
@ResponseBody
|
||||
public String removeByPresupuesto(@PathVariable Long presupuestoId, Principal principal) {
|
||||
service.removeByPresupuesto(currentUserId(principal), presupuestoId);
|
||||
service.removeByPresupuesto(Utils.currentUserId(principal), presupuestoId);
|
||||
return "redirect:/cart";
|
||||
}
|
||||
|
||||
/** Vaciar carrito completo */
|
||||
@DeleteMapping("/clear")
|
||||
public String clear(Principal principal) {
|
||||
service.clear(currentUserId(principal));
|
||||
service.clear(Utils.currentUserId(principal));
|
||||
return "redirect:/cart";
|
||||
}
|
||||
|
||||
@GetMapping("/get-address/{id}")
|
||||
public String getDireccionCard(@PathVariable Long id, @RequestParam(required = false) Long presupuestoId,
|
||||
@RequestParam(required = false) Integer unidades,
|
||||
@RequestParam(required = false) Integer isPalets,
|
||||
Model model, Locale locale) {
|
||||
Direccion dir = direccionService.findById(id)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
model.addAttribute("pais", messageSource.getMessage("paises." + dir.getPais().getKeyword(), null,
|
||||
dir.getPais().getKeyword(), locale));
|
||||
model.addAttribute("presupuestoId", presupuestoId);
|
||||
model.addAttribute("unidades", unidades);
|
||||
model.addAttribute("isPalets", isPalets);
|
||||
model.addAttribute("direccion", dir);
|
||||
|
||||
return "imprimelibros/direcciones/direccionCard :: direccionCard(direccion=${direccion})";
|
||||
}
|
||||
|
||||
@PostMapping(value = "/update/{id}", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
public String updateCart(@PathVariable Long id, UpdateCartRequest updateRequest, Model model, Locale locale,
|
||||
Principal principal) {
|
||||
|
||||
try {
|
||||
service.updateCart(id, updateRequest);
|
||||
var cartSummary = service.getCartSummary(service.getCartById(id), locale);
|
||||
model.addAttribute("cartSummary", cartSummary);
|
||||
|
||||
return "imprimelibros/cart/_cartSummary :: cartSummary(summary=${cartSummary})";
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
// redirect to cart with error message
|
||||
String errorMessage = messageSource.getMessage("cart.update.error", null, "Error updating cart", locale);
|
||||
model.addAttribute("errorMessage", errorMessage);
|
||||
return "redirect:/cart";
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/pass-to-customer/{customerId}")
|
||||
public ResponseEntity<?> moveToCustomer(
|
||||
@PathVariable Long customerId,
|
||||
Principal principal) {
|
||||
|
||||
if(!Utils.isCurrentUserAdmin()) {
|
||||
return ResponseEntity.status(403).body(Map.of("error", "Forbidden"));
|
||||
}
|
||||
|
||||
Long userId = Utils.currentUserId(principal);
|
||||
Cart cart = service.getOrCreateActiveCart(userId);
|
||||
|
||||
boolean ok = service.moveCartToCustomer(cart.getId(), customerId);
|
||||
|
||||
if (ok)
|
||||
return ResponseEntity.ok().build();
|
||||
return ResponseEntity.status(400).body(Map.of("error", "cart.errors.move-cart"));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
152
src/main/java/com/imprimelibros/erp/cart/CartDireccion.java
Normal file
152
src/main/java/com/imprimelibros/erp/cart/CartDireccion.java
Normal file
@ -0,0 +1,152 @@
|
||||
package com.imprimelibros.erp.cart;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import com.imprimelibros.erp.cart.dto.DireccionCardDTO;
|
||||
import com.imprimelibros.erp.direcciones.Direccion;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "cart_direcciones")
|
||||
public class CartDireccion {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "cart_id", nullable = false)
|
||||
private Cart cart;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "direccion_id", nullable = false)
|
||||
private Direccion direccion;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "presupuesto_id")
|
||||
private Presupuesto presupuesto;
|
||||
|
||||
@Column(name = "unidades")
|
||||
private Integer unidades;
|
||||
|
||||
@Column(name = "isPalets", nullable = false)
|
||||
private Boolean isPalets;
|
||||
|
||||
// --- Getters & Setters ---
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Cart getCart() {
|
||||
return cart;
|
||||
}
|
||||
|
||||
public void setCart(Cart cart) {
|
||||
this.cart = cart;
|
||||
}
|
||||
|
||||
public Direccion getDireccion() {
|
||||
return direccion;
|
||||
}
|
||||
|
||||
public void setDireccion(Direccion direccion) {
|
||||
this.direccion = direccion;
|
||||
}
|
||||
|
||||
public Presupuesto getPresupuesto() {
|
||||
return presupuesto;
|
||||
}
|
||||
|
||||
public void setPresupuesto(Presupuesto presupuesto) {
|
||||
this.presupuesto = presupuesto;
|
||||
}
|
||||
|
||||
public Integer getUnidades() {
|
||||
return unidades;
|
||||
}
|
||||
|
||||
public void setUnidades(Integer unidades) {
|
||||
this.unidades = unidades;
|
||||
}
|
||||
|
||||
public Boolean getIsPalets() {
|
||||
return isPalets;
|
||||
}
|
||||
|
||||
public void setIsPalets(Boolean isPalets) {
|
||||
this.isPalets = isPalets;
|
||||
}
|
||||
|
||||
public DireccionCardDTO toDireccionCard(MessageSource messageSource, Locale locale) {
|
||||
|
||||
String pais = messageSource.getMessage("paises." + this.direccion.getPais().getKeyword(), null,
|
||||
this.direccion.getPais().getKeyword(), locale);
|
||||
|
||||
return new DireccionCardDTO(
|
||||
this.direccion,
|
||||
this.presupuesto != null ? this.presupuesto.getId() : null,
|
||||
this.unidades,
|
||||
this.isPalets,
|
||||
pais
|
||||
);
|
||||
}
|
||||
|
||||
public Map<String, Object> toSkMap(Integer numeroUnidades, Double pesoKg, Boolean palets, Boolean ejemplarPrueba) {
|
||||
|
||||
Map<String, Object> direccion = new HashMap<>();
|
||||
direccion.put("cantidad", numeroUnidades);
|
||||
direccion.put("peso", pesoKg);
|
||||
direccion.put("att", this.getDireccion().getAtt());
|
||||
direccion.put("email", this.getDireccion().getUser().getUserName());
|
||||
direccion.put("direccion", this.getDireccion().getDireccion());
|
||||
direccion.put("pais_code3", this.getDireccion().getPaisCode3());
|
||||
direccion.put("cp", this.getDireccion().getCp());
|
||||
direccion.put("municipio", this.getDireccion().getCiudad());
|
||||
direccion.put("provincia", this.getDireccion().getProvincia());
|
||||
direccion.put("telefono", this.getDireccion().getTelefono());
|
||||
direccion.put("entregaPieCalle", palets ? 1 : 0);
|
||||
direccion.put("is_ferro_prototipo", ejemplarPrueba ? 1 : 0);
|
||||
direccion.put("num_ferro_prototipo", ejemplarPrueba ? 1 : 0);
|
||||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("direccion", direccion);
|
||||
map.put("unidades", numeroUnidades);
|
||||
map.put("entregaPalets", palets ? 1 : 0);
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
public Map<String, Object> toSkMapDepositoLegal() {
|
||||
Map<String, Object> direccion = new HashMap<>();
|
||||
direccion.put("cantidad", 4);
|
||||
direccion.put("peso", 0);
|
||||
direccion.put("att", "Unidades para Depósito Legal (sin envío)");
|
||||
direccion.put("email", "");
|
||||
direccion.put("direccion", "");
|
||||
direccion.put("pais_code3", "esp");
|
||||
direccion.put("cp", "");
|
||||
direccion.put("municipio", "");
|
||||
direccion.put("provincia", "");
|
||||
direccion.put("telefono", "");
|
||||
direccion.put("entregaPieCalle", 0);
|
||||
direccion.put("is_ferro_prototipo", 0);
|
||||
direccion.put("num_ferro_prototipo", 0);
|
||||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("direccion", direccion);
|
||||
map.put("unidades", 4);
|
||||
map.put("entregaPalets", 0);
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
@ -3,6 +3,8 @@ package com.imprimelibros.erp.cart;
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "cart_items",
|
||||
@ -17,8 +19,9 @@ public class CartItem {
|
||||
@JoinColumn(name = "cart_id", nullable = false)
|
||||
private Cart cart;
|
||||
|
||||
@Column(name = "presupuesto_id", nullable = false)
|
||||
private Long presupuestoId;
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "presupuesto_id", nullable = false)
|
||||
private Presupuesto presupuesto;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt = LocalDateTime.now();
|
||||
@ -29,8 +32,8 @@ public class CartItem {
|
||||
public Cart getCart() { return cart; }
|
||||
public void setCart(Cart cart) { this.cart = cart; }
|
||||
|
||||
public Long getPresupuestoId() { return presupuestoId; }
|
||||
public void setPresupuestoId(Long presupuestoId) { this.presupuestoId = presupuestoId; }
|
||||
public Presupuesto getPresupuesto() { return presupuesto; }
|
||||
public void setPresupuesto(Presupuesto presupuesto) { this.presupuesto = presupuesto; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
}
|
||||
|
||||
@ -1,9 +1,35 @@
|
||||
package com.imprimelibros.erp.cart;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface CartRepository extends JpaRepository<Cart, Long> {
|
||||
Optional<Cart> findByUserIdAndStatus(Long userId, Cart.Status status);
|
||||
|
||||
@Query("""
|
||||
select distinct c from Cart c
|
||||
left join fetch c.direcciones cd
|
||||
left join fetch cd.direccion d
|
||||
left join fetch d.pais p
|
||||
left join fetch cd.presupuesto pr
|
||||
where c.id = :id
|
||||
""")
|
||||
Optional<Cart> findByIdFetchAll(@Param("id") Long id);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("""
|
||||
UPDATE Cart c
|
||||
SET c.status = 'ABANDONED'
|
||||
WHERE c.status = 'ACTIVE'
|
||||
AND c.updatedAt < :limite
|
||||
""")
|
||||
int markOldCartsAsAbandoned(LocalDateTime limite);
|
||||
|
||||
}
|
||||
|
||||
@ -1,41 +1,69 @@
|
||||
package com.imprimelibros.erp.cart;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
|
||||
import com.imprimelibros.erp.users.UserService;
|
||||
import com.imprimelibros.erp.cart.dto.CartDireccionRepository;
|
||||
import com.imprimelibros.erp.cart.dto.DireccionCardDTO;
|
||||
import com.imprimelibros.erp.cart.dto.DireccionShipment;
|
||||
import com.imprimelibros.erp.cart.dto.UpdateCartRequest;
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.common.email.EmailService;
|
||||
import com.imprimelibros.erp.direcciones.DireccionService;
|
||||
import com.imprimelibros.erp.externalApi.skApiClient;
|
||||
import com.imprimelibros.erp.pedidos.PedidoRepository;
|
||||
import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
|
||||
|
||||
|
||||
@Service
|
||||
public class CartService {
|
||||
|
||||
private final EmailService emailService;
|
||||
|
||||
private final CartRepository cartRepo;
|
||||
private final CartDireccionRepository cartDireccionRepo;
|
||||
private final CartItemRepository itemRepo;
|
||||
private final MessageSource messageSource;
|
||||
private final PresupuestoFormatter presupuestoFormatter;
|
||||
private final PresupuestoRepository presupuestoRepo;
|
||||
private final Utils utils;
|
||||
private final DireccionService direccionService;
|
||||
private final skApiClient skApiClient;
|
||||
private final PresupuestoService presupuestoService;
|
||||
private final PedidoRepository pedidoRepository;
|
||||
private final UserService userService;
|
||||
|
||||
public CartService(CartRepository cartRepo, CartItemRepository itemRepo,
|
||||
MessageSource messageSource, PresupuestoFormatter presupuestoFormatter,
|
||||
PresupuestoRepository presupuestoRepo, Utils utils) {
|
||||
CartDireccionRepository cartDireccionRepo, MessageSource messageSource,
|
||||
PresupuestoFormatter presupuestoFormatter, PresupuestoRepository presupuestoRepo, PedidoRepository pedidoRepository,
|
||||
DireccionService direccionService, skApiClient skApiClient,PresupuestoService presupuestoService, EmailService emailService, UserService userService) {
|
||||
this.cartRepo = cartRepo;
|
||||
this.itemRepo = itemRepo;
|
||||
this.cartDireccionRepo = cartDireccionRepo;
|
||||
this.messageSource = messageSource;
|
||||
this.presupuestoFormatter = presupuestoFormatter;
|
||||
this.presupuestoRepo = presupuestoRepo;
|
||||
this.utils = utils;
|
||||
this.direccionService = direccionService;
|
||||
this.skApiClient = skApiClient;
|
||||
this.presupuestoService = presupuestoService;
|
||||
this.emailService = emailService;
|
||||
this.pedidoRepository = pedidoRepository;
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
public Cart findById(Long cartId) {
|
||||
return cartRepo.findById(cartId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
|
||||
}
|
||||
|
||||
/** Devuelve el carrito activo o lo crea si no existe. */
|
||||
@ -50,6 +78,11 @@ public class CartService {
|
||||
});
|
||||
}
|
||||
|
||||
public Cart getCartById(Long cartId) {
|
||||
return cartRepo.findById(cartId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
|
||||
}
|
||||
|
||||
/** Lista items (presupuestos) del carrito activo del usuario. */
|
||||
@Transactional
|
||||
public List<Map<String, Object>> listItems(Long userId, Locale locale) {
|
||||
@ -58,14 +91,13 @@ public class CartService {
|
||||
List<CartItem> items = itemRepo.findByCartId(cart.getId());
|
||||
for (CartItem item : items) {
|
||||
|
||||
Presupuesto p = presupuestoRepo.findById(item.getPresupuestoId())
|
||||
.orElseThrow(() -> new IllegalStateException("Presupuesto no encontrado: " + item.getPresupuestoId()));
|
||||
Presupuesto p = item.getPresupuesto();
|
||||
|
||||
Map<String, Object> elemento = getElementoCart(p, locale);
|
||||
Map<String, Object> elemento = presupuestoService.getPresupuestoInfoForCard(p, locale);
|
||||
elemento.put("cartItemId", item.getId());
|
||||
resultados.add(elemento);
|
||||
}
|
||||
//System.out.println("Cart items: " + resultados);
|
||||
// System.out.println("Cart items: " + resultados);
|
||||
return resultados;
|
||||
}
|
||||
|
||||
@ -77,7 +109,8 @@ public class CartService {
|
||||
if (!exists) {
|
||||
CartItem ci = new CartItem();
|
||||
ci.setCart(cart);
|
||||
ci.setPresupuestoId(presupuestoId);
|
||||
ci.setPresupuesto(presupuestoRepo.findById(presupuestoId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Presupuesto no encontrado")));
|
||||
itemRepo.save(ci);
|
||||
}
|
||||
}
|
||||
@ -96,8 +129,9 @@ public class CartService {
|
||||
@Transactional
|
||||
public void removeByPresupuesto(Long userId, Long presupuestoId) {
|
||||
Cart cart = getOrCreateActiveCart(userId);
|
||||
itemRepo.findByCartIdAndPresupuestoId(cart.getId(), presupuestoId)
|
||||
.ifPresent(itemRepo::delete);
|
||||
CartItem item = itemRepo.findByCartIdAndPresupuestoId(cart.getId(), presupuestoId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Item no encontrado"));
|
||||
itemRepo.deleteById(item.getId());
|
||||
}
|
||||
|
||||
/** Vacía todo el carrito activo. */
|
||||
@ -115,34 +149,344 @@ public class CartService {
|
||||
cartRepo.save(cart);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void lockCartById(Long cartId) {
|
||||
Cart cart = cartRepo.findById(cartId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
|
||||
cart.setStatus(Cart.Status.LOCKED);
|
||||
cartRepo.save(cart);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public long countItems(Long userId) {
|
||||
Cart cart = getOrCreateActiveCart(userId);
|
||||
return itemRepo.findByCartId(cart.getId()).size();
|
||||
}
|
||||
|
||||
private Map<String, Object> getElementoCart(Presupuesto presupuesto, Locale locale) {
|
||||
|
||||
Map<String, Object> resumen = new HashMap<>();
|
||||
|
||||
resumen.put("titulo", presupuesto.getTitulo());
|
||||
public Map<String, Object> getCartSummaryRaw(Cart cart, Locale locale) {
|
||||
|
||||
resumen.put("imagen",
|
||||
"/assets/images/imprimelibros/presupuestador/" + presupuesto.getTipoEncuadernacion() + ".png");
|
||||
resumen.put("imagen_alt",
|
||||
messageSource.getMessage("presupuesto." + presupuesto.getTipoEncuadernacion(), null, locale));
|
||||
double base = 0.0;
|
||||
double iva4 = 0.0;
|
||||
double iva21 = 0.0;
|
||||
double shipment = 0.0;
|
||||
boolean errorShipementCost = false;
|
||||
|
||||
resumen.put("presupuestoId", presupuesto.getId());
|
||||
List<CartItem> items = cart.getItems();
|
||||
List<CartDireccion> direcciones = cart.getDirecciones();
|
||||
|
||||
Map<String, Object> detalles = utils.getTextoPresupuesto(presupuesto, locale);
|
||||
for (CartItem item : items) {
|
||||
Presupuesto p = item.getPresupuesto();
|
||||
Double peso = p.getPeso() != null ? p.getPeso().doubleValue() : 0.0;
|
||||
base += p.getBaseImponible().doubleValue();
|
||||
iva4 += p.getIvaImporte4().doubleValue();
|
||||
iva21 += p.getIvaImporte21().doubleValue();
|
||||
|
||||
resumen.put("baseTotal", Utils.formatCurrency(presupuesto.getBaseImponible(), locale));
|
||||
resumen.put("base", presupuesto.getBaseImponible());
|
||||
resumen.put("iva4", presupuesto.getIvaImporte4());
|
||||
resumen.put("iva21", presupuesto.getIvaImporte21());
|
||||
if (cart.getOnlyOneShipment() != null && cart.getOnlyOneShipment()) {
|
||||
if (direcciones != null && !direcciones.isEmpty()) {
|
||||
CartDireccion cd = direcciones.get(0);
|
||||
boolean freeShipment = direccionService.checkFreeShipment(
|
||||
cd.getDireccion().getCp(),
|
||||
cd.getDireccion().getPaisCode3()) && !cd.getIsPalets();
|
||||
|
||||
resumen.put("resumen", detalles);
|
||||
if (!freeShipment) {
|
||||
Integer unidades = p.getSelectedTirada();
|
||||
Map<String, Object> res = getShippingCost(cd, peso, unidades, locale);
|
||||
if (Boolean.FALSE.equals(res.get("success"))) {
|
||||
errorShipementCost = true;
|
||||
} else {
|
||||
shipment += (Double) res.get("shipment");
|
||||
iva21 += (Double) res.get("iva21");
|
||||
}
|
||||
}
|
||||
|
||||
return resumen;
|
||||
// ejemplar de prueba
|
||||
if (p.getServiciosJson() != null && p.getServiciosJson().contains("ejemplar-prueba")) {
|
||||
Map<String, Object> res = getShippingCost(cd, peso, 1, locale);
|
||||
if (Boolean.FALSE.equals(res.get("success"))) {
|
||||
errorShipementCost = true;
|
||||
} else {
|
||||
shipment += (Double) res.get("shipment");
|
||||
iva21 += (Double) res.get("iva21");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (direcciones == null)
|
||||
continue;
|
||||
|
||||
List<CartDireccion> cd_presupuesto = direcciones.stream()
|
||||
.filter(d -> d.getPresupuesto() != null
|
||||
&& d.getPresupuesto().getId().equals(p.getId())
|
||||
&& d.getUnidades() != null
|
||||
&& d.getUnidades() > 0)
|
||||
.toList();
|
||||
|
||||
boolean firstDirection = true;
|
||||
for (CartDireccion cd : cd_presupuesto) {
|
||||
Integer unidades = cd.getUnidades();
|
||||
if (firstDirection) {
|
||||
boolean freeShipment = direccionService.checkFreeShipment(
|
||||
cd.getDireccion().getCp(),
|
||||
cd.getDireccion().getPaisCode3()) && !cd.getIsPalets();
|
||||
|
||||
if (!freeShipment && unidades != null && unidades > 0) {
|
||||
Map<String, Object> res = getShippingCost(cd, peso, unidades, locale);
|
||||
if (Boolean.FALSE.equals(res.get("success"))) {
|
||||
errorShipementCost = true;
|
||||
} else {
|
||||
shipment += (Double) res.get("shipment");
|
||||
iva21 += (Double) res.get("iva21");
|
||||
}
|
||||
}
|
||||
firstDirection = false;
|
||||
} else {
|
||||
Map<String, Object> res = getShippingCost(cd, peso, unidades, locale);
|
||||
if (Boolean.FALSE.equals(res.get("success"))) {
|
||||
errorShipementCost = true;
|
||||
} else {
|
||||
shipment += (Double) res.get("shipment");
|
||||
iva21 += (Double) res.get("iva21");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ejemplar de prueba
|
||||
CartDireccion cd_prueba = direcciones.stream()
|
||||
.filter(d -> d.getPresupuesto() != null
|
||||
&& d.getPresupuesto().getId().equals(p.getId())
|
||||
&& d.getUnidades() == null)
|
||||
.findFirst().orElse(null);
|
||||
|
||||
if (cd_prueba != null) {
|
||||
Map<String, Object> res = getShippingCost(cd_prueba, peso, 1, locale);
|
||||
if (Boolean.FALSE.equals(res.get("success"))) {
|
||||
errorShipementCost = true;
|
||||
} else {
|
||||
shipment += (Double) res.get("shipment");
|
||||
iva21 += (Double) res.get("iva21");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double totalBeforeDiscount = base + iva4 + iva21 + shipment;
|
||||
int fidelizacion = this.getDescuentoFidelizacion(cart.getUserId());
|
||||
double descuento = totalBeforeDiscount * fidelizacion / 100.0;
|
||||
double total = totalBeforeDiscount - descuento;
|
||||
|
||||
// Redondeo a 2 decimales
|
||||
base = Utils.round2(base);
|
||||
iva4 = Utils.round2(iva4);
|
||||
iva21 = Utils.round2(iva21);
|
||||
shipment = Utils.round2(shipment);
|
||||
descuento = Utils.round2(descuento);
|
||||
total = Utils.round2(total);
|
||||
|
||||
Map<String, Object> summary = new HashMap<>();
|
||||
summary.put("base", base);
|
||||
summary.put("iva4", iva4);
|
||||
summary.put("iva21", iva21);
|
||||
summary.put("shipment", shipment);
|
||||
summary.put("fidelizacion", fidelizacion);
|
||||
summary.put("descuento", descuento);
|
||||
summary.put("total", total);
|
||||
summary.put("amountCents", Math.round(total * 100));
|
||||
summary.put("errorShipmentCost", errorShipementCost);
|
||||
summary.put("cartId", cart.getId());
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
public int getDescuentoFidelizacion(Long userId) {
|
||||
// descuento entre el 1% y el 6% para clientes fidelidad (mas de 1500€ en el
|
||||
// ultimo año)
|
||||
Instant haceUnAno = Instant.now().minusSeconds(365 * 24 * 60 * 60);
|
||||
double totalGastado = pedidoRepository.sumTotalByCreatedByAndCreatedAtAfter(userId, haceUnAno);
|
||||
if (totalGastado < 1200) {
|
||||
return 0;
|
||||
} else if (totalGastado >= 1200 && totalGastado < 1999) {
|
||||
return 1;
|
||||
} else if (totalGastado >= 2000 && totalGastado < 2999) {
|
||||
return 2;
|
||||
} else if (totalGastado >= 3000 && totalGastado < 3999) {
|
||||
return 3;
|
||||
} else if (totalGastado >= 4000 && totalGastado < 4999) {
|
||||
return 4;
|
||||
} else if (totalGastado >= 5000) {
|
||||
return 5;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public Map<String, Object> getCartSummary(Cart cart, Locale locale) {
|
||||
Map<String, Object> raw = getCartSummaryRaw(cart, locale);
|
||||
|
||||
double base = (Double) raw.get("base");
|
||||
double iva4 = (Double) raw.get("iva4");
|
||||
double iva21 = (Double) raw.get("iva21");
|
||||
double shipment = (Double) raw.get("shipment");
|
||||
int fidelizacion = (Integer) raw.get("fidelizacion");
|
||||
double descuento = (Double) raw.get("descuento");
|
||||
double total = (Double) raw.get("total");
|
||||
|
||||
Map<String, Object> summary = new HashMap<>();
|
||||
summary.put("base", Utils.formatCurrency(base, locale));
|
||||
summary.put("iva4", Utils.formatCurrency(iva4, locale));
|
||||
summary.put("iva21", Utils.formatCurrency(iva21, locale));
|
||||
summary.put("shipment", Utils.formatCurrency(shipment, locale));
|
||||
summary.put("fidelizacion", fidelizacion + "%");
|
||||
summary.put("descuento", Utils.formatCurrency(-descuento, locale)); // negativo para mostrar
|
||||
summary.put("total", Utils.formatCurrency(total, locale));
|
||||
summary.put("amountCents", raw.get("amountCents"));
|
||||
summary.put("errorShipmentCost", raw.get("errorShipmentCost"));
|
||||
summary.put("cartId", raw.get("cartId"));
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, Object> getCartDirecciones(Long cartId, Locale locale) {
|
||||
Cart cart = cartRepo.findByIdFetchAll(cartId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
List<CartDireccion> direcciones = cart.getDirecciones();
|
||||
|
||||
if (cart.getOnlyOneShipment() && !direcciones.isEmpty()) {
|
||||
result.put("mainDir", direcciones.get(0).toDireccionCard(messageSource, locale));
|
||||
} else {
|
||||
List<DireccionCardDTO> dirCards = cart.getDirecciones().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(cd -> cd.toDireccionCard(messageSource, locale))
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
result.put("direcciones", dirCards);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Boolean updateCart(Long cartId, UpdateCartRequest request) {
|
||||
|
||||
try {
|
||||
Cart cart = cartRepo.findById(cartId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
|
||||
cart.setOnlyOneShipment(request.isOnlyOneShipment());
|
||||
// Borramos todas las direcciones actuales de la bbdd
|
||||
// Opcional (limpieza): romper backref antes de clear
|
||||
for (CartDireccion d : cart.getDirecciones()) {
|
||||
d.setCart(null);
|
||||
}
|
||||
cart.getDirecciones().clear();
|
||||
// Guardamos las direcciones
|
||||
List<DireccionShipment> direcciones = request.getDirecciones();
|
||||
if (direcciones != null && direcciones.size() > 0) {
|
||||
for (DireccionShipment dir : direcciones) {
|
||||
// Crear una nueva CartDireccion por cada item
|
||||
CartDireccion cd = new CartDireccion();
|
||||
cd.setCart(cart);
|
||||
cd.setDireccion(dir.getId() != null ? direccionService.findById(dir.getId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Dirección no encontrada")) : null);
|
||||
cd.setIsPalets(dir.getIsPalets() != null ? dir.getIsPalets() : false);
|
||||
cd.setUnidades(dir.getUnidades() != null ? dir.getUnidades() : null);
|
||||
if (dir.getPresupuestoId() != null) {
|
||||
Presupuesto p = presupuestoRepo.findById(dir.getPresupuestoId())
|
||||
.orElse(null);
|
||||
cd.setPresupuesto(p);
|
||||
}
|
||||
cart.addDireccion(cd);
|
||||
}
|
||||
} else {
|
||||
|
||||
}
|
||||
cartRepo.save(cart);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
// Manejo de excepciones
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean moveCartToCustomer(Long cartId, Long customerId) {
|
||||
try {
|
||||
|
||||
// Remove the cart from the customer if they have one
|
||||
Cart existingCart = cartRepo.findByUserIdAndStatus(customerId, Cart.Status.ACTIVE)
|
||||
.orElse(null);
|
||||
if (existingCart != null) {
|
||||
cartRepo.delete(existingCart);
|
||||
}
|
||||
|
||||
Cart cart = cartRepo.findById(cartId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Carrito no encontrado"));
|
||||
|
||||
cart.setUserId(customerId);
|
||||
cartRepo.save(cart);
|
||||
// Se mueven los presupuestos de cartitems a ese usuario
|
||||
List<CartItem> items = itemRepo.findByCartId(cart.getId());
|
||||
for (CartItem item : items) {
|
||||
Presupuesto p = item.getPresupuesto();
|
||||
p.setUser(userService.findById(customerId));
|
||||
presupuestoRepo.save(p);
|
||||
}
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
// Manejo de excepciones
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// delete cart directions by direccion id in ACTIVE carts
|
||||
@Transactional
|
||||
public void deleteCartDireccionesByDireccionId(Long direccionId) {
|
||||
|
||||
cartDireccionRepo.deleteByDireccionIdAndCartStatus(direccionId, Cart.Status.ACTIVE);
|
||||
}
|
||||
|
||||
|
||||
/***************************************
|
||||
* MÉTODOS PRIVADOS
|
||||
***************************************/
|
||||
|
||||
private Map<String, Object> getShippingCost(
|
||||
CartDireccion cd,
|
||||
Double peso,
|
||||
Integer unidades,
|
||||
Locale locale) {
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
try {
|
||||
Map<String, Object> data = Map.of(
|
||||
"cp", cd.getDireccion().getCp(),
|
||||
"pais_code3", cd.getDireccion().getPaisCode3(),
|
||||
"peso", peso != null ? peso : 0.0,
|
||||
"unidades", unidades,
|
||||
"palets", Boolean.TRUE.equals(cd.getIsPalets()) ? 1 : 0);
|
||||
|
||||
var shipmentCost = skApiClient.getCosteEnvio(data, locale);
|
||||
|
||||
if (shipmentCost != null && shipmentCost.get("data") != null) {
|
||||
Number n = (Number) shipmentCost.get("data");
|
||||
double cost = n.doubleValue();
|
||||
|
||||
result.put("success", true);
|
||||
result.put("shipment", cost);
|
||||
result.put("iva21", cost * 0.21);
|
||||
} else {
|
||||
result.put("success", false);
|
||||
result.put("shipment", 0.0);
|
||||
result.put("iva21", 0.0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
result.put("success", false);
|
||||
result.put("shipment", 0.0);
|
||||
result.put("iva21", 0.0);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
package com.imprimelibros.erp.cart.dto;
|
||||
|
||||
import com.imprimelibros.erp.cart.Cart;
|
||||
import com.imprimelibros.erp.cart.CartDireccion;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface CartDireccionRepository extends JpaRepository<CartDireccion, Long> {
|
||||
|
||||
// Borrado masivo por cart_id
|
||||
void deleteByCartId(Long cartId);
|
||||
|
||||
// Lectura por cart_id (útil para componer respuestas)
|
||||
List<CartDireccion> findByCartId(Long cartId);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query("""
|
||||
delete from CartDireccion cd
|
||||
where cd.direccion.id = :direccionId
|
||||
and cd.cart.status = :status
|
||||
""")
|
||||
int deleteByDireccionIdAndCartStatus(@Param("direccionId") Long direccionId,
|
||||
@Param("status") Cart.Status status);
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package com.imprimelibros.erp.cart.dto;
|
||||
|
||||
import com.imprimelibros.erp.direcciones.Direccion;
|
||||
|
||||
public class DireccionCardDTO {
|
||||
private final Direccion direccion;
|
||||
private final Long presupuestoId;
|
||||
private final Integer unidades;
|
||||
private final Boolean isPalets;
|
||||
private final String pais;
|
||||
|
||||
public DireccionCardDTO(Direccion direccion, Long presupuestoId, Integer unidades, Boolean isPalets, String pais) {
|
||||
this.direccion = direccion;
|
||||
this.presupuestoId = presupuestoId;
|
||||
this.unidades = unidades;
|
||||
this.isPalets = isPalets;
|
||||
this.pais = pais;
|
||||
}
|
||||
|
||||
public Direccion getDireccion() {
|
||||
return direccion;
|
||||
}
|
||||
|
||||
public Long getPresupuestoId() {
|
||||
return presupuestoId;
|
||||
}
|
||||
|
||||
public Integer getUnidades() {
|
||||
return unidades;
|
||||
}
|
||||
|
||||
public Boolean getIsPalets() {
|
||||
return isPalets;
|
||||
}
|
||||
|
||||
public String getPais() {
|
||||
return pais;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package com.imprimelibros.erp.cart.dto;
|
||||
|
||||
public class DireccionShipment {
|
||||
|
||||
private Long id; // puede no venir → null
|
||||
private String cp; // puede no venir → null
|
||||
private String paisCode3; // puede no venir → null
|
||||
private Long presupuestoId; // puede no venir → null
|
||||
private Integer unidades; // puede no venir → null
|
||||
private Boolean isPalets; // puede no venir → null
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getCp() {
|
||||
return cp;
|
||||
}
|
||||
|
||||
public void setCp(String cp) {
|
||||
this.cp = cp;
|
||||
}
|
||||
|
||||
public String getPaisCode3() {
|
||||
return paisCode3;
|
||||
}
|
||||
|
||||
public void setPaisCode3(String paisCode3) {
|
||||
this.paisCode3 = paisCode3;
|
||||
}
|
||||
|
||||
public Long getPresupuestoId() {
|
||||
return presupuestoId;
|
||||
}
|
||||
|
||||
public void setPresupuestoId(Long presupuestoId) {
|
||||
this.presupuestoId = presupuestoId;
|
||||
}
|
||||
|
||||
public Integer getUnidades() {
|
||||
return unidades;
|
||||
}
|
||||
|
||||
public void setUnidades(Integer unidades) {
|
||||
this.unidades = unidades;
|
||||
}
|
||||
|
||||
public Boolean getIsPalets() {
|
||||
return isPalets;
|
||||
}
|
||||
|
||||
public void setIsPalets(Boolean isPalets) {
|
||||
this.isPalets = isPalets;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
package com.imprimelibros.erp.cart.dto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class UpdateCartRequest {
|
||||
private Boolean onlyOneShipment = Boolean.FALSE; // default: false
|
||||
private List<DireccionShipment> direcciones = new ArrayList<>();
|
||||
|
||||
public boolean isOnlyOneShipment() { // boolean-style getter
|
||||
return Boolean.TRUE.equals(onlyOneShipment);
|
||||
}
|
||||
|
||||
public void setOnlyOneShipment(Boolean onlyOneShipment) {
|
||||
this.onlyOneShipment = onlyOneShipment;
|
||||
}
|
||||
|
||||
public List<DireccionShipment> getDirecciones() {
|
||||
return direcciones;
|
||||
}
|
||||
|
||||
public void setDirecciones(List<DireccionShipment> direcciones) {
|
||||
this.direcciones = (direcciones != null) ? direcciones : new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,75 @@
|
||||
package com.imprimelibros.erp.checkout;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.i18n.TranslationService;
|
||||
import com.imprimelibros.erp.paises.PaisesService;
|
||||
import com.imprimelibros.erp.direcciones.Direccion;
|
||||
import com.imprimelibros.erp.direcciones.DireccionService;
|
||||
import com.imprimelibros.erp.cart.Cart;
|
||||
import com.imprimelibros.erp.cart.CartService;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/checkout")
|
||||
public class CheckoutController {
|
||||
|
||||
protected CartService cartService;
|
||||
protected TranslationService translationService;
|
||||
protected PaisesService paisesService;
|
||||
protected DireccionService direccionService;
|
||||
protected MessageSource messageSource;
|
||||
|
||||
public CheckoutController(CartService cartService, TranslationService translationService,
|
||||
PaisesService paisesService, DireccionService direccionService, MessageSource messageSource) {
|
||||
this.cartService = cartService;
|
||||
this.translationService = translationService;
|
||||
this.paisesService = paisesService;
|
||||
this.direccionService = direccionService;
|
||||
this.messageSource = messageSource;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String view(Model model, Principal principal, Locale locale) {
|
||||
|
||||
List<String> keys = List.of(
|
||||
"app.cancelar",
|
||||
"app.seleccionar",
|
||||
"app.yes",
|
||||
"checkout.billing-address.title",
|
||||
"checkout.billing-address.new-address",
|
||||
"checkout.billing-address.select-placeholder",
|
||||
"checkout.billing-address.errors.noAddressSelected");
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
|
||||
Long userId = Utils.currentUserId(principal);
|
||||
Cart cart = cartService.getOrCreateActiveCart(userId);
|
||||
model.addAttribute("summary", cartService.getCartSummary(cart, locale));
|
||||
return "imprimelibros/checkout/checkout"; // crea esta vista si quieres (tabla simple)
|
||||
}
|
||||
|
||||
@GetMapping("/get-address/{id}")
|
||||
public String getDireccionCard(@PathVariable Long id, Model model, Locale locale) {
|
||||
Direccion dir = direccionService.findById(id)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
model.addAttribute("pais", messageSource.getMessage("paises." + dir.getPais().getKeyword(), null,
|
||||
dir.getPais().getKeyword(), locale));
|
||||
model.addAttribute("direccion", dir);
|
||||
|
||||
return "imprimelibros/direcciones/direccionBillingCard :: direccionBillingCard(direccion=${direccion}, pais=${pais})";
|
||||
}
|
||||
}
|
||||
@ -2,16 +2,24 @@ package com.imprimelibros.erp.common;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.security.Principal;
|
||||
import java.text.NumberFormat;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
@ -22,6 +30,8 @@ import com.imprimelibros.erp.presupuesto.classes.PresupuestoFormatter;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
import com.imprimelibros.erp.presupuesto.maquetacion.MaquetacionMatrices;
|
||||
import com.imprimelibros.erp.presupuesto.marcapaginas.Marcapaginas;
|
||||
import com.imprimelibros.erp.users.User;
|
||||
import com.imprimelibros.erp.users.UserDetailsImpl;
|
||||
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.Path;
|
||||
@ -40,6 +50,75 @@ public class Utils {
|
||||
this.messageSource = messageSource;
|
||||
}
|
||||
|
||||
public static List<Map<String, Object>> decodeJsonList(String json) {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
try {
|
||||
return mapper.readValue(json, new TypeReference<List<Map<String, Object>>>() {
|
||||
});
|
||||
} catch (JsonProcessingException e) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<String, Object> decodeJsonMap(String json) {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
try {
|
||||
return mapper.readValue(json, new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
} catch (JsonProcessingException e) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
public static double round2(double value) {
|
||||
return BigDecimal.valueOf(value)
|
||||
.setScale(2, RoundingMode.HALF_UP)
|
||||
.doubleValue();
|
||||
}
|
||||
|
||||
public static boolean isCurrentUserAdmin() {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
return auth.getAuthorities().stream()
|
||||
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")
|
||||
|| a.getAuthority().equals("ROLE_SUPERADMIN"));
|
||||
}
|
||||
|
||||
public static Long currentUserId(Principal principal) {
|
||||
|
||||
if (principal == null) {
|
||||
throw new IllegalStateException("Usuario no autenticado");
|
||||
}
|
||||
|
||||
if (principal instanceof Authentication auth) {
|
||||
Object principalObj = auth.getPrincipal();
|
||||
|
||||
if (principalObj instanceof UserDetailsImpl udi) {
|
||||
return udi.getId();
|
||||
} else if (principalObj instanceof User u && u.getId() != null) {
|
||||
return u.getId();
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("No se pudo obtener el ID del usuario actual");
|
||||
}
|
||||
|
||||
public static User currentUser(Principal principal) {
|
||||
|
||||
if (principal == null) {
|
||||
throw new IllegalStateException("Usuario no autenticado");
|
||||
}
|
||||
|
||||
if (principal instanceof Authentication auth) {
|
||||
Object principalObj = auth.getPrincipal();
|
||||
|
||||
if (principalObj instanceof UserDetailsImpl udi) {
|
||||
return udi.getUser();
|
||||
} else if (principalObj instanceof User u && u.getId() != null) {
|
||||
return u;
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("No se pudo obtener el ID del usuario actual");
|
||||
}
|
||||
|
||||
public static String formatCurrency(BigDecimal amount, Locale locale) {
|
||||
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(locale);
|
||||
return currencyFormatter.format(amount);
|
||||
@ -291,4 +370,70 @@ public class Utils {
|
||||
resumen.put("servicios", serviciosExtras);
|
||||
return resumen;
|
||||
}
|
||||
|
||||
public static String formatDateTime(LocalDateTime dateTime, Locale locale) {
|
||||
if (dateTime == null) {
|
||||
return "";
|
||||
}
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm", locale);
|
||||
return dateTime.format(formatter);
|
||||
}
|
||||
|
||||
public static String formatDate(LocalDateTime dateTime, Locale locale) {
|
||||
if (dateTime == null) {
|
||||
return "";
|
||||
}
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy", locale);
|
||||
return dateTime.format(formatter);
|
||||
}
|
||||
|
||||
public static String formatInstant(Instant instant, Locale locale) {
|
||||
if (instant == null) {
|
||||
return "";
|
||||
}
|
||||
ZoneId zone = zoneIdForLocale(locale);
|
||||
|
||||
DateTimeFormatter formatter = DateTimeFormatter
|
||||
.ofPattern("dd/MM/yyyy HH:mm", locale)
|
||||
.withZone(zone);
|
||||
|
||||
return formatter.format(instant);
|
||||
}
|
||||
|
||||
/*********************
|
||||
* Metodos auxiliares
|
||||
*/
|
||||
private static ZoneId zoneIdForLocale(Locale locale) {
|
||||
if (locale == null || locale.getCountry().isEmpty()) {
|
||||
return ZoneId.of("UTC");
|
||||
}
|
||||
|
||||
// Buscar timezones cuyo ID termine con el country code
|
||||
// Ej: ES -> Europe/Madrid
|
||||
String country = locale.getCountry();
|
||||
|
||||
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
|
||||
for (String id : zoneIds) {
|
||||
// TimeZone# getID() no funciona por país, pero sí el prefijo + país
|
||||
if (id.endsWith("/" + country) || id.contains("/" + country)) {
|
||||
return ZoneId.of(id);
|
||||
}
|
||||
}
|
||||
|
||||
// fallback por regiones comunes (manual pero muy útil)
|
||||
Map<String, String> fallback = Map.of(
|
||||
"ES", "Europe/Madrid",
|
||||
"MX", "America/Mexico_City",
|
||||
"AR", "America/Argentina/Buenos_Aires",
|
||||
"US", "America/New_York",
|
||||
"GB", "Europe/London",
|
||||
"FR", "Europe/Paris");
|
||||
|
||||
if (fallback.containsKey(country)) {
|
||||
return ZoneId.of(fallback.get(country));
|
||||
}
|
||||
|
||||
return ZoneId.systemDefault(); // último fallback
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
package com.imprimelibros.erp.common.jpa;
|
||||
|
||||
import com.imprimelibros.erp.users.User;
|
||||
import jakarta.persistence.*;
|
||||
import org.springframework.data.annotation.CreatedBy;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedBy;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@MappedSuperclass
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public abstract class AbstractAuditedEntitySoftTs {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "created_at", updatable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@LastModifiedDate
|
||||
@Column(name = "updated_at")
|
||||
private Instant updatedAt;
|
||||
|
||||
@CreatedBy
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "created_by")
|
||||
private User createdBy;
|
||||
|
||||
@LastModifiedBy
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "updated_by")
|
||||
private User updatedBy;
|
||||
|
||||
@Column(name = "deleted_at")
|
||||
private Instant deletedAt;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "deleted_by")
|
||||
private User deletedBy;
|
||||
|
||||
// Getters/Setters
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Instant getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public Instant getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
|
||||
|
||||
public User getCreatedBy() { return createdBy; }
|
||||
public void setCreatedBy(User createdBy) { this.createdBy = createdBy; }
|
||||
|
||||
public User getUpdatedBy() { return updatedBy; }
|
||||
public void setUpdatedBy(User updatedBy) { this.updatedBy = updatedBy; }
|
||||
|
||||
public Instant getDeletedAt() { return deletedAt; }
|
||||
public void setDeletedAt(Instant deletedAt) { this.deletedAt = deletedAt; }
|
||||
|
||||
public User getDeletedBy() { return deletedBy; }
|
||||
public void setDeletedBy(User deletedBy) { this.deletedBy = deletedBy; }
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package com.imprimelibros.erp.common.web;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Entities;
|
||||
|
||||
public class HtmlToXhtml {
|
||||
|
||||
public static String toXhtml(String html) {
|
||||
if (html == null || html.isBlank()) return "";
|
||||
|
||||
Document doc = Jsoup.parseBodyFragment(html);
|
||||
|
||||
doc.outputSettings()
|
||||
.syntax(Document.OutputSettings.Syntax.xml) // => <br/>
|
||||
.escapeMode(Entities.EscapeMode.xhtml) // entidades XHTML
|
||||
.prettyPrint(false); // no metas saltos raros
|
||||
|
||||
// devolvemos sólo el contenido del body (sin <html><head>…)
|
||||
return doc.body().html();
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package com.imprimelibros.erp.config;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||
@ -10,13 +11,16 @@ import jakarta.validation.ValidatorFactory;
|
||||
@Configuration
|
||||
public class BeanValidationConfig {
|
||||
|
||||
// Asegura que usamos la factory de Spring (con SpringConstraintValidatorFactory)
|
||||
// Usa el MessageSource (messages*.properties) para resolver {códigos}
|
||||
@Bean
|
||||
public LocalValidatorFactoryBean validator() {
|
||||
return new LocalValidatorFactoryBean();
|
||||
public LocalValidatorFactoryBean validator(MessageSource messageSource) {
|
||||
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
|
||||
bean.setValidationMessageSource(messageSource); // <-- CLAVE
|
||||
|
||||
return bean;
|
||||
}
|
||||
|
||||
// Inserta esa factory en Hibernate/JPA
|
||||
// Inserta esa factory en Hibernate/JPA (opcional pero correcto)
|
||||
@Bean
|
||||
public HibernatePropertiesCustomizer hibernateValidationCustomizer(ValidatorFactory vf) {
|
||||
return props -> props.put("jakarta.persistence.validation.factory", vf);
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
package com.imprimelibros.erp.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
public class MethodSecurityConfig {
|
||||
}
|
||||
@ -30,144 +30,151 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
private final DataSource dataSource;
|
||||
private final DataSource dataSource;
|
||||
|
||||
public SecurityConfig(DataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
public SecurityConfig(DataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
// ========== Beans base ==========
|
||||
// ========== Beans base ==========
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
// Remember-me (tabla persistent_logins)
|
||||
@Bean
|
||||
public PersistentTokenRepository persistentTokenRepository() {
|
||||
JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
|
||||
repo.setDataSource(dataSource);
|
||||
// repo.setCreateTableOnStartup(true); // solo 1ª vez si necesitas crear la
|
||||
// tabla
|
||||
return repo;
|
||||
}
|
||||
// Remember-me (tabla persistent_logins)
|
||||
@Bean
|
||||
public PersistentTokenRepository persistentTokenRepository() {
|
||||
JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
|
||||
repo.setDataSource(dataSource);
|
||||
// repo.setCreateTableOnStartup(true); // solo 1ª vez si necesitas crear la
|
||||
// tabla
|
||||
return repo;
|
||||
}
|
||||
|
||||
// Provider que soporta UsernamePasswordAuthenticationToken
|
||||
private static RequestMatcher pathStartsWith(String... prefixes) {
|
||||
return new RequestMatcher() {
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
String uri = request.getRequestURI();
|
||||
if (uri == null)
|
||||
return false;
|
||||
for (String p : prefixes) {
|
||||
if (uri.startsWith(p))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
// Provider que soporta UsernamePasswordAuthenticationToken
|
||||
private static RequestMatcher pathStartsWith(String... prefixes) {
|
||||
return new RequestMatcher() {
|
||||
@Override
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
String uri = request.getRequestURI();
|
||||
if (uri == null)
|
||||
return false;
|
||||
for (String p : prefixes) {
|
||||
if (uri.startsWith(p))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(
|
||||
HttpSecurity http,
|
||||
@Value("${security.rememberme.key}") String keyRememberMe,
|
||||
UserDetailsService userDetailsService,
|
||||
PersistentTokenRepository tokenRepo,
|
||||
PasswordEncoder passwordEncoder, UserServiceImpl userServiceImpl) throws Exception {
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(
|
||||
HttpSecurity http,
|
||||
@Value("${security.rememberme.key}") String keyRememberMe,
|
||||
UserDetailsService userDetailsService,
|
||||
PersistentTokenRepository tokenRepo,
|
||||
PasswordEncoder passwordEncoder, UserServiceImpl userServiceImpl) throws Exception {
|
||||
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userServiceImpl);
|
||||
provider.setPasswordEncoder(passwordEncoder);
|
||||
http.authenticationProvider(provider);
|
||||
http
|
||||
.authenticationProvider(provider)
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userServiceImpl);
|
||||
provider.setPasswordEncoder(passwordEncoder);
|
||||
http.authenticationProvider(provider);
|
||||
http
|
||||
.authenticationProvider(provider)
|
||||
|
||||
.sessionManagement(session -> session
|
||||
//.invalidSessionUrl("/login?expired")
|
||||
.maximumSessions(1))
|
||||
.sessionManagement(session -> session
|
||||
// .invalidSessionUrl("/login?expired")
|
||||
.maximumSessions(1))
|
||||
|
||||
// Ignora CSRF para tu recurso público (sin Ant/Mvc matchers)
|
||||
.csrf(csrf -> csrf
|
||||
.ignoringRequestMatchers(pathStartsWith("/presupuesto/public/")))
|
||||
// Ignora CSRF para tu recurso público (sin Ant/Mvc matchers)
|
||||
.csrf(csrf -> csrf
|
||||
.ignoringRequestMatchers(pathStartsWith("/presupuesto/public/"),
|
||||
pathStartsWith("/pagos/redsys/")))
|
||||
// ====== RequestCache: sólo navegaciones HTML reales ======
|
||||
.requestCache(rc -> {
|
||||
HttpSessionRequestCache cache = new HttpSessionRequestCache();
|
||||
|
||||
// ====== RequestCache: sólo navegaciones HTML reales ======
|
||||
.requestCache(rc -> {
|
||||
HttpSessionRequestCache cache = new HttpSessionRequestCache();
|
||||
// Navegación HTML (por tipo o por cabecera Accept)
|
||||
RequestMatcher htmlPage = new OrRequestMatcher(
|
||||
new MediaTypeRequestMatcher(MediaType.TEXT_HTML),
|
||||
new MediaTypeRequestMatcher(MediaType.APPLICATION_XHTML_XML),
|
||||
new RequestHeaderRequestMatcher("Accept", "text/html"));
|
||||
|
||||
// Navegación HTML (por tipo o por cabecera Accept)
|
||||
RequestMatcher htmlPage = new OrRequestMatcher(
|
||||
new MediaTypeRequestMatcher(MediaType.TEXT_HTML),
|
||||
new MediaTypeRequestMatcher(MediaType.APPLICATION_XHTML_XML),
|
||||
new RequestHeaderRequestMatcher("Accept", "text/html"));
|
||||
// No AJAX
|
||||
RequestMatcher nonAjax = new NegatedRequestMatcher(
|
||||
new RequestHeaderRequestMatcher("X-Requested-With",
|
||||
"XMLHttpRequest"));
|
||||
|
||||
// No AJAX
|
||||
RequestMatcher nonAjax = new NegatedRequestMatcher(
|
||||
new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));
|
||||
// Excluir sondas .well-known
|
||||
RequestMatcher notWellKnown = new NegatedRequestMatcher(
|
||||
pathStartsWith("/.well-known/"));
|
||||
|
||||
// Excluir sondas .well-known
|
||||
RequestMatcher notWellKnown = new NegatedRequestMatcher(pathStartsWith("/.well-known/"));
|
||||
// Excluir estáticos: comunes + tu /assets/**
|
||||
RequestMatcher notStatic = new AndRequestMatcher(
|
||||
new NegatedRequestMatcher(PathRequest.toStaticResources()
|
||||
.atCommonLocations()),
|
||||
new NegatedRequestMatcher(pathStartsWith("/assets/")));
|
||||
|
||||
// Excluir estáticos: comunes + tu /assets/**
|
||||
RequestMatcher notStatic = new AndRequestMatcher(
|
||||
new NegatedRequestMatcher(PathRequest.toStaticResources().atCommonLocations()),
|
||||
new NegatedRequestMatcher(pathStartsWith("/assets/")));
|
||||
|
||||
RequestMatcher cartCount = new AndRequestMatcher(
|
||||
new NegatedRequestMatcher(PathRequest.toStaticResources().atCommonLocations()),
|
||||
new NegatedRequestMatcher(pathStartsWith("/cart/count")));
|
||||
RequestMatcher cartCount = new AndRequestMatcher(
|
||||
new NegatedRequestMatcher(PathRequest.toStaticResources()
|
||||
.atCommonLocations()),
|
||||
new NegatedRequestMatcher(pathStartsWith("/cart/count")));
|
||||
|
||||
cache.setRequestMatcher(new AndRequestMatcher(htmlPage, nonAjax, notStatic, notWellKnown, cartCount));
|
||||
rc.requestCache(cache);
|
||||
})
|
||||
// ========================================================
|
||||
cache.setRequestMatcher(new AndRequestMatcher(htmlPage, nonAjax, notStatic,
|
||||
notWellKnown, cartCount));
|
||||
rc.requestCache(cache);
|
||||
})
|
||||
// ========================================================
|
||||
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// Aquí usa patrones String (no deprecados)
|
||||
.requestMatchers(
|
||||
"/",
|
||||
"/login",
|
||||
"/signup",
|
||||
"/verify",
|
||||
"/auth/password/**",
|
||||
"/assets/**",
|
||||
"/css/**",
|
||||
"/js/**",
|
||||
"/images/**",
|
||||
"/public/**",
|
||||
"/presupuesto/public/**",
|
||||
"/error",
|
||||
"/favicon.ico",
|
||||
"/.well-known/**", // opcional
|
||||
"/api/pdf/presupuesto/**"
|
||||
).permitAll()
|
||||
.requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN")
|
||||
.anyRequest().authenticated())
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// Aquí usa patrones String (no deprecados)
|
||||
.requestMatchers(
|
||||
"/",
|
||||
"/login",
|
||||
"/signup",
|
||||
"/verify",
|
||||
"/auth/password/**",
|
||||
"/assets/**",
|
||||
"/css/**",
|
||||
"/js/**",
|
||||
"/images/**",
|
||||
"/public/**",
|
||||
"/presupuesto/public/**",
|
||||
"/error",
|
||||
"/favicon.ico",
|
||||
"/.well-known/**", // opcional
|
||||
"/api/pdf/presupuesto/**",
|
||||
"/pagos/redsys/**"
|
||||
)
|
||||
.permitAll()
|
||||
.requestMatchers("/users/**").hasAnyRole("SUPERADMIN", "ADMIN")
|
||||
.anyRequest().authenticated())
|
||||
|
||||
.formLogin(login -> login
|
||||
.loginPage("/login").permitAll()
|
||||
.loginProcessingUrl("/login")
|
||||
.usernameParameter("username")
|
||||
.passwordParameter("password")
|
||||
.defaultSuccessUrl("/", false) // respeta SavedRequest (ya filtrada)
|
||||
.failureUrl("/login?error"))
|
||||
.formLogin(login -> login
|
||||
.loginPage("/login").permitAll()
|
||||
.loginProcessingUrl("/login")
|
||||
.usernameParameter("username")
|
||||
.passwordParameter("password")
|
||||
.defaultSuccessUrl("/", false) // respeta SavedRequest (ya filtrada)
|
||||
.failureUrl("/login?error"))
|
||||
|
||||
.rememberMe(rm -> rm
|
||||
.key(keyRememberMe)
|
||||
.rememberMeParameter("remember-me")
|
||||
.rememberMeCookieName("IMPRIMELIBROS_REMEMBER")
|
||||
.tokenValiditySeconds(60 * 60 * 24 * 2)
|
||||
.userDetailsService(userDetailsService)
|
||||
.tokenRepository(tokenRepo))
|
||||
.rememberMe(rm -> rm
|
||||
.key(keyRememberMe)
|
||||
.rememberMeParameter("remember-me")
|
||||
.rememberMeCookieName("IMPRIMELIBROS_REMEMBER")
|
||||
.tokenValiditySeconds(60 * 60 * 24 * 2)
|
||||
.userDetailsService(userDetailsService)
|
||||
.tokenRepository(tokenRepo))
|
||||
|
||||
.logout(logout -> logout
|
||||
.logoutUrl("/logout")
|
||||
.logoutSuccessUrl("/")
|
||||
.invalidateHttpSession(true)
|
||||
.deleteCookies("JSESSIONID", "IMPRIMELIBROS_REMEMBER")
|
||||
.permitAll());
|
||||
.logout(logout -> logout
|
||||
.logoutUrl("/logout")
|
||||
.logoutSuccessUrl("/")
|
||||
.invalidateHttpSession(true)
|
||||
.deleteCookies("JSESSIONID", "IMPRIMELIBROS_REMEMBER")
|
||||
.permitAll());
|
||||
|
||||
return http.build();
|
||||
}
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,11 +146,11 @@ public class MargenPresupuestoController {
|
||||
if (id != null) {
|
||||
var opt = repo.findById(id);
|
||||
if (opt.isEmpty()) {
|
||||
binding.reject("usuarios.error.noEncontrado",
|
||||
messageSource.getMessage("usuarios.error.noEncontrado", null, locale));
|
||||
binding.reject("margenes-presupuesto.error.noEncontrado",
|
||||
messageSource.getMessage("margenes-presupuesto.error.noEncontrado", null, locale));
|
||||
response.setStatus(404);
|
||||
model.addAttribute("action", "/users/" + id);
|
||||
return "imprimelibros/users/user-form :: userForm";
|
||||
model.addAttribute("action", "/configuracion/margenes-presupuesto/" + id);
|
||||
return "imprimelibros/configuracion/margenes-presupuesto/margenes-presupuesto-form :: margenesPresupuestoForm";
|
||||
}
|
||||
|
||||
model.addAttribute("margenPresupuesto", opt.get());
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
package com.imprimelibros.erp.configurationERP;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/configuracion/variables-sistema")
|
||||
@PreAuthorize("hasRole('SUPERADMIN')")
|
||||
public class VariablesController {
|
||||
|
||||
@GetMapping()
|
||||
public String list(Model model, Locale locale) {
|
||||
return new String();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
237
src/main/java/com/imprimelibros/erp/direcciones/Direccion.java
Normal file
237
src/main/java/com/imprimelibros/erp/direcciones/Direccion.java
Normal file
@ -0,0 +1,237 @@
|
||||
package com.imprimelibros.erp.direcciones;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.hibernate.annotations.SQLDelete;
|
||||
import org.hibernate.annotations.SQLRestriction;
|
||||
|
||||
import com.imprimelibros.erp.common.jpa.AbstractAuditedEntity;
|
||||
import com.imprimelibros.erp.paises.Paises;
|
||||
import com.imprimelibros.erp.users.User;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
@Entity
|
||||
@Table(name = "direcciones", indexes = {
|
||||
@Index(name = "idx_direcciones_user", columnList = "user_id"),
|
||||
@Index(name = "idx_direcciones_pais_code3", columnList = "pais_code3")
|
||||
})
|
||||
@SQLDelete(sql = "UPDATE direcciones SET deleted = 1, deleted_at = NOW(3) WHERE id = ?")
|
||||
@SQLRestriction("deleted = 0")
|
||||
public class Direccion extends AbstractAuditedEntity implements Serializable {
|
||||
|
||||
public enum TipoIdentificacionFiscal {
|
||||
DNI("direcciones.dni"),
|
||||
NIE("direcciones.nie"),
|
||||
CIF("direcciones.cif"),
|
||||
Pasaporte("direcciones.pasaporte"),
|
||||
VAT_ID("direcciones.vat_id");
|
||||
|
||||
private String key;
|
||||
|
||||
TipoIdentificacionFiscal(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
// --- FK a users(id)
|
||||
@NotNull(message = "{direcciones.form.error.required}")
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "user_id", nullable = false)
|
||||
private User user;
|
||||
|
||||
@NotBlank(message = "{direcciones.form.error.required}")
|
||||
@Column(name = "alias", length = 100, nullable = false)
|
||||
private String alias;
|
||||
|
||||
@NotBlank(message = "{direcciones.form.error.required}")
|
||||
@Column(name = "att", length = 150, nullable = false)
|
||||
private String att;
|
||||
|
||||
@NotBlank(message = "{direcciones.form.error.required}")
|
||||
@Column(name = "direccion", length = 255, nullable = false)
|
||||
private String direccion;
|
||||
|
||||
@NotNull(message = "{direcciones.form.error.required}")
|
||||
@Column(name = "cp", length = 20, nullable = false)
|
||||
private Integer cp;
|
||||
|
||||
@NotBlank(message = "{direcciones.form.error.required}")
|
||||
@Column(name = "ciudad", length = 100, nullable = false)
|
||||
private String ciudad;
|
||||
|
||||
@NotBlank(message = "{direcciones.form.error.required}")
|
||||
@Column(name = "provincia", length = 100, nullable = false)
|
||||
private String provincia;
|
||||
|
||||
// Usamos el code3 del país como FK lógica (String)
|
||||
@NotBlank(message = "{direcciones.form.error.required}")
|
||||
@Column(name = "pais_code3", length = 3, nullable = false)
|
||||
private String paisCode3 = "esp";
|
||||
|
||||
@NotBlank(message = "{direcciones.form.error.required}")
|
||||
@Column(name = "telefono", length = 30, nullable = false)
|
||||
private String telefono;
|
||||
|
||||
@Column(name = "instrucciones", length = 255)
|
||||
private String instrucciones;
|
||||
|
||||
@Column(name = "is_facturacion", nullable = false)
|
||||
private boolean direccionFacturacion = false;
|
||||
|
||||
@Column(name = "razon_social", length = 150)
|
||||
private String razonSocial;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "tipo_identificacion_fiscal", length = 20, nullable = false)
|
||||
private TipoIdentificacionFiscal tipoIdentificacionFiscal = TipoIdentificacionFiscal.DNI;
|
||||
|
||||
@Column(name = "identificacion_fiscal", length = 50)
|
||||
private String identificacionFiscal;
|
||||
|
||||
// --- Asociación opcional (read-only) a Pais por code3, si tienes la entidad
|
||||
// Pais
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "pais_code3", referencedColumnName = "code3", insertable = false, updatable = false)
|
||||
private Paises pais;
|
||||
|
||||
// --- Getters & Setters ---
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public String getAlias() {
|
||||
return alias;
|
||||
}
|
||||
|
||||
public void setAlias(String alias) {
|
||||
this.alias = alias;
|
||||
}
|
||||
|
||||
public String getAtt() {
|
||||
return att;
|
||||
}
|
||||
|
||||
public void setAtt(String att) {
|
||||
this.att = att;
|
||||
}
|
||||
|
||||
public String getDireccion() {
|
||||
return direccion;
|
||||
}
|
||||
|
||||
public void setDireccion(String direccion) {
|
||||
this.direccion = direccion;
|
||||
}
|
||||
|
||||
public Integer getCp() {
|
||||
return cp;
|
||||
}
|
||||
|
||||
public void setCp(Integer cp) {
|
||||
this.cp = cp;
|
||||
}
|
||||
|
||||
public String getCiudad() {
|
||||
return ciudad;
|
||||
}
|
||||
|
||||
public void setCiudad(String ciudad) {
|
||||
this.ciudad = ciudad;
|
||||
}
|
||||
|
||||
public String getProvincia() {
|
||||
return provincia;
|
||||
}
|
||||
|
||||
public void setProvincia(String provincia) {
|
||||
this.provincia = provincia;
|
||||
}
|
||||
|
||||
public String getPaisCode3() {
|
||||
return paisCode3;
|
||||
}
|
||||
|
||||
public void setPaisCode3(String paisCode3) {
|
||||
this.paisCode3 = paisCode3;
|
||||
}
|
||||
|
||||
public String getTelefono() {
|
||||
return telefono;
|
||||
}
|
||||
|
||||
public void setTelefono(String telefono) {
|
||||
this.telefono = telefono;
|
||||
}
|
||||
|
||||
public String getInstrucciones() {
|
||||
return instrucciones;
|
||||
}
|
||||
|
||||
public void setInstrucciones(String instrucciones) {
|
||||
this.instrucciones = instrucciones;
|
||||
}
|
||||
|
||||
public boolean isDireccionFacturacion() {
|
||||
return direccionFacturacion;
|
||||
}
|
||||
|
||||
public void setDireccionFacturacion(boolean direccionFacturacion) {
|
||||
this.direccionFacturacion = direccionFacturacion;
|
||||
}
|
||||
|
||||
public String getRazonSocial() {
|
||||
return razonSocial;
|
||||
}
|
||||
|
||||
public void setRazonSocial(String razonSocial) {
|
||||
this.razonSocial = razonSocial;
|
||||
}
|
||||
|
||||
public TipoIdentificacionFiscal getTipoIdentificacionFiscal() {
|
||||
return tipoIdentificacionFiscal;
|
||||
}
|
||||
|
||||
public void setTipoIdentificacionFiscal(TipoIdentificacionFiscal tipo) {
|
||||
this.tipoIdentificacionFiscal = tipo;
|
||||
}
|
||||
|
||||
public String getIdentificacionFiscal() {
|
||||
return identificacionFiscal;
|
||||
}
|
||||
|
||||
public void setIdentificacionFiscal(String identificacionFiscal) {
|
||||
this.identificacionFiscal = identificacionFiscal;
|
||||
}
|
||||
|
||||
public Paises getPais() {
|
||||
return pais;
|
||||
}
|
||||
|
||||
public void setPais(Paises pais) {
|
||||
this.pais = pais;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,551 @@
|
||||
package com.imprimelibros.erp.direcciones;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.imprimelibros.erp.cart.CartService;
|
||||
import com.imprimelibros.erp.datatables.DataTable;
|
||||
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
||||
import com.imprimelibros.erp.datatables.DataTablesResponse;
|
||||
import com.imprimelibros.erp.i18n.TranslationService;
|
||||
import com.imprimelibros.erp.paises.PaisesService;
|
||||
import com.imprimelibros.erp.users.User;
|
||||
import com.imprimelibros.erp.users.UserDao;
|
||||
import com.imprimelibros.erp.users.UserDetailsImpl;
|
||||
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/direcciones")
|
||||
public class DireccionController {
|
||||
|
||||
private final DireccionService direccionService;
|
||||
|
||||
protected final DireccionRepository repo;
|
||||
protected final PaisesService paisesService;
|
||||
protected final MessageSource messageSource;
|
||||
protected final UserDao userRepo;
|
||||
protected final TranslationService translationService;
|
||||
protected final CartService cartService;
|
||||
|
||||
public DireccionController(DireccionRepository repo, PaisesService paisesService,
|
||||
MessageSource messageSource, UserDao userRepo, TranslationService translationService,
|
||||
DireccionService direccionService, CartService cartService) {
|
||||
this.repo = repo;
|
||||
this.paisesService = paisesService;
|
||||
this.messageSource = messageSource;
|
||||
this.userRepo = userRepo;
|
||||
this.translationService = translationService;
|
||||
this.direccionService = direccionService;
|
||||
this.cartService = cartService;
|
||||
}
|
||||
|
||||
@GetMapping()
|
||||
public String viewDirecciones(Model model, Authentication auth, Locale locale) {
|
||||
|
||||
boolean isUser = auth != null && auth.getAuthorities().stream()
|
||||
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"));
|
||||
model.addAttribute("isUser", isUser ? 1 : 0);
|
||||
|
||||
List<String> keys = List.of(
|
||||
"direcciones.delete.title",
|
||||
"direcciones.delete.text",
|
||||
"direcciones.eliminar",
|
||||
"direcciones.delete.button",
|
||||
"app.yes",
|
||||
"app.cancelar",
|
||||
"direcciones.delete.ok.title",
|
||||
"direcciones.delete.ok.text",
|
||||
"direcciones.btn.edit",
|
||||
"direcciones.btn.delete",
|
||||
"direcciones.telefono", "direcciones.isFacturacionShort");
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
|
||||
if (isUser)
|
||||
return "imprimelibros/direcciones/direccion-list-cliente";
|
||||
else
|
||||
return "imprimelibros/direcciones/direccion-list";
|
||||
}
|
||||
|
||||
@GetMapping(value = "/datatable", produces = "application/json")
|
||||
@ResponseBody
|
||||
public DataTablesResponse<Map<String, Object>> datatable(
|
||||
HttpServletRequest request,
|
||||
Authentication authentication,
|
||||
Locale locale) {
|
||||
|
||||
DataTablesRequest dt = DataTablesParser.from(request);
|
||||
|
||||
// Columnas visibles / lógicas para el DataTable en el frontend:
|
||||
// id, cliente (nombre de usuario), alias, att, direccion, cp, ciudad,
|
||||
// provincia, pais
|
||||
List<String> searchable = List.of(
|
||||
"id",
|
||||
"cliente", "alias",
|
||||
"att", "direccion", "cp", "ciudad", "provincia", "pais");
|
||||
|
||||
List<String> orderable = List.of(
|
||||
"id",
|
||||
"cliente", "alias",
|
||||
"att", "direccion", "cp", "ciudad", "provincia", "pais");
|
||||
|
||||
// Filtro base por rol (ROLE_USER solo ve sus direcciones)
|
||||
Specification<Direccion> base = (root, query, cb) -> {
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
|
||||
if (authentication != null && authentication.getAuthorities().stream()
|
||||
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"))) {
|
||||
String username = authentication.getName();
|
||||
predicates.add(cb.equal(root.get("user").get("userName"), username));
|
||||
}
|
||||
|
||||
return cb.and(predicates.toArray(new Predicate[0]));
|
||||
};
|
||||
|
||||
long total = repo.count(base);
|
||||
|
||||
// Construcción del datatable con entity + spec
|
||||
return DataTable
|
||||
.of(repo, Direccion.class, dt, searchable)
|
||||
.orderable(orderable)
|
||||
|
||||
// Columnas "crudas" (las que existen tal cual):
|
||||
.edit("id", d -> d.getId())
|
||||
.edit("alias", d -> d.getAlias())
|
||||
.edit("att", d -> d.getAtt())
|
||||
.edit("direccion", d -> d.getDireccion())
|
||||
.edit("cp", d -> d.getCp())
|
||||
.edit("ciudad", d -> d.getCiudad())
|
||||
.edit("provincia", d -> d.getProvincia())
|
||||
|
||||
// Columnas calculadas:
|
||||
|
||||
// cliente = nombre del usuario (o username si no tienes name)
|
||||
.add("cliente", d -> {
|
||||
var u = d.getUser();
|
||||
return (u != null && u.getFullName() != null && !u.getFullName().isBlank())
|
||||
? u.getFullName()
|
||||
: "";
|
||||
})
|
||||
|
||||
// pais = nombre localizado desde MessageSource usando el keyword del país
|
||||
.add("pais", d -> {
|
||||
// si tienes la relación read-only a Pais (d.getPais()) con .getKeyword()
|
||||
String keyword = (d.getPais() != null) ? d.getPais().getKeyword() : null;
|
||||
if (keyword == null || keyword.isBlank())
|
||||
return d.getPaisCode3();
|
||||
return messageSource.getMessage("paises." + keyword, null, keyword, locale);
|
||||
})
|
||||
|
||||
// Ejemplo de columna de acciones:
|
||||
.add("actions", d -> """
|
||||
<div class="hstack gap-3 flex-wrap">
|
||||
<a href="javascript:void(0);" data-id="%d" class="link-success btn-edit-direccion fs-15">
|
||||
<i class="ri-edit-2-line"></i>
|
||||
</a>
|
||||
<a href="javascript:void(0);" data-id="%d" class="link-danger btn-delete-direccion fs-15">
|
||||
<i class="ri-delete-bin-5-line"></i>
|
||||
</a>
|
||||
</div>
|
||||
""".formatted(d.getId(), d.getId()))
|
||||
|
||||
// WHERE dinámico (spec base)
|
||||
.where(base)
|
||||
|
||||
// Si tu DataTable helper soporta “join/alias” para buscar/ordenar por campos
|
||||
// relacionados:
|
||||
// .searchAlias("cliente", (root, cb) -> root.join("user").get("name"))
|
||||
// .orderAlias("cliente", (root) -> root.join("user").get("name"))
|
||||
// .searchAlias("pais", (root, cb) -> root.join("pais",
|
||||
// JoinType.LEFT).get("keyword"))
|
||||
// .orderAlias("pais", (root) -> root.join("pais",
|
||||
// JoinType.LEFT).get("keyword"))
|
||||
|
||||
.toJson(total);
|
||||
}
|
||||
|
||||
@GetMapping(value = "/datatableDirecciones", produces = "application/json")
|
||||
@ResponseBody
|
||||
public DataTablesResponse<Map<String, Object>> datatableCliente(
|
||||
HttpServletRequest request,
|
||||
Authentication authentication,
|
||||
Locale locale) {
|
||||
|
||||
DataTablesRequest dt = DataTablesParser.from(request);
|
||||
|
||||
// Columnas visibles / lógicas para el DataTable en el frontend:
|
||||
// id, cliente (nombre de usuario), alias, att, direccion, cp, ciudad,
|
||||
// provincia, pais
|
||||
List<String> searchable = List.of(
|
||||
"id",
|
||||
"alias",
|
||||
"att", "direccion", "cp", "ciudad", "provincia", "pais", "telefono", "is_facturacion", "razonSocial",
|
||||
"identificacionFiscal");
|
||||
|
||||
List<String> orderable = List.of(
|
||||
"id",
|
||||
"cliente", "alias",
|
||||
"att", "direccion", "cp", "ciudad", "provincia", "pais", "telefono");
|
||||
|
||||
// Filtro base por rol (ROLE_USER solo ve sus direcciones)
|
||||
Specification<Direccion> base = (root, query, cb) -> {
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
|
||||
if (authentication != null && authentication.getAuthorities().stream()
|
||||
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"))) {
|
||||
String username = authentication.getName();
|
||||
predicates.add(cb.equal(root.get("user").get("userName"), username));
|
||||
}
|
||||
|
||||
return cb.and(predicates.toArray(new Predicate[0]));
|
||||
};
|
||||
|
||||
long total = repo.count(base);
|
||||
|
||||
// Construcción del datatable con entity + spec
|
||||
return DataTable
|
||||
.of(repo, Direccion.class, dt, searchable)
|
||||
.orderable(orderable)
|
||||
|
||||
// Columnas "crudas" (las que existen tal cual):
|
||||
.edit("id", d -> d.getId())
|
||||
.edit("alias", d -> d.getAlias())
|
||||
.edit("att", d -> d.getAtt())
|
||||
.edit("direccion", d -> d.getDireccion())
|
||||
.edit("cp", d -> d.getCp())
|
||||
.edit("ciudad", d -> d.getCiudad())
|
||||
.edit("provincia", d -> d.getProvincia())
|
||||
.edit("telefono", d -> d.getTelefono())
|
||||
.edit("is_facturacion", d -> d.isDireccionFacturacion())
|
||||
.edit("razon_social", d -> d.getRazonSocial())
|
||||
.edit("tipo_identificacion_fiscal", d -> d.getTipoIdentificacionFiscal())
|
||||
.edit("identificacion_fiscal", d -> d.getIdentificacionFiscal())
|
||||
|
||||
// pais = nombre localizado desde MessageSource usando el keyword del país
|
||||
.add("pais", d -> {
|
||||
// si tienes la relación read-only a Pais (d.getPais()) con .getKeyword()
|
||||
String keyword = (d.getPais() != null) ? d.getPais().getKeyword() : null;
|
||||
if (keyword == null || keyword.isBlank())
|
||||
return d.getPaisCode3();
|
||||
return messageSource.getMessage("paises." + keyword, null, keyword, locale);
|
||||
})
|
||||
// WHERE dinámico (spec base)
|
||||
.where(base)
|
||||
.toJson(total);
|
||||
}
|
||||
|
||||
@GetMapping("form")
|
||||
public String getForm(@RequestParam(required = false) Long id,
|
||||
Direccion direccion,
|
||||
BindingResult binding,
|
||||
Model model,
|
||||
HttpServletResponse response,
|
||||
Authentication auth,
|
||||
Locale locale) {
|
||||
|
||||
model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results"));
|
||||
|
||||
if (id != null) {
|
||||
var opt = repo.findByIdWithPaisAndUser(id);
|
||||
if (opt == null) {
|
||||
binding.reject("direcciones.error.noEncontrado",
|
||||
messageSource.getMessage("direcciones.error.noEncontrado", null, locale));
|
||||
response.setStatus(404);
|
||||
model.addAttribute("action", "/direcciones/" + id);
|
||||
return "imprimelibros/direcciones/direccion-form :: direccionForm";
|
||||
}
|
||||
|
||||
model.addAttribute("dirForm", opt.get());
|
||||
model.addAttribute("action", "/direcciones/" + id);
|
||||
} else {
|
||||
|
||||
Direccion newDireccion = new Direccion();
|
||||
boolean isUser = auth != null && auth.getAuthorities().stream()
|
||||
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"));
|
||||
if (isUser) {
|
||||
User user = direccion.getUser() != null ? direccion.getUser() : null;
|
||||
if (user != null) {
|
||||
newDireccion.setUser(user);
|
||||
}
|
||||
}
|
||||
model.addAttribute("dirForm", newDireccion);
|
||||
model.addAttribute("action", "/direcciones");
|
||||
}
|
||||
return "imprimelibros/direcciones/direccion-form :: direccionForm";
|
||||
}
|
||||
|
||||
@GetMapping("direction-form")
|
||||
public String getForm(@RequestParam(required = false) Long id,
|
||||
Direccion direccion,
|
||||
BindingResult binding,
|
||||
Model model,
|
||||
HttpServletResponse response,
|
||||
Principal principal,
|
||||
Locale locale) {
|
||||
|
||||
model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results"));
|
||||
|
||||
Direccion newDireccion = new Direccion();
|
||||
|
||||
User user = null;
|
||||
if (principal instanceof UserDetailsImpl udi) {
|
||||
user = new User();
|
||||
user.setId(udi.getId());
|
||||
} else if (principal instanceof User u && u.getId() != null) {
|
||||
user = u;
|
||||
}
|
||||
newDireccion.setUser(user);
|
||||
model.addAttribute("dirForm", newDireccion);
|
||||
model.addAttribute("action", "/direcciones/add");
|
||||
|
||||
return "imprimelibros/direcciones/direccion-form-fixed-user :: direccionForm";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String create(
|
||||
@Valid @ModelAttribute("dirForm") Direccion direccion,
|
||||
BindingResult binding,
|
||||
Model model,
|
||||
HttpServletResponse response,
|
||||
Authentication auth,
|
||||
Locale locale) {
|
||||
|
||||
boolean isUser = auth != null && auth.getAuthorities().stream()
|
||||
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"));
|
||||
|
||||
if (isUser) {
|
||||
User current = userRepo.findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(auth.getName()).orElse(null);
|
||||
direccion.setUser(current); // ignora lo que venga del hidden
|
||||
}
|
||||
|
||||
if (binding.hasErrors()) {
|
||||
response.setStatus(422);
|
||||
model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results"));
|
||||
model.addAttribute("action", "/direcciones");
|
||||
model.addAttribute("dirForm", direccion);
|
||||
return "imprimelibros/direcciones/direccion-form :: direccionForm";
|
||||
}
|
||||
|
||||
var data = direccion;
|
||||
|
||||
repo.save(data);
|
||||
response.setStatus(201);
|
||||
return null;
|
||||
}
|
||||
|
||||
// para el formulario modal en checkout
|
||||
@PostMapping("/add")
|
||||
public String create2(
|
||||
@Valid @ModelAttribute("dirForm") Direccion direccion,
|
||||
BindingResult binding,
|
||||
Model model,
|
||||
HttpServletResponse response,
|
||||
Authentication auth,
|
||||
Locale locale) {
|
||||
|
||||
User current = userRepo.findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(auth.getName()).orElse(null);
|
||||
direccion.setUser(current);
|
||||
|
||||
if (binding.hasErrors()) {
|
||||
response.setStatus(422);
|
||||
model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results"));
|
||||
model.addAttribute("action", "/direcciones/add");
|
||||
model.addAttribute("dirForm", direccion);
|
||||
return "imprimelibros/direcciones/direccion-form-fixed-user :: direccionForm";
|
||||
}
|
||||
|
||||
var data = direccion;
|
||||
|
||||
repo.save(data);
|
||||
response.setStatus(201);
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping("/{id}")
|
||||
public String update(
|
||||
@PathVariable Long id,
|
||||
@Valid @ModelAttribute("dirForm") Direccion direccion, // <- nombre distinto
|
||||
BindingResult binding,
|
||||
Model model,
|
||||
Authentication auth,
|
||||
HttpServletResponse response,
|
||||
Locale locale) {
|
||||
|
||||
var opt = repo.findById(id);
|
||||
if (opt.isEmpty()) {
|
||||
binding.reject("direcciones.error.noEncontrado",
|
||||
messageSource.getMessage("direcciones.error.noEncontrado", null, locale));
|
||||
response.setStatus(404);
|
||||
model.addAttribute("dirForm", direccion); // por si re-renderiza
|
||||
model.addAttribute("action", "/direcciones/" + id);
|
||||
return "imprimelibros/direcciones/direccion-form :: direccionForm";
|
||||
}
|
||||
|
||||
Long ownerId = opt.get().getUser() != null ? opt.get().getUser().getId() : null;
|
||||
if (!isOwnerOrAdmin(auth, ownerId)) {
|
||||
binding.reject("direcciones.error.sinPermiso",
|
||||
messageSource.getMessage("direcciones.error.sinPermiso", null, locale));
|
||||
response.setStatus(403);
|
||||
model.addAttribute("dirForm", direccion); // por si re-renderiza
|
||||
model.addAttribute("action", "/direcciones/" + id);
|
||||
return "imprimelibros/direcciones/direccion-form :: direccionForm";
|
||||
}
|
||||
|
||||
if (binding.hasErrors()) {
|
||||
response.setStatus(422);
|
||||
model.addAttribute("dirForm", direccion); // <- importante
|
||||
model.addAttribute("paises", paisesService.getForSelect("", "", locale).get("results"));
|
||||
model.addAttribute("action", "/direcciones/" + id);
|
||||
return "imprimelibros/direcciones/direccion-form :: direccionForm";
|
||||
}
|
||||
|
||||
repo.save(direccion);
|
||||
response.setStatus(200);
|
||||
return null;
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Transactional
|
||||
public ResponseEntity<?> delete(@PathVariable Long id, Authentication auth, Locale locale) {
|
||||
|
||||
Direccion direccion = repo.findById(id).orElse(null);
|
||||
if (direccion == null) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(Map.of("message", messageSource.getMessage("direcciones.error.noEncontrado", null, locale)));
|
||||
}
|
||||
|
||||
boolean isUser = auth != null && auth.getAuthorities().stream()
|
||||
.anyMatch(a -> a.getAuthority().equals("ROLE_USER"));
|
||||
|
||||
Long ownerId = direccion.getUser() != null ? direccion.getUser().getId() : null;
|
||||
Boolean isOwner = this.isOwnerOrAdmin(auth, ownerId);
|
||||
|
||||
if (isUser && !isOwner) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
||||
.body(Map.of("message",
|
||||
messageSource.getMessage("direcciones.error.sinPermiso", null, locale)));
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
direccion.setDeleted(true);
|
||||
direccion.setDeletedAt(Instant.now());
|
||||
|
||||
if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
|
||||
direccion.setDeletedBy(userRepo.getReferenceById(udi.getId()));
|
||||
} else if (auth != null) {
|
||||
userRepo.findByUserNameIgnoreCase(auth.getName()).ifPresent(direccion::setDeletedBy);
|
||||
}
|
||||
repo.saveAndFlush(direccion);
|
||||
|
||||
// eliminar referencias en carritos activos
|
||||
cartService.deleteCartDireccionesByDireccionId(direccion.getId());
|
||||
|
||||
return ResponseEntity.ok(Map.of("message",
|
||||
messageSource.getMessage("direcciones.exito.eliminado", null, locale)));
|
||||
|
||||
} catch (Exception ex) {
|
||||
// Devuelve SIEMPRE algo en el catch
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("message",
|
||||
messageSource.getMessage("direcciones.error.delete-internal-error", null, locale),
|
||||
"detail",
|
||||
ex.getClass().getSimpleName() + ": " + (ex.getMessage() != null ? ex.getMessage() : "")));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping(value = "/select2", produces = "application/json")
|
||||
@ResponseBody
|
||||
public Map<String, Object> getSelect2(
|
||||
@RequestParam(value = "q", required = false) String q1,
|
||||
@RequestParam(value = "term", required = false) String q2,
|
||||
@RequestParam(value = "presupuestoId", required = false) Long presupuestoId,
|
||||
Authentication auth) {
|
||||
|
||||
boolean isAdmin = auth.getAuthorities().stream()
|
||||
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("ROLE_SUPERADMIN"));
|
||||
|
||||
Long currentUserId = null;
|
||||
if (!isAdmin) {
|
||||
if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
|
||||
currentUserId = udi.getId();
|
||||
} else if (auth != null) {
|
||||
currentUserId = userRepo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
return direccionService.getForSelect(q1, q2, isAdmin ? null : currentUserId);
|
||||
|
||||
}
|
||||
|
||||
@GetMapping(value = "/facturacion/select2", produces = "application/json")
|
||||
@ResponseBody
|
||||
public Map<String, Object> getSelect2Facturacion(
|
||||
@RequestParam(value = "q", required = false) String q1,
|
||||
@RequestParam(value = "term", required = false) String q2,
|
||||
Authentication auth) {
|
||||
|
||||
boolean isAdmin = auth.getAuthorities().stream()
|
||||
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("ROLE_SUPERADMIN"));
|
||||
|
||||
Long currentUserId = null;
|
||||
if (!isAdmin) {
|
||||
if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
|
||||
currentUserId = udi.getId();
|
||||
} else if (auth != null) {
|
||||
currentUserId = userRepo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
return direccionService.getForSelectFacturacion(q1, q2, isAdmin ? null : currentUserId);
|
||||
|
||||
}
|
||||
|
||||
private boolean isOwnerOrAdmin(Authentication auth, Long ownerId) {
|
||||
if (auth == null) {
|
||||
return false;
|
||||
}
|
||||
boolean isAdmin = auth.getAuthorities().stream()
|
||||
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("ROLE_SUPERADMIN"));
|
||||
if (isAdmin) {
|
||||
return true;
|
||||
}
|
||||
// Aquí deberías obtener el ID del usuario actual desde tu servicio de usuarios
|
||||
Long currentUserId = null;
|
||||
if (auth != null && auth.getPrincipal() instanceof UserDetailsImpl udi) {
|
||||
currentUserId = udi.getId();
|
||||
} else if (auth != null) {
|
||||
currentUserId = userRepo.findIdByUserNameIgnoreCase(auth.getName()).orElse(null);
|
||||
}
|
||||
return currentUserId != null && currentUserId.equals(ownerId);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package com.imprimelibros.erp.direcciones;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface DireccionRepository
|
||||
extends JpaRepository<Direccion, Long>,
|
||||
JpaSpecificationExecutor<Direccion> {
|
||||
|
||||
|
||||
@Query("""
|
||||
select d from Direccion d
|
||||
left join fetch d.user
|
||||
left join fetch d.pais
|
||||
where d.user.id = :id
|
||||
""")
|
||||
List<DireccionView> findAllWithPaisAndUser(@Param("userId") Long userId);
|
||||
|
||||
@Query("""
|
||||
select d from Direccion d
|
||||
left join fetch d.user
|
||||
left join fetch d.pais
|
||||
where d.id = :id
|
||||
""")
|
||||
Optional<Direccion> findByIdWithPaisAndUser(@Param("id") Long id);
|
||||
|
||||
|
||||
|
||||
@Query(value = "SELECT * FROM direcciones", nativeQuery = true)
|
||||
List<DireccionView> findAllWithDeleted();
|
||||
|
||||
// find by user_id
|
||||
List<Direccion> findByUserId(Long userId);
|
||||
|
||||
// find by user_id and direccion_facturacion = true
|
||||
@Query("SELECT d FROM Direccion d WHERE (:userId IS NULL OR d.user.id = :userId) AND d.direccionFacturacion = true")
|
||||
List<Direccion> findByUserIdAndDireccionFacturacion(@Param("userId") Long userId);
|
||||
|
||||
// find by user_id with deleted
|
||||
@Query(value = "SELECT * FROM direcciones WHERE user_id = :userId", nativeQuery = true)
|
||||
List<Direccion> findByUserIdWithDeleted(@Param("userId") Long userId);
|
||||
|
||||
}
|
||||
@ -0,0 +1,156 @@
|
||||
package com.imprimelibros.erp.direcciones;
|
||||
|
||||
import java.text.Collator;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class DireccionService {
|
||||
|
||||
protected DireccionRepository repo;
|
||||
|
||||
public DireccionService(DireccionRepository repo) {
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
public Map<String, Object> getForSelect(String q1, String q2, Long userId) {
|
||||
try {
|
||||
|
||||
// Termino de búsqueda (Select2 usa 'q' o 'term' según versión/config)
|
||||
String search = Optional.ofNullable(q1).orElse(q2);
|
||||
if (search != null) {
|
||||
search = search.trim();
|
||||
}
|
||||
final String q = (search == null || search.isEmpty())
|
||||
? null
|
||||
: search.toLowerCase();
|
||||
|
||||
List<Direccion> all = userId != null ? repo.findByUserId(userId) : repo.findAll();
|
||||
|
||||
// Mapear a opciones id/text con i18n y filtrar por búsqueda si llega
|
||||
List<Map<String, String>> options = all.stream()
|
||||
.map(cc -> {
|
||||
String id = cc.getId().toString();
|
||||
String alias = cc.getAlias();
|
||||
String direccion = cc.getDireccion();
|
||||
String cp = String.valueOf(cc.getCp());
|
||||
String ciudad = cc.getCiudad();
|
||||
String att = cc.getAtt();
|
||||
Map<String, String> m = new HashMap<>();
|
||||
m.put("id", id); // lo normal en Select2: id = valor que guardarás (code3)
|
||||
m.put("text", alias); // texto mostrado, i18n con fallback a keyword
|
||||
m.put("cp", cp);
|
||||
m.put("ciudad", ciudad);
|
||||
m.put("att", att);
|
||||
m.put("alias", alias);
|
||||
m.put("direccion", direccion);
|
||||
return m;
|
||||
})
|
||||
.filter(opt -> {
|
||||
if (q == null || q.isEmpty())
|
||||
return true;
|
||||
String cp = opt.get("cp");
|
||||
String ciudad = opt.get("ciudad").toLowerCase();
|
||||
String att = opt.get("att").toLowerCase();
|
||||
String alias = opt.get("alias").toLowerCase();
|
||||
String text = opt.get("text").toLowerCase();
|
||||
String direccion = opt.get("direccion").toLowerCase();
|
||||
return text.contains(q) || cp.contains(q) || ciudad.contains(q) || att.contains(q)
|
||||
|| alias.contains(q) || direccion.contains(q);
|
||||
})
|
||||
.sorted(Comparator.comparing(m -> m.get("text"), Collator.getInstance()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Estructura Select2
|
||||
Map<String, Object> resp = new HashMap<>();
|
||||
resp.put("results", options);
|
||||
return resp;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return Map.of("results", List.of());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Map<String, Object> getForSelectFacturacion(String q1, String q2, Long userId) {
|
||||
try {
|
||||
|
||||
// Termino de búsqueda (Select2 usa 'q' o 'term' según versión/config)
|
||||
String search = Optional.ofNullable(q1).orElse(q2);
|
||||
if (search != null) {
|
||||
search = search.trim();
|
||||
}
|
||||
final String q = (search == null || search.isEmpty())
|
||||
? null
|
||||
: search.toLowerCase();
|
||||
|
||||
List<Direccion> all = repo.findByUserIdAndDireccionFacturacion(userId);
|
||||
|
||||
// Mapear a opciones id/text con i18n y filtrar por búsqueda si llega
|
||||
List<Map<String, String>> options = all.stream()
|
||||
.map(cc -> {
|
||||
String id = cc.getId().toString();
|
||||
String alias = cc.getAlias();
|
||||
String direccion = cc.getDireccion();
|
||||
String cp = String.valueOf(cc.getCp());
|
||||
String ciudad = cc.getCiudad();
|
||||
String att = cc.getAtt();
|
||||
Map<String, String> m = new HashMap<>();
|
||||
m.put("id", id); // lo normal en Select2: id = valor que guardarás (code3)
|
||||
m.put("text", alias); // texto mostrado, i18n con fallback a keyword
|
||||
m.put("cp", cp);
|
||||
m.put("ciudad", ciudad);
|
||||
m.put("att", att);
|
||||
m.put("alias", alias);
|
||||
m.put("direccion", direccion);
|
||||
return m;
|
||||
})
|
||||
.filter(opt -> {
|
||||
if (q == null || q.isEmpty())
|
||||
return true;
|
||||
String cp = opt.get("cp");
|
||||
String ciudad = opt.get("ciudad").toLowerCase();
|
||||
String att = opt.get("att").toLowerCase();
|
||||
String alias = opt.get("alias").toLowerCase();
|
||||
String text = opt.get("text").toLowerCase();
|
||||
String direccion = opt.get("direccion").toLowerCase();
|
||||
return text.contains(q) || cp.contains(q) || ciudad.contains(q) || att.contains(q)
|
||||
|| alias.contains(q) || direccion.contains(q);
|
||||
})
|
||||
.sorted(Comparator.comparing(m -> m.get("text"), Collator.getInstance()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Estructura Select2
|
||||
Map<String, Object> resp = new HashMap<>();
|
||||
resp.put("results", options);
|
||||
return resp;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return Map.of("results", List.of());
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Direccion> findById(Long id) {
|
||||
return repo.findById(id);
|
||||
}
|
||||
|
||||
public Boolean checkFreeShipment(Integer cp, String paisCode3) {
|
||||
if (paisCode3 != null && paisCode3.toLowerCase().equals("esp") && cp != null) {
|
||||
// Excluir Canarias (35xxx y 38xxx), Baleares (07xxx), Ceuta (51xxx), Melilla
|
||||
// (52xxx)
|
||||
int provincia = cp / 1000;
|
||||
|
||||
if (provincia != 7 && provincia != 35 && provincia != 38 && provincia != 51 && provincia != 52) {
|
||||
return true; // España peninsular
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.imprimelibros.erp.direcciones;
|
||||
|
||||
public interface DireccionView {
|
||||
Long getId();
|
||||
UserView getUser();
|
||||
String getAlias();
|
||||
String getAtt();
|
||||
String getDireccion();
|
||||
String getCp();
|
||||
String getCiudad();
|
||||
String getProvincia();
|
||||
PaisView getPais();
|
||||
String getPaisKeyword();
|
||||
String getTelefono();
|
||||
Boolean getIsFacturacion();
|
||||
String getRazonSocial();
|
||||
String getTipoIdentificacionFiscal();
|
||||
String getIdentificacionFiscal();
|
||||
String getCliente();
|
||||
interface UserView {
|
||||
Long getId();
|
||||
String getFullName();
|
||||
}
|
||||
interface PaisView {
|
||||
String getCode3();
|
||||
String getKeyword();
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@ -18,8 +19,10 @@ import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoCubierta;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto.TipoEncuadernacion;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
@ -125,7 +128,127 @@ public class skApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
public Integer getMaxSolapas(Map<String, Object> requestBody, Locale locale) {
|
||||
public Map<String, Object> savePresupuesto(Map<String, Object> requestBody) {
|
||||
return performWithRetryMap(() -> {
|
||||
String url = this.skApiUrl + "api/guardar";
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBearerAuth(authService.getToken());
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.POST,
|
||||
entity,
|
||||
String.class);
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
try {
|
||||
Map<String, Object> responseBody = mapper.readValue(
|
||||
response.getBody(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
|
||||
// Si la API devuelve "error" a nivel raíz
|
||||
if (responseBody.get("error") != null) {
|
||||
// Devolvemos un mapa con sólo el error para que el caller decida
|
||||
return Map.of("error", responseBody.get("error"));
|
||||
}
|
||||
|
||||
Object dataObj = responseBody.get("data");
|
||||
if (dataObj instanceof Map<?, ?> dataRaw) {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> data = (Map<String, Object>) dataRaw;
|
||||
|
||||
Boolean success = (Boolean) (responseBody.get("success") != null ? responseBody.get("success")
|
||||
: false);
|
||||
Long id = ((Integer) data.get("id")).longValue();
|
||||
String iskn = (String) data.get("iskn");
|
||||
|
||||
// OJO: aquí mantengo tu lógica tal cual (success == null o false => OK)
|
||||
// Si tu API realmente usa success=true como éxito, esto habría que invertirlo.
|
||||
if (success != null && success) {
|
||||
if (id != null && iskn != null) {
|
||||
data.put("id", Long.valueOf(id));
|
||||
data.put("iskn", iskn);
|
||||
}
|
||||
} else {
|
||||
// Tu lógica actual: si success es true u otra cosa → error 2
|
||||
return Map.of("error", 2);
|
||||
}
|
||||
|
||||
// Devolvemos sólo la parte interesante: el data ya enriquecido
|
||||
return Map.of("data", data);
|
||||
}
|
||||
|
||||
// Si data no es un Map, devolvemos error genérico
|
||||
return Map.of("error", 1);
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
return Map.of("error", 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Long crearPedido(Map<String, Object> requestBody) {
|
||||
Map<String, Object> result = performWithRetryMap(() -> {
|
||||
String url = this.skApiUrl + "api/crear-pedido";
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBearerAuth(authService.getToken());
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.POST,
|
||||
entity,
|
||||
String.class);
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
try {
|
||||
Map<String, Object> responseBody = mapper.readValue(
|
||||
response.getBody(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
|
||||
// Si la API devuelve "error" a nivel raíz
|
||||
if (responseBody.get("error") != null) {
|
||||
// Devolvemos un mapa con sólo el error para que el caller decida
|
||||
return Map.of("error", responseBody.get("error"));
|
||||
}
|
||||
|
||||
Boolean success = (Boolean) (responseBody.get("success") != null ? responseBody.get("success") : false);
|
||||
Long id = ((Integer) responseBody.get("id")).longValue();
|
||||
|
||||
if (success != null && id != null && success) {
|
||||
|
||||
return Map.of("data", id);
|
||||
} else {
|
||||
// Tu lógica actual: si success es true u otra cosa → error 2
|
||||
return Map.of("error", 2);
|
||||
}
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
return Map.of("error", 1);
|
||||
}
|
||||
});
|
||||
|
||||
if (result.get("error") != null) {
|
||||
throw new RuntimeException("Error al crear el pedido: " + result.get("error"));
|
||||
}
|
||||
return (Long) result.get("data");
|
||||
}
|
||||
|
||||
public Map<String, Object> getMaxSolapas(Map<String, Object> requestBody, Locale locale) {
|
||||
try {
|
||||
String jsonResponse = performWithRetry(() -> {
|
||||
String url = this.skApiUrl + "api/calcular-solapas";
|
||||
@ -166,7 +289,11 @@ public class skApiClient {
|
||||
messageSource.getMessage("presupuesto.errores.error-interior", new Object[] { 1 }, locale));
|
||||
}
|
||||
|
||||
return root.get("data").asInt();
|
||||
Integer maxSolapas = root.get("data").asInt();
|
||||
Double lomo = root.get("lomo").asDouble();
|
||||
return Map.of(
|
||||
"maxSolapas", maxSolapas,
|
||||
"lomo", lomo);
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
// Fallback al 80% del ancho
|
||||
@ -179,7 +306,9 @@ public class skApiClient {
|
||||
throw new RuntimeException("Tamaño no válido en la solicitud: " + requestBody);
|
||||
else {
|
||||
int ancho = (int) tamanio.get("ancho");
|
||||
return (int) Math.floor(ancho * 0.8); // 80% del ancho
|
||||
return Map.of(
|
||||
"maxSolapas", (int) (ancho * 0.8),
|
||||
"lomo", 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -219,6 +348,270 @@ public class skApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> getCosteEnvio(Map<String, Object> data, Locale locale) {
|
||||
|
||||
return performWithRetryMap(() -> {
|
||||
String url = this.skApiUrl + "api/calcular-envio";
|
||||
|
||||
URI uri = UriComponentsBuilder.fromUriString(url)
|
||||
.queryParam("pais_code3", data.get("pais_code3"))
|
||||
.queryParam("cp", data.get("cp"))
|
||||
.queryParam("peso", data.get("peso"))
|
||||
.queryParam("unidades", data.get("unidades"))
|
||||
.queryParam("palets", data.get("palets"))
|
||||
.build(true) // no re-encode []
|
||||
.toUri();
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBearerAuth(authService.getToken());
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
uri,
|
||||
HttpMethod.GET,
|
||||
new HttpEntity<>(headers),
|
||||
String.class);
|
||||
|
||||
try {
|
||||
Map<String, Object> responseBody = new ObjectMapper().readValue(
|
||||
response.getBody(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
Boolean error = (Boolean) responseBody.get("error");
|
||||
if (error != null && error) {
|
||||
return Map.of("error", messageSource.getMessage("direcciones.error.noShippingCost", null, locale));
|
||||
} else {
|
||||
Double total = Optional.ofNullable(responseBody.get("data"))
|
||||
.filter(Number.class::isInstance)
|
||||
.map(Number.class::cast)
|
||||
.map(Number::doubleValue)
|
||||
.orElse(0.0);
|
||||
|
||||
return Map.of("data", total);
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
return Map.of("error", "Internal Server Error: 1"); // Fallback en caso de error
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public Map<String, Object> checkPedidoEstado(Long presupuestoId, Locale locale) {
|
||||
|
||||
try {
|
||||
|
||||
String jsonResponse = performWithRetry(() -> {
|
||||
String url = this.skApiUrl + "api/estado-pedido/" + presupuestoId;
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBearerAuth(authService.getToken()); // token actualizado
|
||||
|
||||
HttpEntity<Void> entity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.GET,
|
||||
entity,
|
||||
String.class);
|
||||
|
||||
return response.getBody();
|
||||
});
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
JsonNode root = mapper.readTree(jsonResponse);
|
||||
|
||||
if (root.get("data") == null) {
|
||||
throw new RuntimeException(
|
||||
"Sin respuesta desde el servidor del proveedor");
|
||||
}
|
||||
|
||||
String estado = root.get("data").asText();
|
||||
return Map.of(
|
||||
"estado", estado);
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
// Fallback al 80% del ancho
|
||||
return Map.of(
|
||||
"estado", null);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> getFilesTypes(Long presupuestoId, Locale locale) {
|
||||
|
||||
try {
|
||||
|
||||
Map<String, Object> result = performWithRetryMap(() -> {
|
||||
String url = this.skApiUrl + "api/files-presupuesto/" + presupuestoId;
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBearerAuth(authService.getToken()); // token actualizado
|
||||
|
||||
HttpEntity<Void> entity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.GET,
|
||||
entity,
|
||||
String.class);
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
try {
|
||||
Map<String, Object> responseBody = mapper.readValue(
|
||||
response.getBody(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
|
||||
// Si la API devuelve "error" a nivel raíz
|
||||
if (responseBody.get("error") != null) {
|
||||
// Devolvemos un mapa con sólo el error para que el caller decida
|
||||
return Map.of("error", responseBody.get("error"));
|
||||
}
|
||||
|
||||
Boolean hasError = (Boolean) (responseBody.get("error") == null
|
||||
|| responseBody.get("error") == "null" ? false : true);
|
||||
Map<String, Boolean> files = (Map<String, Boolean>) responseBody.get("data");
|
||||
|
||||
if (files != null && !hasError) {
|
||||
return Map.of("data", files);
|
||||
} else {
|
||||
// Tu lógica actual: si success es true u otra cosa → error 2
|
||||
return Map.of("error", 2);
|
||||
}
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
return Map.of("error", 1);
|
||||
}
|
||||
});
|
||||
|
||||
if (result.get("error") != null) {
|
||||
throw new RuntimeException(
|
||||
messageSource.getMessage("pedido.errors.connecting-server-error", null, locale));
|
||||
}
|
||||
Map<String, Object> data = (Map<String, Object>) result.get("data");
|
||||
return data;
|
||||
|
||||
} catch (RuntimeException e) {
|
||||
throw new RuntimeException(
|
||||
messageSource.getMessage("pedido.errors.connecting-server-error", null, locale));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public byte[] downloadFile(Long presupuestoId, String fileType, Locale locale) {
|
||||
return performWithRetryBytes(() -> {
|
||||
|
||||
String normalized = (fileType == null) ? "" : fileType.trim().toLowerCase();
|
||||
|
||||
String endpoint = switch (normalized) {
|
||||
case "ferro" -> "api/get-ferro/" + presupuestoId;
|
||||
case "cubierta" -> "api/get-cubierta/" + presupuestoId;
|
||||
case "tapa" -> "api/get-tapa/" + presupuestoId;
|
||||
default -> throw new IllegalArgumentException("Tipo de fichero no soportado: " + fileType);
|
||||
};
|
||||
|
||||
// OJO: skApiUrl debería terminar en "/" para que concatene bien
|
||||
String url = this.skApiUrl + endpoint;
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
// Si tu CI4 requiere Bearer, mantenlo. Si NO lo requiere, puedes quitar esta
|
||||
// línea.
|
||||
headers.setBearerAuth(authService.getToken());
|
||||
headers.setAccept(List.of(MediaType.APPLICATION_PDF, MediaType.APPLICATION_OCTET_STREAM));
|
||||
|
||||
try {
|
||||
ResponseEntity<byte[]> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.GET,
|
||||
new HttpEntity<>(headers),
|
||||
byte[].class);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
return response.getBody(); // bytes del PDF
|
||||
}
|
||||
return null;
|
||||
|
||||
} catch (HttpClientErrorException.NotFound e) {
|
||||
// CI4 no tiene ese fichero
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Boolean aceptarFerro(Long presupuestoId, Locale locale) {
|
||||
|
||||
String result = performWithRetry(() -> {
|
||||
String url = this.skApiUrl + "api/aceptar-ferro/" + presupuestoId;
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBearerAuth(authService.getToken());
|
||||
|
||||
HttpEntity<Void> entity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.POST,
|
||||
entity,
|
||||
String.class);
|
||||
|
||||
try {
|
||||
Map<String, Object> responseBody = new ObjectMapper().readValue(
|
||||
response.getBody(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
|
||||
Boolean success = (Boolean) (responseBody.get("success") != null ? responseBody.get("success") : false);
|
||||
|
||||
return success.toString();
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
return "false"; // Fallback en caso de error
|
||||
}
|
||||
});
|
||||
return Boolean.parseBoolean(result);
|
||||
}
|
||||
|
||||
|
||||
public Boolean cancelarPedido(Long pedidoId) {
|
||||
|
||||
String result = performWithRetry(() -> {
|
||||
String url = this.skApiUrl + "api/cancelar-pedido/" + pedidoId;
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBearerAuth(authService.getToken());
|
||||
|
||||
HttpEntity<Void> entity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.POST,
|
||||
entity,
|
||||
String.class);
|
||||
|
||||
try {
|
||||
Map<String, Object> responseBody = new ObjectMapper().readValue(
|
||||
response.getBody(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
|
||||
Boolean success = (Boolean) (responseBody.get("success") != null ? responseBody.get("success") : false);
|
||||
|
||||
return success.toString();
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
return "false"; // Fallback en caso de error
|
||||
}
|
||||
});
|
||||
return Boolean.parseBoolean(result);
|
||||
}
|
||||
|
||||
/******************
|
||||
* PRIVATE METHODS
|
||||
******************/
|
||||
@ -236,6 +629,33 @@ public class skApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> performWithRetryMap(Supplier<Map<String, Object>> request) {
|
||||
try {
|
||||
return request.get();
|
||||
} catch (HttpClientErrorException.Unauthorized e) {
|
||||
// Token expirado, renovar y reintentar
|
||||
authService.invalidateToken();
|
||||
try {
|
||||
return request.get(); // segundo intento
|
||||
} catch (HttpClientErrorException ex) {
|
||||
throw new RuntimeException("La autenticación ha fallado tras renovar el token.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] performWithRetryBytes(Supplier<byte[]> request) {
|
||||
try {
|
||||
return request.get();
|
||||
} catch (HttpClientErrorException.Unauthorized e) {
|
||||
authService.invalidateToken();
|
||||
try {
|
||||
return request.get();
|
||||
} catch (HttpClientErrorException ex) {
|
||||
throw new RuntimeException("La autenticación ha fallado tras renovar el token.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BigDecimal calcularMargen(
|
||||
BigDecimal importe, BigDecimal importeMin, BigDecimal importeMax,
|
||||
BigDecimal margenMax, BigDecimal margenMin) {
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
package com.imprimelibros.erp.facturacion;
|
||||
|
||||
public enum EstadoFactura {
|
||||
borrador,
|
||||
validada
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.imprimelibros.erp.facturacion;
|
||||
|
||||
public enum EstadoPagoFactura {
|
||||
pendiente,
|
||||
pagada,
|
||||
cancelada
|
||||
}
|
||||
271
src/main/java/com/imprimelibros/erp/facturacion/Factura.java
Normal file
271
src/main/java/com/imprimelibros/erp/facturacion/Factura.java
Normal file
@ -0,0 +1,271 @@
|
||||
package com.imprimelibros.erp.facturacion;
|
||||
|
||||
import com.imprimelibros.erp.common.jpa.AbstractAuditedEntitySoftTs;
|
||||
import com.imprimelibros.erp.users.User;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.hibernate.annotations.Formula;
|
||||
|
||||
@Entity
|
||||
@Table(name = "facturas", uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uq_facturas_numero_factura", columnNames = "numero_factura")
|
||||
})
|
||||
public class Factura extends AbstractAuditedEntitySoftTs {
|
||||
|
||||
@Column(name = "pedido_id")
|
||||
private Long pedidoId;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "factura_rectificada_id")
|
||||
private Factura facturaRectificada;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "factura_rectificativa_id")
|
||||
private Factura facturaRectificativa;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "cliente_id")
|
||||
private User cliente;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "serie_id")
|
||||
private SerieFactura serie;
|
||||
|
||||
@Column(name = "numero_factura", length = 50)
|
||||
private String numeroFactura;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "estado", nullable = false, length = 20)
|
||||
private EstadoFactura estado = EstadoFactura.borrador;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "estado_pago", nullable = false, length = 20)
|
||||
private EstadoPagoFactura estadoPago = EstadoPagoFactura.pendiente;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "tipo_pago", nullable = false, length = 30)
|
||||
private TipoPago tipoPago = TipoPago.otros;
|
||||
|
||||
@Column(name = "fecha_emision")
|
||||
private LocalDateTime fechaEmision;
|
||||
|
||||
@Column(name = "base_imponible", precision = 10, scale = 2)
|
||||
private BigDecimal baseImponible;
|
||||
|
||||
@Column(name = "iva_4", precision = 10, scale = 2)
|
||||
private BigDecimal iva4;
|
||||
|
||||
@Column(name = "iva_21", precision = 10, scale = 2)
|
||||
private BigDecimal iva21;
|
||||
|
||||
@Column(name = "total_factura", precision = 10, scale = 2)
|
||||
private BigDecimal totalFactura;
|
||||
|
||||
@Column(name = "total_pagado", precision = 10, scale = 2)
|
||||
private BigDecimal totalPagado = new BigDecimal("0.00");
|
||||
|
||||
@Lob
|
||||
@Column(name = "notas")
|
||||
private String notas;
|
||||
|
||||
@OneToMany(mappedBy = "factura", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<FacturaLinea> lineas = new ArrayList<>();
|
||||
|
||||
@OneToMany(mappedBy = "factura", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<FacturaPago> pagos = new ArrayList<>();
|
||||
|
||||
@OneToMany(mappedBy = "factura", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<FacturaDireccion> direcciones = new ArrayList<>();
|
||||
|
||||
@Formula("(select u.fullname from users u where u.id = cliente_id)")
|
||||
private String clienteNombre;
|
||||
|
||||
// Helpers
|
||||
public void addLinea(FacturaLinea linea) {
|
||||
linea.setFactura(this);
|
||||
this.lineas.add(linea);
|
||||
}
|
||||
|
||||
public void removeLinea(FacturaLinea linea) {
|
||||
this.lineas.remove(linea);
|
||||
linea.setFactura(null);
|
||||
}
|
||||
|
||||
public void addPago(FacturaPago pago) {
|
||||
pago.setFactura(this);
|
||||
this.pagos.add(pago);
|
||||
}
|
||||
|
||||
public void removePago(FacturaPago pago) {
|
||||
this.pagos.remove(pago);
|
||||
pago.setFactura(null);
|
||||
}
|
||||
|
||||
// Getters/Setters
|
||||
public Long getPedidoId() {
|
||||
return pedidoId;
|
||||
}
|
||||
|
||||
public void setPedidoId(Long pedidoId) {
|
||||
this.pedidoId = pedidoId;
|
||||
}
|
||||
|
||||
public Factura getFacturaRectificada() {
|
||||
return facturaRectificada;
|
||||
}
|
||||
|
||||
public void setFacturaRectificada(Factura facturaRectificada) {
|
||||
this.facturaRectificada = facturaRectificada;
|
||||
}
|
||||
|
||||
public Factura getFacturaRectificativa() {
|
||||
return facturaRectificativa;
|
||||
}
|
||||
|
||||
public void setFacturaRectificativa(Factura facturaRectificativa) {
|
||||
this.facturaRectificativa = facturaRectificativa;
|
||||
}
|
||||
|
||||
public User getCliente() {
|
||||
return cliente;
|
||||
}
|
||||
|
||||
public void setCliente(User cliente) {
|
||||
this.cliente = cliente;
|
||||
}
|
||||
|
||||
public SerieFactura getSerie() {
|
||||
return serie;
|
||||
}
|
||||
|
||||
public void setSerie(SerieFactura serie) {
|
||||
this.serie = serie;
|
||||
}
|
||||
|
||||
public String getNumeroFactura() {
|
||||
return numeroFactura;
|
||||
}
|
||||
|
||||
public void setNumeroFactura(String numeroFactura) {
|
||||
this.numeroFactura = numeroFactura;
|
||||
}
|
||||
|
||||
public EstadoFactura getEstado() {
|
||||
return estado;
|
||||
}
|
||||
|
||||
public void setEstado(EstadoFactura estado) {
|
||||
this.estado = estado;
|
||||
}
|
||||
|
||||
public EstadoPagoFactura getEstadoPago() {
|
||||
return estadoPago;
|
||||
}
|
||||
|
||||
public void setEstadoPago(EstadoPagoFactura estadoPago) {
|
||||
this.estadoPago = estadoPago;
|
||||
}
|
||||
|
||||
public TipoPago getTipoPago() {
|
||||
return tipoPago;
|
||||
}
|
||||
|
||||
public void setTipoPago(TipoPago tipoPago) {
|
||||
this.tipoPago = tipoPago;
|
||||
}
|
||||
|
||||
public LocalDateTime getFechaEmision() {
|
||||
return fechaEmision;
|
||||
}
|
||||
|
||||
public void setFechaEmision(LocalDateTime fechaEmision) {
|
||||
this.fechaEmision = fechaEmision;
|
||||
}
|
||||
|
||||
public BigDecimal getBaseImponible() {
|
||||
return baseImponible;
|
||||
}
|
||||
|
||||
public void setBaseImponible(BigDecimal baseImponible) {
|
||||
this.baseImponible = baseImponible;
|
||||
}
|
||||
|
||||
public BigDecimal getIva4() {
|
||||
return iva4;
|
||||
}
|
||||
|
||||
public void setIva4(BigDecimal iva4) {
|
||||
this.iva4 = iva4;
|
||||
}
|
||||
|
||||
public BigDecimal getIva21() {
|
||||
return iva21;
|
||||
}
|
||||
|
||||
public void setIva21(BigDecimal iva21) {
|
||||
this.iva21 = iva21;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalFactura() {
|
||||
return totalFactura;
|
||||
}
|
||||
|
||||
public void setTotalFactura(BigDecimal totalFactura) {
|
||||
this.totalFactura = totalFactura;
|
||||
}
|
||||
|
||||
public BigDecimal getTotalPagado() {
|
||||
return totalPagado;
|
||||
}
|
||||
|
||||
public void setTotalPagado(BigDecimal totalPagado) {
|
||||
this.totalPagado = totalPagado;
|
||||
}
|
||||
|
||||
public String getNotas() {
|
||||
return notas;
|
||||
}
|
||||
|
||||
public void setNotas(String notas) {
|
||||
this.notas = notas;
|
||||
}
|
||||
|
||||
public List<FacturaLinea> getLineas() {
|
||||
return lineas;
|
||||
}
|
||||
|
||||
public void setLineas(List<FacturaLinea> lineas) {
|
||||
this.lineas = lineas;
|
||||
}
|
||||
|
||||
public List<FacturaPago> getPagos() {
|
||||
return pagos;
|
||||
}
|
||||
|
||||
public void setPagos(List<FacturaPago> pagos) {
|
||||
this.pagos = pagos;
|
||||
}
|
||||
|
||||
public List<FacturaDireccion> getDirecciones() {
|
||||
return direcciones;
|
||||
}
|
||||
|
||||
public void setDirecciones(List<FacturaDireccion> direcciones) {
|
||||
this.direcciones = direcciones;
|
||||
}
|
||||
|
||||
public FacturaDireccion getDireccionFacturacion() {
|
||||
return (direcciones == null || direcciones.isEmpty()) ? null : direcciones.get(0);
|
||||
}
|
||||
|
||||
public void addDireccion(FacturaDireccion direccion) {
|
||||
direccion.setFactura(this);
|
||||
this.direcciones.add(direccion);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,209 @@
|
||||
package com.imprimelibros.erp.facturacion;
|
||||
|
||||
import com.imprimelibros.erp.direcciones.Direccion.TipoIdentificacionFiscal;
|
||||
import com.imprimelibros.erp.paises.Paises;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "facturas_direcciones",
|
||||
indexes = {
|
||||
@Index(name = "idx_facturas_direcciones_factura_id", columnList = "factura_id")
|
||||
}
|
||||
)
|
||||
public class FacturaDireccion {
|
||||
|
||||
@Column(name = "id")
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "factura_id", nullable = false,
|
||||
foreignKey = @ForeignKey(name = "fk_facturas_direcciones_factura"))
|
||||
private Factura factura;
|
||||
|
||||
@Column(name = "unidades")
|
||||
private Integer unidades; // MEDIUMINT UNSIGNED
|
||||
|
||||
@Column(name = "email", length = 255)
|
||||
private String email;
|
||||
|
||||
@Column(name = "att", length = 150, nullable = false)
|
||||
private String att;
|
||||
|
||||
@Column(name = "direccion", length = 255, nullable = false)
|
||||
private String direccion;
|
||||
|
||||
@Column(name = "cp", nullable = false)
|
||||
private Integer cp; // MEDIUMINT UNSIGNED
|
||||
|
||||
@Column(name = "ciudad", length = 100, nullable = false)
|
||||
private String ciudad;
|
||||
|
||||
@Column(name = "provincia", length = 100, nullable = false)
|
||||
private String provincia;
|
||||
|
||||
@Column(name = "pais_code3", length = 3, nullable = false)
|
||||
private String paisCode3 = "esp";
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "pais_code3", referencedColumnName = "code3", insertable = false, updatable = false)
|
||||
private Paises pais;
|
||||
|
||||
@Column(name = "telefono", length = 30)
|
||||
private String telefono;
|
||||
|
||||
@Column(name = "instrucciones", length = 255)
|
||||
private String instrucciones;
|
||||
|
||||
@Column(name = "razon_social", length = 150)
|
||||
private String razonSocial;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "tipo_identificacion_fiscal", length = 20, nullable = false)
|
||||
private TipoIdentificacionFiscal tipoIdentificacionFiscal = TipoIdentificacionFiscal.DNI;
|
||||
|
||||
@Column(name = "identificacion_fiscal", length = 50)
|
||||
private String identificacionFiscal;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private java.time.Instant createdAt;
|
||||
|
||||
|
||||
// Getters / Setters
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Factura getFactura() {
|
||||
return factura;
|
||||
}
|
||||
|
||||
public void setFactura(Factura factura) {
|
||||
this.factura = factura;
|
||||
}
|
||||
|
||||
public Integer getUnidades() {
|
||||
return unidades;
|
||||
}
|
||||
|
||||
public void setUnidades(Integer unidades) {
|
||||
this.unidades = unidades;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getAtt() {
|
||||
return att;
|
||||
}
|
||||
|
||||
public void setAtt(String att) {
|
||||
this.att = att;
|
||||
}
|
||||
|
||||
public String getDireccion() {
|
||||
return direccion;
|
||||
}
|
||||
|
||||
public void setDireccion(String direccion) {
|
||||
this.direccion = direccion;
|
||||
}
|
||||
|
||||
public Integer getCp() {
|
||||
return cp;
|
||||
}
|
||||
|
||||
public void setCp(Integer cp) {
|
||||
this.cp = cp;
|
||||
}
|
||||
|
||||
public String getCiudad() {
|
||||
return ciudad;
|
||||
}
|
||||
|
||||
public void setCiudad(String ciudad) {
|
||||
this.ciudad = ciudad;
|
||||
}
|
||||
|
||||
public String getProvincia() {
|
||||
return provincia;
|
||||
}
|
||||
|
||||
public void setProvincia(String provincia) {
|
||||
this.provincia = provincia;
|
||||
}
|
||||
|
||||
public String getPaisCode3() {
|
||||
return paisCode3;
|
||||
}
|
||||
|
||||
public void setPaisCode3(String paisCode3) {
|
||||
this.paisCode3 = paisCode3;
|
||||
}
|
||||
|
||||
public Paises getPais() {
|
||||
return pais;
|
||||
}
|
||||
|
||||
public void setPais(Paises pais) {
|
||||
this.pais = pais;
|
||||
}
|
||||
|
||||
public String getTelefono() {
|
||||
return telefono;
|
||||
}
|
||||
|
||||
public void setTelefono(String telefono) {
|
||||
this.telefono = telefono;
|
||||
}
|
||||
|
||||
public String getInstrucciones() {
|
||||
return instrucciones;
|
||||
}
|
||||
|
||||
public void setInstrucciones(String instrucciones) {
|
||||
this.instrucciones = instrucciones;
|
||||
}
|
||||
|
||||
public String getRazonSocial() {
|
||||
return razonSocial;
|
||||
}
|
||||
|
||||
public void setRazonSocial(String razonSocial) {
|
||||
this.razonSocial = razonSocial;
|
||||
}
|
||||
|
||||
public TipoIdentificacionFiscal getTipoIdentificacionFiscal() {
|
||||
return tipoIdentificacionFiscal;
|
||||
}
|
||||
|
||||
public void setTipoIdentificacionFiscal(TipoIdentificacionFiscal tipoIdentificacionFiscal) {
|
||||
this.tipoIdentificacionFiscal = tipoIdentificacionFiscal;
|
||||
}
|
||||
|
||||
public String getIdentificacionFiscal() {
|
||||
return identificacionFiscal;
|
||||
}
|
||||
|
||||
public void setIdentificacionFiscal(String identificacionFiscal) {
|
||||
this.identificacionFiscal = identificacionFiscal;
|
||||
}
|
||||
|
||||
public java.time.Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
public void setCreatedAt(java.time.Instant createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package com.imprimelibros.erp.facturacion;
|
||||
|
||||
import com.imprimelibros.erp.common.jpa.AbstractAuditedEntitySoftTs;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Entity
|
||||
@Table(name = "facturas_lineas")
|
||||
public class FacturaLinea extends AbstractAuditedEntitySoftTs {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "factura_id")
|
||||
private Factura factura;
|
||||
|
||||
@Lob
|
||||
@Column(name = "descripcion")
|
||||
private String descripcion;
|
||||
|
||||
@Column(name = "cantidad")
|
||||
private Integer cantidad;
|
||||
|
||||
@Column(name = "base_linea", precision = 10, scale = 2)
|
||||
private BigDecimal baseLinea;
|
||||
|
||||
@Column(name = "iva_4_linea", precision = 10, scale = 2)
|
||||
private BigDecimal iva4Linea;
|
||||
|
||||
@Column(name = "iva_21_linea", precision = 10, scale = 2)
|
||||
private BigDecimal iva21Linea;
|
||||
|
||||
@Column(name = "total_linea", precision = 10, scale = 2)
|
||||
private BigDecimal totalLinea;
|
||||
|
||||
// Getters/Setters
|
||||
public Factura getFactura() { return factura; }
|
||||
public void setFactura(Factura factura) { this.factura = factura; }
|
||||
|
||||
public String getDescripcion() { return descripcion; }
|
||||
public void setDescripcion(String descripcion) { this.descripcion = descripcion; }
|
||||
|
||||
public Integer getCantidad() { return cantidad; }
|
||||
public void setCantidad(Integer cantidad) { this.cantidad = cantidad; }
|
||||
|
||||
public BigDecimal getBaseLinea() { return baseLinea; }
|
||||
public void setBaseLinea(BigDecimal baseLinea) { this.baseLinea = baseLinea; }
|
||||
|
||||
public BigDecimal getIva4Linea() { return iva4Linea; }
|
||||
public void setIva4Linea(BigDecimal iva4Linea) { this.iva4Linea = iva4Linea; }
|
||||
|
||||
public BigDecimal getIva21Linea() { return iva21Linea; }
|
||||
public void setIva21Linea(BigDecimal iva21Linea) { this.iva21Linea = iva21Linea; }
|
||||
|
||||
public BigDecimal getTotalLinea() { return totalLinea; }
|
||||
public void setTotalLinea(BigDecimal totalLinea) { this.totalLinea = totalLinea; }
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package com.imprimelibros.erp.facturacion;
|
||||
|
||||
import com.imprimelibros.erp.common.jpa.AbstractAuditedEntitySoftTs;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "facturas_pagos")
|
||||
public class FacturaPago extends AbstractAuditedEntitySoftTs {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "factura_id")
|
||||
private Factura factura;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "metodo_pago", nullable = false, length = 30)
|
||||
private TipoPago metodoPago = TipoPago.otros;
|
||||
|
||||
@Column(name = "cantidad_pagada", precision = 10, scale = 2)
|
||||
private BigDecimal cantidadPagada;
|
||||
|
||||
@Column(name = "fecha_pago")
|
||||
private LocalDateTime fechaPago;
|
||||
|
||||
@Lob
|
||||
@Column(name = "notas")
|
||||
private String notas;
|
||||
|
||||
// Getters/Setters
|
||||
public Factura getFactura() { return factura; }
|
||||
public void setFactura(Factura factura) { this.factura = factura; }
|
||||
|
||||
public TipoPago getMetodoPago() { return metodoPago; }
|
||||
public void setMetodoPago(TipoPago metodoPago) { this.metodoPago = metodoPago; }
|
||||
|
||||
public BigDecimal getCantidadPagada() { return cantidadPagada; }
|
||||
public void setCantidadPagada(BigDecimal cantidadPagada) { this.cantidadPagada = cantidadPagada; }
|
||||
|
||||
public LocalDateTime getFechaPago() { return fechaPago; }
|
||||
public void setFechaPago(LocalDateTime fechaPago) { this.fechaPago = fechaPago; }
|
||||
|
||||
public String getNotas() { return notas; }
|
||||
public void setNotas(String notas) { this.notas = notas; }
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package com.imprimelibros.erp.facturacion;
|
||||
|
||||
import com.imprimelibros.erp.common.jpa.AbstractAuditedEntitySoftTs;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "series_facturas")
|
||||
public class SerieFactura extends AbstractAuditedEntitySoftTs {
|
||||
|
||||
@Column(name = "nombre_serie", nullable = false, length = 100)
|
||||
private String nombreSerie;
|
||||
|
||||
@Column(name = "prefijo", nullable = false, length = 10)
|
||||
private String prefijo;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "tipo", nullable = false, length = 50)
|
||||
private TipoSerieFactura tipo = TipoSerieFactura.facturacion;
|
||||
|
||||
@Column(name = "numero_actual", nullable = false)
|
||||
private Long numeroActual = 1L;
|
||||
|
||||
public String getNombreSerie() { return nombreSerie; }
|
||||
public void setNombreSerie(String nombreSerie) { this.nombreSerie = nombreSerie; }
|
||||
|
||||
public String getPrefijo() { return prefijo; }
|
||||
public void setPrefijo(String prefijo) { this.prefijo = prefijo; }
|
||||
|
||||
public TipoSerieFactura getTipo() { return tipo; }
|
||||
public void setTipo(TipoSerieFactura tipo) { this.tipo = tipo; }
|
||||
|
||||
public Long getNumeroActual() { return numeroActual; }
|
||||
public void setNumeroActual(Long numeroActual) { this.numeroActual = numeroActual; }
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package com.imprimelibros.erp.facturacion;
|
||||
|
||||
public enum TipoPago {
|
||||
tpv_tarjeta,
|
||||
tpv_bizum,
|
||||
transferencia,
|
||||
otros
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.imprimelibros.erp.facturacion;
|
||||
|
||||
public enum TipoSerieFactura {
|
||||
facturacion
|
||||
}
|
||||
@ -0,0 +1,390 @@
|
||||
package com.imprimelibros.erp.facturacion.controller;
|
||||
|
||||
import com.imprimelibros.erp.configurationERP.VariableService;
|
||||
import com.imprimelibros.erp.datatables.DataTable;
|
||||
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
||||
import com.imprimelibros.erp.datatables.DataTablesResponse;
|
||||
import com.imprimelibros.erp.direcciones.DireccionService;
|
||||
import com.imprimelibros.erp.facturacion.EstadoFactura;
|
||||
import com.imprimelibros.erp.facturacion.Factura;
|
||||
import com.imprimelibros.erp.facturacion.FacturaDireccion;
|
||||
import com.imprimelibros.erp.facturacion.dto.FacturaAddRequestDto;
|
||||
import com.imprimelibros.erp.facturacion.dto.FacturaGuardarDto;
|
||||
import com.imprimelibros.erp.facturacion.dto.FacturaLineaUpsertDto;
|
||||
import com.imprimelibros.erp.facturacion.dto.FacturaPagoUpsertDto;
|
||||
import com.imprimelibros.erp.facturacion.repo.FacturaRepository;
|
||||
import com.imprimelibros.erp.facturacion.service.FacturacionService;
|
||||
import com.imprimelibros.erp.i18n.TranslationService;
|
||||
import com.imprimelibros.erp.pedidos.PedidoDireccion;
|
||||
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/facturas")
|
||||
@PreAuthorize("hasRole('SUPERADMIN') || hasRole('ADMIN')")
|
||||
public class FacturasController {
|
||||
|
||||
private final FacturacionService facturacionService;
|
||||
|
||||
private final FacturaRepository repo;
|
||||
private final TranslationService translationService;
|
||||
private final MessageSource messageSource;
|
||||
private final PedidoService pedidoService;
|
||||
private final VariableService variableService;
|
||||
private final DireccionService direccionService;
|
||||
|
||||
public FacturasController(
|
||||
FacturaRepository repo,
|
||||
TranslationService translationService,
|
||||
MessageSource messageSource,
|
||||
PedidoService pedidoService, FacturacionService facturacionService, VariableService variableService, DireccionService direccionService) {
|
||||
this.repo = repo;
|
||||
this.translationService = translationService;
|
||||
this.messageSource = messageSource;
|
||||
this.pedidoService = pedidoService;
|
||||
this.facturacionService = facturacionService;
|
||||
this.direccionService = direccionService;
|
||||
this.variableService = variableService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String facturasList(Model model, Locale locale) {
|
||||
|
||||
List<String> keys = List.of(
|
||||
"app.eliminar",
|
||||
"app.cancelar",
|
||||
"facturas.delete.title",
|
||||
"facturas.delete.text",
|
||||
"facturas.delete.ok.title",
|
||||
"facturas.delete.ok.text");
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
return "imprimelibros/facturas/facturas-list";
|
||||
}
|
||||
|
||||
@GetMapping("/add")
|
||||
public String facturaAdd(Model model, Locale locale) {
|
||||
|
||||
List<String> keys = List.of(
|
||||
"facturas.form.cliente.placeholder",
|
||||
"facturas.add.form.validation.title",
|
||||
"facturas.add.form.validation",
|
||||
"facturas.error.create"
|
||||
);
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
|
||||
model.addAttribute("defaultSerieRectificativa", variableService.getValorEntero("serie_facturacion_rect_default"));
|
||||
|
||||
return "imprimelibros/facturas/facturas-add-form";
|
||||
}
|
||||
|
||||
@PostMapping("/add")
|
||||
@ResponseBody
|
||||
public Map<String, Object> facturaAddPost(
|
||||
Model model,
|
||||
@RequestBody FacturaAddRequestDto request,
|
||||
Locale locale) {
|
||||
|
||||
Factura nuevaFactura = facturacionService.crearNuevaFactura(
|
||||
request.getUser(),
|
||||
request.getSerie(),
|
||||
request.getDireccion(),
|
||||
request.getFactura_rectificada()
|
||||
);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
if(nuevaFactura == null){
|
||||
result.put("success", false);
|
||||
result.put("message", messageSource.getMessage("facturas.error.create", null, "No se ha podido crear la factura. Revise los datos e inténtelo de nuevo.", locale));
|
||||
return result;
|
||||
}
|
||||
else{
|
||||
result.put("success", true);
|
||||
result.put("facturaId", nuevaFactura.getId());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public String facturaDetail(@PathVariable Long id, Model model, Locale locale) {
|
||||
Factura factura = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||
|
||||
List<String> keys = List.of(
|
||||
"facturas.lineas.error.base",
|
||||
"facturas.lineas.delete.title",
|
||||
"facturas.lineas.delete.text",
|
||||
|
||||
"facturas.pagos.delete.title",
|
||||
"facturas.pagos.delete.text",
|
||||
"facturas.pagos.error.cantidad",
|
||||
"facturas.pagos.error.fecha",
|
||||
|
||||
"app.eliminar",
|
||||
"app.cancelar");
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
|
||||
FacturaDireccion direccionFacturacion = factura.getDireccionFacturacion();
|
||||
|
||||
model.addAttribute("direccionFacturacion", direccionFacturacion);
|
||||
model.addAttribute("factura", factura);
|
||||
|
||||
return "imprimelibros/facturas/facturas-form";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/guardar")
|
||||
public ResponseEntity<?> guardarFacturaCabeceraYDireccion(
|
||||
@PathVariable Long id,
|
||||
@RequestBody @Valid FacturaGuardarDto payload) {
|
||||
facturacionService.guardarCabeceraYDireccionFacturacion(id, payload);
|
||||
return ResponseEntity.ok(Map.of("ok", true));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/container")
|
||||
public String facturaContainer(@PathVariable Long id, Model model, Locale locale) {
|
||||
Factura factura = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||
|
||||
FacturaDireccion direccionFacturacion = factura.getDireccionFacturacion();
|
||||
|
||||
|
||||
model.addAttribute("direccionFacturacion", direccionFacturacion);
|
||||
model.addAttribute("factura", factura);
|
||||
|
||||
return "imprimelibros/facturas/partials/factura-container :: factura-container";
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/validar")
|
||||
public ResponseEntity<?> validarFactura(@PathVariable Long id) {
|
||||
Factura factura = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||
|
||||
if (factura.getEstado() != EstadoFactura.borrador) {
|
||||
return ResponseEntity.badRequest().body("Solo se pueden validar facturas en estado 'borrador'.");
|
||||
}
|
||||
|
||||
facturacionService.validarFactura(factura.getId());
|
||||
repo.save(factura);
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/borrador")
|
||||
public ResponseEntity<?> marcarBorrador(@PathVariable Long id) {
|
||||
Factura factura = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||
|
||||
if (factura.getEstado() != EstadoFactura.validada) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("Solo se pueden marcar como borrador facturas en estado 'validada'.");
|
||||
}
|
||||
|
||||
factura.setEstado(EstadoFactura.borrador);
|
||||
repo.save(factura);
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{facturaId}/lineas")
|
||||
public ResponseEntity<?> createLinea(@PathVariable Long facturaId,
|
||||
@Valid @RequestBody FacturaLineaUpsertDto req) {
|
||||
facturacionService.createLinea(facturaId, req);
|
||||
return ResponseEntity.ok(Map.of("ok", true));
|
||||
}
|
||||
|
||||
@PostMapping("/{facturaId}/lineas/{lineaId}")
|
||||
public ResponseEntity<?> updateLinea(@PathVariable Long facturaId,
|
||||
@PathVariable Long lineaId,
|
||||
@Valid @RequestBody FacturaLineaUpsertDto req) {
|
||||
facturacionService.upsertLinea(facturaId, req);
|
||||
return ResponseEntity.ok(Map.of("ok", true));
|
||||
}
|
||||
|
||||
@PostMapping("/{facturaId}/lineas/{lineaId}/delete")
|
||||
public ResponseEntity<?> deleteLinea(@PathVariable Long facturaId,
|
||||
@PathVariable Long lineaId) {
|
||||
facturacionService.borrarLinea(facturaId, lineaId);
|
||||
return ResponseEntity.ok(Map.of("ok", true));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* -----------------------------
|
||||
* Pagos
|
||||
* --------------------------------
|
||||
*/
|
||||
@PostMapping("/{facturaId}/pagos")
|
||||
public ResponseEntity<?> createPago(
|
||||
@PathVariable Long facturaId,
|
||||
@Valid @RequestBody FacturaPagoUpsertDto req, Principal principal) {
|
||||
facturacionService.upsertPago(facturaId, req, principal);
|
||||
return ResponseEntity.ok(Map.of("ok", true));
|
||||
}
|
||||
|
||||
@PostMapping("/{facturaId}/pagos/{pagoId}")
|
||||
public ResponseEntity<?> updatePago(
|
||||
@PathVariable Long facturaId,
|
||||
@PathVariable Long pagoId,
|
||||
@Valid @RequestBody FacturaPagoUpsertDto req,
|
||||
Principal principal) {
|
||||
// opcional: fuerza consistencia
|
||||
req.setId(pagoId);
|
||||
facturacionService.upsertPago(facturaId, req, principal);
|
||||
return ResponseEntity.ok(Map.of("ok", true));
|
||||
}
|
||||
|
||||
@PostMapping("/{facturaId}/pagos/{pagoId}/delete")
|
||||
public ResponseEntity<?> deletePago(
|
||||
@PathVariable Long facturaId,
|
||||
@PathVariable Long pagoId, Principal principal) {
|
||||
facturacionService.borrarPago(facturaId, pagoId, principal);
|
||||
return ResponseEntity.ok(Map.of("ok", true));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/notas")
|
||||
public ResponseEntity<?> setNotas(
|
||||
@PathVariable Long id,
|
||||
@RequestBody Map<String, String> payload,
|
||||
Model model,
|
||||
Locale locale) {
|
||||
Factura factura = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada con ID: " + id));
|
||||
String notas = payload.get("notas");
|
||||
factura.setNotas(notas);
|
||||
repo.save(factura);
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// API: DataTables (server-side)
|
||||
// -----------------------------
|
||||
@GetMapping("/api/datatables")
|
||||
@ResponseBody
|
||||
public DataTablesResponse<Map<String, Object>> datatables(HttpServletRequest request, Locale locale) {
|
||||
|
||||
DataTablesRequest dt = DataTablesParser.from(request);
|
||||
|
||||
Specification<Factura> notDeleted = (root, q, cb) -> cb.isNull(root.get("deletedAt"));
|
||||
long total = repo.count(notDeleted);
|
||||
|
||||
return DataTable
|
||||
.of(repo, Factura.class, dt, List.of("clienteNombre", "numeroFactura", "estado", "estadoPago"))
|
||||
.where(notDeleted)
|
||||
.orderable(List.of("id", "clienteNombre", "numeroFactura", "estado", "estadoPago"))
|
||||
.onlyAddedColumns()
|
||||
.add("id", Factura::getId)
|
||||
.add("cliente", f -> {
|
||||
var c = f.getCliente();
|
||||
return c == null ? null : c.getFullName(); // o getNombre(), etc.
|
||||
})
|
||||
|
||||
.add("numero_factura", Factura::getNumeroFactura)
|
||||
.add("estado", Factura::getEstado)
|
||||
.add("estado_label", f -> {
|
||||
String key = "facturas.estado." + f.getEstado().name().toLowerCase();
|
||||
return messageSource.getMessage(key, null, f.getEstado().name(), locale);
|
||||
})
|
||||
.add("estado_pago", Factura::getEstadoPago)
|
||||
.add("estado_pago_label", f -> {
|
||||
String key = "facturas.estado-pago." + f.getEstadoPago().name().toLowerCase();
|
||||
return messageSource.getMessage(key, null, f.getEstadoPago().name(), locale);
|
||||
})
|
||||
.add("total", Factura::getTotalFactura)
|
||||
.add("fecha_emision", f -> {
|
||||
LocalDateTime fecha = f.getFechaEmision();
|
||||
return fecha == null ? null : fecha.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
|
||||
})
|
||||
.add("actions", f -> {
|
||||
if (f.getEstado() == EstadoFactura.borrador) {
|
||||
return """
|
||||
<div class="hstack gap-3 flex-wrap">
|
||||
<button type="button"
|
||||
class="btn p-0 link-success btn-view-factura fs-15"
|
||||
data-id="%d">
|
||||
<i class="ri-eye-line"></i>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn p-0 link-danger btn-delete-factura fs-15"
|
||||
data-id="%d">
|
||||
<i class="ri-delete-bin-5-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
""".formatted(f.getId(), f.getId());
|
||||
} else {
|
||||
return """
|
||||
<div class="hstack gap-3 flex-wrap">
|
||||
<button type="button"
|
||||
class="btn p-0 link-success btn-view-factura fs-15"
|
||||
data-id="%d">
|
||||
<i class="ri-eye-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
""".formatted(f.getId());
|
||||
}
|
||||
})
|
||||
.toJson(total);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// API: select2 Direcciones
|
||||
// -----------------------------
|
||||
@GetMapping("/api/get-direcciones")
|
||||
@ResponseBody
|
||||
public Map<String, Object> getSelect2Facturacion(
|
||||
@RequestParam(value = "q", required = false) String q1,
|
||||
@RequestParam(value = "term", required = false) String q2,
|
||||
@RequestParam(value = "user_id", required = true) Long userId,
|
||||
Authentication auth) {
|
||||
|
||||
|
||||
return direccionService.getForSelectFacturacion(q1, q2, userId);
|
||||
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// API: select2 facturas rectificables
|
||||
// -----------------------------
|
||||
@GetMapping("/api/get-facturas-rectificables")
|
||||
@ResponseBody
|
||||
public Map<String, Object> getSelect2FacturasRectificables(
|
||||
@RequestParam(value = "q", required = false) String q1,
|
||||
@RequestParam(value = "term", required = false) String q2,
|
||||
@RequestParam(value = "user_id", required = true) Long userId,
|
||||
Authentication auth) {
|
||||
try {
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return Map.of("results", List.of());
|
||||
}
|
||||
return facturacionService.getForSelectFacturasRectificables(q1, q2, userId);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,226 @@
|
||||
package com.imprimelibros.erp.facturacion.controller;
|
||||
|
||||
import com.imprimelibros.erp.datatables.DataTable;
|
||||
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
||||
import com.imprimelibros.erp.datatables.DataTablesResponse;
|
||||
import com.imprimelibros.erp.facturacion.SerieFactura;
|
||||
import com.imprimelibros.erp.facturacion.TipoSerieFactura;
|
||||
import com.imprimelibros.erp.facturacion.repo.SerieFacturaRepository;
|
||||
import com.imprimelibros.erp.i18n.TranslationService;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/configuracion/series-facturacion")
|
||||
@PreAuthorize("hasRole('SUPERADMIN')")
|
||||
public class SeriesFacturacionController {
|
||||
|
||||
private final SerieFacturaRepository repo;
|
||||
private final TranslationService translationService;
|
||||
private final MessageSource messageSource;
|
||||
|
||||
public SeriesFacturacionController(SerieFacturaRepository repo, TranslationService translationService,
|
||||
MessageSource messageSource) {
|
||||
this.repo = repo;
|
||||
this.translationService = translationService;
|
||||
this.messageSource = messageSource;
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// VISTA
|
||||
// -----------------------------
|
||||
@GetMapping
|
||||
public String listView(Model model, Locale locale) {
|
||||
|
||||
List<String> keys = List.of(
|
||||
"series-facturacion.modal.title.add",
|
||||
"series-facturacion.modal.title.edit",
|
||||
"app.guardar",
|
||||
"app.cancelar",
|
||||
"app.eliminar",
|
||||
"series-facturacion.delete.title",
|
||||
"series-facturacion.delete.text",
|
||||
"series-facturacion.delete.ok.title",
|
||||
"series-facturacion.delete.ok.text");
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
|
||||
return "imprimelibros/configuracion/series-facturas/series-facturas-list";
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// API: DataTables (server-side)
|
||||
// -----------------------------
|
||||
@GetMapping("/api/datatables")
|
||||
@ResponseBody
|
||||
public DataTablesResponse<Map<String, Object>> datatables(HttpServletRequest request, Locale locale) {
|
||||
|
||||
DataTablesRequest dt = DataTablesParser.from(request);
|
||||
|
||||
Specification<SerieFactura> notDeleted = (root, q, cb) -> cb.isNull(root.get("deletedAt"));
|
||||
long total = repo.count(notDeleted);
|
||||
|
||||
return DataTable
|
||||
.of(repo, SerieFactura.class, dt, List.of("nombreSerie", "prefijo"))
|
||||
.where(notDeleted)
|
||||
.orderable(List.of("id", "nombreSerie", "prefijo", "tipo", "numeroActual"))
|
||||
.onlyAddedColumns()
|
||||
.add("id", SerieFactura::getId)
|
||||
.add("nombre_serie", SerieFactura::getNombreSerie)
|
||||
.add("prefijo", SerieFactura::getPrefijo)
|
||||
.add("tipo", s -> s.getTipo() != null ? s.getTipo().name() : null)
|
||||
.add("tipo_label", s -> {
|
||||
if (s.getTipo() == null)
|
||||
return null;
|
||||
return messageSource.getMessage(
|
||||
"series-facturacion.tipo." + s.getTipo().name(),
|
||||
null,
|
||||
s.getTipo().name(),
|
||||
locale);
|
||||
})
|
||||
.add("numero_actual", SerieFactura::getNumeroActual)
|
||||
.add("actions", s -> """
|
||||
<div class="hstack gap-3 flex-wrap">
|
||||
<button type="button"
|
||||
class="btn p-0 link-success btn-edit-serie fs-15"
|
||||
data-id="%d">
|
||||
<i class="ri-edit-2-line"></i>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn p-0 link-danger btn-delete-serie fs-15"
|
||||
data-id="%d">
|
||||
<i class="ri-delete-bin-5-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
""".formatted(s.getId(), s.getId()))
|
||||
.toJson(total);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// API: CREATE
|
||||
// -----------------------------
|
||||
@PostMapping(value = "/api", consumes = "application/json")
|
||||
@ResponseBody
|
||||
public Map<String, Object> create(@RequestBody SerieFacturaPayload payload) {
|
||||
validate(payload);
|
||||
|
||||
SerieFactura s = new SerieFactura();
|
||||
s.setNombreSerie(payload.nombre_serie.trim());
|
||||
s.setPrefijo(payload.prefijo.trim());
|
||||
s.setTipo(TipoSerieFactura.facturacion); // fijo
|
||||
s.setNumeroActual(payload.numero_actual);
|
||||
|
||||
repo.save(s);
|
||||
return Map.of("ok", true, "id", s.getId());
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// API: UPDATE
|
||||
// -----------------------------
|
||||
@PutMapping(value = "/api/{id}", consumes = "application/json")
|
||||
@ResponseBody
|
||||
public Map<String, Object> update(@PathVariable Long id, @RequestBody SerieFacturaPayload payload) {
|
||||
validate(payload);
|
||||
|
||||
SerieFactura s = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Serie no encontrada: " + id));
|
||||
|
||||
if (s.getDeletedAt() != null) {
|
||||
throw new IllegalStateException("No se puede editar una serie eliminada.");
|
||||
}
|
||||
|
||||
s.setNombreSerie(payload.nombre_serie.trim());
|
||||
s.setPrefijo(payload.prefijo.trim());
|
||||
s.setTipo(TipoSerieFactura.facturacion);
|
||||
s.setNumeroActual(payload.numero_actual);
|
||||
|
||||
repo.save(s);
|
||||
return Map.of("ok", true);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// API: DELETE (soft)
|
||||
// -----------------------------
|
||||
@DeleteMapping("/api/{id}")
|
||||
@ResponseBody
|
||||
public ResponseEntity<?> delete(@PathVariable Long id) {
|
||||
SerieFactura s = repo.findById(id)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Serie no encontrada: " + id));
|
||||
|
||||
if (s.getDeletedAt() == null) {
|
||||
s.setDeletedAt(Instant.now());
|
||||
s.setDeletedBy(null); // luego lo conectamos al usuario actual
|
||||
repo.save(s);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Map.of("ok", true));
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// API: GET for select2
|
||||
// -----------------------------
|
||||
@GetMapping("/api/get-series")
|
||||
@ResponseBody
|
||||
public Map<String, Object> getSeriesForSelect(
|
||||
@RequestParam(value = "q", required = false) String q1,
|
||||
@RequestParam(value = "term", required = false) String q2,
|
||||
Locale locale) {
|
||||
String query = (q1 != null && !q1.isBlank()) ? q1
|
||||
: (q2 != null && !q2.isBlank()) ? q2
|
||||
: "";
|
||||
List<Map<String, Object>> results = repo.searchForSelectSeriesFacturacion(query).stream()
|
||||
.map(s -> {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("id", s.getId());
|
||||
m.put("text", s.getNombreSerie());
|
||||
return m;
|
||||
})
|
||||
.toList();
|
||||
|
||||
return Map.of("results", results);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// Payload + validación
|
||||
// -----------------------------
|
||||
public static class SerieFacturaPayload {
|
||||
public String nombre_serie;
|
||||
public String prefijo;
|
||||
public String tipo; // lo manda UI, pero en backend lo fijamos
|
||||
public Long numero_actual;
|
||||
}
|
||||
|
||||
private void validate(SerieFacturaPayload p) {
|
||||
if (p == null)
|
||||
throw new IllegalArgumentException("Body requerido.");
|
||||
if (p.nombre_serie == null || p.nombre_serie.trim().isBlank()) {
|
||||
throw new IllegalArgumentException("nombre_serie es obligatorio.");
|
||||
}
|
||||
if (p.prefijo == null || p.prefijo.trim().isBlank()) {
|
||||
throw new IllegalArgumentException("prefijo es obligatorio.");
|
||||
}
|
||||
if (p.prefijo.trim().length() > 10) {
|
||||
throw new IllegalArgumentException("prefijo máximo 10 caracteres.");
|
||||
}
|
||||
if (p.numero_actual == null || p.numero_actual < 1) {
|
||||
throw new IllegalArgumentException("numero_actual debe ser >= 1.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
package com.imprimelibros.erp.facturacion.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import com.imprimelibros.erp.facturacion.FacturaDireccion;
|
||||
import com.imprimelibros.erp.pedidos.PedidoDireccion;
|
||||
|
||||
public class DireccionFacturacionDto {
|
||||
private String razonSocial;
|
||||
private String identificacionFiscal;
|
||||
private String direccion;
|
||||
private String cp;
|
||||
private String ciudad;
|
||||
private String provincia;
|
||||
private String paisKeyword;
|
||||
private String telefono;
|
||||
|
||||
public String getRazonSocial() {
|
||||
return razonSocial;
|
||||
}
|
||||
|
||||
public void setRazonSocial(String razonSocial) {
|
||||
this.razonSocial = razonSocial;
|
||||
}
|
||||
|
||||
public String getIdentificacionFiscal() {
|
||||
return identificacionFiscal;
|
||||
}
|
||||
|
||||
public void setIdentificacionFiscal(String identificacionFiscal) {
|
||||
this.identificacionFiscal = identificacionFiscal;
|
||||
}
|
||||
|
||||
public String getDireccion() {
|
||||
return direccion;
|
||||
}
|
||||
|
||||
public void setDireccion(String direccion) {
|
||||
this.direccion = direccion;
|
||||
}
|
||||
|
||||
public String getCp() {
|
||||
return cp;
|
||||
}
|
||||
|
||||
public void setCp(String cp) {
|
||||
this.cp = cp;
|
||||
}
|
||||
|
||||
public String getCiudad() {
|
||||
return ciudad;
|
||||
}
|
||||
|
||||
public void setCiudad(String ciudad) {
|
||||
this.ciudad = ciudad;
|
||||
}
|
||||
|
||||
public String getProvincia() {
|
||||
return provincia;
|
||||
}
|
||||
|
||||
public void setProvincia(String provincia) {
|
||||
this.provincia = provincia;
|
||||
}
|
||||
|
||||
public String getPaisKeyword() {
|
||||
return paisKeyword;
|
||||
}
|
||||
|
||||
public void setPaisKeyword(String paisKeyword) {
|
||||
this.paisKeyword = paisKeyword;
|
||||
}
|
||||
|
||||
public String getTelefono() {
|
||||
return telefono;
|
||||
}
|
||||
|
||||
public void setTelefono(String telefono) {
|
||||
this.telefono = telefono;
|
||||
}
|
||||
|
||||
|
||||
public FacturaDireccion toFacturaDireccion() {
|
||||
FacturaDireccion fd = new FacturaDireccion();
|
||||
applyTo(fd);
|
||||
return fd;
|
||||
}
|
||||
|
||||
public PedidoDireccion toPedidoDireccion() {
|
||||
PedidoDireccion pd = new PedidoDireccion();
|
||||
applyTo(pd);
|
||||
pd.setFacturacion(true);
|
||||
return pd;
|
||||
}
|
||||
|
||||
public void applyTo(PedidoDireccion pd) {
|
||||
pd.setAtt("");
|
||||
pd.setRazonSocial(this.razonSocial);
|
||||
pd.setIdentificacionFiscal(this.identificacionFiscal);
|
||||
pd.setDireccion(this.direccion);
|
||||
|
||||
// CP robusto
|
||||
Integer cpInt = null;
|
||||
if (this.cp != null && !this.cp.isBlank()) {
|
||||
try {
|
||||
cpInt = Integer.valueOf(this.cp.trim());
|
||||
} catch (NumberFormatException ignored) {
|
||||
// si quieres, lanza IllegalArgumentException para validarlo
|
||||
}
|
||||
}
|
||||
pd.setCp(cpInt);
|
||||
|
||||
pd.setCiudad(this.ciudad);
|
||||
pd.setProvincia(this.provincia);
|
||||
|
||||
pd.setPaisCode3(this.paisKeyword);
|
||||
|
||||
pd.setTelefono(this.telefono);
|
||||
}
|
||||
|
||||
public void applyTo(FacturaDireccion fd ) {
|
||||
fd.setAtt("");
|
||||
fd.setRazonSocial(this.razonSocial);
|
||||
fd.setIdentificacionFiscal(this.identificacionFiscal);
|
||||
fd.setDireccion(this.direccion);
|
||||
|
||||
// CP robusto
|
||||
Integer cpInt = null;
|
||||
if (this.cp != null && !this.cp.isBlank()) {
|
||||
try {
|
||||
cpInt = Integer.valueOf(this.cp.trim());
|
||||
} catch (NumberFormatException ignored) {
|
||||
// si quieres, lanza IllegalArgumentException para validarlo
|
||||
}
|
||||
}
|
||||
fd.setCp(cpInt);
|
||||
|
||||
fd.setCiudad(this.ciudad);
|
||||
fd.setProvincia(this.provincia);
|
||||
|
||||
fd.setPaisCode3(this.paisKeyword);
|
||||
|
||||
fd.setTelefono(this.telefono);
|
||||
fd.setCreatedAt(Instant.now());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.imprimelibros.erp.facturacion.dto;
|
||||
|
||||
public class FacturaAddRequestDto {
|
||||
|
||||
private Long user;
|
||||
private Long serie;
|
||||
private Long direccion;
|
||||
private Long factura_rectificada;
|
||||
|
||||
// getters y setters
|
||||
public Long getUser() {
|
||||
return user;
|
||||
}
|
||||
public void setUser(Long user) {
|
||||
this.user = user;
|
||||
}
|
||||
public Long getSerie() {
|
||||
return serie;
|
||||
}
|
||||
public void setSerie(Long serie) {
|
||||
this.serie = serie;
|
||||
}
|
||||
public Long getDireccion() {
|
||||
return direccion;
|
||||
}
|
||||
public void setDireccion(Long direccion) {
|
||||
this.direccion = direccion;
|
||||
}
|
||||
public Long getFactura_rectificada() {
|
||||
return factura_rectificada;
|
||||
}
|
||||
public void setFactura_rectificada(Long factura_rectificada) {
|
||||
this.factura_rectificada = factura_rectificada;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package com.imprimelibros.erp.facturacion.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class FacturaCabeceraDto {
|
||||
private Long serieId;
|
||||
private Long clienteId;
|
||||
private LocalDateTime fechaEmision;
|
||||
|
||||
public Long getSerieId() {
|
||||
return serieId;
|
||||
}
|
||||
|
||||
public void setSerieId(Long serieId) {
|
||||
this.serieId = serieId;
|
||||
}
|
||||
|
||||
public Long getClienteId() {
|
||||
return clienteId;
|
||||
}
|
||||
|
||||
public void setClienteId(Long clienteId) {
|
||||
this.clienteId = clienteId;
|
||||
}
|
||||
|
||||
public LocalDateTime getFechaEmision() {
|
||||
return fechaEmision;
|
||||
}
|
||||
|
||||
public void setFechaEmision(LocalDateTime fechaEmision) {
|
||||
this.fechaEmision = fechaEmision;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package com.imprimelibros.erp.facturacion.dto;
|
||||
|
||||
import com.imprimelibros.erp.pedidos.PedidoDireccion;
|
||||
import com.imprimelibros.erp.facturacion.FacturaDireccion;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import com.imprimelibros.erp.direcciones.Direccion.TipoIdentificacionFiscal;
|
||||
|
||||
public final class FacturaDireccionMapper {
|
||||
|
||||
private FacturaDireccionMapper() {}
|
||||
|
||||
public static FacturaDireccion fromPedidoDireccion(PedidoDireccion src) {
|
||||
if (src == null) return null;
|
||||
|
||||
FacturaDireccion dst = new FacturaDireccion();
|
||||
|
||||
dst.setUnidades(src.getUnidades());
|
||||
dst.setEmail(src.getEmail());
|
||||
dst.setAtt(src.getAtt());
|
||||
dst.setDireccion(src.getDireccion());
|
||||
dst.setCp(src.getCp());
|
||||
dst.setCiudad(src.getCiudad());
|
||||
dst.setProvincia(src.getProvincia());
|
||||
dst.setPaisCode3(src.getPaisCode3());
|
||||
dst.setTelefono(src.getTelefono());
|
||||
dst.setInstrucciones(src.getInstrucciones());
|
||||
dst.setRazonSocial(src.getRazonSocial());
|
||||
dst.setCreatedAt(Instant.now());
|
||||
|
||||
// OJO: en PedidoDireccion usas Direccion.TipoIdentificacionFiscal
|
||||
// En FacturaDireccion usa el enum que hayas definido/importado.
|
||||
dst.setTipoIdentificacionFiscal(
|
||||
TipoIdentificacionFiscal.valueOf(src.getTipoIdentificacionFiscal().name())
|
||||
);
|
||||
|
||||
dst.setIdentificacionFiscal(src.getIdentificacionFiscal());
|
||||
|
||||
return dst;
|
||||
}
|
||||
|
||||
public static FacturaDireccion fromDireccion(com.imprimelibros.erp.direcciones.Direccion src) {
|
||||
if (src == null) return null;
|
||||
|
||||
FacturaDireccion dst = new FacturaDireccion();
|
||||
|
||||
dst.setUnidades(null);
|
||||
dst.setEmail(src.getUser().getUserName());
|
||||
dst.setAtt(src.getAtt());
|
||||
dst.setDireccion(src.getDireccion());
|
||||
dst.setCp(src.getCp());
|
||||
dst.setCiudad(src.getCiudad());
|
||||
dst.setProvincia(src.getProvincia());
|
||||
dst.setPaisCode3(src.getPais().getCode3());
|
||||
dst.setTelefono(src.getTelefono());
|
||||
dst.setInstrucciones(src.getInstrucciones());
|
||||
dst.setRazonSocial(src.getRazonSocial());
|
||||
dst.setCreatedAt(Instant.now());
|
||||
|
||||
dst.setTipoIdentificacionFiscal(src.getTipoIdentificacionFiscal());
|
||||
|
||||
dst.setIdentificacionFiscal(src.getIdentificacionFiscal());
|
||||
|
||||
return dst;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.imprimelibros.erp.facturacion.dto;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
public class FacturaGuardarDto {
|
||||
@Valid private FacturaCabeceraDto cabecera;
|
||||
@Valid private DireccionFacturacionDto direccionFacturacion;
|
||||
|
||||
// getters/setters
|
||||
public FacturaCabeceraDto getCabecera() {
|
||||
return cabecera;
|
||||
}
|
||||
public void setCabecera(FacturaCabeceraDto cabecera) {
|
||||
this.cabecera = cabecera;
|
||||
}
|
||||
public DireccionFacturacionDto getDireccionFacturacion() {
|
||||
return direccionFacturacion;
|
||||
}
|
||||
public void setDireccionFacturacion(DireccionFacturacionDto direccionFacturacion) {
|
||||
this.direccionFacturacion = direccionFacturacion;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
package com.imprimelibros.erp.facturacion.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class FacturaLineaUpsertDto {
|
||||
|
||||
// Para update puedes mandarlo, pero realmente lo sacamos del path
|
||||
private Long id;
|
||||
|
||||
@NotNull
|
||||
private String descripcion; // HTML
|
||||
|
||||
@NotNull
|
||||
private BigDecimal base;
|
||||
|
||||
private BigDecimal iva4;
|
||||
private BigDecimal iva21;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getDescripcion() { return descripcion; }
|
||||
public void setDescripcion(String descripcion) { this.descripcion = descripcion; }
|
||||
|
||||
public BigDecimal getBase() { return base; }
|
||||
public void setBase(BigDecimal base) { this.base = base; }
|
||||
|
||||
public BigDecimal getIva4() { return iva4; }
|
||||
public void setIva4(BigDecimal iva4) { this.iva4 = iva4; }
|
||||
|
||||
public BigDecimal getIva21() { return iva21; }
|
||||
public void setIva21(BigDecimal iva21) { this.iva21 = iva21; }
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.imprimelibros.erp.facturacion.dto;
|
||||
|
||||
import com.imprimelibros.erp.facturacion.TipoPago;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class FacturaPagoUpsertDto {
|
||||
|
||||
private Long id; // null => nuevo pago
|
||||
|
||||
@NotNull
|
||||
private TipoPago metodoPago;
|
||||
|
||||
@NotNull
|
||||
private BigDecimal cantidadPagada;
|
||||
|
||||
private LocalDateTime fechaPago;
|
||||
private String notas;
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public TipoPago getMetodoPago() { return metodoPago; }
|
||||
public void setMetodoPago(TipoPago metodoPago) { this.metodoPago = metodoPago; }
|
||||
|
||||
public BigDecimal getCantidadPagada() { return cantidadPagada; }
|
||||
public void setCantidadPagada(BigDecimal cantidadPagada) { this.cantidadPagada = cantidadPagada; }
|
||||
|
||||
public LocalDateTime getFechaPago() { return fechaPago; }
|
||||
public void setFechaPago(LocalDateTime fechaPago) { this.fechaPago = fechaPago; }
|
||||
|
||||
public String getNotas() { return notas; }
|
||||
public void setNotas(String notas) { this.notas = notas; }
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.imprimelibros.erp.facturacion.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public class SerieFacturaForm {
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 100)
|
||||
private String nombreSerie;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 10)
|
||||
private String prefijo;
|
||||
|
||||
@NotNull
|
||||
private Long numeroActual;
|
||||
|
||||
public String getNombreSerie() { return nombreSerie; }
|
||||
public void setNombreSerie(String nombreSerie) { this.nombreSerie = nombreSerie; }
|
||||
|
||||
public String getPrefijo() { return prefijo; }
|
||||
public void setPrefijo(String prefijo) { this.prefijo = prefijo; }
|
||||
|
||||
public Long getNumeroActual() { return numeroActual; }
|
||||
public void setNumeroActual(Long numeroActual) { this.numeroActual = numeroActual; }
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package com.imprimelibros.erp.facturacion.repo;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import com.imprimelibros.erp.facturacion.FacturaDireccion;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface FacturaDireccionRepository extends JpaRepository<FacturaDireccion, Long> {
|
||||
|
||||
List<FacturaDireccion> findByFacturaId(Long facturaId);
|
||||
|
||||
Optional<FacturaDireccion> findFirstByFacturaIdOrderByIdAsc(Long facturaId);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.imprimelibros.erp.facturacion.repo;
|
||||
|
||||
import com.imprimelibros.erp.facturacion.FacturaLinea;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface FacturaLineaRepository extends JpaRepository<FacturaLinea, Long> {
|
||||
List<FacturaLinea> findByFacturaId(Long facturaId);
|
||||
Optional<FacturaLinea> findByIdAndFacturaId(Long id, Long facturaId);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package com.imprimelibros.erp.facturacion.repo;
|
||||
|
||||
import com.imprimelibros.erp.facturacion.FacturaPago;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface FacturaPagoRepository extends JpaRepository<FacturaPago, Long> {
|
||||
List<FacturaPago> findByFacturaIdAndDeletedAtIsNullOrderByFechaPagoDescIdDesc(Long facturaId);
|
||||
Optional<FacturaPago> findByIdAndFacturaIdAndDeletedAtIsNull(Long id, Long facturaId);
|
||||
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package com.imprimelibros.erp.facturacion.repo;
|
||||
|
||||
import com.imprimelibros.erp.facturacion.EstadoFactura;
|
||||
import com.imprimelibros.erp.facturacion.EstadoPagoFactura;
|
||||
import com.imprimelibros.erp.facturacion.Factura;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface FacturaRepository extends JpaRepository<Factura, Long>, JpaSpecificationExecutor<Factura> {
|
||||
Optional<Factura> findByNumeroFactura(String numeroFactura);
|
||||
Factura findByPedidoId(Long pedidoId);
|
||||
List<Factura> findByClienteIdAndEstadoAndEstadoPagoAndSerieId(
|
||||
Long clienteId,
|
||||
EstadoFactura estado,
|
||||
EstadoPagoFactura estadoPago,
|
||||
Long serieId);
|
||||
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package com.imprimelibros.erp.facturacion.repo;
|
||||
|
||||
import com.imprimelibros.erp.facturacion.SerieFactura;
|
||||
import com.imprimelibros.erp.facturacion.TipoSerieFactura;
|
||||
import org.springframework.data.jpa.repository.*;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import jakarta.persistence.LockModeType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface SerieFacturaRepository
|
||||
extends JpaRepository<SerieFactura, Long>, JpaSpecificationExecutor<SerieFactura> {
|
||||
|
||||
Optional<SerieFactura> findByTipo(TipoSerieFactura tipo);
|
||||
|
||||
@Lock(LockModeType.PESSIMISTIC_WRITE)
|
||||
@Query("select s from SerieFactura s where s.id = :id")
|
||||
Optional<SerieFactura> findByIdForUpdate(@Param("id") Long id);
|
||||
|
||||
List<SerieFactura> findAllByDeletedAtIsNullOrderByNombreSerieAsc();
|
||||
|
||||
@Query("""
|
||||
select s
|
||||
from SerieFactura s
|
||||
where s.deletedAt is null
|
||||
and (:query is null or :query = '' or lower(s.nombreSerie) like lower(concat('%', :query, '%')))
|
||||
order by s.nombreSerie
|
||||
""")
|
||||
List<SerieFactura> searchForSelectSeriesFacturacion(@Param("query") String query);
|
||||
}
|
||||
@ -0,0 +1,725 @@
|
||||
package com.imprimelibros.erp.facturacion.service;
|
||||
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.configurationERP.VariableService;
|
||||
import com.imprimelibros.erp.facturacion.*;
|
||||
import com.imprimelibros.erp.facturacion.dto.DireccionFacturacionDto;
|
||||
import com.imprimelibros.erp.facturacion.dto.FacturaDireccionMapper;
|
||||
import com.imprimelibros.erp.facturacion.dto.FacturaGuardarDto;
|
||||
import com.imprimelibros.erp.facturacion.dto.FacturaLineaUpsertDto;
|
||||
import com.imprimelibros.erp.facturacion.dto.FacturaPagoUpsertDto;
|
||||
import com.imprimelibros.erp.facturacion.repo.FacturaDireccionRepository;
|
||||
import com.imprimelibros.erp.facturacion.repo.FacturaLineaRepository;
|
||||
import com.imprimelibros.erp.facturacion.repo.FacturaPagoRepository;
|
||||
import com.imprimelibros.erp.facturacion.repo.FacturaRepository;
|
||||
import com.imprimelibros.erp.facturacion.repo.SerieFacturaRepository;
|
||||
import com.imprimelibros.erp.pedidos.Pedido;
|
||||
import com.imprimelibros.erp.pedidos.PedidoDireccion;
|
||||
import com.imprimelibros.erp.pedidos.PedidoLinea;
|
||||
import com.imprimelibros.erp.pedidos.PedidoLineaRepository;
|
||||
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
import com.imprimelibros.erp.users.User;
|
||||
import com.imprimelibros.erp.users.UserService;
|
||||
import com.imprimelibros.erp.direcciones.Direccion;
|
||||
import com.imprimelibros.erp.direcciones.DireccionRepository;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Locale;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.security.Principal;
|
||||
import java.text.Collator;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
public class FacturacionService {
|
||||
|
||||
private final FacturaRepository facturaRepo;
|
||||
private final SerieFacturaRepository serieRepo;
|
||||
private final FacturaPagoRepository pagoRepo;
|
||||
private final FacturaLineaRepository lineaFacturaRepository;
|
||||
private final DireccionRepository direccionRepo;
|
||||
private final PedidoLineaRepository pedidoLineaRepo;
|
||||
private final UserService userService;
|
||||
private final Utils utils;
|
||||
private final MessageSource messageSource;
|
||||
private final PedidoService pedidoService;
|
||||
private final VariableService variableService;
|
||||
|
||||
public FacturacionService(
|
||||
FacturaRepository facturaRepo,
|
||||
FacturaLineaRepository lineaFacturaRepository,
|
||||
SerieFacturaRepository serieRepo,
|
||||
FacturaPagoRepository pagoRepo,
|
||||
DireccionRepository direccionRepo,
|
||||
PedidoLineaRepository pedidoLineaRepo,
|
||||
UserService userService,
|
||||
Utils utils,
|
||||
MessageSource messageSource,
|
||||
PedidoService pedidoService,
|
||||
VariableService variableService) {
|
||||
this.facturaRepo = facturaRepo;
|
||||
this.lineaFacturaRepository = lineaFacturaRepository;
|
||||
this.serieRepo = serieRepo;
|
||||
this.pagoRepo = pagoRepo;
|
||||
this.direccionRepo = direccionRepo;
|
||||
this.pedidoLineaRepo = pedidoLineaRepo;
|
||||
this.userService = userService;
|
||||
this.utils = utils;
|
||||
this.messageSource = messageSource;
|
||||
this.pedidoService = pedidoService;
|
||||
this.variableService = variableService;
|
||||
}
|
||||
|
||||
public SerieFactura getDefaultSerieFactura() {
|
||||
|
||||
Long defaultSerieId = variableService.getValorEntero("serie_facturacion_default").longValue();
|
||||
SerieFactura serie = serieRepo.findById(defaultSerieId).orElse(null);
|
||||
if (serie == null) {
|
||||
throw new IllegalStateException("No hay ninguna serie de facturación configurada.");
|
||||
}
|
||||
return serie;
|
||||
}
|
||||
|
||||
public Factura getFactura(Long facturaId) {
|
||||
return facturaRepo.findById(facturaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||
}
|
||||
|
||||
public Long getFacturaIdFromPedidoId(Long pedidoId) {
|
||||
Factura factura = facturaRepo.findByPedidoId(pedidoId);
|
||||
if (factura == null) {
|
||||
throw new EntityNotFoundException("Factura no encontrada para el pedido: " + pedidoId);
|
||||
}
|
||||
return factura.getId();
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// Nueva factura
|
||||
// -----------------------
|
||||
@Transactional
|
||||
public Factura crearNuevaFacturaAuto(Pedido pedido, SerieFactura serie, TipoPago tipoPago, Locale locale) {
|
||||
|
||||
Factura factura = new Factura();
|
||||
factura.setCliente(pedido.getCreatedBy());
|
||||
factura.setCreatedAt(Instant.now());
|
||||
factura.setUpdatedAt(Instant.now());
|
||||
Boolean pedidoPendientePago = false;
|
||||
List<PedidoLinea> lineasPedido = pedidoLineaRepo.findByPedidoId(pedido.getId());
|
||||
for (PedidoLinea lineaPedido : lineasPedido) {
|
||||
if (lineaPedido.getEstado() == PedidoLinea.Estado.pendiente_pago) {
|
||||
pedidoPendientePago = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
factura.setEstado(pedidoPendientePago ? EstadoFactura.borrador : EstadoFactura.validada);
|
||||
factura.setEstadoPago(pedidoPendientePago ? EstadoPagoFactura.pendiente : EstadoPagoFactura.pagada);
|
||||
factura.setTipoPago(pedidoPendientePago ? TipoPago.otros : tipoPago);
|
||||
factura.setPedidoId(pedido.getId());
|
||||
factura.setSerie(serie);
|
||||
factura.setNumeroFactura(this.getNumberFactura(serie));
|
||||
factura.setFechaEmision(LocalDateTime.now());
|
||||
factura.setBaseImponible(BigDecimal.valueOf(pedido.getBase()).setScale(2, RoundingMode.HALF_UP));
|
||||
factura.setIva4(BigDecimal.valueOf(pedido.getIva4()).setScale(2, RoundingMode.HALF_UP));
|
||||
factura.setIva21(BigDecimal.valueOf(pedido.getIva21()).setScale(2, RoundingMode.HALF_UP));
|
||||
factura.setTotalFactura(BigDecimal.valueOf(pedido.getTotal()).setScale(2, RoundingMode.HALF_UP));
|
||||
factura.setTotalPagado(BigDecimal.valueOf(pedido.getTotal()).setScale(2, RoundingMode.HALF_UP));
|
||||
// rellenar lineas
|
||||
List<FacturaLinea> lineasFactura = new ArrayList<>();
|
||||
for (PedidoLinea lineaPedido : lineasPedido) {
|
||||
Presupuesto p = lineaPedido.getPresupuesto();
|
||||
FacturaLinea lineaFactura = new FacturaLinea();
|
||||
lineaFactura.setDescripcion(this.obtenerLineaFactura(lineaPedido, locale));
|
||||
lineaFactura.setCantidad(p.getSelectedTirada());
|
||||
lineaFactura.setBaseLinea(p.getBaseImponible());
|
||||
lineaFactura.setIva4Linea(p.getIvaImporte4());
|
||||
lineaFactura.setIva21Linea(p.getIvaImporte21());
|
||||
lineaFactura.setTotalLinea(p.getTotalConIva());
|
||||
lineaFactura.setCreatedBy(p.getUser());
|
||||
lineaFactura.setFactura(factura);
|
||||
lineasFactura.add(lineaFactura);
|
||||
}
|
||||
if(pedido.getEnvio() > 0){
|
||||
FacturaLinea lineaEnvio = new FacturaLinea();
|
||||
lineaEnvio.setDescripcion(messageSource.getMessage("facturas.lineas.gastos-envio", null, "Gastos de envío", locale));
|
||||
lineaEnvio.setCantidad(1);
|
||||
BigDecimal baseEnvio = BigDecimal.valueOf(pedido.getEnvio()).setScale(2, RoundingMode.HALF_UP);
|
||||
lineaEnvio.setBaseLinea(baseEnvio);
|
||||
BigDecimal iva21Envio = baseEnvio.multiply(BigDecimal.valueOf(0.21)).setScale(2, RoundingMode.HALF_UP);
|
||||
lineaEnvio.setIva21Linea(iva21Envio);
|
||||
lineaEnvio.setIva4Linea(BigDecimal.ZERO);
|
||||
lineaEnvio.setTotalLinea(baseEnvio.add(iva21Envio));
|
||||
lineaEnvio.setCreatedBy(pedido.getCreatedBy());
|
||||
lineaEnvio.setCreatedAt(Instant.now());
|
||||
lineaEnvio.setFactura(factura);
|
||||
lineasFactura.add(lineaEnvio);
|
||||
}
|
||||
PedidoDireccion direccionPedido = pedidoService.getDireccionFacturacionPedido(pedido.getId());
|
||||
if(direccionPedido == null){
|
||||
throw new IllegalStateException("El pedido no tiene una dirección de facturación asociada.");
|
||||
}
|
||||
FacturaDireccion fd = FacturaDireccionMapper.fromPedidoDireccion(direccionPedido);
|
||||
|
||||
factura.addDireccion(fd);
|
||||
factura.setLineas(lineasFactura);
|
||||
|
||||
factura = facturaRepo.save(factura);
|
||||
|
||||
if (pedidoPendientePago) {
|
||||
return factura;
|
||||
}
|
||||
FacturaPago pago = new FacturaPago();
|
||||
pago.setMetodoPago(tipoPago);
|
||||
pago.setCantidadPagada(factura.getTotalFactura());
|
||||
pago.setFechaPago(LocalDateTime.now());
|
||||
pago.setFactura(factura);
|
||||
pago.setCreatedBy(pedido.getCreatedBy());
|
||||
pago.setCreatedAt(Instant.now());
|
||||
pagoRepo.save(pago);
|
||||
|
||||
return factura;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Factura crearNuevaFactura(Long userId, Long serieId, Long direccionId, Long facturaRectificadaId) {
|
||||
User cliente = userService.findById(userId);
|
||||
if (cliente == null) {
|
||||
throw new EntityNotFoundException("Cliente no encontrado: " + userId);
|
||||
}
|
||||
|
||||
SerieFactura serie = serieRepo.findById(serieId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Serie no encontrada: " + serieId));
|
||||
|
||||
Factura factura = new Factura();
|
||||
factura.setCliente(cliente);
|
||||
factura.setPedidoId(null);
|
||||
factura.setSerie(serie);
|
||||
factura.setEstado(EstadoFactura.borrador);
|
||||
factura.setEstadoPago(EstadoPagoFactura.pendiente);
|
||||
factura.setFechaEmision(LocalDateTime.now());
|
||||
factura.setCreatedAt(Instant.now());
|
||||
factura.setUpdatedAt(Instant.now());
|
||||
factura.setNumeroFactura(null);
|
||||
factura.setBaseImponible(BigDecimal.ZERO);
|
||||
factura.setIva4(BigDecimal.ZERO);
|
||||
factura.setIva21(BigDecimal.ZERO);
|
||||
factura.setTotalFactura(BigDecimal.ZERO);
|
||||
factura.setTotalPagado(BigDecimal.ZERO);
|
||||
factura.setLineas(new ArrayList<>());
|
||||
factura.setPagos(new ArrayList<>());
|
||||
Direccion direccion = direccionRepo.findById(direccionId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Dirección de factura no encontrada: " + direccionId));
|
||||
FacturaDireccion facturaDireccion = FacturaDireccionMapper.fromDireccion(direccion);
|
||||
factura.addDireccion(facturaDireccion);
|
||||
if(facturaRectificadaId != null){
|
||||
Factura facturaRectificada = facturaRepo.findById(facturaRectificadaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura rectificada no encontrada: " + facturaRectificadaId));
|
||||
factura.setFacturaRectificativa(facturaRectificada);
|
||||
facturaRectificada.setFacturaRectificada(factura);
|
||||
}
|
||||
return facturaRepo.save(factura);
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// Estado / Numeración
|
||||
// -----------------------
|
||||
|
||||
@Transactional
|
||||
public String getNumberFactura(SerieFactura serie) {
|
||||
|
||||
try {
|
||||
long next = (serie.getNumeroActual() == null) ? 1L : serie.getNumeroActual();
|
||||
String numeroFactura = buildNumeroFactura(serie.getPrefijo(), next);
|
||||
|
||||
// Incrementar contador para la siguiente
|
||||
serie.setNumeroActual(next + 1);
|
||||
serieRepo.save(serie);
|
||||
|
||||
return numeroFactura;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void guardarCabeceraYDireccionFacturacion(Long facturaId, FacturaGuardarDto dto) {
|
||||
Factura factura = getFactura(facturaId);
|
||||
|
||||
// ✅ Solo editable si borrador (tu regla actual para cabecera/dirección)
|
||||
if (factura.getEstado() != EstadoFactura.borrador) {
|
||||
throw new IllegalStateException("Solo se puede guardar cabecera/dirección en borrador.");
|
||||
}
|
||||
|
||||
// 1) Cabecera
|
||||
if (dto.getCabecera() != null) {
|
||||
var c = dto.getCabecera();
|
||||
|
||||
if (c.getSerieId() != null) {
|
||||
SerieFactura serie = serieRepo.findById(c.getSerieId())
|
||||
.orElseThrow(() -> new EntityNotFoundException("Serie no encontrada: " + c.getSerieId()));
|
||||
factura.setSerie(serie);
|
||||
}
|
||||
|
||||
if (c.getClienteId() != null) {
|
||||
User cliente = userService.findById(c.getClienteId());
|
||||
if(cliente == null){
|
||||
throw new EntityNotFoundException("Cliente no encontrado: " + c.getClienteId());
|
||||
}
|
||||
factura.setCliente(cliente);
|
||||
}
|
||||
|
||||
if (c.getFechaEmision() != null) {
|
||||
factura.setFechaEmision(c.getFechaEmision());
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Dirección de facturación del pedido asociado
|
||||
Long pedidoId = factura.getPedidoId();
|
||||
if (pedidoId != null && dto.getDireccionFacturacion() != null) {
|
||||
pedidoService.upsertDireccionFacturacion(pedidoId, dto.getDireccionFacturacion());
|
||||
|
||||
}
|
||||
upsertDireccionFacturacion(facturaId, dto.getDireccionFacturacion());
|
||||
|
||||
facturaRepo.save(factura);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Factura validarFactura(Long facturaId) {
|
||||
Factura factura = facturaRepo.findById(facturaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||
|
||||
// Puedes permitir validar desde borrador solamente (lo normal)
|
||||
if (factura.getEstado() == EstadoFactura.validada) {
|
||||
return factura;
|
||||
}
|
||||
|
||||
if (factura.getFechaEmision() == null) {
|
||||
factura.setFechaEmision(LocalDateTime.now());
|
||||
}
|
||||
|
||||
if (factura.getSerie() == null) {
|
||||
throw new IllegalStateException("La factura no tiene serie asignada.");
|
||||
}
|
||||
|
||||
// Si ya tiene numero_factura, no reservamos otro
|
||||
if (factura.getNumeroFactura() == null || factura.getNumeroFactura().isBlank()) {
|
||||
SerieFactura serieLocked = serieRepo.findByIdForUpdate(factura.getSerie().getId())
|
||||
.orElseThrow(
|
||||
() -> new EntityNotFoundException("Serie no encontrada: " + factura.getSerie().getId()));
|
||||
|
||||
long next = (serieLocked.getNumeroActual() == null) ? 1L : serieLocked.getNumeroActual();
|
||||
String numeroFactura = buildNumeroFactura(serieLocked.getPrefijo(), next);
|
||||
|
||||
factura.setNumeroFactura(numeroFactura);
|
||||
|
||||
// Incrementar contador para la siguiente
|
||||
serieLocked.setNumeroActual(next + 1);
|
||||
serieRepo.save(serieLocked);
|
||||
}
|
||||
|
||||
recalcularTotales(factura);
|
||||
factura.setEstado(EstadoFactura.validada);
|
||||
|
||||
return facturaRepo.save(factura);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Factura volverABorrador(Long facturaId) {
|
||||
Factura factura = facturaRepo.findById(facturaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||
|
||||
factura.setEstado(EstadoFactura.borrador);
|
||||
// No tocamos numero_factura (se conserva) -> evita duplicados y auditoría rara
|
||||
|
||||
recalcularTotales(factura);
|
||||
return facturaRepo.save(factura);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Boolean upsertDireccionFacturacion(Long facturaId, DireccionFacturacionDto direccionData) {
|
||||
try {
|
||||
Factura factura = facturaRepo.findById(facturaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||
|
||||
// ✅ Solo editable si borrador (tu regla actual para cabecera/dirección)
|
||||
if (factura.getEstado() != EstadoFactura.borrador) {
|
||||
throw new IllegalStateException("Solo se puede guardar dirección en borrador.");
|
||||
}
|
||||
|
||||
factura.getDirecciones().clear();
|
||||
factura.addDireccion(direccionData.toFacturaDireccion());
|
||||
facturaRepo.save(factura);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> getForSelectFacturasRectificables(String q1, String q2, Long userId) {
|
||||
try {
|
||||
String search = Optional.ofNullable(q1).orElse(q2);
|
||||
if (search != null) {
|
||||
search = search.trim();
|
||||
}
|
||||
final String q = (search == null || search.isEmpty())
|
||||
? null
|
||||
: search.toLowerCase();
|
||||
|
||||
List<Factura> all = facturaRepo.findByClienteIdAndEstadoAndEstadoPagoAndSerieId(
|
||||
userId,
|
||||
EstadoFactura.validada,
|
||||
EstadoPagoFactura.pagada,
|
||||
variableService.getValorEntero("serie_facturacion_default").longValue());
|
||||
|
||||
// Mapear a opciones id/text con i18n y filtrar por búsqueda si llega
|
||||
List<Map<String, String>> options = all.stream()
|
||||
.map(f -> {
|
||||
String id = f.getId().toString();
|
||||
String text = f.getNumeroFactura();
|
||||
Map<String, String> m = new HashMap<>();
|
||||
m.put("id", id); // lo normal en Select2: id = valor que guardarás (code3)
|
||||
m.put("text", text); // texto mostrado, i18n con fallback a keyword
|
||||
return m;
|
||||
})
|
||||
.filter(opt -> {
|
||||
if (q == null || q.isEmpty())
|
||||
return true;
|
||||
String text = opt.get("text").toLowerCase();
|
||||
return text.contains(q);
|
||||
})
|
||||
.sorted(Comparator.comparing(m -> m.get("text"), Collator.getInstance()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Estructura Select2
|
||||
Map<String, Object> resp = new HashMap<>();
|
||||
resp.put("results", options);
|
||||
return resp;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return Map.of("results", List.of());
|
||||
}
|
||||
}
|
||||
|
||||
private String buildNumeroFactura(String prefijo, long numero) {
|
||||
String pref = (prefijo == null) ? "" : prefijo.trim();
|
||||
String num = String.format("%05d", numero);
|
||||
return pref.isBlank() ? num : (pref + " " + num + "/" + LocalDate.now().getYear());
|
||||
}
|
||||
|
||||
|
||||
// -----------------------
|
||||
// Líneas
|
||||
// -----------------------
|
||||
@Transactional
|
||||
public void createLinea(Long facturaId, FacturaLineaUpsertDto req) {
|
||||
Factura factura = this.getFactura(facturaId);
|
||||
|
||||
FacturaLinea lf = new FacturaLinea();
|
||||
lf.setFactura(factura);
|
||||
lf.setCantidad(1);
|
||||
|
||||
applyRequest(lf, req);
|
||||
|
||||
lineaFacturaRepository.save(lf);
|
||||
|
||||
this.recalcularTotales(factura);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Factura upsertLinea(Long facturaId, FacturaLineaUpsertDto dto) {
|
||||
Factura factura = facturaRepo.findById(facturaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||
|
||||
if (factura.getEstado() != EstadoFactura.borrador) {
|
||||
throw new IllegalStateException("Solo se pueden editar líneas en facturas en borrador.");
|
||||
}
|
||||
|
||||
FacturaLinea linea;
|
||||
if (dto.getId() == null) {
|
||||
linea = new FacturaLinea();
|
||||
linea.setFactura(factura);
|
||||
factura.getLineas().add(linea);
|
||||
} else {
|
||||
linea = factura.getLineas().stream()
|
||||
.filter(l -> dto.getId().equals(l.getId()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new EntityNotFoundException("Línea no encontrada: " + dto.getId()));
|
||||
}
|
||||
|
||||
linea.setDescripcion(dto.getDescripcion());
|
||||
|
||||
linea.setBaseLinea(scale2(dto.getBase()));
|
||||
|
||||
linea.setIva4Linea(dto.getIva4());
|
||||
linea.setIva21Linea(dto.getIva21());
|
||||
|
||||
linea.setTotalLinea(scale2(linea.getBaseLinea()
|
||||
.add(nvl(linea.getIva4Linea()))
|
||||
.add(nvl(linea.getIva21Linea()))));
|
||||
|
||||
recalcularTotales(factura);
|
||||
return facturaRepo.save(factura);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Factura borrarLinea(Long facturaId, Long lineaId) {
|
||||
Factura factura = facturaRepo.findById(facturaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||
|
||||
if (factura.getEstado() != EstadoFactura.borrador) {
|
||||
throw new IllegalStateException("Solo se pueden borrar líneas en facturas en borrador.");
|
||||
}
|
||||
|
||||
boolean removed = factura.getLineas().removeIf(l -> lineaId.equals(l.getId()));
|
||||
if (!removed) {
|
||||
throw new EntityNotFoundException("Línea no encontrada: " + lineaId);
|
||||
}
|
||||
|
||||
recalcularTotales(factura);
|
||||
return facturaRepo.save(factura);
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// Pagos
|
||||
// -----------------------
|
||||
|
||||
@Transactional
|
||||
public Factura upsertPago(Long facturaId, FacturaPagoUpsertDto dto, Principal principal) {
|
||||
Factura factura = facturaRepo.findById(facturaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||
|
||||
// Permitir añadir pagos tanto en borrador como validada (según tu regla)
|
||||
FacturaPago pago;
|
||||
if (dto.getId() == null) {
|
||||
pago = new FacturaPago();
|
||||
pago.setFactura(factura);
|
||||
pago.setCreatedBy(Utils.currentUser(principal));
|
||||
pago.setCreatedAt(Instant.now());
|
||||
factura.getPagos().add(pago);
|
||||
} else {
|
||||
pago = factura.getPagos().stream()
|
||||
.filter(p -> dto.getId().equals(p.getId()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new EntityNotFoundException("Pago no encontrado: " + dto.getId()));
|
||||
}
|
||||
|
||||
pago.setMetodoPago(dto.getMetodoPago());
|
||||
pago.setCantidadPagada(scale2(dto.getCantidadPagada()));
|
||||
pago.setFechaPago(dto.getFechaPago() != null ? dto.getFechaPago() : LocalDateTime.now());
|
||||
pago.setNotas(dto.getNotas());
|
||||
pago.setUpdatedAt(Instant.now());
|
||||
pago.setUpdatedBy(Utils.currentUser(principal));
|
||||
// El tipo_pago de la factura: si tiene un pago, lo reflejamos (último pago
|
||||
// manda)
|
||||
factura.setTipoPago(dto.getMetodoPago());
|
||||
|
||||
recalcularTotales(factura);
|
||||
return facturaRepo.save(factura);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Factura borrarPago(Long facturaId, Long pagoId, Principal principal) {
|
||||
Factura factura = facturaRepo.findById(facturaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||
|
||||
FacturaPago pago = factura.getPagos().stream()
|
||||
.filter(p -> pagoId.equals(p.getId()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new EntityNotFoundException("Pago no encontrado: " + pagoId));
|
||||
|
||||
// soft delete
|
||||
pago.setDeletedAt(Instant.now());
|
||||
pago.setDeletedBy(Utils.currentUser(principal));
|
||||
|
||||
recalcularTotales(factura);
|
||||
return facturaRepo.save(factura);
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// Recalcular totales
|
||||
// -----------------------
|
||||
|
||||
@Transactional
|
||||
public void recalcularTotales(Long facturaId) {
|
||||
Factura factura = facturaRepo.findById(facturaId)
|
||||
.orElseThrow(() -> new EntityNotFoundException("Factura no encontrada: " + facturaId));
|
||||
recalcularTotales(factura);
|
||||
facturaRepo.save(factura);
|
||||
}
|
||||
|
||||
private void recalcularTotales(Factura factura) {
|
||||
BigDecimal base = BigDecimal.ZERO;
|
||||
BigDecimal iva4 = BigDecimal.ZERO;
|
||||
BigDecimal iva21 = BigDecimal.ZERO;
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
|
||||
if (factura.getLineas() != null) {
|
||||
for (FacturaLinea l : factura.getLineas()) {
|
||||
base = base.add(nvl(l.getBaseLinea()));
|
||||
iva4 = iva4.add(nvl(l.getIva4Linea()));
|
||||
iva21 = iva21.add(nvl(l.getIva21Linea()));
|
||||
total = total.add(nvl(l.getTotalLinea()));
|
||||
}
|
||||
}
|
||||
|
||||
factura.setBaseImponible(scale2(base));
|
||||
factura.setIva4(scale2(iva4));
|
||||
factura.setIva21(scale2(iva21));
|
||||
factura.setTotalFactura(scale2(total));
|
||||
|
||||
// total_pagado
|
||||
BigDecimal pagado = BigDecimal.ZERO;
|
||||
if (factura.getPagos() != null) {
|
||||
for (FacturaPago p : factura.getPagos()) {
|
||||
if (p.getDeletedAt() != null)
|
||||
continue;
|
||||
pagado = pagado.add(nvl(p.getCantidadPagada()));
|
||||
}
|
||||
}
|
||||
factura.setTotalPagado(scale2(pagado));
|
||||
|
||||
// estado_pago
|
||||
// - cancelada: si la factura está marcada como cancelada manualmente (aquí NO
|
||||
// lo hacemos automático)
|
||||
// - pagada: si total_pagado >= total_factura y total_factura > 0
|
||||
// - pendiente: resto
|
||||
if (factura.getEstadoPago() == EstadoPagoFactura.cancelada) {
|
||||
return;
|
||||
}
|
||||
|
||||
BigDecimal totalFactura = nvl(factura.getTotalFactura());
|
||||
if (totalFactura.compareTo(BigDecimal.ZERO) > 0 &&
|
||||
factura.getTotalPagado().compareTo(totalFactura) >= 0) {
|
||||
factura.setEstadoPago(EstadoPagoFactura.pagada);
|
||||
} else {
|
||||
factura.setEstadoPago(EstadoPagoFactura.pendiente);
|
||||
}
|
||||
}
|
||||
|
||||
private static BigDecimal nvl(BigDecimal v) {
|
||||
return v == null ? BigDecimal.ZERO : v;
|
||||
}
|
||||
|
||||
private static BigDecimal scale2(BigDecimal v) {
|
||||
return (v == null ? BigDecimal.ZERO : v).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private String obtenerLineaFactura(PedidoLinea lineaPedido, Locale locale) {
|
||||
|
||||
Map<String, Object> specs = utils.getTextoPresupuesto(lineaPedido.getPresupuesto(), locale);
|
||||
|
||||
StringBuilder html = new StringBuilder();
|
||||
html.append("<div class=\"specs-wrapper align-with-text \">")
|
||||
.append("<div class=\"specs\">");
|
||||
|
||||
if (specs == null) {
|
||||
return "<div></div>";
|
||||
}
|
||||
|
||||
// 1) Líneas del presupuesto (HTML)
|
||||
Object lineasObj = specs.get("lineas");
|
||||
if (lineasObj instanceof List<?> lineasList) {
|
||||
for (Object o : lineasList) {
|
||||
if (!(o instanceof Map<?, ?> m))
|
||||
continue;
|
||||
|
||||
Object descObj = m.get("descripcion");
|
||||
String descripcionHtml = descObj != null ? descObj.toString() : "";
|
||||
if (descripcionHtml.isBlank())
|
||||
continue;
|
||||
|
||||
html.append("<div class=\"spec-row mb-1\">")
|
||||
.append("<span class=\"spec-label\">")
|
||||
.append(descripcionHtml) // OJO: esto es HTML (como th:utext)
|
||||
.append("</span>")
|
||||
.append("</div>");
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Servicios adicionales (texto)
|
||||
Object serviciosObj = specs.get("servicios");
|
||||
String servicios = (serviciosObj != null) ? serviciosObj.toString().trim() : "";
|
||||
if (!servicios.isBlank()) {
|
||||
String label = messageSource.getMessage("pdf.servicios-adicionales", null, "Servicios adicionales", locale);
|
||||
html.append("<div class=\"spec-row mb-1\">")
|
||||
.append("<span>").append(escapeHtml(label)).append("</span>")
|
||||
.append("<span class=\"spec-label\">").append(escapeHtml(servicios)).append("</span>")
|
||||
.append("</div>");
|
||||
}
|
||||
|
||||
// 3) Datos de maquetación (HTML)
|
||||
Object datosMaqObj = specs.get("datosMaquetacion");
|
||||
if (datosMaqObj != null && !datosMaqObj.toString().isBlank()) {
|
||||
String label = messageSource.getMessage("pdf.datos-maquetacion", null, "Datos de maquetación:", locale);
|
||||
html.append("<div class=\"spec-row mb-1\">")
|
||||
.append("<span>").append(escapeHtml(label)).append("</span>")
|
||||
.append("<span class=\"spec-label\">")
|
||||
.append(datosMaqObj) // HTML (como th:utext)
|
||||
.append("</span>")
|
||||
.append("</div>");
|
||||
}
|
||||
|
||||
// 4) Datos de marcapáginas (HTML)
|
||||
Object datosMarcaObj = specs.get("datosMarcapaginas");
|
||||
if (datosMarcaObj != null && !datosMarcaObj.toString().isBlank()) {
|
||||
String label = messageSource.getMessage("pdf.datos-marcapaginas", null, "Datos de marcapáginas:", locale);
|
||||
html.append("<div class=\"spec-row mb-1\">")
|
||||
.append("<span>").append(escapeHtml(label)).append("</span>")
|
||||
.append("<span class=\"spec-label\">")
|
||||
.append(datosMarcaObj) // HTML (como th:utext)
|
||||
.append("</span>")
|
||||
.append("</div>");
|
||||
}
|
||||
|
||||
html.append("</div></div>");
|
||||
return html.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape mínimo para texto plano (equivalente a th:text).
|
||||
* No lo uses para fragmentos que ya son HTML (th:utext).
|
||||
*/
|
||||
private static String escapeHtml(String s) {
|
||||
if (s == null)
|
||||
return "";
|
||||
return s.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
.replace("'", "'");
|
||||
}
|
||||
|
||||
private void applyRequest(FacturaLinea lf, FacturaLineaUpsertDto req) {
|
||||
// HTML
|
||||
lf.setDescripcion(req.getDescripcion() == null ? "" : req.getDescripcion());
|
||||
|
||||
BigDecimal base = nvl(req.getBase());
|
||||
BigDecimal iva4 = nvl(req.getIva4());
|
||||
BigDecimal iva21 = nvl(req.getIva21());
|
||||
|
||||
lf.setBaseLinea(base);
|
||||
lf.setIva4Linea(iva4);
|
||||
lf.setIva21Linea(iva21);
|
||||
|
||||
// total de línea (por ahora)
|
||||
lf.setTotalLinea(base.add(iva4).add(iva21));
|
||||
}
|
||||
|
||||
}
|
||||
82
src/main/java/com/imprimelibros/erp/paises/Paises.java
Normal file
82
src/main/java/com/imprimelibros/erp/paises/Paises.java
Normal file
@ -0,0 +1,82 @@
|
||||
package com.imprimelibros.erp.paises;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "paises")
|
||||
public class Paises {
|
||||
|
||||
@Id
|
||||
@Column(name = "keyword", length = 64, nullable = false)
|
||||
private String keyword;
|
||||
|
||||
@Column(name = "code", length = 2, nullable = false, unique = true)
|
||||
private String code;
|
||||
|
||||
@Column(name = "code3", length = 3, nullable = false, unique = true)
|
||||
private String code3;
|
||||
|
||||
@Column(name = "currency", length = 3, nullable = false)
|
||||
private String currency;
|
||||
|
||||
// --- Getters & Setters ---
|
||||
|
||||
public String getKeyword() {
|
||||
return keyword;
|
||||
}
|
||||
|
||||
public void setKeyword(String keyword) {
|
||||
this.keyword = keyword;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode3() {
|
||||
return code3;
|
||||
}
|
||||
|
||||
public void setCode3(String code3) {
|
||||
this.code3 = code3;
|
||||
}
|
||||
|
||||
public String getCurrency() {
|
||||
return currency;
|
||||
}
|
||||
|
||||
public void setCurrency(String currency) {
|
||||
this.currency = currency;
|
||||
}
|
||||
|
||||
// --- toString, equals & hashCode ---
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Paises{" +
|
||||
"keyword='" + keyword + '\'' +
|
||||
", code='" + code + '\'' +
|
||||
", code3='" + code3 + '\'' +
|
||||
", currency='" + currency + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return keyword != null ? keyword.hashCode() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (!(obj instanceof Paises other)) return false;
|
||||
return keyword != null && keyword.equals(other.keyword);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package com.imprimelibros.erp.paises;
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/paises")
|
||||
public class PaisesController {
|
||||
|
||||
private final PaisesService paisesService;
|
||||
|
||||
public PaisesController(PaisesService paisesService) {
|
||||
this.paisesService = paisesService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compatible con Select2 (AJAX):
|
||||
* - Soporta parámetros opcionales:
|
||||
* - q / term : texto a buscar
|
||||
* - lang : ej. "es", "en" (si se omite usa Locale actual)
|
||||
*
|
||||
* Respuesta:
|
||||
* { "results": [ { "id": "espania", "text": "España" }, ... ] }
|
||||
*/
|
||||
@GetMapping
|
||||
public Map<String, Object> getPaises(
|
||||
@RequestParam(value = "q", required = false) String q1,
|
||||
@RequestParam(value = "term", required = false) String q2,
|
||||
Locale locale) {
|
||||
|
||||
|
||||
return paisesService.getForSelect(q1, q2, locale);
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.imprimelibros.erp.paises;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface PaisesRepository extends JpaRepository<Paises, String> {
|
||||
|
||||
Optional<Paises> findByCode(String code);
|
||||
|
||||
Optional<Paises> findByCode3(String code3);
|
||||
|
||||
Optional<Paises> findByCurrency(String currency);
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
package com.imprimelibros.erp.paises;
|
||||
|
||||
import java.text.Collator;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class PaisesService {
|
||||
|
||||
protected final PaisesRepository repo;
|
||||
protected final MessageSource messageSource;
|
||||
|
||||
public PaisesService(PaisesRepository repo, MessageSource messageSource) {
|
||||
this.repo = repo;
|
||||
this.messageSource = messageSource;
|
||||
}
|
||||
|
||||
public Map<String, Object> getForSelect(String q1, String q2, Locale locale) {
|
||||
|
||||
try {
|
||||
|
||||
// Termino de búsqueda (Select2 usa 'q' o 'term' según versión/config)
|
||||
String search = Optional.ofNullable(q1).orElse(q2);
|
||||
if (search != null) {
|
||||
search = search.trim();
|
||||
}
|
||||
final String q = (search == null || search.isEmpty())
|
||||
? null
|
||||
: search.toLowerCase(locale);
|
||||
|
||||
List<Paises> all = repo.findAll();
|
||||
|
||||
// Mapear a opciones id/text con i18n y filtrar por búsqueda si llega
|
||||
List<Map<String, String>> options = all.stream()
|
||||
.map(cc -> {
|
||||
String key = cc.getKeyword();
|
||||
String id = cc.getCode3();
|
||||
String text = messageSource.getMessage("paises." + key, null, key, locale);
|
||||
Map<String, String> m = new HashMap<>();
|
||||
m.put("id", id); // lo normal en Select2: id = valor que guardarás (code3)
|
||||
m.put("text", text); // texto mostrado, i18n con fallback a keyword
|
||||
return m;
|
||||
})
|
||||
.filter(opt -> {
|
||||
if (q == null || q.isEmpty())
|
||||
return true;
|
||||
String text = opt.get("text").toLowerCase(locale);
|
||||
String id = opt.get("id").toLowerCase(locale);
|
||||
return text.contains(q) || id.contains(q);
|
||||
})
|
||||
.sorted(Comparator.comparing(m -> m.get("text"), Collator.getInstance(locale)))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Estructura Select2
|
||||
Map<String, Object> resp = new HashMap<>();
|
||||
resp.put("results", options);
|
||||
return resp;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return Map.of("results", List.of());
|
||||
}
|
||||
}
|
||||
|
||||
public String getPaisNombrePorCode3(String code3, Locale locale) {
|
||||
if (code3 == null || code3.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
Optional<Paises> opt = repo.findByCode3(code3);
|
||||
if (opt.isPresent()) {
|
||||
Paises pais = opt.get();
|
||||
String key = pais.getKeyword();
|
||||
return messageSource.getMessage("paises." + key, null, key, locale);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,332 @@
|
||||
package com.imprimelibros.erp.payments;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.datatables.DataTable;
|
||||
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
||||
import com.imprimelibros.erp.datatables.DataTablesResponse;
|
||||
import com.imprimelibros.erp.i18n.TranslationService;
|
||||
import com.imprimelibros.erp.payments.model.Payment;
|
||||
import com.imprimelibros.erp.payments.model.PaymentTransaction;
|
||||
import com.imprimelibros.erp.payments.model.PaymentTransactionStatus;
|
||||
import com.imprimelibros.erp.payments.model.PaymentTransactionType;
|
||||
import com.imprimelibros.erp.payments.repo.PaymentTransactionRepository;
|
||||
import com.imprimelibros.erp.users.User;
|
||||
import com.imprimelibros.erp.users.UserDao;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/pagos")
|
||||
@PreAuthorize("hasRole('SUPERADMIN')")
|
||||
public class PaymentController {
|
||||
|
||||
protected final PaymentService paymentService;
|
||||
protected final MessageSource messageSource;
|
||||
protected final TranslationService translationService;
|
||||
protected final PaymentTransactionRepository repoPaymentTransaction;
|
||||
protected final UserDao repoUser;
|
||||
|
||||
public PaymentController(PaymentTransactionRepository repoPaymentTransaction, UserDao repoUser,
|
||||
MessageSource messageSource, TranslationService translationService, PaymentService paymentService) {
|
||||
this.repoPaymentTransaction = repoPaymentTransaction;
|
||||
this.repoUser = repoUser;
|
||||
this.messageSource = messageSource;
|
||||
this.translationService = translationService;
|
||||
this.paymentService = paymentService;
|
||||
|
||||
}
|
||||
|
||||
@GetMapping()
|
||||
public String index(Model model, Locale locale) {
|
||||
|
||||
List<String> keys = List.of(
|
||||
"app.cancelar",
|
||||
"app.aceptar",
|
||||
"pagos.refund.title",
|
||||
"pagos.refund.text",
|
||||
"pagos.refund.success",
|
||||
"pagos.refund.error.general",
|
||||
"pagos.refund.error.invalid-number",
|
||||
"pagos.transferencia.finalizar.title",
|
||||
"pagos.transferencia.finalizar.text",
|
||||
"pagos.transferencia.finalizar.success");
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
|
||||
return "imprimelibros/pagos/gestion-pagos";
|
||||
}
|
||||
|
||||
@GetMapping(value = "datatable/redsys", produces = "application/json")
|
||||
@ResponseBody
|
||||
public DataTablesResponse<Map<String, Object>> getDatatableRedsys(HttpServletRequest request, Locale locale) {
|
||||
|
||||
DataTablesRequest dt = DataTablesParser.from(request);
|
||||
|
||||
List<String> searchable = List.of(
|
||||
"payment.gatewayOrderId",
|
||||
"payment.orderId"
|
||||
// "client" no, porque lo calculas a posteriori
|
||||
);
|
||||
|
||||
// Campos ordenables
|
||||
List<String> orderable = List.of(
|
||||
"payment.gatewayOrderId",
|
||||
"payment.orderId",
|
||||
"amountCents",
|
||||
"payment.amountRefundedCents",
|
||||
"createdAt");
|
||||
|
||||
Specification<PaymentTransaction> base = Specification.allOf(
|
||||
(root, query, cb) -> cb.equal(root.get("status"), PaymentTransactionStatus.succeeded));
|
||||
base = base.and((root, query, cb) -> cb.equal(root.get("type"), PaymentTransactionType.CAPTURE));
|
||||
base = base.and((root, query, cb) -> cb.notEqual(root.join("payment").get("gateway"), "bank_transfer"));
|
||||
String clientSearch = dt.getColumnSearch("client");
|
||||
|
||||
// 2) Si hay filtro, traducirlo a userIds y añadirlo al Specification
|
||||
if (clientSearch != null) {
|
||||
List<Long> userIds = repoUser.findIdsByFullNameLike(clientSearch.trim());
|
||||
|
||||
if (userIds.isEmpty()) {
|
||||
// Ningún usuario coincide → forzamos 0 resultados
|
||||
base = base.and((root, query, cb) -> cb.disjunction());
|
||||
} else {
|
||||
base = base.and((root, query, cb) -> root.join("payment").get("userId").in(userIds));
|
||||
}
|
||||
}
|
||||
Long total = repoPaymentTransaction.count(base);
|
||||
|
||||
return DataTable
|
||||
.of(repoPaymentTransaction, PaymentTransaction.class, dt, searchable)
|
||||
.orderable(orderable)
|
||||
.add("created_at", pago -> Utils.formatDateTime(pago.getCreatedAt(), locale))
|
||||
.add("client", pago -> {
|
||||
if (pago.getPayment() != null && pago.getPayment().getUserId() != null) {
|
||||
Payment payment = pago.getPayment();
|
||||
if (payment.getUserId() != null) {
|
||||
Optional<User> user = repoUser.findById(payment.getUserId().longValue());
|
||||
return user.map(User::getFullName).orElse("");
|
||||
}
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.add("gateway_order_id", pago -> {
|
||||
if (pago.getPayment() != null) {
|
||||
return pago.getPayment().getGatewayOrderId();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
})
|
||||
.add("orderId", pago -> {
|
||||
if (pago.getPayment() != null && pago.getPayment().getOrderId() != null) {
|
||||
return pago.getPayment().getOrderId().toString();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
})
|
||||
.add("amount_cents", pago -> Utils.formatCurrency(pago.getAmountCents() / 100.0, locale))
|
||||
.add("amount_cents_refund", pago -> {
|
||||
Payment payment = pago.getPayment();
|
||||
if (payment != null) {
|
||||
return Utils.formatCurrency(payment.getAmountRefundedCents() / 100.0, locale);
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.add("actions", pago -> {
|
||||
Payment p = pago.getPayment();
|
||||
if (p != null) {
|
||||
if (pago.getAmountCents() - p.getAmountRefundedCents() > 0) {
|
||||
return "<span class=\'badge bg-secondary btn-refund-payment \' data-dsOrderId=\'"
|
||||
+ p.getGatewayOrderId()
|
||||
+ "\' data-transactionId=\'" + pago.getPayment().getId()
|
||||
+ "\' data-amount=\'" + (pago.getAmountCents() - p.getAmountRefundedCents())
|
||||
+ "\' style=\'cursor: pointer;\'>"
|
||||
+ messageSource.getMessage("pagos.table.devuelto", null, locale) + "</span>";
|
||||
}
|
||||
return "";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
})
|
||||
.where(base)
|
||||
.toJson(total);
|
||||
|
||||
}
|
||||
|
||||
@GetMapping(value = "datatable/transferencias", produces = "application/json")
|
||||
@ResponseBody
|
||||
public DataTablesResponse<Map<String, Object>> getDatatableTransferencias(HttpServletRequest request,
|
||||
Locale locale) {
|
||||
|
||||
DataTablesRequest dt = DataTablesParser.from(request);
|
||||
|
||||
List<String> searchable = List.of(
|
||||
// "client" no, porque lo calculas a posteriori
|
||||
);
|
||||
|
||||
// Campos ordenables
|
||||
List<String> orderable = List.of(
|
||||
"transferId",
|
||||
"status",
|
||||
"amountCents",
|
||||
"payment.amountRefundedCents",
|
||||
"createdAt", "updatedAt");
|
||||
|
||||
Specification<PaymentTransaction> base = (root, query, cb) -> cb.or(
|
||||
cb.equal(root.get("status"), PaymentTransactionStatus.pending),
|
||||
cb.equal(root.get("status"), PaymentTransactionStatus.succeeded));
|
||||
|
||||
base = base.and((root, query, cb) -> cb.equal(root.get("type"), PaymentTransactionType.CAPTURE));
|
||||
base = base.and((root, query, cb) -> cb.equal(root.get("payment").get("gateway"), "bank_transfer"));
|
||||
|
||||
String clientSearch = dt.getColumnSearch("client");
|
||||
|
||||
// 2) Si hay filtro, traducirlo a userIds y añadirlo al Specification
|
||||
if (clientSearch != null) {
|
||||
List<Long> userIds = repoUser.findIdsByFullNameLike(clientSearch.trim());
|
||||
|
||||
if (userIds.isEmpty()) {
|
||||
// Ningún usuario coincide → forzamos 0 resultados
|
||||
base = base.and((root, query, cb) -> cb.disjunction());
|
||||
} else {
|
||||
base = base.and((root, query, cb) -> root.join("payment").get("userId").in(userIds));
|
||||
}
|
||||
}
|
||||
Long total = repoPaymentTransaction.count(base);
|
||||
|
||||
return DataTable
|
||||
.of(repoPaymentTransaction, PaymentTransaction.class, dt, searchable)
|
||||
.orderable(orderable)
|
||||
.add("created_at", pago -> Utils.formatDateTime(pago.getCreatedAt(), locale))
|
||||
.add("processed_at", pago -> Utils.formatDateTime(pago.getProcessedAt(), locale))
|
||||
.add("client", pago -> {
|
||||
if (pago.getPayment() != null && pago.getPayment().getUserId() != null) {
|
||||
Payment payment = pago.getPayment();
|
||||
if (payment.getUserId() != null) {
|
||||
Optional<User> user = repoUser.findById(payment.getUserId().longValue());
|
||||
return user.map(User::getFullName).orElse("");
|
||||
}
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.add("transfer_id", pago -> {
|
||||
if (pago.getPayment() != null) {
|
||||
Long pedido = pago.getPayment().getOrderId();
|
||||
if (pedido != null) {
|
||||
return "TRANSF-" + pedido;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.add("order_id", pago -> {
|
||||
if (pago.getStatus() != PaymentTransactionStatus.pending) {
|
||||
if (pago.getPayment() != null && pago.getPayment().getOrderId() != null) {
|
||||
return pago.getPayment().getOrderId().toString();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return messageSource.getMessage("pagos.transferencia.no-pedido", null, "Pendiente", locale);
|
||||
|
||||
}).add("amount_cents", pago -> Utils.formatCurrency(pago.getAmountCents() / 100.0, locale))
|
||||
.add("amount_cents_refund", pago ->
|
||||
|
||||
{
|
||||
Payment payment = pago.getPayment();
|
||||
if (payment != null) {
|
||||
return Utils.formatCurrency(payment.getAmountRefundedCents() / 100.0, locale);
|
||||
}
|
||||
return "";
|
||||
}).add("status", pago -> {
|
||||
switch (pago.getStatus()) {
|
||||
case PaymentTransactionStatus.pending:
|
||||
return messageSource.getMessage("pagos.table.estado.pending", null, "Pendiente", locale);
|
||||
case PaymentTransactionStatus.succeeded:
|
||||
return messageSource.getMessage("pagos.table.estado.succeeded", null, "Completada", locale);
|
||||
case PaymentTransactionStatus.failed:
|
||||
return messageSource.getMessage("pagos.table.estado.failed", null, "Fallido", locale);
|
||||
default:
|
||||
return pago.getStatus().name();
|
||||
}
|
||||
}).add("actions", pago -> {
|
||||
Payment p = pago.getPayment();
|
||||
if (p != null) {
|
||||
String actions = "";
|
||||
if (pago.getStatus() != PaymentTransactionStatus.succeeded) {
|
||||
actions += "<span class=\'badge bg-success btn-mark-as-completed \' data-paymentId=\'"
|
||||
+ p.getId()
|
||||
+ "\' data-transactionId=\'" + pago.getPayment().getId()
|
||||
+ "\' style=\'cursor: pointer;\'>"
|
||||
+ messageSource.getMessage("pagos.table.finalizar", null, locale) + "</span> ";
|
||||
|
||||
}
|
||||
if ((pago.getAmountCents() - p.getAmountRefundedCents() > 0)
|
||||
&& pago.getStatus() == PaymentTransactionStatus.succeeded) {
|
||||
actions += "<span class=\'badge bg-secondary btn-transfer-refund \' data-dsOrderId=\'"
|
||||
+ p.getGatewayOrderId()
|
||||
+ "\' data-transactionId=\'" + pago.getPayment().getId()
|
||||
+ "\' data-amount=\'" + (pago.getAmountCents() - p.getAmountRefundedCents())
|
||||
+ "\' style=\'cursor: pointer;\'>"
|
||||
+ messageSource.getMessage("pagos.table.devuelto", null, locale) + "</span>";
|
||||
}
|
||||
return actions;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}).where(base).toJson(total);
|
||||
|
||||
}
|
||||
|
||||
@PostMapping(value = "/transfer/completed/{id}", produces = "application/json")
|
||||
public ResponseEntity<Map<String, Object>> markTransferAsCaptured(@PathVariable Long id, Locale locale) {
|
||||
|
||||
Map<String, Object> response;
|
||||
try {
|
||||
paymentService.markBankTransferAsCaptured(id, locale);
|
||||
response = Map.of("success", true);
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
response = Map.of("success", false);
|
||||
response.put("error", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/transfer/refund/{id}", produces = "application/json")
|
||||
public ResponseEntity<Map<String, Object>> refundTransfer(@PathVariable Long id,
|
||||
@RequestParam("amountCents") Long amountCents) {
|
||||
|
||||
Map<String, Object> response;
|
||||
try {
|
||||
paymentService.refundBankTransfer(id, amountCents);
|
||||
response = Map.of("success", true);
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
response = Map.of("success", false);
|
||||
response.put("error", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
574
src/main/java/com/imprimelibros/erp/payments/PaymentService.java
Normal file
574
src/main/java/com/imprimelibros/erp/payments/PaymentService.java
Normal file
@ -0,0 +1,574 @@
|
||||
package com.imprimelibros.erp.payments;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.imprimelibros.erp.cart.Cart;
|
||||
import com.imprimelibros.erp.cart.CartService;
|
||||
import com.imprimelibros.erp.facturacion.SerieFactura;
|
||||
import com.imprimelibros.erp.facturacion.TipoPago;
|
||||
import com.imprimelibros.erp.facturacion.service.FacturacionService;
|
||||
import com.imprimelibros.erp.payments.model.*;
|
||||
import com.imprimelibros.erp.payments.repo.PaymentRepository;
|
||||
import com.imprimelibros.erp.payments.repo.PaymentTransactionRepository;
|
||||
import com.imprimelibros.erp.payments.repo.RefundRepository;
|
||||
import com.imprimelibros.erp.redsys.RedsysService;
|
||||
import com.imprimelibros.erp.redsys.RedsysService.FormPayload;
|
||||
import com.imprimelibros.erp.redsys.RedsysService.RedsysNotification;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.imprimelibros.erp.payments.repo.WebhookEventRepository;
|
||||
import com.imprimelibros.erp.pedidos.Pedido;
|
||||
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
public class PaymentService {
|
||||
|
||||
private final PaymentRepository payRepo;
|
||||
private final PaymentTransactionRepository txRepo;
|
||||
private final RefundRepository refundRepo;
|
||||
private final RedsysService redsysService;
|
||||
private final WebhookEventRepository webhookEventRepo;
|
||||
private final ObjectMapper om = new ObjectMapper();
|
||||
private final CartService cartService;
|
||||
private final PedidoService pedidoService;
|
||||
private final FacturacionService facturacionService;
|
||||
|
||||
public PaymentService(PaymentRepository payRepo,
|
||||
PaymentTransactionRepository txRepo,
|
||||
RefundRepository refundRepo,
|
||||
RedsysService redsysService,
|
||||
WebhookEventRepository webhookEventRepo,
|
||||
CartService cartService,
|
||||
PedidoService pedidoService,
|
||||
FacturacionService facturacionService) {
|
||||
this.payRepo = payRepo;
|
||||
this.txRepo = txRepo;
|
||||
this.refundRepo = refundRepo;
|
||||
this.redsysService = redsysService;
|
||||
this.webhookEventRepo = webhookEventRepo;
|
||||
this.cartService = cartService;
|
||||
this.pedidoService = pedidoService;
|
||||
this.facturacionService = facturacionService;
|
||||
}
|
||||
|
||||
public Payment findFailedPaymentByOrderId(Long orderId) {
|
||||
return payRepo.findFirstByOrderIdAndStatusOrderByIdDesc(orderId, PaymentStatus.failed)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public Map<String, Long> getPaymentTransactionData(Long paymentId) {
|
||||
PaymentTransaction tx = txRepo.findByPaymentIdAndType(
|
||||
paymentId,
|
||||
PaymentTransactionType.CAPTURE)
|
||||
.orElse(null);
|
||||
if (tx == null) {
|
||||
return null;
|
||||
}
|
||||
String resp_payload = tx.getResponsePayload();
|
||||
try {
|
||||
ObjectMapper om = new ObjectMapper();
|
||||
var node = om.readTree(resp_payload);
|
||||
Long cartId = null;
|
||||
Long dirFactId = null;
|
||||
if (node.has("Ds_MerchantData")) {
|
||||
// format: "Ds_MerchantData": "{"dirFactId":3,"cartId":90}"
|
||||
String merchantData = node.get("Ds_MerchantData").asText();
|
||||
merchantData = merchantData.replace(""", "\"");
|
||||
var mdNode = om.readTree(merchantData);
|
||||
if (mdNode.has("cartId")) {
|
||||
cartId = mdNode.get("cartId").asLong();
|
||||
}
|
||||
if (mdNode.has("dirFactId")) {
|
||||
dirFactId = mdNode.get("dirFactId").asLong();
|
||||
}
|
||||
}
|
||||
return Map.of(
|
||||
"cartId", cartId,
|
||||
"dirFactId", dirFactId);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea el Payment en BD y construye el formulario de Redsys usando la API
|
||||
* oficial (ApiMacSha256).
|
||||
*/
|
||||
@Transactional
|
||||
public FormPayload createRedsysPayment(Long cartId, Long dirFactId, Long amountCents, String currency, String method, Long orderId)
|
||||
throws Exception {
|
||||
Payment p = new Payment();
|
||||
p.setOrderId(orderId);
|
||||
|
||||
Cart cart = this.cartService.findById(cartId);
|
||||
if (cart != null && cart.getUserId() != null) {
|
||||
p.setUserId(cart.getUserId());
|
||||
this.cartService.lockCartById(cartId);
|
||||
}
|
||||
p.setCurrency(currency);
|
||||
p.setAmountTotalCents(amountCents);
|
||||
p.setGateway("redsys");
|
||||
p.setStatus(PaymentStatus.requires_payment_method);
|
||||
p = payRepo.saveAndFlush(p);
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
String dsOrder = String.format("%012d", now % 1_000_000_000_000L);
|
||||
|
||||
p.setGatewayOrderId(dsOrder);
|
||||
payRepo.save(p);
|
||||
|
||||
RedsysService.PaymentRequest req = new RedsysService.PaymentRequest(dsOrder, amountCents,
|
||||
"Compra en Imprimelibros", cartId, dirFactId);
|
||||
|
||||
if ("bizum".equalsIgnoreCase(method)) {
|
||||
return redsysService.buildRedirectFormBizum(req);
|
||||
} else {
|
||||
return redsysService.buildRedirectForm(req);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void handleRedsysNotification(String dsSignature, String dsMerchantParameters, Locale locale)
|
||||
throws Exception {
|
||||
|
||||
// 0) Intentamos parsear la notificación. Si falla, registramos el webhook crudo
|
||||
// y salimos.
|
||||
RedsysNotification notif;
|
||||
try {
|
||||
notif = redsysService.validateAndParseNotification(dsSignature, dsMerchantParameters);
|
||||
} catch (Exception ex) {
|
||||
WebhookEvent e = new WebhookEvent();
|
||||
e.setProvider("redsys");
|
||||
e.setEventType("payment_notification_parse_error");
|
||||
e.setEventId("PARSE_ERROR_" + System.currentTimeMillis());
|
||||
e.setSignature(dsSignature);
|
||||
e.setPayload(dsMerchantParameters);
|
||||
e.setProcessed(false);
|
||||
e.setAttempts(1);
|
||||
e.setLastError("Error parsing/validating Redsys notification: " + ex.getMessage());
|
||||
webhookEventRepo.save(e);
|
||||
|
||||
// IMPORTANTE: NO re-lanzamos la excepción
|
||||
// Simplemente salimos. Así se hace commit de este insert.
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) A partir de aquí, el parseo ha ido bien y tenemos notif.order,
|
||||
// notif.amountCents, etc.
|
||||
String provider = "redsys";
|
||||
String eventType = "payment_notification";
|
||||
String eventId = notif.order;
|
||||
|
||||
WebhookEvent ev = webhookEventRepo
|
||||
.findByProviderAndEventId(provider, eventId)
|
||||
.orElseGet(() -> {
|
||||
WebhookEvent e = new WebhookEvent();
|
||||
e.setProvider(provider);
|
||||
e.setEventType(eventType);
|
||||
e.setEventId(eventId);
|
||||
e.setSignature(dsSignature);
|
||||
try {
|
||||
e.setPayload(om.writeValueAsString(notif.raw));
|
||||
} catch (Exception ex) {
|
||||
e.setPayload(dsMerchantParameters);
|
||||
}
|
||||
e.setProcessed(false);
|
||||
e.setAttempts(0);
|
||||
return webhookEventRepo.save(e);
|
||||
});
|
||||
|
||||
if (Boolean.TRUE.equals(ev.getProcessed())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Integer attempts = ev.getAttempts() == null ? 0 : ev.getAttempts();
|
||||
ev.setAttempts(attempts + 1);
|
||||
ev.setLastError(null);
|
||||
webhookEventRepo.save(ev);
|
||||
|
||||
try {
|
||||
Payment p = payRepo.findByGatewayAndGatewayOrderId("redsys", notif.order)
|
||||
.orElseThrow(() -> new IllegalStateException("Payment no encontrado para Ds_Order " + notif.order));
|
||||
|
||||
if (!Objects.equals(p.getAmountTotalCents(), notif.amountCents)) {
|
||||
throw new IllegalStateException("Importe inesperado: esperado=" +
|
||||
p.getAmountTotalCents() + " recibido=" + notif.amountCents);
|
||||
}
|
||||
|
||||
if (p.getStatus() == PaymentStatus.captured
|
||||
|| p.getStatus() == PaymentStatus.partially_refunded
|
||||
|| p.getStatus() == PaymentStatus.refunded) {
|
||||
ev.setProcessed(true);
|
||||
ev.setProcessedAt(LocalDateTime.now());
|
||||
webhookEventRepo.save(ev);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean authorized = isRedsysAuthorized(notif);
|
||||
|
||||
PaymentTransaction tx = new PaymentTransaction();
|
||||
tx.setPayment(p);
|
||||
tx.setType(PaymentTransactionType.CAPTURE);
|
||||
tx.setCurrency(p.getCurrency()); // "EUR"
|
||||
tx.setAmountCents(notif.amountCents);
|
||||
tx.setStatus(authorized
|
||||
? PaymentTransactionStatus.succeeded
|
||||
: PaymentTransactionStatus.failed);
|
||||
|
||||
String gatewayTxId = null;
|
||||
// 1) Si es Bizum y tenemos Ds_Bizum_IdOper, úsalo como ID único
|
||||
if (notif.isBizum()
|
||||
&& notif.bizumIdOper != null
|
||||
&& !notif.bizumIdOper.isBlank()) {
|
||||
|
||||
gatewayTxId = notif.bizumIdOper.trim();
|
||||
|
||||
// 2) Si no es Bizum, intenta usar Ds_AuthorisationCode
|
||||
} else if (notif.authorisationCode != null) {
|
||||
String trimmed = notif.authorisationCode.trim();
|
||||
|
||||
// Redsys suele mandar "000000" para Bizum; por si acaso también lo filtramos
|
||||
if (!trimmed.isEmpty() && !"000000".equals(trimmed)) {
|
||||
gatewayTxId = trimmed;
|
||||
}
|
||||
}
|
||||
// MySQL permite múltiples NULL en un índice UNIQUE, así que es seguro.
|
||||
tx.setGatewayTransactionId(gatewayTxId);
|
||||
tx.setGatewayResponseCode(notif.response);
|
||||
tx.setResponsePayload(om.writeValueAsString(notif.raw));
|
||||
tx.setProcessedAt(LocalDateTime.now());
|
||||
txRepo.save(tx);
|
||||
|
||||
if (authorized) {
|
||||
if (notif.isBizum()) {
|
||||
p.setAuthorizationCode(null); // o "000000" si te interesa mostrarlo
|
||||
} else if (notif.authorisationCode != null
|
||||
&& !"000000".equals(notif.authorisationCode.trim())
|
||||
&& !notif.authorisationCode.isBlank()) {
|
||||
p.setAuthorizationCode(notif.authorisationCode.trim());
|
||||
}
|
||||
|
||||
p.setStatus(PaymentStatus.captured);
|
||||
p.setAmountCapturedCents(p.getAmountCapturedCents() + notif.amountCents);
|
||||
p.setAuthorizedAt(LocalDateTime.now());
|
||||
p.setCapturedAt(LocalDateTime.now());
|
||||
pedidoService.setOrderAsPaid(p.getOrderId());
|
||||
|
||||
Pedido pedido = pedidoService.getPedidoById(p.getOrderId());
|
||||
SerieFactura serie = facturacionService.getDefaultSerieFactura();
|
||||
|
||||
facturacionService.crearNuevaFacturaAuto(pedido, serie, notif.isBizum() ? TipoPago.tpv_bizum : TipoPago.tpv_tarjeta, locale);
|
||||
|
||||
} else {
|
||||
p.setStatus(PaymentStatus.failed);
|
||||
p.setFailedAt(LocalDateTime.now());
|
||||
pedidoService.markPedidoAsPaymentDenied(p.getOrderId());
|
||||
}
|
||||
|
||||
payRepo.save(p);
|
||||
|
||||
if (!authorized) {
|
||||
ev.setLastError("Payment declined (Ds_Response=" + notif.response + ")");
|
||||
}
|
||||
|
||||
ev.setProcessed(true);
|
||||
ev.setProcessedAt(LocalDateTime.now());
|
||||
webhookEventRepo.save(ev);
|
||||
} catch (Exception e) {
|
||||
ev.setProcessed(false);
|
||||
ev.setLastError(e.getMessage());
|
||||
ev.setProcessedAt(null);
|
||||
webhookEventRepo.save(ev);
|
||||
throw e; // aquí sí, porque queremos que si falla lógica de negocio el caller se entere
|
||||
}
|
||||
}
|
||||
|
||||
// ---- refundViaRedsys
|
||||
// ----
|
||||
@Transactional
|
||||
public void refundViaRedsys(Long paymentId, long amountCents, String idempotencyKey) {
|
||||
Payment p = payRepo.findById(paymentId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Payment no encontrado"));
|
||||
|
||||
if (amountCents <= 0)
|
||||
throw new IllegalArgumentException("Importe inválido");
|
||||
|
||||
long maxRefundable = p.getAmountCapturedCents() - p.getAmountRefundedCents();
|
||||
if (amountCents > maxRefundable)
|
||||
throw new IllegalStateException("Importe de devolución supera lo capturado");
|
||||
|
||||
txRepo.findByIdempotencyKey(idempotencyKey)
|
||||
.ifPresent(t -> {
|
||||
throw new IllegalStateException("Reembolso ya procesado");
|
||||
});
|
||||
|
||||
Refund r = new Refund();
|
||||
r.setPayment(p);
|
||||
r.setAmountCents(amountCents);
|
||||
r.setStatus(RefundStatus.pending);
|
||||
r.setRequestedAt(LocalDateTime.now());
|
||||
r = refundRepo.save(r);
|
||||
|
||||
String gatewayRefundId;
|
||||
try {
|
||||
// ⚠️ Usa aquí el mismo valor que mandaste en Ds_Merchant_Order al cobrar
|
||||
// por ejemplo, p.getGatewayOrderId() o similar
|
||||
String originalOrder = p.getGatewayOrderId(); // ajusta al nombre real del campo
|
||||
gatewayRefundId = redsysService.requestRefund(originalOrder, amountCents);
|
||||
} catch (Exception e) {
|
||||
r.setStatus(RefundStatus.failed);
|
||||
r.setProcessedAt(LocalDateTime.now());
|
||||
refundRepo.save(r);
|
||||
throw new IllegalStateException("Error al solicitar la devolución a Redsys", e);
|
||||
}
|
||||
|
||||
// 🔧 NORMALIZAR ANTES DE GUARDAR
|
||||
if (gatewayRefundId != null) {
|
||||
gatewayRefundId = gatewayRefundId.trim();
|
||||
if (gatewayRefundId.isEmpty() || "000000".equals(gatewayRefundId)) {
|
||||
gatewayRefundId = null; // → múltiples NULL NO rompen el UNIQUE
|
||||
}
|
||||
}
|
||||
|
||||
PaymentTransaction tx = new PaymentTransaction();
|
||||
tx.setPayment(p);
|
||||
tx.setType(PaymentTransactionType.REFUND);
|
||||
tx.setStatus(PaymentTransactionStatus.succeeded);
|
||||
tx.setAmountCents(amountCents);
|
||||
tx.setCurrency(p.getCurrency());
|
||||
tx.setGatewayTransactionId(gatewayRefundId);
|
||||
tx.setIdempotencyKey(idempotencyKey);
|
||||
tx.setProcessedAt(LocalDateTime.now());
|
||||
txRepo.save(tx);
|
||||
|
||||
r.setStatus(RefundStatus.succeeded);
|
||||
r.setTransaction(tx);
|
||||
r.setGatewayRefundId(gatewayRefundId);
|
||||
r.setProcessedAt(LocalDateTime.now());
|
||||
refundRepo.save(r);
|
||||
|
||||
p.setAmountRefundedCents(p.getAmountRefundedCents() + amountCents);
|
||||
if (p.getAmountRefundedCents().equals(p.getAmountCapturedCents())) {
|
||||
p.setStatus(PaymentStatus.refunded);
|
||||
} else {
|
||||
p.setStatus(PaymentStatus.partially_refunded);
|
||||
}
|
||||
payRepo.save(p);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Payment createBankTransferPayment(Long cartId, Long dirFactId, long amountCents, String currency, Locale locale, Long orderId) {
|
||||
Payment p = new Payment();
|
||||
p.setOrderId(null);
|
||||
|
||||
Cart cart = this.cartService.findById(cartId);
|
||||
if (cart != null && cart.getUserId() != null) {
|
||||
p.setUserId(cart.getUserId());
|
||||
// Se bloquea el carrito para evitar modificaciones mientras se procesa el pago
|
||||
this.cartService.lockCartById(cartId);
|
||||
}
|
||||
|
||||
p.setCurrency(currency);
|
||||
p.setAmountTotalCents(amountCents);
|
||||
p.setGateway("bank_transfer");
|
||||
p.setStatus(PaymentStatus.requires_action); // pendiente de ingreso
|
||||
if (orderId != null) {
|
||||
p.setOrderId(orderId);
|
||||
}
|
||||
p = payRepo.save(p);
|
||||
|
||||
// Crear transacción pendiente
|
||||
PaymentTransaction tx = new PaymentTransaction();
|
||||
tx.setPayment(p);
|
||||
tx.setType(PaymentTransactionType.CAPTURE); // o AUTH si prefieres
|
||||
tx.setStatus(PaymentTransactionStatus.pending);
|
||||
tx.setAmountCents(amountCents);
|
||||
tx.setCurrency(currency);
|
||||
String payload = "";
|
||||
if (cartId != null) {
|
||||
payload = "{\"cartId\":" + cartId + "}";
|
||||
}
|
||||
if (dirFactId != null) {
|
||||
if (!payload.isEmpty()) {
|
||||
payload = payload.substring(0, payload.length() - 1) + ",\"dirFactId\":" + dirFactId + "}";
|
||||
} else {
|
||||
payload = "{\"dirFactId\":" + dirFactId + "}";
|
||||
}
|
||||
}
|
||||
tx.setResponsePayload(payload);
|
||||
// tx.setProcessedAt(null); // la dejas nula hasta que se confirme
|
||||
txRepo.save(tx);
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void markBankTransferAsCaptured(Long paymentId, Locale locale) {
|
||||
Payment p = payRepo.findById(paymentId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Payment no encontrado: " + paymentId));
|
||||
|
||||
if (!"bank_transfer".equals(p.getGateway())) {
|
||||
throw new IllegalStateException("El Payment " + paymentId + " no es de tipo bank_transfer");
|
||||
}
|
||||
|
||||
// Idempotencia simple: si ya está capturado no hacemos nada
|
||||
if (p.getStatus() == PaymentStatus.captured
|
||||
|| p.getStatus() == PaymentStatus.partially_refunded
|
||||
|| p.getStatus() == PaymentStatus.refunded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) Buscar la transacción pendiente de captura
|
||||
PaymentTransaction tx = txRepo
|
||||
.findFirstByPaymentIdAndTypeAndStatusOrderByIdDesc(
|
||||
paymentId,
|
||||
PaymentTransactionType.CAPTURE,
|
||||
PaymentTransactionStatus.pending)
|
||||
.orElseThrow(() -> new IllegalStateException(
|
||||
"No se ha encontrado transacción PENDING para la transferencia " + paymentId));
|
||||
|
||||
// 2) Actualizarla a SUCCEEDED y rellenar processedAt
|
||||
tx.setStatus(PaymentTransactionStatus.succeeded);
|
||||
tx.setProcessedAt(LocalDateTime.now());
|
||||
txRepo.save(tx);
|
||||
|
||||
// 3) Actualizar el Payment
|
||||
p.setAmountCapturedCents(p.getAmountTotalCents());
|
||||
p.setCapturedAt(LocalDateTime.now());
|
||||
p.setStatus(PaymentStatus.captured);
|
||||
|
||||
Long cartId = null;
|
||||
Long dirFactId = null;
|
||||
try {
|
||||
// Intentar extraer cartId del payload de la transacción
|
||||
if (tx.getResponsePayload() != null && !tx.getResponsePayload().isBlank()) {
|
||||
ObjectMapper om = new ObjectMapper();
|
||||
var node = om.readTree(tx.getResponsePayload());
|
||||
if (node.has("cartId")) {
|
||||
cartId = node.get("cartId").asLong();
|
||||
}
|
||||
if (node.has("dirFactId")) {
|
||||
dirFactId = node.get("dirFactId").asLong();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// ignorar
|
||||
}
|
||||
|
||||
// 4) Procesar el pedido asociado al carrito (si existe) o marcar el pedido como pagado
|
||||
if(p.getOrderId() != null) {
|
||||
pedidoService.setOrderAsPaid(p.getOrderId());
|
||||
|
||||
Pedido pedido = pedidoService.getPedidoById(p.getOrderId());
|
||||
SerieFactura serie = facturacionService.getDefaultSerieFactura();
|
||||
|
||||
facturacionService.crearNuevaFacturaAuto(pedido, serie, TipoPago.transferencia, locale);
|
||||
}
|
||||
/*else if (cartId != null) {
|
||||
// Se procesa el pedido dejando el estado calculado en processOrder
|
||||
Long orderId = processOrder(cartId, dirFactId, locale, null);
|
||||
if (orderId != null) {
|
||||
p.setOrderId(orderId);
|
||||
}
|
||||
}*/
|
||||
payRepo.save(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve (total o parcialmente) un pago hecho por transferencia bancaria.
|
||||
* - Solo permite gateway = "bank_transfer".
|
||||
* - Crea un Refund + PaymentTransaction de tipo REFUND.
|
||||
* - Actualiza amountRefundedCents y el estado del Payment.
|
||||
*/
|
||||
@Transactional
|
||||
public Refund refundBankTransfer(Long paymentId, long amountCents) {
|
||||
Payment p = payRepo.findById(paymentId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Payment no encontrado: " + paymentId));
|
||||
|
||||
if (!"bank_transfer".equals(p.getGateway())) {
|
||||
throw new IllegalStateException("El Payment " + paymentId + " no es de tipo bank_transfer");
|
||||
}
|
||||
|
||||
if (amountCents <= 0) {
|
||||
throw new IllegalArgumentException("El importe de devolución debe ser > 0");
|
||||
}
|
||||
|
||||
// Solo tiene sentido devolver si está capturado o ya parcialmente devuelto
|
||||
if (p.getStatus() != PaymentStatus.captured
|
||||
&& p.getStatus() != PaymentStatus.partially_refunded) {
|
||||
throw new IllegalStateException(
|
||||
"El Payment " + paymentId + " no está capturado; estado actual: " + p.getStatus());
|
||||
}
|
||||
|
||||
long maxRefundable = p.getAmountCapturedCents() - p.getAmountRefundedCents();
|
||||
if (amountCents > maxRefundable) {
|
||||
throw new IllegalStateException(
|
||||
"Importe de devolución supera lo todavía reembolsable. " +
|
||||
"maxRefundable=" + maxRefundable + " requested=" + amountCents);
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// 1) Crear Refund (para transferencias lo marcamos como SUCCEEDED directamente)
|
||||
Refund refund = new Refund();
|
||||
refund.setPayment(p);
|
||||
refund.setAmountCents(amountCents);
|
||||
// reason usa el valor por defecto (customer_request); si quieres otro, cámbialo
|
||||
// aquí
|
||||
refund.setStatus(RefundStatus.succeeded);
|
||||
refund.setRequestedAt(now);
|
||||
refund.setProcessedAt(now);
|
||||
// requestedByUserId, notes, metadata -> opcionales, déjalos en null si no los
|
||||
// usas
|
||||
refund = refundRepo.save(refund);
|
||||
|
||||
// 2) Crear transacción de tipo REFUND
|
||||
PaymentTransaction tx = new PaymentTransaction();
|
||||
tx.setPayment(p);
|
||||
tx.setType(PaymentTransactionType.REFUND);
|
||||
tx.setStatus(PaymentTransactionStatus.succeeded);
|
||||
tx.setAmountCents(amountCents);
|
||||
tx.setCurrency(p.getCurrency());
|
||||
tx.setProcessedAt(now);
|
||||
// gatewayTransactionId lo dejamos null → el índice UNIQUE permite múltiples
|
||||
// NULL
|
||||
tx = txRepo.save(tx);
|
||||
|
||||
// Vincular el Refund con la transacción
|
||||
refund.setTransaction(tx);
|
||||
refundRepo.save(refund);
|
||||
|
||||
// 3) Actualizar Payment: total devuelto y estado
|
||||
p.setAmountRefundedCents(p.getAmountRefundedCents() + amountCents);
|
||||
|
||||
if (p.getAmountRefundedCents().equals(p.getAmountCapturedCents())) {
|
||||
p.setStatus(PaymentStatus.refunded);
|
||||
} else {
|
||||
p.setStatus(PaymentStatus.partially_refunded);
|
||||
}
|
||||
|
||||
payRepo.save(p);
|
||||
|
||||
return refund;
|
||||
}
|
||||
|
||||
private boolean isRedsysAuthorized(RedsysService.RedsysNotification notif) {
|
||||
if (notif.response == null) {
|
||||
return false;
|
||||
}
|
||||
String r = notif.response.trim();
|
||||
// Si no es numérico, lo tratamos como no autorizado
|
||||
if (!r.matches("\\d+")) {
|
||||
return false;
|
||||
}
|
||||
int code = Integer.parseInt(r);
|
||||
// Redsys: 0–99 → autorizado; >=100 → denegado / error
|
||||
return code >= 0 && code <= 99;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
package com.imprimelibros.erp.payments.model;
|
||||
|
||||
|
||||
public enum CaptureMethod { automatic, manual }
|
||||
|
||||
173
src/main/java/com/imprimelibros/erp/payments/model/Payment.java
Normal file
173
src/main/java/com/imprimelibros/erp/payments/model/Payment.java
Normal file
@ -0,0 +1,173 @@
|
||||
package com.imprimelibros.erp.payments.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "payments")
|
||||
public class Payment {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "order_id")
|
||||
private Long orderId;
|
||||
|
||||
@Column(name = "user_id")
|
||||
private Long userId;
|
||||
|
||||
@Column(nullable = false, length = 3)
|
||||
private String currency;
|
||||
|
||||
@Column(name = "amount_total_cents", nullable = false)
|
||||
private Long amountTotalCents;
|
||||
|
||||
@Column(name = "amount_captured_cents", nullable = false)
|
||||
private Long amountCapturedCents = 0L;
|
||||
|
||||
@Column(name = "amount_refunded_cents", nullable = false)
|
||||
private Long amountRefundedCents = 0L;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 32)
|
||||
private PaymentStatus status = PaymentStatus.requires_payment_method;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "capture_method", nullable = false, length = 16)
|
||||
private CaptureMethod captureMethod = CaptureMethod.automatic;
|
||||
|
||||
@Column(nullable = false, length = 32)
|
||||
private String gateway;
|
||||
|
||||
@Column(name = "gateway_payment_id", length = 128)
|
||||
private String gatewayPaymentId;
|
||||
|
||||
@Column(name = "gateway_order_id", length = 12)
|
||||
private String gatewayOrderId;
|
||||
|
||||
@Column(name = "authorization_code", length = 32)
|
||||
private String authorizationCode;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "three_ds_status", nullable = false, length = 32)
|
||||
private ThreeDSStatus threeDsStatus = ThreeDSStatus.not_applicable;
|
||||
|
||||
@Column(length = 22)
|
||||
private String descriptor;
|
||||
|
||||
@Lob
|
||||
@Column(name = "client_ip", columnDefinition = "varbinary(16)")
|
||||
private byte[] clientIp;
|
||||
|
||||
@Column(name = "authorized_at")
|
||||
private LocalDateTime authorizedAt;
|
||||
|
||||
@Column(name = "captured_at")
|
||||
private LocalDateTime capturedAt;
|
||||
|
||||
@Column(name = "canceled_at")
|
||||
private LocalDateTime canceledAt;
|
||||
|
||||
@Column(name = "failed_at")
|
||||
private LocalDateTime failedAt;
|
||||
|
||||
@Column(columnDefinition = "json")
|
||||
private String metadata;
|
||||
|
||||
@Column(name = "created_at", nullable = false,
|
||||
columnDefinition = "datetime default current_timestamp")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at", nullable = false,
|
||||
columnDefinition = "datetime default current_timestamp on update current_timestamp")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public Payment() {}
|
||||
|
||||
// Getters y setters ↓ (los típicos)
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Long getOrderId() { return orderId; }
|
||||
public void setOrderId(Long orderId) { this.orderId = orderId; }
|
||||
|
||||
public Long getUserId() { return userId; }
|
||||
public void setUserId(Long userId) { this.userId = userId; }
|
||||
|
||||
public String getCurrency() { return currency; }
|
||||
public void setCurrency(String currency) { this.currency = currency; }
|
||||
|
||||
public Long getAmountTotalCents() { return amountTotalCents; }
|
||||
public void setAmountTotalCents(Long amountTotalCents) { this.amountTotalCents = amountTotalCents; }
|
||||
|
||||
public Long getAmountCapturedCents() { return amountCapturedCents; }
|
||||
public void setAmountCapturedCents(Long amountCapturedCents) { this.amountCapturedCents = amountCapturedCents; }
|
||||
|
||||
public Long getAmountRefundedCents() { return amountRefundedCents; }
|
||||
public void setAmountRefundedCents(Long amountRefundedCents) { this.amountRefundedCents = amountRefundedCents; }
|
||||
|
||||
public PaymentStatus getStatus() { return status; }
|
||||
public void setStatus(PaymentStatus status) { this.status = status; }
|
||||
|
||||
public CaptureMethod getCaptureMethod() { return captureMethod; }
|
||||
public void setCaptureMethod(CaptureMethod captureMethod) { this.captureMethod = captureMethod; }
|
||||
|
||||
public String getGateway() { return gateway; }
|
||||
public void setGateway(String gateway) { this.gateway = gateway; }
|
||||
|
||||
public String getGatewayPaymentId() { return gatewayPaymentId; }
|
||||
public void setGatewayPaymentId(String gatewayPaymentId) { this.gatewayPaymentId = gatewayPaymentId; }
|
||||
|
||||
public String getGatewayOrderId() { return gatewayOrderId; }
|
||||
public void setGatewayOrderId(String gatewayOrderId) { this.gatewayOrderId = gatewayOrderId; }
|
||||
|
||||
public String getAuthorizationCode() { return authorizationCode; }
|
||||
public void setAuthorizationCode(String authorizationCode) { this.authorizationCode = authorizationCode; }
|
||||
|
||||
public ThreeDSStatus getThreeDsStatus() { return threeDsStatus; }
|
||||
public void setThreeDsStatus(ThreeDSStatus threeDsStatus) { this.threeDsStatus = threeDsStatus; }
|
||||
|
||||
public String getDescriptor() { return descriptor; }
|
||||
public void setDescriptor(String descriptor) { this.descriptor = descriptor; }
|
||||
|
||||
public byte[] getClientIp() { return clientIp; }
|
||||
public void setClientIp(byte[] clientIp) { this.clientIp = clientIp; }
|
||||
|
||||
public LocalDateTime getAuthorizedAt() { return authorizedAt; }
|
||||
public void setAuthorizedAt(LocalDateTime authorizedAt) { this.authorizedAt = authorizedAt; }
|
||||
|
||||
public LocalDateTime getCapturedAt() { return capturedAt; }
|
||||
public void setCapturedAt(LocalDateTime capturedAt) { this.capturedAt = capturedAt; }
|
||||
|
||||
public LocalDateTime getCanceledAt() { return canceledAt; }
|
||||
public void setCanceledAt(LocalDateTime canceledAt) { this.canceledAt = canceledAt; }
|
||||
|
||||
public LocalDateTime getFailedAt() { return failedAt; }
|
||||
public void setFailedAt(LocalDateTime failedAt) { this.failedAt = failedAt; }
|
||||
|
||||
public String getMetadata() { return metadata; }
|
||||
public void setMetadata(String metadata) { this.metadata = metadata; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
|
||||
@PrePersist
|
||||
public void prePersist() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (createdAt == null) {
|
||||
createdAt = now;
|
||||
}
|
||||
if (updatedAt == null) {
|
||||
updatedAt = now;
|
||||
}
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
public void preUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package com.imprimelibros.erp.payments.model;
|
||||
|
||||
public enum PaymentStatus {
|
||||
requires_payment_method, requires_action, authorized,
|
||||
captured, partially_refunded, refunded, canceled, failed
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,129 @@
|
||||
package com.imprimelibros.erp.payments.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "payment_transactions",
|
||||
indexes = {
|
||||
@Index(name = "idx_tx_pay", columnList = "payment_id"),
|
||||
@Index(name = "idx_tx_type_status", columnList = "type,status"),
|
||||
@Index(name = "idx_tx_idem", columnList = "idempotency_key")
|
||||
}
|
||||
)
|
||||
public class PaymentTransaction {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "payment_id", nullable = false)
|
||||
private Payment payment;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "type", nullable = false, length = 16)
|
||||
private PaymentTransactionType type;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false, length = 16)
|
||||
private PaymentTransactionStatus status;
|
||||
|
||||
@Column(name = "amount_cents", nullable = false)
|
||||
private Long amountCents;
|
||||
|
||||
@Column(name = "currency", nullable = false, length = 3)
|
||||
private String currency;
|
||||
|
||||
@Column(name = "gateway_transaction_id", length = 128)
|
||||
private String gatewayTransactionId;
|
||||
|
||||
@Column(name = "gateway_response_code", length = 64)
|
||||
private String gatewayResponseCode;
|
||||
|
||||
@Column(name = "avs_result", length = 8)
|
||||
private String avsResult;
|
||||
|
||||
@Column(name = "cvv_result", length = 8)
|
||||
private String cvvResult;
|
||||
|
||||
@Column(name = "three_ds_version", length = 16)
|
||||
private String threeDsVersion;
|
||||
|
||||
@Column(name = "idempotency_key", length = 128)
|
||||
private String idempotencyKey;
|
||||
|
||||
@Column(name = "request_payload", columnDefinition = "json")
|
||||
private String requestPayload;
|
||||
|
||||
@Column(name = "response_payload", columnDefinition = "json")
|
||||
private String responsePayload;
|
||||
|
||||
@Column(name = "processed_at")
|
||||
private LocalDateTime processedAt;
|
||||
|
||||
@Column(name = "created_at", nullable = false,
|
||||
columnDefinition = "datetime default current_timestamp")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public PaymentTransaction() {}
|
||||
|
||||
// Getters & Setters
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Payment getPayment() { return payment; }
|
||||
public void setPayment(Payment payment) { this.payment = payment; }
|
||||
|
||||
public PaymentTransactionType getType() { return type; }
|
||||
public void setType(PaymentTransactionType type) { this.type = type; }
|
||||
|
||||
public PaymentTransactionStatus getStatus() { return status; }
|
||||
public void setStatus(PaymentTransactionStatus status) { this.status = status; }
|
||||
|
||||
public Long getAmountCents() { return amountCents; }
|
||||
public void setAmountCents(Long amountCents) { this.amountCents = amountCents; }
|
||||
|
||||
public String getCurrency() { return currency; }
|
||||
public void setCurrency(String currency) { this.currency = currency; }
|
||||
|
||||
public String getGatewayTransactionId() { return gatewayTransactionId; }
|
||||
public void setGatewayTransactionId(String gatewayTransactionId) { this.gatewayTransactionId = gatewayTransactionId; }
|
||||
|
||||
public String getGatewayResponseCode() { return gatewayResponseCode; }
|
||||
public void setGatewayResponseCode(String gatewayResponseCode) { this.gatewayResponseCode = gatewayResponseCode; }
|
||||
|
||||
public String getAvsResult() { return avsResult; }
|
||||
public void setAvsResult(String avsResult) { this.avsResult = avsResult; }
|
||||
|
||||
public String getCvvResult() { return cvvResult; }
|
||||
public void setCvvResult(String cvvResult) { this.cvvResult = cvvResult; }
|
||||
|
||||
public String getThreeDsVersion() { return threeDsVersion; }
|
||||
public void setThreeDsVersion(String threeDsVersion) { this.threeDsVersion = threeDsVersion; }
|
||||
|
||||
public String getIdempotencyKey() { return idempotencyKey; }
|
||||
public void setIdempotencyKey(String idempotencyKey) { this.idempotencyKey = idempotencyKey; }
|
||||
|
||||
public String getRequestPayload() { return requestPayload; }
|
||||
public void setRequestPayload(String requestPayload) { this.requestPayload = requestPayload; }
|
||||
|
||||
public String getResponsePayload() { return responsePayload; }
|
||||
public void setResponsePayload(String responsePayload) { this.responsePayload = responsePayload; }
|
||||
|
||||
public LocalDateTime getProcessedAt() { return processedAt; }
|
||||
public void setProcessedAt(LocalDateTime processedAt) { this.processedAt = processedAt; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
@PrePersist
|
||||
public void prePersist() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (createdAt == null) {
|
||||
createdAt = now;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
package com.imprimelibros.erp.payments.model;
|
||||
|
||||
public enum PaymentTransactionStatus { pending, succeeded, failed }
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
package com.imprimelibros.erp.payments.model;
|
||||
|
||||
public enum PaymentTransactionType { AUTH, CAPTURE, REFUND, VOID }
|
||||
|
||||
@ -0,0 +1,99 @@
|
||||
package com.imprimelibros.erp.payments.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "refunds",
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uq_refund_gateway_id", columnNames = {"gateway_refund_id"})
|
||||
},
|
||||
indexes = {
|
||||
@Index(name = "idx_ref_pay", columnList = "payment_id"),
|
||||
@Index(name = "idx_ref_status", columnList = "status")
|
||||
}
|
||||
)
|
||||
public class Refund {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "payment_id", nullable = false)
|
||||
private Payment payment;
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "transaction_id")
|
||||
private PaymentTransaction transaction; // el REFUND en payment_transactions
|
||||
|
||||
@Column(name = "amount_cents", nullable = false)
|
||||
private Long amountCents;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "reason", nullable = false, length = 32)
|
||||
private RefundReason reason = RefundReason.customer_request;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false, length = 16)
|
||||
private RefundStatus status = RefundStatus.pending;
|
||||
|
||||
@Column(name = "requested_by_user_id")
|
||||
private Long requestedByUserId;
|
||||
|
||||
@Column(name = "requested_at", nullable = false,
|
||||
columnDefinition = "datetime default current_timestamp")
|
||||
private LocalDateTime requestedAt;
|
||||
|
||||
@Column(name = "processed_at")
|
||||
private LocalDateTime processedAt;
|
||||
|
||||
@Column(name = "gateway_refund_id", length = 128)
|
||||
private String gatewayRefundId;
|
||||
|
||||
@Column(name = "notes", length = 500)
|
||||
private String notes;
|
||||
|
||||
@Column(name = "metadata", columnDefinition = "json")
|
||||
private String metadata;
|
||||
|
||||
public Refund() {}
|
||||
|
||||
// Getters & Setters
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public Payment getPayment() { return payment; }
|
||||
public void setPayment(Payment payment) { this.payment = payment; }
|
||||
|
||||
public PaymentTransaction getTransaction() { return transaction; }
|
||||
public void setTransaction(PaymentTransaction transaction) { this.transaction = transaction; }
|
||||
|
||||
public Long getAmountCents() { return amountCents; }
|
||||
public void setAmountCents(Long amountCents) { this.amountCents = amountCents; }
|
||||
|
||||
public RefundReason getReason() { return reason; }
|
||||
public void setReason(RefundReason reason) { this.reason = reason; }
|
||||
|
||||
public RefundStatus getStatus() { return status; }
|
||||
public void setStatus(RefundStatus status) { this.status = status; }
|
||||
|
||||
public Long getRequestedByUserId() { return requestedByUserId; }
|
||||
public void setRequestedByUserId(Long requestedByUserId) { this.requestedByUserId = requestedByUserId; }
|
||||
|
||||
public LocalDateTime getRequestedAt() { return requestedAt; }
|
||||
public void setRequestedAt(LocalDateTime requestedAt) { this.requestedAt = requestedAt; }
|
||||
|
||||
public LocalDateTime getProcessedAt() { return processedAt; }
|
||||
public void setProcessedAt(LocalDateTime processedAt) { this.processedAt = processedAt; }
|
||||
|
||||
public String getGatewayRefundId() { return gatewayRefundId; }
|
||||
public void setGatewayRefundId(String gatewayRefundId) { this.gatewayRefundId = gatewayRefundId; }
|
||||
|
||||
public String getNotes() { return notes; }
|
||||
public void setNotes(String notes) { this.notes = notes; }
|
||||
|
||||
public String getMetadata() { return metadata; }
|
||||
public void setMetadata(String metadata) { this.metadata = metadata; }
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package com.imprimelibros.erp.payments.model;
|
||||
|
||||
public enum RefundReason {
|
||||
customer_request, partial_return, pricing_adjustment, duplicate, fraud, other
|
||||
}
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
package com.imprimelibros.erp.payments.model;
|
||||
|
||||
public enum RefundStatus { pending, succeeded, failed, canceled }
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
package com.imprimelibros.erp.payments.model;
|
||||
|
||||
public enum ThreeDSStatus { not_applicable, attempted, challenge, succeeded, failed }
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
package com.imprimelibros.erp.payments.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "webhook_events",
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uq_webhook_provider_event", columnNames = {"provider","event_id"})
|
||||
},
|
||||
indexes = {
|
||||
@Index(name = "idx_webhook_processed", columnList = "processed")
|
||||
}
|
||||
)
|
||||
public class WebhookEvent {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "provider", nullable = false, length = 32)
|
||||
private String provider; // "redsys", etc.
|
||||
|
||||
@Column(name = "event_type", nullable = false, length = 64)
|
||||
private String eventType;
|
||||
|
||||
@Column(name = "event_id", length = 128)
|
||||
private String eventId;
|
||||
|
||||
@Column(name = "signature", length = 512)
|
||||
private String signature;
|
||||
|
||||
@Column(name = "payload", nullable = false, columnDefinition = "json")
|
||||
private String payload;
|
||||
|
||||
@Column(name = "processed", nullable = false)
|
||||
private Boolean processed = false;
|
||||
|
||||
@Column(name = "processed_at")
|
||||
private LocalDateTime processedAt;
|
||||
|
||||
@Column(name = "attempts", nullable = false)
|
||||
private Integer attempts = 0;
|
||||
|
||||
@Column(name = "last_error", length = 500)
|
||||
private String lastError;
|
||||
|
||||
@Column(name = "created_at", nullable = false,
|
||||
columnDefinition = "datetime default current_timestamp")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public WebhookEvent() {}
|
||||
|
||||
// Getters & Setters
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getProvider() { return provider; }
|
||||
public void setProvider(String provider) { this.provider = provider; }
|
||||
|
||||
public String getEventType() { return eventType; }
|
||||
public void setEventType(String eventType) { this.eventType = eventType; }
|
||||
|
||||
public String getEventId() { return eventId; }
|
||||
public void setEventId(String eventId) { this.eventId = eventId; }
|
||||
|
||||
public String getSignature() { return signature; }
|
||||
public void setSignature(String signature) { this.signature = signature; }
|
||||
|
||||
public String getPayload() { return payload; }
|
||||
public void setPayload(String payload) { this.payload = payload; }
|
||||
|
||||
public Boolean getProcessed() { return processed; }
|
||||
public void setProcessed(Boolean processed) { this.processed = processed; }
|
||||
|
||||
public LocalDateTime getProcessedAt() { return processedAt; }
|
||||
public void setProcessedAt(LocalDateTime processedAt) { this.processedAt = processedAt; }
|
||||
|
||||
public Integer getAttempts() { return attempts; }
|
||||
public void setAttempts(Integer attempts) { this.attempts = attempts; }
|
||||
|
||||
public String getLastError() { return lastError; }
|
||||
public void setLastError(String lastError) { this.lastError = lastError; }
|
||||
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
|
||||
@PrePersist
|
||||
public void prePersist() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (createdAt == null) {
|
||||
createdAt = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
// PaymentRepository.java
|
||||
package com.imprimelibros.erp.payments.repo;
|
||||
|
||||
import com.imprimelibros.erp.payments.model.Payment;
|
||||
import com.imprimelibros.erp.payments.model.PaymentStatus;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface PaymentRepository extends JpaRepository<Payment, Long> {
|
||||
Optional<Payment> findByGatewayAndGatewayOrderId(String gateway, String gatewayOrderId);
|
||||
Optional<Payment> findFirstByOrderIdAndStatusOrderByIdDesc(Long orderId, PaymentStatus status);
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
// PaymentTransactionRepository.java
|
||||
package com.imprimelibros.erp.payments.repo;
|
||||
|
||||
import com.imprimelibros.erp.payments.model.PaymentTransaction;
|
||||
import com.imprimelibros.erp.payments.model.PaymentTransactionStatus;
|
||||
import com.imprimelibros.erp.payments.model.PaymentTransactionType;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface PaymentTransactionRepository extends JpaRepository<PaymentTransaction, Long>, JpaSpecificationExecutor<PaymentTransaction> {
|
||||
List<PaymentTransaction> findByGatewayTransactionId(String gatewayTransactionId);
|
||||
Optional<PaymentTransaction> findByIdempotencyKey(String idempotencyKey);
|
||||
Optional<PaymentTransaction> findByPaymentIdAndType(
|
||||
Long paymentId,
|
||||
PaymentTransactionType type
|
||||
);
|
||||
Optional<PaymentTransaction> findFirstByPaymentIdAndTypeAndStatusOrderByIdDesc(
|
||||
Long paymentId,
|
||||
PaymentTransactionType type,
|
||||
PaymentTransactionStatus status
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
// RefundRepository.java
|
||||
package com.imprimelibros.erp.payments.repo;
|
||||
|
||||
import com.imprimelibros.erp.payments.model.Refund;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
public interface RefundRepository extends JpaRepository<Refund, Long> {
|
||||
@Query("select coalesce(sum(r.amountCents),0) from Refund r where r.payment.id = :paymentId and r.status = com.imprimelibros.erp.payments.model.RefundStatus.succeeded")
|
||||
long sumSucceededByPaymentId(@Param("paymentId") Long paymentId);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
// WebhookEventRepository.java
|
||||
package com.imprimelibros.erp.payments.repo;
|
||||
|
||||
import com.imprimelibros.erp.payments.model.WebhookEvent;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface WebhookEventRepository extends JpaRepository<WebhookEvent, Long> {
|
||||
|
||||
Optional<WebhookEvent> findByProviderAndEventId(String provider, String eventId);
|
||||
}
|
||||
@ -25,8 +25,8 @@ public class PdfController {
|
||||
@RequestParam(defaultValue = "inline") String mode,
|
||||
Locale locale) {
|
||||
|
||||
if (type.equals(DocumentType.PRESUPUESTO.toString()) && id == null) {
|
||||
throw new IllegalArgumentException("Falta el ID del presupuesto para generar el PDF");
|
||||
if (id == null) {
|
||||
throw new IllegalArgumentException("Falta el ID para generar el PDF");
|
||||
}
|
||||
if (type.equals(DocumentType.PRESUPUESTO.toString())) {
|
||||
Long presupuestoId = Long.valueOf(id);
|
||||
@ -39,7 +39,22 @@ public class PdfController {
|
||||
: ContentDisposition.inline()).filename("presupuesto-" + id + ".pdf").build());
|
||||
|
||||
return new ResponseEntity<>(pdf, headers, HttpStatus.OK);
|
||||
} else {
|
||||
}/*else if(type.equals(DocumentType.PEDIDO.toString())) {
|
||||
|
||||
} */else if (type.equals(DocumentType.FACTURA.toString())) {
|
||||
Long facturaId = Long.valueOf(id);
|
||||
byte[] pdf = pdfService.generaFactura(facturaId, locale);
|
||||
var headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_PDF);
|
||||
headers.setContentDisposition(
|
||||
("download".equals(mode)
|
||||
? ContentDisposition.attachment()
|
||||
: ContentDisposition.inline()).filename("factura-" + id + ".pdf").build());
|
||||
|
||||
return new ResponseEntity<>(pdf, headers, HttpStatus.OK);
|
||||
|
||||
}
|
||||
else {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
package com.imprimelibros.erp.pdf;
|
||||
|
||||
import com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder;
|
||||
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
||||
@ -16,6 +16,11 @@ import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.common.web.HtmlToXhtml;
|
||||
import com.imprimelibros.erp.facturacion.Factura;
|
||||
import com.imprimelibros.erp.facturacion.service.FacturacionService;
|
||||
import com.imprimelibros.erp.pedidos.PedidoDireccion;
|
||||
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||
|
||||
@Service
|
||||
public class PdfService {
|
||||
@ -24,6 +29,8 @@ public class PdfService {
|
||||
private final PdfRenderer renderer;
|
||||
private final PresupuestoRepository presupuestoRepository;
|
||||
private final Utils utils;
|
||||
private final FacturacionService facturacionService;
|
||||
private final PedidoService pedidoService;
|
||||
|
||||
private final Map<String, String> empresa = Map.of(
|
||||
"nombre", "ImprimeLibros ERP",
|
||||
@ -35,7 +42,6 @@ public class PdfService {
|
||||
"poblacion", "Madrid",
|
||||
"web", "www.imprimelibros.com");
|
||||
|
||||
|
||||
private static class PrecioTirada {
|
||||
private Double peso;
|
||||
@JsonProperty("iva_importe_4")
|
||||
@ -88,12 +94,15 @@ public class PdfService {
|
||||
}
|
||||
|
||||
public PdfService(TemplateRegistry registry, PdfTemplateEngine engine, PdfRenderer renderer,
|
||||
PresupuestoRepository presupuestoRepository, Utils utils) {
|
||||
PresupuestoRepository presupuestoRepository, Utils utils, FacturacionService facturacionService,
|
||||
PedidoService pedidoService) {
|
||||
this.registry = registry;
|
||||
this.engine = engine;
|
||||
this.renderer = renderer;
|
||||
this.presupuestoRepository = presupuestoRepository;
|
||||
this.utils = utils;
|
||||
this.pedidoService = pedidoService;
|
||||
this.facturacionService = facturacionService;
|
||||
}
|
||||
|
||||
private byte[] generate(DocumentSpec spec) {
|
||||
@ -122,27 +131,6 @@ public class PdfService {
|
||||
|
||||
model.put("titulo", presupuesto.getTitulo());
|
||||
|
||||
/*
|
||||
* Map<String, Object> resumen = presupuestoService.getTextosResumen(
|
||||
* presupuesto, null, model, model, null)
|
||||
*/
|
||||
model.put("lineas", List.of(
|
||||
Map.of("descripcion", "Impresión interior B/N offset 80 g",
|
||||
"meta", "300 páginas · tinta negra · papel 80 g",
|
||||
"uds", 1000,
|
||||
"precio", 2.15,
|
||||
"dto", 0,
|
||||
"importe", 2150.0),
|
||||
Map.of("descripcion", "Cubierta color 300 g laminado mate",
|
||||
"meta", "Lomo 15 mm · 4/0 · laminado mate",
|
||||
"uds", 1000,
|
||||
"precio", 0.38,
|
||||
"dto", 5.0,
|
||||
"importe", 361.0)));
|
||||
|
||||
model.put("servicios", List.of(
|
||||
Map.of("descripcion", "Transporte península", "unidades", 1, "precio", 90.00)));
|
||||
|
||||
Map<String, Object> specs = utils.getTextoPresupuesto(presupuesto, locale);
|
||||
model.put("specs", specs);
|
||||
|
||||
@ -202,4 +190,54 @@ public class PdfService {
|
||||
throw new RuntimeException("Error generando presupuesto PDF", e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] generaFactura(Long facturaId, Locale locale) {
|
||||
|
||||
try {
|
||||
|
||||
Factura factura = facturacionService.getFactura(facturaId);
|
||||
if (factura == null) {
|
||||
throw new IllegalArgumentException("Factura no encontrada: " + facturaId);
|
||||
}
|
||||
|
||||
factura.getLineas().forEach(l -> l.setDescripcion(HtmlToXhtml.toXhtml(l.getDescripcion())));
|
||||
|
||||
PedidoDireccion direccionFacturacion = pedidoService
|
||||
.getPedidoDireccionFacturacionByPedidoId(factura.getPedidoId());
|
||||
if (direccionFacturacion == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Dirección de facturación no encontrada para el pedido: " + factura.getPedidoId());
|
||||
}
|
||||
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("factura", factura);
|
||||
model.put("direccionFacturacion", direccionFacturacion);
|
||||
|
||||
var spec = new DocumentSpec(
|
||||
DocumentType.FACTURA,
|
||||
"factura-a4",
|
||||
locale,
|
||||
model);
|
||||
|
||||
byte[] pdf = this.generate(spec);
|
||||
|
||||
// HTML
|
||||
// (Opcional) generar HTML de depuración con CSS incrustado
|
||||
try {
|
||||
String templateName = registry.resolve(DocumentType.FACTURA, "factura-a4");
|
||||
String html = engine.render(templateName, locale, model);
|
||||
String css = Files.readString(Path.of("src/main/resources/static/assets/css/facturapdf.css"));
|
||||
String htmlWithCss = html.replaceFirst("(?i)</head>", "<style>\n" + css + "\n</style>\n</head>");
|
||||
Path htmlPath = Path.of("target/factura-test.html");
|
||||
Files.writeString(htmlPath, htmlWithCss, StandardCharsets.UTF_8);
|
||||
} catch (Exception ignore) {
|
||||
/* solo para depuración */ }
|
||||
|
||||
return pdf;
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error generando factura PDF", e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
120
src/main/java/com/imprimelibros/erp/pedidos/Pedido.java
Normal file
120
src/main/java/com/imprimelibros/erp/pedidos/Pedido.java
Normal file
@ -0,0 +1,120 @@
|
||||
package com.imprimelibros.erp.pedidos;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.imprimelibros.erp.common.jpa.AbstractAuditedEntity;
|
||||
|
||||
@Entity
|
||||
@Table(name = "pedidos")
|
||||
public class Pedido extends AbstractAuditedEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
// Campos económicos
|
||||
@Column(name = "base", nullable = false)
|
||||
private Double base;
|
||||
|
||||
@Column(name = "envio", nullable = false)
|
||||
private Double envio = 0.0;
|
||||
|
||||
@Column(name = "iva4", nullable = false)
|
||||
private Double iva4 = 0.0;
|
||||
|
||||
@Column(name = "iva21", nullable = false)
|
||||
private Double iva21 = 0.0;
|
||||
|
||||
@Column(name = "descuento", nullable = false)
|
||||
private Double descuento = 0.0;
|
||||
|
||||
@Column(name = "total", nullable = false)
|
||||
private Double total = 0.0;
|
||||
|
||||
// Datos de proveedor
|
||||
@Column(name = "proveedor", length = 100)
|
||||
private String proveedor;
|
||||
|
||||
@Column(name = "proveedor_ref", length = 100)
|
||||
private String proveedorRef;
|
||||
|
||||
@OneToMany(mappedBy = "pedido", cascade = CascadeType.ALL, orphanRemoval = false)
|
||||
private List<PedidoLinea> lineas = new ArrayList<>();
|
||||
|
||||
// --- Getters y setters ---
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Double getBase() {
|
||||
return base;
|
||||
}
|
||||
|
||||
public void setBase(Double base) {
|
||||
this.base = base;
|
||||
}
|
||||
|
||||
public Double getEnvio() {
|
||||
return envio;
|
||||
}
|
||||
|
||||
public void setEnvio(Double envio) {
|
||||
this.envio = envio;
|
||||
}
|
||||
|
||||
public Double getIva4() {
|
||||
return iva4;
|
||||
}
|
||||
|
||||
public void setIva4(Double iva4) {
|
||||
this.iva4 = iva4;
|
||||
}
|
||||
|
||||
public Double getIva21() {
|
||||
return iva21;
|
||||
}
|
||||
|
||||
public void setIva21(Double iva21) {
|
||||
this.iva21 = iva21;
|
||||
}
|
||||
|
||||
public Double getDescuento() {
|
||||
return descuento;
|
||||
}
|
||||
|
||||
public void setDescuento(Double descuento) {
|
||||
this.descuento = descuento;
|
||||
}
|
||||
|
||||
public Double getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
public void setTotal(Double total) {
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
public String getProveedor() {
|
||||
return proveedor;
|
||||
}
|
||||
|
||||
public void setProveedor(String proveedor) {
|
||||
this.proveedor = proveedor;
|
||||
}
|
||||
|
||||
public String getProveedorRef() {
|
||||
return proveedorRef;
|
||||
}
|
||||
|
||||
public void setProveedorRef(String proveedorRef) {
|
||||
this.proveedorRef = proveedorRef;
|
||||
}
|
||||
}
|
||||
308
src/main/java/com/imprimelibros/erp/pedidos/PedidoDireccion.java
Normal file
308
src/main/java/com/imprimelibros/erp/pedidos/PedidoDireccion.java
Normal file
@ -0,0 +1,308 @@
|
||||
package com.imprimelibros.erp.pedidos;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import com.imprimelibros.erp.direcciones.Direccion.TipoIdentificacionFiscal;
|
||||
import com.imprimelibros.erp.paises.Paises;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Entity
|
||||
@Table(name = "pedidos_direcciones")
|
||||
public class PedidoDireccion {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
// FK a pedidos_lineas.id (nullable, on delete set null)
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "pedido_linea_id")
|
||||
private PedidoLinea pedidoLinea;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "pedido_id")
|
||||
private Pedido pedido;
|
||||
|
||||
@Column(name = "unidades")
|
||||
private Integer unidades;
|
||||
|
||||
@Column(name = "is_facturacion", nullable = false)
|
||||
private boolean facturacion = false;
|
||||
|
||||
@Column(name = "is_ejemplar_prueba", nullable = false)
|
||||
private boolean ejemplarPrueba = false;
|
||||
|
||||
@Column(name = "email", length = 255)
|
||||
private String email;
|
||||
|
||||
@Column(name = "att", nullable = false, length = 150)
|
||||
private String att;
|
||||
|
||||
@Column(name = "direccion", nullable = false, length = 255)
|
||||
private String direccion;
|
||||
|
||||
@Column(name = "cp", nullable = false)
|
||||
private Integer cp;
|
||||
|
||||
@Column(name = "ciudad", nullable = false, length = 100)
|
||||
private String ciudad;
|
||||
|
||||
@Column(name = "provincia", nullable = false, length = 100)
|
||||
private String provincia;
|
||||
|
||||
@Column(name = "pais_code3", nullable = false, length = 3)
|
||||
private String paisCode3 = "esp";
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "pais_code3", referencedColumnName = "code3", insertable = false, updatable = false)
|
||||
private Paises pais;
|
||||
|
||||
@Transient
|
||||
private String paisNombre;
|
||||
|
||||
@Column(name = "telefono", nullable = false, length = 30)
|
||||
private String telefono;
|
||||
|
||||
@Column(name = "instrucciones", length = 255)
|
||||
private String instrucciones;
|
||||
|
||||
@Column(name = "razon_social", length = 150)
|
||||
private String razonSocial;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "tipo_identificacion_fiscal", nullable = false, length = 20)
|
||||
private TipoIdentificacionFiscal tipoIdentificacionFiscal = TipoIdentificacionFiscal.DNI;
|
||||
|
||||
@Column(name = "identificacion_fiscal", length = 50)
|
||||
private String identificacionFiscal;
|
||||
|
||||
@Column(name = "is_palets", nullable = false)
|
||||
private boolean palets = false;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
// ===== GETTERS & SETTERS =====
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public PedidoLinea getPedidoLinea() {
|
||||
return pedidoLinea;
|
||||
}
|
||||
|
||||
public void setPedidoLinea(PedidoLinea pedidoLinea) {
|
||||
this.pedidoLinea = pedidoLinea;
|
||||
}
|
||||
|
||||
public Pedido getPedido() {
|
||||
return pedido;
|
||||
}
|
||||
|
||||
public void setPedido(Pedido pedido) {
|
||||
this.pedido = pedido;
|
||||
}
|
||||
|
||||
public Integer getUnidades() {
|
||||
return unidades;
|
||||
}
|
||||
|
||||
public void setUnidades(Integer unidades) {
|
||||
this.unidades = unidades;
|
||||
}
|
||||
|
||||
public boolean isFacturacion() {
|
||||
return facturacion;
|
||||
}
|
||||
|
||||
public void setFacturacion(boolean facturacion) {
|
||||
this.facturacion = facturacion;
|
||||
}
|
||||
|
||||
public boolean isEjemplarPrueba() {
|
||||
return ejemplarPrueba;
|
||||
}
|
||||
|
||||
public void setEjemplarPrueba(boolean ejemplarPrueba) {
|
||||
this.ejemplarPrueba = ejemplarPrueba;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getAtt() {
|
||||
return att;
|
||||
}
|
||||
|
||||
public void setAtt(String att) {
|
||||
this.att = att;
|
||||
}
|
||||
|
||||
public String getDireccion() {
|
||||
return direccion;
|
||||
}
|
||||
|
||||
public void setDireccion(String direccion) {
|
||||
this.direccion = direccion;
|
||||
}
|
||||
|
||||
public Integer getCp() {
|
||||
return cp;
|
||||
}
|
||||
|
||||
public void setCp(Integer cp) {
|
||||
this.cp = cp;
|
||||
}
|
||||
|
||||
public String getCiudad() {
|
||||
return ciudad;
|
||||
}
|
||||
|
||||
public void setCiudad(String ciudad) {
|
||||
this.ciudad = ciudad;
|
||||
}
|
||||
|
||||
public String getProvincia() {
|
||||
return provincia;
|
||||
}
|
||||
|
||||
public void setProvincia(String provincia) {
|
||||
this.provincia = provincia;
|
||||
}
|
||||
|
||||
public String getPaisCode3() {
|
||||
return paisCode3;
|
||||
}
|
||||
|
||||
public void setPaisCode3(String paisCode3) {
|
||||
this.paisCode3 = paisCode3;
|
||||
}
|
||||
|
||||
public Paises getPais() {
|
||||
return pais;
|
||||
}
|
||||
|
||||
public void setPais(Paises pais) {
|
||||
this.pais = pais;
|
||||
}
|
||||
|
||||
public String getTelefono() {
|
||||
return telefono;
|
||||
}
|
||||
|
||||
public void setTelefono(String telefono) {
|
||||
this.telefono = telefono;
|
||||
}
|
||||
|
||||
public String getInstrucciones() {
|
||||
return instrucciones;
|
||||
}
|
||||
|
||||
public void setInstrucciones(String instrucciones) {
|
||||
this.instrucciones = instrucciones;
|
||||
}
|
||||
|
||||
public String getRazonSocial() {
|
||||
return razonSocial;
|
||||
}
|
||||
|
||||
public void setRazonSocial(String razonSocial) {
|
||||
this.razonSocial = razonSocial;
|
||||
}
|
||||
|
||||
public TipoIdentificacionFiscal getTipoIdentificacionFiscal() {
|
||||
return tipoIdentificacionFiscal;
|
||||
}
|
||||
|
||||
public void setTipoIdentificacionFiscal(TipoIdentificacionFiscal tipoIdentificacionFiscal) {
|
||||
this.tipoIdentificacionFiscal = tipoIdentificacionFiscal;
|
||||
}
|
||||
|
||||
public String getIdentificacionFiscal() {
|
||||
return identificacionFiscal;
|
||||
}
|
||||
|
||||
public void setIdentificacionFiscal(String identificacionFiscal) {
|
||||
this.identificacionFiscal = identificacionFiscal;
|
||||
}
|
||||
|
||||
public boolean isPalets() {
|
||||
return palets;
|
||||
}
|
||||
|
||||
public void setPalets(boolean palets) {
|
||||
this.palets = palets;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public String getPaisNombre() {
|
||||
return paisNombre;
|
||||
}
|
||||
|
||||
public void setPaisNombre(String paisNombre) {
|
||||
this.paisNombre = paisNombre;
|
||||
}
|
||||
|
||||
|
||||
public Map<String, Object> toSkMap(Double pesoKg) {
|
||||
|
||||
Map<String, Object> direccion = new HashMap<>();
|
||||
direccion.put("cantidad", this.getUnidades());
|
||||
direccion.put("peso", pesoKg);
|
||||
direccion.put("att", this.getAtt());
|
||||
direccion.put("email", this.getEmail());
|
||||
direccion.put("direccion", this.getDireccion());
|
||||
direccion.put("pais_code3", this.getPaisCode3());
|
||||
direccion.put("cp", this.getCp());
|
||||
direccion.put("municipio", this.getCiudad());
|
||||
direccion.put("provincia", this.getProvincia());
|
||||
direccion.put("telefono", this.getTelefono());
|
||||
direccion.put("entregaPieCalle", this.isPalets() ? 1 : 0);
|
||||
direccion.put("is_ferro_prototipo", this.isEjemplarPrueba() ? 1 : 0);
|
||||
direccion.put("num_ferro_prototipo", this.isEjemplarPrueba() ? 1 : 0);
|
||||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("direccion", direccion);
|
||||
map.put("unidades", this.getUnidades());
|
||||
map.put("entregaPalets", this.isPalets() ? 1 : 0);
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
public static Map<String, Object> toSkMapDepositoLegal() {
|
||||
Map<String, Object> direccion = new HashMap<>();
|
||||
direccion.put("cantidad", 4);
|
||||
direccion.put("peso", 0);
|
||||
direccion.put("att", "Unidades para Depósito Legal (sin envío)");
|
||||
direccion.put("email", "");
|
||||
direccion.put("direccion", "");
|
||||
direccion.put("pais_code3", "esp");
|
||||
direccion.put("cp", "");
|
||||
direccion.put("municipio", "");
|
||||
direccion.put("provincia", "");
|
||||
direccion.put("telefono", "");
|
||||
direccion.put("entregaPieCalle", 0);
|
||||
direccion.put("is_ferro_prototipo", 0);
|
||||
direccion.put("num_ferro_prototipo", 0);
|
||||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("direccion", direccion);
|
||||
map.put("unidades", 4);
|
||||
map.put("entregaPalets", 0);
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.imprimelibros.erp.pedidos;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PedidoDireccionRepository extends JpaRepository<PedidoDireccion, Long> {
|
||||
|
||||
// Todas las direcciones de una línea de pedido
|
||||
List<PedidoDireccion> findByPedidoLinea_Id(Long pedidoLineaId);
|
||||
|
||||
// Si en tu código sueles trabajar con el objeto:
|
||||
List<PedidoDireccion> findByPedidoLinea(PedidoLinea pedidoLinea);
|
||||
|
||||
PedidoDireccion findByPedidoIdAndFacturacionTrue(Long pedidoId);
|
||||
|
||||
@Query("""
|
||||
select distinct d
|
||||
from PedidoDireccion d
|
||||
join d.pedidoLinea pl
|
||||
where d.pedidoLinea.id = :pedidoLineaId
|
||||
""")
|
||||
List<PedidoDireccion> findByPedidoLineaId(Long pedidoLineaId);
|
||||
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
package com.imprimelibros.erp.pedidos;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class PedidoEstadoService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PedidoEstadoService.class);
|
||||
|
||||
private final PedidoLineaRepository pedidoLineaRepository;
|
||||
private final PedidoService pedidoService;
|
||||
|
||||
public PedidoEstadoService(PedidoLineaRepository pedidoLineaRepository, PedidoService pedidoService) {
|
||||
this.pedidoLineaRepository = pedidoLineaRepository;
|
||||
this.pedidoService = pedidoService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ejecuta cada noche a las 4:00 AM
|
||||
*/
|
||||
@Scheduled(cron = "0 0 4 * * *")
|
||||
public void actualizarEstadosPedidos() {
|
||||
|
||||
List<PedidoLinea> pedidosLineas = pedidoLineaRepository.findPedidosLineasParaActualizarEstado();
|
||||
|
||||
for (PedidoLinea linea : pedidosLineas) {
|
||||
try {
|
||||
Map<String, Object> resultado = pedidoService.actualizarEstado(linea.getId(), Locale.getDefault());
|
||||
|
||||
if (!Boolean.TRUE.equals(resultado.get("success"))) {
|
||||
log.error("Error al actualizar estado. pedidoLineaId={} message={}",
|
||||
linea.getId(), resultado.get("message"));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("Excepción actualizando estado. pedidoLineaId={}", linea.getId(), ex);
|
||||
}
|
||||
|
||||
// rate limit / delay
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.error("Job interrumpido mientras dormía (rate limit).");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
136
src/main/java/com/imprimelibros/erp/pedidos/PedidoLinea.java
Normal file
136
src/main/java/com/imprimelibros/erp/pedidos/PedidoLinea.java
Normal file
@ -0,0 +1,136 @@
|
||||
package com.imprimelibros.erp.pedidos;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
|
||||
@Entity
|
||||
@Table(name = "pedidos_lineas")
|
||||
public class PedidoLinea {
|
||||
|
||||
public enum Estado {
|
||||
pendiente_pago("pedido.estado.pendiente_pago", 1),
|
||||
procesando_pago("pedido.estado.procesando_pago", 2),
|
||||
denegado_pago("pedido.estado.denegado_pago", 3),
|
||||
aprobado("pedido.estado.aprobado", 4),
|
||||
maquetacion("pedido.estado.maquetacion", 5),
|
||||
haciendo_ferro("pedido.estado.haciendo_ferro", 6),
|
||||
esperando_aceptacion_ferro("pedido.estado.esperando_aceptacion_ferro", 7),
|
||||
ferro_cliente("pedido.estado.ferro_cliente", 8),
|
||||
produccion("pedido.estado.produccion", 9),
|
||||
terminado("pedido.estado.terminado", 10),
|
||||
enviado("pedido.estado.enviado", 11),
|
||||
cancelado("pedido.estado.cancelado", 12);
|
||||
|
||||
private final String messageKey;
|
||||
private final int priority;
|
||||
|
||||
Estado(String messageKey, int priority) {
|
||||
this.messageKey = messageKey;
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
public String getMessageKey() {
|
||||
return messageKey;
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
return priority;
|
||||
}
|
||||
}
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "pedido_id", nullable = false)
|
||||
private Pedido pedido;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "presupuesto_id", nullable = false)
|
||||
private Presupuesto presupuesto;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "estado", nullable = false)
|
||||
private Estado estado = Estado.aprobado;
|
||||
|
||||
@Column(name = "estado_manual", nullable = false)
|
||||
private Boolean estadoManual;
|
||||
|
||||
@Column(name = "fecha_entrega")
|
||||
private LocalDateTime fechaEntrega;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "created_by", nullable = false)
|
||||
private Long createdBy;
|
||||
|
||||
// --- Getters y setters ---
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Pedido getPedido() {
|
||||
return pedido;
|
||||
}
|
||||
|
||||
public void setPedido(Pedido pedido) {
|
||||
this.pedido = pedido;
|
||||
}
|
||||
|
||||
public Presupuesto getPresupuesto() {
|
||||
return presupuesto;
|
||||
}
|
||||
|
||||
public void setPresupuesto(Presupuesto presupuesto) {
|
||||
this.presupuesto = presupuesto;
|
||||
}
|
||||
|
||||
public Estado getEstado() {
|
||||
return estado;
|
||||
}
|
||||
|
||||
public void setEstado(Estado estado) {
|
||||
this.estado = estado;
|
||||
}
|
||||
|
||||
public Boolean getEstadoManual() {
|
||||
return estadoManual;
|
||||
}
|
||||
|
||||
public void setEstadoManual(Boolean estadoManual) {
|
||||
this.estadoManual = estadoManual;
|
||||
}
|
||||
|
||||
public LocalDateTime getFechaEntrega() {
|
||||
return fechaEntrega;
|
||||
}
|
||||
|
||||
public void setFechaEntrega(LocalDateTime fechaEntrega) {
|
||||
this.fechaEntrega = fechaEntrega;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Long getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
public void setCreatedBy(Long createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package com.imprimelibros.erp.pedidos;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface PedidoLineaRepository extends JpaRepository<PedidoLinea, Long> {
|
||||
|
||||
List<PedidoLinea> findByPedidoId(Long pedidoId);
|
||||
|
||||
List<PedidoLinea> findByPedidoIdOrderByIdAsc(Long pedidoId);
|
||||
|
||||
List<PedidoLinea> findByPresupuestoId(Long presupuestoId);
|
||||
|
||||
@Query("""
|
||||
SELECT pl
|
||||
FROM PedidoLinea pl
|
||||
JOIN pl.presupuesto p
|
||||
WHERE pl.estadoManual = false
|
||||
AND pl.estado IN (
|
||||
'haciendo_ferro',
|
||||
'esperando_aceptacion_ferro',
|
||||
'produccion',
|
||||
'terminado'
|
||||
)
|
||||
AND p.proveedor = 'Safekat'
|
||||
AND p.proveedorRef1 IS NOT NULL
|
||||
AND p.proveedorRef2 IS NOT NULL
|
||||
""")
|
||||
List<PedidoLinea> findPedidosLineasParaActualizarEstado();
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package com.imprimelibros.erp.pedidos;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface PedidoRepository extends JpaRepository<Pedido, Long>, JpaSpecificationExecutor<Pedido> {
|
||||
|
||||
// Suma de "total" para un "createdBy" desde una fecha dada
|
||||
@Query("""
|
||||
SELECT COALESCE(SUM(p.total), 0)
|
||||
FROM Pedido p
|
||||
WHERE p.createdBy.id = :userId
|
||||
AND p.createdAt >= :afterDate
|
||||
""")
|
||||
Double sumTotalByCreatedByAndCreatedAtAfter(@Param("userId") Long userId,
|
||||
@Param("afterDate") Instant afterDate);
|
||||
}
|
||||
765
src/main/java/com/imprimelibros/erp/pedidos/PedidoService.java
Normal file
765
src/main/java/com/imprimelibros/erp/pedidos/PedidoService.java
Normal file
@ -0,0 +1,765 @@
|
||||
package com.imprimelibros.erp.pedidos;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.imprimelibros.erp.cart.Cart;
|
||||
import com.imprimelibros.erp.cart.CartDireccion;
|
||||
import com.imprimelibros.erp.cart.CartItem;
|
||||
import com.imprimelibros.erp.cart.CartService;
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.direcciones.Direccion;
|
||||
import com.imprimelibros.erp.presupuesto.PresupuestoRepository;
|
||||
import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
|
||||
import com.imprimelibros.erp.users.UserService;
|
||||
import com.imprimelibros.erp.direcciones.DireccionService;
|
||||
import com.imprimelibros.erp.externalApi.skApiClient;
|
||||
import com.imprimelibros.erp.facturacion.FacturaDireccion;
|
||||
import com.imprimelibros.erp.facturacion.dto.DireccionFacturacionDto;
|
||||
import com.imprimelibros.erp.pedidos.PedidoLinea.Estado;
|
||||
|
||||
@Service
|
||||
public class PedidoService {
|
||||
|
||||
private final PedidoRepository pedidoRepository;
|
||||
private final PedidoLineaRepository pedidoLineaRepository;
|
||||
private final PresupuestoRepository presupuestoRepository;
|
||||
private final PedidoDireccionRepository pedidoDireccionRepository;
|
||||
private final DireccionService direccionService;
|
||||
private final UserService userService;
|
||||
private final PresupuestoService presupuestoService;
|
||||
private final CartService cartService;
|
||||
private final skApiClient skApiClient;
|
||||
private final PresupuestoRepository presupuestoRepo;
|
||||
private final MessageSource messageSource;
|
||||
|
||||
public PedidoService(PedidoRepository pedidoRepository, PedidoLineaRepository pedidoLineaRepository,
|
||||
PresupuestoRepository presupuestoRepository, PedidoDireccionRepository pedidoDireccionRepository,
|
||||
DireccionService direccionService, UserService userService, PresupuestoService presupuestoService,
|
||||
CartService cartService, skApiClient skApiClient, PresupuestoRepository presupuestoRepo,
|
||||
MessageSource messageSource) {
|
||||
this.pedidoRepository = pedidoRepository;
|
||||
this.pedidoLineaRepository = pedidoLineaRepository;
|
||||
this.presupuestoRepository = presupuestoRepository;
|
||||
this.pedidoDireccionRepository = pedidoDireccionRepository;
|
||||
this.direccionService = direccionService;
|
||||
this.userService = userService;
|
||||
this.presupuestoService = presupuestoService;
|
||||
this.cartService = cartService;
|
||||
this.skApiClient = skApiClient;
|
||||
this.presupuestoRepo = presupuestoRepo;
|
||||
this.messageSource = messageSource;
|
||||
}
|
||||
|
||||
public Pedido getPedidoById(Long pedidoId) {
|
||||
return pedidoRepository.findById(pedidoId).orElse(null);
|
||||
}
|
||||
|
||||
public PedidoDireccion getPedidoDireccionFacturacionByPedidoId(Long pedidoId) {
|
||||
return pedidoDireccionRepository.findByPedidoIdAndFacturacionTrue(pedidoId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Pedido crearPedido(
|
||||
Long cartId,
|
||||
Long direccionFacturacionId,
|
||||
String proveedor,
|
||||
String proveedorRef) {
|
||||
|
||||
Pedido pedido = new Pedido();
|
||||
|
||||
Cart cart = cartService.getCartById(cartId);
|
||||
Map<String, Object> cartSummaryRaw = cartService.getCartSummaryRaw(cart, Locale.getDefault());
|
||||
|
||||
// Datos económicos (ojo con las claves, son las del summaryRaw)
|
||||
pedido.setBase((Double) cartSummaryRaw.getOrDefault("base", 0.0d));
|
||||
pedido.setEnvio((Double) cartSummaryRaw.getOrDefault("shipment", 0.0d));
|
||||
pedido.setIva4((Double) cartSummaryRaw.getOrDefault("iva4", 0.0d));
|
||||
pedido.setIva21((Double) cartSummaryRaw.getOrDefault("iva21", 0.0d));
|
||||
pedido.setDescuento((Double) cartSummaryRaw.getOrDefault("descuento", 0.0d));
|
||||
pedido.setTotal((Double) cartSummaryRaw.getOrDefault("total", 0.0d));
|
||||
|
||||
// Proveedor
|
||||
if (proveedor != null && proveedorRef != null) {
|
||||
pedido.setProveedor(proveedor);
|
||||
pedido.setProveedorRef(proveedorRef);
|
||||
}
|
||||
|
||||
// Auditoría mínima
|
||||
/*
|
||||
* Long userId = cart.getUserId();
|
||||
* pedido.setCreatedBy(userService.findById(userId));
|
||||
* pedido.setUpdatedBy(userService.findById(userId));
|
||||
*/
|
||||
// Se obtiene el usuario del primer presupuesto del carrito
|
||||
Long userId = null;
|
||||
List<CartItem> cartItems = cart.getItems();
|
||||
if (!cartItems.isEmpty()) {
|
||||
Presupuesto firstPresupuesto = cartItems.get(0).getPresupuesto();
|
||||
if (firstPresupuesto != null) {
|
||||
userId = firstPresupuesto.getUser().getId();
|
||||
}
|
||||
}
|
||||
if (userId == null) {
|
||||
userId = cart.getUserId();
|
||||
}
|
||||
pedido.setCreatedBy(userService.findById(userId));
|
||||
pedido.setUpdatedBy(userService.findById(userId));
|
||||
pedido.setCreatedAt(Instant.now());
|
||||
pedido.setDeleted(false);
|
||||
pedido.setUpdatedAt(Instant.now());
|
||||
|
||||
// Guardamos el pedido
|
||||
Pedido pedidoGuardado = pedidoRepository.save(pedido);
|
||||
pedidoGuardado.setCreatedBy(userService.findById(userId));
|
||||
pedidoGuardado.setUpdatedBy(userService.findById(userId));
|
||||
pedidoRepository.save(pedidoGuardado);
|
||||
|
||||
List<CartItem> items = cart.getItems();
|
||||
|
||||
for (Integer i = 0; i < items.size(); i++) {
|
||||
CartItem item = items.get(i);
|
||||
Presupuesto pCart = item.getPresupuesto();
|
||||
|
||||
// Asegurarnos de trabajar con la entidad gestionada por JPA
|
||||
Presupuesto p = presupuestoRepository.findById(pCart.getId())
|
||||
.orElseThrow(() -> new IllegalStateException("Presupuesto no encontrado: " + pCart.getId()));
|
||||
p.setEstado(Presupuesto.Estado.aceptado);
|
||||
presupuestoRepository.save(p);
|
||||
|
||||
PedidoLinea linea = new PedidoLinea();
|
||||
linea.setPedido(pedidoGuardado);
|
||||
linea.setPresupuesto(p);
|
||||
linea.setCreatedBy(userId);
|
||||
linea.setCreatedAt(LocalDateTime.now());
|
||||
linea.setEstado(PedidoLinea.Estado.pendiente_pago);
|
||||
linea.setEstadoManual(false);
|
||||
pedidoLineaRepository.save(linea);
|
||||
|
||||
// Guardar las direcciones asociadas a la línea del pedido
|
||||
Map<String, Object> direcciones_presupuesto = this.getDireccionesPresupuesto(cart, p);
|
||||
saveDireccionesPedidoLinea(direcciones_presupuesto, pedidoGuardado, linea, direccionFacturacionId);
|
||||
|
||||
}
|
||||
|
||||
return pedidoGuardado;
|
||||
}
|
||||
|
||||
public Boolean markPedidoAsProcesingPayment(Long pedidoId) {
|
||||
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
|
||||
if (pedido == null) {
|
||||
return false;
|
||||
}
|
||||
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
|
||||
for (PedidoLinea linea : lineas) {
|
||||
linea.setEstado(PedidoLinea.Estado.procesando_pago);
|
||||
pedidoLineaRepository.save(linea);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public Boolean markPedidoAsPaymentDenied(Long pedidoId) {
|
||||
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
|
||||
if (pedido == null) {
|
||||
return false;
|
||||
}
|
||||
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
|
||||
for (PedidoLinea linea : lineas) {
|
||||
linea.setEstado(PedidoLinea.Estado.denegado_pago);
|
||||
pedidoLineaRepository.save(linea);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public Pedido findById(Long pedidoId) {
|
||||
return pedidoRepository.findById(pedidoId).orElse(null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Boolean upsertDireccionFacturacion(Long pedidoId, DireccionFacturacionDto direccionData) {
|
||||
|
||||
try {
|
||||
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
|
||||
if (pedido != null) {
|
||||
|
||||
PedidoDireccion direccionPedido = pedidoDireccionRepository.findByPedidoIdAndFacturacionTrue(pedidoId);
|
||||
|
||||
if (direccionPedido == null) {
|
||||
// crear
|
||||
direccionPedido = direccionData.toPedidoDireccion();
|
||||
direccionPedido.setPedido(pedido);
|
||||
|
||||
} else {
|
||||
// actualizar en la existente (NO crees una nueva, para conservar ID)
|
||||
direccionData.applyTo(direccionPedido); // si implementas applyTo()
|
||||
direccionPedido.setFacturacion(true); // por si acaso
|
||||
}
|
||||
|
||||
pedidoDireccionRepository.save(direccionPedido);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Lista de los items del pedido preparados para la vista */
|
||||
@Transactional
|
||||
public List<Map<String, Object>> getLineas(Long pedidoId, Locale locale) {
|
||||
Pedido p = pedidoRepository.findById(pedidoId).orElse(null);
|
||||
if (p == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
List<Map<String, Object>> resultados = new ArrayList<>();
|
||||
List<PedidoLinea> items = pedidoLineaRepository.findByPedidoIdOrderByIdAsc(p.getId());
|
||||
for (PedidoLinea item : items) {
|
||||
|
||||
Presupuesto presupuesto = item.getPresupuesto();
|
||||
Map<String, Object> elemento = presupuestoService.getPresupuestoInfoForCard(presupuesto, locale);
|
||||
elemento.put("estado", item.getEstado());
|
||||
elemento.put("fechaEntrega",
|
||||
item.getFechaEntrega() != null ? Utils.formatDate(item.getFechaEntrega(), locale) : "");
|
||||
elemento.put("lineaId", item.getId());
|
||||
resultados.add(elemento);
|
||||
}
|
||||
return resultados;
|
||||
}
|
||||
|
||||
public PedidoDireccion getDireccionFacturacionPedido(Long pedidoId) {
|
||||
return pedidoDireccionRepository.findByPedidoIdAndFacturacionTrue(pedidoId);
|
||||
}
|
||||
|
||||
public List<PedidoDireccion> getDireccionesEntregaPedidoLinea(Long pedidoLineaId) {
|
||||
return pedidoDireccionRepository.findByPedidoLinea_Id(pedidoLineaId);
|
||||
}
|
||||
|
||||
public Boolean setOrderAsPaid(Long pedidoId) {
|
||||
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
|
||||
if (pedido == null) {
|
||||
return false;
|
||||
}
|
||||
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
|
||||
List<Map<String, Object>> referenciasProveedor = new ArrayList<>();
|
||||
Integer total = lineas.size();
|
||||
Integer counter = 1;
|
||||
for (PedidoLinea linea : lineas) {
|
||||
if (linea.getEstado() == Estado.pendiente_pago
|
||||
|| linea.getEstado() == Estado.denegado_pago) {
|
||||
|
||||
Presupuesto presupuesto = linea.getPresupuesto();
|
||||
linea.setEstado(getEstadoInicial(presupuesto));
|
||||
pedidoLineaRepository.save(linea);
|
||||
|
||||
// Save presupuesto in SK
|
||||
Map<String, Object> result = savePresupuestoSK(linea.getId(), presupuesto, counter, total);
|
||||
if (result == null) {
|
||||
return false;
|
||||
}
|
||||
referenciasProveedor.add(result);
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
|
||||
if (referenciasProveedor.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save pedido in SK
|
||||
ArrayList<Long> presupuestoSkIds = new ArrayList<>();
|
||||
for (Map<String, Object> presData : referenciasProveedor) {
|
||||
Long presId = ((Number) presData.get("id")).longValue();
|
||||
presupuestoSkIds.add(presId);
|
||||
}
|
||||
|
||||
Map<String, Object> ids = new HashMap<>();
|
||||
ids.put("presupuesto_ids", presupuestoSkIds);
|
||||
Long skPedidoId = skApiClient.crearPedido(ids);
|
||||
if (skPedidoId == null) {
|
||||
System.out.println("No se pudo crear el pedido en SK.");
|
||||
return false;
|
||||
}
|
||||
pedido.setProveedor("Safekat");
|
||||
pedido.setProveedorRef(skPedidoId.toString());
|
||||
pedidoRepository.save(pedido);
|
||||
return true;
|
||||
}
|
||||
|
||||
public Map<String, Object> actualizarEstado(Long pedidoLineaId, Locale locale) {
|
||||
|
||||
PedidoLinea pedidoLinea = pedidoLineaRepository.findById(pedidoLineaId).orElse(null);
|
||||
if (pedidoLinea == null) {
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", messageSource.getMessage("pedido.errors.linea-not-found", null, locale));
|
||||
}
|
||||
|
||||
PedidoLinea.Estado estadoOld = pedidoLinea.getEstado();
|
||||
if (estadoOld == null) {
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", messageSource.getMessage("pedido.errors.cannot-update", null, locale));
|
||||
}
|
||||
|
||||
// Rango: >= haciendo_ferro y < enviado
|
||||
if (estadoOld.getPriority() < PedidoLinea.Estado.haciendo_ferro.getPriority()
|
||||
|| estadoOld.getPriority() >= PedidoLinea.Estado.enviado.getPriority()) {
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", messageSource.getMessage("pedido.errors.cannot-update", null, locale));
|
||||
}
|
||||
|
||||
var presupuesto = pedidoLinea.getPresupuesto();
|
||||
if (presupuesto == null || presupuesto.getProveedorRef2() == null) {
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", messageSource.getMessage("pedido.errors.update-server-error", null, locale));
|
||||
}
|
||||
|
||||
Long refExterna;
|
||||
try {
|
||||
refExterna = Long.valueOf(presupuesto.getProveedorRef2().toString());
|
||||
} catch (Exception ex) {
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", messageSource.getMessage("pedido.errors.update-server-error", null, locale));
|
||||
}
|
||||
|
||||
Map<String, Object> result = skApiClient.checkPedidoEstado(refExterna, locale);
|
||||
|
||||
if (result == null || result.get("estado") == null) {
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", messageSource.getMessage("pedido.errors.update-server-error", null, locale));
|
||||
}
|
||||
|
||||
String estadoStr = String.valueOf(result.get("estado"));
|
||||
|
||||
PedidoLinea.Estado estadoSk;
|
||||
try {
|
||||
// si la API devuelve minúsculas tipo "produccion", esto funciona
|
||||
estadoSk = PedidoLinea.Estado.valueOf(estadoStr.trim().toLowerCase());
|
||||
} catch (Exception ex) {
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", messageSource.getMessage("pedido.errors.update-server-error", null, locale));
|
||||
}
|
||||
|
||||
if (estadoOld == estadoSk) {
|
||||
return Map.of(
|
||||
"success", true,
|
||||
"state", messageSource.getMessage("pedido.estado." + estadoSk.name(), null, locale),
|
||||
"stateKey", estadoSk.name(),
|
||||
"message", messageSource.getMessage("pedido.success.same-estado", null, locale));
|
||||
}
|
||||
|
||||
pedidoLinea.setEstado(estadoSk);
|
||||
pedidoLineaRepository.save(pedidoLinea);
|
||||
|
||||
return Map.of(
|
||||
"success", true,
|
||||
"state", messageSource.getMessage("pedido.estado." + estadoSk.name(), null, locale),
|
||||
"stateKey", estadoSk.name(),
|
||||
"message", messageSource.getMessage("pedido.success.estado-actualizado", null, locale));
|
||||
}
|
||||
|
||||
public Boolean markPedidoAsMaquetacionDone(Long pedidoId) {
|
||||
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
|
||||
if (pedido == null) {
|
||||
return false;
|
||||
}
|
||||
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
|
||||
for (PedidoLinea linea : lineas) {
|
||||
if (linea.getEstado() == Estado.maquetacion) {
|
||||
linea.setEstado(Estado.haciendo_ferro);
|
||||
pedidoLineaRepository.save(linea);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public Map<String, Object> getFilesType(Long pedidoLineaId, Locale locale) {
|
||||
PedidoLinea pedidoLinea = pedidoLineaRepository.findById(pedidoLineaId).orElse(null);
|
||||
if (pedidoLinea == null) {
|
||||
return Map.of("success", false, "message", "Línea de pedido no encontrada.");
|
||||
}
|
||||
Map<String, Object> files = skApiClient.getFilesTypes(
|
||||
Long.valueOf(pedidoLinea.getPresupuesto().getProveedorRef2().toString()), locale);
|
||||
return files;
|
||||
}
|
||||
|
||||
public byte[] getFerroFileContent(Long pedidoLineaId, Locale locale) {
|
||||
return downloadFile(pedidoLineaId, "ferro", locale);
|
||||
}
|
||||
|
||||
public byte[] getCubiertaFileContent(Long pedidoLineaId, Locale locale) {
|
||||
return downloadFile(pedidoLineaId, "cubierta", locale);
|
||||
}
|
||||
|
||||
public byte[] getTapaFileContent(Long pedidoLineaId, Locale locale) {
|
||||
return downloadFile(pedidoLineaId, "tapa", locale);
|
||||
}
|
||||
|
||||
public Boolean aceptarFerro(Long pedidoLineaId, Locale locale) {
|
||||
PedidoLinea pedidoLinea = pedidoLineaRepository.findById(pedidoLineaId).orElse(null);
|
||||
if (pedidoLinea == null) {
|
||||
return false;
|
||||
}
|
||||
if (pedidoLinea.getEstado() != PedidoLinea.Estado.esperando_aceptacion_ferro) {
|
||||
return false;
|
||||
}
|
||||
Boolean result = skApiClient.aceptarFerro(
|
||||
Long.valueOf(pedidoLinea.getPresupuesto().getProveedorRef2().toString()), locale);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
pedidoLinea.setEstado(PedidoLinea.Estado.produccion);
|
||||
pedidoLineaRepository.save(pedidoLinea);
|
||||
return true;
|
||||
}
|
||||
|
||||
public Boolean cancelarPedido(Long pedidoId) {
|
||||
|
||||
Pedido pedido = pedidoRepository.findById(pedidoId).orElse(null);
|
||||
if (pedido == null) {
|
||||
return false;
|
||||
}
|
||||
Boolean result = skApiClient.cancelarPedido(Long.valueOf(pedido.getProveedorRef()));
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
List<PedidoLinea> lineas = pedidoLineaRepository.findByPedidoId(pedidoId);
|
||||
for (PedidoLinea linea : lineas) {
|
||||
if (linea.getEstado() != PedidoLinea.Estado.terminado && linea.getEstado() != PedidoLinea.Estado.enviado) {
|
||||
linea.setEstado(PedidoLinea.Estado.cancelado);
|
||||
pedidoLineaRepository.save(linea);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/***************************
|
||||
* MÉTODOS PRIVADOS
|
||||
***************************/
|
||||
|
||||
private byte[] downloadFile(Long pedidoLineaId, String fileType, Locale locale) {
|
||||
PedidoLinea pedidoLinea = pedidoLineaRepository.findById(pedidoLineaId).orElse(null);
|
||||
if (pedidoLinea == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] fileData = skApiClient.downloadFile(
|
||||
Long.valueOf(pedidoLinea.getPresupuesto().getProveedorRef2().toString()),
|
||||
fileType,
|
||||
locale);
|
||||
return fileData;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
private Map<String, Object> savePresupuestoSK(Long pedidoLineaId, Presupuesto presupuesto, Integer counter,
|
||||
Integer total) {
|
||||
|
||||
Map<String, Object> data_to_send = presupuestoService.toSkApiRequest(presupuesto, true);
|
||||
data_to_send.put("createPedido", 0);
|
||||
|
||||
// Recuperar el mapa anidado datosCabecera
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> datosCabecera = (Map<String, Object>) data_to_send.get("datosCabecera");
|
||||
if (datosCabecera != null) {
|
||||
Object tituloOriginal = datosCabecera.get("titulo");
|
||||
datosCabecera.put(
|
||||
"titulo",
|
||||
"[" + (counter) + "/" + total + "] " + (tituloOriginal != null ? tituloOriginal : ""));
|
||||
}
|
||||
|
||||
List<PedidoDireccion> direccionesPedidoLinea = pedidoDireccionRepository
|
||||
.findByPedidoLineaId(pedidoLineaId);
|
||||
List<Map<String, Object>> direccionesPresupuesto = new ArrayList<>();
|
||||
List<Map<String, Object>> direccionEjemplarPrueba = new ArrayList<>();
|
||||
|
||||
for (PedidoDireccion pd : direccionesPedidoLinea) {
|
||||
if (pd.isEjemplarPrueba()) {
|
||||
direccionEjemplarPrueba.add(
|
||||
pd.toSkMap(presupuesto.getPeso()));
|
||||
} else {
|
||||
direccionesPresupuesto.add(
|
||||
pd.toSkMap(presupuesto.getPeso() * pd.getUnidades()));
|
||||
}
|
||||
|
||||
}
|
||||
if (presupuesto.getServiciosJson() != null && presupuesto.getServiciosJson().contains("deposito-legal")) {
|
||||
direccionesPresupuesto.add(
|
||||
PedidoDireccion.toSkMapDepositoLegal());
|
||||
}
|
||||
data_to_send.put("direcciones", direccionesPresupuesto);
|
||||
if (direccionEjemplarPrueba.size() > 0)
|
||||
data_to_send.put("direccionesFP1", direccionEjemplarPrueba.get(0));
|
||||
else {
|
||||
data_to_send.put("direccionesFP1", new ArrayList<>());
|
||||
}
|
||||
|
||||
Map<String, Object> result = skApiClient.savePresupuesto(data_to_send);
|
||||
|
||||
if (result.containsKey("error")) {
|
||||
System.out.println("Error al guardar presupuesto en SK");
|
||||
System.out.println("-------------------------");
|
||||
System.out.println(result.get("error"));
|
||||
// decide si seguir con otros items o abortar:
|
||||
// continue; o bien throw ...
|
||||
return null;
|
||||
}
|
||||
|
||||
Object dataObj = result.get("data");
|
||||
if (!(dataObj instanceof Map<?, ?> dataRaw)) {
|
||||
System.out.println("Formato inesperado de 'data' en savePresupuesto: " + result);
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> dataMap = (Map<String, Object>) dataRaw;
|
||||
Long presId = ((Number) dataMap.get("id")).longValue();
|
||||
String skin = ((String) dataMap.get("iskn")).toString();
|
||||
presupuesto.setProveedor("Safekat");
|
||||
presupuesto.setProveedorRef1(skin);
|
||||
presupuesto.setProveedorRef2(presId);
|
||||
presupuesto.setEstado(Presupuesto.Estado.aceptado);
|
||||
presupuestoRepo.save(presupuesto);
|
||||
|
||||
return dataMap;
|
||||
}
|
||||
|
||||
// Obtener las direcciones de envío asociadas a un presupuesto en el carrito
|
||||
private Map<String, Object> getDireccionesPresupuesto(Cart cart, Presupuesto presupuesto) {
|
||||
|
||||
List<Map<String, Object>> direccionesPresupuesto = new ArrayList<>();
|
||||
List<Map<String, Object>> direccionesPrueba = new ArrayList<>();
|
||||
if (cart.getOnlyOneShipment()) {
|
||||
List<CartDireccion> direcciones = cart.getDirecciones().stream().limit(1).toList();
|
||||
if (!direcciones.isEmpty()) {
|
||||
if (presupuesto.getServiciosJson() != null
|
||||
&& presupuesto.getServiciosJson().contains("deposito-legal")) {
|
||||
direccionesPresupuesto.add(direcciones.get(0).toSkMap(
|
||||
presupuesto.getSelectedTirada(),
|
||||
presupuesto.getPeso(),
|
||||
direcciones.get(0).getIsPalets(),
|
||||
false));
|
||||
|
||||
direccionesPresupuesto.add(direcciones.get(0).toSkMapDepositoLegal());
|
||||
} else {
|
||||
direccionesPresupuesto.add(direcciones.get(0).toSkMap(
|
||||
presupuesto.getSelectedTirada(),
|
||||
presupuesto.getPeso(),
|
||||
direcciones.get(0).getIsPalets(),
|
||||
false));
|
||||
}
|
||||
if (presupuesto.getServiciosJson() != null
|
||||
&& presupuesto.getServiciosJson().contains("ejemplar-prueba")) {
|
||||
direccionesPrueba.add(direcciones.get(0).toSkMap(
|
||||
1,
|
||||
presupuesto.getPeso(),
|
||||
false,
|
||||
true));
|
||||
}
|
||||
|
||||
Map<String, Object> direccionesRet = new HashMap<>();
|
||||
direccionesRet.put("direcciones", direccionesPresupuesto);
|
||||
if (!direccionesPrueba.isEmpty())
|
||||
direccionesRet.put("direccionesFP1", direccionesPrueba.get(0));
|
||||
else {
|
||||
direccionesRet.put("direccionesFP1", new ArrayList<>());
|
||||
}
|
||||
return direccionesRet;
|
||||
}
|
||||
} else {
|
||||
List<CartDireccion> direcciones = cart.getDirecciones().stream()
|
||||
.filter(d -> d.getPresupuesto() != null && d.getPresupuesto().getId().equals(presupuesto.getId()))
|
||||
.toList();
|
||||
|
||||
for (CartDireccion cd : direcciones) {
|
||||
|
||||
// direccion de ejemplar de prueba
|
||||
if (cd.getPresupuesto() == null || !cd.getPresupuesto().getId().equals(presupuesto.getId())) {
|
||||
continue;
|
||||
}
|
||||
if (cd.getUnidades() == null || cd.getUnidades() <= 0) {
|
||||
direccionesPrueba.add(cd.toSkMap(
|
||||
1,
|
||||
presupuesto.getPeso(),
|
||||
false,
|
||||
true));
|
||||
} else {
|
||||
direccionesPresupuesto.add(cd.toSkMap(
|
||||
cd.getUnidades(),
|
||||
presupuesto.getPeso(),
|
||||
cd.getIsPalets(),
|
||||
false));
|
||||
}
|
||||
}
|
||||
if (presupuesto.getServiciosJson() != null
|
||||
&& presupuesto.getServiciosJson().contains("deposito-legal")) {
|
||||
CartDireccion cd = new CartDireccion();
|
||||
direccionesPresupuesto.add(cd.toSkMapDepositoLegal());
|
||||
}
|
||||
}
|
||||
Map<String, Object> direccionesRet = new HashMap<>();
|
||||
direccionesRet.put("direcciones", direccionesPresupuesto);
|
||||
if (!direccionesPrueba.isEmpty())
|
||||
direccionesRet.put("direccionesFP1", direccionesPrueba.get(0));
|
||||
else {
|
||||
direccionesRet.put("direccionesFP1", new ArrayList<>());
|
||||
}
|
||||
return direccionesRet;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
private void saveDireccionesPedidoLinea(
|
||||
Map<String, Object> direcciones,
|
||||
Pedido pedido,
|
||||
PedidoLinea linea, Long direccionFacturacionId) {
|
||||
|
||||
String email = pedido.getCreatedBy().getUserName();
|
||||
|
||||
// direccion prueba
|
||||
if (direcciones.containsKey("direccionesFP1")) {
|
||||
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> fp1 = (Map<String, Object>) direcciones.get("direccionesFP1");
|
||||
@SuppressWarnings("unchecked")
|
||||
PedidoDireccion direccion = saveDireccion(
|
||||
email,
|
||||
false,
|
||||
(HashMap<String, Object>) fp1.get("direccion"),
|
||||
pedido,
|
||||
linea,
|
||||
true,
|
||||
false);
|
||||
pedidoDireccionRepository.save(direccion);
|
||||
} catch (Exception e) {
|
||||
// Viene vacio
|
||||
}
|
||||
}
|
||||
if (direcciones.containsKey("direcciones")) {
|
||||
List<?> dirs = (List<?>) direcciones.get("direcciones");
|
||||
for (Object dir : dirs) {
|
||||
@SuppressWarnings("unchecked")
|
||||
HashMap<String, Object> direccionEnvio = (HashMap<String, Object>) ((HashMap<String, Object>) dir)
|
||||
.get("direccion");
|
||||
if (direccionEnvio.get("cantidad") != null && (Integer) direccionEnvio.get("cantidad") == 4
|
||||
&& direccionEnvio.get("att").toString().contains("Depósito Legal")) {
|
||||
continue; // Saltar la dirección de depósito legal
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
PedidoDireccion direccion = saveDireccion(
|
||||
email,
|
||||
((Number) ((HashMap<String, Object>) dir)
|
||||
.getOrDefault("entregaPalets", 0))
|
||||
.intValue() == 1,
|
||||
(HashMap<String, Object>) ((HashMap<String, Object>) dir).get("direccion"),
|
||||
pedido,
|
||||
linea, false,
|
||||
false);
|
||||
pedidoDireccionRepository.save(direccion);
|
||||
}
|
||||
}
|
||||
if (direccionFacturacionId != null) {
|
||||
Direccion dirFact = direccionService.findById(direccionFacturacionId).orElse(null);
|
||||
if (dirFact != null) {
|
||||
HashMap<String, Object> dirFactMap = new HashMap<>();
|
||||
dirFactMap.put("att", dirFact.getAtt());
|
||||
dirFactMap.put("direccion", dirFact.getDireccion());
|
||||
dirFactMap.put("cp", dirFact.getCp());
|
||||
dirFactMap.put("municipio", dirFact.getCiudad());
|
||||
dirFactMap.put("provincia", dirFact.getProvincia());
|
||||
dirFactMap.put("pais_code3", dirFact.getPaisCode3());
|
||||
dirFactMap.put("telefono", dirFact.getTelefono());
|
||||
dirFactMap.put("instrucciones", dirFact.getInstrucciones());
|
||||
dirFactMap.put("razon_social", dirFact.getRazonSocial());
|
||||
dirFactMap.put("tipo_identificacion_fiscal", dirFact.getTipoIdentificacionFiscal().name());
|
||||
dirFactMap.put("identificacion_fiscal", dirFact.getIdentificacionFiscal());
|
||||
|
||||
PedidoDireccion direccion = saveDireccion(
|
||||
email,
|
||||
false,
|
||||
dirFactMap,
|
||||
pedido,
|
||||
linea,
|
||||
false,
|
||||
true);
|
||||
pedidoDireccionRepository.save(direccion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PedidoDireccion saveDireccion(
|
||||
String email,
|
||||
Boolean palets,
|
||||
HashMap<String, Object> dir,
|
||||
Pedido pedido,
|
||||
PedidoLinea linea,
|
||||
Boolean isEjemplarPrueba,
|
||||
Boolean isFacturacion) {
|
||||
|
||||
PedidoDireccion direccion = new PedidoDireccion();
|
||||
direccion.setEmail(email);
|
||||
direccion.setPalets(isEjemplarPrueba || isFacturacion ? false : palets);
|
||||
direccion.setPedidoLinea(isFacturacion ? null : linea);
|
||||
if (isFacturacion) {
|
||||
direccion.setUnidades(null);
|
||||
direccion.setFacturacion(true);
|
||||
direccion.setPedido(pedido);
|
||||
|
||||
} else {
|
||||
if (isEjemplarPrueba) {
|
||||
direccion.setUnidades(1);
|
||||
direccion.setEjemplarPrueba(true);
|
||||
} else {
|
||||
direccion.setUnidades((Integer) dir.getOrDefault("cantidad", 1));
|
||||
direccion.setEjemplarPrueba(false);
|
||||
}
|
||||
direccion.setFacturacion(false);
|
||||
}
|
||||
|
||||
direccion.setAtt((String) dir.getOrDefault("att", ""));
|
||||
direccion.setDireccion((String) dir.getOrDefault("direccion", ""));
|
||||
direccion.setCp((Integer) dir.getOrDefault("cp", 0));
|
||||
direccion.setCiudad((String) dir.getOrDefault("municipio", ""));
|
||||
direccion.setProvincia((String) dir.getOrDefault("provincia", ""));
|
||||
direccion.setPaisCode3((String) dir.getOrDefault("pais_code3", "esp"));
|
||||
direccion.setTelefono((String) dir.getOrDefault("telefono", ""));
|
||||
direccion.setInstrucciones((String) dir.getOrDefault("instrucciones", ""));
|
||||
direccion.setRazonSocial((String) dir.getOrDefault("razon_social", ""));
|
||||
direccion.setTipoIdentificacionFiscal(Direccion.TipoIdentificacionFiscal
|
||||
.valueOf((String) dir.getOrDefault("tipo_identificacion_fiscal",
|
||||
Direccion.TipoIdentificacionFiscal.DNI.name())));
|
||||
direccion.setIdentificacionFiscal((String) dir.getOrDefault("identificacion_fiscal", ""));
|
||||
|
||||
return direccion;
|
||||
|
||||
}
|
||||
|
||||
private Estado getEstadoInicial(Presupuesto p) {
|
||||
|
||||
if (presupuestoService.hasMaquetacion(p)) {
|
||||
return Estado.maquetacion;
|
||||
} else {
|
||||
return Estado.haciendo_ferro;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,454 @@
|
||||
package com.imprimelibros.erp.pedidos;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import java.security.Principal;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.datatables.DataTable;
|
||||
import com.imprimelibros.erp.datatables.DataTablesParser;
|
||||
import com.imprimelibros.erp.datatables.DataTablesRequest;
|
||||
import com.imprimelibros.erp.datatables.DataTablesResponse;
|
||||
import com.imprimelibros.erp.facturacion.service.FacturacionService;
|
||||
import com.imprimelibros.erp.i18n.TranslationService;
|
||||
import com.imprimelibros.erp.paises.PaisesService;
|
||||
import com.imprimelibros.erp.presupuesto.service.PresupuestoService;
|
||||
import com.imprimelibros.erp.users.UserDao;
|
||||
|
||||
import jakarta.persistence.criteria.Join;
|
||||
import jakarta.persistence.criteria.JoinType;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/pedidos")
|
||||
public class PedidosController {
|
||||
|
||||
private final PresupuestoService presupuestoService;
|
||||
|
||||
private final PedidoRepository repoPedido;
|
||||
private final PedidoService pedidoService;
|
||||
private final UserDao repoUser;
|
||||
private final MessageSource messageSource;
|
||||
private final PedidoLineaRepository repoPedidoLinea;
|
||||
private final PaisesService paisesService;
|
||||
private final TranslationService translationService;
|
||||
private final FacturacionService facturacionService;
|
||||
|
||||
public PedidosController(PedidoRepository repoPedido, PedidoService pedidoService, UserDao repoUser,
|
||||
MessageSource messageSource, TranslationService translationService,
|
||||
PedidoLineaRepository repoPedidoLinea, PaisesService paisesService,
|
||||
FacturacionService facturacionService, PresupuestoService presupuestoService) {
|
||||
this.repoPedido = repoPedido;
|
||||
this.pedidoService = pedidoService;
|
||||
this.repoUser = repoUser;
|
||||
this.messageSource = messageSource;
|
||||
this.translationService = translationService;
|
||||
this.repoPedidoLinea = repoPedidoLinea;
|
||||
this.paisesService = paisesService;
|
||||
this.facturacionService = facturacionService;
|
||||
this.presupuestoService = presupuestoService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public String listarPedidos(Model model, Locale locale) {
|
||||
|
||||
List<String> keys = List.of(
|
||||
"app.cancelar",
|
||||
"app.seleccionar",
|
||||
"app.yes",
|
||||
"checkout.payment.card",
|
||||
"checkout.payment.bizum",
|
||||
"checkout.payment.bank-transfer",
|
||||
"checkout.error.select-method");
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
|
||||
if (Utils.isCurrentUserAdmin()) {
|
||||
return "imprimelibros/pedidos/pedidos-list";
|
||||
}
|
||||
return "imprimelibros/pedidos/pedidos-list-cliente";
|
||||
}
|
||||
|
||||
@GetMapping(value = "datatable", produces = "application/json")
|
||||
@ResponseBody
|
||||
public DataTablesResponse<Map<String, Object>> getDatatable(
|
||||
HttpServletRequest request,
|
||||
Principal principal,
|
||||
Locale locale) {
|
||||
|
||||
DataTablesRequest dt = DataTablesParser.from(request);
|
||||
|
||||
Boolean isAdmin = Utils.isCurrentUserAdmin();
|
||||
Long currentUserId = Utils.currentUserId(principal);
|
||||
|
||||
List<String> searchable = List.of(
|
||||
"id");
|
||||
|
||||
// Campos ordenables
|
||||
List<String> orderable = List.of(
|
||||
"id",
|
||||
"createdBy.fullName",
|
||||
"createdAt",
|
||||
"total",
|
||||
"estado");
|
||||
|
||||
Specification<Pedido> base = (root, query, cb) -> cb.conjunction();
|
||||
if (!isAdmin) {
|
||||
base = base.and((root, query, cb) -> cb.equal(root.get("createdBy").get("id"), currentUserId));
|
||||
}
|
||||
String clientSearch = dt.getColumnSearch("cliente");
|
||||
String estadoSearch = dt.getColumnSearch("estado");
|
||||
|
||||
// 2) Si hay filtro, traducirlo a userIds y añadirlo al Specification
|
||||
if (clientSearch != null) {
|
||||
List<Long> userIds = repoUser.findIdsByFullNameLike(clientSearch.trim());
|
||||
|
||||
if (userIds.isEmpty()) {
|
||||
// Ningún usuario coincide → forzamos 0 resultados
|
||||
base = base.and((root, query, cb) -> cb.disjunction());
|
||||
} else {
|
||||
base = base.and((root, query, cb) -> root.get("createdBy").in(userIds));
|
||||
}
|
||||
}
|
||||
if (estadoSearch != null && !estadoSearch.isBlank()) {
|
||||
try {
|
||||
PedidoLinea.Estado estadoEnum = PedidoLinea.Estado.valueOf(estadoSearch.trim());
|
||||
|
||||
base = base.and((root, query, cb) -> {
|
||||
// Evitar duplicados de pedidos si el provider usa joins
|
||||
if (Pedido.class.equals(query.getResultType())) {
|
||||
query.distinct(true);
|
||||
}
|
||||
|
||||
Join<Pedido, PedidoLinea> lineas = root.join("lineas", JoinType.INNER);
|
||||
return cb.equal(lineas.get("estado"), estadoEnum);
|
||||
});
|
||||
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// Valor de estado no válido → forzamos 0 resultados
|
||||
base = base.and((root, query, cb) -> cb.disjunction());
|
||||
}
|
||||
}
|
||||
Long total = repoPedido.count(base);
|
||||
|
||||
return DataTable
|
||||
.of(repoPedido, Pedido.class, dt, searchable)
|
||||
.orderable(orderable)
|
||||
.add("id", Pedido::getId)
|
||||
.add("created_at", pedido -> Utils.formatInstant(pedido.getCreatedAt(), locale))
|
||||
.add("cliente", pedido -> {
|
||||
if (pedido.getCreatedBy() != null) {
|
||||
return pedido.getCreatedBy().getFullName();
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.add("total", pedido -> {
|
||||
if (pedido.getTotal() != null) {
|
||||
return Utils.formatCurrency(pedido.getTotal(), locale);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
})
|
||||
.add("estado", pedido -> {
|
||||
List<PedidoLinea> lineas = repoPedidoLinea.findByPedidoId(pedido.getId());
|
||||
if (lineas.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
// concatenar los estados de las líneas, ordenados por prioridad
|
||||
StringBuilder sb = new StringBuilder();
|
||||
lineas.stream()
|
||||
.map(PedidoLinea::getEstado)
|
||||
.distinct()
|
||||
.sorted(Comparator.comparingInt(PedidoLinea.Estado::getPriority))
|
||||
.forEach(estado -> {
|
||||
if (sb.length() > 0) {
|
||||
sb.append(", ");
|
||||
}
|
||||
sb.append(messageSource.getMessage(estado.getMessageKey(), null, locale));
|
||||
});
|
||||
String text = sb.toString();
|
||||
return text;
|
||||
})
|
||||
.add("actions", pedido -> {
|
||||
String data = "<span class=\'badge bg-success btn-view \' data-id=\'" + pedido.getId()
|
||||
+ "\' style=\'cursor: pointer;\'>"
|
||||
+ messageSource.getMessage("app.view", null, locale) + "</span>";
|
||||
List<PedidoLinea> lineas = repoPedidoLinea.findByPedidoId(pedido.getId());
|
||||
boolean hasDenegadoPago = lineas.stream()
|
||||
.anyMatch(linea -> PedidoLinea.Estado.denegado_pago.equals(linea.getEstado()));
|
||||
if (hasDenegadoPago) {
|
||||
data += " <span class='badge bg-danger btn-pay' data-amount='" + (int) (pedido.getTotal() * 100)
|
||||
+ "' data-id='" + pedido.getId() + "' style='cursor: pointer;'>"
|
||||
+ messageSource.getMessage("app.pay", null, locale) + "</span>";
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.where(base)
|
||||
.toJson(total);
|
||||
|
||||
}
|
||||
|
||||
@GetMapping("/view/{id}")
|
||||
public String verPedido(
|
||||
@PathVariable(name = "id", required = true) Long id,
|
||||
Model model, Locale locale) {
|
||||
|
||||
List<String> keys = List.of(
|
||||
"app.cancelar",
|
||||
"app.yes",
|
||||
"pedido.view.cancel-title",
|
||||
"pedido.view.cancel-text");
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
|
||||
Boolean isAdmin = Utils.isCurrentUserAdmin();
|
||||
if (isAdmin) {
|
||||
model.addAttribute("isAdmin", true);
|
||||
} else {
|
||||
model.addAttribute("isAdmin", false);
|
||||
}
|
||||
|
||||
PedidoDireccion direccionFacturacion = pedidoService.getDireccionFacturacionPedido(id);
|
||||
if (direccionFacturacion != null) {
|
||||
String paisNombre = paisesService.getPaisNombrePorCode3(direccionFacturacion.getPaisCode3(), locale);
|
||||
direccionFacturacion.setPaisNombre(paisNombre);
|
||||
}
|
||||
|
||||
model.addAttribute("direccionFacturacion", direccionFacturacion);
|
||||
|
||||
Boolean showCancel = false;
|
||||
Boolean showDownloadFactura = true;
|
||||
List<Map<String, Object>> lineas = pedidoService.getLineas(id, locale);
|
||||
for (Map<String, Object> linea : lineas) {
|
||||
|
||||
PedidoLinea pedidoLinea = repoPedidoLinea.findById(
|
||||
((Number) linea.get("lineaId")).longValue()).orElse(null);
|
||||
if (pedidoLinea != null) {
|
||||
Map<String, Boolean> buttons = new HashMap<>();
|
||||
if (pedidoLinea.getEstado() != PedidoLinea.Estado.enviado) {
|
||||
showDownloadFactura = false;
|
||||
}
|
||||
if (pedidoLinea.getEstado().getPriority() >= PedidoLinea.Estado.esperando_aceptacion_ferro.getPriority()
|
||||
&& pedidoLinea.getEstado().getPriority() <= PedidoLinea.Estado.produccion.getPriority()) {
|
||||
|
||||
if (pedidoLinea.getEstado() == PedidoLinea.Estado.esperando_aceptacion_ferro) {
|
||||
buttons.put("aceptar_ferro", true);
|
||||
} else {
|
||||
buttons.put("aceptar_ferro", false);
|
||||
}
|
||||
|
||||
Map<String, Object> filesType = pedidoService.getFilesType(pedidoLinea.getId(), locale);
|
||||
if (filesType == null || filesType.get("error") != null) {
|
||||
throw new RuntimeException(
|
||||
messageSource.getMessage("pedido.errors.update-server-error", null, locale));
|
||||
}
|
||||
for (String key : filesType.keySet()) {
|
||||
buttons.put(key, (Integer) filesType.get(key) == 1 ? true : false);
|
||||
}
|
||||
linea.put("buttons", buttons);
|
||||
}
|
||||
|
||||
if (pedidoLinea.getEstado() != PedidoLinea.Estado.cancelado
|
||||
&& pedidoLinea.getEstado() != PedidoLinea.Estado.terminado
|
||||
&& pedidoLinea.getEstado() != PedidoLinea.Estado.enviado) {
|
||||
showCancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
List<PedidoDireccion> dirEntrega = pedidoService.getDireccionesEntregaPedidoLinea(
|
||||
((Number) linea.get("lineaId")).longValue());
|
||||
|
||||
if (dirEntrega != null && !dirEntrega.isEmpty()) {
|
||||
for (PedidoDireccion direccion : dirEntrega) {
|
||||
String paisNombre = paisesService.getPaisNombrePorCode3(direccion.getPaisCode3(), locale);
|
||||
direccion.setPaisNombre(paisNombre);
|
||||
}
|
||||
}
|
||||
linea.put("direccionesEntrega", dirEntrega);
|
||||
|
||||
}
|
||||
Long facturaId = null;
|
||||
if (showDownloadFactura) {
|
||||
facturaId = facturacionService.getFacturaIdFromPedidoId(id);
|
||||
}
|
||||
model.addAttribute("lineas", lineas);
|
||||
model.addAttribute("showCancel", showCancel);
|
||||
if (showDownloadFactura && facturaId != null) {
|
||||
model.addAttribute("facturaId", facturaId);
|
||||
model.addAttribute("showDownloadFactura", showDownloadFactura);
|
||||
}
|
||||
model.addAttribute("id", id);
|
||||
return "imprimelibros/pedidos/pedidos-view";
|
||||
}
|
||||
|
||||
@PostMapping("/cancel/{id}")
|
||||
@ResponseBody
|
||||
public Map<String, Object> cancelPedido(
|
||||
@PathVariable(name = "id", required = true) Long id,
|
||||
Locale locale) {
|
||||
Boolean result = pedidoService.cancelarPedido(id);
|
||||
if (result) {
|
||||
String successMsg = messageSource.getMessage("pedido.success.pedido-cancelado", null, locale);
|
||||
return Map.of(
|
||||
"success", true,
|
||||
"message", successMsg);
|
||||
} else {
|
||||
String errorMsg = messageSource.getMessage("pedido.errors.cancel-pedido", null, locale);
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------
|
||||
// Acciones sobre las lineas de pedido
|
||||
// -------------------------------------
|
||||
@PostMapping("/linea/{id}/update-status")
|
||||
@ResponseBody
|
||||
public Map<String, Object> updateStatus(
|
||||
@PathVariable(name = "id", required = true) Long id, Locale locale) {
|
||||
|
||||
Map<String, Object> result = pedidoService.actualizarEstado(id, locale);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@PostMapping("/linea/{id}/update-maquetacion")
|
||||
@ResponseBody
|
||||
public Map<String, Object> updateMaquetacion(
|
||||
@PathVariable(name = "id", required = true) Long id,
|
||||
Locale locale) {
|
||||
|
||||
PedidoLinea entity = repoPedidoLinea.findById(id).orElse(null);
|
||||
if (entity == null) {
|
||||
String errorMsg = messageSource.getMessage("pedido.errors.linea-not-found", null, locale);
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", errorMsg);
|
||||
}
|
||||
|
||||
if (entity.getEstado() != PedidoLinea.Estado.maquetacion) {
|
||||
String errorMsg = messageSource.getMessage("pedido.errors.state-error", null, locale);
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", errorMsg);
|
||||
}
|
||||
|
||||
entity.setEstado(PedidoLinea.Estado.haciendo_ferro);
|
||||
repoPedidoLinea.save(entity);
|
||||
String successMsg = messageSource.getMessage("pedido.success.estado-actualizado", null, locale);
|
||||
return Map.of(
|
||||
"success", true,
|
||||
"message", successMsg,
|
||||
"state", messageSource.getMessage(entity.getEstado().getMessageKey(), null, locale));
|
||||
}
|
||||
|
||||
@GetMapping("/linea/{id}/download-ferro")
|
||||
public ResponseEntity<Resource> downloadFerro(@PathVariable(name = "id", required = true) Long id, Locale locale) {
|
||||
|
||||
byte[] ferroFileContent = pedidoService.getFerroFileContent(id, locale);
|
||||
if (ferroFileContent == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
ByteArrayResource resource = new ByteArrayResource(ferroFileContent);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=ferro_" + id + ".pdf")
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
@GetMapping("/linea/{id}/download-cub")
|
||||
public ResponseEntity<Resource> downloadCubierta(@PathVariable(name = "id", required = true) Long id,
|
||||
Locale locale) {
|
||||
|
||||
byte[] cubFileContent = pedidoService.getCubiertaFileContent(id, locale);
|
||||
if (cubFileContent == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
ByteArrayResource resource = new ByteArrayResource(cubFileContent);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=cubierta_" + id + ".pdf")
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
@GetMapping("/linea/{id}/download-tapa")
|
||||
public ResponseEntity<Resource> downloadTapa(@PathVariable(name = "id", required = true) Long id, Locale locale) {
|
||||
|
||||
byte[] tapaFileContent = pedidoService.getTapaFileContent(id, locale);
|
||||
if (tapaFileContent == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
ByteArrayResource resource = new ByteArrayResource(tapaFileContent);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=tapa_" + id + ".pdf")
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(resource);
|
||||
}
|
||||
|
||||
@PostMapping("/linea/{id}/aceptar-ferro")
|
||||
@ResponseBody
|
||||
public Map<String, Object> aceptarFerro(@PathVariable(name = "id", required = true) Long id,
|
||||
Locale locale) {
|
||||
|
||||
PedidoLinea entity = repoPedidoLinea.findById(id).orElse(null);
|
||||
if (entity == null) {
|
||||
String errorMsg = messageSource.getMessage("pedido.errors.linea-not-found", null, locale);
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", errorMsg);
|
||||
}
|
||||
|
||||
if (entity.getEstado() != PedidoLinea.Estado.esperando_aceptacion_ferro) {
|
||||
String errorMsg = messageSource.getMessage("pedido.errors.state-error", null, locale);
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", errorMsg);
|
||||
}
|
||||
|
||||
Boolean result = pedidoService.aceptarFerro(id, locale);
|
||||
|
||||
if (result) {
|
||||
String successMsg = messageSource.getMessage("pedido.success.estado-actualizado", null, locale);
|
||||
return Map.of(
|
||||
"success", true,
|
||||
"message", successMsg,
|
||||
"state", messageSource.getMessage(entity.getEstado().getMessageKey(), null, locale));
|
||||
} else {
|
||||
String errorMsg = messageSource.getMessage("pedido.errors.update-server-error", null, locale);
|
||||
return Map.of(
|
||||
"success", false,
|
||||
"message", errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -4,6 +4,7 @@ import org.springframework.stereotype.Controller;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.ui.Model;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
@ -13,6 +14,8 @@ import java.util.Optional;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@ -51,6 +54,7 @@ import com.imprimelibros.erp.users.UserDao;
|
||||
import com.imprimelibros.erp.users.UserDetailsImpl;
|
||||
import com.imprimelibros.erp.presupuesto.service.PresupuestoFormDataMapper;
|
||||
import com.imprimelibros.erp.presupuesto.service.PresupuestoFormDataMapper.PresupuestoFormDataDto;
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.common.web.IpUtils;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@ -61,6 +65,8 @@ import jakarta.validation.Valid;
|
||||
@RequestMapping("/presupuesto")
|
||||
public class PresupuestoController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PresupuestoController.class);
|
||||
|
||||
private final PresupuestoRepository presupuestoRepository;
|
||||
|
||||
@Autowired
|
||||
@ -145,7 +151,9 @@ public class PresupuestoController {
|
||||
return ResponseEntity.badRequest().body(errores);
|
||||
}
|
||||
Map<String, Object> resultado = new HashMap<>();
|
||||
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale));
|
||||
Map<String , Object> datosInterior = apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale);
|
||||
resultado.put("solapas", datosInterior.get("maxSolapas"));
|
||||
resultado.put("lomo", datosInterior.get("lomo"));
|
||||
resultado.putAll(presupuestoService.obtenerOpcionesAcabadosCubierta(presupuesto, locale));
|
||||
return ResponseEntity.ok(resultado);
|
||||
}
|
||||
@ -265,7 +273,10 @@ public class PresupuestoController {
|
||||
}
|
||||
}
|
||||
|
||||
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale));
|
||||
Map<String , Object> datosInterior = apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale);
|
||||
resultado.put("solapas", datosInterior.get("maxSolapas"));
|
||||
resultado.put("lomo", datosInterior.get("lomo"));
|
||||
|
||||
return ResponseEntity.ok(resultado);
|
||||
}
|
||||
|
||||
@ -298,7 +309,10 @@ public class PresupuestoController {
|
||||
presupuesto.setGramajeInterior(Integer.parseInt(opciones.get(0))); // Asignar primera opción
|
||||
}
|
||||
}
|
||||
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale));
|
||||
Map<String , Object> datosInterior = apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale);
|
||||
resultado.put("solapas", datosInterior.get("maxSolapas"));
|
||||
resultado.put("lomo", datosInterior.get("lomo"));
|
||||
|
||||
return ResponseEntity.ok(resultado);
|
||||
}
|
||||
|
||||
@ -321,7 +335,10 @@ public class PresupuestoController {
|
||||
}
|
||||
|
||||
Map<String, Object> resultado = new HashMap<>();
|
||||
resultado.put("solapas", apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale));
|
||||
Map<String , Object> datosInterior = apiClient.getMaxSolapas(presupuestoService.toSkApiRequest(presupuesto), locale);
|
||||
resultado.put("solapas", datosInterior.get("maxSolapas"));
|
||||
resultado.put("lomo", datosInterior.get("lomo"));
|
||||
|
||||
return ResponseEntity.ok(resultado);
|
||||
}
|
||||
|
||||
@ -490,7 +507,8 @@ public class PresupuestoController {
|
||||
String sessionId = request.getSession(true).getId();
|
||||
String ip = IpUtils.getClientIp(request);
|
||||
|
||||
var resumen = presupuestoService.getResumen(p, serviciosList, datosMaquetacion, datosMarcapaginas, save, mode, locale, sessionId, ip);
|
||||
var resumen = presupuestoService.getResumen(p, serviciosList, datosMaquetacion, datosMarcapaginas, save, mode,
|
||||
locale, sessionId, ip);
|
||||
|
||||
return ResponseEntity.ok(resumen);
|
||||
}
|
||||
@ -517,7 +535,27 @@ public class PresupuestoController {
|
||||
"presupuesto.add.cancel",
|
||||
"presupuesto.add.select-client",
|
||||
"presupuesto.add.error.options",
|
||||
"presupuesto.add.error.options-client");
|
||||
"presupuesto.add.error.options-client",
|
||||
"presupuesto.duplicar.title",
|
||||
"presupuesto.duplicar.text",
|
||||
"presupuesto.duplicar.confirm",
|
||||
"presupuesto.duplicar.cancelar",
|
||||
"presupuesto.duplicar.aceptar",
|
||||
"presupuesto.duplicar.required",
|
||||
"presupuesto.duplicar.success.title",
|
||||
"presupuesto.duplicar.success.text",
|
||||
"presupuesto.duplicar.error.title",
|
||||
"presupuesto.duplicar.error.internal",
|
||||
"presupuesto.reimprimir.title",
|
||||
"presupuesto.reimprimir.text",
|
||||
"presupuesto.reimprimir.confirm",
|
||||
"presupuesto.reimprimir.cancelar",
|
||||
"presupuesto.reimprimir.aceptar",
|
||||
"presupuesto.reimprimir.success.title",
|
||||
"presupuesto.reimprimir.success.text",
|
||||
"presupuesto.reimprimir.error.title",
|
||||
"presupuesto.reimprimir.error.internal"
|
||||
);
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
@ -541,7 +579,26 @@ public class PresupuestoController {
|
||||
"presupuesto.exito.guardado",
|
||||
"presupuesto.add.error.save.title",
|
||||
"presupuesto.iva-reducido",
|
||||
"presupuesto.iva-reducido-descripcion");
|
||||
"presupuesto.iva-reducido-descripcion",
|
||||
"presupuesto.duplicar.title",
|
||||
"presupuesto.duplicar.text",
|
||||
"presupuesto.duplicar.confirm",
|
||||
"presupuesto.duplicar.cancelar",
|
||||
"presupuesto.duplicar.aceptar",
|
||||
"presupuesto.duplicar.required",
|
||||
"presupuesto.duplicar.success.title",
|
||||
"presupuesto.duplicar.success.text",
|
||||
"presupuesto.duplicar.error.title",
|
||||
"presupuesto.duplicar.error.internal",
|
||||
"presupuesto.reimprimir.title",
|
||||
"presupuesto.reimprimir.text",
|
||||
"presupuesto.reimprimir.confirm",
|
||||
"presupuesto.reimprimir.cancelar",
|
||||
"presupuesto.reimprimir.aceptar",
|
||||
"presupuesto.reimprimir.success.title",
|
||||
"presupuesto.reimprimir.success.text",
|
||||
"presupuesto.reimprimir.error.title",
|
||||
"presupuesto.reimprimir.error.internal");
|
||||
|
||||
Map<String, String> translations = translationService.getTranslations(locale, keys);
|
||||
model.addAttribute("languageBundle", translations);
|
||||
@ -560,6 +617,20 @@ public class PresupuestoController {
|
||||
return "redirect:/presupuesto";
|
||||
}
|
||||
|
||||
if (presupuestoOpt.get().getEstado() == Presupuesto.Estado.aceptado) {
|
||||
|
||||
Map<String, Object> resumen = presupuestoService.getTextosResumen(
|
||||
presupuestoOpt.get(),
|
||||
Utils.decodeJsonList(presupuestoOpt.get().getServiciosJson()),
|
||||
Utils.decodeJsonMap(presupuestoOpt.get().getDatosMaquetacionJson()),
|
||||
Utils.decodeJsonMap(presupuestoOpt.get().getDatosMarcapaginasJson()),
|
||||
locale);
|
||||
|
||||
model.addAttribute("resumen", resumen);
|
||||
model.addAttribute("presupuesto", presupuestoOpt.get());
|
||||
return "imprimelibros/presupuestos/presupuestador-view";
|
||||
}
|
||||
|
||||
if (!presupuestoService.canAccessPresupuesto(presupuestoOpt.get(), authentication)) {
|
||||
// Añadir mensaje flash para mostrar alerta
|
||||
redirectAttributes.addFlashAttribute("errorMessage",
|
||||
@ -572,13 +643,14 @@ public class PresupuestoController {
|
||||
String path = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
|
||||
.getRequest().getRequestURI();
|
||||
String mode = path.contains("/view/") ? "view" : "edit";
|
||||
if (mode.equals("view")) {
|
||||
if (mode.equals("view") || presupuestoOpt.get().getEstado() != Presupuesto.Estado.borrador) {
|
||||
model.addAttribute("appMode", "view");
|
||||
} else {
|
||||
model.addAttribute("cliente_id", presupuestoOpt.get().getUser().getId());
|
||||
model.addAttribute("appMode", "edit");
|
||||
}
|
||||
model.addAttribute("id", presupuestoOpt.get().getId());
|
||||
model.addAttribute("presupuesto", presupuestoOpt.get());
|
||||
return "imprimelibros/presupuestos/presupuesto-form";
|
||||
}
|
||||
|
||||
@ -621,14 +693,14 @@ public class PresupuestoController {
|
||||
@ResponseBody
|
||||
public DataTablesResponse<Map<String, Object>> datatable(
|
||||
HttpServletRequest request, Authentication auth, Locale locale,
|
||||
@PathVariable("tipo") String tipo) {
|
||||
@PathVariable("tipo") String tipo, Principal principal) {
|
||||
|
||||
DataTablesRequest dt = DataTablesParser.from(request);
|
||||
|
||||
if ("anonimos".equals(tipo)) {
|
||||
return dtService.datatablePublicos(dt, locale);
|
||||
return dtService.datatablePublicos(dt, locale, principal);
|
||||
} else if ("clientes".equals(tipo)) {
|
||||
return dtService.datatablePrivados(dt, locale);
|
||||
return dtService.datatablePrivados(dt, locale, principal);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Tipo de datatable no válido");
|
||||
}
|
||||
@ -756,6 +828,7 @@ public class PresupuestoController {
|
||||
return ResponseEntity.ok(Map.of("id", saveResult.get("presupuesto_id"),
|
||||
"message", messageSource.getMessage("presupuesto.exito.guardado", null, locale)));
|
||||
} catch (Exception ex) {
|
||||
log.error("Error al guardar el presupuesto", ex);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("message",
|
||||
messageSource.getMessage("presupuesto.error.save-internal-error", null, locale),
|
||||
@ -764,4 +837,30 @@ public class PresupuestoController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/comentario")
|
||||
@ResponseBody
|
||||
public String actualizarComentario(@PathVariable Long id,
|
||||
@RequestParam String comentario) {
|
||||
presupuestoService.updateComentario(id, comentario);
|
||||
return "OK";
|
||||
}
|
||||
|
||||
@PostMapping("/api/duplicar/{id}")
|
||||
@ResponseBody
|
||||
public Map<String, Object> duplicarPresupuesto(
|
||||
@PathVariable Long id,
|
||||
@RequestParam(name = "titulo", defaultValue = "") String titulo) {
|
||||
|
||||
Long entity = presupuestoService.duplicarPresupuesto(id, titulo);
|
||||
return Map.of("id", entity);
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/api/reimprimir/{id}")
|
||||
@ResponseBody
|
||||
public Map<String, Object> reimprimirPresupuesto(@PathVariable Long id) {
|
||||
|
||||
Long entity = presupuestoService.reimprimirPresupuesto(id);
|
||||
return Map.of("id", entity);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,9 +7,11 @@ import com.imprimelibros.erp.presupuesto.dto.Presupuesto;
|
||||
import jakarta.persistence.criteria.Expression;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
@ -26,18 +28,29 @@ public class PresupuestoDatatableService {
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public DataTablesResponse<Map<String, Object>> datatablePublicos(DataTablesRequest dt, Locale locale) {
|
||||
return commonDataTable(dt, locale, "publico", true);
|
||||
public DataTablesResponse<Map<String, Object>> datatablePublicos(DataTablesRequest dt, Locale locale,
|
||||
Principal principal) {
|
||||
return commonDataTable(dt, locale, "publico", true, principal);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public DataTablesResponse<Map<String, Object>> datatablePrivados(DataTablesRequest dt, Locale locale) {
|
||||
return commonDataTable(dt, locale, "privado", false);
|
||||
public DataTablesResponse<Map<String, Object>> datatablePrivados(DataTablesRequest dt, Locale locale,
|
||||
Principal principal) {
|
||||
return commonDataTable(dt, locale, "privado", false, principal);
|
||||
}
|
||||
|
||||
private DataTablesResponse<Map<String, Object>> commonDataTable(DataTablesRequest dt, Locale locale, String origen,
|
||||
boolean publico) {
|
||||
Long count = repo.findAllByOrigen(Presupuesto.Origen.valueOf(origen)).stream().count();
|
||||
boolean publico, Principal principal) {
|
||||
|
||||
Specification<Presupuesto> base = Specification.allOf(
|
||||
(root, query, cb) -> cb.equal(root.get("origen"), Presupuesto.Origen.valueOf(origen)));
|
||||
|
||||
Boolean isAdmin = Utils.isCurrentUserAdmin();
|
||||
if (!isAdmin) {
|
||||
base = base.and((root, query, cb) -> cb.equal(root.get("user").get("id"), Utils.currentUserId(principal)));
|
||||
}
|
||||
|
||||
Long count = repo.count(base);
|
||||
|
||||
List<String> orderable = List.of(
|
||||
"id", "titulo", "user.fullName", "tipoEncuadernacion", "tipoCubierta", "tipoImpresion",
|
||||
@ -73,7 +86,8 @@ public class PresupuestoDatatableService {
|
||||
.addIf(publico, "ciudad", Presupuesto::getCiudad)
|
||||
.add("updatedAt", p -> formatDate(p.getUpdatedAt(), locale))
|
||||
.addIf(!publico, "user", p -> p.getUser() != null ? p.getUser().getFullName() : "")
|
||||
.add("actions", this::generarBotones)
|
||||
.add("actions", p -> generarBotones(p, locale))
|
||||
.where(base)
|
||||
.toJson(count);
|
||||
}
|
||||
|
||||
@ -101,18 +115,27 @@ public class PresupuestoDatatableService {
|
||||
return df.format(instant);
|
||||
}
|
||||
|
||||
private String generarBotones(Presupuesto p) {
|
||||
private String generarBotones(Presupuesto p, Locale locale) {
|
||||
boolean borrador = p.getEstado() == Presupuesto.Estado.borrador;
|
||||
String id = String.valueOf(p.getId());
|
||||
String editBtn = "<a href=\"javascript:void(0);\" data-id=\"" + id + "\" class=\"link-success btn-edit-" +
|
||||
(p.getOrigen().equals(Presupuesto.Origen.publico) ? "anonimo" : "privado") + " fs-15\"><i class=\"ri-" +
|
||||
(p.getOrigen().equals(Presupuesto.Origen.publico) ? "eye" : "pencil") + "-line\"></i></a>";
|
||||
(p.getOrigen().equals(Presupuesto.Origen.publico) || p.getEstado() == Presupuesto.Estado.aceptado ? "eye" : "pencil") + "-line\" " +
|
||||
"data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"" +
|
||||
msg(p.getEstado() == Presupuesto.Estado.aceptado ? "presupuesto.ver" : "presupuesto.editar", locale) + "\"></i></a>";
|
||||
|
||||
String duplicarBtn = !p.getOrigen().equals(Presupuesto.Origen.publico) ? "<a href=\"javascript:void(0);\" data-id=\"" + id
|
||||
+ "\" class=\"link-success btn-duplicate-privado fs-15\"><i class=\"ri-file-copy-2-line\" data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"" +
|
||||
msg("presupuesto.duplicar", locale) + "\"></i></a>" : "";
|
||||
String reimprimirBtn = p.getEstado() == Presupuesto.Estado.aceptado && !p.getOrigen().equals(Presupuesto.Origen.publico) ? "<a href=\"javascript:void(0);\" data-id=\"" + id
|
||||
+ "\" class=\"link-success btn-reprint-privado fs-15\"><i class=\"ri-printer-line\" data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"" +
|
||||
msg("presupuesto.reimprimir", locale) + "\"></i></a>" : "";
|
||||
String deleteBtn = borrador ? "<a href=\"javascript:void(0);\" data-id=\"" + id
|
||||
+ "\" class=\"link-danger btn-delete-"
|
||||
+ (p.getOrigen().equals(Presupuesto.Origen.publico) ? "anonimo" : "privado")
|
||||
+ " fs-15\"><i class=\"ri-delete-bin-5-line\"></i></a>" : "";
|
||||
+ " fs-15\"><i class=\"ri-delete-bin-5-line\" data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"" +
|
||||
msg("presupuesto.borrar", locale) + "\"></i></a>" : "";
|
||||
|
||||
return "<div class=\"hstack gap-3 flex-wrap\">" + editBtn + deleteBtn + "</div>";
|
||||
return "<div class=\"hstack gap-3 flex-wrap\">" + editBtn + duplicarBtn + reimprimirBtn + deleteBtn + "</div>";
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ public class PresupuestoPapeles {
|
||||
);
|
||||
|
||||
private static final Map<String, String> CABEZADA_COLOR_KEYS = Map.of(
|
||||
"NOCABE", "presupuesto.cabezada-sin-cabezada",
|
||||
"WHI", "presupuesto.cabezada-blanca",
|
||||
"GRE", "presupuesto.cabezada-verde",
|
||||
"BLUE", "presupuesto.cabezada-azul",
|
||||
|
||||
@ -99,23 +99,27 @@ public class Presupuesto extends AbstractAuditedEntity implements Cloneable {
|
||||
modificado("presupuesto.estado.modificado");
|
||||
|
||||
private final String messageKey;
|
||||
|
||||
Estado(String messageKey) {
|
||||
this.messageKey = messageKey;
|
||||
}
|
||||
|
||||
public String getMessageKey() {
|
||||
return messageKey;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Entrega{
|
||||
public enum Entrega {
|
||||
peninsula("presupuesto.entrega.peninsula"),
|
||||
canarias("presupuesto.entrega.canarias"),
|
||||
paises_ue("presupuesto.entrega.paises-ue");
|
||||
|
||||
private final String messageKey;
|
||||
|
||||
Entrega(String messageKey) {
|
||||
this.messageKey = messageKey;
|
||||
}
|
||||
|
||||
public String getMessageKey() {
|
||||
return messageKey;
|
||||
}
|
||||
@ -371,6 +375,21 @@ public class Presupuesto extends AbstractAuditedEntity implements Cloneable {
|
||||
@Column(name = "alto_faja")
|
||||
private Integer altoFaja = 0;
|
||||
|
||||
@Column(name = "comentario", columnDefinition = "TEXT")
|
||||
private String comentario;
|
||||
|
||||
@Column(name = "proveedor", length = 100)
|
||||
private String proveedor;
|
||||
|
||||
@Column(name = "proveedor_ref1", length = 100)
|
||||
private String proveedorRef1;
|
||||
|
||||
@Column(name = "proveedor_ref2")
|
||||
private Long proveedorRef2;
|
||||
|
||||
@Column(name = "is_reimpresion", nullable = false)
|
||||
private Boolean isReimpresion = false;
|
||||
|
||||
// ====== MÉTODOS AUX ======
|
||||
|
||||
public String resumenPresupuesto() {
|
||||
@ -912,12 +931,76 @@ public class Presupuesto extends AbstractAuditedEntity implements Cloneable {
|
||||
this.altoFaja = altoFaja;
|
||||
}
|
||||
|
||||
public Long getId(){
|
||||
public Long getId() {
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id){
|
||||
public String getComentario() {
|
||||
return comentario;
|
||||
}
|
||||
|
||||
public void setComentario(String comentario) {
|
||||
this.comentario = comentario;
|
||||
}
|
||||
|
||||
public String getProveedor() {
|
||||
return proveedor;
|
||||
}
|
||||
|
||||
public void setProveedor(String proveedor) {
|
||||
this.proveedor = proveedor;
|
||||
}
|
||||
|
||||
public String getProveedorRef1() {
|
||||
return proveedorRef1;
|
||||
}
|
||||
|
||||
public void setProveedorRef1(String proveedorRef1) {
|
||||
this.proveedorRef1 = proveedorRef1;
|
||||
}
|
||||
|
||||
public Long getProveedorRef2() {
|
||||
return proveedorRef2;
|
||||
}
|
||||
|
||||
public void setProveedorRef2(Long proveedorRef2) {
|
||||
this.proveedorRef2 = proveedorRef2;
|
||||
}
|
||||
|
||||
public Boolean getIsReimpresion() {
|
||||
return isReimpresion;
|
||||
}
|
||||
|
||||
public void setIsReimpresion(Boolean isReimpresion) {
|
||||
this.isReimpresion = isReimpresion;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Double getPeso() {
|
||||
// get peso from first element of pricingSnapshotJson (need to parse JSON)
|
||||
// pricingSnapshotJson = {"xxx":{"peso":0.5,...}} is a String
|
||||
if (this.pricingSnapshotJson != null && !this.pricingSnapshotJson.isEmpty()) {
|
||||
try {
|
||||
String json = this.pricingSnapshotJson.trim();
|
||||
int pesoIndex = json.indexOf("\"peso\":");
|
||||
if (pesoIndex != -1) {
|
||||
int startIndex = pesoIndex + 7;
|
||||
int endIndex = json.indexOf(",", startIndex);
|
||||
if (endIndex == -1) {
|
||||
endIndex = json.indexOf("}", startIndex);
|
||||
}
|
||||
String pesoStr = json.substring(startIndex, endIndex).trim();
|
||||
return Double.parseDouble(pesoStr);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// log error
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.common.web.IpUtils;
|
||||
import com.imprimelibros.erp.configurationERP.VariableService;
|
||||
import com.imprimelibros.erp.presupuesto.GeoIpService;
|
||||
@ -71,14 +72,16 @@ public class PresupuestoService {
|
||||
private final skApiClient apiClient;
|
||||
private final GeoIpService geoIpService;
|
||||
private final UserDao userRepo;
|
||||
private final Utils utils;
|
||||
|
||||
public PresupuestoService(PresupuestadorItems presupuestadorItems, PresupuestoFormatter presupuestoFormatter,
|
||||
skApiClient apiClient, GeoIpService geoIpService, UserDao userRepo) {
|
||||
skApiClient apiClient, GeoIpService geoIpService, UserDao userRepo, Utils utils) {
|
||||
this.presupuestadorItems = presupuestadorItems;
|
||||
this.presupuestoFormatter = presupuestoFormatter;
|
||||
this.apiClient = apiClient;
|
||||
this.geoIpService = geoIpService;
|
||||
this.userRepo = userRepo;
|
||||
this.utils = utils;
|
||||
}
|
||||
|
||||
public boolean validateDatosGenerales(int[] tiradas) {
|
||||
@ -290,6 +293,10 @@ public class PresupuestoService {
|
||||
}
|
||||
|
||||
public Map<String, Object> toSkApiRequest(Presupuesto presupuesto) {
|
||||
return toSkApiRequest(presupuesto, false);
|
||||
}
|
||||
|
||||
public Map<String, Object> toSkApiRequest(Presupuesto presupuesto, Boolean toSave) {
|
||||
final int SK_CLIENTE_ID = 1284;
|
||||
final int SK_PAGINAS_CUADERNILLO = 32;
|
||||
|
||||
@ -311,9 +318,28 @@ public class PresupuestoService {
|
||||
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("tipo_impresion_id", this.getTipoImpresionId(presupuesto));
|
||||
body.put("tirada", Arrays.stream(presupuesto.getTiradas())
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList()));
|
||||
Boolean hasDepositoLegal = false;
|
||||
if (presupuesto.getServiciosJson() != null
|
||||
&& presupuesto.getServiciosJson().contains("deposito-legal")) {
|
||||
hasDepositoLegal = true;
|
||||
}
|
||||
|
||||
if (toSave && hasDepositoLegal) {
|
||||
body.put("tirada", Arrays.stream(presupuesto.getTiradas())
|
||||
.filter(Objects::nonNull)
|
||||
.map(tirada -> tirada + 4)
|
||||
.collect(Collectors.toList()));
|
||||
if(presupuesto.getSelectedTirada() != null) {
|
||||
presupuesto.setSelectedTirada(presupuesto.getSelectedTirada());
|
||||
}
|
||||
} else {
|
||||
body.put("tirada", Arrays.stream(presupuesto.getTiradas())
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
body.put("selectedTirada",
|
||||
presupuesto.getSelectedTirada() != null ? presupuesto.getSelectedTirada() : presupuesto.getTirada1());
|
||||
body.put("tamanio", tamanio);
|
||||
body.put("tipo", presupuesto.getTipoEncuadernacion());
|
||||
body.put("clienteId", SK_CLIENTE_ID);
|
||||
@ -325,6 +351,11 @@ public class PresupuestoService {
|
||||
body.put("interior", interior);
|
||||
body.put("cubierta", cubierta);
|
||||
body.put("guardas", null);
|
||||
// Para las reimpresiones
|
||||
if(presupuesto.getIsReimpresion() != null && presupuesto.getIsReimpresion()) {
|
||||
body.put("reimpresion", 1);
|
||||
body.put("iskn", presupuesto.getProveedorRef1());
|
||||
}
|
||||
if (presupuesto.getSobrecubierta()) {
|
||||
Map<String, Object> sobrecubierta = new HashMap<>();
|
||||
sobrecubierta.put("papel", presupuesto.getPapelSobrecubiertaId());
|
||||
@ -343,9 +374,43 @@ public class PresupuestoService {
|
||||
faja.put("alto", presupuesto.getAltoFaja());
|
||||
body.put("faja", faja);
|
||||
}
|
||||
// body.put("servicios", servicios);
|
||||
|
||||
if (toSave) {
|
||||
Map<String, Object> servicios = new HashMap<>();
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("input_data", body);
|
||||
data.put("ferroDigital", 1);
|
||||
data.put("ferro", 0);
|
||||
data.put("marcapaginas", 0);
|
||||
data.put("retractilado5", 0);
|
||||
if (presupuesto.getServiciosJson() != null
|
||||
&& presupuesto.getServiciosJson().indexOf("ejemplar-prueba") > 0) {
|
||||
data.put("prototipo", 1);
|
||||
servicios.put("prototipo", "1");
|
||||
} else {
|
||||
data.put("prototipo", 0);
|
||||
}
|
||||
if (presupuesto.getServiciosJson() != null && presupuesto.getServiciosJson().indexOf("retractilado") > 0) {
|
||||
data.put("retractilado", 1);
|
||||
servicios.put("retractilado", "1");
|
||||
} else {
|
||||
data.put("retractilado", 0);
|
||||
}
|
||||
data.put("ivaReducido", presupuesto.getIvaReducido() ? 1 : 0);
|
||||
data.put("confirmar", 1);
|
||||
Map<String, Object> datosCabecera = new HashMap<>();
|
||||
datosCabecera.put("titulo", presupuesto.getTitulo());
|
||||
datosCabecera.put("autor", presupuesto.getAutor());
|
||||
datosCabecera.put("isbn", presupuesto.getIsbn());
|
||||
datosCabecera.put("coleccion", "");
|
||||
datosCabecera.put("referenciaCliente", presupuesto.getId());
|
||||
data.put("datosCabecera", datosCabecera);
|
||||
body.put("servicios", servicios);
|
||||
return data;
|
||||
|
||||
}
|
||||
return body;
|
||||
|
||||
}
|
||||
|
||||
public Integer getTipoImpresionId(Presupuesto presupuesto) {
|
||||
@ -599,11 +664,11 @@ public class PresupuestoService {
|
||||
.doubleValue();
|
||||
}
|
||||
|
||||
// precio calculado por matrices * num. cols -1 * precio por columna
|
||||
// precio calculado por matrices * num. cols -1 * precio por columna
|
||||
if (presupuestoMaquetacion.getNumColumnas() > 1) {
|
||||
precio = precio.add(precio.multiply(
|
||||
BigDecimal.valueOf(presupuestoMaquetacion.getNumColumnas() - 1))
|
||||
.multiply(BigDecimal.valueOf(price.apply("columnas"))) );
|
||||
.multiply(BigDecimal.valueOf(price.apply("columnas"))));
|
||||
}
|
||||
|
||||
precio = precio
|
||||
@ -877,6 +942,7 @@ public class PresupuestoService {
|
||||
/ Double.parseDouble(servicio.get("units").toString())
|
||||
: servicio.get("price"));
|
||||
servicioData.put("unidades", servicio.get("units"));
|
||||
servicioData.put("id", servicio.get("id"));
|
||||
serviciosExtras.add(servicioData);
|
||||
}
|
||||
}
|
||||
@ -962,6 +1028,7 @@ public class PresupuestoService {
|
||||
resumen.put("iva_importe_4", presupuesto.getIvaImporte4());
|
||||
resumen.put("iva_importe_21", presupuesto.getIvaImporte21());
|
||||
resumen.put("total_con_iva", presupuesto.getTotalConIva());
|
||||
resumen.put("isReimpresion", presupuesto.getIsReimpresion());
|
||||
|
||||
return resumen;
|
||||
}
|
||||
@ -1057,10 +1124,14 @@ public class PresupuestoService {
|
||||
try {
|
||||
// retractilado: recalcular precio
|
||||
if (s.get("id").equals("retractilado")) {
|
||||
double precio_retractilado = obtenerPrecioRetractilado(cantidad) != null
|
||||
? Double.parseDouble(obtenerPrecioRetractilado(cantidad))
|
||||
: 0.0;
|
||||
s.put("price", precio_retractilado);
|
||||
|
||||
String p = obtenerPrecioRetractilado(cantidad);
|
||||
if(p != null){
|
||||
double precio_retractilado = Double.parseDouble(p);
|
||||
s.put("price", precio_retractilado);
|
||||
} else {
|
||||
s.put("price", 0.0);
|
||||
}
|
||||
}
|
||||
// si tiene protitipo, guardamos el valor para el IVA al 4%
|
||||
else if (s.get("id").equals("ejemplar-prueba")) {
|
||||
@ -1078,8 +1149,10 @@ public class PresupuestoService {
|
||||
}
|
||||
}
|
||||
try {
|
||||
presupuesto.setServiciosJson(new ObjectMapper().writeValueAsString(servicios));
|
||||
if(presupuesto.getSelectedTirada() != null && presupuesto.getSelectedTirada().equals(tirada))
|
||||
presupuesto.setServiciosJson(new ObjectMapper().writeValueAsString(servicios));
|
||||
} catch (Exception ignore) {
|
||||
System.out.println("Error guardando servicios JSON: " + ignore.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1159,6 +1232,18 @@ public class PresupuestoService {
|
||||
HashMap<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
|
||||
Presupuesto presupuestoExistente = null;
|
||||
if (id != null) {
|
||||
presupuestoExistente = presupuestoRepository.findById(id).orElse(null);
|
||||
}
|
||||
if (presupuestoExistente != null) {
|
||||
// merge de datos que no están en el formulario
|
||||
presupuesto.setIsReimpresion(presupuestoExistente.getIsReimpresion());
|
||||
presupuesto.setProveedor(presupuestoExistente.getProveedor());
|
||||
presupuesto.setProveedorRef1(presupuestoExistente.getProveedorRef1());
|
||||
presupuesto.setProveedorRef2(presupuestoExistente.getProveedorRef2());
|
||||
}
|
||||
|
||||
presupuesto.setDatosMaquetacionJson(
|
||||
datosMaquetacion != null ? new ObjectMapper().writeValueAsString(datosMaquetacion) : null);
|
||||
presupuesto.setDatosMarcapaginasJson(
|
||||
@ -1247,6 +1332,93 @@ public class PresupuestoService {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public Boolean hasMaquetacion(Presupuesto presupuesto) {
|
||||
if (presupuesto.getServiciosJson() != null && !presupuesto.getServiciosJson().isEmpty()) {
|
||||
if(presupuesto.getServiciosJson().contains("maquetacion")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Map<String, Object> getPresupuestoInfoForCard(Presupuesto presupuesto, Locale locale) {
|
||||
|
||||
Map<String, Object> resumen = new HashMap<>();
|
||||
|
||||
resumen.put("titulo", presupuesto.getTitulo());
|
||||
|
||||
resumen.put("imagen",
|
||||
"/assets/images/imprimelibros/presupuestador/" + presupuesto.getTipoEncuadernacion() + ".png");
|
||||
resumen.put("imagen_alt",
|
||||
messageSource.getMessage("presupuesto." + presupuesto.getTipoEncuadernacion(), null, locale));
|
||||
|
||||
resumen.put("presupuestoId", presupuesto.getId());
|
||||
|
||||
if (presupuesto.getServiciosJson() != null && presupuesto.getServiciosJson().contains("ejemplar-prueba")) {
|
||||
resumen.put("hasSample", true);
|
||||
} else {
|
||||
resumen.put("hasSample", false);
|
||||
}
|
||||
Map<String, Object> detalles = utils.getTextoPresupuesto(presupuesto, locale);
|
||||
|
||||
resumen.put("tirada", presupuesto.getSelectedTirada());
|
||||
|
||||
resumen.put("baseTotal", Utils.formatCurrency(presupuesto.getBaseImponible(), locale));
|
||||
resumen.put("base", presupuesto.getBaseImponible());
|
||||
resumen.put("iva4", presupuesto.getIvaImporte4());
|
||||
resumen.put("iva21", presupuesto.getIvaImporte21());
|
||||
resumen.put("total", Utils.formatCurrency(presupuesto.getTotalConIva(), locale));
|
||||
|
||||
resumen.put("resumen", detalles);
|
||||
|
||||
return resumen;
|
||||
}
|
||||
|
||||
public Presupuesto findPresupuestoById(Long id) {
|
||||
return presupuestoRepository.findById(id).orElse(null);
|
||||
}
|
||||
|
||||
public void updateComentario(Long presupuestoId, String comentario) {
|
||||
Presupuesto presupuesto = presupuestoRepository.findById(presupuestoId).orElse(null);
|
||||
if (presupuesto != null) {
|
||||
presupuesto.setComentario(comentario);
|
||||
presupuestoRepository.saveAndFlush(presupuesto);
|
||||
}
|
||||
}
|
||||
|
||||
public long duplicarPresupuesto(Long presupuestoId, String titulo) {
|
||||
|
||||
Presupuesto presupuesto = presupuestoRepository.findById(presupuestoId).orElse(null);
|
||||
if (presupuesto != null) {
|
||||
Presupuesto nuevo = presupuesto.clone();
|
||||
nuevo.setId(null); // para que se genere uno nuevo
|
||||
nuevo.setEstado(Presupuesto.Estado.borrador);
|
||||
nuevo.setTitulo(titulo != null && !titulo.isEmpty() ? titulo : "[D] " + presupuesto.getTitulo());
|
||||
nuevo.setIsReimpresion(false);
|
||||
nuevo.setProveedor(null);
|
||||
nuevo.setProveedorRef1(null);
|
||||
nuevo.setProveedorRef2(null);
|
||||
presupuestoRepository.saveAndFlush(nuevo);
|
||||
return nuevo.getId();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public long reimprimirPresupuesto(Long presupuestoId) {
|
||||
|
||||
Presupuesto presupuesto = presupuestoRepository.findById(presupuestoId).orElse(null);
|
||||
if (presupuesto != null) {
|
||||
Presupuesto nuevo = presupuesto.clone();
|
||||
nuevo.setId(null); // para que se genere uno nuevo
|
||||
nuevo.setEstado(Presupuesto.Estado.borrador);
|
||||
nuevo.setTitulo("[R] " + presupuesto.getTitulo());
|
||||
nuevo.setIsReimpresion(true);
|
||||
presupuestoRepository.saveAndFlush(nuevo);
|
||||
return nuevo.getId();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// Métodos privados
|
||||
|
||||
308
src/main/java/com/imprimelibros/erp/redsys/RedsysController.java
Normal file
308
src/main/java/com/imprimelibros/erp/redsys/RedsysController.java
Normal file
@ -0,0 +1,308 @@
|
||||
package com.imprimelibros.erp.redsys;
|
||||
|
||||
import com.imprimelibros.erp.common.Utils;
|
||||
import com.imprimelibros.erp.payments.PaymentService;
|
||||
import com.imprimelibros.erp.payments.model.Payment;
|
||||
import com.imprimelibros.erp.pedidos.Pedido;
|
||||
import com.imprimelibros.erp.pedidos.PedidoService;
|
||||
import com.imprimelibros.erp.redsys.RedsysService.FormPayload;
|
||||
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.thymeleaf.context.WebContext;
|
||||
import org.thymeleaf.web.IWebExchange;
|
||||
import org.thymeleaf.web.servlet.JakartaServletWebApplication;
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/pagos/redsys")
|
||||
public class RedsysController {
|
||||
|
||||
private final PaymentService paymentService;
|
||||
private final MessageSource messageSource;
|
||||
private final SpringTemplateEngine templateEngine;
|
||||
private final ServletContext servletContext;
|
||||
private final PedidoService pedidoService;
|
||||
|
||||
public RedsysController(PaymentService paymentService, MessageSource messageSource,
|
||||
SpringTemplateEngine templateEngine, ServletContext servletContext,
|
||||
PedidoService pedidoService) {
|
||||
this.paymentService = paymentService;
|
||||
this.messageSource = messageSource;
|
||||
this.templateEngine = templateEngine;
|
||||
this.servletContext = servletContext;
|
||||
this.pedidoService = pedidoService;
|
||||
}
|
||||
|
||||
@PostMapping(value = "/crear", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
@ResponseBody
|
||||
public ResponseEntity<byte[]> crearPago(@RequestParam("amountCents") Long amountCents,
|
||||
@RequestParam("method") String method, @RequestParam("cartId") Long cartId,
|
||||
@RequestParam(value = "dirFactId", required = false) Long dirFactId,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response, Locale locale)
|
||||
throws Exception {
|
||||
|
||||
// Creamos el pedido inteno
|
||||
Pedido order = pedidoService.crearPedido(cartId, dirFactId, null, null);
|
||||
|
||||
if ("bank-transfer".equalsIgnoreCase(method)) {
|
||||
|
||||
// 1) Creamos el Payment interno SIN orderId (null)
|
||||
Payment p = paymentService.createBankTransferPayment(cartId, dirFactId, amountCents, "EUR", locale,
|
||||
order.getId());
|
||||
|
||||
pedidoService.markPedidoAsProcesingPayment(order.getId());
|
||||
|
||||
// 1️⃣ Crear la "aplicación" web de Thymeleaf (Jakarta)
|
||||
JakartaServletWebApplication app = JakartaServletWebApplication.buildApplication(servletContext);
|
||||
|
||||
// 2️⃣ Construir el intercambio web desde request/response
|
||||
response.setContentType("text/html;charset=UTF-8");
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
IWebExchange exchange = app.buildExchange(request, response);
|
||||
|
||||
// 3️⃣ Crear el contexto WebContext con Locale
|
||||
WebContext ctx = new WebContext(exchange, locale);
|
||||
|
||||
String importeFormateado = Utils.formatCurrency(amountCents / 100.0, locale);
|
||||
ctx.setVariable("importe", importeFormateado);
|
||||
ctx.setVariable("concepto", "TRANSF-" + p.getOrderId());
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
boolean isAuth = auth != null
|
||||
&& auth.isAuthenticated()
|
||||
&& !(auth instanceof AnonymousAuthenticationToken);
|
||||
ctx.setVariable("isAuth", isAuth);
|
||||
|
||||
// 3) Renderizamos la plantilla a HTML
|
||||
String html = templateEngine.process("imprimelibros/pagos/transfer", ctx);
|
||||
|
||||
byte[] body = html.getBytes(StandardCharsets.UTF_8);
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.TEXT_HTML)
|
||||
.body(body);
|
||||
}
|
||||
|
||||
// Tarjeta o Bizum (Redsys)
|
||||
FormPayload form = paymentService.createRedsysPayment(cartId, dirFactId, amountCents, "EUR", method,
|
||||
order.getId());
|
||||
|
||||
String html = """
|
||||
<html><head><meta charset="utf-8"><title>Redirigiendo a Redsys…</title></head>
|
||||
<body onload="document.forms[0].submit()">
|
||||
<form action="%s" method="post">
|
||||
<input type="hidden" name="Ds_SignatureVersion" value="%s"/>
|
||||
<input type="hidden" name="Ds_MerchantParameters" value="%s"/>
|
||||
<input type="hidden" name="Ds_Signature" value="%s"/>
|
||||
<input type="hidden" name="cartId" value="%d"/>
|
||||
<noscript>
|
||||
<p>Haz clic en pagar para continuar</p>
|
||||
<button type="submit">Pagar</button>
|
||||
</noscript>
|
||||
</form>
|
||||
</body></html>
|
||||
""".formatted(
|
||||
form.action(),
|
||||
form.signatureVersion(),
|
||||
form.merchantParameters(),
|
||||
form.signature(), cartId);
|
||||
|
||||
byte[] body = html.getBytes(StandardCharsets.UTF_8);
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.TEXT_HTML)
|
||||
.body(body);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/reintentar", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
@ResponseBody
|
||||
public ResponseEntity<byte[]> reintentarPago(@RequestParam("amountCents") Long amountCents,
|
||||
@RequestParam("method") String method, @RequestParam("orderId") Long orderId,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response, Locale locale)
|
||||
throws Exception {
|
||||
|
||||
// Creamos el pedido inteno
|
||||
Pedido order = pedidoService.findById(orderId);
|
||||
|
||||
// Find the payment with orderId = order.getId() and status = failed
|
||||
Payment failedPayment = paymentService.findFailedPaymentByOrderId(order.getId());
|
||||
if (failedPayment == null) {
|
||||
throw new Exception("No se encontró un pago fallido para el pedido " + order.getId());
|
||||
}
|
||||
|
||||
Long cartId = null;
|
||||
Long dirFactId = null;
|
||||
// Find payment transaction details from failedPayment if needed
|
||||
try {
|
||||
Map<String, Long> transactionDetails = paymentService.getPaymentTransactionData(failedPayment.getId());
|
||||
cartId = transactionDetails.get("cartId");
|
||||
dirFactId = transactionDetails.get("dirFactId");
|
||||
} catch (Exception e) {
|
||||
throw new Exception(
|
||||
"No se pudieron obtener los detalles de la transacción para el pago " + failedPayment.getId());
|
||||
}
|
||||
|
||||
if ("bank-transfer".equalsIgnoreCase(method)) {
|
||||
|
||||
// 1) Creamos el Payment interno SIN orderId (null)
|
||||
Payment p = paymentService.createBankTransferPayment(cartId, dirFactId, amountCents, "EUR", locale,
|
||||
order.getId());
|
||||
|
||||
pedidoService.markPedidoAsProcesingPayment(order.getId());
|
||||
|
||||
// 1️⃣ Crear la "aplicación" web de Thymeleaf (Jakarta)
|
||||
JakartaServletWebApplication app = JakartaServletWebApplication.buildApplication(servletContext);
|
||||
|
||||
// 2️⃣ Construir el intercambio web desde request/response
|
||||
response.setContentType("text/html;charset=UTF-8");
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
IWebExchange exchange = app.buildExchange(request, response);
|
||||
|
||||
// 3️⃣ Crear el contexto WebContext con Locale
|
||||
WebContext ctx = new WebContext(exchange, locale);
|
||||
|
||||
String importeFormateado = Utils.formatCurrency(amountCents / 100.0, locale);
|
||||
ctx.setVariable("importe", importeFormateado);
|
||||
ctx.setVariable("concepto", "TRANSF-" + p.getOrderId());
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
boolean isAuth = auth != null
|
||||
&& auth.isAuthenticated()
|
||||
&& !(auth instanceof AnonymousAuthenticationToken);
|
||||
ctx.setVariable("isAuth", isAuth);
|
||||
|
||||
// 3) Renderizamos la plantilla a HTML
|
||||
String html = templateEngine.process("imprimelibros/pagos/transfer", ctx);
|
||||
|
||||
byte[] body = html.getBytes(StandardCharsets.UTF_8);
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.TEXT_HTML)
|
||||
.body(body);
|
||||
}
|
||||
|
||||
// Tarjeta o Bizum (Redsys)
|
||||
FormPayload form = paymentService.createRedsysPayment(cartId, dirFactId, amountCents, "EUR", method,
|
||||
order.getId());
|
||||
|
||||
String html = """
|
||||
<html><head><meta charset="utf-8"><title>Redirigiendo a Redsys…</title></head>
|
||||
<body onload="document.forms[0].submit()">
|
||||
<form action="%s" method="post">
|
||||
<input type="hidden" name="Ds_SignatureVersion" value="%s"/>
|
||||
<input type="hidden" name="Ds_MerchantParameters" value="%s"/>
|
||||
<input type="hidden" name="Ds_Signature" value="%s"/>
|
||||
<input type="hidden" name="cartId" value="%d"/>
|
||||
<noscript>
|
||||
<p>Haz clic en pagar para continuar</p>
|
||||
<button type="submit">Pagar</button>
|
||||
</noscript>
|
||||
</form>
|
||||
</body></html>
|
||||
""".formatted(
|
||||
form.action(),
|
||||
form.signatureVersion(),
|
||||
form.merchantParameters(),
|
||||
form.signature(), cartId);
|
||||
|
||||
byte[] body = html.getBytes(StandardCharsets.UTF_8);
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.TEXT_HTML)
|
||||
.body(body);
|
||||
}
|
||||
|
||||
// GET: cuando el usuario cae aquí sin parámetros, o Redsys redirige por GET
|
||||
@GetMapping("/ok")
|
||||
public String okGet(RedirectAttributes redirectAttrs, Model model, Locale locale) {
|
||||
return "imprimelibros/pagos/pago-ok";
|
||||
}
|
||||
|
||||
// POST: si Redsys envía Ds_Signature y Ds_MerchantParameters (muchas
|
||||
// integraciones ni lo usan)
|
||||
@PostMapping(value = "/ok", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
@ResponseBody
|
||||
public ResponseEntity<String> okPost(@RequestParam("Ds_Signature") String signature,
|
||||
@RequestParam("Ds_MerchantParameters") String merchantParameters, Locale locale) {
|
||||
try {
|
||||
// opcional: idempotente, si /notify ya ha hecho el trabajo no pasa nada
|
||||
paymentService.handleRedsysNotification(signature, merchantParameters, locale);
|
||||
return ResponseEntity.ok("<h2>Pago realizado correctamente</h2><a href=\"/cart\">Volver</a>");
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body("<h2>Error validando pago</h2><pre>" + e.getMessage() + "</pre>");
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/ko")
|
||||
public String koGet(RedirectAttributes redirectAttrs, Model model, Locale locale) {
|
||||
|
||||
String msg = messageSource.getMessage("checkout.error.payment", null,
|
||||
"Error al procesar el pago: el pago ha sido cancelado o rechazado Por favor, inténtelo de nuevo.",
|
||||
locale);
|
||||
model.addAttribute("errorPago", msg);
|
||||
redirectAttrs.addFlashAttribute("errorPago", msg);
|
||||
return "redirect:/cart";
|
||||
}
|
||||
|
||||
@PostMapping(value = "/ko", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
@ResponseBody
|
||||
public ResponseEntity<String> koPost(
|
||||
@RequestParam("Ds_Signature") String signature,
|
||||
@RequestParam("Ds_MerchantParameters") String merchantParameters, Locale locale) {
|
||||
|
||||
try {
|
||||
// Procesamos la notificación IGUAL que en /ok y /notify
|
||||
paymentService.handleRedsysNotification(signature, merchantParameters, locale);
|
||||
|
||||
// Mensaje para el usuario (pago cancelado/rechazado)
|
||||
String html = "<h2>Pago cancelado o rechazado</h2><a href=\"/checkout\">Volver</a>";
|
||||
return ResponseEntity.ok(html);
|
||||
} catch (Exception e) {
|
||||
// Si algo falla al validar/procesar, lo mostramos (útil en entorno de pruebas)
|
||||
String html = "<h2>Error procesando notificación KO</h2><pre>" + e.getMessage() + "</pre>";
|
||||
return ResponseEntity.badRequest().body(html);
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/notify", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
@ResponseBody
|
||||
public String notifyRedsys(@RequestParam("Ds_Signature") String signature,
|
||||
@RequestParam("Ds_MerchantParameters") String merchantParameters, Locale locale) {
|
||||
try {
|
||||
paymentService.handleRedsysNotification(signature, merchantParameters, locale);
|
||||
return "OK";
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(); // 👈 para ver el motivo del 500 en logs
|
||||
return "ERROR";
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/refund/{paymentId}", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
@ResponseBody
|
||||
public ResponseEntity<String> refund(@PathVariable Long paymentId,
|
||||
@RequestParam("amountCents") Long amountCents) {
|
||||
try {
|
||||
String idem = "refund-" + paymentId + "-" + amountCents + "-" + UUID.randomUUID();
|
||||
paymentService.refundViaRedsys(paymentId, amountCents, idem);
|
||||
return ResponseEntity.ok("{\"success\":true}");
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest().body("{\"success\":false, \"error\": \"" + e.getMessage() + "\"}");
|
||||
}
|
||||
}
|
||||
}
|
||||
376
src/main/java/com/imprimelibros/erp/redsys/RedsysService.java
Normal file
376
src/main/java/com/imprimelibros/erp/redsys/RedsysService.java
Normal file
@ -0,0 +1,376 @@
|
||||
package com.imprimelibros.erp.redsys;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import sis.redsys.api.ApiMacSha256;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
public class RedsysService {
|
||||
|
||||
// ---------- CONFIG ----------
|
||||
@Value("${redsys.url}")
|
||||
private String url;
|
||||
@Value("${redsys.refund.url}")
|
||||
private String urlRefund;
|
||||
@Value("${redsys.merchant-code}")
|
||||
private String merchantCode;
|
||||
@Value("${redsys.terminal}")
|
||||
private String terminal;
|
||||
@Value("${redsys.currency}")
|
||||
private String currency;
|
||||
@Value("${redsys.transaction-type}")
|
||||
private String txType;
|
||||
@Value("${redsys.secret-key}")
|
||||
private String secretKeyBase64;
|
||||
@Value("${redsys.urls.ok}")
|
||||
private String urlOk;
|
||||
@Value("${redsys.urls.ko}")
|
||||
private String urlKo;
|
||||
@Value("${redsys.urls.notify}")
|
||||
private String urlNotify;
|
||||
@Value("${redsys.environment}")
|
||||
private String env;
|
||||
|
||||
private final HttpClient httpClient = HttpClient.newHttpClient();
|
||||
|
||||
// ---------- RECORDS ----------
|
||||
// Pedido a Redsys
|
||||
public record PaymentRequest(String order, long amountCents, String description, Long cartId, Long dirFactId) {
|
||||
}
|
||||
|
||||
// Payload para el formulario
|
||||
public record FormPayload(String action, String signatureVersion, String merchantParameters, String signature) {
|
||||
}
|
||||
|
||||
// ---------- MÉTODO PRINCIPAL (TARJETA) ----------
|
||||
public FormPayload buildRedirectForm(PaymentRequest req) throws Exception {
|
||||
return buildRedirectFormInternal(req, false); // false = tarjeta (sin PAYMETHODS)
|
||||
}
|
||||
|
||||
// ---------- NUEVO: MÉTODO PARA BIZUM ----------
|
||||
public FormPayload buildRedirectFormBizum(PaymentRequest req) throws Exception {
|
||||
return buildRedirectFormInternal(req, true); // true = Bizum (PAYMETHODS = z)
|
||||
}
|
||||
|
||||
// ---------- LÓGICA COMÚN ----------
|
||||
private FormPayload buildRedirectFormInternal(PaymentRequest req, boolean bizum) throws Exception {
|
||||
ApiMacSha256 api = new ApiMacSha256();
|
||||
|
||||
api.setParameter("DS_MERCHANT_AMOUNT", String.valueOf(req.amountCents()));
|
||||
api.setParameter("DS_MERCHANT_ORDER", req.order()); // Usa 12 dígitos con ceros
|
||||
api.setParameter("DS_MERCHANT_MERCHANTCODE", merchantCode);
|
||||
api.setParameter("DS_MERCHANT_CURRENCY", currency);
|
||||
api.setParameter("DS_MERCHANT_TRANSACTIONTYPE", txType);
|
||||
api.setParameter("DS_MERCHANT_TERMINAL", terminal);
|
||||
api.setParameter("DS_MERCHANT_MERCHANTURL", urlNotify);
|
||||
api.setParameter("DS_MERCHANT_URLOK", urlOk);
|
||||
api.setParameter("DS_MERCHANT_URLKO", urlKo);
|
||||
|
||||
// ✅ Añadir contexto adicional (por ejemplo, cartId)
|
||||
// Si tu PaymentRequest no lo lleva todavía, puedes pasarlo en description o
|
||||
// crear otro campo.
|
||||
JSONObject ctx = new JSONObject();
|
||||
ctx.put("cartId", req.cartId());
|
||||
if (req.dirFactId() != null) {
|
||||
ctx.put("dirFactId", req.dirFactId());
|
||||
}
|
||||
api.setParameter("DS_MERCHANT_MERCHANTDATA", ctx.toString());
|
||||
|
||||
if (req.description() != null && !req.description().isBlank()) {
|
||||
api.setParameter("DS_MERCHANT_PRODUCTDESCRIPTION", req.description());
|
||||
}
|
||||
|
||||
// 🔹 Bizum: PAYMETHODS = "z" según Redsys
|
||||
if (bizum) {
|
||||
api.setParameter("DS_MERCHANT_PAYMETHODS", "z");
|
||||
}
|
||||
|
||||
String merchantParameters = api.createMerchantParameters();
|
||||
String signature = api.createMerchantSignature(secretKeyBase64);
|
||||
|
||||
String action = url;
|
||||
/*
|
||||
* ? "https://sis-t.redsys.es:25443/sis/realizarPago"
|
||||
* : "https://sis.redsys.es/sis/realizarPago";
|
||||
*/
|
||||
|
||||
return new FormPayload(action, "HMAC_SHA256_V1", merchantParameters, signature);
|
||||
}
|
||||
|
||||
// ---------- STEP 3: Decodificar Ds_MerchantParameters ----------
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
public Map<String, Object> decodeMerchantParametersToMap(String dsMerchantParametersB64) throws Exception {
|
||||
try {
|
||||
byte[] decoded = Base64.getDecoder().decode(dsMerchantParametersB64);
|
||||
String json = new String(decoded, StandardCharsets.UTF_8);
|
||||
return MAPPER.readValue(json, new TypeReference<>() {
|
||||
});
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("No se pudo decodificar Ds_MerchantParameters", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- STEP 4: Validar notificación ----------
|
||||
public RedsysNotification validateAndParseNotification(String dsSignature, String dsMerchantParametersB64)
|
||||
throws Exception {
|
||||
|
||||
ApiMacSha256 api = new ApiMacSha256();
|
||||
|
||||
// 1) Decodificar Ds_MerchantParameters usando la librería oficial
|
||||
String json = api.decodeMerchantParameters(dsMerchantParametersB64);
|
||||
|
||||
// 2) Convertir a Map para tu modelo
|
||||
Map<String, Object> mp = MAPPER.readValue(json, new TypeReference<>() {
|
||||
});
|
||||
RedsysNotification notif = new RedsysNotification(mp);
|
||||
|
||||
if (notif.order == null || notif.order.isBlank()) {
|
||||
System.out.println("### ATENCIÓN: Ds_Order no viene en MerchantParameters");
|
||||
throw new IllegalArgumentException("Falta Ds_Order en Ds_MerchantParameters");
|
||||
}
|
||||
|
||||
// 3) Calcular firma esperada: clave comercio + MerchantParameters en B64
|
||||
String expected = api.createMerchantSignatureNotif(
|
||||
secretKeyBase64, // 👈 La misma que usas para crear la firma del pago
|
||||
dsMerchantParametersB64 // 👈 SIEMPRE el B64 tal cual llega de Redsys, sin tocar
|
||||
);
|
||||
|
||||
// 4) Comparar firma Redsys vs firma calculada
|
||||
if (!safeEqualsB64(dsSignature, expected)) {
|
||||
System.out.println("### Firma Redsys no válida");
|
||||
System.out.println("Ds_Signature (Redsys) = " + dsSignature);
|
||||
System.out.println("Expected (local) = " + expected);
|
||||
throw new SecurityException("Firma Redsys no válida");
|
||||
}
|
||||
|
||||
return notif;
|
||||
}
|
||||
|
||||
// ---------- HELPERS ----------
|
||||
private static boolean safeEqualsB64(String a, String b) {
|
||||
if (Objects.equals(a, b))
|
||||
return true;
|
||||
try {
|
||||
String na = normalizeB64(a);
|
||||
String nb = normalizeB64(b);
|
||||
byte[] da = Base64.getDecoder().decode(na);
|
||||
byte[] db = Base64.getDecoder().decode(nb);
|
||||
return MessageDigest.isEqual(da, db);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalizeB64(String s) {
|
||||
if (s == null)
|
||||
return "";
|
||||
String n = s.replace('-', '+').replace('_', '/');
|
||||
int mod = n.length() % 4;
|
||||
if (mod == 2)
|
||||
n += "==";
|
||||
else if (mod == 3)
|
||||
n += "=";
|
||||
else if (mod == 1)
|
||||
n += "===";
|
||||
return n;
|
||||
}
|
||||
|
||||
// ---------- MODELO DE NOTIFICACIÓN ----------
|
||||
public static final class RedsysNotification {
|
||||
public final Map<String, Object> raw;
|
||||
public final String order;
|
||||
public final String response;
|
||||
public final long amountCents;
|
||||
public final String currency;
|
||||
public final Long cartId;
|
||||
public final Long dirFactId;
|
||||
public final String processedPayMethod; // Ds_ProcessedPayMethod
|
||||
public final String bizumIdOper; // Ds_Bizum_IdOper
|
||||
public final String authorisationCode; // Ds_AuthorisationCode
|
||||
|
||||
public RedsysNotification(Map<String, Object> raw) {
|
||||
this.raw = raw;
|
||||
this.order = str(raw.get("Ds_Order"));
|
||||
this.response = str(raw.get("Ds_Response"));
|
||||
this.currency = str(raw.get("Ds_Currency"));
|
||||
this.amountCents = parseLongSafe(raw.get("Ds_Amount"));
|
||||
this.cartId = extractCartId(raw.get("Ds_MerchantData"));
|
||||
this.dirFactId = extractDirFactId(raw.get("Ds_MerchantData"));
|
||||
this.processedPayMethod = str(raw.get("Ds_ProcessedPayMethod"));
|
||||
this.bizumIdOper = str(raw.get("Ds_Bizum_IdOper"));
|
||||
this.authorisationCode = str(raw.get("Ds_AuthorisationCode"));
|
||||
}
|
||||
|
||||
private static Long extractCartId(Object merchantDataObj) {
|
||||
if (merchantDataObj == null)
|
||||
return null;
|
||||
try {
|
||||
String json = String.valueOf(merchantDataObj);
|
||||
|
||||
// 👇 DES-ESCAPAR las comillas HTML que vienen de Redsys
|
||||
json = json.replace(""", "\"");
|
||||
|
||||
org.json.JSONObject ctx = new org.json.JSONObject(json);
|
||||
return ctx.optLong("cartId", 0L);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(); // te ayudará si vuelve a fallar
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Long extractDirFactId(Object merchantDataObj) {
|
||||
if (merchantDataObj == null)
|
||||
return null;
|
||||
try {
|
||||
String json = String.valueOf(merchantDataObj);
|
||||
|
||||
// 👇 DES-ESCAPAR las comillas HTML que vienen de Redsys
|
||||
json = json.replace(""", "\"");
|
||||
|
||||
org.json.JSONObject ctx = new org.json.JSONObject(json);
|
||||
long v = ctx.optLong("dirFactId", 0L);
|
||||
return v != 0L ? v : null;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(); // te ayudará si vuelve a fallar
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean authorized() {
|
||||
try {
|
||||
int r = Integer.parseInt(response);
|
||||
return r >= 0 && r <= 99;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isBizum() {
|
||||
// Redsys suele usar 68 para Bizum; ajustable si tu banco usa otro código.
|
||||
return "68".equals(processedPayMethod);
|
||||
}
|
||||
|
||||
private static String str(Object o) {
|
||||
return o == null ? null : String.valueOf(o);
|
||||
}
|
||||
|
||||
private static long parseLongSafe(Object o) {
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(o));
|
||||
} catch (Exception e) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Solicita a Redsys una devolución (TransactionType = 3)
|
||||
*
|
||||
* @param order El mismo Ds_Merchant_Order que se usó en el cobro.
|
||||
* @param amountCents Importe en céntimos a devolver.
|
||||
* @return gatewayRefundId (p.ej. Ds_AuthorisationCode o Ds_Order)
|
||||
*/
|
||||
public String requestRefund(String order, long amountCents) throws Exception {
|
||||
ApiMacSha256 api = new ApiMacSha256();
|
||||
|
||||
// Montar parámetros para el refund
|
||||
api.setParameter("DS_MERCHANT_MERCHANTCODE", merchantCode);
|
||||
api.setParameter("DS_MERCHANT_TERMINAL", terminal);
|
||||
api.setParameter("DS_MERCHANT_ORDER", order);
|
||||
api.setParameter("DS_MERCHANT_AMOUNT", String.valueOf(amountCents));
|
||||
api.setParameter("DS_MERCHANT_CURRENCY", currency);
|
||||
api.setParameter("DS_MERCHANT_TRANSACTIONTYPE", "3"); // 3 = devolución
|
||||
api.setParameter("DS_MERCHANT_MERCHANTURL", "");
|
||||
api.setParameter("DS_MERCHANT_URLOK", "");
|
||||
api.setParameter("DS_MERCHANT_URLKO", "");
|
||||
|
||||
// Crear parámetros y firma (como en tu PHP)
|
||||
String merchantParameters = api.createMerchantParameters();
|
||||
String signature = api.createMerchantSignature(secretKeyBase64);
|
||||
|
||||
// Montar el JSON para Redsys REST
|
||||
String json = """
|
||||
{
|
||||
"Ds_MerchantParameters": "%s",
|
||||
"Ds_Signature": "%s",
|
||||
"Ds_SignatureVersion": "HMAC_SHA256_V1"
|
||||
}
|
||||
""".formatted(merchantParameters, signature);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(urlRefund))
|
||||
.header("Content-Type", "application/json; charset=UTF-8")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(json))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
if (response.statusCode() / 100 != 2)
|
||||
throw new IllegalStateException("HTTP error Redsys refund: " + response.statusCode());
|
||||
|
||||
if (response.body() == null || response.body().isBlank())
|
||||
throw new IllegalStateException("Respuesta vacía de Redsys refund REST");
|
||||
|
||||
// Parsear la respuesta JSON
|
||||
Map<String, Object> respMap = MAPPER.readValue(response.body(), new TypeReference<>() {
|
||||
});
|
||||
|
||||
// Redsys puede devolver "Ds_MerchantParameters" o "errorCode"
|
||||
if (respMap.containsKey("errorCode")) {
|
||||
throw new IllegalStateException("Error Redsys refund: " + respMap.get("errorCode"));
|
||||
}
|
||||
|
||||
String dsMerchantParametersResp = (String) respMap.get("Ds_MerchantParameters");
|
||||
if (dsMerchantParametersResp == null) {
|
||||
throw new IllegalStateException("Respuesta Redsys refund sin Ds_MerchantParameters");
|
||||
}
|
||||
|
||||
// Decodificar MerchantParameters de la respuesta
|
||||
Map<String, Object> decoded = decodeMerchantParametersToMap(dsMerchantParametersResp);
|
||||
|
||||
String dsResponse = String.valueOf(decoded.get("Ds_Response"));
|
||||
if (dsResponse == null) {
|
||||
throw new IllegalStateException("Respuesta Redsys refund sin Ds_Response");
|
||||
}
|
||||
|
||||
int code;
|
||||
try {
|
||||
code = Integer.parseInt(dsResponse);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalStateException("Código Ds_Response no numérico en refund: " + dsResponse, e);
|
||||
}
|
||||
|
||||
// ✅ Consideramos OK: 0–99 (éxito típico) o 900 (0900)
|
||||
boolean ok = (code >= 0 && code <= 99) || code == 900;
|
||||
if (!ok) {
|
||||
throw new IllegalStateException("Devolución rechazada, Ds_Response=" + dsResponse);
|
||||
}
|
||||
|
||||
// Devolvemos algún identificador razonable para la transacción de refund
|
||||
Object authCodeObj = decoded.get("Ds_AuthorisationCode");
|
||||
String authCode = authCodeObj != null ? String.valueOf(authCodeObj).trim() : null;
|
||||
|
||||
if (authCode == null || authCode.isEmpty()) {
|
||||
// Fallback: usa el Ds_Order original como ID de refund
|
||||
return order;
|
||||
}
|
||||
|
||||
return authCode;
|
||||
}
|
||||
|
||||
}
|
||||
@ -359,6 +359,7 @@ public class UserController {
|
||||
@GetMapping(value = "api/get-users", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Map<String, Object> getUsers(
|
||||
@RequestParam(required = false) String role, // puede venir ausente
|
||||
@RequestParam(required = false) Boolean showUsername,
|
||||
@RequestParam(required = false) String q,
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "10") int size) {
|
||||
@ -373,9 +374,15 @@ public class UserController {
|
||||
.map(u -> {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("id", u.getId());
|
||||
m.put("text", (u.getFullName() != null && !u.getFullName().isBlank())
|
||||
? u.getFullName()
|
||||
: u.getUserName());
|
||||
if (showUsername != null && Boolean.TRUE.equals(showUsername)) {
|
||||
m.put("text", (u.getFullName() != null && !u.getFullName().isBlank())
|
||||
? u.getFullName() + " (" + u.getUserName() + ")"
|
||||
: u.getUserName());
|
||||
} else {
|
||||
m.put("text", (u.getFullName() != null && !u.getFullName().isBlank())
|
||||
? u.getFullName()
|
||||
: u.getUserName());
|
||||
}
|
||||
return m;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
@ -385,4 +392,20 @@ public class UserController {
|
||||
"pagination", Map.of("more", more));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping(value = "api/get-user/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Map<String, Object> getUser(@PathVariable Long id) {
|
||||
User u = userService.findById(id);
|
||||
if (u == null) {
|
||||
return Map.of();
|
||||
}
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("id", u.getId());
|
||||
m.put("userName", u.getUserName());
|
||||
m.put("fullName", u.getFullName());
|
||||
return m;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -19,60 +19,63 @@ import org.springframework.lang.Nullable;
|
||||
@Repository
|
||||
public interface UserDao extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
|
||||
|
||||
// Aplicamos EntityGraph a la versión con Specification+Pageable
|
||||
@Override
|
||||
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
||||
@NonNull
|
||||
Page<User> findAll(@Nullable Specification<User> spec, @NonNull Pageable pageable);
|
||||
// Aplicamos EntityGraph a la versión con Specification+Pageable
|
||||
@Override
|
||||
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
||||
@NonNull
|
||||
Page<User> findAll(@Nullable Specification<User> spec, @NonNull Pageable pageable);
|
||||
|
||||
Optional<User> findByUserNameIgnoreCase(String userName);
|
||||
Optional<User> findByUserNameIgnoreCase(String userName);
|
||||
|
||||
boolean existsByUserNameIgnoreCase(String userName);
|
||||
boolean existsByUserNameIgnoreCase(String userName);
|
||||
|
||||
// Para comprobar si existe al hacer signup
|
||||
@Query(value = """
|
||||
SELECT id, deleted, enabled
|
||||
FROM users
|
||||
WHERE LOWER(username) = LOWER(:userName)
|
||||
LIMIT 1
|
||||
""", nativeQuery = true)
|
||||
Optional<UserLite> findLiteByUserNameIgnoreCase(@Param("userName") String userName);
|
||||
// Para comprobar si existe al hacer signup
|
||||
@Query(value = """
|
||||
SELECT id, deleted, enabled
|
||||
FROM users
|
||||
WHERE LOWER(username) = LOWER(:userName)
|
||||
LIMIT 1
|
||||
""", nativeQuery = true)
|
||||
Optional<UserLite> findLiteByUserNameIgnoreCase(@Param("userName") String userName);
|
||||
|
||||
boolean existsByUserNameIgnoreCaseAndIdNot(String userName, Long id);
|
||||
boolean existsByUserNameIgnoreCaseAndIdNot(String userName, Long id);
|
||||
|
||||
// Nuevo: para login/negocio "activo"
|
||||
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
||||
Optional<User> findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(String userName);
|
||||
// Nuevo: para login/negocio "activo"
|
||||
@EntityGraph(attributePaths = { "rolesLink", "rolesLink.role" })
|
||||
Optional<User> findByUserNameIgnoreCaseAndEnabledTrueAndDeletedFalse(String userName);
|
||||
|
||||
// Para poder restaurar, necesitas leer ignorando @Where (native):
|
||||
@Query(value = "SELECT * FROM users WHERE id = :id", nativeQuery = true)
|
||||
Optional<User> findByIdIncludingDeleted(@Param("id") Long id);
|
||||
// Para poder restaurar, necesitas leer ignorando @Where (native):
|
||||
@Query(value = "SELECT * FROM users WHERE id = :id", nativeQuery = true)
|
||||
Optional<User> findByIdIncludingDeleted(@Param("id") Long id);
|
||||
|
||||
@Query(value = "SELECT * FROM users WHERE deleted = TRUE", nativeQuery = true)
|
||||
List<User> findAllDeleted();
|
||||
@Query(value = "SELECT * FROM users WHERE deleted = TRUE", nativeQuery = true)
|
||||
List<User> findAllDeleted();
|
||||
|
||||
@Query("select u.id from User u where lower(u.userName) = lower(:userName)")
|
||||
Optional<Long> findIdByUserNameIgnoreCase(@Param("userName") String userName);
|
||||
@Query("select u.id from User u where lower(u.userName) = lower(:userName)")
|
||||
Optional<Long> findIdByUserNameIgnoreCase(@Param("userName") String userName);
|
||||
|
||||
@Query(value = """
|
||||
SELECT DISTINCT u
|
||||
FROM User u
|
||||
JOIN u.rolesLink rl
|
||||
JOIN rl.role r
|
||||
WHERE (:role IS NULL OR r.name = :role)
|
||||
AND (:q IS NULL OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%'))
|
||||
OR LOWER(u.userName) LIKE LOWER(CONCAT('%', :q, '%')))
|
||||
""", countQuery = """
|
||||
SELECT COUNT(DISTINCT u.id)
|
||||
FROM User u
|
||||
JOIN u.rolesLink rl
|
||||
JOIN rl.role r
|
||||
WHERE (:role IS NULL OR r.name = :role)
|
||||
AND (:q IS NULL OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%'))
|
||||
OR LOWER(u.userName) LIKE LOWER(CONCAT('%', :q, '%')))
|
||||
""")
|
||||
Page<User> searchUsers(@Param("role") String role,
|
||||
@Param("q") String q,
|
||||
Pageable pageable);
|
||||
@Query(value = """
|
||||
SELECT DISTINCT u
|
||||
FROM User u
|
||||
JOIN u.rolesLink rl
|
||||
JOIN rl.role r
|
||||
WHERE (:role IS NULL OR r.name = :role)
|
||||
AND (:q IS NULL OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%'))
|
||||
OR LOWER(u.userName) LIKE LOWER(CONCAT('%', :q, '%')))
|
||||
""", countQuery = """
|
||||
SELECT COUNT(DISTINCT u.id)
|
||||
FROM User u
|
||||
JOIN u.rolesLink rl
|
||||
JOIN rl.role r
|
||||
WHERE (:role IS NULL OR r.name = :role)
|
||||
AND (:q IS NULL OR LOWER(u.fullName) LIKE LOWER(CONCAT('%', :q, '%'))
|
||||
OR LOWER(u.userName) LIKE LOWER(CONCAT('%', :q, '%')))
|
||||
""")
|
||||
Page<User> searchUsers(@Param("role") String role,
|
||||
@Param("q") String q,
|
||||
Pageable pageable);
|
||||
|
||||
@Query("select u.id from User u where lower(u.fullName) like lower(concat('%', :name, '%'))")
|
||||
List<Long> findIdsByFullNameLike(@Param("name") String name);
|
||||
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.imprimelibros.erp.users;
|
||||
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
@ -16,4 +17,5 @@ public interface UserService extends UserDetailsService {
|
||||
* @return página de usuarios
|
||||
*/
|
||||
Page<User> findByRoleAndSearch(String role, String query, Pageable pageable);
|
||||
User findById(Long id);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user