diff --git a/api/src/main/java/fr/gameovergne/api/config/SecurityConfig.java b/api/src/main/java/fr/gameovergne/api/config/SecurityConfig.java index 85fcab9..b5debcc 100644 --- a/api/src/main/java/fr/gameovergne/api/config/SecurityConfig.java +++ b/api/src/main/java/fr/gameovergne/api/config/SecurityConfig.java @@ -46,8 +46,7 @@ public class SecurityConfig { .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // autoriser les preflight .requestMatchers("/api/auth/**").permitAll() .requestMatchers("/api/users/**").authenticated() - .requestMatchers("/api/brands/**").permitAll() - .requestMatchers("/api/platforms/**").permitAll() + .requestMatchers("/api/app/**").permitAll() .anyRequest().permitAll() ) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) diff --git a/api/src/main/java/fr/gameovergne/api/controller/app/BrandController.java b/api/src/main/java/fr/gameovergne/api/controller/app/BrandController.java index 0a99ee6..ad0a1a4 100644 --- a/api/src/main/java/fr/gameovergne/api/controller/app/BrandController.java +++ b/api/src/main/java/fr/gameovergne/api/controller/app/BrandController.java @@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.*; import java.util.List; @RestController -@RequestMapping("/api/brands") +@RequestMapping("/api/app/brands") public class BrandController { private final BrandService brandService; diff --git a/api/src/main/java/fr/gameovergne/api/controller/app/CategoryController.java b/api/src/main/java/fr/gameovergne/api/controller/app/CategoryController.java new file mode 100644 index 0000000..9b9757a --- /dev/null +++ b/api/src/main/java/fr/gameovergne/api/controller/app/CategoryController.java @@ -0,0 +1,60 @@ +package fr.gameovergne.api.controller.app; + +import fr.gameovergne.api.dto.app.CategoryDTO; +import fr.gameovergne.api.mapper.app.CategoryMapper; +import fr.gameovergne.api.model.app.Category; +import fr.gameovergne.api.service.app.CategoryService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/app/categories") +public class CategoryController { + + private final CategoryService categoryService; + private final CategoryMapper categoryMapper; + + @Autowired + public CategoryController(CategoryService categoryService, CategoryMapper categoryMapper) { + this.categoryService = categoryService; + this.categoryMapper = categoryMapper; + } + + @GetMapping + public List getAllCategories() { + return categoryService.getAllCategories(); + } + + @GetMapping("/{id}") + public ResponseEntity getCategoryById(@PathVariable Long id) { + return categoryService.getCategoryById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @PostMapping + public void saveCategory(@RequestBody CategoryDTO categoryDTO) { + categoryService.saveCategory(categoryMapper.fromDto(categoryDTO)); + } + + @PutMapping("/{id}") + public ResponseEntity updateCategory(@PathVariable Long id, @RequestBody CategoryDTO categoryDTO) { + Category category = categoryMapper.fromDto(categoryDTO); + category.setId(id); + return categoryService.getCategoryById(id) + .map(existingCategory -> categoryService.updateCategory(category) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build())) + .orElse(ResponseEntity.notFound().build()); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteCategoryById(@PathVariable Long id) { + return categoryService.deleteCategoryById(id) + .map(category -> ResponseEntity.ok().body(category)) + .orElse(ResponseEntity.notFound().build()); + } +} diff --git a/api/src/main/java/fr/gameovergne/api/controller/app/ConditionController.java b/api/src/main/java/fr/gameovergne/api/controller/app/ConditionController.java new file mode 100644 index 0000000..2b0c596 --- /dev/null +++ b/api/src/main/java/fr/gameovergne/api/controller/app/ConditionController.java @@ -0,0 +1,23 @@ +package fr.gameovergne.api.controller.app; + +import fr.gameovergne.api.dto.app.ConditionDTO; +import fr.gameovergne.api.model.app.Condition; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/app/conditions") +public class ConditionController { + + @GetMapping + public List list() { + return Arrays.stream(Condition.values()) + .map(condition -> new ConditionDTO(condition.name(), condition.getDisplayName())) + .collect(Collectors.toList()); + } +} diff --git a/api/src/main/java/fr/gameovergne/api/controller/app/PlatformController.java b/api/src/main/java/fr/gameovergne/api/controller/app/PlatformController.java index c6b8f26..94557b5 100644 --- a/api/src/main/java/fr/gameovergne/api/controller/app/PlatformController.java +++ b/api/src/main/java/fr/gameovergne/api/controller/app/PlatformController.java @@ -10,7 +10,7 @@ import org.springframework.web.bind.annotation.*; import java.util.List; @RestController -@RequestMapping("/api/platforms") +@RequestMapping("/api/app/platforms") public class PlatformController { private final PlatformService platformService; diff --git a/api/src/main/java/fr/gameovergne/api/controller/app/ProductController.java b/api/src/main/java/fr/gameovergne/api/controller/app/ProductController.java new file mode 100644 index 0000000..fd0742a --- /dev/null +++ b/api/src/main/java/fr/gameovergne/api/controller/app/ProductController.java @@ -0,0 +1,60 @@ +package fr.gameovergne.api.controller.app; + +import fr.gameovergne.api.dto.app.ProductDTO; +import fr.gameovergne.api.mapper.app.ProductMapper; +import fr.gameovergne.api.model.app.Product; +import fr.gameovergne.api.service.app.ProductService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/app/products") +public class ProductController { + + private final ProductService productService; + private final ProductMapper productMapper; + + @Autowired + public ProductController(ProductService productService, ProductMapper productMapper) { + this.productService = productService; + this.productMapper = productMapper; + } + + @GetMapping + public List getAllProducts() { + return productService.getAllProducts(); + } + + @GetMapping("/{id}") + public ResponseEntity getProductById(@PathVariable Long id) { + return productService.getProductById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @PostMapping + public void saveProduct(@RequestBody ProductDTO productDTO) { + productService.saveProduct(productMapper.fromDto(productDTO)); + } + + @PutMapping("/{id}") + public ResponseEntity updateProduct(@PathVariable Long id, @RequestBody ProductDTO productDTO) { + Product product = productMapper.fromDto(productDTO); + product.setId(id); + return productService.getProductById(id) + .map(existingProduct -> productService.updateProduct(product) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build())) + .orElse(ResponseEntity.notFound().build()); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteProductById(@PathVariable Long id) { + return productService.deleteProductById(id) + .map(product -> ResponseEntity.ok().body(product)) + .orElse(ResponseEntity.notFound().build()); + } +} diff --git a/api/src/main/java/fr/gameovergne/api/dto/app/CategoryDTO.java b/api/src/main/java/fr/gameovergne/api/dto/app/CategoryDTO.java new file mode 100644 index 0000000..f1b685f --- /dev/null +++ b/api/src/main/java/fr/gameovergne/api/dto/app/CategoryDTO.java @@ -0,0 +1,15 @@ +package fr.gameovergne.api.dto.app; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CategoryDTO { + + @JsonProperty("name") + private String name; +} diff --git a/api/src/main/java/fr/gameovergne/api/dto/app/ConditionDTO.java b/api/src/main/java/fr/gameovergne/api/dto/app/ConditionDTO.java new file mode 100644 index 0000000..8766b22 --- /dev/null +++ b/api/src/main/java/fr/gameovergne/api/dto/app/ConditionDTO.java @@ -0,0 +1,16 @@ +package fr.gameovergne.api.dto.app; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ConditionDTO { + + @JsonProperty("name") + private final String name; + + @JsonProperty("displayName") + private final String displayName; +} diff --git a/api/src/main/java/fr/gameovergne/api/dto/app/ProductDTO.java b/api/src/main/java/fr/gameovergne/api/dto/app/ProductDTO.java new file mode 100644 index 0000000..dad2a68 --- /dev/null +++ b/api/src/main/java/fr/gameovergne/api/dto/app/ProductDTO.java @@ -0,0 +1,39 @@ +package fr.gameovergne.api.dto.app; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ProductDTO { + + @JsonProperty("title") + private String title; + + @JsonProperty("description") + private String description; + + @JsonProperty("price") + private double price; + + @JsonProperty("quantity") + private int quantity; + + @JsonProperty("complete") + private boolean complete = false; + + @JsonProperty("manualIncluded") + private boolean manualIncluded = false; + + @JsonProperty("category") + private CategoryDTO categoryDTO; + + @JsonProperty("platform") + private PlatformDTO platformDTO; + + @JsonProperty("condition") + private ConditionDTO conditionDTO; +} diff --git a/api/src/main/java/fr/gameovergne/api/mapper/app/CategoryMapper.java b/api/src/main/java/fr/gameovergne/api/mapper/app/CategoryMapper.java new file mode 100644 index 0000000..5c518cf --- /dev/null +++ b/api/src/main/java/fr/gameovergne/api/mapper/app/CategoryMapper.java @@ -0,0 +1,23 @@ +package fr.gameovergne.api.mapper.app; + +import fr.gameovergne.api.dto.app.CategoryDTO; +import fr.gameovergne.api.model.app.Category; +import org.springframework.stereotype.Component; + +@Component +public class CategoryMapper { + + public Category fromDto(CategoryDTO categoryDTO) { + if (categoryDTO == null) return null; + Category category = new Category(); + category.setName(categoryDTO.getName()); + return category; + } + + public CategoryDTO toDto(Category category) { + if (category == null) return null; + CategoryDTO categoryDTO = new CategoryDTO(); + categoryDTO.setName(category.getName()); + return categoryDTO; + } +} diff --git a/api/src/main/java/fr/gameovergne/api/mapper/app/PlatformMapper.java b/api/src/main/java/fr/gameovergne/api/mapper/app/PlatformMapper.java index a2c386d..de1680c 100644 --- a/api/src/main/java/fr/gameovergne/api/mapper/app/PlatformMapper.java +++ b/api/src/main/java/fr/gameovergne/api/mapper/app/PlatformMapper.java @@ -19,6 +19,7 @@ public class PlatformMapper { if (platformDTO == null) return null; Platform platform = new Platform(); platform.setName(platformDTO.getName()); + if (platformDTO.getBrandDTO() != null && platformDTO.getBrandDTO().getName() != null) { platform.setBrand(this.brandService.getBrandByName(platformDTO.getBrandDTO().getName()).orElse(null)); } else { diff --git a/api/src/main/java/fr/gameovergne/api/mapper/app/ProductMapper.java b/api/src/main/java/fr/gameovergne/api/mapper/app/ProductMapper.java new file mode 100644 index 0000000..91a8ae9 --- /dev/null +++ b/api/src/main/java/fr/gameovergne/api/mapper/app/ProductMapper.java @@ -0,0 +1,121 @@ +package fr.gameovergne.api.mapper.app; + +import fr.gameovergne.api.dto.app.*; +import fr.gameovergne.api.model.app.Brand; +import fr.gameovergne.api.model.app.Condition; +import fr.gameovergne.api.model.app.Platform; +import fr.gameovergne.api.model.app.Product; +import fr.gameovergne.api.service.app.BrandService; +import fr.gameovergne.api.service.app.CategoryService; +import fr.gameovergne.api.service.app.PlatformService; +import org.springframework.stereotype.Component; + +@Component +public class ProductMapper { + + private final CategoryService categoryService; + private final BrandService brandService; + private final PlatformService platformService; + + public ProductMapper(CategoryService categoryService, BrandService brandService, PlatformService platformService) { + this.categoryService = categoryService; + this.brandService = brandService; + this.platformService = platformService; + } + + public Product fromDto(ProductDTO productDTO) { + if (productDTO == null) return null; + Product product = new Product(); + product.setTitle(productDTO.getTitle()); + product.setDescription(productDTO.getDescription()); + product.setPrice(productDTO.getPrice()); + product.setQuantity(productDTO.getQuantity()); + product.setComplete(productDTO.isComplete()); + product.setManualIncluded(productDTO.isManualIncluded()); + + if (productDTO.getCategoryDTO() != null && productDTO.getCategoryDTO().getName() != null) { + product.setCategory(this.categoryService.getCategoryByName(productDTO.getCategoryDTO().getName()).orElse(null)); + } else { + product.setCategory(null); + } + + // Platform + Brand handling — ensure platform is set on product + if (productDTO.getPlatformDTO() != null && productDTO.getPlatformDTO().getName() != null) { + String platformName = productDTO.getPlatformDTO().getName(); + Platform platform = this.platformService.getPlatformByName(platformName) + .orElseGet(() -> { + Platform p = new Platform(); + p.setName(platformName); + return p; + }); + + if (productDTO.getPlatformDTO().getBrandDTO() != null && productDTO.getPlatformDTO().getBrandDTO().getName() != null) { + String brandName = productDTO.getPlatformDTO().getBrandDTO().getName(); + Brand brand = this.brandService.getBrandByName(brandName) + .orElseGet(() -> { + Brand b = new Brand(); + b.setName(brandName); + return b; + }); + platform.setBrand(brand); + } + + product.setPlatform(platform); + } else { + product.setPlatform(null); + } + + // Condition handling with null/invalid protection + if (productDTO.getConditionDTO() != null && productDTO.getConditionDTO().getName() != null) { + try { + product.setCondition(Condition.valueOf(productDTO.getConditionDTO().getName())); + } catch (IllegalArgumentException e) { + product.setCondition(null); + } + } else { + product.setCondition(null); + } + + return product; + } + + public ProductDTO toDto(Product product) { + if (product == null) return null; + ProductDTO productDTO = new ProductDTO(); + productDTO.setTitle(product.getTitle()); + productDTO.setDescription(product.getDescription()); + productDTO.setPrice(product.getPrice()); + productDTO.setQuantity(product.getQuantity()); + productDTO.setComplete(product.isComplete()); + productDTO.setManualIncluded(product.isManualIncluded()); + + if (product.getCategory() != null) { + productDTO.setCategoryDTO(new CategoryDTO(product.getCategory().getName())); + } else { + productDTO.setCategoryDTO(null); + } + + if (product.getPlatform() != null) { + Platform platform = product.getPlatform(); + PlatformDTO platformDTO = new PlatformDTO(); + platformDTO.setName(platform.getName()); + + if (platform.getBrand() != null) { + platformDTO.setBrandDTO(new BrandDTO(platform.getBrand().getName())); + } else { + platformDTO.setBrandDTO(null); + } + productDTO.setPlatformDTO(platformDTO); + } else { + productDTO.setPlatformDTO(null); + } + + if (product.getCondition() != null) { + productDTO.setConditionDTO(new ConditionDTO(product.getCondition().name(), product.getCondition().getDisplayName())); + } else { + productDTO.setConditionDTO(null); + } + + return productDTO; + } +} diff --git a/api/src/main/java/fr/gameovergne/api/model/app/Category.java b/api/src/main/java/fr/gameovergne/api/model/app/Category.java index 21d044c..160a6e5 100644 --- a/api/src/main/java/fr/gameovergne/api/model/app/Category.java +++ b/api/src/main/java/fr/gameovergne/api/model/app/Category.java @@ -22,7 +22,7 @@ public class Category { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private long id; + private Long id; @NotBlank @Column(name = "category_name", length = 30, unique = true, nullable = false) diff --git a/api/src/main/java/fr/gameovergne/api/model/app/Product.java b/api/src/main/java/fr/gameovergne/api/model/app/Product.java index 2534547..3a2a073 100644 --- a/api/src/main/java/fr/gameovergne/api/model/app/Product.java +++ b/api/src/main/java/fr/gameovergne/api/model/app/Product.java @@ -32,13 +32,21 @@ public class Product { @Column(name = "product_description", length = 500) private String description; + @NotNull + @Column(name = "product_price") + private double price; + + @NotNull + @Column(name = "product_quantity") + private int quantity; + @NotNull @Column(name = "product_complete") private boolean complete = false; @NotNull @Column(name = "product_manual") - private boolean manual = false; // Notice + private boolean manualIncluded = false; // Notice @ManyToOne @JoinColumn(name = "category_id") diff --git a/api/src/main/java/fr/gameovergne/api/repository/app/BrandRepository.java b/api/src/main/java/fr/gameovergne/api/repository/app/BrandRepository.java index be9593c..365037b 100644 --- a/api/src/main/java/fr/gameovergne/api/repository/app/BrandRepository.java +++ b/api/src/main/java/fr/gameovergne/api/repository/app/BrandRepository.java @@ -6,6 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface BrandRepository extends JpaRepository { - Optional findById(Long id); Optional findByName(String name); } diff --git a/api/src/main/java/fr/gameovergne/api/repository/app/CategoryRepository.java b/api/src/main/java/fr/gameovergne/api/repository/app/CategoryRepository.java new file mode 100644 index 0000000..6bbbc24 --- /dev/null +++ b/api/src/main/java/fr/gameovergne/api/repository/app/CategoryRepository.java @@ -0,0 +1,10 @@ +package fr.gameovergne.api.repository.app; + +import fr.gameovergne.api.model.app.Category; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface CategoryRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/api/src/main/java/fr/gameovergne/api/repository/app/PlatformRepository.java b/api/src/main/java/fr/gameovergne/api/repository/app/PlatformRepository.java index 5e58792..8e7b49f 100644 --- a/api/src/main/java/fr/gameovergne/api/repository/app/PlatformRepository.java +++ b/api/src/main/java/fr/gameovergne/api/repository/app/PlatformRepository.java @@ -6,6 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface PlatformRepository extends JpaRepository { - Optional findById(Long id); Optional findByName(String name); } diff --git a/api/src/main/java/fr/gameovergne/api/repository/app/ProductRepository.java b/api/src/main/java/fr/gameovergne/api/repository/app/ProductRepository.java new file mode 100644 index 0000000..115e6b4 --- /dev/null +++ b/api/src/main/java/fr/gameovergne/api/repository/app/ProductRepository.java @@ -0,0 +1,13 @@ +package fr.gameovergne.api.repository.app; + +import fr.gameovergne.api.model.app.*; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface ProductRepository extends JpaRepository { + Optional findByTitle(String title); + Optional findByCategory(Category category); + Optional findByPlatform(Platform platform); + Optional findByCondition(Condition condition); +} diff --git a/api/src/main/java/fr/gameovergne/api/service/app/CategoryService.java b/api/src/main/java/fr/gameovergne/api/service/app/CategoryService.java new file mode 100644 index 0000000..6b76317 --- /dev/null +++ b/api/src/main/java/fr/gameovergne/api/service/app/CategoryService.java @@ -0,0 +1,55 @@ +package fr.gameovergne.api.service.app; + +import fr.gameovergne.api.model.app.Category; +import fr.gameovergne.api.repository.app.CategoryRepository; +import jakarta.transaction.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class CategoryService { + + private final CategoryRepository categoryRepository; + + @Autowired + public CategoryService(CategoryRepository categoryRepository) { + this.categoryRepository = categoryRepository; + } + + @Transactional + public void saveCategory(Category category) { + if (category.getId() == null) { + categoryRepository.save(category); + } + } + + public List getAllCategories() { + return categoryRepository.findAll(); + } + + public Optional getCategoryById(Long id) { + return categoryRepository.findById(id); + } + + public Optional getCategoryByName(String name) { + return categoryRepository.findByName(name); + } + + @Transactional + public Optional updateCategory(Category category) { + return categoryRepository.findById(category.getId()).map(existingCategory -> { + existingCategory.setName(category.getName()); + return categoryRepository.save(existingCategory); + }); + } + + @Transactional + public Optional deleteCategoryById(Long id) { + Optional category = categoryRepository.findById(id); + category.ifPresent(categoryRepository::delete); + return category; + } +} diff --git a/api/src/main/java/fr/gameovergne/api/service/app/ProductService.java b/api/src/main/java/fr/gameovergne/api/service/app/ProductService.java new file mode 100644 index 0000000..ee14353 --- /dev/null +++ b/api/src/main/java/fr/gameovergne/api/service/app/ProductService.java @@ -0,0 +1,65 @@ +package fr.gameovergne.api.service.app; + +import fr.gameovergne.api.model.app.Product; +import fr.gameovergne.api.model.app.Platform; +import fr.gameovergne.api.repository.app.ProductRepository; +import jakarta.transaction.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +public class ProductService { + + private final ProductRepository productRepository; + + @Autowired + public ProductService(ProductRepository productRepository) { + this.productRepository = productRepository; + } + + @Transactional + public void saveProduct(Product product) { + if (product.getId() == null) { + productRepository.save(product); + } + } + + public List getAllProducts() { + return productRepository.findAll(); + } + + public Optional getProductById(Long id) { + return productRepository.findById(id); + } + + public Optional getProductByName(String name) { + return productRepository.findByTitle(name); + } + + @Transactional + public Optional updateProduct(Product product) { + return productRepository.findById(product.getId()).map(existingProduct -> { + existingProduct.setTitle(product.getTitle()); + existingProduct.setDescription(product.getDescription()); + existingProduct.setPrice(product.getPrice()); + existingProduct.setQuantity(product.getQuantity()); + existingProduct.setComplete(product.isComplete()); + existingProduct.setManualIncluded(product.isManualIncluded()); + existingProduct.setCategory(product.getCategory()); + existingProduct.setPlatform(product.getPlatform()); + existingProduct.setCondition(product.getCondition()); + return productRepository.save(existingProduct); + }); + } + + @Transactional + public Optional deleteProductById(Long id) { + Optional product = productRepository.findById(id); + product.ifPresent(productRepository::delete); + return product; + } +}