InventoryItemController.java
package com.smartsupplypro.inventory.controller;
import java.math.BigDecimal;
import java.net.URI;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
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.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import com.smartsupplypro.inventory.dto.InventoryItemDTO;
import com.smartsupplypro.inventory.enums.StockChangeReason;
import com.smartsupplypro.inventory.service.InventoryItemService;
/**
* Inventory item REST controller for CRUD operations.
*
* <p>Provides item management with role-based authorization and validation.
* Follows standard HTTP status conventions for REST APIs.</p>
*
* @see InventoryItemService
* @see <a href="file:../../../../../../docs/architecture/patterns/controller-patterns.md">Controller Patterns</a>
*/
@RestController
@RequestMapping("/api/inventory")
@Validated
public class InventoryItemController {
private final InventoryItemService inventoryItemService;
public InventoryItemController(InventoryItemService inventoryItemService) {
this.inventoryItemService = inventoryItemService;
}
/**
* Gets single inventory item by ID.
*
* @param id unique item identifier
* @return inventory item details
* @throws ResponseStatusException 404 if item not found
*/
@GetMapping("/{id}")
public InventoryItemDTO getById(@PathVariable String id) {
return inventoryItemService.getById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Item not found"));
}
/**
* Gets all inventory items (non-paginated).
*
* @return list of all inventory items
*/
@PreAuthorize("isAuthenticated() or @appProperties.demoReadonly")
@GetMapping
public List<InventoryItemDTO> getAll() {
return inventoryItemService.getAll();
}
/**
* Gets total count of inventory items.
*
* @return total number of items
*/
@PreAuthorize("isAuthenticated() or @appProperties.demoReadonly")
@GetMapping("/count")
public long countItems() {
return inventoryItemService.countItems();
}
/**
* Searches items by name with pagination and sorting.
*
* @param name case-insensitive name substring
* @param pageable pagination and sorting parameters
* @return page of matching items
*/
@PreAuthorize("isAuthenticated() or @appProperties.demoReadonly")
@GetMapping("/search")
public Page<InventoryItemDTO> search(
@RequestParam String name,
@org.springframework.data.web.PageableDefault(size = 20, sort = "price") Pageable pageable) {
return inventoryItemService.findByNameSortedByPrice(name, pageable);
}
/**
* Creates new inventory item (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>
*
* @param body item data (ID must be absent)
* @return 201 Created with Location header and created item
* @throws ResponseStatusException 400/409 on validation/duplicate errors
* @throws ResponseStatusException 403 if user is in demo mode
*/
@PreAuthorize("hasRole('ADMIN') and !@securityService.isDemo()")
@PostMapping
public ResponseEntity<InventoryItemDTO> create(
@Validated(InventoryItemDTO.Create.class) @RequestBody InventoryItemDTO body) {
InventoryItemDTO created = inventoryItemService.save(body);
if (created == null) {
return ResponseEntity.badRequest().build();
}
// REST Location Header Pattern
// Generate Location header pointing to the newly created resource
// Follows RFC 7231 standard for 201 Created responses
URI location = ServletUriComponentsBuilder
.fromCurrentRequest().path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
}
/**
* Updates existing inventory item completely (full replacement).
*
* <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>Semantics</b>: PUT replaces entire item (id must match path parameter)</p>
*
* @param id path identifier
* @param body updated item data (id in body is ignored for consistency)
* @return updated inventory item
* @throws ResponseStatusException 404 if item not found
* @throws ResponseStatusException 403 if user is in demo mode
*/
@PreAuthorize("hasRole('ADMIN') and !@securityService.isDemo()")
@PutMapping("/{id}")
public InventoryItemDTO update(
@PathVariable String id,
@Validated /* or @Valid */ @RequestBody InventoryItemDTO body) {
// ID Consistency Strategy
// Ignore client-sent body.id to prevent conflicts and match test expectations
// Path parameter takes precedence for resource identification
body.setId(null); // or body.setId(id) if you prefer
return inventoryItemService.update(id, body)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Item not found"));
}
/**
* Delete inventory item (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>Audit Trail</b>: Deletion reason is captured for compliance and troubleshooting</p>
*
* @param id item identifier
* @param reason business reason for deletion (StockChangeReason enum)
* @return 204 No Content on success
* @throws ResponseStatusException 404 if item not found
* @throws ResponseStatusException 403 if user is in demo mode
*/
@PreAuthorize("hasRole('ADMIN') and !@securityService.isDemo()")
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable String id, @RequestParam StockChangeReason reason) {
inventoryItemService.delete(id, reason);
}
/**
* Adjusts item quantity by delta amount (partial update).
*
* <p><b>Authorization</b>:
* - Requires ROLE_USER or ROLE_ADMIN and non-demo mode (read-write access)
* - Demo users receive 403 Forbidden with demo mode message</p>
*
* <p><b>Business Rules</b>:
* - Delta can be positive (receive stock) or negative (consume/return stock)
* - Reason is recorded for audit trail and compliance
* - Stock balance can go negative (backorder support)</p>
*
* @param id item identifier
* @param delta quantity change (positive=add, negative=remove)
* @param reason business reason for stock change (StockChangeReason enum)
* @return updated inventory item with new quantity
* @throws ResponseStatusException 404 if item not found
* @throws ResponseStatusException 403 if user is in demo mode
*/
@PreAuthorize("hasAnyRole('USER','ADMIN') and !@securityService.isDemo()")
@PatchMapping("/{id}/quantity")
public InventoryItemDTO adjustQuantity(@PathVariable String id,
@RequestParam int delta,
@RequestParam StockChangeReason reason) {
return inventoryItemService.adjustQuantity(id, delta, reason);
}
/**
* Updates item unit price (partial update).
*
* <p><b>Authorization</b>:
* - Requires ROLE_USER or ROLE_ADMIN and non-demo mode (read-write access)
* - Demo users receive 403 Forbidden with demo mode message</p>
*
* <p><b>Business Rules</b>:
* - Price must be positive (validated via @Positive constraint)
* - Previous prices are not retained (no price history in this version)
* - Price changes immediately affect all future calculations and reports</p>
*
* @param id item identifier
* @param price new unit price (must be positive, e.g., 19.99)
* @return updated inventory item with new price
* @throws ResponseStatusException 404 if item not found
* @throws ResponseStatusException 403 if user is in demo mode
* @throws ResponseStatusException 400 if price is not positive
*/
@PreAuthorize("hasAnyRole('USER','ADMIN') and !@securityService.isDemo()")
@PatchMapping("/{id}/price")
public InventoryItemDTO updatePrice(@PathVariable String id,
@RequestParam @jakarta.validation.constraints.Positive BigDecimal price) {
return inventoryItemService.updatePrice(id, price);
}
/**
* Renames an inventory item (changes the item name 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>Business Rules</b>:
* - Name must be unique per supplier (prevents duplicate SKU issues)
* - Name change is immediately visible in all reports and search results
* - Rename does not affect stock balance or price</p>
*
* @param id item identifier
* @param name new item name (must not be empty or whitespace-only)
* @return updated inventory item with new name
* @throws ResponseStatusException 400 if name is empty or blank
* @throws ResponseStatusException 404 if item not found
* @throws ResponseStatusException 409 if name already exists for the same supplier
* @throws ResponseStatusException 403 if user is in demo mode
*/
@PreAuthorize("hasRole('ADMIN') and !@securityService.isDemo()")
@PatchMapping("/{id}/name")
public InventoryItemDTO renameItem(@PathVariable String id,
@RequestParam String name) {
try {
return inventoryItemService.renameItem(id, name);
} catch (IllegalArgumentException e) {
String message = e.getMessage();
if (message != null && message.contains("empty")) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
} else if (message != null && message.contains("already exists")) {
throw new ResponseStatusException(HttpStatus.CONFLICT, message);
} else {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, message);
}
}
}
}