SupplierController.java
package com.smartsupplypro.inventory.controller;
import java.net.URI;
import java.util.List;
import java.util.NoSuchElementException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import com.smartsupplypro.inventory.dto.SupplierDTO;
import com.smartsupplypro.inventory.service.SupplierService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
/**
* REST controller for supplier management with full CRUD operations and search.
* Supports role-based authorization (USER read-only, ADMIN full access).
* @see SupplierService
* @see controller-patterns.md for REST API patterns
*/
@RestController
@RequestMapping("/api/suppliers")
@RequiredArgsConstructor
public class SupplierController {
private final SupplierService supplierService;
/**
* Lists all suppliers with optional demo readonly access.
* @return list of supplier DTOs
*/
@PreAuthorize("isAuthenticated() or @appProperties.demoReadonly")
@GetMapping
public ResponseEntity<List<SupplierDTO>> listAll() {
return ResponseEntity.ok(supplierService.findAll());
}
/**
* Returns total count of suppliers in system.
* @return supplier count as long value
*/
@PreAuthorize("isAuthenticated() or @appProperties.demoReadonly")
@GetMapping("/count")
public long countSuppliers() {
return supplierService.countSuppliers();
}
/**
* Retrieves supplier by unique identifier.
* @param id supplier ID
* @return supplier DTO or 404 if not found
*/
@PreAuthorize("isAuthenticated()")
@GetMapping("/{id}")
public ResponseEntity<SupplierDTO> getById(@PathVariable String id) {
return supplierService.findById(id)
.map(ResponseEntity::ok)
.orElseThrow(() -> new NoSuchElementException("Supplier not found: " + id));
}
/**
* Searches suppliers by partial name match.
* @param name partial or full supplier name
* @return list of matching suppliers
*/
@PreAuthorize("isAuthenticated() or @appProperties.demoReadonly")
@GetMapping("/search")
public ResponseEntity<List<SupplierDTO>> search(@RequestParam String name) {
return ResponseEntity.ok(supplierService.findByName(name));
}
/**
* Creates new supplier (ADMIN only).
*
* <p><b>Authorization</b>:
* - Requires ROLE_ADMIN and non-demo mode (read-write access)
* - Demo users receive 403 Forbidden with demo mode message</p>
*
* <p><b>REST Pattern</b>: Returns 201 Created with Location header for resource discovery</p>
*
* @param dto supplier data (ID must be null for creation)
* @return created supplier with 201 status and Location header
* @throws ResponseStatusException 400 if ID is not null
* @throws ResponseStatusException 403 if user is in demo mode
*/
@PreAuthorize("hasRole('ADMIN') and !@securityService.isDemo()")
@PostMapping
public ResponseEntity<SupplierDTO> create(@Valid @RequestBody SupplierDTO dto) {
if (dto.getId() != null) {
// ID consistency validation - prevent client-generated IDs on creation
// to maintain server-side ID generation control and avoid potential ID conflicts
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "ID must be null on create");
}
SupplierDTO created = supplierService.create(dto);
// REST Location header pattern - provide resource URI for immediate access
// enabling client-side navigation and RESTful resource discovery
return ResponseEntity.created(URI.create("/api/suppliers/" + created.getId()))
.header(HttpHeaders.LOCATION, "/api/suppliers/" + created.getId())
.body(created);
}
/**
* Updates existing supplier (ADMIN only).
*
* <p><b>Authorization</b>:
* - Requires ROLE_ADMIN and non-demo mode (read-write access)
* - Demo users receive 403 Forbidden with demo mode message</p>
*
* <p><b>ID Consistency</b>: Path ID takes precedence; body ID must match or be null</p>
*
* @param id supplier ID from path parameter
* @param dto updated supplier data (id in body is ignored for consistency)
* @return updated supplier DTO
* @throws ResponseStatusException 400 if path ID and body ID mismatch
* @throws ResponseStatusException 404 if supplier not found
* @throws ResponseStatusException 403 if user is in demo mode
*/
@PreAuthorize("hasRole('ADMIN') and !@securityService.isDemo()")
@PutMapping("/{id}")
public ResponseEntity<SupplierDTO> update(@PathVariable String id, @Valid @RequestBody SupplierDTO dto) {
// Enterprise Comment: Path vs body ID validation - ensure API contract consistency
// by preventing mismatched identifiers that could lead to unintended updates
if (dto.getId() != null && !id.equals(dto.getId())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Path id and body id must match");
}
SupplierDTO updated = supplierService.update(id, dto);
return ResponseEntity.ok(updated);
}
/**
* Deletes supplier (ADMIN only).
*
* <p><b>Authorization</b>:
* - Requires ROLE_ADMIN and non-demo mode (read-write access)
* - Demo users receive 403 Forbidden with demo mode message</p>
*
* <p><b>Referential Integrity</b>: Supplier deletion may cascade or be blocked based on business rules</p>
*
* @param id supplier ID to delete
* @return 204 No Content on success
* @throws ResponseStatusException 404 if supplier not found
* @throws ResponseStatusException 403 if user is in demo mode
*/
@PreAuthorize("hasRole('ADMIN') and !@securityService.isDemo()")
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable String id) {
supplierService.delete(id);
return ResponseEntity.noContent().build();
}
}