Skip to content
This repository was archived by the owner on Nov 23, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .github/workflows/buildtest.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
name: Build and Test Project Service

on:
push:
branches:
- '**'
pull_request:
branches:
- '**'
- main
- dev
- devOps

jobs:
build-test:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.techtorque.project_service.client;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.client.HttpClientErrorException;

/**
* Client for inter-service communication with Appointment Service
*/
@Component
@Slf4j
public class AppointmentClient {

private final RestTemplate restTemplate;
private final String appointmentServiceUrl;

public AppointmentClient(
RestTemplate restTemplate,
@Value("${services.appointment.url:http://localhost:8083}") String appointmentServiceUrl) {
this.restTemplate = restTemplate;
this.appointmentServiceUrl = appointmentServiceUrl;
}

/**
* Cancel an appointment (used when project is rejected)
*/
public void cancelAppointment(String appointmentId, String adminId) {
try {
String url = appointmentServiceUrl + "/api/appointments/" + appointmentId;

HttpHeaders headers = new HttpHeaders();
headers.set("X-User-Subject", adminId);
headers.set("X-User-Roles", "ADMIN");

HttpEntity<Void> request = new HttpEntity<>(headers);

restTemplate.exchange(url, HttpMethod.DELETE, request, Void.class);

log.info("Successfully cancelled appointment {} via Appointment Service", appointmentId);
} catch (HttpClientErrorException e) {
log.error("Failed to cancel appointment {}: {}", appointmentId, e.getMessage());
// Don't throw - project rejection should still succeed even if appointment cancellation fails
} catch (Exception e) {
log.error("Error communicating with Appointment Service: {}", e.getMessage());
}
}

/**
* Update appointment status to CONFIRMED (used when project is approved)
*/
public void confirmAppointment(String appointmentId, String adminId) {
try {
String url = appointmentServiceUrl + "/api/appointments/" + appointmentId + "/status";

HttpHeaders headers = new HttpHeaders();
headers.set("X-User-Subject", adminId);
headers.set("X-User-Roles", "ADMIN");
headers.setContentType(MediaType.APPLICATION_JSON);

String body = "{\"newStatus\":\"CONFIRMED\"}";
HttpEntity<String> request = new HttpEntity<>(body, headers);

restTemplate.exchange(url, HttpMethod.PATCH, request, String.class);

log.info("Successfully confirmed appointment {} via Appointment Service", appointmentId);
} catch (HttpClientErrorException e) {
log.error("Failed to confirm appointment {}: {}", appointmentId, e.getMessage());
// Don't throw - project approval should still succeed even if appointment confirmation fails
} catch (Exception e) {
log.error("Error communicating with Appointment Service: {}", e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.techtorque.project_service.client;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.client.HttpClientErrorException;

/**
* Client for inter-service communication with Notification Service
*/
@Component
@Slf4j
public class NotificationClient {

private final RestTemplate restTemplate;
private final String notificationServiceUrl;

public NotificationClient(
RestTemplate restTemplate,
@Value("${services.notification.url:http://localhost:8088}") String notificationServiceUrl) {
this.restTemplate = restTemplate;
this.notificationServiceUrl = notificationServiceUrl;
}

/**
* Send project notification to user
*/
public void sendProjectNotification(String userId, String type, String title, String message, String projectId) {
try {
String url = notificationServiceUrl + "/api/notifications/project";

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);

String body = String.format(
"{\"userId\":\"%s\",\"type\":\"%s\",\"title\":\"%s\",\"message\":\"%s\",\"referenceId\":\"%s\",\"referenceType\":\"PROJECT\"}",
userId, type, title, message, projectId);

HttpEntity<String> request = new HttpEntity<>(body, headers);

restTemplate.postForEntity(url, request, String.class);

log.info("Successfully sent project notification to user {}", userId);
} catch (HttpClientErrorException e) {
log.error("Failed to send notification to user {}: {}", userId, e.getMessage());
// Don't throw - project operations should still succeed even if notification fails
} catch (Exception e) {
log.error("Error communicating with Notification Service: {}", e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ private void seedProjects() {
Project project1 = Project.builder()
.customerId(CUSTOMER_1_ID)
.vehicleId("VEH-001")
.projectType("Performance")
.description("Install custom exhaust system and performance tuning")
.budget(new BigDecimal("5000.00"))
.status(ProjectStatus.APPROVED)
Expand All @@ -121,6 +122,7 @@ private void seedProjects() {
Project project2 = Project.builder()
.customerId(CUSTOMER_2_ID)
.vehicleId("VEH-002")
.projectType("Interior")
.description("Full interior leather upholstery replacement")
.budget(new BigDecimal("3000.00"))
.status(ProjectStatus.QUOTED)
Expand All @@ -131,6 +133,7 @@ private void seedProjects() {
Project project3 = Project.builder()
.customerId(CUSTOMER_1_ID)
.vehicleId("VEH-001")
.projectType("Body Work")
.description("Custom body kit installation and paint job")
.budget(new BigDecimal("8000.00"))
.status(ProjectStatus.IN_PROGRESS)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.techtorque.project_service.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,42 @@ public ResponseEntity<ApiResponse> listAllProjects() {
return ResponseEntity.ok(ApiResponse.success("All projects retrieved successfully", response));
}

@Operation(summary = "Approve a custom project request (admin only)")
@PostMapping("/{projectId}/approve")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ApiResponse> approveProject(
@PathVariable String projectId,
@RequestHeader("X-User-Subject") String adminId) {

Project project = projectService.approveProject(projectId, adminId);
ProjectResponseDto response = mapToResponseDto(project);

return ResponseEntity.ok(ApiResponse.success("Project approved successfully", response));
}

@Operation(summary = "Reject a custom project request (admin only)")
@PostMapping("/{projectId}/admin/reject")
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL pattern is inconsistent with the approve endpoint. The approve endpoint uses '/{projectId}/approve' (line 149), but this reject endpoint uses '/{projectId}/admin/reject'. For consistency and better API design, consider using '/{projectId}/reject' since the @PreAuthorize annotation already restricts access to admins. Note there's already a customer reject endpoint at '/{projectId}/reject' for quote rejection (line 110), so you may need to use a different pattern like '/{projectId}/admin/approve' and '/{projectId}/admin/reject' for both admin operations.

Copilot uses AI. Check for mistakes.
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ApiResponse> rejectProject(
@PathVariable String projectId,
@RequestParam(required = false) String reason,
@RequestHeader("X-User-Subject") String adminId) {

Project project = projectService.rejectProject(projectId, reason, adminId);
ProjectResponseDto response = mapToResponseDto(project);

return ResponseEntity.ok(ApiResponse.success("Project rejected 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())
.projectType(project.getProjectType())
.description(project.getDescription())
.desiredCompletionDate(project.getDesiredCompletionDate())
.budget(project.getBudget())
.status(project.getStatus())
.progress(project.getProgress())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.time.LocalDate;
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LocalDate is imported but the desiredCompletionDate field on line 30 is declared as String. This creates a type mismatch. Either change the field type to LocalDate or remove the unused import. Using LocalDate would provide better type safety and validation.

Copilot uses AI. Check for mistakes.

@Data
@Builder
Expand All @@ -19,9 +20,14 @@ public class ProjectRequestDto {
@NotBlank(message = "Vehicle ID is required")
private String vehicleId;

@NotBlank(message = "Project type is required")
private String projectType;

@NotBlank(message = "Description is required")
@Size(min = 10, max = 2000, message = "Description must be between 10 and 2000 characters")
private String description;

private String desiredCompletionDate;

private BigDecimal budget;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ public class ProjectResponseDto {
private String id;
private String customerId;
private String vehicleId;
private String projectType;
private String description;
private String desiredCompletionDate;
private BigDecimal budget;
private ProjectStatus status;
private int progress;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.math.BigDecimal;
import java.time.LocalDate;
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LocalDate is imported but the desiredCompletionDate field on line 37 is declared as String. This creates a type mismatch. Either change the field type to LocalDate or remove the unused import. Using LocalDate would be more appropriate for date handling.

Copilot uses AI. Check for mistakes.
import java.time.LocalDateTime;

@Entity
Expand All @@ -24,10 +25,17 @@ public class Project {
@Column(nullable = false)
private String vehicleId;

private String appointmentId; // Link to appointment if project was created from appointment booking

@Column(nullable = false)
private String projectType;

@Lob
@Column(nullable = false)
private String description;

private String desiredCompletionDate;

private BigDecimal budget; // Use BigDecimal for currency

@Enumerated(EnumType.STRING)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

public enum ProjectStatus {
REQUESTED,
PENDING_ADMIN_REVIEW, // Custom project awaiting admin approval
QUOTED,
APPROVED,
IN_PROGRESS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ public interface ProjectService {
Project updateProgress(String projectId, ProgressUpdateDto dto);

List<Project> getAllProjects();

Project approveProject(String projectId, String adminId);

Project rejectProject(String projectId, String reason, String adminId);
}
Loading