Skip to content
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
17 changes: 13 additions & 4 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,30 @@ repositories {
}

dependencies {
implementation("org.springframework.boot:spring-boot-starter-flyway")
// MongoDB
implementation("org.springframework.boot:spring-boot-starter-data-mongodb")

// Lombok
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")

// MapStruct
implementation("org.mapstruct:mapstruct:1.6.3")
annotationProcessor("org.mapstruct:mapstruct-processor:1.6.3")
annotationProcessor("org.projectlombok:lombok-mapstruct-binding:0.2.0")

// implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.micrometer:micrometer-registry-prometheus")
implementation("org.springframework.boot:spring-boot-starter-webmvc")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("net.logstash.logback:logstash-logback-encoder:8.0")
implementation("org.flywaydb:flyway-database-postgresql")
//zipkin(tracing)
implementation("org.springframework.boot:spring-boot-micrometer-tracing-brave")
implementation("org.springframework.boot:spring-boot-starter-zipkin")
implementation("io.micrometer:micrometer-tracing-bridge-brave")
implementation("io.zipkin.reporter2:zipkin-reporter-brave")

runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-flyway-test")
// testImplementation("org.springframework.boot:spring-boot-starter-security-test")
testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
Expand Down
6 changes: 6 additions & 0 deletions environment/.local.env
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@ SERVER_PORT=8080
LOGSTASH_HOST=logstash:5000
ZIPKIN_HOST=zipkin
ZIPKIN_PORT=9411

# MongoDB
MONGODB_HOST=devoops-mongodb
MONGODB_PORT=27017
MONGODB_USERNAME=devoops
MONGODB_PASSWORD=devoops
3 changes: 1 addition & 2 deletions src/main/java/com/devoops/rating/RatingApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration;

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@SpringBootApplication
public class RatingApplication {

public static void main(String[] args) {
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/devoops/rating/config/MongoConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.devoops.rating.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.EnableMongoAuditing;

@Configuration
@EnableMongoAuditing
public class MongoConfig {

}
71 changes: 71 additions & 0 deletions src/main/java/com/devoops/rating/controller/RatingController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.devoops.rating.controller;

import com.devoops.rating.dto.request.CreateRatingRequest;
import com.devoops.rating.dto.response.RatingResponse;
import com.devoops.rating.dto.request.UpdateRatingRequest;
import com.devoops.rating.service.RatingService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.UUID;

@RestController
@RequestMapping("/api/v1/ratings")
public class RatingController {

private final RatingService ratingService;

public RatingController(RatingService ratingService) {
this.ratingService = ratingService;
}

@PostMapping
public ResponseEntity<RatingResponse> createRating(@Valid @RequestBody CreateRatingRequest request) {
RatingResponse response = ratingService.createRating(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

@GetMapping("/{id}")
public ResponseEntity<RatingResponse> getRatingById(@PathVariable UUID id) {
RatingResponse response = ratingService.getRatingById(id);
return ResponseEntity.ok(response);
}

@GetMapping
public ResponseEntity<List<RatingResponse>> getAllRatings() {
List<RatingResponse> responses = ratingService.getAllRatings();
return ResponseEntity.ok(responses);
}

@GetMapping("/target/{targetId}")
public ResponseEntity<List<RatingResponse>> getRatingsByTargetId(@PathVariable UUID targetId) {
List<RatingResponse> responses = ratingService.getRatingsByTargetId(targetId);
return ResponseEntity.ok(responses);
}



@GetMapping("/guest/{guestId}")
public ResponseEntity<List<RatingResponse>> getRatingsByGuestId(@PathVariable UUID guestId) {
List<RatingResponse> responses = ratingService.getRatingsByGuestId(guestId);
return ResponseEntity.ok(responses);
}

@PutMapping("/{id}")
public ResponseEntity<RatingResponse> updateRating(
@PathVariable UUID id,
@Valid @RequestBody UpdateRatingRequest request) {
RatingResponse response = ratingService.updateRating(id, request);
return ResponseEntity.ok(response);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteRating(@PathVariable UUID id) {
ratingService.deleteRating(id);
return ResponseEntity.noContent().build();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.devoops.rating.dto.request;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

import java.util.UUID;

public record CreateRatingRequest(
@NotNull(message = "Target ID is required")
UUID targetId,

@NotBlank(message = "Guest first name is required")
String guestFirstName,

@NotBlank(message = "Guest last name is required")
String guestLastName,

@NotNull(message = "Guest ID is required")
UUID guestId,

@NotNull(message = "Score is required")
@Min(value = 1, message = "Score must be at least 1")
@Max(value = 5, message = "Score must be at most 5")
Integer score
) {
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.devoops.rating.dto.request;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;

public record UpdateRatingRequest(
@NotNull(message = "Score is required")
@Min(value = 1, message = "Score must be at least 1")
@Max(value = 5, message = "Score must be at most 5")
Integer score
) {
}

16 changes: 16 additions & 0 deletions src/main/java/com/devoops/rating/dto/response/RatingResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.devoops.rating.dto.response;

import java.time.LocalDateTime;
import java.util.UUID;

public record RatingResponse(
UUID id,
UUID targetId,
String guestFirstName,
String guestLastName,
UUID guestId,
Integer score,
LocalDateTime createdAt,
LocalDateTime updatedAt
) { }

Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.devoops.rating.exception;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.time.Instant;
import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(RatingNotFoundException.class)
public ResponseEntity<ErrorResponse> handleRatingNotFound(RatingNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
Instant.now()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});

ValidationErrorResponse response = new ValidationErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Validation failed",
errors,
Instant.now()
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
log.error("Unexpected error occurred: ", ex);
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
ex.getMessage(),
Instant.now()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}

public record ErrorResponse(int status, String message, Instant timestamp) {}

public record ValidationErrorResponse(int status, String message, Map<String, String> errors, Instant timestamp) {}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.devoops.rating.exception;

import java.util.UUID;

public class RatingNotFoundException extends RuntimeException {

public RatingNotFoundException(UUID id) {
super("Rating not found with id: " + id);
}
}

25 changes: 25 additions & 0 deletions src/main/java/com/devoops/rating/mapper/RatingMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.devoops.rating.mapper;

import com.devoops.rating.dto.request.CreateRatingRequest;
import com.devoops.rating.dto.response.RatingResponse;
import com.devoops.rating.model.Rating;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;


import java.util.List;

@Mapper(componentModel = "spring")
public interface RatingMapper {

@Mapping(target = "id", ignore = true)
@Mapping(target = "createdAt", ignore = true)
@Mapping(target = "updatedAt", ignore = true)
@Mapping(target = "isDeleted", ignore = true)
Rating toEntity(CreateRatingRequest request);

RatingResponse toResponse(Rating rating);

List<RatingResponse> toResponseList(List<Rating> ratings);
}

44 changes: 44 additions & 0 deletions src/main/java/com/devoops/rating/model/BaseDocument.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.devoops.rating.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.annotation.Transient;
import org.springframework.data.domain.Persistable;
import org.springframework.data.mongodb.core.mapping.MongoId;
import org.springframework.data.mongodb.core.mapping.FieldType;

import java.time.LocalDateTime;
import java.util.UUID;

@Data
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
public abstract class BaseDocument implements Persistable<UUID> {

@Id
@Builder.Default
private UUID id = UUID.randomUUID();

@CreatedDate
private LocalDateTime createdAt;

@LastModifiedDate
private LocalDateTime updatedAt;

@Builder.Default
private boolean isDeleted = false;

@Override
@Transient
public boolean isNew() {
return createdAt == null;
}
}

30 changes: 30 additions & 0 deletions src/main/java/com/devoops/rating/model/Rating.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.devoops.rating.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.springframework.data.mongodb.core.mapping.Document;

import java.util.UUID;

@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Document(collection = "ratings")
public class Rating extends BaseDocument {

private UUID targetId;

private String guestFirstName;

private String guestLastName;

private UUID guestId;

private Integer score;
}

Loading