Payflow API — Automated JaCoCo Code Coverage (90% Line / 73% Branch) UserController.java
package com.payflow.controller;
import java.math.BigDecimal;
import java.net.URI;
import java.util.List;
import java.util.UUID;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
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.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.servlet.support.ServletUriComponentsBuilder;
import com.payflow.dto.request.CreateUserRequest;
import com.payflow.dto.response.LedgerEntryResponse;
import com.payflow.dto.response.PagedResponse;
import com.payflow.dto.response.UserResponse;
import com.payflow.entity.BalanceLedgerEntry;
import com.payflow.entity.User;
import com.payflow.exception.ForbiddenOperationException;
import com.payflow.exception.UserNotFoundException;
import com.payflow.mapper.LedgerMapper;
import com.payflow.mapper.UserMapper;
import com.payflow.security.SecurityUtils;
import com.payflow.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
@RestController
@RequestMapping("/api/v1/users")
@Validated
@Tag(name = "User Management", description = "Endpoints for registering and querying user accounts")
public class UserController {
private final UserService userService;
private final UserMapper userMapper;
private final LedgerMapper ledgerMapper;
public UserController(UserService userService, UserMapper userMapper, LedgerMapper ledgerMapper) {
this.userService = userService;
this.userMapper = userMapper;
this.ledgerMapper = ledgerMapper;
}
@PostMapping
@Operation(summary = "Register a new user", description = "Creates a new user profile with "
+ "initial balance and unique UPI ID")
@ApiResponse(responseCode = "201", description = "User successfully registered")
@ApiResponse(responseCode = "400", description = "Invalid request payload or constraint validation error")
public ResponseEntity<UserResponse> registerUser(@Valid @RequestBody CreateUserRequest request) {
User createdUser = userService.registerUser(request);
UserResponse response = userMapper.toResponse(createdUser);
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
.buildAndExpand(createdUser.getReferenceId()).toUri();
return ResponseEntity.created(location).body(response);
}
@GetMapping
@Operation(summary = "Get paginated users", description = "Retrieves a paginated list of "
+ "registered users sorted by specified attribute (Requires ROLE_ADMIN)")
@ApiResponse(responseCode = "200", description = "Paginated users list retrieved successfully")
@ApiResponse(responseCode = "403", description = "Access forbidden to non-admin callers")
public ResponseEntity<PagedResponse<UserResponse>> getUsers(@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "10") @Min(1) @Max(100) int size,
@RequestParam(defaultValue = "userId") String sortBy) {
if (!SecurityUtils.hasRole("ADMIN")) {
throw new ForbiddenOperationException(
"Access forbidden: Administrator privileges required to list all users");
}
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy).ascending());
Page<UserResponse> userPage = userService.getAllUsers(pageable).map(userMapper::toResponse);
return ResponseEntity.ok(PagedResponse.fromPage(userPage));
}
@GetMapping("/{id}")
@Operation(summary = "Get user by reference ID", description = "Fetches user details by UUID reference ID")
@ApiResponse(responseCode = "200", description = "User found and returned")
@ApiResponse(responseCode = "403", description = "Access forbidden to other users' profile")
@ApiResponse(responseCode = "404", description = "User not found")
public ResponseEntity<UserResponse> getUserById(@PathVariable UUID id) {
User user = userService.getUserByReferenceId(id)
.orElseThrow(() -> new UserNotFoundException("User not found: " + id));
String authenticatedUpi = SecurityUtils.getAuthenticatedUpiId();
if (authenticatedUpi != null && !authenticatedUpi.equalsIgnoreCase(user.getUpiId())) {
String msg = "User '" + authenticatedUpi + "' is not authorized for user: " + id;
throw new ForbiddenOperationException(msg);
}
return ResponseEntity.ok(userMapper.toResponse(user));
}
@GetMapping("/upi/{upiId}")
@Operation(summary = "Get user by UPI ID", description = "Fetches details of a user by their unique UPI ID")
@ApiResponse(responseCode = "200", description = "User found and returned")
@ApiResponse(responseCode = "403", description = "Access forbidden to other users' profile")
@ApiResponse(responseCode = "404", description = "User not found")
public ResponseEntity<UserResponse> getUserByUpiId(@PathVariable String upiId) {
User user = userService.findByUpiId(upiId)
.orElseThrow(() -> new UserNotFoundException("User not found with UPI ID: " + upiId));
String authenticatedUpi = SecurityUtils.getAuthenticatedUpiId();
if (authenticatedUpi != null && !authenticatedUpi.equalsIgnoreCase(upiId)) {
String msg = "User '" + authenticatedUpi + "' is not authorized for user: " + upiId;
throw new ForbiddenOperationException(msg);
}
return ResponseEntity.ok(userMapper.toResponse(user));
}
@GetMapping("/balance/{amount}")
@Operation(summary = "Get users by balance threshold", description = "Fetches users whose balance "
+ "exceeds minimum threshold (Requires ROLE_ADMIN)")
@ApiResponse(responseCode = "200", description = "Matching users list returned")
@ApiResponse(responseCode = "403", description = "Access forbidden to non-admin callers")
public ResponseEntity<List<UserResponse>> getUsersWithBalanceAbove(@PathVariable BigDecimal amount) {
if (!SecurityUtils.hasRole("ADMIN")) {
throw new ForbiddenOperationException(
"Access forbidden: Administrator privileges required to query user balances");
}
List<User> entityList = userService.getUsersWithBalanceAbove(amount);
List<UserResponse> users = entityList.stream().map(userMapper::toResponse).toList();
return ResponseEntity.ok(users);
}
@GetMapping("/{id}/ledger")
@Operation(summary = "Get user balance ledger history", description = "Retrieves paginated double-entry "
+ "balance ledger audit entries for a user by UUID reference ID")
@ApiResponse(responseCode = "200", description = "Paginated ledger history returned")
@ApiResponse(responseCode = "403", description = "Access forbidden to other users' ledger")
@ApiResponse(responseCode = "404", description = "User not found")
public ResponseEntity<PagedResponse<LedgerEntryResponse>> getUserLedger(@PathVariable UUID id,
@RequestParam(defaultValue = "0") @Min(0) int page,
@RequestParam(defaultValue = "10") @Min(1) @Max(100) int size) {
Pageable pageable = PageRequest.of(page, size);
Page<BalanceLedgerEntry> entries = userService.getUserLedger(id, pageable);
Page<LedgerEntryResponse> ledgerPage = entries.map(ledgerMapper::toResponse);
return ResponseEntity.ok(PagedResponse.fromPage(ledgerPage));
}
}