GoalController.java

package no.ntnu.idi.stud.savingsapp.controller.goal;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import no.ntnu.idi.stud.savingsapp.dto.goal.*;
import no.ntnu.idi.stud.savingsapp.exception.ExceptionResponse;
import no.ntnu.idi.stud.savingsapp.model.goal.Challenge;
import no.ntnu.idi.stud.savingsapp.model.goal.Goal;
import no.ntnu.idi.stud.savingsapp.model.goal.Group;
import no.ntnu.idi.stud.savingsapp.security.AuthIdentity;
import no.ntnu.idi.stud.savingsapp.service.ChallengeService;
import no.ntnu.idi.stud.savingsapp.service.GoalService;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * Controller for managing goals within the application.
 */
@RestController
@RequestMapping("/api/goals")
@EnableAutoConfiguration
@Tag(name = "Goal")
@Slf4j
public class GoalController {

	@Autowired
	private GoalService goalService;

	@Autowired
	private ChallengeService challengeService;

	@Autowired
	private ModelMapper modelMapper;

	/**
	 * Creates a new goal based on the provided goal data.
	 * @param identity The security context of the authenticated user.
	 * @param request The data transfer object containing the details needed to create a
	 * goal.
	 * @return ResponseEntity containing the newly created GoalDTO and the HTTP status.
	 */
	@Operation(summary = "Create a goal", description = "Create a new goal")
	@ApiResponses(value = { @ApiResponse(responseCode = "201", description = "Successfully created a goal") })
	@ResponseStatus(HttpStatus.CREATED)
	@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<GoalDTO> createGoal(@AuthenticationPrincipal AuthIdentity identity,
			@RequestBody CreateGoalDTO request) {
		Goal createGoal = modelMapper.map(request, Goal.class);
		Goal goal = goalService.createGoal(createGoal, request.getDistribution(), identity.getId());
		GoalDTO goalDTO = modelMapper.map(goal, GoalDTO.class);
		log.info("[GoalController:createGoal] goal: {}", goalDTO.getId());
		return ResponseEntity.status(HttpStatus.CREATED).body(goalDTO);
	}

	/**
	 * Retrieves all goals associated with the authenticated user.
	 * @param identity The security context of the authenticated user.
	 * @return ResponseEntity containing a list of GoalDTOs for the user's goals and the
	 * HTTP status.
	 */
	@Operation(summary = "Get goals", description = "Get the goals of the authenticated user")
	@ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully retrieved the goals") })
	@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<List<GoalDTO>> getGoals(@AuthenticationPrincipal AuthIdentity identity) {
		List<Goal> goals = goalService.getGoals(identity.getId());
		List<GoalDTO> goalsDTO = goals.stream().map(goal -> modelMapper.map(goal, GoalDTO.class)).toList();
		log.info("[GoalController:getGoals] user: {}", identity.getId());
		for (GoalDTO goalDTO : goalsDTO) {
			log.info("[GoalController:getGoals] goal: {}", goalDTO.getId());
		}
		return ResponseEntity.ok(goalsDTO);
	}

	/**
	 * Updates the progress of a specific challenge by marking a particular day as
	 * completed. This method allows users to record progress on challenges associated
	 * with their goals.
	 * @param identity The security context of the authenticated user, including user
	 * identification.
	 * @param request The data transfer object containing the challenge ID, the day to
	 * mark, and the amount saved.
	 * @return A ResponseEntity indicating the status of the operation.
	 * @apiNote This endpoint returns HTTP 202 (Accepted) on successful update, and may
	 * return HTTP 401 if the specified day is already marked as completed or is outside
	 * the allowed range. Error details are provided in the response body.
	 */
	@Operation(summary = "Update a challenge", description = "Update a challenge day as completed")
	@ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully updated the challenge"),
			@ApiResponse(responseCode = "401", description = "Day is already completed or day outside of range",
					content = @Content(schema = @Schema(implementation = ExceptionResponse.class))) })
	@PostMapping(value = "/update-challenge", consumes = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<Void> updateChallenge(@AuthenticationPrincipal AuthIdentity identity,
			@RequestBody MarkChallengeDTO request) {
		challengeService.updateProgress(identity.getId(), request.getId(), request.getDay(), request.getAmount());
		log.info("[GoalController:updateChallenge] challenge: {}", request.getId());
		return ResponseEntity.status(HttpStatus.ACCEPTED).build();
	}

	/**
	 * Updates the saving amount for a specific challenge. This allows users to modify the
	 * potential savings target for any challenge associated with their goals.
	 * @param identity The security context of the authenticated user.
	 * @param request The data transfer object containing the challenge ID and the new
	 * saving amount.
	 * @return A ResponseEntity indicating the success of the update operation.
	 * @apiNote This endpoint returns HTTP 202 (Accepted) on a successful update of the
	 * challenge amount.
	 */
	@Operation(summary = "Update challenge saving amount", description = "Update the challenge saving amount")
	@ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successfully updated the challenge") })
	@PostMapping(value = "/update-challenge-amount", consumes = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<Void> updateChallengeAmount(@AuthenticationPrincipal AuthIdentity identity,
			@RequestBody MarkChallengeDTO request) {
		challengeService.updateSavingAmount(identity.getId(), request.getId(), request.getAmount());
		log.info("[GoalController:updateChallengeAmount] challenge: {}", request.getId());
		return ResponseEntity.status(HttpStatus.ACCEPTED).build();
	}

	@GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<GoalDTO> getGoal(@PathVariable Long id) {
		Goal goal = goalService.getGoal(id);
		GoalDTO goalDTO = modelMapper.map(goal, GoalDTO.class);
		log.info("[GoalController:getGoal] goal: {}", goalDTO.getId());
		return ResponseEntity.ok(goalDTO);
	}

	@PatchMapping(value = "/challenge/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<ChallengeDTO> regenerateChallenge(@AuthenticationPrincipal AuthIdentity identity,
			@PathVariable Long id) {
		Challenge challenge = challengeService.regenerateChallenge(identity.getId(), id);
		ChallengeDTO challengeDTO = modelMapper.map(challenge, ChallengeDTO.class);
		return ResponseEntity.ok(challengeDTO);
	}

	@GetMapping(value = "/group/{goalId}", produces = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<List<GoalDTO>> getGroupInfo(@PathVariable Long goalId) {
		Group group = goalService.getGroup(goalId);
		List<GoalDTO> goals = group.getGoals().stream().map(g -> modelMapper.map(g, GoalDTO.class)).toList();
		return ResponseEntity.ok(goals);
	}

}