From f31617f4fdab5321a77a9555bfdf1c69670b5133 Mon Sep 17 00:00:00 2001 From: AdithaBuwaneka Date: Fri, 31 Oct 2025 16:05:24 +0530 Subject: [PATCH] feat: Enhance project and service management with new endpoints and DTOs - Implemented new endpoints in ProjectController for project requests, listing, and quote management. - Added ApiResponse DTO for standardized API responses. - Introduced ProgressUpdateDto, ProjectRequestDto, ProjectResponseDto, QuoteDto, and RejectionDto for request handling. - Enhanced ProjectService and ProjectServiceImpl with new methods for project management. - Added exception handling for project-related operations. --- .../controller/ProjectController.java | 122 +++++++++--- .../controller/ServiceController.java | 26 ++- .../project_service/dto/ApiResponse.java | 38 ++++ .../dto/ProgressUpdateDto.java | 23 +++ .../dto/ProjectRequestDto.java | 27 +++ .../dto/ProjectResponseDto.java | 26 +++ .../project_service/dto/QuoteDto.java | 23 +++ .../project_service/dto/RejectionDto.java | 17 ++ .../exception/GlobalExceptionHandler.java | 55 ++++++ .../InvalidProjectOperationException.java | 11 ++ .../exception/ProjectNotFoundException.java | 11 ++ .../service/ProjectService.java | 14 +- .../service/impl/ProjectServiceImpl.java | 185 ++++++++++++++++-- 13 files changed, 529 insertions(+), 49 deletions(-) create mode 100644 project-service/src/main/java/com/techtorque/project_service/dto/ApiResponse.java create mode 100644 project-service/src/main/java/com/techtorque/project_service/dto/ProgressUpdateDto.java create mode 100644 project-service/src/main/java/com/techtorque/project_service/dto/ProjectRequestDto.java create mode 100644 project-service/src/main/java/com/techtorque/project_service/dto/ProjectResponseDto.java create mode 100644 project-service/src/main/java/com/techtorque/project_service/dto/QuoteDto.java create mode 100644 project-service/src/main/java/com/techtorque/project_service/dto/RejectionDto.java create mode 100644 project-service/src/main/java/com/techtorque/project_service/exception/GlobalExceptionHandler.java create mode 100644 project-service/src/main/java/com/techtorque/project_service/exception/InvalidProjectOperationException.java create mode 100644 project-service/src/main/java/com/techtorque/project_service/exception/ProjectNotFoundException.java diff --git a/project-service/src/main/java/com/techtorque/project_service/controller/ProjectController.java b/project-service/src/main/java/com/techtorque/project_service/controller/ProjectController.java index c7fcd08..6350448 100644 --- a/project-service/src/main/java/com/techtorque/project_service/controller/ProjectController.java +++ b/project-service/src/main/java/com/techtorque/project_service/controller/ProjectController.java @@ -1,73 +1,149 @@ package com.techtorque.project_service.controller; +import com.techtorque.project_service.dto.*; +import com.techtorque.project_service.entity.Project; +import com.techtorque.project_service.service.ProjectService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import java.util.List; +import java.util.stream.Collectors; + @RestController @RequestMapping("/projects") @Tag(name = "Custom Projects (Modifications)", description = "Endpoints for managing custom vehicle modifications.") @SecurityRequirement(name = "bearerAuth") +@RequiredArgsConstructor public class ProjectController { - // @Autowired - // private ProjectService projectService; + private final ProjectService projectService; @Operation(summary = "Request a new modification project (customer only)") @PostMapping @PreAuthorize("hasRole('CUSTOMER')") - public ResponseEntity requestModification( - /* @RequestBody ProjectRequestDto dto, */ + public ResponseEntity requestModification( + @Valid @RequestBody ProjectRequestDto dto, @RequestHeader("X-User-Subject") String customerId) { - // TODO: Delegate to projectService.requestNewProject(...); - return ResponseEntity.ok().build(); + + Project project = projectService.requestNewProject(dto, customerId); + ProjectResponseDto response = mapToResponseDto(project); + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(ApiResponse.success("Project request submitted successfully", response)); } @Operation(summary = "List projects for the current customer") @GetMapping @PreAuthorize("hasRole('CUSTOMER')") - public ResponseEntity listCustomerProjects(@RequestHeader("X-User-Subject") String customerId) { - // TODO: Delegate to projectService.getProjectsForCustomer(customerId); - return ResponseEntity.ok().build(); + public ResponseEntity listCustomerProjects(@RequestHeader("X-User-Subject") String customerId) { + List projects = projectService.getProjectsForCustomer(customerId); + List response = projects.stream() + .map(this::mapToResponseDto) + .collect(Collectors.toList()); + + return ResponseEntity.ok(ApiResponse.success("Projects retrieved successfully", response)); } @Operation(summary = "Get details for a specific project") @GetMapping("/{projectId}") - @PreAuthorize("hasAnyRole('CUSTOMER', 'EMPLOYEE')") - public ResponseEntity getProjectDetails(@PathVariable String projectId) { - // TODO: Delegate to projectService, ensuring access rights - return ResponseEntity.ok().build(); + @PreAuthorize("hasAnyRole('CUSTOMER', 'EMPLOYEE', 'ADMIN')") + public ResponseEntity getProjectDetails( + @PathVariable String projectId, + @RequestHeader("X-User-Subject") String userId, + @RequestHeader("X-User-Roles") String userRoles) { + + Project project = projectService.getProjectDetails(projectId, userId, userRoles) + .orElseThrow(() -> new RuntimeException("Project not found or access denied")); + + ProjectResponseDto response = mapToResponseDto(project); + return ResponseEntity.ok(ApiResponse.success("Project retrieved successfully", response)); } @Operation(summary = "Submit a quote for a project (employee/admin only)") @PutMapping("/{projectId}/quote") @PreAuthorize("hasAnyRole('EMPLOYEE', 'ADMIN')") - public ResponseEntity submitQuote(@PathVariable String projectId /*, @RequestBody QuoteDto dto */) { - // TODO: Delegate to projectService.submitQuoteForProject(...); - return ResponseEntity.ok().build(); + public ResponseEntity submitQuote( + @PathVariable String projectId, + @Valid @RequestBody QuoteDto dto) { + + Project project = projectService.submitQuoteForProject(projectId, dto); + ProjectResponseDto response = mapToResponseDto(project); + + return ResponseEntity.ok(ApiResponse.success("Quote submitted successfully", response)); } @Operation(summary = "Accept a quote for a project (customer only)") @PostMapping("/{projectId}/accept") @PreAuthorize("hasRole('CUSTOMER')") - public ResponseEntity acceptQuote( + public ResponseEntity acceptQuote( @PathVariable String projectId, @RequestHeader("X-User-Subject") String customerId) { - // TODO: Delegate to projectService.acceptQuote(projectId, customerId); - return ResponseEntity.ok().build(); + + Project project = projectService.acceptQuote(projectId, customerId); + ProjectResponseDto response = mapToResponseDto(project); + + return ResponseEntity.ok(ApiResponse.success("Quote accepted successfully", response)); } @Operation(summary = "Reject a quote for a project (customer only)") @PostMapping("/{projectId}/reject") @PreAuthorize("hasRole('CUSTOMER')") - public ResponseEntity rejectQuote( + public ResponseEntity rejectQuote( @PathVariable String projectId, - // @RequestBody RejectionDto dto, + @Valid @RequestBody RejectionDto dto, @RequestHeader("X-User-Subject") String customerId) { - // TODO: Delegate to projectService.rejectQuote(projectId, dto, customerId); - return ResponseEntity.ok().build(); + + Project project = projectService.rejectQuote(projectId, dto, customerId); + ProjectResponseDto response = mapToResponseDto(project); + + return ResponseEntity.ok(ApiResponse.success("Quote rejected successfully", response)); + } + + @Operation(summary = "Update project progress (employee/admin only)") + @PutMapping("/{projectId}/progress") + @PreAuthorize("hasAnyRole('EMPLOYEE', 'ADMIN')") + public ResponseEntity updateProgress( + @PathVariable String projectId, + @Valid @RequestBody ProgressUpdateDto dto) { + + Project project = projectService.updateProgress(projectId, dto); + ProjectResponseDto response = mapToResponseDto(project); + + return ResponseEntity.ok(ApiResponse.success("Progress updated successfully", response)); + } + + @Operation(summary = "List all projects (admin/employee only)") + @GetMapping("/all") + @PreAuthorize("hasAnyRole('EMPLOYEE', 'ADMIN')") + public ResponseEntity listAllProjects() { + List projects = projectService.getAllProjects(); + List response = projects.stream() + .map(this::mapToResponseDto) + .collect(Collectors.toList()); + + return ResponseEntity.ok(ApiResponse.success("All projects retrieved successfully", response)); + } + + // Helper method to map Entity to DTO + private ProjectResponseDto mapToResponseDto(Project project) { + return ProjectResponseDto.builder() + .id(project.getId()) + .customerId(project.getCustomerId()) + .vehicleId(project.getVehicleId()) + .description(project.getDescription()) + .budget(project.getBudget()) + .status(project.getStatus()) + .progress(project.getProgress()) + .createdAt(project.getCreatedAt()) + .updatedAt(project.getUpdatedAt()) + .build(); } } \ No newline at end of file diff --git a/project-service/src/main/java/com/techtorque/project_service/controller/ServiceController.java b/project-service/src/main/java/com/techtorque/project_service/controller/ServiceController.java index b782694..57aa97c 100644 --- a/project-service/src/main/java/com/techtorque/project_service/controller/ServiceController.java +++ b/project-service/src/main/java/com/techtorque/project_service/controller/ServiceController.java @@ -1,38 +1,48 @@ package com.techtorque.project_service.controller; +import com.techtorque.project_service.dto.ApiResponse; +import com.techtorque.project_service.entity.StandardService; +import com.techtorque.project_service.service.StandardServiceService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import java.util.List; + @RestController @RequestMapping("/services") @Tag(name = "Standard Services", description = "Endpoints for managing appointment-based services.") @SecurityRequirement(name = "bearerAuth") +@RequiredArgsConstructor public class ServiceController { - // @Autowired - // private StandardService serviceLayer; // Renamed to avoid confusion + private final StandardServiceService standardServiceService; @Operation(summary = "List services for the current customer") @GetMapping @PreAuthorize("hasRole('CUSTOMER')") - public ResponseEntity listCustomerServices( + public ResponseEntity listCustomerServices( @RequestHeader("X-User-Subject") String customerId, @RequestParam(required = false) String status) { - // TODO: Delegate to serviceLayer.getServicesForCustomer(customerId, status); - return ResponseEntity.ok().build(); + List services = standardServiceService.getServicesForCustomer(customerId, status); + return ResponseEntity.ok(ApiResponse.success("Services retrieved successfully", services)); } @Operation(summary = "Get details for a specific service") @GetMapping("/{serviceId}") @PreAuthorize("hasAnyRole('CUSTOMER', 'EMPLOYEE')") - public ResponseEntity getServiceDetails(@PathVariable String serviceId) { - // TODO: Delegate to serviceLayer, ensuring customer/employee has access - return ResponseEntity.ok().build(); + public ResponseEntity getServiceDetails( + @PathVariable String serviceId, + @RequestHeader("X-User-Subject") String userId, + @RequestHeader("X-User-Roles") String userRole) { + return standardServiceService.getServiceDetails(serviceId, userId, userRole) + .map(service -> ResponseEntity.ok(ApiResponse.success("Service retrieved successfully", service))) + .orElse(ResponseEntity.notFound().build()); } @Operation(summary = "Update service status, notes, or completion estimate (employee only)") diff --git a/project-service/src/main/java/com/techtorque/project_service/dto/ApiResponse.java b/project-service/src/main/java/com/techtorque/project_service/dto/ApiResponse.java new file mode 100644 index 0000000..69437e7 --- /dev/null +++ b/project-service/src/main/java/com/techtorque/project_service/dto/ApiResponse.java @@ -0,0 +1,38 @@ +package com.techtorque.project_service.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApiResponse { + private boolean success; + private String message; + private Object data; + + public static ApiResponse success(String message) { + return ApiResponse.builder() + .success(true) + .message(message) + .build(); + } + + public static ApiResponse success(String message, Object data) { + return ApiResponse.builder() + .success(true) + .message(message) + .data(data) + .build(); + } + + public static ApiResponse error(String message) { + return ApiResponse.builder() + .success(false) + .message(message) + .build(); + } +} diff --git a/project-service/src/main/java/com/techtorque/project_service/dto/ProgressUpdateDto.java b/project-service/src/main/java/com/techtorque/project_service/dto/ProgressUpdateDto.java new file mode 100644 index 0000000..17820d4 --- /dev/null +++ b/project-service/src/main/java/com/techtorque/project_service/dto/ProgressUpdateDto.java @@ -0,0 +1,23 @@ +package com.techtorque.project_service.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProgressUpdateDto { + + @NotNull(message = "Progress percentage is required") + @Min(value = 0, message = "Progress must be between 0 and 100") + @Max(value = 100, message = "Progress must be between 0 and 100") + private Integer progress; + + private String notes; +} diff --git a/project-service/src/main/java/com/techtorque/project_service/dto/ProjectRequestDto.java b/project-service/src/main/java/com/techtorque/project_service/dto/ProjectRequestDto.java new file mode 100644 index 0000000..93e1eb4 --- /dev/null +++ b/project-service/src/main/java/com/techtorque/project_service/dto/ProjectRequestDto.java @@ -0,0 +1,27 @@ +package com.techtorque.project_service.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProjectRequestDto { + + @NotBlank(message = "Vehicle ID is required") + private String vehicleId; + + @NotBlank(message = "Description is required") + @Size(min = 10, max = 2000, message = "Description must be between 10 and 2000 characters") + private String description; + + private BigDecimal budget; +} diff --git a/project-service/src/main/java/com/techtorque/project_service/dto/ProjectResponseDto.java b/project-service/src/main/java/com/techtorque/project_service/dto/ProjectResponseDto.java new file mode 100644 index 0000000..eaa756b --- /dev/null +++ b/project-service/src/main/java/com/techtorque/project_service/dto/ProjectResponseDto.java @@ -0,0 +1,26 @@ +package com.techtorque.project_service.dto; + +import com.techtorque.project_service.entity.ProjectStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProjectResponseDto { + private String id; + private String customerId; + private String vehicleId; + private String description; + private BigDecimal budget; + private ProjectStatus status; + private int progress; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/project-service/src/main/java/com/techtorque/project_service/dto/QuoteDto.java b/project-service/src/main/java/com/techtorque/project_service/dto/QuoteDto.java new file mode 100644 index 0000000..0d25fda --- /dev/null +++ b/project-service/src/main/java/com/techtorque/project_service/dto/QuoteDto.java @@ -0,0 +1,23 @@ +package com.techtorque.project_service.dto; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QuoteDto { + + @NotNull(message = "Quote amount is required") + @Min(value = 0, message = "Quote amount must be positive") + private BigDecimal quoteAmount; + + private String notes; +} diff --git a/project-service/src/main/java/com/techtorque/project_service/dto/RejectionDto.java b/project-service/src/main/java/com/techtorque/project_service/dto/RejectionDto.java new file mode 100644 index 0000000..797cf84 --- /dev/null +++ b/project-service/src/main/java/com/techtorque/project_service/dto/RejectionDto.java @@ -0,0 +1,17 @@ +package com.techtorque.project_service.dto; + +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RejectionDto { + + @Size(max = 500, message = "Reason cannot exceed 500 characters") + private String reason; +} diff --git a/project-service/src/main/java/com/techtorque/project_service/exception/GlobalExceptionHandler.java b/project-service/src/main/java/com/techtorque/project_service/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..4422926 --- /dev/null +++ b/project-service/src/main/java/com/techtorque/project_service/exception/GlobalExceptionHandler.java @@ -0,0 +1,55 @@ +package com.techtorque.project_service.exception; + +import com.techtorque.project_service.dto.ApiResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.HashMap; +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(ProjectNotFoundException.class) + public ResponseEntity handleProjectNotFound(ProjectNotFoundException ex) { + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(ApiResponse.error(ex.getMessage())); + } + + @ExceptionHandler(InvalidProjectOperationException.class) + public ResponseEntity handleInvalidOperation(InvalidProjectOperationException ex) { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(ApiResponse.error(ex.getMessage())); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) { + Map errors = new HashMap<>(); + ex.getBindingResult().getAllErrors().forEach((error) -> { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + errors.put(fieldName, errorMessage); + }); + + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(ApiResponse.builder() + .success(false) + .message("Validation failed") + .data(errors) + .build()); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGlobalException(Exception ex) { + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error("An unexpected error occurred: " + ex.getMessage())); + } +} diff --git a/project-service/src/main/java/com/techtorque/project_service/exception/InvalidProjectOperationException.java b/project-service/src/main/java/com/techtorque/project_service/exception/InvalidProjectOperationException.java new file mode 100644 index 0000000..e112bf1 --- /dev/null +++ b/project-service/src/main/java/com/techtorque/project_service/exception/InvalidProjectOperationException.java @@ -0,0 +1,11 @@ +package com.techtorque.project_service.exception; + +public class InvalidProjectOperationException extends RuntimeException { + public InvalidProjectOperationException(String message) { + super(message); + } + + public InvalidProjectOperationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/project-service/src/main/java/com/techtorque/project_service/exception/ProjectNotFoundException.java b/project-service/src/main/java/com/techtorque/project_service/exception/ProjectNotFoundException.java new file mode 100644 index 0000000..0b3cb00 --- /dev/null +++ b/project-service/src/main/java/com/techtorque/project_service/exception/ProjectNotFoundException.java @@ -0,0 +1,11 @@ +package com.techtorque.project_service.exception; + +public class ProjectNotFoundException extends RuntimeException { + public ProjectNotFoundException(String message) { + super(message); + } + + public ProjectNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/project-service/src/main/java/com/techtorque/project_service/service/ProjectService.java b/project-service/src/main/java/com/techtorque/project_service/service/ProjectService.java index 249ce03..506aa36 100644 --- a/project-service/src/main/java/com/techtorque/project_service/service/ProjectService.java +++ b/project-service/src/main/java/com/techtorque/project_service/service/ProjectService.java @@ -1,20 +1,28 @@ package com.techtorque.project_service.service; +import com.techtorque.project_service.dto.ProjectRequestDto; +import com.techtorque.project_service.dto.QuoteDto; +import com.techtorque.project_service.dto.RejectionDto; +import com.techtorque.project_service.dto.ProgressUpdateDto; import com.techtorque.project_service.entity.Project; import java.util.List; import java.util.Optional; public interface ProjectService { - Project requestNewProject(/* ProjectRequestDto dto, */ String customerId); + Project requestNewProject(ProjectRequestDto dto, String customerId); List getProjectsForCustomer(String customerId); Optional getProjectDetails(String projectId, String userId, String userRole); - Project submitQuoteForProject(String projectId /*, QuoteDto dto */); + Project submitQuoteForProject(String projectId, QuoteDto dto); Project acceptQuote(String projectId, String customerId); - Project rejectQuote(String projectId, /* RejectionDto dto, */ String customerId); + Project rejectQuote(String projectId, RejectionDto dto, String customerId); + + Project updateProgress(String projectId, ProgressUpdateDto dto); + + List getAllProjects(); } \ No newline at end of file diff --git a/project-service/src/main/java/com/techtorque/project_service/service/impl/ProjectServiceImpl.java b/project-service/src/main/java/com/techtorque/project_service/service/impl/ProjectServiceImpl.java index b880d67..233998a 100644 --- a/project-service/src/main/java/com/techtorque/project_service/service/impl/ProjectServiceImpl.java +++ b/project-service/src/main/java/com/techtorque/project_service/service/impl/ProjectServiceImpl.java @@ -1,15 +1,25 @@ package com.techtorque.project_service.service.impl; +import com.techtorque.project_service.dto.ProgressUpdateDto; +import com.techtorque.project_service.dto.ProjectRequestDto; +import com.techtorque.project_service.dto.QuoteDto; +import com.techtorque.project_service.dto.RejectionDto; import com.techtorque.project_service.entity.Project; +import com.techtorque.project_service.entity.ProjectStatus; +import com.techtorque.project_service.exception.InvalidProjectOperationException; +import com.techtorque.project_service.exception.ProjectNotFoundException; import com.techtorque.project_service.repository.ProjectRepository; import com.techtorque.project_service.service.ProjectService; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; + import java.util.List; import java.util.Optional; @Service @Transactional +@Slf4j public class ProjectServiceImpl implements ProjectService { private final ProjectRepository projectRepository; @@ -19,39 +29,184 @@ public ProjectServiceImpl(ProjectRepository projectRepository) { } @Override - public Project requestNewProject(/* ProjectRequestDto dto, */ String customerId) { - // TODO: Logic for creating a new project request. - return null; + public Project requestNewProject(ProjectRequestDto dto, String customerId) { + log.info("Creating new project request for customer: {}", customerId); + + // Create new project + Project newProject = Project.builder() + .customerId(customerId) + .vehicleId(dto.getVehicleId()) + .description(dto.getDescription()) + .budget(dto.getBudget()) + .status(ProjectStatus.REQUESTED) + .progress(0) + .build(); + + Project savedProject = projectRepository.save(newProject); + log.info("Successfully created project with ID: {} for customer: {}", + savedProject.getId(), customerId); + + return savedProject; } @Override public List getProjectsForCustomer(String customerId) { - // TODO: Call projectRepository.findByCustomerId(customerId). - return List.of(); + log.info("Fetching all projects for customer: {}", customerId); + return projectRepository.findByCustomerId(customerId); } @Override public Optional getProjectDetails(String projectId, String userId, String userRole) { - // TODO: Find project by ID and verify user has permission to view. + log.info("Fetching project {} for user: {} with role: {}", projectId, userId, userRole); + + Optional projectOpt = projectRepository.findById(projectId); + + if (projectOpt.isEmpty()) { + return Optional.empty(); + } + + Project project = projectOpt.get(); + + // Role-based access control + if (userRole.contains("ADMIN") || userRole.contains("EMPLOYEE")) { + // Admins and employees can see all projects + return projectOpt; + } else if (userRole.contains("CUSTOMER")) { + // Customers can only see their own projects + if (project.getCustomerId().equals(userId)) { + return projectOpt; + } + } + + log.warn("User {} with role {} attempted to access project {} without permission", + userId, userRole, projectId); return Optional.empty(); } @Override - public Project submitQuoteForProject(String projectId /*, QuoteDto dto */) { - // TODO: Find project, associate the quote details, update status to QUOTED, and save. - return null; + public Project submitQuoteForProject(String projectId, QuoteDto dto) { + log.info("Submitting quote for project: {}", projectId); + + Project project = projectRepository.findById(projectId) + .orElseThrow(() -> { + log.warn("Project {} not found", projectId); + return new ProjectNotFoundException("Project not found"); + }); + + // Can only quote projects in REQUESTED status + if (project.getStatus() != ProjectStatus.REQUESTED) { + throw new InvalidProjectOperationException( + "Can only quote projects in REQUESTED status. Current status: " + project.getStatus()); + } + + // Update with quote + project.setBudget(dto.getQuoteAmount()); + project.setStatus(ProjectStatus.QUOTED); + + Project updatedProject = projectRepository.save(project); + log.info("Successfully submitted quote for project: {}", projectId); + + return updatedProject; } @Override public Project acceptQuote(String projectId, String customerId) { - // TODO: Find project, verify customer ownership, update status to APPROVED. - // Potentially make an inter-service call to the Billing Service to generate a deposit invoice. - return null; + log.info("Customer {} accepting quote for project: {}", customerId, projectId); + + Project project = projectRepository.findById(projectId) + .orElseThrow(() -> { + log.warn("Project {} not found", projectId); + return new ProjectNotFoundException("Project not found"); + }); + + // Verify ownership + if (!project.getCustomerId().equals(customerId)) { + throw new InvalidProjectOperationException("You don't have permission to accept this quote"); + } + + // Can only accept projects in QUOTED status + if (project.getStatus() != ProjectStatus.QUOTED) { + throw new InvalidProjectOperationException( + "Can only accept projects in QUOTED status. Current status: " + project.getStatus()); + } + + // Update status to approved + project.setStatus(ProjectStatus.APPROVED); + + Project updatedProject = projectRepository.save(project); + log.info("Successfully accepted quote for project: {}", projectId); + + // TODO: Inter-service call to Payment Service to generate deposit invoice + + return updatedProject; + } + + @Override + public Project rejectQuote(String projectId, RejectionDto dto, String customerId) { + log.info("Customer {} rejecting quote for project: {}", customerId, projectId); + + Project project = projectRepository.findById(projectId) + .orElseThrow(() -> { + log.warn("Project {} not found", projectId); + return new ProjectNotFoundException("Project not found"); + }); + + // Verify ownership + if (!project.getCustomerId().equals(customerId)) { + throw new InvalidProjectOperationException("You don't have permission to reject this quote"); + } + + // Can only reject projects in QUOTED status + if (project.getStatus() != ProjectStatus.QUOTED) { + throw new InvalidProjectOperationException( + "Can only reject projects in QUOTED status. Current status: " + project.getStatus()); + } + + // Update status to rejected + project.setStatus(ProjectStatus.REJECTED); + + Project updatedProject = projectRepository.save(project); + log.info("Successfully rejected quote for project: {}", projectId); + + return updatedProject; + } + + @Override + public Project updateProgress(String projectId, ProgressUpdateDto dto) { + log.info("Updating progress for project: {} to {}%", projectId, dto.getProgress()); + + Project project = projectRepository.findById(projectId) + .orElseThrow(() -> { + log.warn("Project {} not found", projectId); + return new ProjectNotFoundException("Project not found"); + }); + + // Can only update progress for approved or in-progress projects + if (project.getStatus() != ProjectStatus.APPROVED && + project.getStatus() != ProjectStatus.IN_PROGRESS) { + throw new InvalidProjectOperationException( + "Can only update progress for APPROVED or IN_PROGRESS projects"); + } + + // Update progress + project.setProgress(dto.getProgress()); + + // Auto-update status based on progress + if (dto.getProgress() > 0 && project.getStatus() == ProjectStatus.APPROVED) { + project.setStatus(ProjectStatus.IN_PROGRESS); + } else if (dto.getProgress() == 100) { + project.setStatus(ProjectStatus.COMPLETED); + } + + Project updatedProject = projectRepository.save(project); + log.info("Successfully updated progress for project: {}", projectId); + + return updatedProject; } @Override - public Project rejectQuote(String projectId, /* RejectionDto dto, */ String customerId) { - // TODO: Find project, verify customer ownership, update status to REJECTED, and save the reason. - return null; + public List getAllProjects() { + log.info("Fetching all projects"); + return projectRepository.findAll(); } } \ No newline at end of file